diff --git a/.env.example b/.env.example index e10db22..c8d3274 100644 --- a/.env.example +++ b/.env.example @@ -39,14 +39,12 @@ COINPAY_REDIRECT_URI=tronbrowser://oauth/coinpay # --- Extension store (tronbrowser.dev/store) ------------------------------- APP_URL=https://tronbrowser.dev -# The $1 listing fee — Stripe (card): -STRIPE_SECRET_KEY= -STRIPE_WEBHOOK_SECRET= # for /api/store/payments/stripe/webhook -# The $1 listing fee — CoinPay / x402 (1 USDC): -STORE_X402_NETWORK=base -STORE_X402_PAY_TO= # your USDC receiving address -# COINPAY_SETTLEMENT_URL=https://coinpayportal.com/api/settlements -# STORE_X402_TRUST_CLIENT=1 # DEV ONLY: accept client-reported settlement +# The $1 listing fee: a CoinPay hosted checkout session (any coin or PayPal). +# `npx @profullstack/coinpay-onboard site tronbrowser.dev --webhook ` writes these. +COINPAY_API_URL=https://coinpayportal.com/api +COINPAY_BUSINESS_ID= +COINPAY_API_KEY= # business cp_live_ key +COINPAY_WEBHOOK_SECRET= # for /api/store/payments/coinpay/webhook # vu1nz.com security scan (non-gating; recorded 'skipped' if unset): VU1NZ_API_URL= VU1NZ_API_KEY= diff --git a/apps/extensions/README.md b/apps/extensions/README.md index 7415099..ed880b2 100644 --- a/apps/extensions/README.md +++ b/apps/extensions/README.md @@ -41,8 +41,8 @@ one-click off-store install. 1. `POST /api/store/extensions` — create a draft (auth required). 2. `POST /api/store/extensions/:id/versions` — submit the MV3 manifest + bundle URL(s). Rejected unless it's valid Manifest V3. -3. `POST /api/store/extensions/:id/checkout` — pay the $1 fee via **Stripe** - (card) or **CoinPay / x402** (1 USDC). +3. `POST /api/store/extensions/:id/checkout`: pay the $1 fee on a + **CoinPay** hosted checkout (any supported coin, or PayPal). 4. On payment the listing flips to **live**, is mirrored to the git registry, and a **vu1nz.com** security scan runs asynchronously (badge on the listing). diff --git a/apps/extensions/public/extension.html b/apps/extensions/public/extension.html index 13da12d..084e534 100644 --- a/apps/extensions/public/extension.html +++ b/apps/extensions/public/extension.html @@ -22,7 +22,8 @@

← All extensions

- + +

Loading…

diff --git a/apps/extensions/public/store.js b/apps/extensions/public/store.js index 89d0a9e..34619a3 100644 --- a/apps/extensions/public/store.js +++ b/apps/extensions/public/store.js @@ -301,6 +301,7 @@ async function initDetail() { const root = document.getElementById('detail'); if (!slug) { root.innerHTML = '

No extension specified.

'; return; } if (qs('paid')) document.getElementById('paidNote')?.classList.remove('hidden'); + if (qs('canceled')) document.getElementById('canceledNote')?.classList.remove('hidden'); try { await renderDetail(slug, root); } catch (e) { @@ -334,6 +335,7 @@ async function renderDetail(slug, root) { ⬇ Install / Download How to install + ${ext.isOwner && ext.status !== 'live' ? '' : ''} ${ext.isOwner ? '' : ''} ${ext.isOwner ? '' : ''} ${ext.isOwner && !ext.crxId ? '' : ''} @@ -391,6 +393,14 @@ async function renderDetail(slug, root) { // Scans are recorded at publish time, so a listing published before its // bundle could be scanned needs one re-run to earn a badge. + document.getElementById('payBtn')?.addEventListener('click', async (e) => { + const btn = e.currentTarget; + btn.disabled = true; + const pay = await startListingCheckout(ext.id); + if (pay.checkoutUrl) { location.href = pay.checkoutUrl; return; } + btn.disabled = false; + await uiConfirm('Payment', pay.alreadyLive ? 'This listing is already paid.' : `Payment could not be started: ${pay.error || 'unknown'}`, { confirmLabel: 'OK' }); + }); document.getElementById('rescanBtn')?.addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; @@ -479,7 +489,6 @@ async function initSubmit() { let manifest; try { manifest = JSON.parse(fd.get('manifest')); } catch { out.innerHTML = '

manifest.json is not valid JSON.

'; return; } - const method = fd.get('method'); const files = {}; if (fd.get('crxFile')) files.crx = fd.get('crxFile').trim(); if (fd.get('zipFile')) files.zip = fd.get('zipFile').trim(); @@ -497,10 +506,10 @@ async function initSubmit() { out.innerHTML = `

Draft created: ${esc(draft.slug)}. Upload your bundle, then finish:

scp ${esc(files.crx || files.zip)} ${esc(SCP_TARGET)}:/public/extensions/${esc(draft.slug)}/
`; - document.getElementById('finishBtn').addEventListener('click', () => finishPublish(draft, manifest, method, files, urls, out)); + document.getElementById('finishBtn').addEventListener('click', () => finishPublish(draft, manifest, files, urls, out)); return; } - await finishPublish(draft, manifest, method, files, urls, out); + await finishPublish(draft, manifest, files, urls, out); } catch (err) { const extra = err.data?.errors ? '
• ' + err.data.errors.map(esc).join('
• ') : ''; out.innerHTML = `

${esc(err.message)}${extra}

`; @@ -509,7 +518,7 @@ async function initSubmit() { } // Submit the MV3 version (HEAD-checks uploaded files) then start payment. -async function finishPublish(draft, manifest, method, files, urls, out) { +async function finishPublish(draft, manifest, files, urls, out) { try { out.innerHTML = '

Validating…

'; const ver = await api(`/extensions/${draft.id}/versions`, { @@ -522,23 +531,8 @@ async function finishPublish(draft, manifest, method, files, urls, out) { }); const warn = (ver.warnings || []).length ? `

⚠ ${ver.warnings.map(esc).join('
⚠ ')}

` : ''; out.innerHTML = `

Validated MV3 ✓ (v${esc(ver.version)}). Starting payment…

${warn}`; - const pay = await api(`/extensions/${draft.id}/checkout`, { - method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ method }), - }).catch((err) => err.data || {}); + const pay = await startListingCheckout(draft.id); if (pay.checkoutUrl) { location.href = pay.checkoutUrl; return; } - if (pay.x402Version) { - out.innerHTML += `

Pay 1 USDC via your CoinPay wallet, then confirm. Pay-to: ${esc(pay.accepts?.[0]?.payTo || '(configure STORE_X402_PAY_TO)')}

-
- `; - document.getElementById('x402confirm').addEventListener('click', async () => { - const reference = document.getElementById('x402ref').value.trim(); - try { - await api(`/extensions/${draft.id}/confirm`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ paymentId: pay.paymentId, reference }) }); - location.href = `/store/extension.html?slug=${encodeURIComponent(draft.slug)}&paid=1`; - } catch (err) { out.innerHTML += `

${esc(err.message)}

`; } - }); - return; - } out.innerHTML += `

Payment could not be started: ${esc(pay.error || 'unknown')}

`; } catch (err) { const extra = err.data?.errors ? '
• ' + err.data.errors.map(esc).join('
• ') : ''; @@ -546,6 +540,14 @@ async function finishPublish(draft, manifest, method, files, urls, out) { } } +// The $1 listing fee: the API makes a CoinPay hosted checkout session and we +// send the publisher there (any coin or PayPal); CoinPay's webhook lists it. +async function startListingCheckout(extId) { + return api(`/extensions/${encodeURIComponent(extId)}/checkout`, { + method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}', + }).catch((err) => err.data || { error: err.message }); +} + /* ---------- tabs (install-guide.html) ---------- */ function initTabs() { document.querySelectorAll('.tab').forEach((tab) => { diff --git a/apps/extensions/public/submit.html b/apps/extensions/public/submit.html index eb47f87..684b3d3 100644 --- a/apps/extensions/public/submit.html +++ b/apps/extensions/public/submit.html @@ -66,13 +66,7 @@

4 · manifest.json (Manifest V3) *

}'>

5 · Pay the $1 listing fee

-
- - -
+

You pay on CoinPay's hosted checkout: any supported coin, or PayPal.

By publishing you confirm you have the rights to distribute this code. Malicious extensions are removed on report.

diff --git a/docs/extension-store.md b/docs/extension-store.md index faf3390..543d147 100644 --- a/docs/extension-store.md +++ b/docs/extension-store.md @@ -28,7 +28,7 @@ exactly as Chromium expects. - `extensions` — listing (slug, owner, status `draft|live|removed`) - `extension_versions` — per-version MV3 manifest + bundle/CRX URLs (`manifest_version` enforced = 3 at the app layer) -- `extension_payments` — the $1 fee; `stripe|coinpay|x402`; `pending|paid|failed` +- `extension_payments`: the $1 fee; `coinpay` (older rows may say `stripe`/`x402`); `pending|paid|failed` - `extension_scans` — async vu1nz results; non-gating badge - `extension_flags` — community flagging @@ -37,8 +37,8 @@ exactly as Chromium expects. ``` create draft ─▶ submit MV3 version ─▶ pay $1 ─▶ LIVE (instant) POST /extensions POST .../versions POST .../checkout │ - (validateManifest) Stripe or x402 ├─▶ git mirror (registry/) - webhook/confirm └─▶ vu1nz scan (async badge) + (validateManifest) CoinPay checkout ├─▶ git mirror (registry/) + CoinPay webhook └─▶ vu1nz scan (async badge) ``` Two publish paths (both supported): @@ -50,14 +50,17 @@ Publishing is **instant after payment** — the vu1nz scan and git mirror happen out of band and never block going live ("scan + community flagging", not pre-review). -## Payments (`store/payments.ts`, dependency-free) +## Payments (`store/payments.ts`) -- **Stripe** — Checkout Session created server-side; `checkout.session.completed` - webhook (HMAC-verified, replay-protected) marks the payment paid → publish. -- **CoinPay / x402** — `/checkout` returns an HTTP-402 challenge (1 USDC); the - client settles via the CoinPay wallet and calls `/confirm` with the reference. - Real settlement verification (`confirmCoinPaySettlement`) lands post-stub; it - refuses to mark paid without a verified settlement unless `STORE_X402_TRUST_CLIENT=1` (dev). +One rail: a **CoinPay hosted checkout session** (`@profullstack/coinpay-onboard`). +`/checkout` records a pending `extension_payments` row, creates a session for $1 +(customer_email = owner email, account_id = user id, property `tronbrowser.dev`, +metadata `{extension_id, payment_id, slug}`), stores the `cs_...` id as +`provider_ref` and returns `checkoutUrl`. The publisher picks any coin or PayPal +on CoinPay's page. CoinPay posts `payment.confirmed` / `payment.forwarded` to +`/payments/coinpay/webhook` (X-CoinPay-Signature, HMAC over the raw body); that +marks the row paid (idempotent) and publishes. `/confirm` with `{sessionId}` +reconciles by reading the session back with the API key. ## Security scan — vu1nz.com (`store/vu1nz.ts`) @@ -76,16 +79,16 @@ GitHub Action check on the PR path. | POST | `/extensions` | create draft (auth) | | POST | `/extensions/:id/versions` | submit MV3 version (auth) | | POST | `/extensions/:id/checkout` | start $1 payment (auth) | -| POST | `/extensions/:id/confirm` | confirm x402 settlement (auth) | -| POST | `/payments/stripe/webhook` | Stripe webhook | +| POST | `/extensions/:id/confirm` | reconcile a CoinPay checkout session (auth) | +| POST | `/payments/coinpay/webhook` | CoinPay webhook | | POST | `/extensions/:slug/flag` | community flag | | GET | `/updates.xml?id=` | Chromium gupdate manifest (auto-update) | | GET | `/extensions/:slug/download` | redirect to CRX/bundle | ## Env -See `.env.example` (Extension store section): `APP_URL`, `STRIPE_SECRET_KEY`, -`STRIPE_WEBHOOK_SECRET`, `STORE_X402_NETWORK`, `STORE_X402_PAY_TO`, +See `.env.example` (Extension store section): `APP_URL`, `COINPAY_API_URL`, +`COINPAY_API_KEY`, `COINPAY_WEBHOOK_SECRET`, `VU1NZ_API_URL`, `VU1NZ_API_KEY`, `STORE_REGISTRY_REPO`, `GITHUB_TOKEN`. ## Bundle hosting — files.profullstack.com (AgentBBS Files) @@ -119,6 +122,5 @@ the Dockerfile. - **Browser file upload** (multipart straight through the API to files) so devs don't need an SCP client at all — today they `scp` (or use FileZilla). - **Server-side CRX3 signing** from an uploaded `.zip`. -- **Real CoinPay settlement verification** + Stripe live keys. - **TronBrowser one-click** deep-link handler in the desktop app. - **Admin/moderation UI** for resolving flags. diff --git a/packages/storage/migrations/0003_extension_store.sql b/packages/storage/migrations/0003_extension_store.sql index d10bf59..e5e076a 100644 --- a/packages/storage/migrations/0003_extension_store.sql +++ b/packages/storage/migrations/0003_extension_store.sql @@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS extension_versions ( CREATE INDEX IF NOT EXISTS idx_ext_versions_ext ON extension_versions (extension_id, created_at DESC); --- The $1 pay-to-list fee. Paid via Stripe OR CoinPay/x402. A listing only goes +-- The $1 pay-to-list fee. Paid via CoinPay. A listing only goes -- 'live' once a matching row reaches status 'paid'. CREATE TABLE IF NOT EXISTS extension_payments ( id TEXT PRIMARY KEY, @@ -58,9 +58,9 @@ CREATE TABLE IF NOT EXISTS extension_payments ( user_id TEXT NOT NULL, amount_cents INTEGER NOT NULL DEFAULT 100, -- $1.00 currency TEXT NOT NULL DEFAULT 'usd', - method TEXT NOT NULL, -- 'stripe' | 'coinpay' | 'x402' + method TEXT NOT NULL, -- 'coinpay' (legacy rows: other values) status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'paid' | 'failed' - provider_ref TEXT, -- stripe session id / coinpay reference + provider_ref TEXT, -- CoinPay checkout session id (cs_...) paid_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (extension_id) REFERENCES extensions(id), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be09c1..f5d87b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,13 +290,16 @@ importers: version: 1.5.1(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) '@langchain/langgraph': specifier: ^1.4.6 - version: 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3) + version: 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@langchain/openai': specifier: ^1.5.3 version: 1.5.3(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(ws@8.21.0) '@logicsrc/agentswarm': specifier: ^0.1.0 - version: 0.1.0(@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3))(deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)) + version: 0.1.0(@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3))) + '@profullstack/coinpay-onboard': + specifier: 0.2.0 + version: 0.2.0(@cfworker/json-schema@4.1.1) '@profullstack/libsql-pg': specifier: ^0.1.1 version: 0.1.1(@libsql/client@0.14.0) @@ -305,7 +308,7 @@ importers: version: link:../../packages/sdk deepagents: specifier: ^1.10.5 - version: 1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0) + version: 1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) fflate: specifier: ^0.8.3 version: 0.8.3 @@ -1228,6 +1231,16 @@ packages: deepagents: optional: true + '@modelcontextprotocol/sdk@1.32.1': + resolution: {integrity: sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} @@ -1243,6 +1256,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@profullstack/coinpay-onboard@0.2.0': + resolution: {integrity: sha512-FxIiTefcAswyCyEapEcC+vSai3IZcOPPxaBGnT7L/VPGfZJYjH1kHoN6DCRMBQx0l2IbJ2aDLqrYUhqRUaI2cA==} + engines: {node: '>=20'} + hasBin: true + '@profullstack/emailer@1.0.1': resolution: {integrity: sha512-/uhHJJGH+1xSSz3mJn6X+m6aruYjMD3JOaRp/d4R/YWlzpy07H9z0/JUleIyRyBPNmaANSIwjTZ7aVjaukOEpg==} @@ -1608,10 +1626,12 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xmldom/xmldom@0.9.10': resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} engines: {node: '>=14.6'} + deprecated: this version has critical issues, please update to the latest version abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -1644,9 +1664,20 @@ packages: engines: {node: '>=18.18'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -1751,6 +1782,10 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -1796,6 +1831,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1893,12 +1936,36 @@ packages: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + content-disposition@2.0.1: + resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1973,6 +2040,10 @@ packages: dnssd-advertise@1.1.6: resolution: {integrity: sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1997,6 +2068,10 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -2004,6 +2079,10 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2088,6 +2167,14 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2180,6 +2267,16 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express-rate-limit@8.7.1: + resolution: {integrity: sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.3.0: + resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2196,6 +2293,9 @@ packages: fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.9: + resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2238,6 +2338,10 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2259,10 +2363,18 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2279,6 +2391,14 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + getenv@2.0.0: resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} engines: {node: '>=6'} @@ -2303,6 +2423,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2318,6 +2442,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -2359,6 +2487,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2386,6 +2518,14 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@10.7.4: + resolution: {integrity: sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} @@ -2415,6 +2555,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -2441,6 +2584,9 @@ packages: jimp-compact@0.16.1: resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==} + jose@6.2.13: + resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -2472,6 +2618,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2646,9 +2798,21 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2823,6 +2987,14 @@ packages: resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -2835,6 +3007,9 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@2.0.1: resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} engines: {node: '>=4'} @@ -2930,6 +3105,9 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2982,6 +3160,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + plist@3.1.1: resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} engines: {node: '>=10.4.0'} @@ -3036,10 +3218,18 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.16.1: + resolution: {integrity: sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3050,6 +3240,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} @@ -3127,6 +3321,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3156,12 +3354,19 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -3182,6 +3387,10 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -3190,6 +3399,10 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3205,6 +3418,22 @@ packages: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3376,6 +3605,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript-eslint@8.62.0: resolution: {integrity: sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3539,6 +3772,9 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.11: resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} engines: {node: '>=8.3.0'} @@ -3607,6 +3843,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -4643,7 +4884,7 @@ snapshots: optionalDependencies: react: 19.2.7 - '@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3)': + '@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: '@langchain/core': 1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) '@langchain/langgraph-checkpoint': 1.1.3(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) @@ -4651,6 +4892,8 @@ snapshots: '@langchain/protocol': 0.0.18 '@standard-schema/spec': 1.1.0 zod: 4.4.3 + optionalDependencies: + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - react - react-dom @@ -4727,10 +4970,34 @@ snapshots: '@libsql/win32-x64-msvc@0.4.7': optional: true - '@logicsrc/agentswarm@0.1.0(@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3))(deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0))': + '@logicsrc/agentswarm@0.1.0(@langchain/langgraph@1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3))(deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)))': optionalDependencies: - '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3) - deepagents: 1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0) + '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + deepagents: 1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) + + '@modelcontextprotocol/sdk@1.32.1(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.27) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + express: 5.3.0 + express-rate-limit: 8.7.1(express@5.3.0) + hono: 4.12.27 + jose: 6.2.13 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color '@neon-rs/load@0.0.4': {} @@ -4746,6 +5013,14 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@profullstack/coinpay-onboard@0.2.0(@cfworker/json-schema@4.1.1)': + dependencies: + '@modelcontextprotocol/sdk': 1.32.1(@cfworker/json-schema@4.1.1)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + '@profullstack/emailer@1.0.1': {} '@profullstack/libsql-pg@0.1.1(@libsql/client@0.14.0)': @@ -5127,6 +5402,10 @@ snapshots: agent-cli-detector@0.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -5134,6 +5413,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.9 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + anser@1.4.10: {} ansi-escapes@4.3.2: @@ -5268,6 +5554,20 @@ snapshots: big-integer@1.6.52: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.16.1 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -5315,6 +5615,16 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camelcase@6.3.0: {} @@ -5424,12 +5734,27 @@ snapshots: transitivePeerDependencies: - supports-color + content-disposition@2.0.1: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + core-js-compat@3.49.0: dependencies: browserslist: 4.28.4 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5456,13 +5781,13 @@ snapshots: deep-is@0.1.4: {} - deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0): + deepagents@1.10.5(langsmith@0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)): dependencies: '@langchain/core': 1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) - '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3) + '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@langchain/langgraph-sdk': 1.9.25(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7) fast-glob: 3.3.3 - langchain: 1.5.2(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0) + langchain: 1.5.2(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)) langsmith: 0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) micromatch: 4.0.8 yaml: 2.9.0 @@ -5495,6 +5820,12 @@ snapshots: dnssd-advertise@1.1.6: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + ee-first@1.1.1: {} electron-to-chromium@1.5.383: {} @@ -5511,10 +5842,16 @@ snapshots: dependencies: stackframe: 1.3.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5629,6 +5966,12 @@ snapshots: eventemitter3@5.0.4: {} + eventsource-parser@3.1.1: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.1 + expect-type@1.3.0: {} expo-asset@57.0.8(expo@57.0.9)(react-native@0.86.2(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@5.9.3): @@ -5739,6 +6082,47 @@ snapshots: exponential-backoff@3.1.3: {} + express-rate-limit@8.7.1(express@5.3.0): + dependencies: + debug: 4.4.3 + express: 5.3.0 + ip-address: 10.7.4 + transitivePeerDependencies: + - supports-color + + express@5.3.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 2.0.1 + content-type: 2.1.0 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.8 + qs: 6.16.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -5755,6 +6139,8 @@ snapshots: fast-sha256@1.3.0: {} + fast-uri@3.1.9: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5798,6 +6184,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5818,8 +6215,12 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -5829,6 +6230,24 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + getenv@2.0.0: {} glob-parent@5.1.2: @@ -5849,6 +6268,8 @@ snapshots: globals@15.15.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} happy-dom@20.10.6: @@ -5868,6 +6289,8 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -5913,6 +6336,10 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -5934,6 +6361,10 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@10.7.4: {} + + ipaddr.js@1.9.1: {} + is-core-module@2.16.2: dependencies: hasown: 2.0.4 @@ -5952,6 +6383,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -5987,6 +6420,8 @@ snapshots: jimp-compact@0.16.1: {} + jose@6.2.13: {} + js-base64@3.7.8: {} js-tiktoken@1.0.21: @@ -6012,6 +6447,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -6024,10 +6463,10 @@ snapshots: lan-network@0.2.1: {} - langchain@1.5.2(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0): + langchain@1.5.2(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(react@19.2.7)(ws@8.21.0)(zod-to-json-schema@3.25.2(zod@4.4.3)): dependencies: '@langchain/core': 1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) - '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod@4.4.3) + '@langchain/langgraph': 1.4.6(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0))(react@19.2.7)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) '@langchain/langgraph-checkpoint': 1.1.3(@langchain/core@1.2.1(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)) langsmith: 0.7.12(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) zod: 4.4.3 @@ -6164,8 +6603,14 @@ snapshots: marky@1.3.0: {} + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + memoize-one@5.2.1: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6422,6 +6867,10 @@ snapshots: dependencies: flow-enums-runtime: 0.0.6 + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -6432,6 +6881,10 @@ snapshots: on-headers@1.1.0: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@2.0.1: dependencies: mimic-fn: 1.2.0 @@ -6515,6 +6968,8 @@ snapshots: lru-cache: 11.5.1 minipass: 7.1.3 + path-to-regexp@8.4.2: {} + pathe@1.1.2: {} pathval@2.0.1: {} @@ -6560,6 +7015,8 @@ snapshots: picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} + plist@3.1.1: dependencies: '@xmldom/xmldom': 0.9.10 @@ -6607,8 +7064,18 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.16.1: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} queue@6.0.2: @@ -6617,6 +7084,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-devtools-core@6.1.5: dependencies: shell-quote: 1.9.0 @@ -6724,6 +7198,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -6775,12 +7251,24 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + sax@1.6.0: {} scheduler@0.27.0: {} @@ -6807,6 +7295,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serve-static@1.16.3: @@ -6818,6 +7322,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -6828,6 +7341,34 @@ snapshots: shell-quote@1.9.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -6966,6 +7507,12 @@ snapshots: type-fest@0.7.1: {} + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript-eslint@8.62.0(eslint@9.39.4)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) @@ -7112,6 +7659,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@7.5.11: {} ws@8.21.0: {} @@ -7152,6 +7701,15 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + optional: true + zod@3.25.76: {} zod@4.4.3: {} diff --git a/services/api/package.json b/services/api/package.json index 5d68a61..859ead5 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -20,11 +20,12 @@ "@langchain/langgraph": "^1.4.6", "@langchain/openai": "^1.5.3", "@logicsrc/agentswarm": "^0.1.0", + "@profullstack/coinpay-onboard": "0.2.0", + "@profullstack/libsql-pg": "^0.1.1", "@tronbrowser/sdk": "workspace:*", "deepagents": "^1.10.5", "fflate": "^0.8.3", - "hono": "^4.6.14", - "@profullstack/libsql-pg": "^0.1.1" + "hono": "^4.6.14" }, "devDependencies": { "@libsql/client": "^0.14.0", diff --git a/services/api/src/store/db.ts b/services/api/src/store/db.ts index b88926d..6ecebdf 100644 --- a/services/api/src/store/db.ts +++ b/services/api/src/store/db.ts @@ -243,7 +243,7 @@ export async function latestVersion(extensionId: string): Promise return (r.rows[0]?.extension_id as string) ?? null; } +/** + * Fallback for a webhook whose session id never made it into provider_ref: mark + * our row (from the session metadata) paid, but only if it is unpaid and either + * has no ref yet or already carries this session. Idempotent. + */ +export async function markPaidById(id: string, providerRef: string): Promise { + const r = await db().execute({ + sql: `UPDATE extension_payments SET status = 'paid', paid_at = datetime('now'), provider_ref = ? + WHERE id = ? AND status != 'paid' AND (provider_ref IS NULL OR provider_ref = ?) RETURNING extension_id`, + args: [providerRef, id, providerRef], + }); + return (r.rows[0]?.extension_id as string) ?? null; +} + export async function hasPaidListing(extensionId: string): Promise { const r = await db().execute({ sql: `SELECT 1 FROM extension_payments WHERE extension_id = ? AND status = 'paid' LIMIT 1`, diff --git a/services/api/src/store/payments.test.ts b/services/api/src/store/payments.test.ts index 1e0d277..f6b1079 100644 --- a/services/api/src/store/payments.test.ts +++ b/services/api/src/store/payments.test.ts @@ -1,53 +1,193 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createHmac } from 'node:crypto'; -import { verifyStripeWebhook, listingPaymentRequirements, LISTING_FEE_CENTS } from './payments.js'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { signWebhook } from '@profullstack/coinpay-onboard'; -function sign(body: string, secret: string, t = Math.floor(Date.now() / 1000)): string { - const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex'); - return `t=${t},v1=${v1}`; +// In-memory stand-ins for the Postgres-backed store db, so the routes run end to end. +const state = vi.hoisted(() => ({ + payments: new Map(), + ext: { id: 'ext-1', slug: 'acme-dark', owner_user_id: 'user-1', status: 'draft' } as any, + published: [] as string[], +})); + +vi.mock('../db.js', () => ({ + userBySession: vi.fn(async (s: string) => (s === 'good' ? { id: 'user-1', email: 'owner@example.com' } : null)), +})); + +vi.mock('./db.js', () => { + const fn = (impl: any) => vi.fn(impl); + return { + extensionById: fn(async (id: string) => (id === state.ext.id ? state.ext : null)), + latestVersion: fn(async () => ({ id: 'v1', version: '1.0.0' })), + hasPaidListing: fn(async (extId: string) => [...state.payments.values()].some((p) => p.extension_id === extId && p.status === 'paid')), + createPayment: fn(async (p: any) => { + const id = `pay-${state.payments.size + 1}`; + state.payments.set(id, { id, extension_id: p.extensionId, status: 'pending', provider_ref: null }); + return id; + }), + setPaymentRef: fn(async (id: string, ref: string) => { state.payments.get(id)!.provider_ref = ref; }), + markPaidByRef: fn(async (ref: string) => { + const p = [...state.payments.values()].find((x) => x.provider_ref === ref && x.status !== 'paid'); + if (!p) return null; + p.status = 'paid'; + return p.extension_id; + }), + markPaidById: fn(async (id: string, ref: string) => { + const p = state.payments.get(id); + if (!p || p.status === 'paid' || (p.provider_ref && p.provider_ref !== ref)) return null; + p.status = 'paid'; p.provider_ref = ref; + return p.extension_id; + }), + setExtensionStatus: fn(async (id: string, s: string) => { state.published.push(id); state.ext.status = s; }), + }; +}); +vi.mock('./mirror.js', () => ({ mirrorListing: vi.fn(async () => {}) })); +vi.mock('./vu1nz.js', () => ({ enqueueScan: vi.fn(async () => {}) })); + +const { store } = await import('./routes.js'); +const { readListingWebhook, LISTING_FEE_CENTS } = await import('./payments.js'); + +const SECRET = 'whsec_test_123'; +const fetchMock = vi.fn(); + +function event(type: string, data: Record) { + return JSON.stringify({ id: `evt_${Math.random()}`, type, business_id: 'biz', data }); +} +function postWebhook(body: string, sig = signWebhook(body, SECRET)) { + return store.request('/payments/coinpay/webhook', { + method: 'POST', headers: { 'content-type': 'application/json', 'x-coinpay-signature': sig }, body, + }); +} +async function startCheckout() { + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ + success: true, id: 'cs_abc', url: 'https://coinpayportal.com/checkout/cs_abc', status: 'open', methods: ['BTC', 'USDC_POL', 'PAYPAL'], + }), { status: 201 })); + return store.request('/extensions/ext-1/checkout', { + method: 'POST', headers: { authorization: 'Bearer good', 'content-type': 'application/json' }, body: JSON.stringify({ method: 'stripe' }), + }); } -describe('verifyStripeWebhook', () => { - const secret = 'whsec_test_123'; - beforeEach(() => { process.env.STRIPE_WEBHOOK_SECRET = secret; }); - afterEach(() => { delete process.env.STRIPE_WEBHOOK_SECRET; }); +beforeEach(() => { + state.payments.clear(); state.published = []; state.ext.status = 'draft'; + process.env.COINPAY_API_KEY = 'cp_live_test'; + process.env.COINPAY_WEBHOOK_SECRET = SECRET; + fetchMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); +}); +afterEach(() => { + vi.unstubAllGlobals(); + delete process.env.COINPAY_API_KEY; + delete process.env.COINPAY_WEBHOOK_SECRET; +}); + +describe('POST /extensions/:id/checkout', () => { + it('creates a CoinPay checkout session with the owner, property and metadata', async () => { + const res = await startCheckout(); + expect(res.status).toBe(200); + const body: any = await res.json(); + expect(body.checkoutUrl).toBe('https://coinpayportal.com/checkout/cs_abc'); + expect(body.method).toBe('coinpay'); + + const [url, init] = fetchMock.mock.calls[0]; + expect(url).toBe('https://coinpayportal.com/api/checkout/sessions'); + expect(init.headers.authorization).toBe('Bearer cp_live_test'); + const sent = JSON.parse(init.body); + expect(sent).toMatchObject({ + amount: LISTING_FEE_CENTS / 100, currency: 'USD', customer_email: 'owner@example.com', account_id: 'user-1', + property: 'tronbrowser.dev', metadata: { extension_id: 'ext-1', payment_id: 'pay-1', slug: 'acme-dark' }, + success_url: 'https://tronbrowser.dev/store/extension.html?slug=acme-dark&paid=1', + cancel_url: 'https://tronbrowser.dev/store/extension.html?slug=acme-dark&canceled=1', + }); + expect(sent.payment_method).toBeUndefined(); + expect(state.payments.get('pay-1')!.provider_ref).toBe('cs_abc'); + }); + + it('needs a signed-in owner', async () => { + const res = await store.request('/extensions/ext-1/checkout', { method: 'POST' }); + expect(res.status).toBe(401); + }); + + it('answers 502 when CoinPay is not configured', async () => { + delete process.env.COINPAY_API_KEY; + const res = await store.request('/extensions/ext-1/checkout', { method: 'POST', headers: { authorization: 'Bearer good' } }); + expect(res.status).toBe(502); + }); +}); + +describe('POST /payments/coinpay/webhook', () => { + it('settles on a nested payment.confirmed and publishes once (idempotent with forwarded)', async () => { + await startCheckout(); + const data = { payment_id: 'p1', status: 'confirmed', amount_usd: 1, checkout_session_id: 'cs_abc', metadata: { payment_id: 'pay-1', extension_id: 'ext-1' } }; + expect((await postWebhook(event('payment.confirmed', data))).status).toBe(200); + expect(state.payments.get('pay-1')!.status).toBe('paid'); + expect(state.published).toEqual(['ext-1']); - it('accepts a valid signature and parses the event', () => { - const body = JSON.stringify({ type: 'checkout.session.completed', data: { object: { id: 'cs_1' } } }); - const event = verifyStripeWebhook(body, sign(body, secret)); - expect(event?.type).toBe('checkout.session.completed'); - expect(event?.data?.object?.id).toBe('cs_1'); + expect((await postWebhook(event('payment.forwarded', { ...data, status: 'forwarded' }))).status).toBe(200); + expect(state.published).toEqual(['ext-1']); }); - it('rejects a tampered body', () => { - const body = JSON.stringify({ type: 'a' }); - const header = sign(body, secret); - expect(verifyStripeWebhook(body + 'x', header)).toBeNull(); + it('settles a PayPal capture (confirmed, status completed)', async () => { + await startCheckout(); + await postWebhook(event('payment.confirmed', { payment_id: 'pp1', status: 'completed', rail: 'paypal', checkout_session_id: 'cs_abc', metadata: { payment_id: 'pay-1' } })); + expect(state.payments.get('pay-1')!.status).toBe('paid'); }); - it('rejects a wrong secret', () => { - const body = '{}'; - expect(verifyStripeWebhook(body, sign(body, 'whsec_other'))).toBeNull(); + it('settles on forwarded alone', async () => { + await startCheckout(); + await postWebhook(event('payment.forwarded', { payment_id: 'p1', status: 'forwarded', checkout_session_id: 'cs_abc' })); + expect(state.payments.get('pay-1')!.status).toBe('paid'); }); - it('rejects a stale timestamp', () => { - const body = '{}'; - const old = Math.floor(Date.now() / 1000) - 10_000; - expect(verifyStripeWebhook(body, sign(body, secret, old))).toBeNull(); + it('falls back to metadata.payment_id when provider_ref was never stored', async () => { + await startCheckout(); + state.payments.get('pay-1')!.provider_ref = null; + await postWebhook(event('payment.confirmed', { payment_id: 'p1', status: 'confirmed', checkout_session_id: 'cs_abc', metadata: { payment_id: 'pay-1' } })); + expect(state.payments.get('pay-1')).toMatchObject({ status: 'paid', provider_ref: 'cs_abc' }); + }); + + it('grants nothing for payment.expired', async () => { + await startCheckout(); + const res = await postWebhook(event('payment.expired', { payment_id: 'p1', status: 'expired', checkout_session_id: 'cs_abc', metadata: { payment_id: 'pay-1' } })); + expect(res.status).toBe(200); + expect(state.payments.get('pay-1')!.status).toBe('pending'); + expect(state.published).toEqual([]); + }); + + it('rejects a bad signature with 401', async () => { + await startCheckout(); + const body = event('payment.confirmed', { status: 'confirmed', checkout_session_id: 'cs_abc' }); + expect((await postWebhook(body, signWebhook(body, 'wrong'))).status).toBe(401); + expect((await postWebhook(body + ' ', signWebhook(body, SECRET))).status).toBe(401); + expect((await postWebhook(body, '')).status).toBe(401); + expect(state.payments.get('pay-1')!.status).toBe('pending'); + }); +}); + +describe('POST /extensions/:id/confirm', () => { + it('reads the session back with the key and settles when paid', async () => { + await startCheckout(); + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, session: { id: 'cs_abc', status: 'paid' } }))); + const res = await store.request('/extensions/ext-1/confirm', { + method: 'POST', headers: { authorization: 'Bearer good', 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: 'cs_abc' }), + }); + expect(res.status).toBe(200); + expect(fetchMock.mock.calls[1][0]).toBe('https://coinpayportal.com/api/checkout/sessions/cs_abc'); + expect(state.payments.get('pay-1')!.status).toBe('paid'); }); - it('rejects when secret or header missing', () => { - delete process.env.STRIPE_WEBHOOK_SECRET; - expect(verifyStripeWebhook('{}', 't=1,v1=abc')).toBeNull(); + it('answers 402 while the session is open', async () => { + await startCheckout(); + fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true, session: { id: 'cs_abc', status: 'open' } }))); + const res = await store.request('/extensions/ext-1/confirm', { + method: 'POST', headers: { authorization: 'Bearer good', 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: 'cs_abc' }), + }); + expect(res.status).toBe(402); + expect(state.payments.get('pay-1')!.status).toBe('pending'); }); }); -describe('listingPaymentRequirements', () => { - it('returns a $1 USDC x402 challenge', () => { - const r = listingPaymentRequirements('https://tronbrowser.dev/x'); - expect(r.x402Version).toBe(1); - expect(r.accepts[0].asset).toBe('USDC'); - expect(r.accepts[0].amount).toBe('1000000'); - expect(LISTING_FEE_CENTS).toBe(100); +describe('readListingWebhook', () => { + it('returns null without a configured secret', () => { + delete process.env.COINPAY_WEBHOOK_SECRET; + const body = event('payment.confirmed', {}); + expect(readListingWebhook(body, signWebhook(body, SECRET))).toBeNull(); }); }); diff --git a/services/api/src/store/payments.ts b/services/api/src/store/payments.ts index d529c83..a833d14 100644 --- a/services/api/src/store/payments.ts +++ b/services/api/src/store/payments.ts @@ -1,143 +1,97 @@ -// The $1 pay-to-list fee. Two methods, both self-contained (no SDK deps — just -// fetch + node:crypto) so this builds inside the slim API Docker image: -// - Stripe: Checkout Session + webhook signature verification (fully wired) -// - CoinPay / x402: HTTP-402 challenge + a settlement confirm (verification -// against CoinPay lands post-stub, matching @tronbrowser/payments' style) -import { createHmac, timingSafeEqual } from 'node:crypto'; +// The $1 pay-to-list fee, paid through a CoinPay hosted checkout session. +// +// We create a session (POST /api/checkout/sessions with the business API key) +// and send the publisher to its hosted page, where they pick any coin or PayPal. +// CoinPay then posts a signed webhook; a paid event marks our local +// extension_payments row paid (keyed on the session id stored as provider_ref). +// /confirm can also reconcile by reading the session back with the key. +import { CoinPay, verifyWebhook, readWebhook } from '@profullstack/coinpay-onboard'; export const LISTING_FEE_CENTS = 100; // $1.00 export const LISTING_FEE_CURRENCY = 'usd'; +export const LISTING_PROPERTY = 'tronbrowser.dev'; -/* ---------------- Stripe ---------------- */ +function client(): CoinPay { + const key = process.env.COINPAY_API_KEY; + if (!key) throw new Error('COINPAY_API_KEY not configured'); + const base = process.env.COINPAY_API_URL; + return new CoinPay(base ? { token: key, base } : { token: key }); +} -export interface StripeCheckout { +export interface ListingCheckout { id: string; url: string; + methods: string[]; } /** - * Create a Stripe Checkout Session for the $1 listing fee. `clientReferenceId` - * is our extension_payments.id so the webhook can reconcile. + * Create a CoinPay checkout session for the $1 listing fee. `paymentId` is our + * extension_payments.id; it rides in metadata so the webhook can reconcile even + * if provider_ref was never written. */ -export async function createStripeCheckout(opts: { +export async function createListingCheckout(opts: { extensionId: string; paymentId: string; + slug: string; + userId: string; + email: string; successUrl: string; cancelUrl: string; -}): Promise { - const key = process.env.STRIPE_SECRET_KEY; - if (!key) throw new Error('STRIPE_SECRET_KEY not configured'); - - const form = new URLSearchParams(); - form.set('mode', 'payment'); - form.set('success_url', opts.successUrl); - form.set('cancel_url', opts.cancelUrl); - form.set('client_reference_id', opts.paymentId); - form.set('metadata[extension_id]', opts.extensionId); - form.set('metadata[payment_id]', opts.paymentId); - form.set('line_items[0][quantity]', '1'); - form.set('line_items[0][price_data][currency]', LISTING_FEE_CURRENCY); - form.set('line_items[0][price_data][unit_amount]', String(LISTING_FEE_CENTS)); - form.set('line_items[0][price_data][product_data][name]', 'TronBrowser extension listing'); - - const res = await fetch('https://api.stripe.com/v1/checkout/sessions', { - method: 'POST', - headers: { authorization: `Bearer ${key}`, 'content-type': 'application/x-www-form-urlencoded' }, - body: form, +}): Promise { + const s = await client().createCheckoutSession({ + amount: LISTING_FEE_CENTS / 100, + currency: 'USD', + description: `TronBrowser extension listing: ${opts.slug}`, + customerEmail: opts.email, + accountId: opts.userId, + property: LISTING_PROPERTY, + metadata: { extension_id: opts.extensionId, payment_id: opts.paymentId, slug: opts.slug }, + successUrl: opts.successUrl, + cancelUrl: opts.cancelUrl, + expiresInMinutes: 60, }); - if (!res.ok) throw new Error(`stripe checkout failed: ${res.status} ${await res.text()}`); - const session: any = await res.json(); - return { id: session.id, url: session.url }; + return { id: s.id, url: s.url, methods: (s as any).methods ?? [] }; } -/** - * Verify a Stripe webhook signature (the `Stripe-Signature` header) against the - * raw request body and STRIPE_WEBHOOK_SECRET. Returns the parsed event, or null - * if the signature is missing/invalid/stale. Implements Stripe's scheme without - * the SDK: signed_payload = `${t}.${rawBody}`, HMAC-SHA256 with the secret. - */ -export function verifyStripeWebhook(rawBody: string, sigHeader: string | undefined, toleranceSec = 300): any | null { - const secret = process.env.STRIPE_WEBHOOK_SECRET; - if (!secret || !sigHeader) return null; - - const parts = Object.fromEntries( - sigHeader.split(',').map((kv) => kv.split('=', 2) as [string, string]), - ); - const t = parts['t']; - const v1 = parts['v1']; - if (!t || !v1) return null; - - // Reject stale timestamps (replay protection). - const age = Math.abs(Date.now() / 1000 - Number(t)); - if (!Number.isFinite(age) || age > toleranceSec) return null; - - const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); - const a = Buffer.from(expected); - const b = Buffer.from(v1); - if (a.length !== b.length || !timingSafeEqual(a, b)) return null; - +/** True when CoinPay reports the session as paid (read with the business key). */ +export async function listingSessionPaid(sessionId: string): Promise { + if (!sessionId?.startsWith('cs_')) return false; try { - return JSON.parse(rawBody); + const s: any = await client().checkoutSession(sessionId); + return s?.status === 'paid'; } catch { - return null; + return false; } } -/* ---------------- CoinPay / x402 ---------------- */ - -export const X402_VERSION = 1 as const; - -export interface X402PaymentRequirements { - scheme: 'exact'; - network: string; - asset: string; - amount: string; // atomic units - payTo: string; - resource: string; - description: string; -} - -/** - * Build the HTTP-402 challenge body for paying the listing fee with a CoinPay - * global wallet (USDC). The client (or TronBrowser's built-in wallet) settles - * it and re-requests with an X-PAYMENT header. - */ -export function listingPaymentRequirements(resource: string): { x402Version: number; accepts: X402PaymentRequirements[] } { - const network = process.env.STORE_X402_NETWORK || 'base'; - const payTo = process.env.STORE_X402_PAY_TO || ''; - return { - x402Version: X402_VERSION, - accepts: [ - { - scheme: 'exact', - network, - asset: 'USDC', - amount: '1000000', // 1 USDC (6 decimals) - payTo, - resource, - description: 'TronBrowser extension listing fee', - }, - ], - }; +export interface ListingWebhook { + paid: boolean; + type: string; + sessionId: string | null; + localPaymentId: string | null; } /** - * Confirm a CoinPay/x402 settlement by reference. Real verification (querying - * CoinPay's settlement API for the reference + amount) lands post-stub; for now - * we accept a non-empty reference when STORE_X402_TRUST_CLIENT is set (dev), and - * otherwise refuse so we never mark a listing paid without a real settlement. + * Verify a CoinPay webhook (X-CoinPay-Signature over the RAW body with + * COINPAY_WEBHOOK_SECRET) and read the nested payload. Returns null when the + * signature is missing, wrong or stale. */ -export async function confirmCoinPaySettlement(reference: string): Promise { - if (!reference) return false; - if (process.env.STORE_X402_TRUST_CLIENT === '1') return true; - const api = process.env.COINPAY_SETTLEMENT_URL; - if (!api) return false; +export function readListingWebhook(rawBody: string, sigHeader: string | undefined): ListingWebhook | null { + const secret = process.env.COINPAY_WEBHOOK_SECRET; + if (!secret || !sigHeader) return null; + if (!verifyWebhook(rawBody, sigHeader, secret)) return null; + let payload: unknown; try { - const r = await fetch(`${api.replace(/\/$/, '')}/${encodeURIComponent(reference)}`); - if (!r.ok) return false; - const s: any = await r.json(); - return s?.status === 'settled' || s?.status === 'paid'; + payload = JSON.parse(rawBody); } catch { - return false; + return null; } + const ev = readWebhook(payload); + const md = (ev.metadata ?? {}) as Record; + return { + paid: ev.paid, + type: ev.type, + sessionId: ev.checkoutSessionId, + localPaymentId: typeof md.payment_id === 'string' && md.payment_id ? md.payment_id : null, + }; } diff --git a/services/api/src/store/routes.ts b/services/api/src/store/routes.ts index 2e17c7f..6efaa40 100644 --- a/services/api/src/store/routes.ts +++ b/services/api/src/store/routes.ts @@ -1,7 +1,7 @@ // Extension store routes, mounted at /api/store by services/api/src/index.ts. // -// Flow: create draft -> submit MV3 version (upload or PR) -> pay $1 (Stripe or -// CoinPay/x402) -> listing goes LIVE instantly + git mirror + async vu1nz scan. +// Flow: create draft -> submit MV3 version (upload or PR) -> pay $1 (CoinPay +// hosted checkout: any coin or PayPal) -> listing goes LIVE instantly + git mirror + async vu1nz scan. // Chromium install/auto-update keeps working via /api/store/updates.xml. import { Hono } from 'hono'; import { getCookie } from 'hono/cookie'; @@ -9,7 +9,7 @@ import { userBySession, type User } from '../db.js'; import { createExtension, extensionById, extensionBySlug, slugTaken, listLiveExtensions, setExtensionStatus, addVersion, latestVersion, createPayment, setPaymentRef, - markPaidByRef, hasPaidListing, latestScan, addFlag, openFlagCount, + markPaidByRef, markPaidById, hasPaidListing, latestScan, addFlag, openFlagCount, publisherKey, handleTaken, upsertPublisherKey, createPublisherToken, userByPublisherToken, listPublisherTokens, revokePublisherToken, updateExtension, signingKeyFor, insertSigningKey, @@ -18,10 +18,7 @@ import { validateManifest, slugify } from './manifest.js'; import { provisionPublisher, generateKeypair, publicUrlFor, scpCommand, artifactExists, SCP_TARGET, } from './fileshost.js'; -import { - createStripeCheckout, verifyStripeWebhook, listingPaymentRequirements, - confirmCoinPaySettlement, LISTING_FEE_CENTS, -} from './payments.js'; +import { createListingCheckout, listingSessionPaid, readListingWebhook } from './payments.js'; import { enqueueScan } from './vu1nz.js'; import { mirrorListing } from './mirror.js'; import { fetchArtifact, artifactToZip, extractListingFromCrx, stampUpdateUrl } from './crx.js'; @@ -516,65 +513,63 @@ store.post('/extensions/:id/checkout', async (c) => { if (!(await latestVersion(ext.id))) return c.json({ error: 'submit a version before paying' }, 400); if (await hasPaidListing(ext.id)) return c.json({ error: 'already paid', alreadyLive: true }, 409); - const method = (await c.req.json().catch(() => ({}))).method || 'stripe'; - - if (method === 'stripe') { - const paymentId = await createPayment({ extensionId: ext.id, userId: user.id, method: 'stripe' }); - try { - const session = await createStripeCheckout({ - extensionId: ext.id, - paymentId, - successUrl: `${APP_URL}/store/extension.html?slug=${ext.slug}&paid=1`, - cancelUrl: `${APP_URL}/store/submit.html?ext=${ext.id}&canceled=1`, - }); - await setPaymentRef(paymentId, session.id); - return c.json({ method: 'stripe', checkoutUrl: session.url }); - } catch (e: any) { - return c.json({ error: e.message }, 502); - } - } + // A `method` field is tolerated and ignored: there is one rail (CoinPay's hosted + // checkout, where the buyer picks any coin or PayPal), so it is ignored. + await c.req.json().catch(() => ({})); - if (method === 'coinpay' || method === 'x402') { - const resource = `${APP_URL}/api/store/extensions/${ext.id}/checkout`; - const reqs = listingPaymentRequirements(resource); - // Record a pending payment the client confirms via /confirm. - const paymentId = await createPayment({ extensionId: ext.id, userId: user.id, method: 'x402' }); - return c.json({ method: 'x402', paymentId, amountCents: LISTING_FEE_CENTS, ...reqs }, 402); + const paymentId = await createPayment({ extensionId: ext.id, userId: user.id, method: 'coinpay' }); + try { + const slug = encodeURIComponent(ext.slug); + const session = await createListingCheckout({ + extensionId: ext.id, + paymentId, + slug: ext.slug, + userId: user.id, + // CoinPay requires an email; wallet-only accounts get a stable per-user address. + email: user.email || `${user.id}@users.tronbrowser.dev`, + successUrl: `${APP_URL}/store/extension.html?slug=${slug}&paid=1`, + cancelUrl: `${APP_URL}/store/extension.html?slug=${slug}&canceled=1`, + }); + await setPaymentRef(paymentId, session.id); + return c.json({ method: 'coinpay', paymentId, sessionId: session.id, checkoutUrl: session.url }); + } catch (e: any) { + return c.json({ error: e.message }, 502); } - - return c.json({ error: 'unknown payment method' }, 400); }); -/* ---------- CoinPay/x402 settlement confirm ---------- */ +/* ---------- reconcile a checkout session (optional; the webhook is primary) ---------- */ store.post('/extensions/:id/confirm', async (c) => { const user = await currentUser(c); if (!user) return c.json({ error: 'unauthorized' }, 401); const ext = await extensionById(c.req.param('id')); if (!ext) return c.json({ error: 'not found' }, 404); if (ext.owner_user_id !== user.id) return c.json({ error: 'forbidden' }, 403); + if (await hasPaidListing(ext.id)) return c.json({ ok: true, live: true }); - const { paymentId, reference } = await c.req.json().catch(() => ({})); - if (!paymentId || !reference) return c.json({ error: 'paymentId and reference required' }, 400); - if (!(await confirmCoinPaySettlement(reference))) return c.json({ error: 'settlement not verified' }, 402); + const { sessionId, reference } = await c.req.json().catch(() => ({})); + const ref = sessionId || reference; + if (!ref) return c.json({ error: 'sessionId required' }, 400); + if (!(await listingSessionPaid(ref))) return c.json({ error: 'payment not received yet' }, 402); - await setPaymentRef(paymentId, reference); - const extId = await markPaidByRef(reference); + // Only a session we created for this extension can settle it. + const extId = await markPaidByRef(ref); + if (extId && extId !== ext.id) return c.json({ error: 'session belongs to another listing' }, 409); if (extId) await publish(extId); - return c.json({ ok: true, live: true }); + return c.json({ ok: true, live: await hasPaidListing(ext.id) }); }); -/* ---------- Stripe webhook (raw body) ---------- */ -store.post('/payments/stripe/webhook', async (c) => { +/* ---------- CoinPay webhook (signature over the raw body) ---------- */ +store.post('/payments/coinpay/webhook', async (c) => { const raw = await c.req.text(); - const event = verifyStripeWebhook(raw, c.req.header('stripe-signature')); - if (!event) return c.json({ error: 'invalid signature' }, 400); - - if (event.type === 'checkout.session.completed') { - const sessionId = event.data?.object?.id; - if (sessionId) { - const extId = await markPaidByRef(sessionId); - if (extId) await publish(extId); - } + const ev = readListingWebhook(raw, c.req.header('x-coinpay-signature')); + if (!ev) return c.json({ error: 'invalid signature' }, 401); + + if (ev.paid && ev.sessionId) { + // confirmed and forwarded can both arrive for one session: markPaid* only + // flips a pending row, so the second event is a no-op. + let extId = await markPaidByRef(ev.sessionId); + if (!extId && ev.localPaymentId) extId = await markPaidById(ev.localPaymentId, ev.sessionId); + if (extId) await publish(extId); } return c.json({ received: true }); });