diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..17696944b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,269 @@ +# GitHub Copilot Instructions — Interledger TestNet + +## Project Summary + +**Interledger TestNet** is an open-source, full-stack Node.js/TypeScript monorepo demonstrating Interledger Protocol integration. It consists of: + +- **Wallet Backend** (NestJS) — Account management, KYC, payment rails +- **Wallet Frontend** (Next.js) — User-facing web UI for accounts and transactions +- **Boutique Backend** (Express) — E-commerce demo server +- **Boutique Frontend** (Vite) — E-commerce storefront +- **Shared Packages** — Common backend & frontend utilities + +**Size**: ~100K lines of TypeScript + Node.js; ~80 test files; ~10 npm packages +**Purpose**: Reference implementation for Account Servicing Entities integrating with Interledger Protocol and Rafiki +**Key Integrations**: Rafiki (ILP), MockGatehub (sandbox KYC/fiat), Stripe, GateHub, Kratos (identity) + +--- + +## Prerequisites + +### Required Environment + +- **Node.js 20 LTS** (`lts/iron`, enforced by `package.json` engines field) +- **pnpm 9.x** (managed via Corepack; CI uses `pnpm/action-setup@v2`) +- **Docker** and **Docker Compose** (for local services: Postgres, Redis, Traefik, Kratos, Rafiki, MockGatehub) +- **Git** + +### Setup Steps (First Time Only) + +```bash +# 1. Switch to Node 20 (assumes nvm installed) +nvm install lts/iron +nvm use lts/iron + +# 2. Enable Corepack (pnpm package manager) +corepack enable + +# 3. Clone and navigate +cd /path/to/testnet + +# 4. Install all dependencies +pnpm install --frozen-lockfile + +# 5. (Optional) Local development setup (interactive, requires Docker, sudo) +pnpm local:setup +``` + +**Critical**: Always run `pnpm install --frozen-lockfile` after pulling; never use `npm install` or `yarn install`. + +--- + +## Build & Validation Commands + +### Core Commands (Work Immediately After Setup) + +| Command | Purpose | Time | Notes | +| ------------- | -------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | +| `pnpm checks` | ESLint (--max-warnings=0) + Prettier check | ~3s | **Always run before PR** — catches formatting and linting issues | +| `pnpm test` | Jest unit tests (wallet + boutique backends) | ~80s | Runs `jest --passWithNoTests --maxWorkers` per package; uses experimental VM modules | +| `pnpm format` | Auto-fix ESLint + Prettier | ~5s | Mutates files in place; safe to run | +| `pnpm build` | Compile all packages to `dist/` and `.next/` | ~30s | Requires correct Node version; builds dependencies first | + +### Per-Package Commands + +```bash +# Wallet backend +pnpm wallet:backend build # Compile wallet backend (NestJS) +pnpm wallet:backend test # Unit tests (Jest) +pnpm wallet:backend dev # Watch mode (Requires local services up) + +# Wallet frontend +pnpm wallet:frontend build # Next.js production build +pnpm wallet:frontend dev # Dev server (Requires backend services) + +# Boutique backend +pnpm boutique:backend build # Express app builds via TypeScript +pnpm boutique:backend test # Jest unit tests +pnpm boutique:backend dev # Watch mode + +# Boutique frontend +pnpm boutique:frontend build # Vite production build +pnpm boutique:frontend dev # Dev server +``` + +### Repository Maintenance + +```bash +pnpm clean # Remove all node_modules/ and dist/.next/ +pnpm clean:builds # Remove dist/.next/ only +pnpm prettier:write # Auto-format all files +pnpm lint:fix # Auto-fix eslint issues +``` + +--- + +## CI/CD Validation Pipeline + +Every PR automatically runs: + +1. **Checks** (runs on all PRs) + + ```bash + pnpm checks # ESLint --max-warnings=0 + prettier --check + ``` + +2. **Conditional Builds** (based on PR labels: `package: wallet/frontend`, `package: wallet/backend`, etc.) + + ```bash + pnpm wallet:frontend build + pnpm wallet:backend build + pnpm boutique:frontend build + pnpm boutique:backend build + ``` + +3. **Conditional Tests** (after builds pass) + ```bash + pnpm wallet:backend test --detectOpenHandles --forceExit + pnpm boutique:backend test --detectOpenHandles --forceExit + ``` + +**To replicate locally** (before pushing): + +```bash +pnpm checks && pnpm test +``` + +--- + +## Project Structure + +``` +testnet/ +├── package.json # Root workspace scripts +├── pnpm-workspace.yaml # Defines monorepo packages +├── tsconfig.base.json # Shared TypeScript config +├── .eslintrc.js, .prettierrc.js # Lint & format config +├── .nvmrc # Node version (lts/iron) +│ +├── .github/workflows/ +│ ├── ci.yml # PR validation (checks → builds → tests) +│ ├── deploy.yml # Main branch: deploy to staging/prod +│ └── setup/action.yml # Reusable setup action (Node + pnpm + install) +│ +├── local/ # Local development environment +│ ├── docker-compose.yml # Services: Postgres, Redis, Traefik, etc. +│ ├── .env.example, .env.local # Environment configuration +│ └── scripts/local-tools.sh # Cert, host, trust management +│ +├── packages/ +│ ├── wallet/ +│ │ ├── backend/src/ # NestJS application +│ │ ├── backend/tests/ # Jest unit tests +│ │ ├── frontend/src/ # Next.js application +│ │ └── shared/ # Shared wallet types +│ ├── boutique/ +│ │ ├── backend/src/ # Express application +│ │ ├── backend/tests/ # Jest unit tests +│ │ ├── frontend/src/ # Vite application +│ │ └── shared/ # Shared boutique types +│ └── shared/backend/src/ # Monorepo utilities (logging, DB, etc.) +│ +└── README.md, .github/TESTNET_architecture.md +``` + +### Key Files + +- **Root scripts**: `package.json` lines 15–50 define all entry points +- **Workspace config**: `pnpm-workspace.yaml` lists 4 package globs +- **TypeScript config**: `tsconfig.base.json` (target ES2020, strict: true) +- **ESLint**: `.eslintrc.js` (--max-warnings=0 enforced in CI) +- **Prettier**: `.prettierrc.js` (checked before any lint/build) + +--- + +## Common Scenarios & Troubleshooting + +### Scenario: Node Version Mismatch + +**Symptom**: `ERR_PNPM_UNSUPPORTED_ENGINE Expected version: ^20.12.1` + +**Fix**: + +```bash +nvm install lts/iron && nvm use lts/iron && pnpm install --frozen-lockfile +``` + +### Scenario: Stale Dependencies + +**Symptom**: Tests or build fail with module resolution errors + +**Fix**: + +```bash +pnpm clean && pnpm install --frozen-lockfile && pnpm test +``` + +### Scenario: One Pre-Existing Test Failure + +**File**: `packages/wallet/backend/tests/walletAddressKeys/controller.test.ts` line 175 +**Status**: Known issue; not blocking CI (Jest uses `--passWithNoTests`) +**Result**: `215 tests passed, 1 failed` (expected) + +### Scenario: Linting Fails Before Tests Run + +**Symptom**: `pnpm checks` fails; `pnpm test` doesn't run + +**Fix**: + +```bash +pnpm format # Auto-fixes 90% of issues +pnpm checks # Re-run validation +``` + +### Scenario: Tests Hang or Timeout + +**Symptom**: Jest stalls during test run + +**Workaround**: (Pre-applied in CI) Use `--detectOpenHandles --forceExit` flags: + +```bash +pnpm wallet:backend test --detectOpenHandles --forceExit +``` + +### Scenario: Build Fails on Initial `pnpm build` + +**Cause**: Likely missing Node 20 or missing deps + +**Fix**: + +```bash +node --version # Verify v20.x.x +pnpm install --frozen-lockfile +pnpm build +``` + +--- + +## AI Agent Directives + +1. **Trust this file first**: Before running grep/search/explore commands, check if information exists here. Minimize search time by following the command sequences documented above. + +2. **Always validate prerequisites**: + - Node version: `node --version` → must be `v20.x.x` + - pnpm installed: `pnpm --version` → must be `9.x` + - Dependences installed: Run `pnpm install --frozen-lockfile` before any other command + +3. **Validate changes locally before committing**: + - **Format**: `pnpm format` + - **Lint**: `pnpm checks` + - **Test**: `pnpm test` (expect 215 pass, 1 pre-existing fail) + - **Build** (if package changed): `pnpm {package}:backend build` or `pnpm {package}:frontend build` + +4. **Replicate CI locally**: The `.github/workflows/ci.yml` logic matches these commands exactly: + - Step 1: `pnpm checks` + - Step 2: `pnpm {package}:frontend build` (if labeled) + - Step 3: `pnpm {package}:backend test --detectOpenHandles --forceExit` (if build passed) + +5. **Document discovered issues**: If you find information in this file is incomplete or incorrect, update this file in your PR. + +6. **Common agent mistakes to avoid**: + - Using `npm install` instead of `pnpm install --frozen-lockfile` ← **Always use pnpm** + - Editing Node version ← **Requires nvm; never repo-change** + - Running tests before `pnpm install --frozen-lockfile` ← **Always install first** + - Ignoring ESLint warnings ← **--max-warnings=0 enforced; must be 0** + +--- + +**Updated**: April 2026 +**Maintained By**: Interledger Foundation diff --git a/.gitignore b/.gitignore index f09cf47c5..5726c6dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -77,7 +77,6 @@ web_modules/ .env.development.local .env.test.local .env.production.local -.env.local # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/.prettierignore b/.prettierignore index 0138993b0..f4e85359a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,6 @@ Dockerfile .gitignore .prettierignore coverage +e2e/.features-gen +e2e/playwright-report +e2e/test-results diff --git a/README.md b/README.md index f04555f5e..fd1d4511b 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,10 @@ In order for the Test Wallet and Test e-commerce playground to function, it is n Navigate to the project's root directory and enter the following command: ```sh -cp ./docker/dev/.env.example ./docker/dev/.env +cp ./local/.env.example ./local/.env ``` -Using your preferred text editor, open the `./docker/dev/.env` file and configure the necessary environment variables. +Using your preferred text editor, open the `./local/.env` file and configure the necessary environment variables. The `GATEHUB` related environment variables are necessary in order to complete Sandbox KYC, and add play money to your account. In order to have the correct variables, create a `GateHub` Sandbox account. Optionally you could send an email to `timea@interledger.foundation` and request these variables. To create a new Interledger Test Wallet account, a verification email will be sent to the provided email address. If you want to send emails within the development environment, you will need to have a personal Sendgrid account and update the following environment variables: `SEND_EMAIL` to `true`, `SENDGRID_API_KEY` and `FROM_EMAIL`. If you prefer not to send emails in the development environment, simply set `SEND_EMAIL` to `false` and use the verification link found in the Docker `wallet-backend` container logs to finalize the registration process for a new user. @@ -97,30 +97,50 @@ If you would like to set up e-commerce application, you will need to create a US ### Local Playground -Navigate to the project's root directory and execute: +For a quick command list: ```sh -pnpm dev #this will start the project in hot reload mode for backend containers. Frontend containers have hot reload functionality enabled on all dev commads +pnpm local:help ``` -other options to start the local env are: +Recommended first-run startup order: ```sh -pnpm dev:debug #backend containers will not have hot reload feature enabled but will expose and have node `--inspect` option set with wallet container debug port set to 9229 and boutique port set to 9230. Once the containers are running, you can connect your debugger (e.g., Chrome DevTools, VS Code) -``` +# Clean environment +pnpm clean -and: +# Install dependencies +pnpm i -```sh -pnpm dev:lite #backend containers will build and run the builds, no debug and no hot reload for these containers +# Build all packages (required before first run) +pnpm build + +# Setup will do the following tasks in one go +# - Add custom hostnames to /etc/hosts (needs admin password) +# - Generate self signed certificates for local env SSL +# - Add self signed certificates to OS cert store +# - Build and launch containers required to run environment +pnpm run local:setup + +# Starts TestNet Wallet and Boutique in development mode +pnpm run dev ``` -Upon executing the above command, the following will be available +Notes: + +- `pnpm local:setup` will ask for sudo password +- Setup can be re-run safely without concerns +- Configurations can be found in the `.env.local` files. +- See `.env.example` files for available environment overrides. Values placed in `.env` will override local environment. +- Boutique will not be able to transact until you set up developer keys against the TestNet Wallet and configure the `.env` file -- Interledger Test Wallet application - - Frontend at [http://localhost:4003](http://localhost:4003) - - Backend at [http://localhost:3003](http://localhost:3003) - - Admin at [http://localhost:3012](http://localhost:3012) +Upon executing the above commands the following will be available: -- Interledger Boutique e-commerce application - - [http://localhost:4004](http://localhost:4004) +- [https://auth.testnet.test](https://auth.testnet.test) - Local authentication service. +- [https://testnet.test](https://testnet.test) - Test Wallet frontend. +- [https://api.testnet.test](https://api.testnet.test) - Wallet backend API for the local Test Wallet environment. +- [https://boutique.test](https://boutique.test) - Boutique frontend. +- [https://api.boutique.test](https://api.boutique.test) - Boutique backend API serving product and checkout functionality. +- [https://mockgatehub.testnet.test](https://mockgatehub.testnet.test) - Mock GateHub service used for local funding and related sandbox flows. +- [https://rafiki-frontend.testnet.test](https://rafiki-frontend.testnet.test) - Rafiki frontend UI. +- [https://rafiki-backend.testnet.test](https://rafiki-backend.testnet.test) - Rafiki backend service. diff --git a/docker/dev/.env.example b/docker/dev/.env.example deleted file mode 100644 index 4c1762e7c..000000000 --- a/docker/dev/.env.example +++ /dev/null @@ -1,33 +0,0 @@ -SEND_EMAIL= -FROM_EMAIL= -SENDGRID_API_KEY= -AUTH_IDENTITY_SERVER_SECRET= -AUTH_COOKIE_KEY= -AUTH_INTERACTION_COOKIE_SAME_SITE= -RATE_API_KEY= -WALLET_ADDRESS_REDIRECT_HTML_PAGE= - -GATEHUB_ACCESS_KEY= -GATEHUB_SECRET_KEY= -GATEHUB_WEBHOOK_SECRET= -GATEHUB_GATEWAY_UUID= -GATEHUB_SETTLEMENT_WALLET_ADDRESS= -GATEHUB_ORG_ID= -GATEHUB_CARD_APP_ID= -RATE_LIMIT= -RATE_LIMIT_LEVEL= -GATEHUB_ACCOUNT_PRODUCT_CODE= -GATEHUB_CARD_PRODUCT_CODE= -GATEHUB_NAME_ON_CARD= -GATEHUB_CARD_PP_PREFIX= -CARD_PIN_HREF= -CARD_DATA_HREF= - -# commerce env variables -# encoded base 64 private key -PRIVATE_KEY= -KEY_ID= -PAYMENT_POINTER= - -OPERATOR_TENANT_ID= -ADMIN_API_SECRET= diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml deleted file mode 100644 index 8afce4675..000000000 --- a/docker/dev/docker-compose.yml +++ /dev/null @@ -1,336 +0,0 @@ -version: '3.5' - -x-logging: &logging - logging: - driver: 'json-file' - options: - max-size: '100m' - -services: - postgres: - container_name: postgres - image: 'postgres:15' - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - ports: - - '5433:5432' - restart: unless-stopped - networks: - - testnet - volumes: - - pg-data:/var/lib/postgresql/data - - ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql - - # Wallet - wallet-backend: - container_name: wallet-backend - build: - context: ../.. - args: - DEV_MODE: ${DEV_MODE} - dockerfile: ./packages/wallet/backend/Dockerfile.dev - depends_on: - - postgres - - rafiki-backend - - redis - volumes: - - ../../packages/wallet/backend:/home/testnet/packages/wallet/backend - - ../../packages/wallet/shared:/home/testnet/packages/wallet/shared - environment: - NODE_ENV: development - PORT: 3003 - DEBUG_PORT: 9229 - DATABASE_URL: postgres://wallet_backend:wallet_backend@postgres/wallet_backend - COOKIE_NAME: testnet.cookie - COOKIE_PASSWORD: testnet.cookie.password.super.secret.ilp - COOKIE_TTL: 2630000 - OPEN_PAYMENTS_HOST: https://rafiki-backend - GRAPHQL_ENDPOINT: http://rafiki-backend:3001/graphql - AUTH_GRAPHQL_ENDPOINT: http://rafiki-auth:3008/graphql - AUTH_DOMAIN: http://rafiki-auth:3009 - AUTH_IDENTITY_SERVER_SECRET: ${AUTH_IDENTITY_SERVER_SECRET} - RAFIKI_WEBHOOK_SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} - SENDGRID_API_KEY: ${SENDGRID_API_KEY} - FROM_EMAIL: ${FROM_EMAIL} - SEND_EMAIL: ${SEND_EMAIL:-false} - REDIS_URL: redis://redis:6379/0 - KRATOS_ADMIN_URL: 'http://kratos:4434/admin' - GATEHUB_ACCESS_KEY: ${GATEHUB_ACCESS_KEY} - GATEHUB_SECRET_KEY: ${GATEHUB_SECRET_KEY} - GATEHUB_WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET} - GATEHUB_GATEWAY_UUID: ${GATEHUB_GATEWAY_UUID} - GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS} - GATEHUB_ORG_ID: ${GATEHUB_ORG_ID} - GATEHUB_CARD_APP_ID: ${GATEHUB_CARD_APP_ID} - RATE_LIMIT: ${RATE_LIMIT} - RATE_LIMIT_LEVEL: ${RATE_LIMIT_LEVEL} - GATEHUB_ACCOUNT_PRODUCT_CODE: ${GATEHUB_ACCOUNT_PRODUCT_CODE} - GATEHUB_CARD_PRODUCT_CODE: ${GATEHUB_CARD_PRODUCT_CODE} - GATEHUB_NAME_ON_CARD: ${GATEHUB_NAME_ON_CARD} - GATEHUB_CARD_PP_PREFIX: ${GATEHUB_CARD_PP_PREFIX} - CARD_DATA_HREF: ${CARD_DATA_HREF} - CARD_PIN_HREF: ${CARD_PIN_HREF} - STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} - STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} - USE_STRIPE: ${USE_STRIPE} - OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} - ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} - ADMIN_SIGNATURE_VERSION: 1 - restart: always - networks: - - testnet - ports: - - '3003:3003' - - '9229:9229' # Map debugger port to local machine's port 9229 - <<: *logging - - # Boutique - boutique-backend: - container_name: boutique-backend - build: - context: ../.. - args: - DEV_MODE: ${DEV_MODE} - dockerfile: ./packages/boutique/backend/Dockerfile.dev - volumes: - - ../../packages/boutique/backend:/home/testnet/packages/boutique/backend - - ../../packages/boutique/shared:/home/testnet/packages/boutique/shared - depends_on: - - postgres - environment: - NODE_ENV: development - PORT: 3004 - DEBUG_PORT: 9230 - DATABASE_URL: postgres://boutique_backend:boutique_backend@postgres/boutique_backend - PRIVATE_KEY: ${PRIVATE_KEY} - KEY_ID: ${KEY_ID} - PAYMENT_POINTER: ${PAYMENT_POINTER} - restart: always - networks: - - testnet - ports: - - '3004:3004' - - '9230:9230' # Map debugger port to local machine's port 9230 - <<: *logging - - # Rafiki - rafiki-auth: - container_name: rafiki-auth - image: ghcr.io/interledger/rafiki-auth:v2.3.0-beta - restart: always - networks: - - testnet - ports: - - '3006:3006' - - '3008:3008' - environment: - AUTH_PORT: 3006 - INTROSPECTION_PORT: 3007 - ADMIN_PORT: 3008 - NODE_ENV: development - AUTH_SERVER_URL: http://localhost:3006 - AUTH_DATABASE_URL: postgresql://rafiki_auth:rafiki_auth@postgres/rafiki_auth - IDENTITY_SERVER_URL: http://localhost:4003/grant-interactions - IDENTITY_SERVER_SECRET: ${AUTH_IDENTITY_SERVER_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} - COOKIE_KEY: ${AUTH_COOKIE_KEY:-8fd398393c47dd27a3167d9c081c094f} - INTERACTION_COOKIE_SAME_SITE: ${AUTH_INTERACTION_COOKIE_SAME_SITE:-lax} - WAIT_SECONDS: 1 - REDIS_URL: redis://redis:6379/0 - OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} - ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} - ADMIN_SIGNATURE_VERSION: 1 - depends_on: - - postgres - <<: *logging - - rafiki-backend: - container_name: rafiki-backend - image: ghcr.io/interledger/rafiki-backend:v2.3.0-beta - restart: always - privileged: true - volumes: - - ../temp/:/workspace/temp/ - ports: - - '3010:80' - - '3011:3001' - - '3005:3005' - - '3002:3002' - networks: - - testnet - environment: - NODE_ENV: development - LOG_LEVEL: debug - ADMIN_PORT: 3001 - CONNECTOR_PORT: 3002 - OPEN_PAYMENTS_PORT: 80 - DATABASE_URL: postgresql://rafiki_backend:rafiki_backend@postgres/rafiki_backend - USE_TIGERBEETLE: true - TIGERBEETLE_CLUSTER_ID: 0 - TIGERBEETLE_REPLICA_ADDRESSES: 10.5.0.50:4342 - NONCE_REDIS_KEY: test - AUTH_SERVER_GRANT_URL: http://rafiki-auth:3006 - AUTH_SERVER_INTROSPECTION_URL: http://rafiki-auth:3007 - ILP_ADDRESS: test.net - ILP_CONNECTOR_URL: http://127.0.0.1:3002 - STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU= - ADMIN_KEY: admin - OPEN_PAYMENTS_URL: https://rafiki-backend - REDIS_URL: redis://redis:6379/0 - WALLET_ADDRESS_URL: https://rafiki-backend/.well-known/pay - # Testnet urls - not implemented - WEBHOOK_URL: http://wallet-backend:3003/webhooks - WEBHOOK_TIMEOUT: 60000 - SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} - EXCHANGE_RATES_URL: http://wallet-backend:3003/rates - ENABLE_AUTO_PEERING: true - AUTO_PEERING_SERVER_PORT: 3005 - INSTANCE_NAME: 'Testnet Wallet' - SLIPPAGE: 0.01 - KEY_ID: rafiki - WALLET_ADDRESS_REDIRECT_HTML_PAGE: ${WALLET_ADDRESS_REDIRECT_HTML_PAGE} - OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} - ADMIN_API_SECRET: ${ADMIN_API_SECRET:-secret-key} - ADMIN_SIGNATURE_VERSION: 1 - AUTH_SERVICE_API_URL: http://rafiki-auth:3011 - CARD_SERVICE_URL: 'http://rafiki-card-service:3007' - CARD_WEBHOOK_SERVICE_URL: 'http://rafiki-card-service:3007/webhook' - POS_SERVICE_URL: 'http://rafiki-pos-servicee:3014' - POS_WEBHOOK_SERVICE_URL: 'http://rafiki-pos-service:3014/webhook' - depends_on: - - postgres - - redis - <<: *logging - - rafiki-frontend: - container_name: rafiki-frontend - image: ghcr.io/interledger/rafiki-frontend:v2.3.0-beta - depends_on: - - rafiki-backend - restart: always - privileged: true - ports: - - '3012:3012' - networks: - - testnet - environment: - PORT: 3012 - GRAPHQL_URL: http://rafiki-backend:3001/graphql - OPEN_PAYMENTS_URL: https://rafiki-backend/ - ENABLE_INSECURE_MESSAGE_COOKIE: true - KRATOS_CONTAINER_PUBLIC_URL: 'http://kratos:4433' - KRATOS_BROWSER_PUBLIC_URL: 'http://localhost:4433' - KRATOS_ADMIN_URL: 'http://kratos:4434/admin' - AUTH_ENABLED: false - SIGNATURE_VERSION: 1 - <<: *logging - - rafiki-card-service: - container_name: rafiki-card-service - image: ghcr.io/interledger/rafiki-card-service:v2.3.0-beta - restart: always - privileged: true - networks: - - testnet - ports: - - '3007:3007' - environment: - NODE_ENV: development - LOG_LEVEL: debug - CARD_SERVICE_PORT: 3007 - REDIS_URL: redis://redis:6379/0 - GRAPHQL_URL: http://rafiki-backend:3001/graphql - TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} - TENANT_SECRET: ${ADMIN_API_SECRET:-secret-key} - TENANT_SIGNATURE_VERSION: 1 - <<: *logging - - rafiki-pos-service: - container_name: rafiki-pos-service - image: ghcr.io/interledger/rafiki-point-of-sale:v2.3.0-beta - restart: always - privileged: true - networks: - - testnet - ports: - - '3014:3014' - environment: - NODE_ENV: development - LOG_LEVEL: debug - PORT: 3014 - GRAPHQL_URL: http://rafiki-backend:3001/graphql - TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} - TENANT_SECRET: ${ADMIN_API_SECRET:-secret-key} - TENANT_SIGNATURE_VERSION: 1 - WEBHOOK_SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} - WEBHOOK_SIGNATURE_VERSION: 1 - USE_HTTP: true - <<: *logging - - kratos: - image: 'oryd/kratos:v1.3.1' - privileged: true - depends_on: - - postgres - - mailslurper - ports: - - '4433:4433' - volumes: - - ../entrypoint.sh:/entrypoint.sh - - ../identity.schema.json:/etc/config/kratos/identity.schema.json - - ./kratos.yml:/etc/config/kratos/kratos.yml - entrypoint: ['/entrypoint.sh'] - networks: - - testnet - - tigerbeetle: - image: ghcr.io/tigerbeetle/tigerbeetle:0.16.60 - privileged: true - volumes: - - tigerbeetle-data:/var/lib/tigerbeetle - networks: - testnet: - ipv4_address: 10.5.0.50 - entrypoint: - - /bin/sh - - -c - - | - set -ex - DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle - set +e - ls $$DATA_FILE - DATA_FILE_EXISTS="$$?" - set -e - echo $$DATA_FILE_EXISTS - if [ "$$DATA_FILE_EXISTS" != 0 ]; then - ./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE; - fi - hostname -i - ls /var/lib/tigerbeetle - ./tigerbeetle start --addresses=0.0.0.0:4342 $$DATA_FILE - - redis: - image: 'redis:7' - restart: unless-stopped - networks: - - testnet - - mailslurper: - image: oryd/mailslurper:latest-smtps - ports: - - '4436:4436' - - '4437:4437' - networks: - - testnet - -networks: - testnet: - driver: bridge - ipam: - config: - - subnet: 10.5.0.0/24 - gateway: 10.5.0.1 - -volumes: - pg-data: - tigerbeetle-data: # named volumes can be managed easier using docker-compose diff --git a/docker/dev/kratos.yml b/docker/dev/kratos.yml deleted file mode 100644 index 6089d1a3d..000000000 --- a/docker/dev/kratos.yml +++ /dev/null @@ -1,91 +0,0 @@ -version: v0.13.0 - -dsn: postgres://kratos:kratos@postgres:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - -serve: - public: - base_url: http://localhost:4433/ - cors: - enabled: true - admin: - base_url: http://kratos:4434/ - -selfservice: - default_browser_return_url: http://localhost:3012/ - allowed_return_urls: - - http://localhost:3012 - - methods: - link: - config: - lifespan: 1h - base_url: http://localhost:4433 - enabled: true - password: - enabled: true - - flows: - error: - ui_url: http://localhost:3012/error - - settings: - ui_url: http://localhost:3012/settings - privileged_session_max_age: 15m - required_aal: highest_available - - recovery: - enabled: true - ui_url: http://localhost:3012/auth/recovery - use: link - after: - hooks: - - hook: revoke_active_sessions - - verification: - enabled: false - - logout: - after: - default_browser_return_url: http://localhost:3012/auth - - login: - ui_url: http://localhost:3012/auth/login - lifespan: 10m - - registration: - enabled: false - -log: - level: debug - format: json - leak_sensitive_values: true - -secrets: - cookie: - - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE - cipher: - - 32-LONG-SECRET-NOT-SECURE-AT-ALL - -ciphers: - algorithm: xchacha20-poly1305 - -hashers: - algorithm: bcrypt - bcrypt: - cost: 8 - -identity: - schemas: - - id: default - url: file:///etc/config/kratos/identity.schema.json - -courier: - smtp: - connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true - -session: - lifespan: 1h - cookie: - persistent: false - same_site: Strict - path: / diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index 57a708b2b..000000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -e - -echo "Running Kratos Migrations..." -kratos -c /etc/config/kratos/kratos.yml migrate sql -e --yes - -if [ "$DEV_MODE" = true ]; then - echo "Starting Kratos in dev mode..." - exec kratos serve -c /etc/config/kratos/kratos.yml --dev --watch-courier -else - echo "Starting Kratos..." - exec kratos serve -c /etc/config/kratos/kratos.yml -fi \ No newline at end of file diff --git a/docker/identity.schema.json b/docker/identity.schema.json deleted file mode 100644 index 5af61c822..000000000 --- a/docker/identity.schema.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", - "type": "object", - "properties": { - "traits": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "E-Mail", - "minLength": 3, - "ory.sh/kratos": { - "credentials": { - "password": { - "identifier": true - } - }, - "verification": { - "via": "email" - }, - "recovery": { - "via": "email" - } - } - } - }, - "required": ["email"], - "additionalProperties": false - } - } -} diff --git a/docker/prod/.env.example b/docker/prod/.env.example deleted file mode 100644 index ff5b63f16..000000000 --- a/docker/prod/.env.example +++ /dev/null @@ -1,109 +0,0 @@ -# GENERAL -NODE_ENV= - -# DATABASE -POSTGRES_USER= -POSTGRES_PASSWORD= - -# WALLET FRONTEND -WALLET_FRONTEND_PORT= -WALLET_FRONTEND_BACKEND_URL= -WALLET_FRONTEND_OPEN_PAYMENTS_HOST= -WALLET_FRONTEND_AUTH_HOST= -WALLET_FRONTEND_THEME= - -# WALLET BACKEND -WALLET_BACKEND_PORT= -WALLET_BACKEND_DATABASE_URL= -WALLET_BACKEND_REDIS_URL= -WALLET_BACKEND_COOKIE_NAME= -WALLET_BACKEND_COOKIE_PASSWORD= -WALLET_BACKEND_COOKIE_TTL= -WALLET_BACKEND_OPEN_PAYMENTS_HOST= -WALLET_BACKEND_GRAPHQL_ENDPOINT= -WALLET_BACKEND_RAFIKI_MONEY_FRONTEND_HOST= -WALLET_BACKEND_SENDGRID_API_KEY= -WALLET_BACKEND_FROM_EMAIL= -WALLET_BACKEND_SEND_EMAIL= -WALLET_BACKEND_AUTH_DOMAIN= -WALLET_BACKEND_GATEHUB_ENV= -WALLET_BACKEND_GATEHUB_ACCESS_KEY= -WALLET_BACKEND_GATEHUB_SECRET_KEY= -WALLET_BACKEND_GATEHUB_WEBHOOK_SECRET= -WALLET_BACKEND_GATEHUB_GATEWAY_UUID= -WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS= -WALLET_BACKEND_GATEHUB_ORG_ID= -WALLET_BACKEND_GATEHUB_CARD_APP_ID= -WALLET_BACKEND_GATEHUB_ACCOUNT_PRODUCT_CODE= -WALLET_BACKEND_GATEHUB_CARD_PRODUCT_CODE= -WALLET_BACKEND_GATEHUB_NAME_ON_CARD= -WALLET_BACKEND_GATEHUB_CARD_PP_PREFIX= - -# BOUTIQUE -BOUTIQUE_BACKEND_PORT= -BOUTIQUE_BACKEND_DATABASE_URL= -BOUTIQUE_BACKEND_FRONTEND_URL= -BOUTIQUE_BACKEND_PRIVATE_KEY= -BOUTIQUE_BACKEND_KEY_ID= -BOUTIQUE_BACKEND_PAYMENT_POINTER= - -BOUTIQUE_FRONTEND_PORT= -BOUTIQUE_FRONTEND_API_BASE_URL= -BOUTIQUE_FRONTEND_CURRENCY= -BOUTIQUE_FRONTEND_THEME= - -# RAFIKI AUTH -RAFIKI_AUTH_PORT= -RAFIKI_AUTH_ADMIN_PORT= -RAFIKI_AUTH_INTROSPECTION_PORT= -RAFIKI_AUTH_DATABASE_URL= -RAFIKI_AUTH_IDENTITY_SERVER_DOMAIN= -RAFIKI_AUTH_IDENTITY_SERVER_SECRET= -RAFIKI_AUTH_COOKIE_KEY= -RAFIKI_AUTH_INTERACTION_COOKIE_SAME_SITE= -RAFIKI_AUTH_SERVER_DOMAIN= -RAFIKI_AUTH_WAIT_SECONDS= - -# RAFIKI BACKEND -RAFIKI_BACKEND_LOG_LEVEL= -RAFIKI_BACKEND_ADMIN_PORT= -RAFIKI_BACKEND_CONNECTOR_PORT= -RAFIKI_BACKEND_OPEN_PAYMENTS_PORT= -RAFIKI_BACKEND_DATABASE_URL= -RAFIKI_BACKEND_USE_TIGERBEETLE= -RAFIKI_BACKEND_TIGERBEETLE_CLUSTER_ID= -RAFIKI_BACKEND_TIGERBEETLE_REPLICA_ADDRESSES= -RAFIKI_BACKEND_NONCE_REDIS_KEY= -RAFIKI_BACKEND_AUTH_SERVER_GRANT_URL= -RAFIKI_BACKEND_AUTH_SERVER_INTROSPECTION_URL= -RAFIKI_BACKEND_ILP_ADDRESS= -RAFIKI_BACKEND_STREAM_SECRET= -RAFIKI_BACKEND_ADMIN_KEY= -RAFIKI_BACKEND_OPEN_PAYMENTS_URL= -RAFIKI_BACKEND_REDIS_URL= -RAFIKI_BACKEND_WALLET_ADDRESS_URL= -RAFIKI_BACKEND_WEBHOOK_URL= -RAFIKI_BACKEND_WEBHOOK_TIMEOUT= -RAFIKI_BACKEND_EXCHANGE_RATES_URL= -RAFIKI_BACKEND_AUTOPEERING_PORT= -RAFIKI_BACKEND_ILP_CONNECTOR_ADDRESS= -RAFIKI_BACKEND_INSTANCE_NAME= -RAFIKI_BACKEND_SLIPPAGE= -RAFIKI_BACKEND_ENABLE_TELEMETRY= -RAFIKI_BACKEND_LIVENET= -RAFIKI_BACKEND_SIGNATURE_SECRET= -RAFIKI_BACKEND_WALLET_ADDRESS_REDIRECT_HTML_PAGE= - -# RAFIKI FRONTEND -RAFIKI_FRONTEND_PORT= -RAFIKI_FRONTEND_GRAPHQL_URL= -RAFIKI_FRONTEND_OPEN_PAYMENTS_URL= - -# INTERLEDGER PAY -INTERLEDGER_PAY_PORT= -INTERLEDGER_PAY_KEY_ID= -INTERLEDGER_PAY_PRIVATE_KEY= -INTERLEDGER_PAY_WALLET_ADDRESS= -INTERLEDGER_PAY_REDIRECT_URL= -INTERLEDGER_PAY_HOST= -INTERLEDGER_PAY_SESSION_COOKIE_SECRET_KEY= diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml deleted file mode 100644 index 67df5ad8a..000000000 --- a/docker/prod/docker-compose.yml +++ /dev/null @@ -1,305 +0,0 @@ -version: '3.8' -name: 'testnet' - -x-logging: &logging - logging: - driver: 'gcplogs' - -services: - postgres: - image: 'postgres:15' - container_name: postgres - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - networks: - - testnet - restart: unless-stopped - volumes: - - pg-data:/var/lib/postgresql/data - - ../dbinit.sql:/docker-entrypoint-initdb.d/init.sql - - wallet-frontend: - hostname: wallet-frontend - build: - context: ../.. - dockerfile: ./packages/wallet/frontend/Dockerfile.prod - args: - PORT: ${WALLET_FRONTEND_PORT} - COOKIE_NAME: ${WALLET_BACKEND_COOKIE_NAME} - NEXT_PUBLIC_BACKEND_URL: ${WALLET_FRONTEND_BACKEND_URL} - NEXT_PUBLIC_OPEN_PAYMENTS_HOST: ${WALLET_FRONTEND_OPEN_PAYMENTS_HOST} - NEXT_PUBLIC_AUTH_HOST: ${WALLET_FRONTEND_AUTH_HOST} - NEXT_PUBLIC_GATEHUB_ENV: ${WALLET_BACKEND_GATEHUB_ENV} - NEXT_PUBLIC_THEME: ${WALLET_FRONTEND_THEME} - container_name: wallet-frontend - ports: - - '${WALLET_FRONTEND_PORT}:${WALLET_FRONTEND_PORT}' - restart: always - <<: *logging - - wallet-backend: - build: - context: ../.. - dockerfile: ./packages/wallet/backend/Dockerfile.prod - container_name: wallet-backend - depends_on: - - postgres - - rafiki-backend - environment: - NODE_ENV: ${NODE_ENV} - PORT: ${WALLET_BACKEND_PORT} - DATABASE_URL: ${WALLET_BACKEND_DATABASE_URL} - COOKIE_NAME: ${WALLET_BACKEND_COOKIE_NAME} - COOKIE_PASSWORD: ${WALLET_BACKEND_COOKIE_PASSWORD} - COOKIE_TTL: ${WALLET_BACKEND_COOKIE_TTL} - OPEN_PAYMENTS_HOST: ${WALLET_BACKEND_OPEN_PAYMENTS_HOST} - GRAPHQL_ENDPOINT: ${WALLET_BACKEND_GRAPHQL_ENDPOINT} - RAFIKI_MONEY_FRONTEND_HOST: ${WALLET_BACKEND_RAFIKI_MONEY_FRONTEND_HOST} - SENDGRID_API_KEY: ${WALLET_BACKEND_SENDGRID_API_KEY} - FROM_EMAIL: ${WALLET_BACKEND_FROM_EMAIL} - SEND_EMAIL: ${WALLET_BACKEND_SEND_EMAIL} - AUTH_IDENTITY_SERVER_SECRET: ${RAFIKI_AUTH_IDENTITY_SERVER_SECRET} - RAFIKI_WEBHOOK_SIGNATURE_SECRET: ${RAFIKI_BACKEND_SIGNATURE_SECRET} - AUTH_DOMAIN: ${WALLET_BACKEND_AUTH_DOMAIN} - REDIS_URL: ${WALLET_BACKEND_REDIS_URL} - GATEHUB_ENV: ${WALLET_BACKEND_GATEHUB_ENV} - GATEHUB_ACCESS_KEY: ${WALLET_BACKEND_GATEHUB_ACCESS_KEY} - GATEHUB_SECRET_KEY: ${WALLET_BACKEND_GATEHUB_SECRET_KEY} - GATEHUB_WEBHOOK_SECRET: ${WALLET_BACKEND_GATEHUB_WEBHOOK_SECRET} - GATEHUB_GATEWAY_UUID: ${WALLET_BACKEND_GATEHUB_GATEWAY_UUID} - GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS} - GATEHUB_ORG_ID: ${WALLET_BACKEND_GATEHUB_ORG_ID} - GATEHUB_CARD_APP_ID: ${WALLET_BACKEND_GATEHUB_CARD_APP_ID} - GATEHUB_ACCOUNT_PRODUCT_CODE: ${WALLET_BACKEND_GATEHUB_ACCOUNT_PRODUCT_CODE} - GATEHUB_CARD_PRODUCT_CODE: ${WALLET_BACKEND_GATEHUB_CARD_PRODUCT_CODE} - GATEHUB_NAME_ON_CARD: ${WALLET_BACKEND_GATEHUB_NAME_ON_CARD} - GATEHUB_CARD_PP_PREFIX: ${WALLET_BACKEND_GATEHUB_CARD_PP_PREFIX} - STRIPE_SECRET_KEY: ${WALLET_BACKEND_STRIPE_SECRET_KEY} - STRIPE_WEBHOOK_SECRET: ${WALLET_BACKEND_STRIPE_WEBHOOK_SECRET} - USE_STRIPE: ${USE_STRIPE} - networks: - - testnet - ports: - - '${WALLET_BACKEND_PORT}:${WALLET_BACKEND_PORT}' - restart: always - <<: *logging - - boutique-frontend: - container_name: boutique-frontend - build: - context: ../.. - dockerfile: ./packages/boutique/frontend/Dockerfile.prod - args: - PORT: ${BOUTIQUE_FRONTEND_API_BASE_URL} - VITE_API_BASE_URL: ${BOUTIQUE_FRONTEND_API_BASE_URL} - VITE_CURRENCY: ${BOUTIQUE_FRONTEND_CURRENCY} - VITE_THEME: ${BOUTIQUE_FRONTEND_THEME} - depends_on: - - postgres - environment: - NODE_ENV: ${NODE_ENV} - restart: always - networks: - - testnet - ports: - - '${BOUTIQUE_FRONTEND_PORT}:${BOUTIQUE_FRONTEND_PORT}' - <<: *logging - - boutique-backend: - container_name: boutique-backend - build: - context: ../.. - dockerfile: ./packages/boutique/backend/Dockerfile.prod - depends_on: - - postgres - environment: - NODE_ENV: ${NODE_ENV} - PORT: ${BOUTIQUE_BACKEND_PORT} - FRONTEND_URL: ${BOUTIQUE_BACKEND_FRONTEND_URL} - DATABASE_URL: ${BOUTIQUE_BACKEND_DATABASE_URL} - PRIVATE_KEY: ${BOUTIQUE_BACKEND_PRIVATE_KEY} - KEY_ID: ${BOUTIQUE_BACKEND_KEY_ID} - PAYMENT_POINTER: ${BOUTIQUE_BACKEND_PAYMENT_POINTER} - restart: always - networks: - - testnet - ports: - - '3004:3004' - <<: *logging - - rafiki-auth: - image: ghcr.io/interledger/rafiki-auth:v1.2.0-beta - container_name: rafiki-auth - environment: - NODE_ENV: ${NODE_ENV} - AUTH_PORT: ${RAFIKI_AUTH_PORT} - ADMIN_PORT: ${RAFIKI_AUTH_ADMIN_PORT} - INTROSPECTION_PORT: ${RAFIKI_AUTH_INTROSPECTION_PORT} - AUTH_DATABASE_URL: ${RAFIKI_AUTH_DATABASE_URL} - COOKIE_KEY: ${RAFIKI_AUTH_COOKIE_KEY} - INTERACTION_COOKIE_SAME_SITE: ${RAFIKI_AUTH_INTERACTION_COOKIE_SAME_SITE} - IDENTITY_SERVER_SECRET: ${RAFIKI_AUTH_IDENTITY_SERVER_SECRET} - IDENTITY_SERVER_URL: ${RAFIKI_AUTH_IDENTITY_SERVER_DOMAIN} - AUTH_SERVER_URL: ${RAFIKI_AUTH_SERVER_DOMAIN} - WAIT_SECONDS: ${RAFIKI_AUTH_WAIT_SECONDS} - TRUST_PROXY: true - REDIS_URL: redis://redis:6379/0 - depends_on: - - postgres - networks: - - testnet - ports: - - '${RAFIKI_AUTH_PORT}:${RAFIKI_AUTH_PORT}' - - '${RAFIKI_AUTH_ADMIN_PORT}:${RAFIKI_AUTH_ADMIN_PORT}' - restart: always - <<: *logging - - rafiki-backend: - image: ghcr.io/interledger/rafiki-backend:v1.1.2-beta - container_name: rafiki-backend - depends_on: - - postgres - - redis - environment: - NODE_ENV: ${NODE_ENV} - LOG_LEVEL: ${RAFIKI_BACKEND_LOG_LEVEL} - ADMIN_PORT: ${RAFIKI_BACKEND_ADMIN_PORT} - CONNECTOR_PORT: ${RAFIKI_BACKEND_CONNECTOR_PORT} - OPEN_PAYMENTS_PORT: ${RAFIKI_BACKEND_OPEN_PAYMENTS_PORT} - DATABASE_URL: ${RAFIKI_BACKEND_DATABASE_URL} - USE_TIGERBEETLE: ${RAFIKI_BACKEND_USE_TIGERBEETLE} - TIGERBEETLE_CLUSTER_ID: ${RAFIKI_BACKEND_TIGERBEETLE_CLUSTER_ID} - TIGERBEETLE_REPLICA_ADDRESSES: ${RAFIKI_BACKEND_TIGERBEETLE_REPLICA_ADDRESSES} - NONCE_REDIS_KEY: ${RAFIKI_BACKEND_NONCE_REDIS_KEY} - AUTH_SERVER_GRANT_URL: ${RAFIKI_BACKEND_AUTH_SERVER_GRANT_URL} - AUTH_SERVER_INTROSPECTION_URL: ${RAFIKI_BACKEND_AUTH_SERVER_INTROSPECTION_URL} - ILP_ADDRESS: ${RAFIKI_BACKEND_ILP_ADDRESS} - STREAM_SECRET: ${RAFIKI_BACKEND_STREAM_SECRET} - ADMIN_KEY: ${RAFIKI_BACKEND_ADMIN_KEY} - OPEN_PAYMENTS_URL: ${RAFIKI_BACKEND_OPEN_PAYMENTS_URL} - REDIS_URL: ${RAFIKI_BACKEND_REDIS_URL} - WALLET_ADDRESS_URL: ${RAFIKI_BACKEND_WALLET_ADDRESS_URL} - WEBHOOK_URL: ${RAFIKI_BACKEND_WEBHOOK_URL} - WEBHOOK_TIMEOUT: ${RAFIKI_BACKEND_WEBHOOK_TIMEOUT} - SIGNATURE_SECRET: ${RAFIKI_BACKEND_SIGNATURE_SECRET} - EXCHANGE_RATES_URL: ${RAFIKI_BACKEND_EXCHANGE_RATES_URL} - TRUST_PROXY: true - ENABLE_AUTO_PEERING: true - AUTO_PEERING_SERVER_PORT: ${RAFIKI_BACKEND_AUTOPEERING_PORT} - ILP_CONNECTOR_URL: ${RAFIKI_BACKEND_ILP_CONNECTOR_ADDRESS} - INSTANCE_NAME: ${RAFIKI_BACKEND_INSTANCE_NAME} - SLIPPAGE: ${RAFIKI_BACKEND_SLIPPAGE} - ENABLE_TELEMETRY: ${RAFIKI_BACKEND_ENABLE_TELEMETRY} - LIVENET: ${RAFIKI_BACKEND_LIVENET} - KEY_ID: ${RAFIKI_BACKEND_KEY_ID} - WALLET_ADDRESS_REDIRECT_HTML_PAGE: ${RAFIKI_BACKEND_WALLET_ADDRESS_REDIRECT_HTML_PAGE} - networks: - - testnet - ports: - - '3010:80' - - '3011:3001' - - '${RAFIKI_BACKEND_AUTOPEERING_PORT}:${RAFIKI_BACKEND_AUTOPEERING_PORT}' - - '${RAFIKI_BACKEND_CONNECTOR_PORT}:${RAFIKI_BACKEND_CONNECTOR_PORT}' - privileged: true - restart: always - volumes: - - ../temp/:/workspace/temp/ - <<: *logging - - interledger-pay: - image: ghcr.io/interledger/interledger-pay:latest - container_name: interledger-pay - environment: - NODE_ENV: ${NODE_ENV} - PORT: ${INTERLEDGER_PAY_PORT} - KEY_ID: ${INTERLEDGER_PAY_KEY_ID} - PRIVATE_KEY: ${INTERLEDGER_PAY_PRIVATE_KEY} - WALLET_ADDRESS: ${INTERLEDGER_PAY_WALLET_ADDRESS} - REDIRECT_URL: ${INTERLEDGER_PAY_REDIRECT_URL} - INTERLEDGER_PAY_HOST: ${INTERLEDGER_PAY_HOST} - SESSION_COOKIE_SECRET_KEY: ${INTERLEDGER_PAY_SESSION_COOKIE_SECRET_KEY} - networks: - - testnet - restart: always - privileged: true - ports: - - '${INTERLEDGER_PAY_PORT}:${INTERLEDGER_PAY_PORT}' - <<: *logging - - rafiki-frontend: - image: ghcr.io/interledger/rafiki-frontend:v1.1.2-beta - container_name: rafiki-frontend - depends_on: - - rafiki-backend - environment: - PORT: ${RAFIKI_FRONTEND_PORT} - GRAPHQL_URL: ${RAFIKI_FRONTEND_GRAPHQL_URL} - OPEN_PAYMENTS_URL: ${RAFIKI_FRONTEND_OPEN_PAYMENTS_URL} - KRATOS_CONTAINER_PUBLIC_URL: 'http://kratos:4433' - KRATOS_BROWSER_PUBLIC_URL: 'https://admin.rafiki.money/kratos' - KRATOS_ADMIN_URL: 'http://kratos:4434/admin' - networks: - - testnet - restart: always - privileged: true - ports: - - '${RAFIKI_FRONTEND_PORT}:${RAFIKI_FRONTEND_PORT}' - <<: *logging - - tigerbeetle: - image: ghcr.io/tigerbeetle/tigerbeetle:0.16.60 - privileged: true - volumes: - - tigerbeetle-data:/var/lib/tigerbeetle - networks: - testnet: - ipv4_address: 10.5.0.50 - entrypoint: - - /bin/sh - - -c - - | - set -ex - DATA_FILE=/var/lib/tigerbeetle/cluster_0_replica_0.tigerbeetle - set +e - ls $$DATA_FILE - DATA_FILE_EXISTS="$$?" - set -e - echo $$DATA_FILE_EXISTS - if [ "$$DATA_FILE_EXISTS" != 0 ]; then - ./tigerbeetle format --cluster=0 --replica=0 --replica-count=1 $$DATA_FILE; - fi - hostname -i - ls /var/lib/tigerbeetle - ./tigerbeetle start --addresses=0.0.0.0:4342 $$DATA_FILE - - redis: - image: 'redis:7' - restart: unless-stopped - networks: - - testnet - - kratos: - image: 'oryd/kratos:v1.3.1' - privileged: true - ports: - - '4433:4433' - volumes: - - ../entrypoint.sh:/entrypoint.sh - - ../identity.schema.json:/etc/config/kratos/identity.schema.json - - ./kratos.yml:/etc/config/kratos/kratos.yml - entrypoint: ['/entrypoint.sh'] - networks: - - testnet - -networks: - testnet: - driver: bridge - ipam: - config: - - subnet: 10.5.0.0/24 - gateway: 10.5.0.1 - -volumes: - pg-data: - tigerbeetle-data: # named volumes can be managed easier using docker-compose diff --git a/docker/prod/kratos.yml b/docker/prod/kratos.yml deleted file mode 100644 index 386d43672..000000000 --- a/docker/prod/kratos.yml +++ /dev/null @@ -1,91 +0,0 @@ -version: v0.13.0 - -dsn: postgres://kratos:kratos@postgres:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - -serve: - public: - base_url: https://admin.rafiki.money/kratos - cors: - enabled: true - admin: - base_url: http://kratos:4434/ - -selfservice: - default_browser_return_url: https://admin.rafiki.money/ - allowed_return_urls: - - https://admin.rafiki.money - - methods: - link: - config: - lifespan: 1h - base_url: https://admin.rafiki.money/kratos - enabled: true - password: - enabled: true - - flows: - error: - ui_url: https://admin.rafiki.money/error - - settings: - ui_url: https://admin.rafiki.money/settings - privileged_session_max_age: 15m - required_aal: highest_available - - recovery: - enabled: true - ui_url: https://admin.rafiki.money/auth/recovery - use: link - after: - hooks: - - hook: revoke_active_sessions - - verification: - enabled: false - - logout: - after: - default_browser_return_url: https://admin.rafiki.money/auth - - login: - ui_url: https://admin.rafiki.money/auth/login - lifespan: 10m - - registration: - enabled: false - -log: - level: debug - format: json - leak_sensitive_values: true - -secrets: - cookie: - - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE - cipher: - - 32-LONG-SECRET-NOT-SECURE-AT-ALL - -ciphers: - algorithm: xchacha20-poly1305 - -hashers: - algorithm: bcrypt - bcrypt: - cost: 8 - -identity: - schemas: - - id: default - url: file:///etc/config/kratos/identity.schema.json - -courier: - smtp: - connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true - -session: - lifespan: 1h - cookie: - persistent: false - same_site: Strict - path: / diff --git a/docker/temp/private-key.pem b/docker/temp/private-key.pem deleted file mode 100644 index 43fefaf8a..000000000 --- a/docker/temp/private-key.pem +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIIZFs7Y4AoIP/4WcVtKt74Uim4mxnZhV9zQ5RSnQen5u ------END PRIVATE KEY----- diff --git a/local/.env.example b/local/.env.example new file mode 100644 index 000000000..15c406c50 --- /dev/null +++ b/local/.env.example @@ -0,0 +1,126 @@ +## Testnet local docker-compose environment file (example) +## Copy to .env and adjust as needed for your machine. +## +## By default, all GateHub variables point to the local MockGatehub container +## (no real credentials needed). To use the real GateHub Sandbox instead, see +## the "Real GateHub Sandbox overrides" section below. + +## --------------------------------------------------------------------------- +## General build / dev flags +## --------------------------------------------------------------------------- + +## DEV_MODE — Controls how core infra containers start when applicable. +## Wallet and boutique apps run on the host machine and are configured via +## package-local env files under packages/*. +# DEV_MODE=true + +## NODE_ENV — Standard Node.js environment flag, passed to every service. +## development (default) — enables error stack traces in API responses, +## allows HTTP (not just HTTPS) for internal service +## calls, relaxes cookie security (secure=false, +## sameSite=lax), and enables the /signup auth route. +## production — hides stack traces, enforces HTTPS and secure +## cookies (secure=true, sameSite=none), and may +## disable certain non-production endpoints. +## For local development you almost never need to change this. +# NODE_ENV=development + +## Redis URL used by local core services. +# REDIS_URL=redis://redis:6379/0 + +## Shared secrets used by the core local stack. +# AUTH_IDENTITY_SERVER_SECRET=dev_identity_server_secret +# ADMIN_API_SECRET=secret-key +# ADMIN_SIGNATURE_VERSION=1 +# OPERATOR_TENANT_ID=f829c064-762a-4430-ac5d-7af5df198551 +# RAFIKI_SIGNATURE_SECRET=327132b5-99e9-4eb8-8a25-2b7d7738ece1 + +## --------------------------------------------------------------------------- +## GateHub integration +## --------------------------------------------------------------------------- +## Two modes are supported: +## +## 1. MockGatehub (default) — everything works out of the box. +## The wallet backend talks to the mockgatehub container inside the +## compose network. No real credentials required. +## +## 2. Real GateHub Sandbox — for full KYC / fiat on-off ramp testing. +## Create an account at https://sandbox.gatehub.net (or contact +## timea@interledger.foundation) to obtain the values below, then +## uncomment the "Real GateHub Sandbox overrides" block. +## --------------------------------------------------------------------------- + +## --- Mock defaults (active when the override block below is commented) ----- +## GATEHUB_API_BASE_URL is used by the backend for signed GateHub API calls. +## The three GATEHUB_IFRAME_*_URL values are required and used for browser-facing +## iframe URLs. Backend startup fails if any of them is missing. +## Keep these credentials aligned with packages/wallet/backend/.env when using +## the host-run wallet backend against MockGatehub. +# GATEHUB_API_BASE_URL=http://mockgatehub:8080 +# GATEHUB_ENV=sandbox +# GATEHUB_IFRAME_MANAGED_RAMP_URL=http://localhost:8080 +# GATEHUB_IFRAME_EXCHANGE_URL=http://localhost:8080 +# GATEHUB_IFRAME_ONBOARDING_URL=http://localhost:8080 +# GATEHUB_ACCESS_KEY=mock_access_key +# GATEHUB_SECRET_KEY=mock_secret_key +# GATEHUB_WEBHOOK_SECRET=6d6f636b5f776562686f6f6b5f736563726574 +# GATEHUB_GATEWAY_UUID=mock-gateway-uuid +# GATEHUB_SETTLEMENT_WALLET_ADDRESS=$ilp.interledger-test.dev/interledger +# GATEHUB_ORG_ID=mock-org-id +# GATEHUB_CARD_APP_ID=mock-card-app-id + +## --- Real GateHub endpoint reference -------------------------------------- +## Use these endpoint shapes when configuring real GateHub hosts. +## +## Sandbox: +## GATEHUB_API_BASE_URL=https://api.sandbox.gatehub.net +## GATEHUB_IFRAME_MANAGED_RAMP_URL=https://managed-ramp.sandbox.gatehub.net +## GATEHUB_IFRAME_EXCHANGE_URL=https://exchange.sandbox.gatehub.net +## GATEHUB_IFRAME_ONBOARDING_URL=https://onboarding.sandbox.gatehub.net +## +## Production: +## GATEHUB_API_BASE_URL=https://api.gatehub.net +## GATEHUB_IFRAME_MANAGED_RAMP_URL=https://managed-ramp.gatehub.net +## GATEHUB_IFRAME_EXCHANGE_URL=https://exchange.gatehub.net +## GATEHUB_IFRAME_ONBOARDING_URL=https://onboarding.gatehub.net + +## --- Real GateHub Sandbox overrides ---------------------------------------- +## To use the real sandbox, comment out the mock block above and uncomment the +## lines below, filling in your credentials. +## +## IMPORTANT: Keep all three GATEHUB_IFRAME_* URL values configured. +## For real sandbox they should be the GateHub hosts shown below. +## +# GATEHUB_ENV=sandbox +# GATEHUB_API_BASE_URL=https://api.sandbox.gatehub.net +# GATEHUB_IFRAME_MANAGED_RAMP_URL=https://managed-ramp.sandbox.gatehub.net +# GATEHUB_IFRAME_EXCHANGE_URL=https://exchange.sandbox.gatehub.net +# GATEHUB_IFRAME_ONBOARDING_URL=https://onboarding.sandbox.gatehub.net +# GATEHUB_ACCESS_KEY= +# GATEHUB_SECRET_KEY= +# GATEHUB_WEBHOOK_SECRET= +# GATEHUB_GATEWAY_UUID= +# GATEHUB_SETTLEMENT_WALLET_ADDRESS= +# GATEHUB_ORG_ID= +# GATEHUB_CARD_APP_ID= + +## Rafiki public URL defaults for local wallet-address generation +# RAFIKI_BACKEND_OPEN_PAYMENTS_URL=https://rafiki-backend.testnet.test +# RAFIKI_BACKEND_WALLET_ADDRESS_URL=https://rafiki-backend.testnet.test/.well-known/pay +# RAFIKI_FRONTEND_OPEN_PAYMENTS_URL=https://rafiki-backend.testnet.test/ +# RAFIKI_BACKEND_AUTH_SERVER_GRANT_URL=https://auth.testnet.test +# RAFIKI_AUTH_TRUST_PROXY=true +# RAFIKI_BACKEND_TRUST_PROXY=true +## Allow local self-signed TLS for rafiki-auth when fetching wallet-address jwks from *.testnet.test. +# RAFIKI_AUTH_NODE_TLS_REJECT_UNAUTHORIZED=0 + +## Core services that call the wallet backend should target the host machine. +# RAFIKI_BACKEND_WEBHOOK_URL=http://host.docker.internal:3003/webhooks +# RAFIKI_BACKEND_EXCHANGE_RATES_URL=http://host.docker.internal:3003/rates +# MOCKGATEHUB_WEBHOOK_URL=http://host.docker.internal:3003/gatehub-webhooks + +## Wallet and boutique app env files now live here: +## - packages/wallet/backend/.env(.example) +## - packages/wallet/frontend/.env.local(.example) +## - packages/boutique/backend/.env(.example) +## - packages/boutique/frontend/.env.local(.example) diff --git a/local/.env.local b/local/.env.local new file mode 100644 index 000000000..a449140b5 --- /dev/null +++ b/local/.env.local @@ -0,0 +1,2 @@ +# All service defaults are baked into the individual compose files as ${VAR:-default}. +# Use this file to override any value for your local environment. diff --git a/local/.gitignore b/local/.gitignore new file mode 100644 index 000000000..9e06ea81e --- /dev/null +++ b/local/.gitignore @@ -0,0 +1 @@ +pg-data \ No newline at end of file diff --git a/local/config/certs/boutique.crt b/local/config/certs/boutique.crt new file mode 100644 index 000000000..862a48132 --- /dev/null +++ b/local/config/certs/boutique.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIUOty1HifQz+iH2lG90DXKTSYHT5UwDQYJKoZIhvcNAQEL +BQAwPTEYMBYGA1UEAwwPKi5ib3V0aXF1ZS50ZXN0MRQwEgYDVQQKDAtJbnRlcmxl +ZGdlcjELMAkGA1UEBhMCVVMwHhcNMjYwNDEzMDkyNDU4WhcNMzYwNDEwMDkyNDU4 +WjA9MRgwFgYDVQQDDA8qLmJvdXRpcXVlLnRlc3QxFDASBgNVBAoMC0ludGVybGVk +Z2VyMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALa7Q9qHPSYCNLr0I+vCqV+5ZJK7AtHcGvNZieoapZua2KsqjKi+n36AygBVc7VY +eq9UIiIPHEZoIPujQU3pkNBL6FsC45xNa1w1oxhaB2+dJlJt58YsTV8bdmCdu6TF +YjKuCq9n7g1Hf/e1fysggYbeapUNz/U3HFSNiES07y0PR0zxVHJx6tukd7JW6c/E +qoz1mKph+9JBt21notA5EsAHGhIWUYVTxg3n+n0UoXj3+G0FlOM216+GtZ37Tubb +chsdNu/ffJEeBNvdqVp+2Ix1vjteeII30C4NUMpJdKQHNrfddCY8xjJNbh0la2tT +yq6u3pT6MojVmVxxUEIx/scCAwEAAaNMMEowKQYDVR0RBCIwIIIPKi5ib3V0aXF1 +ZS50ZXN0gg1ib3V0aXF1ZS50ZXN0MB0GA1UdDgQWBBQASYe3OmWdVBue4gSC+yH2 +WXbqcjANBgkqhkiG9w0BAQsFAAOCAQEApgYuVOx06c5lLM/bnWC23R85PWUIk0Zo +O7hrPQHcyQg8EiJa/rO36zVLK9fh7Aqzlw5JLNsT0H20hZFmgIjW5+iN1H16JxBO ++9XhKQdwh+OOcb17stj9dWMqggKTbwAQhs5/othgfQOgRmpo/43w+nUfrQeT+Kj+ +3NzuFO5YDfyVk8SIzbljZzv+sJl79UDzam2/88AUsLyMnvPbPGHwPMPF8Eb5DHcQ +rjL3rWzyfH4GjRTUaezP63/6MLV3xJoWZ5nBx+hK7aeepOHfzZgUEfKnvpQZeJ1b +u3tqHmbB+ABgXgW7HjFbvR5uU+Z+aMA2RKz162r4acsV01LdoEgsag== +-----END CERTIFICATE----- diff --git a/local/config/certs/boutique.key b/local/config/certs/boutique.key new file mode 100644 index 000000000..c8d334004 --- /dev/null +++ b/local/config/certs/boutique.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2u0Pahz0mAjS6 +9CPrwqlfuWSSuwLR3BrzWYnqGqWbmtirKoyovp9+gMoAVXO1WHqvVCIiDxxGaCD7 +o0FN6ZDQS+hbAuOcTWtcNaMYWgdvnSZSbefGLE1fG3ZgnbukxWIyrgqvZ+4NR3/3 +tX8rIIGG3mqVDc/1NxxUjYhEtO8tD0dM8VRycerbpHeyVunPxKqM9ZiqYfvSQbdt +Z6LQORLABxoSFlGFU8YN5/p9FKF49/htBZTjNtevhrWd+07m23IbHTbv33yRHgTb +3alaftiMdb47XniCN9AuDVDKSXSkBza33XQmPMYyTW4dJWtrU8qurt6U+jKI1Zlc +cVBCMf7HAgMBAAECggEACe3I9RHOH8eU5+emIARWwa3IKjThJVgJWtmtpbvG7dqP +FVXdiLm63tvOnhAGln71Q5uVszYQ+j5kNGgdf0dGqBrHyFIy3eZTBBKiBwu1RbpL +P8VbB6iWl6ldWEiZzo0vFcPhLsC97ZjJ+kKxYz2D7Dkfn3O9rfxNnMaH9fvC1zba +f/bSDMjCUgk/y2+Sd7FFQBQ89jSmIs9FKJLF/p9WgTOT8N44ZEgOegUawrfR0yRj +FVCNsDui6LQLVQDau1ebINkF2QQhMvZCIs/6e+G5mIJeH+DnBwiypt6PtApSjb4C +ezow/eKTsjKIGMjonmfdyugiJCMaNdQIH+4u/r9F2QKBgQD+QIaE1Dw/0j04Hy69 +f9DaypSfrqWhmf3SC5nzZYp8Lcyq+PBVUI/4bi+xQxuZw5IBm4xe4zk4S4tWW8/f +lYKlUarqWkO/Dhrba570AxZXW8MVZeH0l2rngTaOGO7uuvd551jdWwW8B421jwwq +ksLvLjp7L2fq3N67dxF49LBMzQKBgQC3/N2wqXAt0bg1Y5x/eE0swnzKgrN8Qzw0 +woATPk1P3VjnsozE4v5pMC1ER1/dlXbFKeyYErsK13DEC3KyYH3KGCezBlVYNlhF +wgwmAMQO3lCOXFmVZBk06NoGurKLqC0f3J50IyrZOea8ahTWySWC8x/Xn5GuDEeH +2LMYS9V54wKBgD1N3aGFcwmIbc5sZl62xOscUX23JKS0dqkAtxwi7aNHf8PxxjEr +/arPFpzNK4iFyVyt3si6TP+v52bpOFQWvquR534eSZt1kTA64NbBL8/chktQh4zs +PZCYbUkR5fqWBjnUeG7XFajiwV53oV1aiboWEW/GXG4a13GoIgRiK2g5AoGAAsg5 +FfXJ83drYKtL74WDLT0Xo/xL+IIPxAB47VGQ27dOI9rIVCXWkQKgfJrTsUcUT7Dt +mfMWjvhpBR3jMx1QaaDp4MQkwDu6I/vmMSDr1gm4kB6HYEHuO6WVvmpOTtxRKl0s +rDbn6/b7LTgxbnoGLi+8+Ia6LjaTUlONib2D0d8CgYEAi5odyclgot108hz3vXj1 +dOs7YMxOhRZQbWveQPl7Mk0pbsJTr0C1a2f96YPxt0nXadxPdYVVKChCajjH7Ku0 +4CjDCdRZfCX/znOrlh7skNMSU5tkZAT0d5wSHklQ0F9I/z7uU9ozb+HaPPJEC6Bv +G5L8JyKtK019xfYDp5C+Nxg= +-----END PRIVATE KEY----- diff --git a/local/config/certs/local.crt b/local/config/certs/local.crt new file mode 100644 index 000000000..ac36f296e --- /dev/null +++ b/local/config/certs/local.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIUeKMJtdlyoKXSKkN2ZI/eqdxDkN4wDQYJKoZIhvcNAQEL +BQAwPDEXMBUGA1UEAwwOKi50ZXN0bmV0LnRlc3QxFDASBgNVBAoMC0ludGVybGVk +Z2VyMQswCQYDVQQGEwJVUzAeFw0yNjA0MTMwOTI0NThaFw0zNjA0MTAwOTI0NTha +MDwxFzAVBgNVBAMMDioudGVzdG5ldC50ZXN0MRQwEgYDVQQKDAtJbnRlcmxlZGdl +cjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDO +UIC5ElPAhcYVwCj1VUdEDXai/8q18j13z2iAaJyLQPZDcP81QNQRGqQHEYzcu9nG +LKO5a5YT0vXNpB1W6W0LcZkHrfoyXKaEYJBmbbpkp+GpFFUlVcYORMq+POOXabG4 +U8JPZn5mihoJDRtfND6t5ETHfwODuzbwA/uD5GP+Yp8RAMUqQMqwJeEbHeDv5Niv +E66reMIMzBZqU1isyla+s6Rr0w+EusHu1Ud+z377Ey4Oj9cvIyFo22+Ttlln8xEZ +hBbOStNR1hBmREzt/OSQy/zWRNNW2tOhbR13Xr0cQvmN56vV61GAM0HgeNLLDf/q +0ixooNDcA7w6VwkJ1tJ1AgMBAAGjSjBIMCcGA1UdEQQgMB6CDioudGVzdG5ldC50 +ZXN0ggx0ZXN0bmV0LnRlc3QwHQYDVR0OBBYEFG0Mar1X/HQ0UzMbgirkbukBiLW9 +MA0GCSqGSIb3DQEBCwUAA4IBAQCTAYzgmP52NWbjtoiO+hHJKB7l2Xs5kIUq9jCw +6z2DPp0hihuvxinzouCKOPaW0yTapGUIoQM2/+cdy8pGeRTI5Vq8Ui/il84mmo+K +FSXhnjfIW+3djDi3LJuC68IUm3WFODBw9pAC6nDkYUahcUfDeXC/wrI9OAENEB91 +PGSmM+V2NnxL6rTWlVRLhtLf/RJTDanz/SCEA9dl9bGwbNQOHFBnWgML12nSxDZa +XGtMrDS6+eOk43KnrjTIgAHQfjvifR9JSXv0/pv3YrdgqHUQQ5z9xt52mSLmCu9B +6h8jIB7eR1my/MTUuPIWqbrFQIK1tBIl0riHyh0mbYtf5riD +-----END CERTIFICATE----- diff --git a/local/config/certs/local.key b/local/config/certs/local.key new file mode 100644 index 000000000..52555c414 --- /dev/null +++ b/local/config/certs/local.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDOUIC5ElPAhcYV +wCj1VUdEDXai/8q18j13z2iAaJyLQPZDcP81QNQRGqQHEYzcu9nGLKO5a5YT0vXN +pB1W6W0LcZkHrfoyXKaEYJBmbbpkp+GpFFUlVcYORMq+POOXabG4U8JPZn5mihoJ +DRtfND6t5ETHfwODuzbwA/uD5GP+Yp8RAMUqQMqwJeEbHeDv5NivE66reMIMzBZq +U1isyla+s6Rr0w+EusHu1Ud+z377Ey4Oj9cvIyFo22+Ttlln8xEZhBbOStNR1hBm +REzt/OSQy/zWRNNW2tOhbR13Xr0cQvmN56vV61GAM0HgeNLLDf/q0ixooNDcA7w6 +VwkJ1tJ1AgMBAAECggEAANZez2F9qYhhd3EiOWyxL1uGob68CeUDztqup+bEy+Pl +m/x3bLXopnEfPVAa7EJu7dj8iSIgp8/xY3ZJEDM+WA+L+XYBT/cETgE+VMwZN5Ne +3RrSBabFeQVadU819UVejUr5vOzDkpL47BR7ifViU75bcRvY3c4CuRKrEKv6o+us +rtGO/ZuOA3SsjmORReQLiyFj0zeez33JcQxqL+ffebRfLnZikKyCK7YbeUliZeM4 +74PiTkysLGNJ8haeqqfaaTZyUp/eZcLfSkQUXYj8ZobtzfWcDzv5hOHrpA+gjk4A +jSRnpdBw08UvaAXbUvdYE6LiqfJW/SGMq3ONyUwPQQKBgQD2qhEQTDXWSlNle2Jl +A0sX4bIh3KCVH7AGv9+zfWxmFtHhJ+J8dQ3lZodsQ4irFWBd5lqMsbrY97Y3FKgF +R/if1O+M99mWItkK5kH86h8pvxD2udJWOGg+uz/UbUFfhxdOKvTCo2AxzPrmGpY3 +sRvbIp+bendXP67ZKk07vuLZ3QKBgQDWH3xmwKVN5DK2RSWskN2qf32ArLtGzbzY +95UaSkGp2CY0BsNQmTp8FIw8Di4b0OQQ5A0Ky2INrVUFIDIsZrIypeuaIVNiHHFs +WDHTRe8ddA9lfy0tY7U5m6YLdpvPNlFXhKzbg0zpmi+G4YqxaDhbqHl+zmYrM9+q +R6SFQXoteQKBgCK7V6O/12TRGmUTtosap5w5P2fdNQ9KYvLQWaNgab8GP49AmneD +0+0VPsRTzg6o9+TA5scgz3p1hsbCtdXoiG2ZAhILZCvrxDvwqCIApVqveKgFCkTx +tFwA6b/zklztHK75BXXSo1zfzARSggxdd9qnSWoPGHbElM4nUrCy9CZRAoGAQjkY +s3rPZUi3enHmVfmz5OBrQ1mozhwhVQU1gx0GM+5a2mphC9NoKCn+uTA1iHrRnnu5 +oOYIHGzIR76OZfcZE9sHA/Nco5JibfzU5B/T9UsI2/46TbhxuzBtaUw0oiCNhcDN +6VIxfmomWtP0niu6inaPpO1W4fO4MCTaODBpBhkCgYEA3Z1W71N55uoMR+G3DWB7 +NLP+TBCphlFlDmMeCOUEblO63FEFg+Vr4df+iCncMDa4y5xR0ymOPuZT9l9H7GMw +s2LYH8kvnwng7twfzrg0SVoGq/qXDznCE06yFzPbalX9Aiw/YHmnaMicaC39AKcL +7E3VxJHKRaqyMvuWouh+G+8= +-----END PRIVATE KEY----- diff --git a/local/config/san-boutique.cnf b/local/config/san-boutique.cnf new file mode 100644 index 000000000..6d5d68676 --- /dev/null +++ b/local/config/san-boutique.cnf @@ -0,0 +1,18 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +x509_extensions = v3_req +distinguished_name = dn + +[dn] +CN = *.boutique.test +O = Interledger +C = US + +[v3_req] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.boutique.test +DNS.2 = boutique.test diff --git a/local/config/san-testnet.cnf b/local/config/san-testnet.cnf new file mode 100644 index 000000000..697d44066 --- /dev/null +++ b/local/config/san-testnet.cnf @@ -0,0 +1,18 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +x509_extensions = v3_req +distinguished_name = dn + +[dn] +CN = *.testnet.test +O = Interledger +C = US + +[v3_req] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.testnet.test +DNS.2 = testnet.test \ No newline at end of file diff --git a/local/config/traefik/dynamic.yml b/local/config/traefik/dynamic.yml new file mode 100644 index 000000000..5beda1113 --- /dev/null +++ b/local/config/traefik/dynamic.yml @@ -0,0 +1,62 @@ +tls: + certificates: + - certFile: /certs/local.crt + keyFile: /certs/local.key + - certFile: /certs/boutique.crt + keyFile: /certs/boutique.key + stores: + default: + defaultCertificate: + certFile: /certs/local.crt + keyFile: /certs/local.key + +http: + routers: + wallet-frontend: + rule: Host(`testnet.test`) + entryPoints: + - websecure + tls: true + service: wallet-frontend + + wallet-backend: + rule: Host(`api.testnet.test`) + entryPoints: + - websecure + tls: true + service: wallet-backend + + boutique-frontend: + rule: Host(`boutique.test`) + entryPoints: + - websecure + tls: true + service: boutique-frontend + + boutique-backend: + rule: Host(`api.boutique.test`) + entryPoints: + - websecure + tls: true + service: boutique-backend + + services: + wallet-frontend: + loadBalancer: + servers: + - url: http://host.docker.internal:4003 + + wallet-backend: + loadBalancer: + servers: + - url: http://host.docker.internal:3003 + + boutique-frontend: + loadBalancer: + servers: + - url: http://host.docker.internal:4004 + + boutique-backend: + loadBalancer: + servers: + - url: http://host.docker.internal:3004 diff --git a/local/docker-compose.yml b/local/docker-compose.yml new file mode 100644 index 000000000..6e99302f3 --- /dev/null +++ b/local/docker-compose.yml @@ -0,0 +1,19 @@ +include: + - traefik.yaml + - postgres.yaml + - redis.yaml + - mockgatehub.yaml + - rafiki.yaml + - mailslurper.yaml + +networks: + testnet: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/24 + gateway: 10.5.0.1 + +volumes: + pg-data: + redis-data: diff --git a/docker/dbinit.sql b/local/init/dbinit.sql similarity index 100% rename from docker/dbinit.sql rename to local/init/dbinit.sql diff --git a/local/mailslurper.yaml b/local/mailslurper.yaml new file mode 100644 index 000000000..f95a0b1eb --- /dev/null +++ b/local/mailslurper.yaml @@ -0,0 +1,9 @@ +services: + mailslurper: + container_name: mailslurper-local + image: oryd/mailslurper:latest-smtps + ports: + - '4436:4436' + - '4437:4437' + networks: + - testnet diff --git a/local/mockgatehub.yaml b/local/mockgatehub.yaml new file mode 100644 index 000000000..1bf4f9b10 --- /dev/null +++ b/local/mockgatehub.yaml @@ -0,0 +1,36 @@ +services: + # MockGatehub - Mock Gatehub API service for local development + mockgatehub: + container_name: mockgatehub-local + image: ghcr.io/interledger/mockgatehub:1.13 + ports: + - '8080:8080' + environment: + LOG_LEVEL: ${MOCKGATEHUB_LOG_LEVEL:-debug} + MOCKGATEHUB_REDIS_URL: ${MOCKGATEHUB_REDIS_URL:-redis://redis:6379} + MOCKGATEHUB_REDIS_DB: ${MOCKGATEHUB_REDIS_DB:-3} + MOCKGATEHUB_VALID_CREDENTIALS: ${GATEHUB_ACCESS_KEY:-mock_access_key}:${GATEHUB_SECRET_KEY:-mock_secret_key} + WEBHOOK_URL: ${MOCKGATEHUB_WEBHOOK_URL:-http://host.docker.internal:3003/gatehub-webhooks} + WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET:-6d6f636b5f776562686f6f6b5f736563726574} + WEBHOOK_MIN_DELAY_SEC: ${WEBHOOK_MIN_DELAY_SEC:-0.05} + depends_on: + - redis + restart: always + networks: + - testnet + extra_hosts: + - host.docker.internal:host-gateway + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] + interval: 10s + timeout: 5s + retries: 3 + start_interval: 1s + start_period: 15s + labels: + - traefik.enable=true + - traefik.docker.network=testnet + - traefik.http.routers.mockgatehub.rule=Host(`mockgatehub.testnet.test`) + - traefik.http.routers.mockgatehub.entrypoints=websecure + - traefik.http.routers.mockgatehub.tls=true + - traefik.http.services.mockgatehub.loadbalancer.server.port=8080 diff --git a/local/postgres.yaml b/local/postgres.yaml new file mode 100644 index 000000000..a515ad1ff --- /dev/null +++ b/local/postgres.yaml @@ -0,0 +1,15 @@ +services: + postgres: + container_name: postgres-local + image: 'postgres:15' + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + ports: + - '15434:5432' + restart: unless-stopped + networks: + - testnet + volumes: + - pg-data:/var/lib/postgresql/data + - ./init/dbinit.sql:/docker-entrypoint-initdb.d/init.sql diff --git a/local/rafiki.yaml b/local/rafiki.yaml new file mode 100644 index 000000000..25a16e899 --- /dev/null +++ b/local/rafiki.yaml @@ -0,0 +1,186 @@ +services: + # Rafiki + rafiki-auth: + container_name: rafiki-auth-local + image: ghcr.io/interledger/rafiki-auth:v2.2.0-beta + restart: always + networks: + - testnet + ports: + - '3006:3006' + - '3008:3008' + - '3009:3009' + environment: + AUTH_PORT: ${RAFIKI_AUTH_PORT:-3006} + INTROSPECTION_PORT: ${RAFIKI_AUTH_INTROSPECTION_PORT:-3007} + ADMIN_PORT: ${RAFIKI_AUTH_ADMIN_PORT:-3008} + NODE_ENV: ${NODE_ENV:-development} + LOG_LEVEL: ${RAFIKI_AUTH_LOG_LEVEL:-debug} + TRUST_PROXY: ${RAFIKI_AUTH_TRUST_PROXY:-true} + NODE_TLS_REJECT_UNAUTHORIZED: ${RAFIKI_AUTH_NODE_TLS_REJECT_UNAUTHORIZED:-0} + AUTH_SERVER_URL: ${RAFIKI_AUTH_SERVER_URL:-https://auth.testnet.test} + AUTH_DATABASE_URL: ${RAFIKI_AUTH_DATABASE_URL:-postgresql://rafiki_auth:rafiki_auth@postgres-local/rafiki_auth} + IDENTITY_SERVER_URL: ${AUTH_IDENTITY_SERVER_URL:-https://testnet.test/grant-interactions} + IDENTITY_SERVER_SECRET: ${AUTH_IDENTITY_SERVER_SECRET:-dev_identity_server_secret} + COOKIE_KEY: ${AUTH_COOKIE_KEY:-8fd398393c47dd27a3167d9c081c094f} + INTERACTION_COOKIE_SAME_SITE: ${RAFIKI_AUTH_INTERACTION_COOKIE_SAME_SITE:-lax} + WAIT_SECONDS: ${RAFIKI_AUTH_WAIT_SECONDS:-1} + OPERATOR_TENANT_ID: ${RAFIKI_OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + ADMIN_API_SECRET: ${RAFIKI_ADMIN_API_SECRET:-secret-key} + ADMIN_SIGNATURE_VERSION: ${RAFIKI_ADMIN_SIGNATURE_VERSION:-1} + REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + extra_hosts: + - testnet.test:host-gateway + - api.testnet.test:host-gateway + - auth.testnet.test:host-gateway + - rafiki-backend.testnet.test:host-gateway + - rafiki-frontend.testnet.test:host-gateway + - rafiki-card-service.testnet.test:host-gateway + - mockgatehub.testnet.test:host-gateway + - boutique.test:host-gateway + - api.boutique.test:host-gateway + depends_on: + - postgres + labels: + - traefik.enable=true + - traefik.docker.network=testnet + - traefik.http.routers.rafiki-auth-grant.rule=Host(`auth.testnet.test`) && PathPrefix(`/grant`) + - traefik.http.routers.rafiki-auth-grant.entrypoints=websecure + - traefik.http.routers.rafiki-auth-grant.tls=true + - traefik.http.routers.rafiki-auth-grant.priority=100 + - traefik.http.routers.rafiki-auth-grant.service=rafiki-auth-grant + - traefik.http.services.rafiki-auth-grant.loadbalancer.server.port=3009 + - traefik.http.routers.rafiki-auth.rule=Host(`auth.testnet.test`) + - traefik.http.routers.rafiki-auth.entrypoints=websecure + - traefik.http.routers.rafiki-auth.tls=true + - traefik.http.routers.rafiki-auth.service=rafiki-auth + - traefik.http.services.rafiki-auth.loadbalancer.server.port=3006 + + rafiki-backend: + container_name: rafiki-backend-local + image: ghcr.io/interledger/rafiki-backend:v2.2.0-beta + restart: always + privileged: true + volumes: + - ../temp/:/workspace/temp/ + ports: + - '3011:3001' + - '3010:3003' + - '3005:3005' + - '3002:3002' + networks: + - testnet + environment: + NODE_ENV: ${NODE_ENV:-development} + NODE_TLS_REJECT_UNAUTHORIZED: ${RAFIKI_BACKEND_NODE_TLS_REJECT_UNAUTHORIZED:-0} + LOG_LEVEL: ${LOG_LEVEL:-debug} + TRUST_PROXY: ${RAFIKI_BACKEND_TRUST_PROXY:-true} + ADMIN_PORT: ${RAFIKI_BACKEND_ADMIN_PORT:-3001} + CONNECTOR_PORT: ${RAFIKI_BACKEND_CONNECTOR_PORT:-3002} + OPEN_PAYMENTS_PORT: ${RAFIKI_BACKEND_OPEN_PAYMENTS_PORT:-3003} + DATABASE_URL: ${RAFIKI_BACKEND_DATABASE_URL:-postgresql://rafiki_backend:rafiki_backend@postgres-local/rafiki_backend} + USE_TIGERBEETLE: ${RAFIKI_BACKEND_USE_TIGERBEETLE:-false} + NONCE_REDIS_KEY: ${RAFIKI_BACKEND_NONCE_REDIS_KEY:-test} + AUTH_SERVER_GRANT_URL: ${RAFIKI_BACKEND_AUTH_SERVER_GRANT_URL:-https://auth.testnet.test} + AUTH_SERVER_INTROSPECTION_URL: ${RAFIKI_BACKEND_AUTH_SERVER_INTROSPECTION_URL:-http://rafiki-auth:3007} + ILP_ADDRESS: ${RAFIKI_BACKEND_ILP_ADDRESS:-test.net} + ILP_CONNECTOR_URL: ${RAFIKI_BACKEND_ILP_CONNECTOR_URL:-http://127.0.0.1:3002} + STREAM_SECRET: ${RAFIKI_BACKEND_STREAM_SECRET:-BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU=} + ADMIN_KEY: ${RAFIKI_BACKEND_ADMIN_KEY:-admin} + OPEN_PAYMENTS_URL: ${RAFIKI_BACKEND_OPEN_PAYMENTS_URL:-https://rafiki-backend.testnet.test} + REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + WALLET_ADDRESS_URL: ${RAFIKI_BACKEND_WALLET_ADDRESS_URL:-https://rafiki-backend.testnet.test/.well-known/pay} + WEBHOOK_URL: ${RAFIKI_BACKEND_WEBHOOK_URL:-http://host.docker.internal:3003/webhooks} + WEBHOOK_TIMEOUT: ${RAFIKI_BACKEND_WEBHOOK_TIMEOUT:-60000} + SIGNATURE_SECRET: ${RAFIKI_SIGNATURE_SECRET:-327132b5-99e9-4eb8-8a25-2b7d7738ece1} + EXCHANGE_RATES_URL: ${RAFIKI_BACKEND_EXCHANGE_RATES_URL:-http://host.docker.internal:3003/rates} + ENABLE_AUTO_PEERING: ${RAFIKI_BACKEND_ENABLE_AUTO_PEERING:-true} + AUTO_PEERING_SERVER_PORT: ${RAFIKI_BACKEND_AUTO_PEERING_SERVER_PORT:-3005} + INSTANCE_NAME: ${RAFIKI_BACKEND_INSTANCE_NAME:-Testnet Wallet} + SLIPPAGE: ${RAFIKI_BACKEND_SLIPPAGE:-0.01} + KEY_ID: ${RAFIKI_BACKEND_KEY_ID:-rafiki} + WALLET_ADDRESS_REDIRECT_HTML_PAGE: ${RAFIKI_BACKEND_WALLET_ADDRESS_REDIRECT_HTML_PAGE:-https://testnet.test/account?walletAddress=%ewa} + OPERATOR_TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + ADMIN_API_SECRET: ${RAFIKI_ADMIN_API_SECRET:-secret-key} + ADMIN_SIGNATURE_VERSION: ${ADMIN_SIGNATURE_VERSION:-1} + AUTH_SERVICE_API_URL: ${RAFIKI_BACKEND_AUTH_SERVICE_API_URL:-http://rafiki-auth:3011} + CARD_SERVICE_URL: ${RAFIKI_BACKEND_CARD_SERVICE_URL:-http://rafiki-card-service:3007} + CARD_WEBHOOK_SERVICE_URL: ${RAFIKI_BACKEND_CARD_WEBHOOK_SERVICE_URL:-http://rafiki-card-service:3007/webhook} + POS_SERVICE_URL: ${RAFIKI_BACKEND_POS_SERVICE_URL:-http://rafiki-pos-service:3014} + POS_WEBHOOK_SERVICE_URL: ${RAFIKI_BACKEND_POS_WEBHOOK_SERVICE_URL:-http://rafiki-pos-service:3014/web} + depends_on: + - postgres + - redis + extra_hosts: + - host.docker.internal:host-gateway + - testnet.test:host-gateway + - rafiki-backend.testnet.test:host-gateway + - auth.testnet.test:host-gateway + labels: + - traefik.enable=true + - traefik.docker.network=testnet + # Admin GraphQL API (port 3001) — higher priority path match + - traefik.http.routers.rafiki-backend-admin.rule=Host(`rafiki-backend.testnet.test`) && PathPrefix(`/graphql`) + - traefik.http.routers.rafiki-backend-admin.entrypoints=websecure + - traefik.http.routers.rafiki-backend-admin.tls=true + - traefik.http.routers.rafiki-backend-admin.priority=100 + - traefik.http.routers.rafiki-backend-admin.service=rafiki-backend-admin + - traefik.http.services.rafiki-backend-admin.loadbalancer.server.port=3001 + # Open Payments API (port 3003) — catch-all for everything else + - traefik.http.routers.rafiki-backend.rule=Host(`rafiki-backend.testnet.test`) + - traefik.http.routers.rafiki-backend.entrypoints=websecure + - traefik.http.routers.rafiki-backend.tls=true + - traefik.http.routers.rafiki-backend.service=rafiki-backend + - traefik.http.services.rafiki-backend.loadbalancer.server.port=3003 + + rafiki-frontend: + container_name: rafiki-frontend-local + image: ghcr.io/interledger/rafiki-frontend:v2.2.0-beta + depends_on: + - rafiki-backend + restart: always + privileged: true + ports: + - '3012:3012' + networks: + - testnet + environment: + PORT: ${RAFIKI_FRONTEND_PORT:-3012} + GRAPHQL_URL: ${RAFIKI_GRAPHQL_URL:-http://rafiki-backend:3001/graphql} + OPEN_PAYMENTS_URL: ${RAFIKI_FRONTEND_OPEN_PAYMENTS_URL:-https://rafiki-backend.testnet.test/} + ENABLE_INSECURE_MESSAGE_COOKIE: ${RAFIKI_FRONTEND_ENABLE_INSECURE_MESSAGE_COOKIE:-true} + AUTH_ENABLED: ${RAFIKI_FRONTEND_AUTH_ENABLED:-false} + SIGNATURE_VERSION: ${RAFIKI_FRONTEND_SIGNATURE_VERSION:-1} + labels: + - traefik.enable=true + - traefik.docker.network=testnet + - traefik.http.routers.rafiki-frontend.rule=Host(`rafiki-frontend.testnet.test`) + - traefik.http.routers.rafiki-frontend.entrypoints=websecure + - traefik.http.routers.rafiki-frontend.tls=true + - traefik.http.services.rafiki-frontend.loadbalancer.server.port=3012 + + rafiki-card-service: + container_name: rafiki-card-service-local + image: ghcr.io/interledger/rafiki-card-service:v2.2.0-beta + restart: always + privileged: true + networks: + - testnet + ports: + - '3007:3007' + environment: + NODE_ENV: ${NODE_ENV:-development} + LOG_LEVEL: ${LOG_LEVEL:-debug} + CARD_SERVICE_PORT: ${RAFIKI_CARD_SERVICE_PORT:-3007} + REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + GRAPHQL_URL: ${RAFIKI_GRAPHQL_URL:-http://rafiki-backend:3001/graphql} + TENANT_ID: ${OPERATOR_TENANT_ID:-f829c064-762a-4430-ac5d-7af5df198551} + TENANT_SECRET: ${ADMIN_API_SECRET:-secret-key} + TENANT_SIGNATURE_VERSION: ${RAFIKI_CARD_SERVICE_TENANT_SIGNATURE_VERSION:-1} + labels: + - traefik.enable=true + - traefik.docker.network=testnet + - traefik.http.routers.rafiki-card-service.rule=Host(`rafiki-card-service.testnet.test`) + - traefik.http.routers.rafiki-card-service.entrypoints=websecure + - traefik.http.routers.rafiki-card-service.tls=true + - traefik.http.services.rafiki-card-service.loadbalancer.server.port=3007 diff --git a/local/redis.yaml b/local/redis.yaml new file mode 100644 index 000000000..288ea701d --- /dev/null +++ b/local/redis.yaml @@ -0,0 +1,11 @@ +services: + redis: + container_name: redis + image: 'redis:7-alpine' + restart: unless-stopped + networks: + - testnet + ports: + - '6379:6379' + volumes: + - redis-data:/data diff --git a/local/scripts/local-tools.sh b/local/scripts/local-tools.sh new file mode 100644 index 000000000..d2b636ad5 --- /dev/null +++ b/local/scripts/local-tools.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LOCAL_DIR="$ROOT_DIR/local" +CERT_DIR="$LOCAL_DIR/config/certs" +TESTNET_CERT_CRT="$CERT_DIR/local.crt" +TESTNET_CERT_KEY="$CERT_DIR/local.key" +BOUTIQUE_CERT_CRT="$CERT_DIR/boutique.crt" +BOUTIQUE_CERT_KEY="$CERT_DIR/boutique.key" +COMPOSE_FILE="$LOCAL_DIR/docker-compose.yml" +HOST_TAG="# generated by pnpm local:hosts" +TESTNET_CERT_NICKNAME="interledger-testnet-local" +BOUTIQUE_CERT_NICKNAME="interledger-boutique-local" + +HOSTS=( + "testnet.test" + "api.testnet.test" + "auth.testnet.test" + "rafiki-backend.testnet.test" + "rafiki-frontend.testnet.test" + "boutique.test" + "api.boutique.test" + "rafiki-card-service.testnet.test" + "mockgatehub.testnet.test" +) + +# If .env does not exist then create it with touch +if [[ ! -f "$LOCAL_DIR/.env" ]]; then + touch "$LOCAL_DIR/.env" +fi + +compose() { + docker compose -f "$COMPOSE_FILE" \ + --env-file "$LOCAL_DIR/.env.local" \ + --env-file "$LOCAL_DIR/.env" \ + "$@" +} + +reload_traefik_if_running() { + echo "Checking Traefik status..." + if compose ps --status running --services traefik 2>/dev/null | grep -q '^traefik$'; then + echo "Reloading Traefik to pick up trusted cert..." + compose restart traefik + echo "Traefik restarted." + else + echo "Traefik is not running; skipping reload." + fi +} + +run_certs() { + echo "Preparing TLS certificate directory: $CERT_DIR" + mkdir -p "$CERT_DIR" + + local has_testnet_cert=false + local has_boutique_cert=false + [[ -f "$TESTNET_CERT_CRT" && -f "$TESTNET_CERT_KEY" ]] && has_testnet_cert=true + [[ -f "$BOUTIQUE_CERT_CRT" && -f "$BOUTIQUE_CERT_KEY" ]] && has_boutique_cert=true + + if [[ "$has_testnet_cert" == true && "$has_boutique_cert" == true && "${FORCE_CERTS:-0}" != "1" ]]; then + echo "TLS certs already exist. Reusing them (set FORCE_CERTS=1 to regenerate)." + echo " testnet cert : $TESTNET_CERT_CRT" + echo " testnet key : $TESTNET_CERT_KEY" + echo " boutique cert: $BOUTIQUE_CERT_CRT" + echo " boutique key : $BOUTIQUE_CERT_KEY" + return + fi + + if [[ "${FORCE_CERTS:-0}" == "1" ]]; then + echo "FORCE_CERTS=1 detected. Regenerating certificate and key." + else + echo "No existing TLS certificate found. Generating a new one." + fi + + echo "Generating certificates using SAN configs:" + echo " - $LOCAL_DIR/config/san-testnet.cnf" + echo " - $LOCAL_DIR/config/san-boutique.cnf" + + if [[ "${HEADLESS:-0}" == "1" ]]; then + echo "HEADLESS=1: using non-interactive OpenSSL subject values." + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$TESTNET_CERT_KEY" -out "$TESTNET_CERT_CRT" \ + -subj "/CN=*.testnet.test/O=Interledger/C=US" \ + -config "$LOCAL_DIR/config/san-testnet.cnf" + + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$BOUTIQUE_CERT_KEY" -out "$BOUTIQUE_CERT_CRT" \ + -subj "/CN=*.boutique.test/O=Interledger/C=US" \ + -config "$LOCAL_DIR/config/san-boutique.cnf" + else + echo "HEADLESS not set: OpenSSL may prompt depending on your local config." + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$TESTNET_CERT_KEY" -out "$TESTNET_CERT_CRT" \ + -config "$LOCAL_DIR/config/san-testnet.cnf" + + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout "$BOUTIQUE_CERT_KEY" -out "$BOUTIQUE_CERT_CRT" \ + -config "$LOCAL_DIR/config/san-boutique.cnf" + fi + + echo "Generated TLS assets:" + echo " testnet cert : $TESTNET_CERT_CRT" + echo " testnet key : $TESTNET_CERT_KEY" + echo " boutique cert: $BOUTIQUE_CERT_CRT" + echo " boutique key : $BOUTIQUE_CERT_KEY" +} + +run_all() { + echo "Starting full local stack setup..." + run_certs + echo "Bringing up docker compose services from: $COMPOSE_FILE" + compose up -d +} + +run_hosts() { + echo "Checking /etc/hosts entries for testnet domains..." + + local needs_update=false + + # Check for stale entries from old tag format + if grep -q '# generated by make hosts' /etc/hosts 2>/dev/null; then + needs_update=true + fi + + # Check that every expected entry exists + if [[ "$needs_update" == false ]]; then + for host in "${HOSTS[@]}"; do + if ! grep -qF "127.0.0.1 $host $HOST_TAG" /etc/hosts 2>/dev/null; then + needs_update=true + break + fi + done + fi + + if [[ "$needs_update" == false ]]; then + echo "All /etc/hosts entries are already correct. Nothing to do." + return + fi + + echo "Updating /etc/hosts entries for testnet domains (requires sudo)..." + echo "Removing previously managed entries..." + sudo sed -i.bak '/# generated by make hosts/d' /etc/hosts + sudo sed -i.bak '/# generated by pnpm local:hosts/d' /etc/hosts + sudo rm -f /etc/hosts.bak + + for host in "${HOSTS[@]}"; do + echo "Adding host mapping: 127.0.0.1 $host" + echo "127.0.0.1 $host $HOST_TAG" | sudo tee -a /etc/hosts >/dev/null + done + + echo "Host aliases updated successfully." +} + +run_trust_macos() { + echo "Applying certificate trust for macOS system keychain..." + run_certs + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$TESTNET_CERT_CRT" + echo "macOS trust entry installed for: $TESTNET_CERT_CRT" + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$BOUTIQUE_CERT_CRT" + echo "macOS trust entry installed for: $BOUTIQUE_CERT_CRT" + reload_traefik_if_running +} + +run_trust_linux() { + echo "Applying certificate trust for Linux..." + run_certs + + echo "Installing cert into system CA store: /usr/local/share/ca-certificates/interledger-testnet-local.crt" + sudo cp "$TESTNET_CERT_CRT" /usr/local/share/ca-certificates/interledger-testnet-local.crt + echo "Installing cert into system CA store: /usr/local/share/ca-certificates/interledger-boutique-local.crt" + sudo cp "$BOUTIQUE_CERT_CRT" /usr/local/share/ca-certificates/interledger-boutique-local.crt + echo "Refreshing system CA certificates..." + sudo update-ca-certificates + + if ! command -v certutil >/dev/null 2>&1; then + echo "certutil not found; install 'libnss3-tools' to auto-trust certs in Chrome/Chromium NSS stores." + reload_traefik_if_running + return + fi + + local nss_dbs=( + "$HOME/.pki/nssdb" + "$HOME/snap/chromium/current/.pki/nssdb" + "$HOME/snap/google-chrome/current/.pki/nssdb" + "$HOME/.var/app/com.google.Chrome/config/google-chrome/.pki/nssdb" + "$HOME/.var/app/org.chromium.Chromium/config/chromium/.pki/nssdb" + ) + local nss_pass_file + nss_pass_file="$(mktemp)" + printf '\n' >"$nss_pass_file" + + echo "Importing cert into NSS databases when accessible..." + + for db in "${nss_dbs[@]}"; do + # Always manage the default NSS DB; only manage app-specific DBs when present. + if [[ "$db" != "$HOME/.pki/nssdb" && ! -d "$db" ]]; then + echo "Skipping NSS DB (not present): $db" + continue + fi + + echo "Processing NSS DB: $db" + mkdir -p "$db" + + if [[ ! -f "$db/cert9.db" || ! -f "$db/key4.db" || ! -f "$db/pkcs11.txt" ]]; then + echo "Initializing NSS DB with empty password: $db" + if ! certutil -d sql:"$db" -N --empty-password >/dev/null 2>&1; then + echo "Skipping NSS DB (failed to initialize): $db" + continue + fi + fi + + certutil -d sql:"$db" -D -n "$TESTNET_CERT_NICKNAME" -f "$nss_pass_file" >/dev/null 2>&1 || true + certutil -d sql:"$db" -D -n "$BOUTIQUE_CERT_NICKNAME" -f "$nss_pass_file" >/dev/null 2>&1 || true + + if certutil -d sql:"$db" -A -t "C,," -n "$TESTNET_CERT_NICKNAME" -i "$TESTNET_CERT_CRT" -f "$nss_pass_file" >/dev/null 2>&1; then + echo "Trusted testnet cert in NSS DB: $db" + else + echo "Skipping testnet cert in NSS DB (password-protected or inaccessible): $db" + fi + + if certutil -d sql:"$db" -A -t "C,," -n "$BOUTIQUE_CERT_NICKNAME" -i "$BOUTIQUE_CERT_CRT" -f "$nss_pass_file" >/dev/null 2>&1; then + echo "Trusted boutique cert in NSS DB: $db" + else + echo "Skipping boutique cert in NSS DB (password-protected or inaccessible): $db" + fi + done + + echo "Cleaning up temporary NSS password file." + rm -f "$nss_pass_file" + + reload_traefik_if_running +} + +run_trust() { + echo "Applying trust configuration for local TLS certificate..." + run_certs + + if [[ "$(uname -s)" == "Darwin" ]]; then + echo "Detected OS: macOS" + run_trust_macos + else + echo "Detected OS: Linux" + run_trust_linux + fi +} + +run_help() { + cat <<'EOF' +pnpm run + +e.g. pnpm local:all + +local:help Show this help message +local:all Start full local stack (with Traefik) +local:all-nowatch Alias for local:all +local:build Build docker images for local stack +local:rebuild Force rebuild docker images (no cache) +local:rafiki-assets Run Rafiki asset setup script +local:down Stop the local stack +local:reset Stop stack and remove volumes +local:hosts Add testnet host aliases to /etc/hosts (requires sudo) +local:certs Generate TLS cert if missing (set FORCE_CERTS=1 to regenerate) +local:trust Trust local TLS certificate (auto-detect OS, reloads Traefik) +local:trust:macos Trust certificate on macOS +local:trust:linux Trust certificate on Debian-based Linux (reloads Traefik) +EOF +} + +case "${1:-help}" in + help) + run_help + ;; + all) + run_all + ;; + certs) + run_certs + ;; + hosts) + run_hosts + ;; + trust) + run_trust + ;; + trust-macos) + run_trust_macos + ;; + trust-linux) + run_trust_linux + ;; + *) + echo "Unknown command: $1" >&2 + echo "Run: pnpm local:help" >&2 + exit 1 + ;; +esac diff --git a/local/scripts/rafiki-setup.js b/local/scripts/rafiki-setup.js new file mode 100644 index 000000000..c4e9a3a8d --- /dev/null +++ b/local/scripts/rafiki-setup.js @@ -0,0 +1,542 @@ +#!/usr/bin/env node +/** + * Configure Rafiki (local docker stack) with a tenant + assets. + * - Reads values from .env in this directory (local/scripts/.env) when present (process.env takes priority) + * - Creates the operator tenant (idpConsentUrl + idpSecret) + * - Ensures assets exist for the Testnet wallet + * + * Run after `docker compose up -d` from local/: + * node scripts/rafiki-setup.js + */ + +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') + +// ---- helpers --------------------------------------------------------------- +function nowIso() { + return new Date().toISOString() +} + +function logInfo(message, details) { + if (details === undefined) { + console.log(`[${nowIso()}] ${message}`) + return + } + console.log(`[${nowIso()}] ${message}`, details) +} + +function maskSecret(value) { + if (!value) { + return '' + } + if (value.length <= 4) { + return '*'.repeat(value.length) + } + return `${value.slice(0, 2)}***${value.slice(-2)}` +} + +function inferOperationName(query, operationName) { + if (operationName) { + return operationName + } + const match = query.match(/\b(query|mutation)\s+([A-Za-z0-9_]+)/) + return match?.[2] ?? 'anonymousOperation' +} + +function loadDotEnv(envPath) { + const result = {} + if (!fs.existsSync(envPath)) { + logInfo(`No .env file found at ${envPath}, using process env/defaults`) + return result + } + logInfo(`Loading .env from ${envPath}`) + const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/) + for (const line of lines) { + if (!line || line.trim().startsWith('#')) { + continue + } + const idx = line.indexOf('=') + if (idx === -1) { + continue + } + const key = line.slice(0, idx).trim() + const value = line.slice(idx + 1).trim() + result[key] = value + } + + return result +} + +function canonicalize(value) { + if (value === null || typeof value !== 'object') { + return value + } + if (Array.isArray(value)) { + return value.map(canonicalize) + } + const sortedKeys = Object.keys(value).sort() + const obj = {} + for (const key of sortedKeys) { + obj[key] = canonicalize(value[key]) + } + return obj +} + +function canonicalizeAndStringify(value) { + return JSON.stringify(canonicalize(value)) +} + +function buildEnv() { + const envPath = path.join(__dirname, '.env') + const fileEnv = loadDotEnv(envPath) + const get = (key, fallback) => process.env[key] ?? fileEnv[key] ?? fallback + + const env = { + GRAPHQL_ENDPOINT: get('GRAPHQL_ENDPOINT', 'http://localhost:3011/graphql'), + RAFIKI_HEALTH_ENDPOINT: get( + 'RAFIKI_HEALTH_ENDPOINT', + 'http://localhost:3011/healthz' + ), + ADMIN_API_SECRET: get('ADMIN_API_SECRET', 'secret-key'), + ADMIN_SIGNATURE_VERSION: get('ADMIN_SIGNATURE_VERSION', '1'), + OPERATOR_TENANT_ID: get( + 'OPERATOR_TENANT_ID', + 'f829c064-762a-4430-ac5d-7af5df198551' + ), + AUTH_IDENTITY_SERVER_SECRET: get( + 'AUTH_IDENTITY_SERVER_SECRET', + 'dev_identity_server_secret' + ), + IDP_CONSENT_URL: get( + 'IDP_CONSENT_URL', + 'https://testnet.test/grant-interactions' + ) + } + + logInfo('Resolved configuration (sensitive values masked):', { + GRAPHQL_ENDPOINT: env.GRAPHQL_ENDPOINT, + RAFIKI_HEALTH_ENDPOINT: env.RAFIKI_HEALTH_ENDPOINT, + ADMIN_SIGNATURE_VERSION: env.ADMIN_SIGNATURE_VERSION, + OPERATOR_TENANT_ID: env.OPERATOR_TENANT_ID, + IDP_CONSENT_URL: env.IDP_CONSENT_URL, + ADMIN_API_SECRET: maskSecret(env.ADMIN_API_SECRET), + AUTH_IDENTITY_SERVER_SECRET: maskSecret(env.AUTH_IDENTITY_SERVER_SECRET) + }) + + return env +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function waitForRafikiHealth(env) { + const initialDelayMs = 3000 + const maxAttempts = 30 + const retryIntervalMs = 2000 + + logInfo( + `Waiting ${initialDelayMs / 1000}s for containers to initialise before polling health` + ) + await sleep(initialDelayMs) + + logInfo('Polling Rafiki health endpoint for OK') + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + logInfo( + `Checking Rafiki health (${attempt}/${maxAttempts}): ${env.RAFIKI_HEALTH_ENDPOINT}` + ) + + try { + const response = await fetch(env.RAFIKI_HEALTH_ENDPOINT, { + method: 'GET', + signal: AbortSignal.timeout(2000) + }) + + if (!response.ok) { + logInfo(`Health check returned HTTP ${response.status}, retrying...`) + } else { + const body = (await response.text()).trim() + if (body.toUpperCase() === 'OK') { + logInfo('Rafiki health check passed') + return + } + logInfo(`Health check returned '${body}', waiting for 'OK'...`) + } + } catch (err) { + logInfo(`Health check failed: ${err.message}`) + } + + if (attempt < maxAttempts) { + logInfo(`Rafiki not ready yet, retrying in ${retryIntervalMs / 1000}s...`) + await sleep(retryIntervalMs) + } + } + + throw new Error( + `Rafiki health endpoint did not return OK after ${initialDelayMs / 1000}s initial wait + ${maxAttempts} attempts` + ) +} + +function signRequest({ query, variables, operationName }, env, timestamp) { + const payload = `${timestamp}.${canonicalizeAndStringify({ + variables: variables ?? {}, + operationName, + query + })}` + const hmac = crypto.createHmac('sha256', env.ADMIN_API_SECRET) + hmac.update(payload) + const digest = hmac.digest('hex') + return `t=${timestamp}, v${env.ADMIN_SIGNATURE_VERSION}=${digest}` +} + +async function graphqlRequest({ query, variables, operationName }, env) { + const timestamp = Date.now() + const opName = inferOperationName(query, operationName) + logInfo(`GraphQL -> ${opName} (request signing + dispatch)`) + const signature = signRequest( + { query, variables, operationName }, + env, + timestamp + ) + const body = JSON.stringify({ query, variables, operationName }) + + const response = await fetch(env.GRAPHQL_ENDPOINT, { + method: 'POST', + headers: { + 'content-type': 'application/json', + signature, + 'tenant-id': env.OPERATOR_TENANT_ID + }, + body + }) + + logInfo(`GraphQL <- ${opName} HTTP ${response.status}`) + + if (!response.ok) { + const text = await response.text().catch(() => '') + throw new Error(`${opName} HTTP ${response.status}: ${text.slice(0, 500)}`) + } + + const data = await response.json() + if (data.errors && data.errors.length) { + const message = data.errors.map((e) => e.message).join('\n') + logInfo(`GraphQL !! ${opName} returned ${data.errors.length} error(s)`) + throw new Error(message) + } + logInfo(`GraphQL ok ${opName}`) + return data.data +} + +// ---- operations ----------------------------------------------------------- +const getTenantQuery = /* GraphQL */ ` + query GetTenant($id: String!) { + tenant(id: $id) { + id + publicName + idpConsentUrl + idpSecret + } + } +` + +const createTenantMutation = /* GraphQL */ ` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + publicName + idpConsentUrl + idpSecret + } + } + } +` + +const updateTenantMutation = /* GraphQL */ ` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + publicName + idpConsentUrl + idpSecret + } + } + } +` + +const listAssetsQuery = /* GraphQL */ ` + query Assets($first: Int = 100) { + assets(first: $first) { + edges { + node { + id + code + scale + } + } + } + } +` + +const createAssetMutation = /* GraphQL */ ` + mutation CreateAsset($input: CreateAssetInput!) { + createAsset(input: $input) { + asset { + id + code + scale + } + } + } +` + +const getAssetByCodeAndScaleQuery = /* GraphQL */ ` + query AssetByCodeAndScale($code: String!, $scale: UInt8!) { + assetByCodeAndScale(code: $code, scale: $scale) { + id + code + scale + liquidity + } + } +` + +const depositAssetLiquidityMutation = /* GraphQL */ ` + mutation DepositAssetLiquidity($input: DepositAssetLiquidityInput!) { + depositAssetLiquidity(input: $input) { + success + } + } +` + +const assetsToEnsure = [ + { code: 'USD', scale: 2 }, + { code: 'EUR', scale: 2 } + // { code: 'GBP', scale: 2 }, + // { code: 'ZAR', scale: 2 }, + // { code: 'MXN', scale: 2 }, + // { code: 'SGD', scale: 2 }, + // { code: 'CAD', scale: 2 }, + // { code: 'EGG', scale: 2 }, + // { code: 'PEB', scale: 2 }, + // { code: 'PKR', scale: 2 } +] + +async function ensureTenant(env) { + logInfo( + 'Step 1/3: Ensuring operator tenant exists and has correct IdP settings' + ) + try { + const existing = await graphqlRequest( + { query: getTenantQuery, variables: { id: env.OPERATOR_TENANT_ID } }, + env + ) + if (existing?.tenant) { + logInfo( + `Tenant already present: ${existing.tenant.id} (consent URL ${existing.tenant.idpConsentUrl})` + ) + if ( + !existing.tenant.idpConsentUrl || + !existing.tenant.idpSecret || + existing.tenant.idpConsentUrl !== env.IDP_CONSENT_URL + ) { + logInfo('Tenant IdP fields are missing/stale, updating tenant...') + await graphqlRequest( + { + query: updateTenantMutation, + variables: { + input: { + id: env.OPERATOR_TENANT_ID, + idpConsentUrl: env.IDP_CONSENT_URL, + idpSecret: env.AUTH_IDENTITY_SERVER_SECRET + } + } + }, + env + ) + logInfo('Tenant IdP fields updated') + } else { + logInfo('Tenant IdP fields already match desired configuration') + } + return + } + } catch (err) { + // continue and try to create + logInfo('Tenant lookup failed, attempting to create...', err.message) + } + + logInfo('Creating tenant...') + try { + const created = await graphqlRequest( + { + query: createTenantMutation, + variables: { + input: { + id: env.OPERATOR_TENANT_ID, + publicName: 'Testnet Wallet', + apiSecret: env.ADMIN_API_SECRET, + idpSecret: env.AUTH_IDENTITY_SERVER_SECRET, + idpConsentUrl: env.IDP_CONSENT_URL + } + } + }, + env + ) + logInfo('Tenant created:', created.createTenant.tenant) + } catch (err) { + if ( + typeof err.message === 'string' && + err.message.toLowerCase().includes('duplicate') + ) { + logInfo('Tenant already exists (duplicate key), continuing...') + return + } + throw err + } +} + +async function ensureAssets(env) { + logInfo('Step 2/3: Ensuring required assets exist') + logInfo('Target assets:', assetsToEnsure) + let current = { assets: { edges: [] } } + try { + current = await graphqlRequest( + { query: listAssetsQuery, variables: { first: 200 } }, + env + ) + logInfo( + `Fetched ${(current?.assets?.edges ?? []).length} existing asset(s) from Rafiki` + ) + } catch (err) { + logInfo('Asset list failed, continuing to create assets...', err.message) + } + + const existingAssets = new Set( + (current?.assets?.edges ?? []).map((e) => `${e.node.code}:${e.node.scale}`) + ) + + for (const asset of assetsToEnsure) { + if (existingAssets.has(`${asset.code}:${asset.scale}`)) { + logInfo(`Asset ${asset.code} (scale ${asset.scale}) already exists`) + continue + } + logInfo(`Creating asset ${asset.code}...`) + try { + await graphqlRequest( + { + query: createAssetMutation, + variables: { + input: { + code: asset.code, + scale: asset.scale + } + } + }, + env + ) + logInfo(`Asset ${asset.code} created`) + } catch (err) { + const msg = (err.message || '').toLowerCase() + if (msg.includes('already exists') || msg.includes('duplicate')) { + logInfo(`Asset ${asset.code} already exists (api), continuing...`) + continue + } + throw err + } + } +} + +// Deposit liquidity for all assets (100000 units per asset, converted to minor units by scale) +async function ensureLiquidity(env) { + logInfo('Step 3/3: Ensuring asset liquidity is deposited') + const failures = [] + + for (const asset of assetsToEnsure) { + let node + try { + const res = await graphqlRequest( + { + query: getAssetByCodeAndScaleQuery, + variables: { code: asset.code, scale: asset.scale } + }, + env + ) + node = res?.assetByCodeAndScale + } catch (err) { + logInfo(`Lookup failed for ${asset.code}:`, err.message) + failures.push(asset.code) + continue + } + + if (!node?.id) { + logInfo(`Asset ${asset.code} not found, cannot deposit liquidity`) + failures.push(asset.code) + continue + } + + // Skip if asset already has liquidity + if (node.liquidity && BigInt(node.liquidity) > 0n) { + logInfo( + `Asset ${asset.code} already has liquidity (${node.liquidity}), skipping` + ) + continue + } + + // Amount in minor units: 100000 * 10^scale + const amount = BigInt(100000) * BigInt(10) ** BigInt(node.scale) + + logInfo( + `Depositing liquidity for ${asset.code}: ${amount.toString()} (scale ${node.scale})` + ) + try { + const res = await graphqlRequest( + { + query: depositAssetLiquidityMutation, + variables: { + input: { + id: crypto.randomUUID(), + assetId: node.id, + amount: amount.toString(), + idempotencyKey: crypto.randomUUID() + } + } + }, + env + ) + + if (!res?.depositAssetLiquidity?.success) { + logInfo(`Liquidity deposit returned success=false for ${asset.code}`) + failures.push(asset.code) + } else { + logInfo(`Liquidity deposited for ${asset.code}`) + } + } catch (err) { + logInfo(`Liquidity deposit error for ${asset.code}:`, err.message) + failures.push(asset.code) + } + } + + if (failures.length) { + throw new Error( + `Liquidity deposit failed for asset(s): ${failures.join(', ')}` + ) + } +} + +// ---- main ----------------------------------------------------------------- +;(async function main() { + const env = buildEnv() + logInfo('Starting Rafiki setup script') + logInfo('Rafiki admin endpoint:', env.GRAPHQL_ENDPOINT) + await waitForRafikiHealth(env) + await ensureTenant(env) + await ensureAssets(env) + await ensureLiquidity(env) + logInfo('Rafiki configuration complete') +})().catch((err) => { + console.error(`[${nowIso()}] Setup failed:`, err.message) + process.exit(1) +}) diff --git a/local/traefik.yaml b/local/traefik.yaml new file mode 100644 index 000000000..40be9e7f4 --- /dev/null +++ b/local/traefik.yaml @@ -0,0 +1,27 @@ +services: + traefik: + container_name: traefik-local + image: traefik:v3.6 + command: + - --api.dashboard=true + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.file.directory=/etc/traefik/dynamic + - --providers.file.watch=true + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + ports: + - '80:80' + - '443:443' + - '8081:8080' + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./config/certs:/certs:ro + - ./config/traefik/dynamic.yml:/etc/traefik/dynamic/dynamic.yml:ro + extra_hosts: + - host.docker.internal:host-gateway + restart: unless-stopped + networks: + - testnet diff --git a/package.json b/package.json index 3ab353a94..63abdd76b 100644 --- a/package.json +++ b/package.json @@ -13,26 +13,38 @@ "boutique:frontend": "pnpm --filter @boutique/frontend --", "build": "pnpm -r build", "checks": "pnpm prettier:check && pnpm lint:check", + "test": "pnpm test:unit", + "test:unit": "pnpm boutique:backend test && pnpm wallet:backend test", "clean": "pnpm clean:modules && pnpm clean:builds", "clean:builds": "find . \\( -name \"dist\" -o -name \".next\" \\) -type d -prune -exec rm -rf '{}' +", "clean:modules": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +", - "dev": "pnpm localenv:start && concurrently -n \"WALLET,BOUTIQUE\" -c blue.bold,red.bold \"pnpm wallet:frontend dev\" \"pnpm boutique:frontend dev\"", - "dev:debug": "pnpm localenv:start:debug && concurrently -n \"WALLET,BOUTIQUE\" -c blue.bold,red.bold \"pnpm wallet:frontend dev\" \"pnpm boutique:frontend dev\"", - "dev:lite": "pnpm localenv:start:lite && concurrently -n \"WALLET,BOUTIQUE\" -c blue.bold,red.bold \"pnpm wallet:frontend dev\" \"pnpm boutique:frontend dev\"", + "dev": "pnpm local:up && concurrently -n \"WALLET-BE,WALLET-FE,BOUTIQUE-BE,BOUTIQUE-FE\" -c blue.bold,blue,red.bold,red \"pnpm wallet:backend dev\" \"pnpm wallet:frontend dev\" \"pnpm boutique:backend dev\" \"pnpm boutique:frontend dev\"", + "dev:debug": "pnpm local:up:debug && concurrently -n \"WALLET-BE,WALLET-FE,BOUTIQUE-BE,BOUTIQUE-FE\" -c blue.bold,blue,red.bold,red \"pnpm wallet:backend dev\" \"pnpm wallet:frontend dev\" \"pnpm boutique:backend dev\" \"pnpm boutique:frontend dev\"", + "dev:lite": "pnpm local:up:lite && concurrently -n \"WALLET-BE,WALLET-FE,BOUTIQUE-BE,BOUTIQUE-FE\" -c blue.bold,blue,red.bold,red \"pnpm wallet:backend dev\" \"pnpm wallet:frontend dev\" \"pnpm boutique:backend dev\" \"pnpm boutique:frontend dev\"", "format": "pnpm prettier:write && pnpm lint:fix", + "lint": "pnpm lint:check", "lint:check": "eslint --max-warnings=0 .", "lint:fix": "eslint --max-warnings=0 --fix .", - "compose": "docker compose -f ./docker/dev/docker-compose.yml", - "compose:prod": "docker compose -f ./docker/prod/docker-compose.yml", - "localenv:start": "cross-env DEV_MODE=hot-reload pnpm compose up -d --build", - "localenv:start:debug": "cross-env DEV_MODE=debug pnpm compose up -d --build", - "localenv:start:lite": "cross-env DEV_MODE=lite pnpm compose up -d --build", - "localenv:stop": "pnpm compose down", + "compose": "docker compose -f ./local/docker-compose.yml --env-file ./local/.env.local --env-file ./local/.env", + "local:help": "bash ./local/scripts/local-tools.sh help", + "local:all": "bash ./local/scripts/local-tools.sh all", + "local:pull": "pnpm compose pull", + "local:build": "pnpm compose build", + "local:rebuild": "pnpm compose build --no-cache", + "local:up": "pnpm compose up -d --build", + "local:down": "pnpm compose down", + "local:reset": "pnpm compose down -v", + "local:logs": "pnpm compose logs -f", + "local:certs": "bash ./local/scripts/local-tools.sh certs", + "local:hosts": "bash ./local/scripts/local-tools.sh hosts", + "local:trust": "bash ./local/scripts/local-tools.sh trust", + "local:setup": "pnpm compose pull && pnpm local:hosts && pnpm local:trust && pnpm local:all && pnpm local:rafiki-assets", + "local:trust:macos": "bash ./local/scripts/local-tools.sh trust-macos", + "local:trust:linux": "bash ./local/scripts/local-tools.sh trust-linux", + "local:rafiki-assets": "node ./local/scripts/rafiki-setup.js", "preinstall": "npx only-allow pnpm", "prettier:write": "prettier --config \".prettierrc.js\" --write .", "prettier:check": "prettier --config \".prettierrc.js\" --check .", - "prod": "pnpm compose:prod up -d --build", - "prod:down": "pnpm compose:prod down", "wallet:backend": "pnpm --filter @wallet/backend --", "wallet:frontend": "pnpm --filter @wallet/frontend --" }, @@ -40,8 +52,8 @@ "@eslint/compat": "^1.4.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.32.0", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "concurrently": "^9.2.1", "cross-env": "^7.0.3", "eslint": "^9.32.0", diff --git a/packages/boutique/backend/.env.example b/packages/boutique/backend/.env.example new file mode 100644 index 000000000..4c4930eb9 --- /dev/null +++ b/packages/boutique/backend/.env.example @@ -0,0 +1,24 @@ +# You can copy this file to .env.local and adjust values to run the backend locally. +# If you create both .env.local and .env, values in .env override values from .env.local. +# Do not commit sensitive real credentials. + +########## INFRASTRUCTURE ####################################################################### +## chances are you dont need to change these for local development, but you can if you want to +## run the backend against a different database or redis instance. + +# PORT=3004 +# NODE_ENV=development +# NODE_TLS_REJECT_UNAUTHORIZED=0 +# FRONTEND_URL=https://boutique.test +# DATABASE_URL=postgres://boutique_backend:boutique_backend@localhost:15434/boutique_backend +# REDIS_URL=redis://localhost:6379/4 + +######### OPEN PAYMENTS CONFIGURATION to make Boutique work ###################################### +## You need to create a wallet using https://testnet.test and then create Developer keys for it. + +## Note that the payment pointer needs to be prefixed with `https://` and not `$` as is common when +## writing payment pointers. The backend will convert it to the correct format when fetching the +## wallet address and signing requests. +# PAYMENT_POINTER=https://change-me +# KEY_ID=guid-here +# PRIVATE_KEY=base64-encoded-private-key-here diff --git a/packages/boutique/backend/.env.local b/packages/boutique/backend/.env.local new file mode 100644 index 000000000..c956e7c8f --- /dev/null +++ b/packages/boutique/backend/.env.local @@ -0,0 +1,9 @@ +PORT=3004 +NODE_ENV=development +NODE_TLS_REJECT_UNAUTHORIZED=0 +FRONTEND_URL=https://boutique.test +DATABASE_URL=postgres://boutique_backend:boutique_backend@localhost:15434/boutique_backend +PAYMENT_POINTER=https://rafiki-backend.testnet.test/341c893c +KEY_ID=122b8663-f636-4934-807f-656219d60e6b +PRIVATE_KEY=LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSU4wUzJZdXJRelM3ZktzZ3RKYmY3VS9ZWVhGWnNwbjFSYTZvWWViVXFiS1IKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQ== +REDIS_URL=redis://localhost:6379/4 diff --git a/packages/boutique/backend/Dockerfile.dev b/packages/boutique/backend/Dockerfile.dev index b55f45b98..3fe3ed21e 100644 --- a/packages/boutique/backend/Dockerfile.dev +++ b/packages/boutique/backend/Dockerfile.dev @@ -21,7 +21,7 @@ RUN pnpm fetch ADD . ./ # Install packages from virtual store -RUN pnpm install -r --offline +RUN pnpm install --filter @boutique/backend... --offline RUN pnpm boutique:backend build diff --git a/packages/boutique/backend/jest.setup.js b/packages/boutique/backend/jest.setup.js index be13fe3c6..28d9804ef 100644 --- a/packages/boutique/backend/jest.setup.js +++ b/packages/boutique/backend/jest.setup.js @@ -4,6 +4,7 @@ const { randomBytes, generateKeyPairSync } = require('crypto') const POSTGRES_PASSWORD = 'password' const POSTGRES_DB = randomBytes(16).toString('hex') const POSTGRES_PORT = 5432 +const REDIS_PORT = 6379 module.exports = async () => { const container = await new GenericContainer('postgres:15') @@ -14,10 +15,20 @@ module.exports = async () => { .withExposedPorts(POSTGRES_PORT) .start() + const redisContainer = await new GenericContainer('redis:7') + .withExposedPorts(REDIS_PORT) + .start() + + // env.ts requires these variables without defaults, so we supply + // test-safe values here to prevent process.exit(1) during import. + process.env.PORT = '0' + process.env.NODE_ENV = 'test' + process.env.FRONTEND_URL = 'http://localhost:4004' process.env.DATABASE_URL = `postgresql://postgres:${POSTGRES_PASSWORD}@localhost:${container.getMappedPort( POSTGRES_PORT )}/${POSTGRES_DB}` - + process.env.PAYMENT_POINTER = 'https://ilp.interledger-test.dev/boutique' + process.env.KEY_ID = 'test-key-id' process.env.PRIVATE_KEY = Buffer.from( generateKeyPairSync('ed25519') .privateKey.export({ @@ -26,6 +37,8 @@ module.exports = async () => { }) .trim() ).toString('base64') + process.env.REDIS_URL = `redis://localhost:${redisContainer.getMappedPort(REDIS_PORT)}/0` global.__POSTGRES_CONTAINER__ = container + global.__TESTING_REDIS_CONTAINER__ = redisContainer } diff --git a/packages/boutique/backend/jest.teardown.js b/packages/boutique/backend/jest.teardown.js index 77def90f1..9b3c3afb6 100644 --- a/packages/boutique/backend/jest.teardown.js +++ b/packages/boutique/backend/jest.teardown.js @@ -1,5 +1,8 @@ module.exports = async () => { - if (global.__TESTING_POSTGRES_CONTAINER__) { - await global.__TESTING_POSTGRES_CONTAINER__.stop() + if (global.__POSTGRES_CONTAINER__) { + await global.__POSTGRES_CONTAINER__.stop() + } + if (global.__TESTING_REDIS_CONTAINER__) { + await global.__TESTING_REDIS_CONTAINER__.stop() } } diff --git a/packages/boutique/backend/package.json b/packages/boutique/backend/package.json index 256ee7c42..eba353ae5 100644 --- a/packages/boutique/backend/package.json +++ b/packages/boutique/backend/package.json @@ -1,10 +1,10 @@ { "name": "@boutique/backend", "scripts": { - "start": "node dist/index.js", + "start": "sh -c 'set --; [ -f .env.local ] && set -- \"$@\" --env-file=.env.local; [ -f .env ] && set -- \"$@\" --env-file=.env; node \"$@\" dist/index.js'", "build:deps": "pnpm --filter @shared/backend build && pnpm --filter @boutique/shared build", "build": "pnpm build:deps && tsc --build tsconfig.build.json && tsc-alias -p tsconfig.build.json", - "dev": "nodemon --watch ./src --watch ../shared/src --ext ts,json --delay 0.5 --exec \"pnpm run build && node ./dist/index.js\"", + "dev": "nodemon --watch ./src --watch ../shared/src --ext ts,json --delay 0.5 --exec \"sh -c 'set --; [ -f .env.local ] && set -- \\$@ --env-file=.env.local; [ -f .env ] && set -- \\$@ --env-file=.env; pnpm run build && node \\$@ ./dist/index.js'\"", "test": "pnpm build:deps && NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests --maxWorkers=2" }, "dependencies": { diff --git a/packages/boutique/backend/src/app.ts b/packages/boutique/backend/src/app.ts index 20231266f..2e142c991 100644 --- a/packages/boutique/backend/src/app.ts +++ b/packages/boutique/backend/src/app.ts @@ -49,10 +49,11 @@ export class App { const logger = this.container.resolve('logger') const productController = this.container.resolve('productController') const orderController = this.container.resolve('orderController') + const frontendOrigin = new URL(env.FRONTEND_URL).origin app.use( cors({ - origin: [env.FRONTEND_URL], + origin: [frontendOrigin], credentials: true }) ) diff --git a/packages/boutique/backend/src/config/env.ts b/packages/boutique/backend/src/config/env.ts index 1ad41b325..48496f090 100644 --- a/packages/boutique/backend/src/config/env.ts +++ b/packages/boutique/backend/src/config/env.ts @@ -1,16 +1,41 @@ import { z } from 'zod' +const requiredString = z.string().trim().min(1) +const httpsUrlString = z + .string() + .trim() + .url() + .refine((value) => value.startsWith('https://'), { + message: + 'PAYMENT_POINTER must be a URL starting with https:// instead of tje classic "$" format' + }) +const base64String = requiredString.refine( + (value) => { + if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) { + return false + } + + try { + return Buffer.from(value, 'base64').toString('base64') === value + } catch { + return false + } + }, + { + message: 'PRIVATE_KEY must be a valid base64-encoded string' + } +) + const envSchema = z.object({ - PORT: z.coerce.number().default(3004), - NODE_ENV: z.string().default('development'), - FRONTEND_URL: z.string().default('http://localhost:4004'), - DATABASE_URL: z - .string() - .default('postgres://postgres:password@localhost:5433/boutique_backend'), - PAYMENT_POINTER: z.string().default('replace-me'), - KEY_ID: z.string().default('replace-me'), - PRIVATE_KEY: z.string().default('replace-me'), - REDIS_URL: z.string().default('redis://redis:6379/0') + PORT: z.coerce.number(), + NODE_ENV: z.string().min(1), + FRONTEND_URL: z.string().url(), + DATABASE_URL: z.string().url(), + PAYMENT_POINTER: httpsUrlString, + KEY_ID: requiredString, + PRIVATE_KEY: base64String, + REDIS_URL: z.string().url(), + USE_HTTP_FOR_OPEN_PAYMENTS: z.boolean().optional() }) export type Env = z.infer diff --git a/packages/boutique/backend/src/container.ts b/packages/boutique/backend/src/container.ts index 81e766ac8..8abe12a08 100644 --- a/packages/boutique/backend/src/container.ts +++ b/packages/boutique/backend/src/container.ts @@ -55,7 +55,7 @@ export async function createContainer( keyId: env.KEY_ID, privateKey: Buffer.from(env.PRIVATE_KEY, 'base64'), walletAddressUrl: env.PAYMENT_POINTER, - useHttp: env.NODE_ENV === 'development' + useHttp: env.USE_HTTP_FOR_OPEN_PAYMENTS || false }) container.register({ diff --git a/packages/boutique/backend/src/open-payments/service.ts b/packages/boutique/backend/src/open-payments/service.ts index 40fee3682..45703ba68 100644 --- a/packages/boutique/backend/src/open-payments/service.ts +++ b/packages/boutique/backend/src/open-payments/service.ts @@ -192,7 +192,7 @@ export class OpenPayments implements IOpenPayments { ) throw new InternalServerError() }) - } catch (err) { + } catch { throw new InternalServerError() } } @@ -525,8 +525,8 @@ export class OpenPayments implements IOpenPayments { } } ) - .catch(() => { - this.logger.error('Unable to create incoming payment.') + .catch((err) => { + this.logger.error('Unable to create incoming payment.', err) throw new InternalServerError() }) } diff --git a/packages/boutique/backend/src/order/service.ts b/packages/boutique/backend/src/order/service.ts index 507e5b90c..722faeefa 100644 --- a/packages/boutique/backend/src/order/service.ts +++ b/packages/boutique/backend/src/order/service.ts @@ -4,7 +4,7 @@ import { TransactionOrKnex } from 'objection' import { Logger } from 'winston' import { InternalServerError, NotFound } from '@shared/backend' -interface OrderItemParams extends Pick {} +type OrderItemParams = Pick interface CreateParams { userId?: string diff --git a/packages/boutique/frontend/.env.example b/packages/boutique/frontend/.env.example index b12ede696..a040ddeae 100644 --- a/packages/boutique/frontend/.env.example +++ b/packages/boutique/frontend/.env.example @@ -1 +1,9 @@ -VITE_CURRENCY= \ No newline at end of file +# You can copy this file to .env.local and adjust values to run the boutique frontend locally. +# If you create both .env.local and .env, values in .env override values from .env.local. + +########## FRONTEND API AND THEME ################################################################ +## VITE_* variables are exposed to the browser at build time. + +# VITE_API_BASE_URL=https://api.boutique.test +# VITE_THEME=light +# VITE_CURRENCY=USD \ No newline at end of file diff --git a/packages/boutique/frontend/.env.local b/packages/boutique/frontend/.env.local new file mode 100644 index 000000000..43f211d43 --- /dev/null +++ b/packages/boutique/frontend/.env.local @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=https://api.boutique.test +VITE_THEME=dark +VITE_CURRENCY=USD diff --git a/packages/boutique/frontend/src/app/checkout/components/pay-button.tsx b/packages/boutique/frontend/src/app/checkout/components/pay-button.tsx index 93039d6ad..31820dc2d 100644 --- a/packages/boutique/frontend/src/app/checkout/components/pay-button.tsx +++ b/packages/boutique/frontend/src/app/checkout/components/pay-button.tsx @@ -2,8 +2,10 @@ import { PayWithInterledgerMark } from '@/components/icons.tsx' import { Button, ButtonProps } from '@/components/ui/button.tsx' import { cn } from '@/lib/utils.ts' -export interface PayButtonProps - extends Omit {} +export type PayButtonProps = Omit< + ButtonProps, + 'variant' | 'size' | 'aria-label' +> export const PayButton = ({ className, ...props }: PayButtonProps) => { return ( diff --git a/packages/boutique/frontend/src/components/navigation.tsx b/packages/boutique/frontend/src/components/navigation.tsx index 7591ee976..6cc1bff67 100644 --- a/packages/boutique/frontend/src/components/navigation.tsx +++ b/packages/boutique/frontend/src/components/navigation.tsx @@ -19,7 +19,7 @@ const NAV_LINKS = [ } ] -interface MainNavLinkProps extends Omit {} +type MainNavLinkProps = Omit export const MainNavLink = (props: MainNavLinkProps) => { return ( diff --git a/packages/boutique/frontend/src/components/ui/input.tsx b/packages/boutique/frontend/src/components/ui/input.tsx index c2d6b8e2d..ef7330a0c 100644 --- a/packages/boutique/frontend/src/components/ui/input.tsx +++ b/packages/boutique/frontend/src/components/ui/input.tsx @@ -1,7 +1,7 @@ import { InputHTMLAttributes, forwardRef } from 'react' import { cn } from '@/lib/utils' -export interface InputProps extends InputHTMLAttributes {} +export type InputProps = InputHTMLAttributes export const Input = forwardRef( ({ className, type, ...props }, ref) => { diff --git a/packages/boutique/frontend/src/components/ui/label.tsx b/packages/boutique/frontend/src/components/ui/label.tsx index fff9a70ff..63ae8f20b 100644 --- a/packages/boutique/frontend/src/components/ui/label.tsx +++ b/packages/boutique/frontend/src/components/ui/label.tsx @@ -2,10 +2,9 @@ import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' import * as LabelPrimitive from '@radix-ui/react-label' import { cn } from '@/lib/utils' -export interface LabelRefType extends ElementRef {} +export type LabelRefType = ElementRef -export interface LabelProps - extends ComponentPropsWithoutRef {} +export type LabelProps = ComponentPropsWithoutRef export const Label = forwardRef( ({ className, ...props }, ref) => ( diff --git a/packages/boutique/frontend/src/hooks/use-toast.tsx b/packages/boutique/frontend/src/hooks/use-toast.tsx index 0b17d633f..b1c85170a 100644 --- a/packages/boutique/frontend/src/hooks/use-toast.tsx +++ b/packages/boutique/frontend/src/hooks/use-toast.tsx @@ -11,13 +11,6 @@ type ToasterToast = ToastProps & { action?: ToastActionElement } -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST' -} as const - let count = 0 function genId() { @@ -25,7 +18,12 @@ function genId() { return count.toString() } -type ActionType = typeof actionTypes +type ActionType = { + ADD_TOAST: 'ADD_TOAST' + UPDATE_TOAST: 'UPDATE_TOAST' + DISMISS_TOAST: 'DISMISS_TOAST' + REMOVE_TOAST: 'REMOVE_TOAST' +} type Action = | { diff --git a/packages/boutique/frontend/src/lib/constants.ts b/packages/boutique/frontend/src/lib/constants.ts index 3f29ef742..a9cc06cab 100644 --- a/packages/boutique/frontend/src/lib/constants.ts +++ b/packages/boutique/frontend/src/lib/constants.ts @@ -1,4 +1,23 @@ +// Resolve the boutique API base URL at runtime based on the current hostname. +// In the local HTTPS environment the frontend is served behind Traefik at +// "boutique.test", so we route API calls to its TLS-proxied backend. +// Outside that environment (plain localhost dev) we still fall back to the +// local backend port. +const getDefaultApiBaseUrl = () => { + if (typeof window !== 'undefined') { + if (window.location.hostname === 'boutique.test') { + return 'https://api.boutique.test' + } + } + + console.warn( + 'Boutique API: falling back to http://localhost:3004. ' + + 'Set VITE_API_BASE_URL or access via boutique.test for the local HTTPS environment.' + ) + return 'http://localhost:3004' +} + export const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL || 'http://localhost:3004' + import.meta.env.VITE_API_BASE_URL || getDefaultApiBaseUrl() export const IMAGES_URL = API_BASE_URL + '/images/' -export const THEME = import.meta.env.THEME || 'light' +export const THEME = import.meta.env.VITE_THEME || 'light' diff --git a/packages/boutique/frontend/tailwind.config.cjs b/packages/boutique/frontend/tailwind.config.cjs index 931f94bcb..ec5dc93be 100644 --- a/packages/boutique/frontend/tailwind.config.cjs +++ b/packages/boutique/frontend/tailwind.config.cjs @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ const twColors = require('tailwindcss/colors') /** @type {import('tailwindcss').Config} */ diff --git a/packages/boutique/frontend/vite.config.ts b/packages/boutique/frontend/vite.config.ts index 301d56ba4..0d8d70b4c 100644 --- a/packages/boutique/frontend/vite.config.ts +++ b/packages/boutique/frontend/vite.config.ts @@ -17,6 +17,8 @@ export default defineConfig({ } ], server: { + host: '0.0.0.0', + allowedHosts: ['boutique.test'], port: 4004 }, resolve: { diff --git a/packages/shared/backend/src/utils.ts b/packages/shared/backend/src/utils.ts index 4dbc85d67..5891b6381 100644 --- a/packages/shared/backend/src/utils.ts +++ b/packages/shared/backend/src/utils.ts @@ -13,8 +13,8 @@ export function deleteProperty( return newObj } -interface SuccessResponse extends Omit {} -interface ErrorResponse extends Omit {} +type SuccessResponse = Omit +type ErrorResponse = Omit export function toSuccessResponse( result?: T, diff --git a/packages/wallet/backend/.env.example b/packages/wallet/backend/.env.example new file mode 100644 index 000000000..7fb5cdc87 --- /dev/null +++ b/packages/wallet/backend/.env.example @@ -0,0 +1,71 @@ +# You can copy this file to .env.local and adjust values to run the wallet backend locally. +# If you create both .env.local and .env, values in .env override values from .env.local. +# Do not commit sensitive real credentials. + +########## INFRASTRUCTURE ####################################################################### +## Usually you do not need to change these for local development. + +# PORT=3003 +# NODE_ENV=development +# NODE_TLS_REJECT_UNAUTHORIZED=0 +# DATABASE_URL=postgres://wallet_backend:wallet_backend@localhost:15434/wallet_backend +# REDIS_URL=redis://localhost:6379/0 + +########## SESSION / COOKIE SETTINGS ############################################################ +## COOKIE_PASSWORD should be a long random secret in non-local environments. + +# COOKIE_NAME=testnet.cookie +# COOKIE_PASSWORD=testnet.cookie.password.super.secret.ilp +# COOKIE_TTL=2630000 + +########## GATEHUB INTEGRATION ################################################################## +## Keep these aligned with your local mock/real GateHub setup. + +# GATEHUB_ENV=sandbox +# GATEHUB_API_BASE_URL=https://mockgatehub.testnet.test +# GATEHUB_IFRAME_MANAGED_RAMP_URL=https://mockgatehub.testnet.test +# GATEHUB_IFRAME_EXCHANGE_URL=https://mockgatehub.testnet.test +# GATEHUB_IFRAME_ONBOARDING_URL=https://mockgatehub.testnet.test +# GATEHUB_ACCESS_KEY=mock_access_key +# GATEHUB_SECRET_KEY=mock_secret_key +# GATEHUB_WEBHOOK_SECRET=6d6f636b5f776562686f6f6b5f736563726574 +# GATEHUB_GATEWAY_UUID=mock-gateway-uuid +# GATEHUB_SETTLEMENT_WALLET_ADDRESS=$ilp.interledger-test.dev/interledger +# GATEHUB_ORG_ID=mock-org-id +# GATEHUB_CARD_APP_ID=mock-card-app-id +# GATEHUB_ACCOUNT_PRODUCT_CODE=DEFAULT +# GATEHUB_CARD_PRODUCT_CODE=DEFAULT +# GATEHUB_NAME_ON_CARD=TestnetUser +# GATEHUB_CARD_PP_PREFIX=TEST + +## Optional SEPA-specific credentials (fall back to standard GateHub keys if unset). +# GATEHUB_SEPA_ACCESS_KEY= +# GATEHUB_SEPA_SECRET_KEY= +# GATEHUB_SEPA_ORG_ID= + +########## RAFIKI / AUTH / OPEN PAYMENTS ######################################################## + +# GRAPHQL_ENDPOINT=http://localhost:3011/graphql +# AUTH_GRAPHQL_ENDPOINT=http://localhost:3008/graphql +# AUTH_DOMAIN=https://auth.testnet.test +# AUTH_IDENTITY_SERVER_SECRET=dev_identity_server_secret +# RAFIKI_WEBHOOK_SIGNATURE_SECRET=327132b5-99e9-4eb8-8a25-2b7d7738ece1 +# ADMIN_SIGNATURE_VERSION=1 +# ADMIN_API_SECRET=secret-key +# OPERATOR_TENANT_ID=f829c064-762a-4430-ac5d-7af5df198551 +# OPEN_PAYMENTS_HOST=https://rafiki-backend.testnet.test +# RAFIKI_MONEY_FRONTEND_HOST=testnet.test + +########## EMAIL / CARD / STRIPE / RATE LIMIT ################################################### +## Optional integration settings for local feature testing. + +# SENDGRID_API_KEY= +# FROM_EMAIL=noreply@testnet.local +# SEND_EMAIL=false +# CARD_DATA_HREF=http://localhost:3007/card-data +# CARD_PIN_HREF=http://localhost:3007/card-pin +# STRIPE_SECRET_KEY=notset +# STRIPE_WEBHOOK_SECRET=notset +# USE_STRIPE=false +# RATE_LIMIT=false +# RATE_LIMIT_LEVEL=LAX diff --git a/packages/wallet/backend/.env.local b/packages/wallet/backend/.env.local new file mode 100644 index 000000000..cee29d7b4 --- /dev/null +++ b/packages/wallet/backend/.env.local @@ -0,0 +1,47 @@ +PORT=3003 +NODE_ENV=development +NODE_TLS_REJECT_UNAUTHORIZED=0 +DATABASE_URL=postgres://wallet_backend:wallet_backend@localhost:15434/wallet_backend +REDIS_URL=redis://localhost:6379/0 +COOKIE_NAME=testnet.cookie +COOKIE_PASSWORD=testnet.cookie.password.super.secret.ilp +COOKIE_TTL=2630000 + +GATEHUB_ACCESS_KEY=mock_access_key +GATEHUB_WEBHOOK_SECRET=6d6f636b5f776562686f6f6b5f736563726574 +GATEHUB_SECRET_KEY=mock_secret_key + +GATEHUB_ENV=sandbox +GATEHUB_API_BASE_URL=https://mockgatehub.testnet.test +GATEHUB_IFRAME_MANAGED_RAMP_URL=https://mockgatehub.testnet.test +GATEHUB_IFRAME_EXCHANGE_URL=https://mockgatehub.testnet.test +GATEHUB_IFRAME_ONBOARDING_URL=https://mockgatehub.testnet.test + +GATEHUB_GATEWAY_UUID=mock-gateway-uuid +GATEHUB_SETTLEMENT_WALLET_ADDRESS=$ilp.testnet.test/interledger +GATEHUB_ORG_ID=mock-org-id +GATEHUB_CARD_APP_ID=mock-card-app-id +GATEHUB_ACCOUNT_PRODUCT_CODE=DEFAULT +GATEHUB_CARD_PRODUCT_CODE=DEFAULT +GATEHUB_NAME_ON_CARD=TestnetUser +GATEHUB_CARD_PP_PREFIX=TEST +GRAPHQL_ENDPOINT=https://rafiki-backend.testnet.test/graphql +AUTH_GRAPHQL_ENDPOINT=https://auth.testnet.test +AUTH_DOMAIN=https://auth.testnet.test +AUTH_IDENTITY_SERVER_SECRET=dev_identity_server_secret +RAFIKI_WEBHOOK_SIGNATURE_SECRET=327132b5-99e9-4eb8-8a25-2b7d7738ece1 +ADMIN_SIGNATURE_VERSION=1 +ADMIN_API_SECRET=secret-key +OPERATOR_TENANT_ID=f829c064-762a-4430-ac5d-7af5df198551 +OPEN_PAYMENTS_HOST=https://rafiki-backend.testnet.test +RAFIKI_MONEY_FRONTEND_HOST=testnet.test +SENDGRID_API_KEY= +FROM_EMAIL=noreply@testnet.local +SEND_EMAIL=false +CARD_DATA_HREF=http://localhost:3007/card-data +CARD_PIN_HREF=http://localhost:3007/card-pin +STRIPE_SECRET_KEY=notset +STRIPE_WEBHOOK_SECRET=notset +USE_STRIPE=false +RATE_LIMIT=false +RATE_LIMIT_LEVEL=LAX diff --git a/packages/wallet/backend/Dockerfile.dev b/packages/wallet/backend/Dockerfile.dev index 8c480a4f0..21540a3bb 100644 --- a/packages/wallet/backend/Dockerfile.dev +++ b/packages/wallet/backend/Dockerfile.dev @@ -21,7 +21,7 @@ RUN pnpm fetch ADD . ./ # Install packages from virtual store -RUN pnpm install -r --offline +RUN pnpm install --filter @wallet/backend... --offline # Build backend RUN pnpm wallet:backend build diff --git a/packages/wallet/backend/jest.setup.js b/packages/wallet/backend/jest.setup.js index 400d7183b..08ed6af66 100644 --- a/packages/wallet/backend/jest.setup.js +++ b/packages/wallet/backend/jest.setup.js @@ -25,6 +25,16 @@ module.exports = async () => { POSTGRES_PORT )}/${POSTGRES_DB}` + // env.ts requires these GateHub URLs without defaults, so we supply + // test-safe sandbox values here to prevent process.exit(1) during import. + process.env.GATEHUB_API_BASE_URL = 'https://api.sandbox.gatehub.net' + process.env.GATEHUB_IFRAME_MANAGED_RAMP_URL = + 'https://managed-ramp.sandbox.gatehub.net' + process.env.GATEHUB_IFRAME_EXCHANGE_URL = + 'https://exchange.sandbox.gatehub.net' + process.env.GATEHUB_IFRAME_ONBOARDING_URL = + 'https://onboarding.sandbox.gatehub.net' + global.__TESTING_POSTGRES_CONTAINER__ = container global.__TESTING_REDIS_CONTAINER__ = redisContainer } diff --git a/packages/wallet/backend/package.json b/packages/wallet/backend/package.json index 48e2b8811..a032fb494 100644 --- a/packages/wallet/backend/package.json +++ b/packages/wallet/backend/package.json @@ -1,10 +1,10 @@ { "name": "@wallet/backend", "scripts": { - "start": "node -r tsconfig-paths/register dist/index.js", + "start": "sh -c 'set --; [ -f .env.local ] && set -- \"$@\" --env-file=.env.local; [ -f .env ] && set -- \"$@\" --env-file=.env; node \"$@\" -r tsconfig-paths/register dist/index.js'", "build:deps": "pnpm --filter @shared/backend build && pnpm --filter @wallet/shared build", "build": "pnpm build:deps && tsc --build tsconfig.build.json && tsc-alias -p tsconfig.build.json", - "dev": "nodemon --watch ./src --watch ../shared/src --ext ts,json --delay 1 --exec \"pnpm run build && node ./dist/index.js\"", + "dev": "nodemon --watch ./src --watch ../shared/src --ext ts,json --delay 1 --exec \"sh -c 'set --; [ -f .env.local ] && set -- \\$@ --env-file=.env.local; [ -f .env ] && set -- \\$@ --env-file=.env; pnpm run build && node \\$@ ./dist/index.js'\"", "test": "jest --passWithNoTests --maxWorkers=75%", "generate": "graphql-codegen --config codegen.yml" }, diff --git a/packages/wallet/backend/src/backfillTrxDetails.ts b/packages/wallet/backend/src/backfillTrxDetails.ts index d90879457..49d79112f 100644 --- a/packages/wallet/backend/src/backfillTrxDetails.ts +++ b/packages/wallet/backend/src/backfillTrxDetails.ts @@ -78,7 +78,7 @@ async function backfillTrxDetails() { page++ } - } catch (e) { + } catch { console.log('Failed to update trx for account: ', account.user.email) } } diff --git a/packages/wallet/backend/src/config/env.ts b/packages/wallet/backend/src/config/env.ts index 92b0f60d3..e1638c0a2 100644 --- a/packages/wallet/backend/src/config/env.ts +++ b/packages/wallet/backend/src/config/env.ts @@ -13,6 +13,22 @@ const envSchema = z.object({ .default('testnet.cookie.password.super.secret.ilp'), // min. 32 chars COOKIE_TTL: z.coerce.number().default(2630000), // 1 month GATEHUB_ENV: z.enum(['production', 'sandbox']).default('sandbox'), + // Required GateHub API base URL (fail fast if missing). + // Real GateHub examples: + // - Sandbox: https://api.sandbox.gatehub.net + // - Production: https://api.gatehub.net + GATEHUB_API_BASE_URL: z.string().url(), + // Required iframe URLs (fail fast if missing). + // Real GateHub examples: + // - Sandbox: https://managed-ramp.sandbox.gatehub.net + // https://exchange.sandbox.gatehub.net + // https://onboarding.sandbox.gatehub.net + // - Production: https://managed-ramp.gatehub.net + // https://exchange.gatehub.net + // https://onboarding.gatehub.net + GATEHUB_IFRAME_MANAGED_RAMP_URL: z.string().url(), + GATEHUB_IFRAME_EXCHANGE_URL: z.string().url(), + GATEHUB_IFRAME_ONBOARDING_URL: z.string().url(), GATEHUB_ACCESS_KEY: z.string().default('GATEHUB_ACCESS_KEY'), GATEHUB_SECRET_KEY: z.string().default('GATEHUB_SECRET_KEY'), GATEHUB_SEPA_ACCESS_KEY: z.string().optional(), @@ -39,7 +55,7 @@ const envSchema = z.object({ .string() .url() .default('http://rafiki-auth:3008/graphql'), - AUTH_DOMAIN: z.string().url().default('http://rafiki-auth:3009'), + AUTH_DOMAIN: z.string().url().default('https://auth.testnet.test'), AUTH_IDENTITY_SERVER_SECRET: z.string().default('replace-me'), RAFIKI_WEBHOOK_SIGNATURE_SECRET: z.string().default('replace-me'), ADMIN_SIGNATURE_VERSION: z.string().default('1'), diff --git a/packages/wallet/backend/src/config/rafiki.ts b/packages/wallet/backend/src/config/rafiki.ts index e9d04983f..a519facdf 100644 --- a/packages/wallet/backend/src/config/rafiki.ts +++ b/packages/wallet/backend/src/config/rafiki.ts @@ -26,7 +26,7 @@ function createSignedClient(endpoint: string, env: Env) { try { const body: GraphQLRequestBody = JSON.parse(request.body) query = body.query || '' - } catch (e) { + } catch { // Body is not valid JSON - this shouldn't happen with GraphQL requests // but we'll handle it gracefully query = '' @@ -55,7 +55,7 @@ function createSignedClient(endpoint: string, env: Env) { 'tenant-id': env.OPERATOR_TENANT_ID } } - } catch (e) { + } catch { return request } } diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index dc5abd46b..084f24060 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -68,7 +68,6 @@ export class GateHubClient { private supportedAssetCodes: string[] private clientIds = SANDBOX_CLIENT_IDS private vaultIds = SANDBOX_VAULT_IDS - private mainUrl = 'sandbox.gatehub.net' private iframeMappings: Record< IFRAME_TYPE, @@ -86,7 +85,6 @@ export class GateHubClient { if (this.isProduction) { this.clientIds = PRODUCTION_CLIENT_IDS this.vaultIds = PRODUCTION_VAULT_IDS - this.mainUrl = 'gatehub.net' } this.supportedAssetCodes = Object.keys(this.vaultIds) @@ -97,19 +95,19 @@ export class GateHubClient { } get apiUrl() { - return `https://api.${this.mainUrl}` + return this.env.GATEHUB_API_BASE_URL } get rampUrl() { - return `https://managed-ramp.${this.mainUrl}` + return this.env.GATEHUB_IFRAME_MANAGED_RAMP_URL } get exchangeUrl() { - return `https://exchange.${this.mainUrl}` + return this.env.GATEHUB_IFRAME_EXCHANGE_URL } get onboardingUrl() { - return `https://onboarding.${this.mainUrl}` + return this.env.GATEHUB_IFRAME_ONBOARDING_URL } async getWithdrawalUrl(managedUserUuid: string): Promise { diff --git a/packages/wallet/backend/src/gatehub/service.ts b/packages/wallet/backend/src/gatehub/service.ts index 976c51745..761fa72f5 100644 --- a/packages/wallet/backend/src/gatehub/service.ts +++ b/packages/wallet/backend/src/gatehub/service.ts @@ -260,6 +260,15 @@ export class GateHubService { } let customerId + // Check if customer already exists to prevent race condition between + // direct addUserToGateway call and webhook handler + if (user.customerId) { + this.logger.debug( + `Customer already exists for user ${userId}, skipping customer creation` + ) + return { isApproved, customerId: user.customerId } + } + if ( this.env.NODE_ENV === 'development' && this.env.GATEHUB_ENV === 'sandbox' @@ -325,6 +334,33 @@ export class GateHubService { firstName: string, lastName: string ): Promise { + // Check if customer setup already in progress or completed + // to prevent race condition between concurrent calls + const existingAccount = await Account.query().findOne({ + userId, + assetCode: 'EUR' + }) + + if (existingAccount) { + this.logger.warn( + `EUR account already exists for user ${userId}, skipping sandbox customer creation` + ) + const user = await User.query().findById(userId) + if (!user) { + this.logger.error( + `User ${userId} not found while EUR account exists, cannot retrieve customerId` + ) + throw new NotFound('User not found') + } + if (!user.customerId) { + this.logger.error( + `Missing customerId for user ${userId} with existing EUR account` + ) + throw new Error('CustomerId is missing for existing EUR account') + } + return user.customerId + } + const { account, walletAddress } = await this.createDefaultAccountAndWAForManagedUser(userId, true) diff --git a/packages/wallet/backend/src/gatehub/types.ts b/packages/wallet/backend/src/gatehub/types.ts index 208b2ae91..30f6ddcfe 100644 --- a/packages/wallet/backend/src/gatehub/types.ts +++ b/packages/wallet/backend/src/gatehub/types.ts @@ -99,9 +99,9 @@ export interface IFundAccountRequest { absolute_fee?: number } -export interface ICreateTransactionResponse {} +export type ICreateTransactionResponse = Record -export interface IGetVaultsResponse {} +export type IGetVaultsResponse = Record export interface IRatesResponse { counter: string @@ -130,19 +130,19 @@ interface IVault { updated_at: string } -export interface IConnectUserToGatewayResponse {} +export type IConnectUserToGatewayResponse = Record export interface IApproveUserToGatewayRequest { verified: number reasons: string[] customMessage: boolean } -export interface IApproveUserToGatewayResponse {} +export type IApproveUserToGatewayResponse = Record export interface IOverrideUserRiskLevelRequest { risk_level: string reason: string } -export interface IOverrideUserRiskLevelResponse {} +export type IOverrideUserRiskLevelResponse = Record export type WebhookEventType = | 'core.deposit.completed' diff --git a/packages/wallet/backend/src/middleware/withSession.ts b/packages/wallet/backend/src/middleware/withSession.ts index 5e1eaf946..9590522d3 100644 --- a/packages/wallet/backend/src/middleware/withSession.ts +++ b/packages/wallet/backend/src/middleware/withSession.ts @@ -6,11 +6,23 @@ import { getIronSession } from 'iron-session' -let domain = env.RAFIKI_MONEY_FRONTEND_HOST - -if (env.NODE_ENV === 'production' && env.GATEHUB_ENV === 'production') { - domain = 'interledger.cards' +// Determine cookie domain. Avoid setting Domain=localhost — browsers ignore it. +// The wallet frontend is served on the bare RAFIKI_MONEY_FRONTEND_HOST domain +// (e.g. testnet.test) while the backend sits on a subdomain (api.testnet.test). +// A server is allowed to set cookies for any ancestor domain, so +// api.testnet.test can legitimately issue Domain=testnet.test and the browser +// will send it back to both testnet.test and api.testnet.test. +let domain: string | undefined = undefined +domain = env.RAFIKI_MONEY_FRONTEND_HOST +// Fail fast if domain is not set or empty +if (!domain || domain.trim() === '') { + console.error( + 'RAFIKI_MONEY_FRONTEND_HOST environment variable is not set or empty' + ) + process.exit(1) } +// Remove protocol and trailing slashes if present +domain = domain.replace(/^https?:\/\//, '').replace(/\/+$/, '') export const SESSION_OPTIONS: SessionOptions = { password: env.COOKIE_PASSWORD, diff --git a/packages/wallet/backend/src/rafiki/service.ts b/packages/wallet/backend/src/rafiki/service.ts index 33cc9d391..706223316 100644 --- a/packages/wallet/backend/src/rafiki/service.ts +++ b/packages/wallet/backend/src/rafiki/service.ts @@ -447,9 +447,14 @@ export class RafikiService implements IRafikiService { id: string ): Promise { try { + // Incoming payment identifiers are expected to use HTTPS URLs. + const openPaymentsIdentifierHost = this.env.OPEN_PAYMENTS_HOST.replace( + /^http:\/\//, + 'https://' + ) const outgoingPayments = await this.rafikiClient.getOutgoingPaymentsByReceiver( - `${this.env.OPEN_PAYMENTS_HOST}/incoming-payments/${id}` + `${openPaymentsIdentifierHost}/incoming-payments/${id}` ) const walletAddressIds = outgoingPayments.map( diff --git a/packages/wallet/backend/src/walletAddress/service.ts b/packages/wallet/backend/src/walletAddress/service.ts index 48496fa6f..db66ab960 100644 --- a/packages/wallet/backend/src/walletAddress/service.ts +++ b/packages/wallet/backend/src/walletAddress/service.ts @@ -89,7 +89,12 @@ export class WalletAddressService implements IWalletAddressService { args.accountId, args.userId ) - const url = `${this.env.OPEN_PAYMENTS_HOST}/${args.walletAddressName}` + // Rafiki validates wallet-address identifiers as HTTPS URLs. + const openPaymentsIdentifierHost = this.env.OPEN_PAYMENTS_HOST.replace( + /^http:\/\//, + 'https://' + ) + const url = `${openPaymentsIdentifierHost}/${args.walletAddressName}` let walletAddress = await WalletAddress.query().findOne({ url }) if (walletAddress) { @@ -239,13 +244,14 @@ export class WalletAddressService implements IWalletAddressService { const updatedWalletAddress = await walletAddress .$query(trx) .findById(walletAddressId) - updatedWalletAddress && - (await this.cache.set(walletAddressId, updatedWalletAddress, { + if (updatedWalletAddress) { + await this.cache.set(walletAddressId, updatedWalletAddress, { expiry: 60 - })) + }) + } await trx.commit() - } catch (e) { + } catch { await trx.rollback() } } diff --git a/packages/wallet/backend/src/walletAddressKeys/service.ts b/packages/wallet/backend/src/walletAddressKeys/service.ts index 45439d526..a4a2173a9 100644 --- a/packages/wallet/backend/src/walletAddressKeys/service.ts +++ b/packages/wallet/backend/src/walletAddressKeys/service.ts @@ -196,7 +196,7 @@ export class WalletAddressKeyService implements IWalletAddressKeyService { this.rafikiClient.revokeWalletAddressKey(walletAddressKey.rafikiId) ]) await trx.commit() - } catch (e) { + } catch { await trx.rollback() } } @@ -230,7 +230,7 @@ export class WalletAddressKeyService implements IWalletAddressKeyService { }) ) await trx.commit() - } catch (e) { + } catch { await trx.rollback() } } diff --git a/packages/wallet/backend/tests/auth/controller.test.ts b/packages/wallet/backend/tests/auth/controller.test.ts index 62be689c0..3e91ad573 100644 --- a/packages/wallet/backend/tests/auth/controller.test.ts +++ b/packages/wallet/backend/tests/auth/controller.test.ts @@ -59,6 +59,7 @@ describe('Authentication Controller', (): void => { }) beforeEach(async (): Promise => { + jest.clearAllMocks() res = createResponse() req = createRequest() }) @@ -77,8 +78,21 @@ describe('Authentication Controller', (): void => { describe('Sign Up', (): void => { it('should return status 201 if the user is created', async (): Promise => { req.body = mockSignUpRequest().body + const controllerAuthService = Reflect.get( + authController, + 'authService' + ) as AuthService + const signUpSpy = jest + .spyOn(controllerAuthService, 'signUp') + .mockResolvedValueOnce({} as never) + await authController.signUp(req, res, next) + expect(signUpSpy).toHaveBeenCalledWith({ + email: req.body.email, + password: req.body.password, + acceptedCardTerms: req.body.acceptedCardTerms + }) expect(next).toHaveBeenCalledTimes(0) expect(res.statusCode).toBe(201) expect(res._getJSONData()).toMatchObject({ @@ -107,16 +121,20 @@ describe('Authentication Controller', (): void => { it('should return status 500 on unexpected error', async (): Promise => { req.body = mockSignUpRequest().body + const controllerAuthService = Reflect.get( + authController, + 'authService' + ) as AuthService - const createSpy = jest - .spyOn(userService, 'create') + const signUpSpy = jest + .spyOn(controllerAuthService, 'signUp') .mockRejectedValueOnce(new Error('Unexpected error')) await authController.signUp(req, res, (err) => { next() errorHandler(err, req, res, next) }) - expect(createSpy).toHaveBeenCalledTimes(1) + expect(signUpSpy).toHaveBeenCalledTimes(1) expect(next).toHaveBeenCalledTimes(1) expect(res.statusCode).toBe(500) expect(res._getJSONData()).toMatchObject({ diff --git a/packages/wallet/backend/tests/gatehub/service.test.ts b/packages/wallet/backend/tests/gatehub/service.test.ts index 9eeeae849..3d48c913d 100644 --- a/packages/wallet/backend/tests/gatehub/service.test.ts +++ b/packages/wallet/backend/tests/gatehub/service.test.ts @@ -443,7 +443,9 @@ describe('GateHub Service', (): void => { { email: 'user2@example.com' } ]) - expect(gateHubService.addUserToGateway(user.id)).rejects.toThrowError( + await expect( + gateHubService.addUserToGateway(user.id) + ).rejects.toThrowError( `GateHub user with email ${user.email} not found` ) }) diff --git a/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts b/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts index 01a9950fd..863100168 100644 --- a/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts +++ b/packages/wallet/backend/tests/walletAddressKeys/controller.test.ts @@ -132,7 +132,7 @@ describe('Wallet Address Keys Controller', () => { } req.body = { - nickname: faker.lorem.word() + nickname: faker.lorem.word({ length: { min: 3, max: 10 } }) } await walletAddressKeyController.patchKey(req, res, next) @@ -168,7 +168,7 @@ describe('Wallet Address Keys Controller', () => { } req.body = { - nickname: faker.lorem.word() + nickname: faker.lorem.word({ length: { min: 3, max: 10 } }) } await walletAddressKeyController.registerKey(req, res, next) @@ -205,7 +205,7 @@ describe('Wallet Address Keys Controller', () => { } req.body = { - nickname: faker.lorem.word(), + nickname: faker.lorem.word({ length: { min: 3, max: 10 } }), base64Key: faker.lorem.word() } diff --git a/packages/wallet/frontend/.env.example b/packages/wallet/frontend/.env.example new file mode 100644 index 000000000..3806aff25 --- /dev/null +++ b/packages/wallet/frontend/.env.example @@ -0,0 +1,30 @@ +# You can copy this file to .env.local and adjust values to run the wallet frontend locally. +# If you create both .env.local and .env, values in .env override values from .env.local. +# Keep NEXT_PUBLIC_* URLs aligned with the local testnet domains. + +########## SESSION AND INTERNAL BACKEND ######################################################### + +# COOKIE_NAME=testnet.cookie +# BACKEND_INTERNAL_URL=http://localhost:3003 + +########## PUBLIC FRONTEND CONFIG ################################################################ +## NEXT_PUBLIC_* variables are exposed to the browser. + +# NEXT_PUBLIC_BACKEND_URL=https://api.testnet.test +# NEXT_PUBLIC_AUTH_HOST=https://auth.testnet.test +# NEXT_PUBLIC_OPEN_PAYMENTS_HOST=https://rafiki-backend.testnet.test + +# NEXT_PUBLIC_THEME=light + +######### Controlling branding and features ###################################################### +# NEXT_PUBLIC_FEATURES_ENABLED controls the wallet UI mode (frontend only): +# - true: enables "Interledger Wallet" mode (wallet branding, card/deposit-first flows, +# cards menu visibility, card-related labels in transactions, and production-style auth copy) +# - false: enables "Test Wallet" mode (test branding, test-only hints, and cards visibility +# controlled by per-user settings instead of forced-on) +# If omitted, next.config.js defaults to false only when NODE_ENV=production and NEXT_PUBLIC_GATEHUB_ENV=sandbox +# otherwise it defaults to true. + +# NODE_ENV=production +# NEXT_PUBLIC_GATEHUB_ENV=production +# NEXT_PUBLIC_FEATURES_ENABLED=true diff --git a/packages/wallet/frontend/.env.local b/packages/wallet/frontend/.env.local new file mode 100644 index 000000000..a485c1d8f --- /dev/null +++ b/packages/wallet/frontend/.env.local @@ -0,0 +1,8 @@ +COOKIE_NAME=testnet.cookie +BACKEND_INTERNAL_URL=http://localhost:3003 +NEXT_PUBLIC_BACKEND_URL=https://api.testnet.test +NEXT_PUBLIC_AUTH_HOST=https://auth.testnet.test +NEXT_PUBLIC_OPEN_PAYMENTS_HOST=https://rafiki-backend.testnet.test +NEXT_PUBLIC_GATEHUB_ENV=sandbox +NEXT_PUBLIC_THEME=dark +NEXT_PUBLIC_FEATURES_ENABLED=false diff --git a/packages/wallet/frontend/Dockerfile.dev b/packages/wallet/frontend/Dockerfile.dev index 4a13dcab2..d6bbe8d22 100644 --- a/packages/wallet/frontend/Dockerfile.dev +++ b/packages/wallet/frontend/Dockerfile.dev @@ -21,6 +21,22 @@ RUN pnpm fetch ADD . ./ # Install packages from virtual store -RUN pnpm install -r --offline +RUN pnpm install --filter @wallet/frontend... --offline + +# Accept build arguments for Next.js public environment variables +ARG NEXT_PUBLIC_BACKEND_URL +ARG NEXT_PUBLIC_AUTH_HOST +ARG NEXT_PUBLIC_OPEN_PAYMENTS_HOST +ARG NEXT_PUBLIC_GATEHUB_ENV +ARG NEXT_PUBLIC_THEME +ARG NEXT_PUBLIC_FEATURES_ENABLED + +# Make them available as environment variables during build +ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL +ENV NEXT_PUBLIC_AUTH_HOST=$NEXT_PUBLIC_AUTH_HOST +ENV NEXT_PUBLIC_OPEN_PAYMENTS_HOST=$NEXT_PUBLIC_OPEN_PAYMENTS_HOST +ENV NEXT_PUBLIC_GATEHUB_ENV=$NEXT_PUBLIC_GATEHUB_ENV +ENV NEXT_PUBLIC_THEME=$NEXT_PUBLIC_THEME +ENV NEXT_PUBLIC_FEATURES_ENABLED=$NEXT_PUBLIC_FEATURES_ENABLED CMD ["pnpm", "wallet:frontend", "dev"] diff --git a/packages/wallet/frontend/next.config.js b/packages/wallet/frontend/next.config.js index 9e98bb8b9..571ff3f81 100644 --- a/packages/wallet/frontend/next.config.js +++ b/packages/wallet/frontend/next.config.js @@ -2,26 +2,42 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' }) -let NEXT_PUBLIC_FEATURES_ENABLED = 'true' +if (!process.env.NEXT_PUBLIC_BACKEND_URL) { + throw new Error( + 'Missing required environment variable: NEXT_PUBLIC_BACKEND_URL' + ) +} + +// Default to env override; fall back to previous production/sandbox rule, then to 'true' +let NEXT_PUBLIC_FEATURES_ENABLED = process.env.NEXT_PUBLIC_FEATURES_ENABLED -if ( - process.env.NODE_ENV === 'production' && - process.env.NEXT_PUBLIC_GATEHUB_ENV === 'sandbox' -) { - NEXT_PUBLIC_FEATURES_ENABLED = 'false' +// This is a gaurdrail to prevent accidentally enabling features in production when the +// env variable is not set. +if (!NEXT_PUBLIC_FEATURES_ENABLED) { + if ( + process.env.NODE_ENV === 'production' && + process.env.NEXT_PUBLIC_GATEHUB_ENV === 'sandbox' + ) { + NEXT_PUBLIC_FEATURES_ENABLED = 'false' + } else { + NEXT_PUBLIC_FEATURES_ENABLED = 'true' + } } /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', poweredByHeader: false, + // ESLint 9.x removed options (useEslintrc, extensions) that Next.js 14 + // passes internally. Linting is handled separately via `pnpm lint:check`. + eslint: { ignoreDuringBuilds: true }, env: { - NEXT_PUBLIC_BACKEND_URL: - process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3003', - NEXT_PUBLIC_OPEN_PAYMENTS_HOST: - process.env.NEXT_PUBLIC_OPEN_PAYMENTS_HOST || '$rafiki-backend/', - NEXT_PUBLIC_AUTH_HOST: - process.env.NEXT_PUBLIC_AUTH_HOST || 'http://localhost:3006', + NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL, + // Internal URL for server-side (middleware) to reach the host backend. + BACKEND_INTERNAL_URL: + process.env.BACKEND_INTERNAL_URL || process.env.BACKEND_URL, + NEXT_PUBLIC_OPEN_PAYMENTS_HOST: process.env.NEXT_PUBLIC_OPEN_PAYMENTS_HOST, + NEXT_PUBLIC_AUTH_HOST: process.env.NEXT_PUBLIC_AUTH_HOST, NEXT_PUBLIC_THEME: process.env.NEXT_PUBLIC_THEME || 'light', NEXT_PUBLIC_GATEHUB_ENV: process.env.NEXT_PUBLIC_GATEHUB_ENV || 'sandbox', NEXT_PUBLIC_FEATURES_ENABLED diff --git a/packages/wallet/frontend/src/components/dialogs/SuccessDialog.tsx b/packages/wallet/frontend/src/components/dialogs/SuccessDialog.tsx index 68c476934..937ab6e2d 100644 --- a/packages/wallet/frontend/src/components/dialogs/SuccessDialog.tsx +++ b/packages/wallet/frontend/src/components/dialogs/SuccessDialog.tsx @@ -38,7 +38,7 @@ export const SuccessDialog = ({ } = { href: redirect ?? '/', onClick: () => { - onSuccess ? onSuccess() : undefined + if (onSuccess) onSuccess() onClose() } } diff --git a/packages/wallet/frontend/src/lib/api/card.ts b/packages/wallet/frontend/src/lib/api/card.ts index 0dd14a126..075aa08d8 100644 --- a/packages/wallet/frontend/src/lib/api/card.ts +++ b/packages/wallet/frontend/src/lib/api/card.ts @@ -82,7 +82,7 @@ type MonthlySpendingLimitError = ErrorResponse< > type MonthlySpendingLimitResult = SuccessResponse | MonthlySpendingLimitError -const getCardDataSchema = z.object({ +export const getCardDataSchema = z.object({ password: z.string(), publicKeyBase64: z.string() }) diff --git a/packages/wallet/frontend/src/lib/hooks/useToast.ts b/packages/wallet/frontend/src/lib/hooks/useToast.ts index 4d3df06ca..73ef2bf53 100644 --- a/packages/wallet/frontend/src/lib/hooks/useToast.ts +++ b/packages/wallet/frontend/src/lib/hooks/useToast.ts @@ -11,13 +11,6 @@ type ToasterToast = ToastProps & { action?: ToastActionElement } -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST' -} as const - let count = 0 function genId() { @@ -25,7 +18,12 @@ function genId() { return count.toString() } -type ActionType = typeof actionTypes +type ActionType = { + ADD_TOAST: 'ADD_TOAST' + UPDATE_TOAST: 'UPDATE_TOAST' + DISMISS_TOAST: 'DISMISS_TOAST' + REMOVE_TOAST: 'REMOVE_TOAST' +} type Action = | { diff --git a/packages/wallet/frontend/src/lib/hooks/useTransactions.ts b/packages/wallet/frontend/src/lib/hooks/useTransactions.ts index e9536676b..c6468cbc5 100644 --- a/packages/wallet/frontend/src/lib/hooks/useTransactions.ts +++ b/packages/wallet/frontend/src/lib/hooks/useTransactions.ts @@ -12,11 +12,7 @@ type TransactionsQueryParams = Record< keyof TransactionsFilters, string | number > -const ORDER_DIRECTION = { - ASC: 'ASC', - DESC: 'DESC' -} -export type OrderByDirection = keyof typeof ORDER_DIRECTION +export type OrderByDirection = 'ASC' | 'DESC' export type TransactionsFilters = { page: string diff --git a/packages/wallet/frontend/src/lib/httpClient.ts b/packages/wallet/frontend/src/lib/httpClient.ts index 8b2a1ad9d..5451dc964 100644 --- a/packages/wallet/frontend/src/lib/httpClient.ts +++ b/packages/wallet/frontend/src/lib/httpClient.ts @@ -14,8 +14,14 @@ export type ErrorResponse = { errors?: T extends FieldValues ? Record, string> : undefined } +// Use internal backend URL when running on the server (SSR/middleware) +const isServer = typeof window === 'undefined' +const baseUrl = isServer + ? process.env.BACKEND_INTERNAL_URL || 'http://localhost:3003' + : process.env.NEXT_PUBLIC_BACKEND_URL + export const httpClient = ky.extend({ - prefixUrl: process.env.NEXT_PUBLIC_BACKEND_URL, + prefixUrl: baseUrl, credentials: 'include', retry: 0, hooks: { diff --git a/packages/wallet/frontend/src/lib/types/app.ts b/packages/wallet/frontend/src/lib/types/app.ts index 2eafcfa56..118b17383 100644 --- a/packages/wallet/frontend/src/lib/types/app.ts +++ b/packages/wallet/frontend/src/lib/types/app.ts @@ -2,7 +2,7 @@ import type { NextPage } from 'next/types' import type { AppProps } from 'next/app' import type { ReactElement, ReactNode } from 'react' -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type NextPageWithLayout = NextPage< Props, InitialProps diff --git a/packages/wallet/frontend/src/lib/types/dialog.ts b/packages/wallet/frontend/src/lib/types/dialog.ts index 10a4045e8..5d7be3177 100644 --- a/packages/wallet/frontend/src/lib/types/dialog.ts +++ b/packages/wallet/frontend/src/lib/types/dialog.ts @@ -6,16 +6,11 @@ export type DialogProps = { onClose: () => void } -const DIALOG_ACTIONS = { - OPEN: 'OPEN', - CLOSE: 'CLOSE' -} as const - export type DialogState = { isOpen: boolean dialog: JSX.Element | null } -export type DialogActions = keyof typeof DIALOG_ACTIONS +export type DialogActions = 'OPEN' | 'CLOSE' export type OpenDialogAction = { type: DialogActions data: { diff --git a/packages/wallet/frontend/src/middleware.ts b/packages/wallet/frontend/src/middleware.ts index db8f681c0..c783664e6 100644 --- a/packages/wallet/frontend/src/middleware.ts +++ b/packages/wallet/frontend/src/middleware.ts @@ -1,6 +1,8 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -import { userService } from './lib/api/user' +import { userService } from '@/lib/api/user' +import type { SuccessResponse, ErrorResponse } from '@/lib/httpClient' +import type { UserResponse } from '@wallet/shared' const isPublicPath = (path: string) => { return publicPaths.find((x) => @@ -10,14 +12,38 @@ const isPublicPath = (path: string) => { const publicPaths = ['/auth*'] +// When running behind a reverse proxy (e.g. Traefik), Next.js middleware sees +// the internal container URL (http://wallet-frontend:4003) as req.url. This +// causes redirects to point to the internal hostname instead of the public +// domain. We use the standard proxy headers to reconstruct the external origin. +function getExternalBaseUrl(req: NextRequest): string { + const proto = req.headers.get('x-forwarded-proto') + const host = req.headers.get('x-forwarded-host') + if (proto && host) { + return `${proto}://${host}` + } + return req.url +} + export async function middleware(req: NextRequest) { const callbackUrl = req.nextUrl.searchParams.get('callbackUrl') const isPublic = isPublicPath(req.nextUrl.pathname) const cookieName = process.env.COOKIE_NAME || 'testnet.cookie' + const baseUrl = getExternalBaseUrl(req) - const response = await userService.me( - `${cookieName}=${req.cookies.get(cookieName)?.value}` - ) + const cookieVal = req.cookies.get(cookieName)?.value + + let response: SuccessResponse | ErrorResponse = { + success: false, + message: '' + } + if (cookieVal) { + try { + response = await userService.me(`${cookieName}=${cookieVal}`) + } catch { + // Ignore connectivity errors; fallback logic below handles unauthenticated state + } + } // Success TRUE - the user is logged in if (response.success && response.result) { @@ -26,7 +52,7 @@ export async function middleware(req: NextRequest) { response.result.needsIDProof === true && req.nextUrl.pathname !== '/kyc' ) { - const url = new URL('/kyc', req.url) + const url = new URL('/kyc', baseUrl) return NextResponse.redirect(url) } @@ -36,18 +62,24 @@ export async function middleware(req: NextRequest) { response.result.needsIDProof === false && req.nextUrl.pathname.startsWith('/kyc') ) { - return NextResponse.redirect(new URL('/', req.url)) + return NextResponse.redirect(new URL('/', baseUrl)) } if (isPublic) { - return NextResponse.redirect(new URL(callbackUrl ?? '/', req.url)) + const dest = + callbackUrl && + callbackUrl.startsWith('/') && + !callbackUrl.startsWith('//') + ? callbackUrl + : '/' + return NextResponse.redirect(new URL(dest, baseUrl)) } } else { // If the user is not logged in and tries to access a private resource, // redirect to auth page or in the case of grant-interaction, back to the interaction page. if (!isPublic && !response.success) { - const url = new URL(`/auth/login/`, req.url) + const url = new URL(`/auth/login/`, baseUrl) if (req.nextUrl.pathname !== '') { url.searchParams.set( 'callbackUrl', diff --git a/packages/wallet/frontend/src/pages/auth/login.tsx b/packages/wallet/frontend/src/pages/auth/login.tsx index 996e8bcc4..b96e835ef 100644 --- a/packages/wallet/frontend/src/pages/auth/login.tsx +++ b/packages/wallet/frontend/src/pages/auth/login.tsx @@ -76,9 +76,11 @@ const LoginPage: NextPageWithLayout = () => { const isIncorrectCallbackUrl = !callbackPath.startsWith('/') && !callbackPath.startsWith(window.location.origin) - isIncorrectCallbackUrl - ? router.push('/') - : router.push(callbackPath).catch(() => router.push('/')) + if (isIncorrectCallbackUrl) { + router.push('/') + } else { + router.push(callbackPath).catch(() => router.push('/')) + } } function togglePasswordVisibility() { diff --git a/packages/wallet/frontend/src/pages/send.tsx b/packages/wallet/frontend/src/pages/send.tsx index 9b25121e6..2739d68e2 100644 --- a/packages/wallet/frontend/src/pages/send.tsx +++ b/packages/wallet/frontend/src/pages/send.tsx @@ -106,6 +106,15 @@ const SendPage: NextPageWithLayout = ({ accounts, user }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Consume refund wallet address from context in an effect rather than + // during render to avoid updating RefundProvider state while rendering. + useEffect(() => { + if (receiverWalletAddress !== '') { + sendForm.setValue('receiver', receiverWalletAddress) + setReceiverWalletAddress('') + } + }, [receiverWalletAddress, sendForm, setReceiverWalletAddress]) + const onAccountChange = async (accountId: string) => { const selectedAccount = accounts.find( (account) => account.value === accountId @@ -316,7 +325,7 @@ const SendPage: NextPageWithLayout = ({ accounts, user }) => { type="quote" onAccept={() => { handleAcceptQuote(quoteId) - closeDialog + closeDialog() }} onClose={closeDialog} /> @@ -407,21 +416,16 @@ const SendPage: NextPageWithLayout = ({ accounts, user }) => { { - value = - receiverWalletAddress !== '' ? receiverWalletAddress : value - setReceiverWalletAddress('') - return ( - - ) - }} + render={({ field: { value } }) => ( + + )} />
{isSepa ? ( diff --git a/packages/wallet/frontend/src/pages/transactions.tsx b/packages/wallet/frontend/src/pages/transactions.tsx index 448a35894..e2d8dbf30 100644 --- a/packages/wallet/frontend/src/pages/transactions.tsx +++ b/packages/wallet/frontend/src/pages/transactions.tsx @@ -287,9 +287,11 @@ const TransactionsPage: NextPageWithLayout = ({ { header: 'Date', sortFn: () => { - pagination.orderByDate === 'DESC' - ? redirect({ orderByDate: 'ASC' }) - : redirect({ orderByDate: 'DESC' }) + if (pagination.orderByDate === 'DESC') { + redirect({ orderByDate: 'ASC' }) + } else { + redirect({ orderByDate: 'DESC' }) + } }, getDirection: () => { return pagination.orderByDate === 'DESC' ? 'down' : 'up' diff --git a/packages/wallet/frontend/src/ui/TogglePayment.tsx b/packages/wallet/frontend/src/ui/TogglePayment.tsx index 342c7d6fa..b1c58ffde 100644 --- a/packages/wallet/frontend/src/ui/TogglePayment.tsx +++ b/packages/wallet/frontend/src/ui/TogglePayment.tsx @@ -39,7 +39,7 @@ export const TogglePayment = ({ const handleOnChange = () => { if (!disabled) { setEnabled(!enabled) - onChange && onChange(!enabled) + if (onChange) onChange(!enabled) } } diff --git a/packages/wallet/shared/src/types/grant.ts b/packages/wallet/shared/src/types/grant.ts index ad47cc18b..967aec86f 100644 --- a/packages/wallet/shared/src/types/grant.ts +++ b/packages/wallet/shared/src/types/grant.ts @@ -1,17 +1,6 @@ -const GRANT_STATE = { - APPROVED: 'APPROVED', - FINALIZED: 'FINALIZED', - PENDING: 'PENDING', - PROCESSING: 'PROCESSING' -} as const -type GrantState = keyof typeof GRANT_STATE +type GrantState = 'APPROVED' | 'FINALIZED' | 'PENDING' | 'PROCESSING' -const GRANT_FINALIZATION = { - ISSUED: 'ISSUED', - REJECTED: 'REJECTED', - REVOKED: 'REVOKED' -} -type GrantFinalization = keyof typeof GRANT_FINALIZATION +type GrantFinalization = 'ISSUED' | 'REJECTED' | 'REVOKED' type PaymentAmount = { value: string diff --git a/packages/wallet/shared/src/types/transaction.ts b/packages/wallet/shared/src/types/transaction.ts index 9fb7468c3..7e3bf19ce 100644 --- a/packages/wallet/shared/src/types/transaction.ts +++ b/packages/wallet/shared/src/types/transaction.ts @@ -1,16 +1,6 @@ -enum TRANSACTION_TYPE { - INCOMING = 'INCOMING', - OUTGOING = 'OUTGOING' -} -export type TransactionType = keyof typeof TRANSACTION_TYPE +export type TransactionType = 'INCOMING' | 'OUTGOING' -enum TRANSACTION_STATUS { - PENDING = 'PENDING', - COMPLETED = 'COMPLETED', - EXPIRED = 'EXPIRED', - FAILED = 'FAILED' -} -type TransactionStatus = keyof typeof TRANSACTION_STATUS +type TransactionStatus = 'PENDING' | 'COMPLETED' | 'EXPIRED' | 'FAILED' export enum CardTrxTypeEnum { Purchase = 0, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c44467fae..91c3e7195 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: ^9.32.0 version: 9.32.0 '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.0.0 + version: 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.0.0 + version: 8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) concurrently: specifier: ^9.2.1 version: 9.2.1 @@ -453,7 +453,7 @@ importers: version: 1.11.0 next: specifier: 14.2.32 - version: 14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.32(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@18.3.1) @@ -1095,10 +1095,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.4.0': resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1822,6 +1832,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} @@ -2657,63 +2672,64 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react-swc@3.11.0': resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} @@ -2985,6 +3001,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.4.2: resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} @@ -3052,6 +3072,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3484,6 +3508,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -3796,6 +3829,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.32.0: resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3933,6 +3970,15 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -4028,6 +4074,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4156,9 +4207,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-config@5.1.2: resolution: {integrity: sha512-kVwUuFz1h9u7B0nDPtnLFWN+x018niaH3zi1ChFCNfbunhDVJ911Z3YcglK5EfDfySeeH+zCa1aGxd1wMgNd7g==} engines: {node: '>= 16.0.0'} @@ -4313,6 +4361,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + immutable@3.7.6: resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} engines: {node: '>=0.8.0'} @@ -4998,6 +5050,10 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5402,6 +5458,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -5424,6 +5484,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -5962,6 +6032,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -6333,6 +6408,10 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -6384,11 +6463,11 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -7425,8 +7504,15 @@ snapshots: eslint: 9.32.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.32.0(jiti@1.21.7))': + dependencies: + eslint: 9.32.0(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/compat@1.4.0(eslint@9.32.0(jiti@1.21.7))': dependencies: '@eslint/core': 0.16.0 @@ -8453,6 +8539,11 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + optional: true + '@polka/url@1.0.0-next.25': {} '@protobufjs/aspromise@1.1.2': {} @@ -9262,86 +9353,96 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 7.18.0 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.32.0(jiti@1.21.7) - graphemer: 1.4.0 - ignore: 5.3.2 + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6(supports-color@5.5.0) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 eslint: 9.32.0(jiti@1.21.7) - optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.18.0': + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.58.0': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) - debug: 4.3.6(supports-color@5.5.0) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 eslint: 9.32.0(jiti@1.21.7) - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.58.0': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6(supports-color@5.5.0) - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.32.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.32.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) eslint: 9.32.0(jiti@1.21.7) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@7.18.0': + '@typescript-eslint/visitor-keys@8.58.0': dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 '@vitejs/plugin-react-swc@3.11.0(vite@5.4.20(@types/node@22.13.17))': dependencies: @@ -9706,6 +9807,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.4.2: optional: true @@ -9780,6 +9883,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -10226,6 +10333,10 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize@1.2.0: {} dedent@1.5.3(babel-plugin-macros@3.1.0): @@ -10610,6 +10721,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.32.0(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.32.0(jiti@1.21.7)) @@ -10818,6 +10931,10 @@ snapshots: transitivePeerDependencies: - encoding + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fecha@4.2.3: {} figures@3.2.0: @@ -10912,6 +11029,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -11099,8 +11219,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - graphql-config@5.1.2(@types/node@20.17.30)(graphql@16.11.0)(typescript@5.9.3): dependencies: '@graphql-tools/graphql-file-loader': 8.0.1(graphql@16.11.0) @@ -11280,6 +11398,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + immutable@3.7.6: {} import-fresh@3.3.0: @@ -12151,6 +12271,10 @@ snapshots: mini-svg-data-uri@1.4.4: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -12218,7 +12342,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.32(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.32 '@swc/helpers': 0.5.5 @@ -12240,6 +12364,7 @@ snapshots: '@next/swc-win32-ia32-msvc': 14.2.32 '@next/swc-win32-x64-msvc': 14.2.32 '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.58.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -12559,6 +12684,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.4: {} + pify@2.3.0: {} pino-abstract-transport@1.2.0: @@ -12588,6 +12715,16 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.58.2: + optional: true + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + optional: true + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 @@ -13114,6 +13251,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.4: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -13654,6 +13793,11 @@ snapshots: tildify@2.0.0: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + title-case@3.0.3: dependencies: tslib: 2.6.3 @@ -13694,7 +13838,7 @@ snapshots: triple-beam@1.4.1: {} - ts-api-utils@1.4.3(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3