diff --git a/backlog/tasks/task-124 - Encrypt-all-PII-at-rest-in-EncryptID-database.md b/backlog/tasks/task-124 - Encrypt-all-PII-at-rest-in-EncryptID-database.md new file mode 100644 index 0000000..66b1810 --- /dev/null +++ b/backlog/tasks/task-124 - Encrypt-all-PII-at-rest-in-EncryptID-database.md @@ -0,0 +1,51 @@ +--- +id: TASK-124 +title: Encrypt all PII at rest in EncryptID database +status: Done +assignee: [] +created_date: '2026-03-24 00:29' +updated_date: '2026-03-24 00:29' +labels: + - security + - encryptid + - database +dependencies: [] +references: + - src/encryptid/server-crypto.ts + - src/encryptid/migrations/encrypt-pii.ts +priority: high +--- + +## Description + + +Server-side AES-256-GCM encryption for all PII fields stored in PostgreSQL. Keys derived from JWT_SECRET via HKDF with dedicated salts (`pii-v1` for encryption, `pii-hash-v1` for HMAC). HMAC-SHA256 hash indexes for equality lookups on email and UP address fields. + +**Scope:** 18 fields across 6 tables (users, guardians, identity_invites, space_invites, notifications, fund_claims). Username and display_name excluded (public identifiers, needed for ILIKE search). + +**Files:** +- `src/encryptid/server-crypto.ts` — NEW: encryptField(), decryptField(), hashForLookup() +- `src/encryptid/schema.sql` — 18 _enc/_hash columns + 4 indexes +- `src/encryptid/db.ts` — async row mappers with decrypt fallback, dual-write on inserts/updates, hash-based lookups +- `src/encryptid/server.ts` — replaced unkeyed hashEmail() with HMAC hashForLookup() +- `src/encryptid/migrations/encrypt-pii.ts` — NEW: idempotent backfill script + +**Remaining:** Drop plaintext columns after extended verification period. + + +## Acceptance Criteria + +- [x] #1 All PII fields have corresponding _enc columns with AES-256-GCM ciphertext +- [x] #2 HMAC-SHA256 hash indexes enable email and UP address lookups without plaintext +- [x] #3 Row mappers decrypt transparently — callers receive plaintext +- [x] #4 Wrong encryption key cannot decrypt (verified with test) +- [x] #5 Same plaintext produces different ciphertext each time (random IV) +- [x] #6 Backfill migration encrypts all existing rows (0 remaining unencrypted) +- [x] #7 Legacy plaintext fallback works for pre-migration rows during transition + + +## Final Summary + + +Deployed 2026-03-23. Commit `9695e95`. Backfill completed: 1 user, 2 guardians, 8 identity invites, 2 fund claims encrypted. 19/19 verification tests passed (ciphertext format, decryption, HMAC determinism, wrong-key rejection, random IV uniqueness). Plaintext columns retained for rollback safety — drop in follow-up task after extended verification. + diff --git a/backlog/tasks/task-125 - Configure-Stripe-Mollie-API-keys-and-test-HyperSwitch-payment-channels.md b/backlog/tasks/task-125 - Configure-Stripe-Mollie-API-keys-and-test-HyperSwitch-payment-channels.md new file mode 100644 index 0000000..ef39b3a --- /dev/null +++ b/backlog/tasks/task-125 - Configure-Stripe-Mollie-API-keys-and-test-HyperSwitch-payment-channels.md @@ -0,0 +1,63 @@ +--- +id: TASK-125 +title: Configure Stripe & Mollie API keys and test HyperSwitch payment channels +status: To Do +assignee: [] +created_date: '2026-03-24 00:56' +labels: + - payments + - hyperswitch + - infrastructure +dependencies: [] +references: + - 'https://pay.rspace.online/health' + - 'https://dashboard.stripe.com/test/apikeys' + - 'https://my.mollie.com/dashboard' +priority: medium +--- + +## Description + + +HyperSwitch payment orchestrator is deployed at `pay.rspace.online` with merchant account `rspace_merchant` and DB migrations complete. The connector configuration and end-to-end payment testing is blocked on obtaining real API keys from Stripe and Mollie. + +## Context +- HyperSwitch is live: `https://pay.rspace.online/health` +- Merchant account created with publishable key `pk_snd_9167de4f...` +- Merchant API key saved to `.env` as `HS_MERCHANT_SECRET_KEY` +- Internal mint/escrow/confirm APIs verified working on rspace-online +- Bonding curve ($MYCO) endpoints live and tested +- `INTERNAL_API_KEY` and `RSPACE_INTERNAL_API_KEY` deployed to both repos + +## Steps +1. **Obtain Stripe API key** — create Stripe account or use existing, get test mode API key (`sk_test_...`) +2. **Obtain Mollie API key** — create Mollie account, get test API key +3. **Add keys to Infisical** — `STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET`, `MOLLIE_API_KEY` in rspace project +4. **Add keys to payment-infra `.env`** on Netcup +5. **Run `scripts/setup-hyperswitch.sh`** — configures Stripe + Mollie connectors, geo-based routing (EU→Mollie, US→Stripe), webhook endpoint +6. **Rebuild payment-infra onramp/offramp services** — they have new HyperSwitch integration code (`hyperswitch.ts`, `hyperswitch-offramp.ts`) but haven't been rebuilt +7. **Test Stripe channel** — create payment intent, complete with test card `4242424242424242`, verify cUSDC minted +8. **Test Mollie channel** — create payment intent with EU billing, complete via Mollie test mode, verify cUSDC minted +9. **Test off-ramp** — initiate withdrawal, verify escrow burn, simulate payout webhook, verify confirm/reverse +10. **Run `bun scripts/test-full-loop.ts`** — full loop: fiat in → cUSDC → $MYCO → cUSDC → fiat out + +## Key files +- `payment-infra/scripts/setup-hyperswitch.sh` — connector + routing setup script +- `payment-infra/services/onramp-service/src/hyperswitch.ts` — on-ramp integration +- `payment-infra/services/offramp-service/src/hyperswitch-offramp.ts` — off-ramp integration +- `rspace-online/scripts/test-full-loop.ts` — end-to-end test script +- `rspace-online/server/index.ts` — internal mint/escrow/confirm endpoints (lines 570-680) +- `payment-infra/config/hyperswitch/config.toml` — HyperSwitch TOML config on Netcup + + +## Acceptance Criteria + +- [ ] #1 Stripe test API key obtained and added to Infisical + payment-infra .env +- [ ] #2 Mollie test API key obtained and added to Infisical + payment-infra .env +- [ ] #3 setup-hyperswitch.sh runs successfully — Stripe + Mollie connectors configured with geo-based routing +- [ ] #4 onramp-service and offramp-service rebuilt with HyperSwitch integration code +- [ ] #5 Stripe test payment completes end-to-end: card payment → webhook → cUSDC minted in CRDT ledger +- [ ] #6 Mollie test payment completes end-to-end: iDEAL/SEPA → webhook → cUSDC minted +- [ ] #7 Off-ramp escrow flow verified: escrow burn → payout → confirm (or reverse on failure) +- [ ] #8 Full loop test passes: fiat → cUSDC → $MYCO swap → cUSDC → fiat withdrawal + diff --git a/bun.lock b/bun.lock index 977170e..6df8928 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@google/genai": "^1.43.0", "@google/generative-ai": "^0.24.1", "@lit/reactive-element": "^2.0.4", + "@modelcontextprotocol/sdk": "^1.27.1", "@noble/curves": "^1.8.0", "@noble/hashes": "^1.7.0", "@openfort/openfort-node": "^0.7.0", @@ -42,6 +43,7 @@ "mailparser": "^3.7.2", "marked": "^17.0.3", "nodemailer": "^6.9.0", + "pdf-lib": "^1.17.1", "perfect-arrows": "^0.3.7", "perfect-freehand": "^1.2.2", "postgres": "^3.4.5", @@ -215,6 +217,8 @@ "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -261,6 +265,8 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], @@ -271,6 +277,10 @@ "@openfort/shield-js": ["@openfort/shield-js@0.1.35", "", { "dependencies": { "axios": "1.13.6", "axios-retry": "4.5.0" } }, "sha512-S/v73xRnbgv5i47IRJ7cPWOnJ1bUTOJN+048Y8mJ+ya+iRJUXKTTqePaWApvfrVHHcKA+li8QGyDsRRDefLLVQ=="], + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], + + "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -603,12 +613,16 @@ "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "aes-js": ["aes-js@4.0.0-beta.5", "", {}, "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -637,6 +651,8 @@ "bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -647,8 +663,12 @@ "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -663,8 +683,18 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="], @@ -683,6 +713,8 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -705,8 +737,12 @@ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding-japanese": ["encoding-japanese@2.2.0", "", {}, "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -721,12 +757,24 @@ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "ethers": ["ethers@6.16.0", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", "tslib": "2.7.0", "ws": "8.17.1" } }, "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -739,6 +787,8 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -749,6 +799,10 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -789,6 +843,8 @@ "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "http_ece": ["http_ece@1.2.0", "", {}, "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -803,10 +859,14 @@ "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-retry-allowed": ["is-retry-allowed@2.2.0", "", {}, "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -825,6 +885,8 @@ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], @@ -867,9 +929,13 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], @@ -883,14 +949,24 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "nodemailer": ["nodemailer@6.10.1", "", {}, "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], "ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="], @@ -909,12 +985,18 @@ "parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], "perfect-arrows": ["perfect-arrows@0.3.7", "", {}, "sha512-wEN2gerTPVWl3yqoFEF8OeGbg3aRe2sxNUi9rnyYrCsL4JcI6K2tBDezRtqVrYG0BNtsWLdYiiTrYm+X//8yLQ=="], @@ -931,6 +1013,8 @@ "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], @@ -983,6 +1067,8 @@ "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -991,8 +1077,14 @@ "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], @@ -1011,6 +1103,8 @@ "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -1021,16 +1115,30 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "send": ["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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], @@ -1047,6 +1155,8 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1065,6 +1175,8 @@ "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1073,12 +1185,16 @@ "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1087,6 +1203,8 @@ "valid-url": ["valid-url@1.0.9", "", {}, "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="], "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], @@ -1113,6 +1231,8 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "y-indexeddb": ["y-indexeddb@9.0.12", "", { "dependencies": { "lib0": "^0.2.74" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg=="], @@ -1133,6 +1253,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "@automerge/automerge/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -1153,6 +1275,8 @@ "@openfort/openfort-node/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "ethers/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.10.1", "", {}, "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw=="], @@ -1167,6 +1291,8 @@ "ethers/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "imapflow/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -1181,8 +1307,12 @@ "mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="], + "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -1197,6 +1327,8 @@ "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], diff --git a/lib/folk-blender.ts b/lib/folk-blender.ts index 27c2179..ed3b03f 100644 --- a/lib/folk-blender.ts +++ b/lib/folk-blender.ts @@ -269,6 +269,7 @@ export class FolkBlender extends FolkShape { #isLoading = false; #error: string | null = null; + #prompt: string | null = null; #renderUrl: string | null = null; #script: string | null = null; #blendUrl: string | null = null; @@ -365,6 +366,25 @@ export class FolkBlender extends FolkShape { this.dispatchEvent(new CustomEvent("close")); }); + // Restore persisted state + if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt; + if (this.#renderUrl || this.#script) { + this.#renderResult(); + } + + // Health check + fetch("/api/blender-gen/health").then(r => r.json()).then((h: any) => { + if (!h.available && this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = (h.issues || []).join(", ") || "Blender service unavailable"; + } + }).catch(() => { + if (this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = "Cannot reach Blender health endpoint"; + } + }); + return root; } @@ -372,6 +392,7 @@ export class FolkBlender extends FolkShape { const prompt = this.#promptInput?.value.trim(); if (!prompt || this.#isLoading) return; + this.#prompt = prompt; this.#isLoading = true; this.#error = null; if (this.#generateBtn) this.#generateBtn.disabled = true; @@ -437,6 +458,10 @@ export class FolkBlender extends FolkShape { static override fromData(data: Record): FolkBlender { const shape = FolkShape.fromData(data) as FolkBlender; + if (data.prompt) shape.#prompt = data.prompt; + if (data.renderUrl) shape.#renderUrl = data.renderUrl; + if (data.script) shape.#script = data.script; + if (data.blendUrl) shape.#blendUrl = data.blendUrl; return shape; } @@ -444,6 +469,7 @@ export class FolkBlender extends FolkShape { return { ...super.toJSON(), type: "folk-blender", + prompt: this.#prompt, renderUrl: this.#renderUrl, script: this.#script, blendUrl: this.#blendUrl, @@ -452,5 +478,13 @@ export class FolkBlender extends FolkShape { override applyData(data: Record): void { super.applyData(data); + if ("prompt" in data) this.#prompt = data.prompt; + if ("renderUrl" in data) this.#renderUrl = data.renderUrl; + if ("script" in data) this.#script = data.script; + if ("blendUrl" in data) this.#blendUrl = data.blendUrl; + if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt; + if (this.#renderUrl || this.#script) { + this.#renderResult(); + } } } diff --git a/lib/folk-embed.ts b/lib/folk-embed.ts index 15f3941..704768b 100644 --- a/lib/folk-embed.ts +++ b/lib/folk-embed.ts @@ -369,6 +369,7 @@ export class FolkEmbed extends FolkShape { "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; iframe.allowFullscreen = true; iframe.referrerPolicy = "no-referrer"; + iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"); content.appendChild(iframe); } diff --git a/lib/folk-freecad.ts b/lib/folk-freecad.ts index 5b04546..95e85a7 100644 --- a/lib/folk-freecad.ts +++ b/lib/folk-freecad.ts @@ -221,6 +221,7 @@ export class FolkFreeCAD extends FolkShape { #isLoading = false; #error: string | null = null; + #prompt: string | null = null; #previewUrl: string | null = null; #stepUrl: string | null = null; #stlUrl: string | null = null; @@ -293,6 +294,25 @@ export class FolkFreeCAD extends FolkShape { this.dispatchEvent(new CustomEvent("close")); }); + // Restore persisted state + if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt; + if (this.#previewUrl || this.#stepUrl || this.#stlUrl) { + this.#renderResult(); + } + + // Health check + fetch("/api/freecad/health").then(r => r.json()).then((h: any) => { + if (!h.available && this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = h.error || "FreeCAD MCP server unavailable"; + } + }).catch(() => { + if (this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = "Cannot reach FreeCAD health endpoint"; + } + }); + return root; } @@ -300,12 +320,13 @@ export class FolkFreeCAD extends FolkShape { const prompt = this.#promptInput?.value.trim(); if (!prompt || this.#isLoading) return; + this.#prompt = prompt; this.#isLoading = true; this.#error = null; if (this.#generateBtn) this.#generateBtn.disabled = true; if (this.#previewArea) { - this.#previewArea.innerHTML = '
Generating CAD model...
'; + this.#previewArea.innerHTML = '
Generating CAD model with AI... (may take 30-60s)
'; } try { @@ -369,6 +390,10 @@ export class FolkFreeCAD extends FolkShape { static override fromData(data: Record): FolkFreeCAD { const shape = FolkShape.fromData(data) as FolkFreeCAD; + if (data.prompt) shape.#prompt = data.prompt; + if (data.previewUrl) shape.#previewUrl = data.previewUrl; + if (data.stepUrl) shape.#stepUrl = data.stepUrl; + if (data.stlUrl) shape.#stlUrl = data.stlUrl; return shape; } @@ -376,6 +401,7 @@ export class FolkFreeCAD extends FolkShape { return { ...super.toJSON(), type: "folk-freecad", + prompt: this.#prompt, previewUrl: this.#previewUrl, stepUrl: this.#stepUrl, stlUrl: this.#stlUrl, @@ -384,5 +410,13 @@ export class FolkFreeCAD extends FolkShape { override applyData(data: Record): void { super.applyData(data); + if ("prompt" in data) this.#prompt = data.prompt; + if ("previewUrl" in data) this.#previewUrl = data.previewUrl; + if ("stepUrl" in data) this.#stepUrl = data.stepUrl; + if ("stlUrl" in data) this.#stlUrl = data.stlUrl; + if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt; + if (this.#previewUrl || this.#stepUrl || this.#stlUrl) { + this.#renderResult(); + } } } diff --git a/lib/folk-kicad.ts b/lib/folk-kicad.ts index 5ea6666..a1542d3 100644 --- a/lib/folk-kicad.ts +++ b/lib/folk-kicad.ts @@ -275,6 +275,8 @@ export class FolkKiCAD extends FolkShape { #isLoading = false; #error: string | null = null; + #prompt: string | null = null; + #components: string[] = []; #schematicSvg: string | null = null; #boardSvg: string | null = null; #gerberUrl: string | null = null; @@ -373,6 +375,27 @@ export class FolkKiCAD extends FolkShape { this.dispatchEvent(new CustomEvent("close")); }); + // Restore persisted state + if (this.#prompt && this.#promptInput) this.#promptInput.value = this.#prompt; + if (this.#components.length && this.#componentInput) this.#componentInput.value = this.#components.join(", "); + if (this.#schematicSvg || this.#boardSvg || this.#drcResults) { + this.#renderPreview(); + this.#showExports(); + } + + // Health check + fetch("/api/kicad/health").then(r => r.json()).then((h: any) => { + if (!h.available && this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = h.error || "KiCAD MCP server unavailable"; + } + }).catch(() => { + if (this.#generateBtn) { + this.#generateBtn.disabled = true; + this.#generateBtn.title = "Cannot reach KiCAD health endpoint"; + } + }); + return root; } @@ -385,17 +408,18 @@ export class FolkKiCAD extends FolkShape { .map((c) => c.trim()) .filter(Boolean) || []; + this.#prompt = prompt; + this.#components = components; this.#isLoading = true; this.#error = null; if (this.#generateBtn) this.#generateBtn.disabled = true; if (this.#previewArea) { - this.#previewArea.innerHTML = '
Generating PCB design...
'; + this.#previewArea.innerHTML = '
Designing PCB with AI... (may take 30-60s)
'; } try { - // Step 1: Create project - const createRes = await fetch("/api/kicad/create_project", { + const createRes = await fetch("/api/kicad/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt, components }), @@ -485,6 +509,14 @@ export class FolkKiCAD extends FolkShape { static override fromData(data: Record): FolkKiCAD { const shape = FolkShape.fromData(data) as FolkKiCAD; + if (data.prompt) shape.#prompt = data.prompt; + if (data.components) shape.#components = data.components; + if (data.schematicSvg) shape.#schematicSvg = data.schematicSvg; + if (data.boardSvg) shape.#boardSvg = data.boardSvg; + if (data.gerberUrl) shape.#gerberUrl = data.gerberUrl; + if (data.bomUrl) shape.#bomUrl = data.bomUrl; + if (data.pdfUrl) shape.#pdfUrl = data.pdfUrl; + if (data.drcResults) shape.#drcResults = data.drcResults; return shape; } @@ -492,15 +524,32 @@ export class FolkKiCAD extends FolkShape { return { ...super.toJSON(), type: "folk-kicad", + prompt: this.#prompt, + components: this.#components, schematicSvg: this.#schematicSvg, boardSvg: this.#boardSvg, gerberUrl: this.#gerberUrl, bomUrl: this.#bomUrl, pdfUrl: this.#pdfUrl, + drcResults: this.#drcResults, }; } override applyData(data: Record): void { super.applyData(data); + if ("prompt" in data) this.#prompt = data.prompt; + if ("components" in data) this.#components = data.components || []; + if ("schematicSvg" in data) this.#schematicSvg = data.schematicSvg; + if ("boardSvg" in data) this.#boardSvg = data.boardSvg; + if ("gerberUrl" in data) this.#gerberUrl = data.gerberUrl; + if ("bomUrl" in data) this.#bomUrl = data.bomUrl; + if ("pdfUrl" in data) this.#pdfUrl = data.pdfUrl; + if ("drcResults" in data) this.#drcResults = data.drcResults; + if (this.#promptInput && this.#prompt) this.#promptInput.value = this.#prompt; + if (this.#componentInput && this.#components.length) this.#componentInput.value = this.#components.join(", "); + if (this.#schematicSvg || this.#boardSvg || this.#drcResults) { + this.#renderPreview(); + this.#showExports(); + } } } diff --git a/modules/rnotes/components/folk-notes-app.ts b/modules/rnotes/components/folk-notes-app.ts index a4cf0c4..ed9c952 100644 --- a/modules/rnotes/components/folk-notes-app.ts +++ b/modules/rnotes/components/folk-notes-app.ts @@ -29,7 +29,6 @@ import { createSlashCommandPlugin } from './slash-command'; import type { ImportExportDialog } from './import-export-dialog'; import { SpeechDictation } from '../../../lib/speech-dictation'; import { TourEngine } from '../../../shared/tour-engine'; -import { ViewHistory } from '../../../shared/view-history.js'; import * as Y from 'yjs'; import { IndexeddbPersistence } from 'y-indexeddb'; import { ySyncPlugin, yUndoPlugin, yCursorPlugin } from '@tiptap/y-tiptap'; @@ -124,7 +123,6 @@ interface NotebookDoc { class FolkNotesApp extends HTMLElement { private shadow!: ShadowRoot; private space = ""; - private view: "notebooks" | "notebook" | "note" = "notebooks"; private notebooks: Notebook[] = []; private selectedNotebook: (Notebook & { notes: Note[] }) | null = null; private selectedNote: Note | null = null; @@ -134,19 +132,21 @@ class FolkNotesApp extends HTMLElement { private loading = false; private error = ""; + // Sidebar state + private expandedNotebooks = new Set(); + private notebookNotes = new Map(); + private sidebarOpen = true; + // Zone-based rendering private navZone!: HTMLDivElement; private contentZone!: HTMLDivElement; private metaZone!: HTMLDivElement; - // Navigation history - private _history = new ViewHistory<"notebooks" | "notebook" | "note">("notebooks"); - // Guided tour private _tour!: TourEngine; private static readonly TOUR_STEPS = [ { target: '#create-notebook', title: "Create a Notebook", message: "Notebooks organise your notes by topic. Click '+ New Notebook' to create one.", advanceOnClick: true }, - { target: '#create-note', title: "Create a Note", message: "Inside a notebook you can add notes, links, tasks, and more. Click '+ New Note' to add one.", advanceOnClick: true }, + { target: '.sbt-nb-add', title: "Create a Note", message: "Each notebook has a '+' button to add new notes. Click it to create one.", advanceOnClick: true }, { target: '#editor-toolbar', title: "Editor Toolbar", message: "Format text with the toolbar — bold, lists, code blocks, headings, and more. Click Next to continue.", advanceOnClick: false }, { target: '[data-cmd="mic"]', title: "Voice Notes", message: "Record voice notes with live transcription. Your words appear as you speak — no uploads needed.", advanceOnClick: false }, ]; @@ -260,16 +260,50 @@ class FolkNotesApp extends HTMLElement { private setupShadow() { const style = document.createElement('style'); style.textContent = this.getStyles(); + + const layout = document.createElement('div'); + layout.id = 'notes-layout'; + this.navZone = document.createElement('div'); this.navZone.id = 'nav-zone'; + + const rightCol = document.createElement('div'); + rightCol.className = 'notes-right-col'; + this.contentZone = document.createElement('div'); this.contentZone.id = 'content-zone'; this.metaZone = document.createElement('div'); this.metaZone.id = 'meta-zone'; + + rightCol.appendChild(this.contentZone); + rightCol.appendChild(this.metaZone); + + layout.appendChild(this.navZone); + layout.appendChild(rightCol); + this.shadow.appendChild(style); - this.shadow.appendChild(this.navZone); - this.shadow.appendChild(this.contentZone); - this.shadow.appendChild(this.metaZone); + this.shadow.appendChild(layout); + + // Mobile sidebar toggle + const mobileToggle = document.createElement('button'); + mobileToggle.className = 'mobile-sidebar-toggle'; + mobileToggle.innerHTML = '\u2630'; + mobileToggle.addEventListener('click', () => { + this.sidebarOpen = !this.sidebarOpen; + this.navZone.querySelector('.notes-sidebar')?.classList.toggle('open', this.sidebarOpen); + this.shadow.querySelector('.sidebar-overlay')?.classList.toggle('open', this.sidebarOpen); + }); + this.shadow.appendChild(mobileToggle); + + // Mobile overlay + const overlay = document.createElement('div'); + overlay.className = 'sidebar-overlay'; + overlay.addEventListener('click', () => { + this.sidebarOpen = false; + this.navZone.querySelector('.notes-sidebar')?.classList.remove('open'); + overlay.classList.remove('open'); + }); + this.shadow.appendChild(overlay); } // ── Demo data ── @@ -429,6 +463,13 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF ]; this.notebooks = this.demoNotebooks.map(({ notes, ...nb }) => nb as Notebook); + // Populate sidebar note cache and expand first notebook + for (const nb of this.demoNotebooks) { + this.notebookNotes.set(nb.id, nb.notes); + } + if (this.demoNotebooks.length > 0) { + this.expandedNotebooks.add(this.demoNotebooks[0].id); + } this.loading = false; this.render(); } diff --git a/modules/rsocials/components/campaign-wizard.css b/modules/rsocials/components/campaign-wizard.css index 9ce55c8..437b7e5 100644 --- a/modules/rsocials/components/campaign-wizard.css +++ b/modules/rsocials/components/campaign-wizard.css @@ -401,6 +401,126 @@ background: rgba(20, 184, 166, 0.1); } +/* ── Platform selector chips ── */ +.cw-platform-selector { + margin-top: 1rem; +} + +.cw-platform-selector__label { + font-size: 0.8rem; + color: var(--rs-text-muted, #64748b); + display: block; + margin-bottom: 0.5rem; +} + +.cw-platform-selector__chips { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.cw-platform-chip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.35rem 0.7rem; + border-radius: 20px; + border: 1px solid var(--rs-border, #444); + background: transparent; + color: var(--rs-text-muted, #64748b); + font-size: 0.8rem; + font-family: inherit; + cursor: pointer; + transition: all 0.15s; + text-transform: capitalize; +} + +.cw-platform-chip:hover { + border-color: var(--rs-accent, #14b8a6); + color: var(--rs-text-primary, #e1e1e1); +} + +.cw-platform-chip--active { + background: rgba(20, 184, 166, 0.15); + border-color: var(--rs-accent, #14b8a6); + color: var(--rs-accent, #14b8a6); +} + +/* ── Clickable phase card headers ── */ +.cw-phase-card__header--clickable { + cursor: pointer; + user-select: none; + border-radius: 6px; + transition: background 0.15s; +} + +.cw-phase-card__header--clickable:hover { + background: rgba(255, 255, 255, 0.03); +} + +/* ── Phase chevron ── */ +.cw-phase-chevron { + display: inline-block; + font-size: 0.65rem; + color: var(--rs-text-muted, #64748b); + transition: transform 0.2s ease; +} + +.cw-phase-chevron--open { + transform: rotate(90deg); +} + +/* ── Phase expanded preview ── */ +.cw-phase-card__preview { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--rs-border, #333); + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.cw-phase-preview-post { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + color: var(--rs-text-secondary, #94a3b8); +} + +.cw-phase-preview-post__spread { + color: var(--rs-text-muted, #64748b); + font-size: 0.75rem; + margin-left: auto; +} + +/* ── Phase instructions textarea ── */ +.cw-phase-instructions { + width: 100%; + min-height: 56px; + padding: 0.5rem 0.75rem; + margin-top: 0.5rem; + border: 1px solid var(--rs-input-border, #334155); + border-radius: 6px; + background: var(--rs-bg-surface, #1e1e2e); + color: var(--rs-text-primary, #e1e1e1); + font-family: inherit; + font-size: 0.8rem; + line-height: 1.5; + resize: vertical; + box-sizing: border-box; +} + +.cw-phase-instructions:focus { + outline: none; + border-color: var(--rs-accent, #14b8a6); +} + +.cw-phase-instructions::placeholder { + color: var(--rs-text-muted, #64748b); + font-style: italic; +} + /* ── Responsive ── */ @media (max-width: 640px) { .cw-brief-summary { diff --git a/modules/rsocials/components/folk-campaign-wizard.ts b/modules/rsocials/components/folk-campaign-wizard.ts index 284b682..c94c5f6 100644 --- a/modules/rsocials/components/folk-campaign-wizard.ts +++ b/modules/rsocials/components/folk-campaign-wizard.ts @@ -101,6 +101,9 @@ export class FolkCampaignWizard extends HTMLElement { private _expandedPosts: Set = new Set(); private _regenIndex = -1; private _regenInstructions = ''; + private _selectedPlatforms: Set = new Set(['x', 'linkedin', 'instagram', 'youtube', 'threads', 'bluesky', 'newsletter']); + private _expandedPhases: Set = new Set(); + private _phaseInstructions: Map = new Map(); static get observedAttributes() { return ['space', 'wizard-id']; } @@ -155,6 +158,11 @@ export class FolkCampaignWizard extends HTMLElement { this._campaignDraft = data.campaignDraft; this._committedCampaignId = data.committedCampaignId; + // Sync selected platforms from extracted brief + if (data.extractedBrief?.platforms?.length) { + this._selectedPlatforms = new Set(data.extractedBrief.platforms); + } + if (data.step === 'committed') { this._step = 'activate'; } else if (data.step === 'abandoned') { @@ -212,7 +220,7 @@ export class FolkCampaignWizard extends HTMLElement { try { const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/structure`, { method: 'POST', - body: JSON.stringify({ rawBrief: this._rawBrief }), + body: JSON.stringify({ rawBrief: this._rawBrief, selectedPlatforms: Array.from(this._selectedPlatforms) }), }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Analysis failed'); const data: WizardState = await res.json(); @@ -232,9 +240,11 @@ export class FolkCampaignWizard extends HTMLElement { this.render(); try { + const phaseInstructions: Record = {}; + this._phaseInstructions.forEach((v, k) => { if (v.trim()) phaseInstructions[k] = v; }); const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/content`, { method: 'POST', - body: JSON.stringify({}), + body: JSON.stringify({ phaseInstructions }), }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Generation failed'); const data: WizardState = await res.json(); @@ -367,10 +377,20 @@ export class FolkCampaignWizard extends HTMLElement { } private renderBriefStep(): string { + const allPlatforms = ['x', 'linkedin', 'instagram', 'youtube', 'threads', 'bluesky', 'newsletter']; + const platformChips = allPlatforms.map(p => { + const active = this._selectedPlatforms.has(p); + return ``; + }).join(''); + return `

Step 1: Paste Your Campaign Brief

Paste raw text describing your event, product launch, or campaign goals. The AI will extract key details and propose a structure.

+
+ Include platforms: +
${platformChips}
+
@@ -394,19 +414,48 @@ export class FolkCampaignWizard extends HTMLElement {
`; - const phases = structure.phases.map(p => ` + const phases = structure.phases.map((p, pi) => { + const isExpanded = this._expandedPhases.has(pi); + const chevronCls = isExpanded ? 'cw-phase-chevron cw-phase-chevron--open' : 'cw-phase-chevron'; + const instructions = this._phaseInstructions.get(pi) || ''; + + // Infer post type per platform + const postTypeMap: Record = { + x: 'thread', linkedin: 'text', instagram: 'carousel', + youtube: 'video', threads: 'thread', bluesky: 'text', newsletter: 'email', + }; + + const postStubs = Object.entries(p.cadence || {}).map(([plat, count]) => + `
+ ${PLATFORM_ICONS[plat] || ''} ${plat} + ${count} ${postTypeMap[plat] || 'text'} post${count !== 1 ? 's' : ''} + spread over ${this.escHtml(p.days)} +
` + ).join(''); + + return `
-
+
${this.escHtml(p.label)} - ${this.escHtml(p.days)} + + ${this.escHtml(p.days)} + \u25B6 +
${Object.entries(p.cadence || {}).map(([plat, count]) => `${PLATFORM_ICONS[plat] || ''} ${plat}: ${count} post${count !== 1 ? 's' : ''}` ).join('')}
+ ${isExpanded ? ` +
+ ${postStubs} + +
+ ` : ''}
- `).join(''); + `; + }).join(''); return `
@@ -567,6 +616,21 @@ export class FolkCampaignWizard extends HTMLElement { }); } + // Platform chip toggles + sr.querySelectorAll('[data-platform]').forEach(el => { + el.addEventListener('click', () => { + const plat = el.getAttribute('data-platform')!; + if (this._selectedPlatforms.has(plat)) { + if (this._selectedPlatforms.size > 1) { + this._selectedPlatforms.delete(plat); + } + } else { + this._selectedPlatforms.add(plat); + } + this.render(); + }); + }); + sr.querySelector('#analyze-btn')?.addEventListener('click', () => this.analyzebrief()); sr.querySelector('#mi-btn')?.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('mi-prompt', { @@ -575,7 +639,24 @@ export class FolkCampaignWizard extends HTMLElement { })); }); - // Structure step + // Structure step — phase card expand/collapse + sr.querySelectorAll('[data-phase-toggle]').forEach(el => { + el.addEventListener('click', () => { + const pi = parseInt(el.getAttribute('data-phase-toggle')!); + if (this._expandedPhases.has(pi)) this._expandedPhases.delete(pi); + else this._expandedPhases.add(pi); + this.render(); + }); + }); + sr.querySelectorAll('[data-phase-instructions]').forEach(el => { + const ta = el as HTMLTextAreaElement; + ta.addEventListener('click', (e) => e.stopPropagation()); + ta.addEventListener('input', () => { + const pi = parseInt(ta.getAttribute('data-phase-instructions')!); + this._phaseInstructions.set(pi, ta.value); + }); + }); + sr.querySelector('#approve-structure-btn')?.addEventListener('click', () => this.generateContent()); sr.querySelector('#regen-structure-btn')?.addEventListener('click', () => this.analyzebrief()); sr.querySelector('#back-to-brief-btn')?.addEventListener('click', () => { this._step = 'brief'; this.render(); }); diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index 7cec291..ccf224c 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -1241,6 +1241,13 @@ routes.post("/api/campaign/wizard/:id/structure", async (c) => { return c.json({ error: "Brief is required (min 10 characters)" }, 400); } + const selectedPlatforms: string[] = Array.isArray(body.selectedPlatforms) && body.selectedPlatforms.length > 0 + ? body.selectedPlatforms + : null; + const platformConstraint = selectedPlatforms + ? `\n- ONLY use these platforms: ${selectedPlatforms.join(', ')}. Do NOT include any other platforms.` + : ''; + const { GoogleGenerativeAI } = await import("@google/generative-ai"); const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ @@ -1284,7 +1291,7 @@ Return JSON with this exact shape: Rules: - Generate 3-5 phases (pre-launch, launch, amplification, follow-up, etc.) - Cadence = number of posts per platform for that phase -- Platforms should be realistic for the brief content +- Platforms should be realistic for the brief content${platformConstraint} - Use today's date (${new Date().toISOString().split('T')[0]}) as reference for dates`; try { @@ -1292,6 +1299,25 @@ Rules: const text = result.response.text(); const parsed = JSON.parse(text); + // Safety net: filter platforms and cadences to only selected platforms + if (selectedPlatforms && parsed.extractedBrief) { + parsed.extractedBrief.platforms = (parsed.extractedBrief.platforms || []) + .filter((p: string) => selectedPlatforms.includes(p)); + if (parsed.extractedBrief.platforms.length === 0) { + parsed.extractedBrief.platforms = selectedPlatforms; + } + } + if (selectedPlatforms && parsed.structure?.phases) { + for (const phase of parsed.structure.phases) { + phase.platforms = (phase.platforms || []).filter((p: string) => selectedPlatforms.includes(p)); + if (phase.cadence) { + for (const key of Object.keys(phase.cadence)) { + if (!selectedPlatforms.includes(key)) delete phase.cadence[key]; + } + } + } + } + _syncServer!.changeDoc(docId, `wizard ${id} → structure`, (d) => { const w = d.campaignWizards?.[id]; if (!w) return; @@ -1325,10 +1351,19 @@ routes.post("/api/campaign/wizard/:id/content", async (c) => { return c.json({ error: "Must complete structure step first" }, 400); } + const body = await c.req.json().catch(() => ({})); + const phaseInstructions: Record = body.phaseInstructions || {}; + const brief = wizard.extractedBrief; const structure = wizard.structure; const selectedPlatforms = brief.platforms.length > 0 ? brief.platforms : ["x", "linkedin", "instagram", "newsletter"]; + // Build phase-specific instruction hints + const phaseHints = structure.phases.map((p, i) => { + const hint = phaseInstructions[String(i)]; + return hint ? `- Phase ${i + 1} (${p.label}): ${hint}` : ''; + }).filter(Boolean).join('\n'); + const { GoogleGenerativeAI } = await import("@google/generative-ai"); const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ @@ -1391,7 +1426,7 @@ Rules: - For newsletter, include emailSubject + emailHtml with inline CSS - scheduledAt dates should spread across the phase day ranges, during working hours (9am-5pm) - Content must reference specific details from the brief key messages -- Respect each platform's character limits`; +- Respect each platform's character limits${phaseHints ? `\n\nPhase-specific instructions from the user:\n${phaseHints}` : ''}`; try { const result = await model.generateContent(prompt); diff --git a/package.json b/package.json index f89b735..19fc666 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@google/genai": "^1.43.0", "@google/generative-ai": "^0.24.1", "@lit/reactive-element": "^2.0.4", + "@modelcontextprotocol/sdk": "^1.27.1", "@noble/curves": "^1.8.0", "@noble/hashes": "^1.7.0", "@openfort/openfort-node": "^0.7.0", diff --git a/server/cad-orchestrator.ts b/server/cad-orchestrator.ts new file mode 100644 index 0000000..d185940 --- /dev/null +++ b/server/cad-orchestrator.ts @@ -0,0 +1,324 @@ +/** + * CAD Orchestrator — LLM-driven MCP tool calling for KiCad and FreeCAD + * + * Converts MCP tool schemas to Gemini function declarations, runs an agentic + * loop where Gemini Flash plans and executes real MCP tool sequences, then + * assembles artifacts (SVGs, exports) for the frontend. + */ + +import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import type { Tool } from "@modelcontextprotocol/sdk/types.js"; + +// ── MCP → Gemini schema conversion ── + +/** Strip keys that Gemini function-calling schema rejects */ +function cleanSchema(obj: any): any { + if (obj === null || obj === undefined) return obj; + if (Array.isArray(obj)) return obj.map(cleanSchema); + if (typeof obj !== "object") return obj; + + const out: any = {}; + for (const [k, v] of Object.entries(obj)) { + // Gemini rejects these in FunctionDeclaration parameters + if (["default", "additionalProperties", "$schema", "examples", "title"].includes(k)) continue; + out[k] = cleanSchema(v); + } + return out; +} + +/** Convert a single MCP Tool to a Gemini FunctionDeclaration */ +export function mcpToolToGeminiFn(tool: Tool): any { + const params = tool.inputSchema ? cleanSchema(tool.inputSchema) : { type: "object", properties: {} }; + // Gemini requires type:"OBJECT" (uppercase) in some SDK versions, but @google/generative-ai handles lowercase + return { + name: tool.name, + description: tool.description || tool.name, + parameters: params, + }; +} + +// ── Extract text from MCP CallToolResult ── + +export function extractMcpText(result: any): string { + const content = result?.content; + if (!Array.isArray(content)) return JSON.stringify(result); + return content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text || "") + .join("\n"); +} + +// ── Agentic loop ── + +export interface ToolCallLogEntry { + tool: string; + args: Record; + result: string; +} + +export interface OrchestrationResult { + artifacts: Map; + finalMessage: string; + toolCallLog: ToolCallLogEntry[]; +} + +export async function runCadAgentLoop( + client: Client, + systemPrompt: string, + userPrompt: string, + geminiApiKey: string, + maxTurns = 8, +): Promise { + // 1. Fetch real tool schemas from MCP server + const { tools } = await client.listTools(); + const functionDeclarations = tools.map(mcpToolToGeminiFn); + + // 2. Initialize Gemini Flash with function calling + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(geminiApiKey); + const model = genAI.getGenerativeModel({ + model: "gemini-2.5-flash", + systemInstruction: systemPrompt, + generationConfig: { temperature: 0.2 }, + tools: [{ functionDeclarations }], + }); + + // 3. Agentic loop + const artifacts = new Map(); + const toolCallLog: ToolCallLogEntry[] = []; + const deadline = Date.now() + 60_000; + + let contents: any[] = [ + { role: "user", parts: [{ text: userPrompt }] }, + ]; + + for (let turn = 0; turn < maxTurns; turn++) { + if (Date.now() > deadline) { + console.warn("[cad-orchestrator] 60s deadline reached"); + break; + } + + const result = await model.generateContent({ contents }); + const candidate = result.response.candidates?.[0]; + if (!candidate) break; + + const parts = candidate.content?.parts || []; + const fnCalls = parts.filter((p: any) => p.functionCall); + + if (fnCalls.length === 0) { + // Done — extract final text + const finalText = parts + .filter((p: any) => p.text) + .map((p: any) => p.text) + .join("\n"); + return { artifacts, finalMessage: finalText || "Design complete.", toolCallLog }; + } + + // Execute each function call on MCP + const fnResponseParts: any[] = []; + for (const part of fnCalls) { + const fc = part.functionCall!; + console.log(`[cad-orchestrator] Turn ${turn + 1}: ${fc.name}(${JSON.stringify(fc.args).slice(0, 200)})`); + + let mcpResultText: string; + try { + const mcpResult = await client.callTool({ + name: fc.name, + arguments: (fc.args || {}) as Record, + }); + mcpResultText = extractMcpText(mcpResult); + } catch (err) { + mcpResultText = `Error: ${err instanceof Error ? err.message : String(err)}`; + } + + toolCallLog.push({ tool: fc.name, args: fc.args || {}, result: mcpResultText }); + artifacts.set(fc.name, mcpResultText); + + fnResponseParts.push({ + functionResponse: { + name: fc.name, + response: { result: mcpResultText }, + }, + }); + } + + // Feed results back for next turn + contents.push({ role: "model", parts }); + contents.push({ role: "user", parts: fnResponseParts }); + } + + return { + artifacts, + finalMessage: "Design generation completed (max turns reached).", + toolCallLog, + }; +} + +// ── Result assemblers ── + +export interface KicadResult { + schematicSvg: string | null; + boardSvg: string | null; + gerberUrl: string | null; + bomUrl: string | null; + pdfUrl: string | null; + drcResults: { violations: string[] } | null; + summary: string; + toolCallLog: ToolCallLogEntry[]; +} + +export function assembleKicadResult(orch: OrchestrationResult): KicadResult { + let schematicSvg: string | null = null; + let boardSvg: string | null = null; + let gerberUrl: string | null = null; + let bomUrl: string | null = null; + let pdfUrl: string | null = null; + let drcResults: { violations: string[] } | null = null; + + for (const entry of orch.toolCallLog) { + try { + const parsed = JSON.parse(entry.result); + switch (entry.tool) { + case "export_svg": + // Could be schematic or board SVG — check args or content + if (entry.args.type === "board" || entry.args.board) { + boardSvg = parsed.svg_path || parsed.path || parsed.url || null; + } else { + schematicSvg = parsed.svg_path || parsed.path || parsed.url || null; + } + break; + case "run_drc": + drcResults = { + violations: parsed.violations || parsed.errors || [], + }; + break; + case "export_gerber": + gerberUrl = parsed.gerber_path || parsed.path || parsed.url || null; + break; + case "export_bom": + bomUrl = parsed.bom_path || parsed.path || parsed.url || null; + break; + case "export_pdf": + pdfUrl = parsed.pdf_path || parsed.path || parsed.url || null; + break; + } + } catch { + // Non-JSON results are fine (intermediate steps) + } + } + + return { + schematicSvg, + boardSvg, + gerberUrl, + bomUrl, + pdfUrl, + drcResults, + summary: orch.finalMessage, + toolCallLog: orch.toolCallLog, + }; +} + +export interface FreecadResult { + previewUrl: string | null; + stepUrl: string | null; + stlUrl: string | null; + summary: string; + toolCallLog: ToolCallLogEntry[]; +} + +export function assembleFreecadResult(orch: OrchestrationResult): FreecadResult { + let previewUrl: string | null = null; + let stepUrl: string | null = null; + let stlUrl: string | null = null; + + for (const entry of orch.toolCallLog) { + try { + const parsed = JSON.parse(entry.result); + // FreeCAD exports via execute_python_script — look for file paths in results + if (entry.tool === "execute_python_script" || entry.tool === "execute_script") { + const text = entry.result.toLowerCase(); + if (text.includes(".step") || text.includes(".stp")) { + stepUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".step", ".stp"]); + } + if (text.includes(".stl")) { + stlUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".stl"]); + } + } + // save_document may also produce a path + if (entry.tool === "save_document") { + const path = parsed.path || parsed.file_path || null; + if (path && (path.endsWith(".FCStd") || path.endsWith(".fcstd"))) { + // Not directly servable, but note it + } + } + } catch { + // Try extracting paths from raw text + stepUrl = stepUrl || extractPathFromText(entry.result, [".step", ".stp"]); + stlUrl = stlUrl || extractPathFromText(entry.result, [".stl"]); + } + } + + return { + previewUrl, + stepUrl, + stlUrl, + summary: orch.finalMessage, + toolCallLog: orch.toolCallLog, + }; +} + +/** Extract a file path ending with one of the given extensions from text */ +function extractPathFromText(text: string, extensions: string[]): string | null { + for (const ext of extensions) { + const regex = new RegExp(`(/[\\w./-]+${ext.replace(".", "\\.")})`, "i"); + const match = text.match(regex); + if (match) return match[1]; + } + return null; +} + +// ── System prompts ── + +export const KICAD_SYSTEM_PROMPT = `You are a KiCad PCB design assistant. You have access to KiCad MCP tools to create real PCB designs. + +Follow this workflow: +1. create_project — Create a new KiCad project in /tmp/kicad-gen-/ +2. search_symbols — Find component symbols in KiCad libraries (e.g. ESP32, BME280, capacitors, resistors) +3. add_schematic_component — Place each component on the schematic +4. add_schematic_net_label — Add net labels for connections +5. add_schematic_connection — Wire components together +6. generate_netlist — Generate the netlist from schematic +7. place_component — Place footprints on the board +8. route_trace — Route traces between pads +9. export_svg — Export schematic SVG (type: "schematic") and board SVG (type: "board") +10. run_drc — Run Design Rule Check +11. export_gerber, export_bom, export_pdf — Generate manufacturing outputs + +Important: +- Use /tmp/kicad-gen-${Date.now()}/ as the project directory +- Search for real symbols before placing components +- Add decoupling capacitors and pull-up resistors as needed +- Set reasonable board outline dimensions +- After placing components, route critical traces +- Always run DRC before exporting +- If a tool call fails, try an alternative approach rather than repeating the same call`; + +export const FREECAD_SYSTEM_PROMPT = `You are a FreeCAD parametric CAD assistant. You have access to FreeCAD MCP tools to create real 3D models. + +Follow this workflow: +1. execute_python_script — Create output directory: import os; os.makedirs("/tmp/freecad-gen-", exist_ok=True) +2. Create base geometry using create_box, create_cylinder, or create_sphere +3. Use boolean_operation (union, cut, intersection) to combine shapes +4. list_objects to verify the model state +5. save_document to save the FreeCAD file +6. execute_python_script to export STEP: Part.export([obj], "/tmp/freecad-gen-/model.step") +7. execute_python_script to export STL: Mesh.export([obj], "/tmp/freecad-gen-/model.stl") + +Important: +- Use /tmp/freecad-gen-${Date.now()}/ as the working directory +- For hollow objects, create the outer shell then cut the inner volume +- For complex shapes, build up from primitives with boolean operations +- Wall thickness should be at least 1mm for 3D printing +- Always export both STEP (for CAD) and STL (for 3D printing) +- If a tool call fails, try an alternative approach`; diff --git a/server/index.ts b/server/index.ts index 4987d8a..bf6032f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1144,6 +1144,20 @@ async function process3DGenJob(job: Gen3DJob) { // ── Image helpers ── +/** Copy a file from a tmp path to the served generated directory → return server-relative URL */ +async function copyToServed(srcPath: string): Promise { + try { + const srcFile = Bun.file(srcPath); + if (!(await srcFile.exists())) return null; + const basename = srcPath.split("/").pop() || `file-${Date.now()}`; + const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); + await Bun.write(resolve(dir, basename), srcFile); + return `/data/files/generated/${basename}`; + } catch { + return null; + } +} + /** Read a /data/files/generated/... path from disk → base64 */ async function readFileAsBase64(serverPath: string): Promise { const filename = serverPath.split("/").pop(); @@ -1606,6 +1620,19 @@ app.get("/api/3d-gen/:jobId", async (c) => { // Blender 3D generation via LLM + RunPod const RUNPOD_API_KEY = process.env.RUNPOD_API_KEY || ""; +app.get("/api/blender-gen/health", async (c) => { + const issues: string[] = []; + if (!RUNPOD_API_KEY) issues.push("RUNPOD_API_KEY not configured"); + const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; + try { + const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) }); + if (!res.ok) issues.push("Ollama not responding"); + } catch { + issues.push("Ollama unreachable"); + } + return c.json({ available: issues.length === 0, issues }); +}); + app.post("/api/blender-gen", async (c) => { if (!RUNPOD_API_KEY) return c.json({ error: "RUNPOD_API_KEY not configured" }, 503); @@ -1675,8 +1702,89 @@ app.post("/api/blender-gen", async (c) => { } }); -// KiCAD PCB design — REST-to-MCP bridge -const KICAD_MCP_URL = process.env.KICAD_MCP_URL || "http://localhost:3001"; +// KiCAD PCB design — MCP stdio bridge +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { runCadAgentLoop, assembleKicadResult, assembleFreecadResult, KICAD_SYSTEM_PROMPT, FREECAD_SYSTEM_PROMPT } from "./cad-orchestrator"; + +const KICAD_MCP_PATH = process.env.KICAD_MCP_PATH || "/home/jeffe/KiCAD-MCP-Server/dist/index.js"; +let kicadClient: Client | null = null; + +async function getKicadClient(): Promise { + if (kicadClient) return kicadClient; + + const transport = new StdioClientTransport({ + command: "node", + args: [KICAD_MCP_PATH], + }); + + const client = new Client({ name: "rspace-kicad-bridge", version: "1.0.0" }); + + transport.onclose = () => { kicadClient = null; }; + transport.onerror = () => { kicadClient = null; }; + + await client.connect(transport); + kicadClient = client; + return client; +} + +app.get("/api/kicad/health", async (c) => { + try { + const client = await getKicadClient(); + const tools = await client.listTools(); + return c.json({ available: true, tools: tools.tools.length }); + } catch (e) { + return c.json({ available: false, error: e instanceof Error ? e.message : "Connection failed" }); + } +}); + +// KiCAD AI-orchestrated design generation +app.post("/api/kicad/generate", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { prompt, components } = await c.req.json(); + if (!prompt) return c.json({ error: "prompt required" }, 400); + + const enrichedPrompt = components?.length + ? `${prompt}\n\nKey components to use: ${components.join(", ")}` + : prompt; + + try { + const client = await getKicadClient(); + const orch = await runCadAgentLoop(client, KICAD_SYSTEM_PROMPT, enrichedPrompt, GEMINI_API_KEY); + const result = assembleKicadResult(orch); + + // Copy generated files to served directory + const filesToCopy = [ + { path: result.schematicSvg, key: "schematicSvg" }, + { path: result.boardSvg, key: "boardSvg" }, + { path: result.gerberUrl, key: "gerberUrl" }, + { path: result.bomUrl, key: "bomUrl" }, + { path: result.pdfUrl, key: "pdfUrl" }, + ]; + + for (const { path, key } of filesToCopy) { + if (path && path.startsWith("/tmp/")) { + const served = await copyToServed(path); + if (served) (result as any)[key] = served; + } + } + + return c.json({ + schematic_svg: result.schematicSvg, + board_svg: result.boardSvg, + gerber_url: result.gerberUrl, + bom_url: result.bomUrl, + pdf_url: result.pdfUrl, + drc: result.drcResults, + summary: result.summary, + }); + } catch (e) { + console.error("[kicad/generate] error:", e); + kicadClient = null; + return c.json({ error: e instanceof Error ? e.message : "KiCAD generation failed" }, 502); + } +}); app.post("/api/kicad/:action", async (c) => { const action = c.req.param("action"); @@ -1693,38 +1801,102 @@ app.post("/api/kicad/:action", async (c) => { } try { - const mcpRes = await fetch(`${KICAD_MCP_URL}/call-tool`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: action, - arguments: body, - }), - }); + const client = await getKicadClient(); + const result = await client.callTool({ name: action, arguments: body }); - if (!mcpRes.ok) { - const err = await mcpRes.text(); - console.error(`[kicad/${action}] MCP error:`, err); - return c.json({ error: `KiCAD action failed: ${action}` }, 502); + // Extract text content and parse as JSON if possible + const content = result.content as Array<{ type: string; text?: string }>; + const textContent = content + ?.filter((c) => c.type === "text") + .map((c) => c.text) + .join("\n"); + + try { + return c.json(JSON.parse(textContent || "{}")); + } catch { + return c.json({ result: textContent }); } - - const data = await mcpRes.json(); - return c.json(data); } catch (e) { - console.error(`[kicad/${action}] Connection error:`, e); + console.error(`[kicad/${action}] MCP error:`, e); + kicadClient = null; return c.json({ error: "KiCAD MCP server not available" }, 503); } }); -// FreeCAD parametric CAD — REST-to-MCP bridge -const FREECAD_MCP_URL = process.env.FREECAD_MCP_URL || "http://localhost:3002"; +// FreeCAD parametric CAD — MCP stdio bridge +const FREECAD_MCP_PATH = process.env.FREECAD_MCP_PATH || "/home/jeffe/freecad-mcp-server/build/index.js"; +let freecadClient: Client | null = null; + +async function getFreecadClient(): Promise { + if (freecadClient) return freecadClient; + + const transport = new StdioClientTransport({ + command: "node", + args: [FREECAD_MCP_PATH], + }); + + const client = new Client({ name: "rspace-freecad-bridge", version: "1.0.0" }); + + transport.onclose = () => { freecadClient = null; }; + transport.onerror = () => { freecadClient = null; }; + + await client.connect(transport); + freecadClient = client; + return client; +} + +app.get("/api/freecad/health", async (c) => { + try { + const client = await getFreecadClient(); + const tools = await client.listTools(); + return c.json({ available: true, tools: tools.tools.length }); + } catch (e) { + return c.json({ available: false, error: e instanceof Error ? e.message : "Connection failed" }); + } +}); + +// FreeCAD AI-orchestrated CAD generation +app.post("/api/freecad/generate", async (c) => { + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { prompt } = await c.req.json(); + if (!prompt) return c.json({ error: "prompt required" }, 400); + + try { + const client = await getFreecadClient(); + const orch = await runCadAgentLoop(client, FREECAD_SYSTEM_PROMPT, prompt, GEMINI_API_KEY); + const result = assembleFreecadResult(orch); + + // Copy generated files to served directory + for (const key of ["stepUrl", "stlUrl"] as const) { + const path = result[key]; + if (path && path.startsWith("/tmp/")) { + const served = await copyToServed(path); + if (served) (result as any)[key] = served; + } + } + + return c.json({ + preview_url: result.previewUrl, + step_url: result.stepUrl, + stl_url: result.stlUrl, + summary: result.summary, + }); + } catch (e) { + console.error("[freecad/generate] error:", e); + freecadClient = null; + return c.json({ error: e instanceof Error ? e.message : "FreeCAD generation failed" }, 502); + } +}); app.post("/api/freecad/:action", async (c) => { const action = c.req.param("action"); const body = await c.req.json(); const validActions = [ - "generate", "export_step", "export_stl", "update_parameters", + "generate", "create_box", "create_cylinder", "create_sphere", + "boolean_operation", "save_document", "list_objects", "execute_script", + "export_step", "export_stl", "update_parameters", ]; if (!validActions.includes(action)) { @@ -1732,25 +1904,23 @@ app.post("/api/freecad/:action", async (c) => { } try { - const mcpRes = await fetch(`${FREECAD_MCP_URL}/call-tool`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: action, - arguments: body, - }), - }); + const client = await getFreecadClient(); + const result = await client.callTool({ name: action, arguments: body }); - if (!mcpRes.ok) { - const err = await mcpRes.text(); - console.error(`[freecad/${action}] MCP error:`, err); - return c.json({ error: `FreeCAD action failed: ${action}` }, 502); + const fcContent = result.content as Array<{ type: string; text?: string }>; + const textContent = fcContent + ?.filter((c) => c.type === "text") + .map((c) => c.text) + .join("\n"); + + try { + return c.json(JSON.parse(textContent || "{}")); + } catch { + return c.json({ result: textContent }); } - - const data = await mcpRes.json(); - return c.json(data); } catch (e) { - console.error(`[freecad/${action}] Connection error:`, e); + console.error(`[freecad/${action}] MCP error:`, e); + freecadClient = null; return c.json({ error: "FreeCAD MCP server not available" }, 503); } });