From f5c4b3e65b66aba52bcce5e02c762703e8c3feda Mon Sep 17 00:00:00 2001 From: WISDOM Date: Fri, 29 May 2026 12:14:04 +0000 Subject: [PATCH 01/76] docs: add hardware meter integration guide (#320) - Hardware requirements (Ed25519 key storage, HSM/TPM recommendations) - API integration steps (key gen, hash, sign, submit) - Reference scripts documented - Certification checklist for new meter models - Contact information for integration support Closes #320 --- docs/HARDWARE_METER_INTEGRATION_GUIDE.md | 240 +++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 docs/HARDWARE_METER_INTEGRATION_GUIDE.md diff --git a/docs/HARDWARE_METER_INTEGRATION_GUIDE.md b/docs/HARDWARE_METER_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..27c0263 --- /dev/null +++ b/docs/HARDWARE_METER_INTEGRATION_GUIDE.md @@ -0,0 +1,240 @@ +# Hardware Meter Integration Guide + +This guide is for hardware manufacturers who want to integrate their smart meters with SolarProof. It covers hardware requirements, the signing protocol, API integration steps, and the certification process for new meter models. + +--- + +## Hardware Requirements + +### Ed25519 Key Storage + +Every SolarProof-compatible meter must generate and store an Ed25519 keypair. The private key must never leave the device. + +| Requirement | Minimum | Recommended | +|---|---|---| +| Key storage | Secure flash with access control | Hardware Security Module (HSM) or TPM 2.0 | +| Key generation | On-device CSPRNG | On-device CSPRNG with hardware entropy source | +| Key protection | Software access control | HSM / secure enclave (YubiKey, ATECC608, TPM) | +| Curve | Ed25519 | Ed25519 | +| Key size | 32-byte private key, 32-byte public key | Same | + +**Minimum viable hardware:** +- Microcontroller with at least 64 KB flash and 16 KB RAM +- Hardware RNG or entropy source +- Persistent storage that survives power cycles +- Network interface (Ethernet, Wi-Fi, or cellular) capable of HTTPS + +**Recommended hardware:** +- Dedicated HSM chip (e.g., Microchip ATECC608B, Infineon SLB 9670 TPM) +- Secure boot to prevent firmware tampering +- Tamper-evident enclosure + +### Connectivity + +- HTTPS (TLS 1.2+) to reach the SolarProof API +- Accurate real-time clock (RTC) — timestamp drift must be within ±30 seconds +- Minimum 1 kB/s uplink for reading submissions + +--- + +## API Integration Steps + +### Step 1 — Generate and Register the Meter Keypair + +Generate an Ed25519 keypair at manufacture time or on first boot. Store the private key in secure storage. Register the public key with the SolarProof operator before the meter goes live. + +**Reference script (development/testing only — do not use in production firmware):** + +```bash +node scripts/gen-meter-key.mjs +# Outputs meter-key.json: { private_key_hex, public_key_hex } +``` + +Register the public key in the SolarProof database: + +```sql +INSERT INTO meters (id, pubkey_hex, cooperative_id, active) +VALUES ('', '<64-char hex public key>', '', true); +``` + +The operator will provide the `cooperative_id` and confirm the `meter_id` (UUID) assigned to your device. + +### Step 2 — Compute the Canonical Reading Hash + +Before signing, compute a deterministic SHA-256 hash of the reading: + +``` +SHA-256( meter_id_utf8 || kwh_stroops_le64 || timestamp_le64 ) +``` + +- `meter_id_utf8` — the meter UUID as a UTF-8 byte string (e.g. `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`) +- `kwh_stroops_le64` — `round(kwh × 10_000_000)` as a little-endian 64-bit signed integer +- `timestamp_le64` — Unix epoch seconds (UTC) as a little-endian 64-bit signed integer + +**Node.js reference:** + +```js +import { createHash } from 'crypto' + +function computeReadingHash(meterId, kwh, timestamp) { + const kwhStroops = BigInt(Math.round(kwh * 1e7)) + const meterBytes = Buffer.from(meterId, 'utf8') + const kwhBuf = Buffer.alloc(8) + kwhBuf.writeBigInt64LE(kwhStroops) + const tsBuf = Buffer.alloc(8) + tsBuf.writeBigInt64LE(BigInt(timestamp)) + return createHash('sha256').update(meterBytes).update(kwhBuf).update(tsBuf).digest() +} +``` + +The server uses the identical algorithm in `apps/web/src/lib/crypto.ts`. Any deviation will cause signature verification to fail. + +### Step 3 — Sign the Reading + +Sign the 32-byte hash with the device's Ed25519 private key. The signature must be 64 bytes, encoded as a 128-character lowercase hex string. + +**Node.js reference:** + +```js +import { createSign } from 'crypto' + +function signReading(readingHash, privateKeyHex) { + const privKeyDer = Buffer.concat([ + Buffer.from('302e020100300506032b657004220420', 'hex'), + Buffer.from(privateKeyHex, 'hex'), + ]) + const sign = createSign('ed25519') + sign.update(readingHash) + return sign.sign({ key: privKeyDer, format: 'der', type: 'pkcs8' }).toString('hex') +} +``` + +For HSM-backed devices, use the HSM's signing API to produce the Ed25519 signature over the hash bytes — the output format is the same. + +### Step 4 — Submit the Reading + +``` +POST /api/readings +Content-Type: application/json +``` + +**Request body:** + +```json +{ + "meter_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "kwh": 12.5, + "timestamp": 1745500800, + "signature_hex": "<128-char lowercase hex>" +} +``` + +**Success — `201 Created`:** + +```json +{ + "reading_id": "", + "anchor_tx_hash": "<64-char hex>", + "mint_tx_hash": "<64-char hex>" +} +``` + +**Error codes:** + +| Status | Meaning | +|---|---| +| `400` | Malformed payload or validation failure | +| `401` | Invalid Ed25519 signature | +| `404` | Meter not found or inactive | +| `409` | Duplicate reading (already anchored) | +| `500` | Stellar transaction failure | + +### Step 5 — End-to-End Test with Reference Scripts + +Use the reference scripts in `scripts/` to validate your integration before certification: + +```bash +# Generate a test keypair +node scripts/gen-meter-key.mjs + +# Send a signed reading to a local or staging instance +node scripts/send-reading.mjs \ + --meter-id \ + --kwh 12.5 \ + --key ./meter-key.json \ + --api http://localhost:3000 + +# Run the full end-to-end flow +node scripts/e2e-meter-reading-flow.mjs +``` + +See `docs/METER_INTEGRATION.md` for the full protocol reference including the complete API specification. + +--- + +## Reference Scripts + +| Script | Purpose | +|---|---| +| `scripts/gen-meter-key.mjs` | Generate an Ed25519 keypair for a meter device | +| `scripts/send-reading.mjs` | Sign and submit a single meter reading | +| `scripts/e2e-meter-reading-flow.mjs` | Full end-to-end flow: key generation → reading → anchor → mint → verify | + +All scripts require Node.js v22+ and are intended for development, testing, and certification validation. Do not use them in production firmware. + +--- + +## Certification Checklist for New Meter Models + +Before a meter model is approved for production use with SolarProof, the manufacturer must complete the following checklist. Submit the completed checklist to the integration support contact below. + +### Hardware + +- [ ] Ed25519 keypair generated on-device using a hardware entropy source +- [ ] Private key stored in HSM, TPM, or secure enclave (not in plain flash) +- [ ] Secure boot enabled to prevent firmware tampering +- [ ] RTC accuracy verified to be within ±30 seconds of UTC +- [ ] Device passes tamper-detection requirements (physical or logical) + +### Protocol Compliance + +- [ ] Canonical reading hash matches the reference implementation for at least 100 test vectors +- [ ] Ed25519 signatures verified by the SolarProof server for at least 100 test readings +- [ ] Duplicate reading rejection confirmed (server returns `409` for repeated submissions) +- [ ] Replay attack prevention confirmed (server rejects stale timestamps) +- [ ] Error handling tested for all documented error codes (`400`, `401`, `404`, `409`, `500`) + +### Integration Testing + +- [ ] End-to-end test completed against the SolarProof staging environment +- [ ] `scripts/e2e-meter-reading-flow.mjs` passes against staging with the device's public key registered +- [ ] Reading submission latency measured and within acceptable bounds (<5 s under normal conditions) +- [ ] Behavior under network failure documented (retry logic, no duplicate submissions) + +### Security Review + +- [ ] Private key never transmitted or logged +- [ ] Firmware update mechanism does not expose key material +- [ ] Security contact and vulnerability disclosure process documented for the device + +### Documentation + +- [ ] Firmware version and hardware revision documented +- [ ] Key provisioning process documented +- [ ] Operator setup instructions provided + +--- + +## Contact Information for Integration Support + +For integration questions, certification submissions, or to report a protocol issue: + +- **GitHub Issues:** [github.com/AnnabelJoe/solarproof/issues](https://github.com/AnnabelJoe/solarproof/issues) — use the label `hardware-integration` +- **Security issues:** See [SECURITY.md](../SECURITY.md) for the responsible disclosure process +- **Protocol questions:** Open a discussion in the repository or reference `docs/METER_INTEGRATION.md` and `docs/adr/001-ed25519-signing.md` + +When submitting a certification request, include: +1. Completed certification checklist (above) +2. Hardware model name and firmware version +3. Test vector results (hash and signature outputs for the provided test inputs) +4. Contact name and organisation From b0bdc8d1c890a72739c8a8bf3dd115380382754e Mon Sep 17 00:00:00 2001 From: WISDOM Date: Fri, 29 May 2026 12:14:56 +0000 Subject: [PATCH 02/76] devops: configure Dependabot for npm, Cargo, and GitHub Actions (#306) - npm: weekly on Monday, patch/minor grouped, major flagged for review - Cargo: weekly on Monday, patch/minor grouped, major flagged for review - GitHub Actions: monthly, major flagged for review - Auto-merge for patch-level updates already wired via dependabot-auto-merge.yml Closes #306 --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8a344ce..4c20936 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -39,3 +39,17 @@ updates: # Flag major version bumps for manual review - dependency-name: "*" update-types: [version-update:semver-major] + + # GitHub Actions — monthly to reduce noise + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + labels: + - dependencies + - github-actions + ignore: + # Flag major version bumps for manual review + - dependency-name: "*" + update-types: [version-update:semver-major] From cabf63bfcc25fd5064e34492fb8ebb116528a0ec Mon Sep 17 00:00:00 2001 From: WISDOM Date: Fri, 29 May 2026 12:15:39 +0000 Subject: [PATCH 03/76] devops: add resource limits to Docker Compose services (#304) - web: 1 GB memory / 1.0 CPU (256 MB / 0.25 CPU reserved) - supabase-db: 512 MB memory / 0.5 CPU (128 MB / 0.1 CPU reserved) - redis: 128 MB memory / 0.25 CPU (32 MB / 0.05 CPU reserved) - README updated with minimum host requirements table Closes #304 --- README.md | 16 ++++++++++++++++ docker-compose.yml | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/README.md b/README.md index 147e3f2..4288db4 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,22 @@ To stop and remove containers (volumes are kept): docker compose down ``` +**Minimum host requirements for Docker Compose:** + +| Resource | Minimum | +|---|---| +| RAM | 2 GB available to Docker | +| CPU | 2 cores | +| Disk | 4 GB free | + +Resource limits per service (defined in `docker-compose.yml`): + +| Service | Memory limit | CPU limit | +|---|---|---| +| `web` | 1 GB | 1.0 core | +| `supabase-db` | 512 MB | 0.5 core | +| `redis` | 128 MB | 0.25 core | + ### Simulate a meter reading ```bash diff --git a/docker-compose.yml b/docker-compose.yml index ccdb5da..861918b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,14 @@ services: - ./apps/web:/app/apps/web - /app/apps/web/node_modules - /app/node_modules + deploy: + resources: + limits: + memory: 1g + cpus: "1.0" + reservations: + memory: 256m + cpus: "0.25" supabase-db: image: supabase/postgres:15.1.0.147 @@ -29,6 +37,14 @@ services: POSTGRES_DB: solarproof volumes: - supabase_data:/var/lib/postgresql/data + deploy: + resources: + limits: + memory: 512m + cpus: "0.5" + reservations: + memory: 128m + cpus: "0.1" redis: image: redis:7-alpine @@ -36,6 +52,14 @@ services: - "6379:6379" volumes: - redis_data:/data + deploy: + resources: + limits: + memory: 128m + cpus: "0.25" + reservations: + memory: 32m + cpus: "0.05" volumes: supabase_data: From 2638a9f73d6ef1036a34961819d35a8c52e4e428 Mon Sep 17 00:00:00 2001 From: WISDOM Date: Fri, 29 May 2026 12:17:35 +0000 Subject: [PATCH 04/76] docs: add operational runbooks (#315) - docs/runbooks/contract-deployment.md: testnet/mainnet deploy, verify, rollback - docs/runbooks/meter-key-rotation.md: scheduled and emergency key rotation - docs/runbooks/failed-mint-investigation.md: diagnose and retry failed mints - docs/runbooks/incident-response.md: triage, containment, resolution, postmortem - docs/runbooks/README.md: index of all runbooks Closes #315 --- docs/runbooks/README.md | 10 ++ docs/runbooks/contract-deployment.md | 89 +++++++++++++++ docs/runbooks/failed-mint-investigation.md | 97 ++++++++++++++++ docs/runbooks/incident-response.md | 125 +++++++++++++++++++++ docs/runbooks/meter-key-rotation.md | 77 +++++++++++++ 5 files changed, 398 insertions(+) create mode 100644 docs/runbooks/README.md create mode 100644 docs/runbooks/contract-deployment.md create mode 100644 docs/runbooks/failed-mint-investigation.md create mode 100644 docs/runbooks/incident-response.md create mode 100644 docs/runbooks/meter-key-rotation.md diff --git a/docs/runbooks/README.md b/docs/runbooks/README.md new file mode 100644 index 0000000..8e50226 --- /dev/null +++ b/docs/runbooks/README.md @@ -0,0 +1,10 @@ +# Runbooks + +Operational runbooks for common SolarProof procedures. + +| Runbook | Description | +|---|---| +| [contract-deployment.md](contract-deployment.md) | Deploy Soroban contracts to testnet and mainnet | +| [meter-key-rotation.md](meter-key-rotation.md) | Rotate an Ed25519 meter signing key | +| [failed-mint-investigation.md](failed-mint-investigation.md) | Diagnose and resolve failed energy token mint jobs | +| [incident-response.md](incident-response.md) | Detect, contain, resolve, and learn from incidents | diff --git a/docs/runbooks/contract-deployment.md b/docs/runbooks/contract-deployment.md new file mode 100644 index 0000000..3bd3b5b --- /dev/null +++ b/docs/runbooks/contract-deployment.md @@ -0,0 +1,89 @@ +# Runbook: Contract Deployment + +Covers deploying SolarProof Soroban contracts to testnet and mainnet. + +For full deployment documentation see [docs/DEPLOYMENT.md](../DEPLOYMENT.md). + +--- + +## Prerequisites + +- Rust toolchain (see `apps/contracts/rust-toolchain.toml`) +- `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` +- Stellar CLI: `cargo install --locked stellar-cli --features opt` +- Funded deployer account (testnet: use friendbot; mainnet: real XLM) +- `DEPLOYER_SECRET_KEY` environment variable set + +--- + +## Testnet Deployment + +```bash +# 1. Build contracts +cd apps/contracts && stellar contract build + +# 2. Deploy (idempotent — skips already-deployed contracts) +DEPLOYER_SECRET_KEY= bash scripts/deploy-testnet.sh +``` + +The script writes contract IDs to `scripts/deployments/testnet.json`. + +```bash +# 3. Initialize each contract +ADMIN=$(stellar keys address deployer) + +stellar contract invoke --id $TOKEN_ID --source deployer --network testnet \ + -- initialize --admin $ADMIN --minter $ADMIN + +stellar contract invoke --id $REGISTRY_ID --source deployer --network testnet \ + -- initialize --admin $ADMIN + +stellar contract invoke --id $GOV_ID --source deployer --network testnet \ + -- initialize --admin $ADMIN --quorum 51 --voting_period_ledgers 17280 + +# 4. Update docs/deployments.md with the new contract IDs +# 5. Set contract IDs in .env.local (see docs/DEPLOYMENT.md §2d) +``` + +--- + +## Mainnet Deployment + +> ⚠️ Irreversible. Use an HSM-backed key. Test on testnet first. + +Same steps as testnet — replace `--network testnet` with `--network mainnet` and use `scripts/deploy-mainnet.sh`. + +--- + +## Verify Deployed Bytecode + +```bash +# Compute local WASM hash +sha256sum apps/contracts/target/wasm32-unknown-unknown/release/energy_token.wasm + +# Compare against on-chain hash at: +# https://stellar.expert/explorer/testnet/contract/ +# Contract tab → WASM section → WASM hash +``` + +Hashes must match. A mismatch means the on-chain contract differs from the local build. + +--- + +## Rollback + +Soroban contracts are immutable. Rollback = deploy a new contract and update env vars. + +1. Deploy corrected WASM → new contract ID +2. Update `NEXT_PUBLIC_ENERGY_TOKEN_ID` (and/or other IDs) in environment +3. Redeploy web app (Vercel picks up new env vars automatically) +4. Update `docs/deployments.md` with new ID and rollback note +5. Do not delete the old contract — it is an audit record + +--- + +## CI / Automated Deployment + +Testnet deployment runs automatically on push to `main` via `.github/workflows/deploy-contracts.yml`. + +To trigger manually: GitHub → Actions → Deploy Contracts → Run workflow. diff --git a/docs/runbooks/failed-mint-investigation.md b/docs/runbooks/failed-mint-investigation.md new file mode 100644 index 0000000..163e912 --- /dev/null +++ b/docs/runbooks/failed-mint-investigation.md @@ -0,0 +1,97 @@ +# Runbook: Investigating Failed Mint Jobs + +Covers diagnosing and resolving failed energy token mint jobs. + +--- + +## Background + +When a meter reading is submitted, the API: +1. Verifies the Ed25519 signature +2. Anchors the reading hash to Stellar via `audit_registry` +3. Mints an `energy_token` (1 token = 1 kWh) + +A mint failure means step 3 failed. The reading may still be anchored (step 2 succeeded). Failed mints are recorded in the `mint_jobs` table with a `status` of `failed` and a `diagnosis` field populated by tracer-sim. + +--- + +## Step 1 — Identify the Failed Job + +```sql +SELECT id, meter_id, kwh, created_at, status, diagnosis, anchor_tx_hash, mint_tx_hash +FROM mint_jobs +WHERE status = 'failed' +ORDER BY created_at DESC +LIMIT 20; +``` + +Check the `diagnosis` field — tracer-sim auto-populates a failure reason when available. + +--- + +## Step 2 — Common Failure Causes + +| Diagnosis / symptom | Likely cause | Resolution | +|---|---|---| +| `insufficient_balance` | Minter account out of XLM | Top up the minter account (see Step 3) | +| `contract_not_found` | Wrong contract ID in env | Verify `NEXT_PUBLIC_ENERGY_TOKEN_ID` matches deployed contract | +| `sequence_number_mismatch` | Concurrent mint race | Retry the job (usually self-resolving) | +| `network_timeout` | Stellar RPC unreachable | Check Stellar network status; retry after recovery | +| `signature_invalid` | Minter key mismatch | Verify `MINTER_SECRET_KEY` env var matches the contract's authorized minter | +| `already_minted` | Duplicate job | Check if a successful mint exists for the same `reading_id`; mark job resolved | + +--- + +## Step 3 — Top Up the Minter Account (if needed) + +```bash +# Check minter balance +stellar account info --account --network testnet + +# Testnet: use friendbot +curl "https://friendbot.stellar.org?addr=" + +# Mainnet: transfer XLM from a funded account +stellar payment send \ + --source \ + --destination \ + --amount 100 \ + --network mainnet +``` + +--- + +## Step 4 — Retry the Failed Job + +```bash +# Trigger a retry via the API (if a retry endpoint exists) +curl -X POST https:///api/admin/mint-jobs//retry \ + -H "Authorization: Bearer " +``` + +Or re-submit the original reading — the server is idempotent for anchoring (returns `409` if already anchored) but will attempt a fresh mint if the previous one failed. + +--- + +## Step 5 — Verify Resolution + +```sql +SELECT id, status, mint_tx_hash FROM mint_jobs WHERE id = ''; +``` + +Confirm `status = 'completed'` and `mint_tx_hash` is populated. + +Verify the on-chain mint at: +``` +https://stellar.expert/explorer/testnet/tx/ +``` + +--- + +## Step 6 — Escalate if Unresolved + +If the failure persists after retrying: +1. Capture the full `diagnosis` text and `anchor_tx_hash` +2. Open an incident (see [incident-response.md](incident-response.md)) +3. Check Stellar network status at https://status.stellar.org +4. Review tracer-sim output in application logs for the full replay trace diff --git a/docs/runbooks/incident-response.md b/docs/runbooks/incident-response.md new file mode 100644 index 0000000..9002c9e --- /dev/null +++ b/docs/runbooks/incident-response.md @@ -0,0 +1,125 @@ +# Runbook: Incident Response + +Covers detecting, containing, resolving, and learning from incidents affecting SolarProof. + +--- + +## Severity Levels + +| Level | Description | Response time | +|---|---|---| +| P1 — Critical | Production down, data loss, security breach | Immediate | +| P2 — High | Core feature broken, significant user impact | < 1 hour | +| P3 — Medium | Degraded performance, non-critical feature broken | < 4 hours | +| P4 — Low | Minor issue, cosmetic, no user impact | Next business day | + +--- + +## Phase 1 — Detection and Triage + +1. **Detect** — via monitoring alert, error report, or user feedback +2. **Record** — open an incident issue on GitHub with: + - Severity level + - Affected systems (web app, API, database, smart contracts, infrastructure) + - Observed symptoms and first detection time +3. **Assign** — designate an incident commander (IC) responsible for coordination +4. **Communicate** — notify stakeholders via the agreed channel (Slack, email, etc.) + +--- + +## Phase 2 — Containment + +Act to stop the incident from worsening before root cause is known. + +| Affected system | Containment action | +|---|---| +| Web app / API | Roll back the last Vercel deployment | +| Smart contract exploit | Invoke contract pause via governance (see below) | +| Compromised meter key | Deactivate the meter record immediately (see [meter-key-rotation.md](meter-key-rotation.md)) | +| Database corruption | Stop write traffic; put app in maintenance mode | +| Failed mints (bulk) | Pause the mint job queue; investigate (see [failed-mint-investigation.md](failed-mint-investigation.md)) | + +**Pause a smart contract (if pause function available):** + +```bash +stellar contract invoke --id --source --network mainnet \ + -- pause +``` + +**Roll back a Vercel deployment:** + +```bash +vercel rollback --token +# Or via Vercel dashboard: Deployments → previous deployment → Promote to Production +``` + +**Preserve evidence before making changes:** + +```bash +# Capture recent application logs +# Export relevant database tables +# Screenshot monitoring dashboards +``` + +--- + +## Phase 3 — Investigation + +1. Review application logs for errors around the incident start time +2. Check recent deployments, config changes, and dependency updates +3. Query the database for anomalous data: + +```sql +-- Recent failed readings +SELECT * FROM readings WHERE created_at > now() - interval '1 hour' AND status != 'anchored'; + +-- Recent failed mints +SELECT * FROM mint_jobs WHERE status = 'failed' AND created_at > now() - interval '1 hour'; + +-- Audit log for recent admin actions +SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 50; +``` + +4. Check Stellar network status: https://status.stellar.org +5. Check Vercel deployment status: https://vercel.com/status + +--- + +## Phase 4 — Resolution + +1. Apply the fix (code patch, config change, data correction, or rollback) +2. Validate recovery: + - Run smoke tests against production + - Confirm error rates return to baseline in monitoring + - Verify a successful end-to-end reading submission if the API was affected +3. Lift containment measures (re-enable features, unpause contracts, restore write traffic) +4. Confirm with stakeholders that the incident is resolved + +--- + +## Phase 5 — Postmortem + +Complete within 48 hours of resolution for P1/P2 incidents. + +1. Write a postmortem document covering: + - Timeline (detection → containment → resolution) + - Root cause + - Impact (users affected, data affected, duration) + - What went well + - What went wrong + - Action items with owners and due dates +2. Update this runbook if any procedure was unclear or missing +3. Add monitoring or alerting to catch the same issue earlier next time +4. Share the postmortem with the team + +--- + +## Useful Links + +- Stellar network status: https://status.stellar.org +- Stellar Expert (testnet): https://stellar.expert/explorer/testnet +- Stellar Expert (mainnet): https://stellar.expert/explorer/mainnet +- Vercel dashboard: https://vercel.com/dashboard +- Supabase dashboard: https://app.supabase.com +- GitHub Actions: https://github.com/AnnabelJoe/solarproof/actions +- Security policy: [SECURITY.md](../../SECURITY.md) diff --git a/docs/runbooks/meter-key-rotation.md b/docs/runbooks/meter-key-rotation.md new file mode 100644 index 0000000..809b63c --- /dev/null +++ b/docs/runbooks/meter-key-rotation.md @@ -0,0 +1,77 @@ +# Runbook: Meter Key Rotation + +Covers rotating the Ed25519 signing key for a meter device — scheduled rotation, suspected compromise, or key loss. + +--- + +## When to Rotate + +- Scheduled rotation (recommended: annually or per security policy) +- Private key suspected compromised or exposed +- Device transferred to a new operator +- Key material lost + +--- + +## Steps + +### 1. Generate a new keypair + +```bash +node scripts/gen-meter-key.mjs +# Writes meter-key-new.json: { private_key_hex, public_key_hex } +``` + +For production devices, generate the keypair on the device itself (HSM/TPM). Never generate a production key on a workstation. + +### 2. Register the new public key + +Insert the new key into the `meters` table with a new UUID, keeping the old record active during the transition: + +```sql +INSERT INTO meters (id, pubkey_hex, cooperative_id, active) +VALUES (gen_random_uuid(), '', '', true); +``` + +Note the new `meter_id` — the device must use this UUID in all future reading submissions. + +### 3. Update the device + +Deploy the new private key and new `meter_id` to the device. For HSM-backed devices, provision the new key into the secure enclave and update the device configuration. + +### 4. Verify the new key works + +Send a test reading using the new key and confirm a `201 Created` response: + +```bash +node scripts/send-reading.mjs \ + --meter-id \ + --kwh 0.001 \ + --key ./meter-key-new.json \ + --api https:// +``` + +### 5. Deactivate the old meter record + +Once the new key is confirmed working, deactivate the old record: + +```sql +UPDATE meters SET active = false WHERE id = ''; +``` + +The old record is retained for audit purposes — do not delete it. + +### 6. Securely destroy the old private key + +- Remove the old key from the device's secure storage +- Delete any copies from workstations, CI secrets, or backups +- Record the rotation in the audit log + +--- + +## Notes + +- The server rejects readings from inactive meter records (`404` response) +- Readings signed with the old key after deactivation will be rejected +- If the key was compromised, deactivate the old record immediately (step 5) before completing the rest of the rotation +- Test rotation in staging before applying to production meters From 9385ff4964ff0ec1a516b56a574f7f7eecab3672 Mon Sep 17 00:00:00 2001 From: UNIXX Date: Fri, 29 May 2026 13:46:38 +0000 Subject: [PATCH 05/76] feat: enable Turborepo remote caching for all CI steps (#297) - Enable test task caching in turbo.json (was cache: false) - Add TURBO_TOKEN/TURBO_TEAM env vars to all CI steps (test, contracts fmt/clippy/test) - Remote cache signature verification already enabled --- .github/workflows/ci.yml | 12 ++++++++++++ turbo.json | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eeb65f..ef373a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,9 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - run: pnpm test working-directory: apps/web + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - run: pnpm build working-directory: apps/web env: @@ -128,14 +131,23 @@ jobs: - name: fmt run: cargo fmt --all -- --check working-directory: apps/contracts + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: clippy run: cargo clippy --all-targets --all-features -- -D warnings working-directory: apps/contracts + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - name: test run: cargo test --all working-directory: apps/contracts + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} proptest: name: Property-based tests (proptest) diff --git a/turbo.json b/turbo.json index 8a533ca..458167b 100644 --- a/turbo.json +++ b/turbo.json @@ -14,7 +14,11 @@ "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^lint"] }, "type-check": { "dependsOn": ["^type-check"] }, - "test": { "cache": false }, + "test": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$"], + "outputs": ["coverage/**"] + }, "clean": { "cache": false } } } From 60bfed06c8c067140e0b5fd8ad398cd166715e83 Mon Sep 17 00:00:00 2001 From: UNIXX Date: Fri, 29 May 2026 13:47:53 +0000 Subject: [PATCH 06/76] test: add contract event emission tests for all state-changing functions (#330) - energy_token: mint, transfer, burn, retire event tests - audit_registry: anchor event test (topics + data fields verified) - community_governance: propose, vote, finalize, execute event tests - Each test verifies event topic and data payload fields --- apps/contracts/audit_registry/src/lib.rs | 19 ++++ .../contracts/community_governance/src/lib.rs | 94 +++++++++++++++++++ apps/contracts/energy_token/src/lib.rs | 69 ++++++++++++++ 3 files changed, 182 insertions(+) diff --git a/apps/contracts/audit_registry/src/lib.rs b/apps/contracts/audit_registry/src/lib.rs index 68e5520..2532be2 100644 --- a/apps/contracts/audit_registry/src/lib.rs +++ b/apps/contracts/audit_registry/src/lib.rs @@ -451,4 +451,23 @@ mod tests { soroban_sdk::String::from_str(&env, "1.0.0") ); } + + // ── event emission tests (#330) ────────────────────────────────────────── + + #[test] + fn test_anchor_emits_event() { + let (env, api_signer, client) = setup(); + let h = hash(&env); + client.anchor(&api_signer, &h).unwrap(); + let events = env.events().all(); + let anchor_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("anchor"), &env)] + }); + assert!(anchor_event.is_some(), "anchor event not emitted"); + // data = (reading_hash, ledger_sequence, timestamp) + let (_, _, data) = anchor_event.unwrap(); + let (emitted_hash, _ledger, _ts): (BytesN<32>, u32, u64) = + soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(emitted_hash, h); + } } diff --git a/apps/contracts/community_governance/src/lib.rs b/apps/contracts/community_governance/src/lib.rs index 02ea4a6..920ff10 100644 --- a/apps/contracts/community_governance/src/lib.rs +++ b/apps/contracts/community_governance/src/lib.rs @@ -1081,4 +1081,98 @@ mod tests { client.finalize(&id); assert_eq!(client.get_proposal(&id).unwrap().status, ProposalStatus::Expired); } + + // ── event emission tests (#330) ────────────────────────────────────────── + + #[test] + fn test_propose_emits_event() { + let (env, _admin, client) = setup(); + let proposer = Address::generate(&env); + client.propose( + &proposer, + &String::from_str(&env, "Title"), + &String::from_str(&env, "Desc"), + ); + let events = env.events().all(); + let propose_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("propose"), &env)] + }); + assert!(propose_event.is_some(), "propose event not emitted"); + let (_, _, data) = propose_event.unwrap(); + let proposal_id: u32 = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(proposal_id, 1_u32); + } + + #[test] + fn test_vote_emits_event() { + let (env, _admin, client) = setup(); + let proposer = Address::generate(&env); + let voter = Address::generate(&env); + let id = client.propose( + &proposer, + &String::from_str(&env, "T"), + &String::from_str(&env, "D"), + ); + client.vote(&voter, &id, &true); + let events = env.events().all(); + let vote_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("vote"), &env)] + }); + assert!(vote_event.is_some(), "vote event not emitted"); + let (_, _, data) = vote_event.unwrap(); + let (pid, _voter_addr, approve): (u32, Address, bool) = + soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(pid, id); + assert!(approve); + } + + #[test] + fn test_finalize_emits_event() { + let (env, _admin, client) = setup(); + let proposer = Address::generate(&env); + let id = client.propose( + &proposer, + &String::from_str(&env, "T"), + &String::from_str(&env, "D"), + ); + client.vote(&Address::generate(&env), &id, &true); + client.vote(&Address::generate(&env), &id, &true); + env.ledger().with_mut(|l| l.sequence_number += 101); + client.finalize(&id); + let events = env.events().all(); + let final_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("final"), &env)] + }); + assert!(final_event.is_some(), "finalize event not emitted"); + let (_, _, data) = final_event.unwrap(); + let (pid, status): (u32, ProposalStatus) = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(pid, id); + assert_eq!(status, ProposalStatus::Passed); + } + + #[test] + fn test_execute_emits_event() { + let (env, _admin, client) = setup(); + let proposer = Address::generate(&env); + let id = client.propose( + &proposer, + &String::from_str(&env, "T"), + &String::from_str(&env, "D"), + ); + client.vote(&Address::generate(&env), &id, &true); + client.vote(&Address::generate(&env), &id, &true); + env.ledger().with_mut(|l| l.sequence_number += 101); + client.finalize(&id); + env.ledger() + .with_mut(|l| l.sequence_number += EXECUTE_TIMELOCK_LEDGERS); + client.execute(&id); + let events = env.events().all(); + let exec_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("exec"), &env)] + }); + assert!(exec_event.is_some(), "execute event not emitted"); + let (_, _, data) = exec_event.unwrap(); + let pid: u32 = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(pid, id); + } } diff --git a/apps/contracts/energy_token/src/lib.rs b/apps/contracts/energy_token/src/lib.rs index acf029f..b088b94 100644 --- a/apps/contracts/energy_token/src/lib.rs +++ b/apps/contracts/energy_token/src/lib.rs @@ -1013,4 +1013,73 @@ mod tests { client.mint(&user, &1_i128); assert_eq!(client.balance(&user), 1_i128); } + + // ── event emission tests (#330) ────────────────────────────────────────── + + #[test] + fn test_mint_emits_event() { + let (env, client) = setup(); + let user = Address::generate(&env); + client.mint(&user, &500_i128); + let events = env.events().all(); + // Find the mint event: topic = ("mint",), data = (to, amount) + let mint_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("mint"), &env)] + }); + assert!(mint_event.is_some(), "mint event not emitted"); + let (_, _, data) = mint_event.unwrap(); + let (to, amount): (Address, i128) = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(to, user); + assert_eq!(amount, 500_i128); + } + + #[test] + fn test_transfer_emits_event() { + let (env, client) = setup(); + let a = Address::generate(&env); + let b = Address::generate(&env); + client.mint(&a, &1000_i128); + env.events().all(); // clear + client.transfer(&a, &b, &300_i128); + let events = env.events().all(); + let transfer_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("transfer"), &env)] + }); + assert!(transfer_event.is_some(), "transfer event not emitted"); + let (_, _, data) = transfer_event.unwrap(); + let (from, to, amount): (Address, Address, i128) = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(from, a); + assert_eq!(to, b); + assert_eq!(amount, 300_i128); + } + + #[test] + fn test_retire_emits_event() { + let (env, client) = setup(); + let user = Address::generate(&env); + client.mint(&user, &1000_i128); + client.retire(&user, &String::from_str(&env, "REC compliance")); + let events = env.events().all(); + let retire_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("retire"), &env)] + }); + assert!(retire_event.is_some(), "retire event not emitted"); + } + + #[test] + fn test_burn_emits_event() { + let (env, client) = setup(); + let user = Address::generate(&env); + client.mint(&user, &1000_i128); + client.burn(&user, &200_i128); + let events = env.events().all(); + let burn_event = events.iter().find(|(_, topics, _)| { + topics == &soroban_sdk::vec![&env, soroban_sdk::IntoVal::::into_val(&symbol_short!("burn"), &env)] + }); + assert!(burn_event.is_some(), "burn event not emitted"); + let (_, _, data) = burn_event.unwrap(); + let (from, amount): (Address, i128) = soroban_sdk::FromVal::from_val(&env, &data); + assert_eq!(from, user); + assert_eq!(amount, 200_i128); + } } From 3ee0154492cccc7ce78c61925c0b72c44120fdf4 Mon Sep 17 00:00:00 2001 From: DEVEUNICE Date: Fri, 29 May 2026 16:03:43 +0000 Subject: [PATCH 07/76] feat: configure log aggregation and retention (#299) --- apps/web/.env.example | 3 ++ apps/web/instrumentation.ts | 5 +++ apps/web/src/lib/logger.ts | 17 ++++++++++ apps/web/tsconfig.tsbuildinfo | 1 + docs/LOGGING.md | 64 +++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 apps/web/instrumentation.ts create mode 100644 apps/web/src/lib/logger.ts create mode 100644 apps/web/tsconfig.tsbuildinfo create mode 100644 docs/LOGGING.md diff --git a/apps/web/.env.example b/apps/web/.env.example index 40e72c8..93ac024 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -11,3 +11,6 @@ NEXT_PUBLIC_COMMUNITY_GOVERNANCE_ID= # Minter keypair (server-side only) MINTER_SECRET_KEY= + +# Logging (Better Stack / Logtail) +LOGTAIL_SOURCE_TOKEN= \ No newline at end of file diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts new file mode 100644 index 0000000..d9e6af3 --- /dev/null +++ b/apps/web/instrumentation.ts @@ -0,0 +1,5 @@ +import { log } from "@logtail/next"; + +export async function register() { + log.info("SolarProof API initializing", { env: process.env.NODE_ENV }); +} diff --git a/apps/web/src/lib/logger.ts b/apps/web/src/lib/logger.ts new file mode 100644 index 0000000..f722045 --- /dev/null +++ b/apps/web/src/lib/logger.ts @@ -0,0 +1,17 @@ +import { log as logtail } from "@logtail/next"; + +const SENSITIVE = /secret|key|signature|token/i; + +function sanitize(meta: Record): Record { + return Object.fromEntries( + Object.entries(meta).map(([k, v]) => [k, SENSITIVE.test(k) ? "[REDACTED]" : v]) + ); +} + +export function log( + level: "debug" | "info" | "warn" | "error", + message: string, + meta: Record = {} +) { + logtail[level](message, sanitize(meta)); +} diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo new file mode 100644 index 0000000..73f08a8 --- /dev/null +++ b/apps/web/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.2.15/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.15/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/css.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/macro.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/style.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/global.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/amp.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/amp.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/get-page-files.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.19/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.15/node_modules/@types/react/canary.d.ts","../../node_modules/.pnpm/@types+react@19.2.15/node_modules/@types/react/experimental.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.15/node_modules/@types/react-dom/index.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.15/node_modules/@types/react-dom/canary.d.ts","../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.15/node_modules/@types/react-dom/experimental.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/fallback.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/webpack/webpack.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/load-custom-routes.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/body-streams.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/revalidate.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/worker.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/constants.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/app-router-headers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/rendering-mode.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/require-hook.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/page-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-kind.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render-result.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/route-module.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/load-components.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/flight-data-helpers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/mitt.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/with-router.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/router.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/route-loader.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/page-loader.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/router.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/pages.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/instrumentation/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/i18n-provider.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/normalizer.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/builtin-request-context.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/next-url.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/request.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/response.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/adapter.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-life.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/constants.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/page-extensions-type.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/app-dir-module.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/cache-signal.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/fallback-params.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/clean-async-snapshot-instance.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/clean-async-snapshot.external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/app-render.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../../node_modules/.pnpm/@types+react@19.2.15/node_modules/@types/react/jsx-runtime.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/error-boundary.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/layout-router.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/render-from-template-context.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-page.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-segment.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/search-params.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/hooks-server-context.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/icons.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/metadata.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/metadata-boundary.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/entry-base.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-page.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-baseline.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/work-store.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/http.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-status-code.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-error.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-route.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/utils.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/worker.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/worker.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after-context.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/params.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/route-match.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request-meta.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/cli/next-test.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config-shared.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/api-utils/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/node.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/async-callback-set.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../../node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/image-optimizer.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/coalesced-function.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/trace.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/shared.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/load-jsconfig.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack-config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/generated-native.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/parse-version-info.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/react-dev-overlay/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/telemetry/storage.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/render-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/lru-cache.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/next-dev-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../../node_modules/.pnpm/@next+env@15.1.3/node_modules/@next/env/dist/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/utils.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_app.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/app.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/cache.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/config.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_document.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/document.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/dynamic.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dynamic.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_error.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/error.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/head.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/cookies.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/headers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/draft-mode.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/headers.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/get-img-props.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/image-component.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-external.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/link.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/link.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/not-found.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/forbidden.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unauthorized.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.react-server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/navigation.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/router.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/script.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/script.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/connection.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/server.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/global.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/compiled.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image-types/global.d.ts","./next-env.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/shared.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/platform/base.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/platform/generic.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/platform/netlify.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/logger.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/config.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/withBetterStack.d.ts","../../node_modules/.pnpm/@logtail+next@0.3.1_next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6__react@19.2.6/node_modules/@logtail/next/dist/index.d.ts","./instrumentation.ts","./next.config.ts","../../node_modules/.pnpm/@supabase+functions-js@2.106.2/node_modules/@supabase/functions-js/dist/module/types.d.ts","../../node_modules/.pnpm/@supabase+functions-js@2.106.2/node_modules/@supabase/functions-js/dist/module/FunctionsClient.d.ts","../../node_modules/.pnpm/@supabase+functions-js@2.106.2/node_modules/@supabase/functions-js/dist/module/index.d.ts","../../node_modules/.pnpm/@supabase+postgrest-js@2.106.2/node_modules/@supabase/postgrest-js/dist/index.d.mts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/lib/websocket-factory.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/lib/serializer.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/constants.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/longpoll.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/types.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/timer.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/socket.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/push.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/channel.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/presence.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/serializer.d.ts","../../node_modules/.pnpm/@supabase+phoenix@0.4.2/node_modules/@supabase/phoenix/priv/static/types/index.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/phoenix/types.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/lib/constants.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/RealtimePresence.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/RealtimeChannel.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/RealtimeClient.d.ts","../../node_modules/.pnpm/@supabase+realtime-js@2.106.2/node_modules/@supabase/realtime-js/dist/module/index.d.ts","../../node_modules/.pnpm/iceberg-js@0.8.1/node_modules/iceberg-js/dist/index.d.ts","../../node_modules/.pnpm/@supabase+storage-js@2.106.2/node_modules/@supabase/storage-js/dist/index.d.mts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/error-codes.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/errors.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/web3/ethereum.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/web3/solana.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/webauthn.dom.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/helpers.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/GoTrueClient.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/webauthn.errors.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/webauthn.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/types.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/fetch.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/GoTrueAdminApi.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/AuthAdminApi.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/AuthClient.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/lib/locks.d.ts","../../node_modules/.pnpm/@supabase+auth-js@2.106.2/node_modules/@supabase/auth-js/dist/module/index.d.ts","../../node_modules/.pnpm/@supabase+supabase-js@2.106.2/node_modules/@supabase/supabase-js/dist/index.d.mts","./src/lib/database.types.ts","./src/lib/supabase.ts","./src/app/api/health/route.ts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/index.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/enumUtil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/partialUtil.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.d.cts","../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.d.cts","../../node_modules/.pnpm/@stellar+stellar-base@13.1.0/node_modules/@stellar/stellar-base/types/curr.d.ts","../../node_modules/.pnpm/@stellar+stellar-base@13.1.0/node_modules/@stellar/stellar-base/types/xdr.d.ts","../../node_modules/.pnpm/@stellar+stellar-base@13.1.0/node_modules/@stellar/stellar-base/types/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/horizon_api.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/network.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/not_found.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/bad_request.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/bad_response.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/account_requires_memo.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/errors/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/config.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/utils.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/stellartoml/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/federation/api.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/federation/server.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/federation/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/types/account.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/types/assets.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/types/offer.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/types/effects.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/types/trade.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/server_api.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/webauth/utils.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/webauth/errors.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/webauth/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/friendbot/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/account_response.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/account_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/assets_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/claimable_balances_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/effect_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/friendbot_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/ledger_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/liquidity_pool_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/offer_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/operation_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/orderbook_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/path_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/payment_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/trade_aggregation_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/trades_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/transaction_call_builder.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/server.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/http-client/types.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/http-client/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/horizon_axios_client.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/horizon/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/server.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/axios.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/parsers.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/transaction.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/rpc/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/types.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/sent_transaction.d.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/spec.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/assembled_transaction.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/basic_node_signer.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/client.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/rust_result.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/contract/index.d.ts","../../node_modules/.pnpm/@stellar+stellar-sdk@13.3.0/node_modules/@stellar/stellar-sdk/lib/index.d.ts","./src/lib/stellar.ts","./src/lib/crypto.ts","./src/app/api/readings/route.ts","./src/app/api/verify/route.ts","./src/lib/logger.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/types/components.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/types/sdk.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/types/storage.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/types/mod.d.ts","../../node_modules/.pnpm/preact@10.29.2/node_modules/preact/src/jsx.d.ts","../../node_modules/.pnpm/preact@10.29.2/node_modules/preact/src/dom.d.ts","../../node_modules/.pnpm/preact@10.29.2/node_modules/preact/src/index.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/shared/header.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/shared/footer.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/shared/avatar.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/shared/button.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/shared/mod.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/app.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/kit-button.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/router.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/components/mod.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/sdk/kit.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/sdk/utils.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/sdk/mod.d.ts","../../node_modules/.pnpm/@creit.tech+stellar-wallets-kit@2.2.0_@solana+sysvars@5.5.1_fastestsmallesttextencoderd_f4f27a0f56a835ef31948561206a12a4/node_modules/@creit.tech/stellar-wallets-kit/esm/sdk/modules/freighter.module.d.ts","./src/lib/wallet.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@next/font/dist/types.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","../../node_modules/.pnpm/next@15.1.3_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/font/google/index.d.ts","../../node_modules/.pnpm/@tanstack+query-core@5.100.14/node_modules/@tanstack/query-core/build/modern/_tsup-dts-rollup.d.ts","../../node_modules/.pnpm/@tanstack+query-core@5.100.14/node_modules/@tanstack/query-core/build/modern/index.d.ts","../../node_modules/.pnpm/@tanstack+react-query@5.100.14_react@19.2.6/node_modules/@tanstack/react-query/build/modern/_tsup-dts-rollup.d.ts","../../node_modules/.pnpm/@tanstack+react-query@5.100.14_react@19.2.6/node_modules/@tanstack/react-query/build/modern/index.d.ts","./src/app/providers.tsx","../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.6/node_modules/lucide-react/dist/lucide-react.d.ts","./src/components/wallet-button.tsx","./src/components/navbar.tsx","./src/app/layout.tsx","./src/app/page.tsx","./src/app/status/page.tsx","./src/app/verify/page.tsx","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/container/Surface.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/container/Layer.d.ts","../../node_modules/.pnpm/@types+d3-time@3.0.4/node_modules/@types/d3-time/index.d.ts","../../node_modules/.pnpm/@types+d3-scale@4.0.9/node_modules/@types/d3-scale/index.d.ts","../../node_modules/.pnpm/victory-vendor@36.9.2/node_modules/victory-vendor/d3-scale.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/XAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/YAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/types.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/DefaultLegendContent.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/payload/getUniqPayload.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Legend.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/DefaultTooltipContent.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Tooltip.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/ResponsiveContainer.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Cell.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Text.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Label.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/LabelList.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/component/Customized.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Sector.d.ts","../../node_modules/.pnpm/@types+d3-path@3.1.1/node_modules/@types/d3-path/index.d.ts","../../node_modules/.pnpm/@types+d3-shape@3.1.8/node_modules/@types/d3-shape/index.d.ts","../../node_modules/.pnpm/victory-vendor@36.9.2/node_modules/victory-vendor/d3-shape.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Curve.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Rectangle.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Polygon.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Dot.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Cross.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Symbols.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/PolarGrid.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/PolarRadiusAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/PolarAngleAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/Pie.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/Radar.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/polar/RadialBar.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/Brush.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/IfOverflowMatches.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/ReferenceLine.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/ReferenceDot.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/ReferenceArea.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/CartesianAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/CartesianGrid.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/Line.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/Area.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/BarUtils.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/Bar.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/ZAxis.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/ErrorBar.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/cartesian/Scatter.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/getLegendProps.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/ChartUtils.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/AccessibilityManager.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/types.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/generateCategoricalChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/LineChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/BarChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/PieChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/Treemap.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/Sankey.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/RadarChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/ScatterChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/AreaChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/RadialBarChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/ComposedChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/SunburstChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/shape/Trapezoid.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/numberAxis/Funnel.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/chart/FunnelChart.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/util/Global.d.ts","../../node_modules/.pnpm/recharts@2.15.4_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/recharts/types/index.d.ts","./src/components/charts.tsx"],"fileIdsList":[[76,125,142,143,458],[76,125,142,143,448,449],[76,125,142,143,448],[76,125,142,143,444,503],[76,125,142,143,444,503,518,583,584],[76,125,142,143,448,611,616,619],[76,125,142,143,424,617],[62,76,125,142,143,608,615],[76,125,142,143],[62,76,125,142,143,617],[76,125,142,143,410,693],[76,125,142,143,424,617,618],[76,125,142,143,608],[76,125,130,142,143],[76,125,142,143,582],[76,125,142,143,501,502],[62,76,125,142,143,606,607],[76,125,142,143,594],[76,125,142,143,594,599],[76,125,142,143,599,600,601,602],[76,125,142,143,591,594],[76,125,142,143,595,596,597,598],[76,125,142,143,591,603],[76,125,142,143,604,605],[76,125,142,143,591],[76,125,142,143,588,589,590],[76,125,142,143,588,591],[76,125,142,143,453],[76,125,142,143,451,452,455,456,457],[76,125,142,143,444,451,454],[76,125,142,143,399,451],[76,125,142,143,448,451,452,455],[76,125,142,143,452,453,455],[76,125,142,143,444],[76,125,142,143,444,448,451,455],[76,125,142,143,521],[76,125,142,143,175,520],[76,125,142,143,519],[76,125,142,143,521,567,573,574,576],[76,125,142,143,521,573],[76,125,142,143,573,576,577],[76,125,142,143,573,574,576,577,578,579,580],[76,125,142,143,567,572,577],[76,125,142,143,521,575],[76,125,142,143,523],[76,125,142,143,523,524,525,526,527],[76,125,142,143,522],[76,125,142,143,532,533],[76,125,142,143,532],[76,125,142,143,521,540,546],[76,125,142,143,522,540],[76,125,142,143,540,546],[76,125,142,143,546],[76,125,142,143,564],[76,125,142,143,522,540,545,562,565],[76,125,142,143,521,522,545,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561],[76,125,142,143,521,522,535,536,537,538,539],[76,125,142,143,521,522,540,546],[76,125,142,143,521,522],[76,125,142,143,522,537],[76,125,142,143,538],[76,125,142,143,563],[76,125,142,143,521,528,529,530,531,534,543,544,566,572,581],[76,125,142,143,567,568,569,570,571],[76,125,142,143,567],[76,125,142,143,521,567],[76,125,142,143,541,542],[76,125,142,143,521,540],[76,125,142,143,496],[76,125,142,143,491],[76,125,142,143,486,494,495],[76,125,142,143,486,490,494,495,496],[76,125,142,143,486,491,494,496,497,498,499],[76,125,142,143,485,494],[76,125,142,143,494],[76,125,142,143,489,494],[76,125,142,143,486,487,488,489,493,495],[76,125,142,143,486,489,491,492,494],[76,125,142,143,461],[76,125,142,143,461,462],[76,125,142,143,469,470,471,472],[76,125,142,143,468,469,470,471,472,473,474,475],[76,125,142,143,469,473],[76,125,142,143,469],[76,125,142,143,468,469,470,473],[76,125,142,143,467,468],[76,125,142,143,476,477,478,479,481],[76,125,142,143,465,466,477,478,480],[76,125,142,143,480],[76,125,142,143,465,479,480,481],[76,125,142,143,477],[76,125,142,143,476],[76,125,142,143,483],[76,125,142,143,463,464,482,484,500],[76,125,142,143,612],[62,76,125,142,143,292,613],[76,125,142,143,614],[76,125,142,143,626],[76,125,142,143,644],[76,122,123,125,142,143],[76,124,125,142,143],[125,142,143],[76,125,130,142,143,160],[76,125,126,131,136,142,143,145,157,168],[76,125,126,127,136,142,143,145],[71,72,73,76,125,142,143],[76,125,128,142,143,169],[76,125,129,130,137,142,143,146],[76,125,130,142,143,157,165],[76,125,131,133,136,142,143,145],[76,124,125,132,142,143],[76,125,133,134,142,143],[76,125,135,136,142,143],[76,124,125,136,142,143],[76,125,136,137,138,142,143,157,168],[76,125,136,137,138,142,143,152,157,160],[76,118,125,133,136,139,142,143,145,157,168],[76,125,136,137,139,140,142,143,145,157,165,168],[76,125,139,141,142,143,157,165,168],[74,75,76,77,78,79,80,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174],[76,125,136,142,143],[76,125,142,143,144,168],[76,125,133,136,142,143,145,157],[76,125,142,143,146],[76,125,142,143,147],[76,124,125,142,143,148],[76,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174],[76,125,142,143,150],[76,125,142,143,151],[76,125,136,142,143,152,153],[76,125,142,143,152,154,169,171],[76,125,137,142,143],[76,125,136,142,143,157,158,160],[76,125,142,143,159,160],[76,125,142,143,157,158],[76,125,142,143,160],[76,125,142,143,161],[76,122,125,142,143,157,162,168],[76,125,136,142,143,163,164],[76,125,142,143,163,164],[76,125,130,142,143,145,157,165],[76,125,142,143,166],[76,125,142,143,145,167],[76,125,139,142,143,151,168],[76,125,130,142,143,169],[76,125,142,143,157,170],[76,125,142,143,144,171],[76,125,142,143,172],[76,118,125,142,143],[76,118,125,136,138,142,143,148,157,160,168,170,171,173],[76,125,142,143,157,174],[62,66,76,125,142,143,176,177,178,180,394,441],[62,66,76,125,142,143,176,177,178,179,394,441],[62,66,76,125,142,143,176,177,179,180,394,441],[62,66,76,125,142,143,177,178,179,180,394,441],[62,66,76,125,142,143,176,178,179,180,394,441],[60,61,76,125,142,143],[62,76,125,142,143],[68,76,125,142,143],[76,125,142,143,398],[76,125,142,143,400,401,402,403],[76,125,142,143,405],[76,125,142,143,184,197,198,199,201,358],[76,125,142,143,184,188,190,191,192,193,347,358,360],[76,125,142,143,358],[76,125,142,143,198,214,291,338,354],[76,125,142,143,184],[76,125,142,143,378],[76,125,142,143,358,360,377],[76,125,142,143,277,291,319,446],[76,125,142,143,284,301,338,353],[76,125,142,143,239],[76,125,142,143,342],[76,125,142,143,341,342,343],[76,125,142,143,341],[70,76,125,139,142,143,181,184,191,194,195,196,198,202,270,275,321,329,339,349,358,394],[76,125,142,143,184,200,228,273,358,374,375,446],[76,125,142,143,200,446],[76,125,142,143,273,274,275,358,446],[76,125,142,143,446],[76,125,142,143,184,200,201,446],[76,125,142,143,194,340,346],[76,125,142,143,151,292,354],[76,125,142,143,292,354],[62,76,125,142,143,292],[62,76,125,142,143,271,292,293],[76,125,142,143,219,237,354,430],[76,125,142,143,335,425,426,427,428,429],[76,125,142,143,334],[76,125,142,143,334,335],[76,125,142,143,192,216,217,271],[76,125,142,143,218,219,271],[76,125,142,143,271],[62,76,125,142,143,185,419],[62,76,125,142,143,168],[62,76,125,142,143,200,226],[62,76,125,142,143,200],[76,125,142,143,224,229],[62,76,125,142,143,225,397],[76,125,142,143,609],[62,66,76,125,139,142,143,175,176,177,178,179,180,394,439,440],[76,125,137,139,142,143,188,214,242,260,271,344,358,359,446],[76,125,142,143,329,345],[76,125,142,143,394],[76,125,142,143,183],[76,125,142,143,151,277,289,310,312,353,354],[76,125,142,143,151,277,289,309,310,311,353,354],[76,125,142,143,303,304,305,306,307,308],[76,125,142,143,305],[76,125,142,143,309],[62,76,125,142,143,225,292,397],[62,76,125,142,143,292,395,397],[62,76,125,142,143,292,397],[76,125,142,143,260,350],[76,125,142,143,350],[76,125,139,142,143,359,397],[76,125,142,143,297],[76,124,125,142,143,296],[76,125,142,143,210,211,213,243,271,284,285,286,288,321,353,356,359],[76,125,142,143,287],[76,125,142,143,211,219,271],[76,125,142,143,284,353],[76,125,142,143,284,293,294,295,297,298,299,300,301,302,313,314,315,316,317,318,353,354,446],[76,125,142,143,282],[76,125,139,142,143,151,188,209,211,213,214,215,219,247,260,269,270,321,349,358,359,360,394,446],[76,125,142,143,353],[76,124,125,142,143,198,213,270,286,301,349,351,352,359],[76,125,142,143,284],[76,124,125,142,143,209,243,263,278,279,280,281,282,283],[76,125,139,142,143,263,264,278,359,360],[76,125,142,143,198,260,270,271,286,349,353,359],[76,125,139,142,143,358,360],[76,125,139,142,143,157,356,359,360],[76,125,139,142,143,151,168,181,188,200,210,211,213,214,215,220,242,243,244,246,247,250,251,253,256,257,258,259,271,348,349,354,356,358,359,360],[76,125,139,142,143,157],[76,125,142,143,184,185,186,188,195,356,357,394,397,446],[76,125,139,142,143,157,168,204,376,378,379,380,446],[76,125,142,143,151,168,181,204,214,243,244,251,260,268,271,349,354,356,361,362,368,374,390,391],[76,125,142,143,194,195,270,329,340,349,358],[76,125,139,142,143,168,185,243,356,358,366],[76,125,142,143,276],[76,125,139,142,143,387,388,389],[76,125,142,143,356,358],[76,125,142,143,188,213,243,348,397],[76,125,139,142,143,151,251,260,356,362,368,370,374,390,393],[76,125,139,142,143,194,329,374,383],[76,125,142,143,184,220,348,358,385],[76,125,139,142,143,200,220,358,369,370,381,382,384,386],[70,76,125,142,143,211,212,213,394,397],[76,125,139,142,143,151,168,188,194,202,210,214,215,243,244,246,247,259,260,268,271,329,348,349,354,355,356,361,362,363,365,367,397],[76,125,139,142,143,157,194,356,368,387,392],[76,125,142,143,324,325,326,327,328],[76,125,142,143,250,252],[76,125,142,143,254],[76,125,142,143,252],[76,125,142,143,254,255],[76,125,139,142,143,188,209,359],[62,76,125,139,142,143,151,183,185,188,210,211,213,214,215,241,356,360,394,397],[76,125,139,142,143,151,168,187,192,243,355,359],[76,125,142,143,278],[76,125,142,143,279],[76,125,142,143,280],[76,125,142,143,203,207],[76,125,139,142,143,188,203,210],[76,125,142,143,206,207],[76,125,142,143,208],[76,125,142,143,203,204],[76,125,142,143,203,221],[76,125,142,143,203],[76,125,142,143,249,250,355],[76,125,142,143,248],[76,125,142,143,204,354,355],[76,125,142,143,245,355],[76,125,142,143,204,354],[76,125,142,143,321],[76,125,142,143,205,210,212,243,271,277,286,289,290,320,356,359],[76,125,142,143,219,230,233,234,235,236,237],[76,125,142,143,337],[76,125,142,143,198,212,213,264,271,284,297,301,330,331,332,333,335,336,339,348,353,358],[76,125,142,143,219],[76,125,142,143,241],[76,125,139,142,143,210,212,222,238,240,242,356,394,397],[76,125,142,143,219,230,231,232,233,234,235,236,237,395],[76,125,142,143,204],[76,125,142,143,264,265,268,349],[76,125,139,142,143,250,358],[76,125,139,142,143],[76,125,142,143,263,284],[76,125,142,143,262],[76,125,142,143,259,264],[76,125,142,143,261,263,358],[76,125,139,142,143,187,264,265,266,267,358,359],[62,76,125,142,143,216,218,271],[76,125,142,143,272],[62,76,125,142,143,185],[62,76,125,142,143,354],[62,70,76,125,142,143,213,215,394,397],[76,125,142,143,185,419,420],[62,76,125,142,143,229],[62,76,125,142,143,151,168,183,223,225,227,228,397],[76,125,142,143,200,354,359],[76,125,142,143,354,364],[62,76,125,137,139,142,143,151,183,229,273,394,395,396],[62,76,125,142,143,176,177,178,179,180,394,441],[62,63,64,65,66,76,125,142,143],[76,125,142,143,371,372,373],[76,125,142,143,371],[62,66,76,125,139,141,142,143,151,175,176,177,178,179,180,181,183,247,309,360,393,397,441],[76,125,142,143,407],[76,125,142,143,409],[76,125,142,143,411],[76,125,142,143,610],[76,125,142,143,413],[76,125,142,143,415,416,417],[76,125,142,143,421],[67,69,76,125,142,143,399,404,406,408,410,412,414,418,422,424,432,433,435,444,445,446,447],[76,125,142,143,423],[76,125,142,143,431],[76,125,142,143,225],[76,125,142,143,434],[76,124,125,142,143,264,265,266,268,300,354,436,437,438,441,442,443],[76,125,142,143,175],[76,125,142,143,592,593],[62,76,125,142,143,629,630,631,647,650],[62,76,125,142,143,629,630,631,640,648,668],[62,76,125,142,143,628,631],[62,76,125,142,143,631],[62,76,125,142,143,629,630,631],[62,76,125,142,143,629,630,631,666,669,672],[62,76,125,142,143,629,630,631,640,647,650],[62,76,125,142,143,629,630,631,640,648,660],[62,76,125,142,143,629,630,631,640,650,660],[62,76,125,142,143,629,630,631,640,660],[62,76,125,142,143,629,630,631,635,641,647,652,670,671],[76,125,142,143,631],[62,76,125,142,143,631,675,676,677],[62,76,125,142,143,631,648],[62,76,125,142,143,631,674,675,676],[62,76,125,142,143,631,674],[62,76,125,142,143,631,640],[62,76,125,142,143,631,632,633],[62,76,125,142,143,631,633,635],[76,125,142,143,624,625,629,630,631,632,634,635,636,637,638,639,640,641,642,643,647,648,649,650,651,652,653,654,655,656,657,658,659,661,662,663,664,665,666,667,669,670,671,672,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692],[62,76,125,142,143,631,689],[62,76,125,142,143,631,643],[62,76,125,142,143,631,650,654,655],[62,76,125,142,143,631,641,643],[62,76,125,142,143,631,646],[62,76,125,142,143,631,669],[62,76,125,142,143,631,646,673],[62,76,125,142,143,634,674],[62,76,125,142,143,628,629,630],[76,125,142,143,157,175],[76,90,94,125,142,143,168],[76,90,125,142,143,157,168],[76,85,125,142,143],[76,87,90,125,142,143,165,168],[76,125,142,143,145,165],[76,85,125,142,143,175],[76,87,90,125,142,143,145,168],[76,82,83,86,89,125,136,142,143,157,168],[76,90,97,125,142,143],[76,82,88,125,142,143],[76,90,111,112,125,142,143],[76,86,90,125,142,143,160,168,175],[76,111,125,142,143,175],[76,84,85,125,142,143,175],[76,90,125,142,143],[76,84,85,86,87,88,89,90,91,92,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,112,113,114,115,116,117,125,142,143],[76,90,105,125,142,143],[76,90,97,98,125,142,143],[76,88,90,98,99,125,142,143],[76,89,125,142,143],[76,82,85,90,125,142,143],[76,90,94,98,99,125,142,143],[76,94,125,142,143],[76,88,90,93,125,142,143,168],[76,82,87,90,97,125,142,143],[76,125,142,143,157],[76,85,90,111,125,142,143,173,175],[76,125,142,143,627],[76,125,142,143,645],[76,125,142,143,517],[76,125,142,143,505,506,507],[76,125,142,143,508,509],[76,125,142,143,505,506,508,510,511,516],[76,125,142,143,506,508],[76,125,142,143,516],[76,125,142,143,508],[76,125,142,143,505,506,508,511,512,513,514,515]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"bd7dee3446a5b94651d58000ddfda40296f073e9372891f65003a524b4620697","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"73b5fa37db36eeac90c4d752e39586f1b57187400c4f5280fd05f16437287a45","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"3da0083607976261730c44908eab1b6262f727747ef3230a65ecd0153d9e8639","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"dd721e5707f241e4ef4ab36570d9e2a79f66aad63a339e3cbdbac7d9164d2431","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"3849a7f92d0e11b785f6ae7bedb25d9aad8d1234b3f1cf530a4e7404be26dd0a","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"f040772329d757ecd38479991101ef7bc9bf8d8f4dd8ee5d96fe00aa264f2a2b","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"57e47d02e88abef89d214cdf52b478104dc17997015746e288cbb580beaef266","impliedFormat":1},{"version":"04a2d0bd8166f057cc980608bd5898bfc91198636af3c1eb6cb4eb5e8652fbea","impliedFormat":1},{"version":"376c21ad92ca004531807ea4498f90a740fd04598b45a19335a865408180eddd","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","impliedFormat":1},{"version":"48d37b90a04e753a925228f50304d02c4f95d57bf682f8bb688621c3cd9d32ec","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"b68c4ed987ef5693d3dccd85222d60769463aca404f2ffca1c4c42781dce388e","impliedFormat":1},{"version":"cfb5b5d514eb4ad0ee25f313b197f3baa493eee31f27613facd71efb68206720","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"9715fe982fccf375c88ac4d3cc8f6a126a7b7596be8d60190a0c7d22b45b4be4","impliedFormat":1},{"version":"1fe24e25a00c7dd689cb8c0fb4f1048b4a6d1c50f76aaca2ca5c6cdb44e01442","impliedFormat":1},{"version":"672f293c53a07b8c1c1940797cd5c7984482a0df3dd9c1f14aaee8d3474c2d83","impliedFormat":1},{"version":"0a66cb2511fa8e3e0e6ba9c09923f664a0a00896f486e6f09fc11ff806a12b0c","impliedFormat":1},{"version":"d703f98676a44f90d63b3ffc791faac42c2af0dd2b4a312f4afdb5db471df3de","impliedFormat":1},{"version":"0cfe1d0b90d24f5c105db5a2117192d082f7d048801d22a9ea5c62fae07b80a0","impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"414cc05e215b7fc5a4a6ece431985e05e03762c8eb5bf1e0972d477f97832956","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"5c2e5ca7d53236bbf483a81ae283e2695e291fe69490cd139b33fa9e71838a69","impliedFormat":1},{"version":"a73bee51e3820392023252c36348e62dd72e6bae30a345166e9c78360f1aba7e","impliedFormat":1},{"version":"6ea68b3b7d342d1716cc4293813410d3f09ff1d1ca4be14c42e6d51e810962e1","impliedFormat":1},{"version":"c319e82ac16a5a5da9e28dfdefdad72cebb5e1e67cbdcc63cce8ae86be1e454f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a020158a317c07774393974d26723af551e569f1ba4d6524e8e245f10e11b976","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"8bba776476c48b0e319d243f353190f24096057acede3c2f620fee17ff885dba","impliedFormat":1},{"version":"a3abe92070fbd33714bd837806030b39cfb1f8283a98c7c1f55fffeea388809e","impliedFormat":1},{"version":"ceb6696b98a72f2dae802260c5b0940ea338de65edd372ff9e13ab0a410c3a88","impliedFormat":1},{"version":"2cd914e04d403bdc7263074c63168335d44ce9367e8a74f6896c77d4d26a1038","impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"3bc8605900fd1668f6d93ce8e14386478b6caa6fda41be633ee0fe4d0c716e62","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"9f31420a5040dbfb49ab94bcaaa5103a9a464e607cabe288958f53303f1da32e","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"f11d0dcaa4a1cba6d6513b04ceb31a262f223f56e18b289c0ba3133b4d3cd9a6","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"56013416784a6b754f3855f8f2bf6ce132320679b8a435389aca0361bce4df6b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"9c066f3b46cf016e5d072b464821c5b21cc9adcc44743de0f6c75e2509a357ab","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c51641ab4bfa31b7a50a0ca37edff67f56fab3149881024345b13f2b48b7d2de","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"52abbd5035a97ebfb4240ec8ade2741229a7c26450c84eb73490dc5ea048b911","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"4360ad4de54de2d5c642c4375d5eab0e7fe94ebe8adca907e6c186bbef75a54d","impliedFormat":1},{"version":"c338dff3233675f87a3869417aaea8b8bf590505106d38907dc1d0144f6402ef","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"9c9cae45dc94c2192c7d25f80649414fa13c425d0399a2c7cb2b979e4e50af42","impliedFormat":1},{"version":"068f063c2420b20f8845afadb38a14c640aed6bb01063df224edb24af92b4550","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"b8719d4483ebef35e9cb67cd5677b7e0103cf2ed8973df6aba6fdd02896ddc6e","impliedFormat":1},{"version":"643672ce383e1c58ea665a92c5481f8441edbd3e91db36e535abccbc9035adeb","impliedFormat":1},{"version":"6dd9bcf10678b889842d467706836a0ab42e6c58711e33918ed127073807ee65","impliedFormat":1},{"version":"8fa022ea514ce0ea78ac9b7092a9f97f08ead20c839c779891019e110fce8307","impliedFormat":1},{"version":"c93235337600b786fd7d0ff9c71a00f37ca65c4d63e5d695fc75153be2690f09","impliedFormat":1},{"version":"10179c817a384983f6925f778a2dac2c9427817f7d79e27d3e9b1c8d0564f1f4","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"c0a666b005521f52e2db0b685d659d7ee9b0b60bc0d347dfc5e826c7957bdb83","impliedFormat":1},{"version":"807d38d00ce6ab9395380c0f64e52f2f158cc804ac22745d8f05f0efdec87c33","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"d05fb434f4ba073aed74b6c62eff1723c835de2a963dbb091e000a2decb5a691","impliedFormat":1},{"version":"10e6166be454ddb8c81000019ce1069b476b478c316e7c25965a91904ec5c1e3","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"703989a003790524b4e34a1758941d05c121d5d352bccca55a5cfb0c76bca592","impliedFormat":1},{"version":"a58abf1f5c8feb335475097abeddd32fd71c4dc2065a3d28cf15cacabad9654a","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"671aeae7130038566a8d00affeb1b3e3b131edf93cbcfff6f55ed68f1ca4c1b3","impliedFormat":1},{"version":"f0f05149debcf31b3a717ce8dd16e0323a789905cb9e27239167b604153b8885","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"955c69dde189d5f47a886ed454ff50c69d4d8aaec3a454c9ab9c3551db727861","impliedFormat":1},{"version":"cec8b16ff98600e4f6777d1e1d4ddf815a5556a9c59bc08cc16db4fd4ae2cf00","impliedFormat":1},{"version":"9e21f8e2c0cfea713a4a372f284b60089c0841eb90bf3610539d89dbcd12d65a","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"c226288bda11cee97850f0149cc4ff5a244d42ed3f5a9f6e9b02f1162bf1e3f4","impliedFormat":1},{"version":"210a4ec6fd58f6c0358e68f69501a74aef547c82deb920c1dec7fa04f737915a","impliedFormat":1},{"version":"8eea4cc42d04d26bcbcaf209366956e9f7abaf56b0601c101016bb773730c5fe","impliedFormat":1},{"version":"f5319e38724c54dff74ee734950926a745c203dcce00bb0343cb08fbb2f6b546","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"12b8dfed70961bea1861e5d39e433580e71323abb5d33da6605182ec569db584","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"7e560f533aaf88cf9d3b427dcf6c112dd3f2ee26d610e2587583b6c354c753db","impliedFormat":1},{"version":"71e0082342008e4dfb43202df85ea0986ef8e003c921a1e49999d0234a3019da","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"e71e103fb212e015394def7f1379706fce637fec9f91aa88410a73b7c5cbd4e3","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"794998dc1c5a19ce77a75086fe829fb9c92f2fd07b5631c7d5e0d04fd9bc540c","impliedFormat":1},{"version":"2b0b12d0ee52373b1e7b09226eae8fbf6a2043916b7c19e2c39b15243f32bde2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"bdc5fd605a6d315ded648abf2c691a22d0b0c774b78c15512c40ddf138e51950","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"6cd4b0986c638d92f7204d1407b1cb3e0a79d7a2d23b0f141c1a0829540ce7ef","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"d58265e159fc3cb30aa8878ba5e986a314b1759c824ff66d777b9fe42117231a","impliedFormat":1},{"version":"ff8fccaae640b0bb364340216dcc7423e55b6bb182ca2334837fee38636ad32e","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"59ee66cf96b093b18c90a8f6dbb3f0e3b65c758fba7b8b980af9f2726c32c1a2","impliedFormat":1},{"version":"c590195790d7fa35b4abed577a605d283b8336b9e01fa9bf4ae4be49855940f9","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"026a43d8239b8f12d2fc4fa5a7acbc2ad06dd989d8c71286d791d9f57ca22b78","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"14cf3683955f914b4695e92c93aae5f3fe1e60f3321d712605164bfe53b34334","impliedFormat":1},{"version":"12f0fb50e28b9d48fe5b7580580efe7cc0bd38e4b8c02d21c175aa9a4fd839b0","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"7cd657e359eac7829db5f02c856993e8945ffccc71999cdfb4ab3bf801a1bbc6","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"29c2aa0712786a4a504fce3acd50928f086027276f7490965cb467d2ce638bae","impliedFormat":1},{"version":"f14e63395b54caecc486f00a39953ab00b7e4d428a4e2c38325154b08eb5dcc2","impliedFormat":1},{"version":"e749bbd37dadf82c9833278780527c717226e1e2c9bc7b2576c8ec1c40ec5647","impliedFormat":1},{"version":"46e4e179b295f08d0bd0176fe44cf6c89558c9091d3cb3894f9eaaa42ea1add1","impliedFormat":1},{"version":"08f52a9edaabeda3b2ea19a54730174861ceed637c5ca1c1b0c39459fdc0853e","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"29164fb428c851bc35b632761daad3ae075993a0bf9c43e9e3bc6468b32d9aa5","impliedFormat":1},{"version":"3c01539405051bffccacffd617254c8d0f665cdce00ec568c6f66ccb712b734f","impliedFormat":1},{"version":"ef9021bdfe54f4df005d0b81170bd2da9bfd86ef552cde2a049ba85c9649658f","impliedFormat":1},{"version":"17a1a0d1c492d73017c6e9a8feb79e9c8a2d41ef08b0fe51debc093a0b2e9459","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"96e1caae9b78cde35c62fee46c1ec9fa5f12c16bc1e2ab08d48e5921e29a6958","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"9e0327857503a958348d9e8e9dd57ed155a1e6ec0071eb5eb946fe06ccdf7680","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","impliedFormat":1},{"version":"397f568f996f8ffcf12d9156342552b0da42f6571eadba6bce61c99e1651977d","impliedFormat":1},{"version":"e2fd426f3cbc5bbff7860378784037c8fa9c1644785eed83c47c902b99b6cda9","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"a52674bc98da7979607e0f44d4c015c59c1b1d264c83fc50ec79ff2cfea06723","impliedFormat":1},{"version":"bcca16e60015db8bbf6bd117e88c5f7269337aebb05fc2b0701ae658a458c9c3","impliedFormat":1},{"version":"5e1246644fab20200cdc7c66348f3c861772669e945f2888ef58b461b81e1cd8","impliedFormat":1},{"version":"eb39550e2485298d91099e8ab2a1f7b32777d9a5ba34e9028ea8df2e64891172","impliedFormat":1},{"version":"e108f38a04a607f9386d68a4c6f3fdae1b712960f11f6482c6f1769bab056c2e","impliedFormat":1},{"version":"a3128a84a9568762a2996df79717d92154d18dd894681fc0ab3a098fa7f8ee3b","affectsGlobalScope":true,"impliedFormat":1},{"version":"347791f3792f436950396dd6171d6450234358001ae7c94ca209f1406566ccbf","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"714d8ebb298c7acc9bd1f34bd479c57d12b73371078a0c5a1883a68b8f1b9389","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"51bf55bb6eb80f11b3aa59fb0a9571565a7ea304a19381f6da5630f4b2e206c4","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"02f8ef78d46c5b27f108dbb56709daa0aff625c20247abb0e6bb67cd73439f9f","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"6812502cc640de74782ce9121592ae3765deb1c5c8e795b179736b308dd65e90","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"b10bc147143031b250dc36815fd835543f67278245bf2d0a46dca765f215124e","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"1e4c6ac595b6d734c056ac285b9ee50d27a2c7afe7d15bd14ed16210e71593b0","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"3c7b3aecd652169787b3c512d8f274a3511c475f84dcd6cead164e40cad64480","impliedFormat":1},{"version":"9a01f12466488eccd8d9eafc8fecb9926c175a4bf4a8f73a07c3bcf8b3363282","impliedFormat":1},{"version":"b80f624162276f24a4ec78b8e86fbee80ca255938e12f8b58e7a8f1a6937120b","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"00b0f43b3770f66aa1e105327980c0ff17a868d0e5d9f5689f15f8d6bf4fb1f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"272a7e7dbe05e8aaba1662ef1a16bbd57975cc352648b24e7a61b7798f3a0ad7","affectsGlobalScope":true,"impliedFormat":1},{"version":"a1219ee18b9282b4c6a31f1f0bcc9255b425e99363268ba6752a932cf76662f0","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"879741880b6ab48da99dcc06dcc674d381c9826137b0496bf7bb368f302de9fc",{"version":"8b3676be3385ef2734caa9f1f8e1876d6b1e52dad637eb972295f49aa9364ec7","impliedFormat":1},{"version":"17821f1110ec84eccfb9a173f743084e150d2542e56e7729ba71e8a9b34e4c3a","impliedFormat":1},{"version":"82ff3379586e21539c6cb74c30f4295038b140e354db0c7e04f7131f32ed4dae","impliedFormat":1},{"version":"e81a87cf06b1a3b41d650b7f34e00c2b6014f2c046ade0e82e06d63000e56ae3","impliedFormat":1},{"version":"3b5286016da91a05cb4c39cd74f27f74518986eec98048ca1c253fdbef62dc05","impliedFormat":1},{"version":"e53c9efe4ff7aaec717d97845b96c74be687968ac59f5d5df4bb0b8ed46ff575","affectsGlobalScope":true,"impliedFormat":1},{"version":"e05bbbcb535eb4edfd8be2d8da09daffd2d0a1c4bff143fd8a0706702ff7847a","impliedFormat":1},{"version":"39d4b34221f63af96547fd2442fd09c2ec40f38c57faaf2ca7a1b8bef6e6cb65","impliedFormat":1},{"version":"3821ae43d5c40cdd6ad376464dff45aed3b4709b470ddce011278105f4e8efca","signature":"2792c0250ec7248836a611f060e41f9faf5173307add741b30f349016b9baa39"},"128aa98acce609a3ac9249a791b2c20dd37933f11929b46e16e1e74c9b81865d",{"version":"5a6237f90ea7b312ce8e331ad5ab88661ca01c64aad1fdfa4d8a9f2f64caf57d","impliedFormat":1},{"version":"67bd7f459d684b496170f8c6d48d309a4c94bc726b127e7c1704234c0d5c5f46","impliedFormat":1},{"version":"a3628f430f8d502a5c026a0c932a5c41e6361d8e0248287872cd8999bc534399","impliedFormat":1},{"version":"5244034d59b188756ec9bc13ce72e04c89cbc9cbd2b6c6dc3b1412c7cc0e1957","impliedFormat":99},{"version":"0deff63aaf571504405cb4be9de0bf0b445056d063d27d8ce9ab5a85da09d8c8","impliedFormat":1},{"version":"2b6c6039f4d2f656904d66f82231488f4852f861d27147884895097f74e3e812","impliedFormat":1},{"version":"cc6c527d304da87b8873bcf1cf9a47a12fe1630abaf5cbb2c60cbabd8e85e4c2","impliedFormat":1},{"version":"8d14d903616d2aad418dd902e2a7be61e8ba7b80b9ba33b2b1257bb79e961ca2","impliedFormat":1},{"version":"941959cd493fe9e8780f8a704791c83ffff2499447622f7ee63acc7bf08be0f9","impliedFormat":1},{"version":"1fcc4bb6d083b31e1587711ab5a8b0467b52a125f9735467774285bc8cc127e6","impliedFormat":1},{"version":"c938244bbaf0fe8eedb23df3f0da99dc945635043313cfeb0044eead923da54f","impliedFormat":1},{"version":"0e085cc503ad1332728d56244e9f7a603404beca17c0c5b2d815ed29e0727d4b","impliedFormat":1},{"version":"deda38d3245acb0404dd845dae172547c895c99c442082f176071cbb40d092f3","impliedFormat":1},{"version":"2776f7230a2ae50a27bc595893d0fcd8943869a8a3aaff99a2e3f86aafe54bdb","impliedFormat":1},{"version":"585951f20abc465c5acb3674fe5bad232c299f00d073d90c8cb1a416c807d41e","impliedFormat":1},{"version":"e6f3d02d69394dae0771c088b3c0b982cf15b6a91678c59f1d5fbd7c5e6ad8f8","impliedFormat":1},{"version":"ef182902b33ac9b9ad90c163b313722d2bc9d8c2cfefeb418b3205d70504a486","impliedFormat":1},{"version":"9e186460be8b3215058dbbb969c1726412ab117a08ab6ac6666835813dc36596","impliedFormat":1},{"version":"95bdd836ed77c23e530fcd3a0823df8fd611035590dfd8d38ee164c56f2bd2c4","impliedFormat":1},{"version":"059a18ce7a1e33c1b4b7796f72549bdd98d27f43f82f27fb336392281253d236","impliedFormat":1},{"version":"e689e94b2f63be1124a32f5dfe4a6951aa0d39efb2bae9a635539780ed6f8c82","impliedFormat":1},{"version":"c0e42e780d502d530ce67e30d09a3b81c5d37d500c1f7ef04f4bd806f648b96a","impliedFormat":1},{"version":"447b6a80636a59c918ed18af1019de1efa94109a086e8fd8f3d20eb9b9a6937b","impliedFormat":99},{"version":"54c77a931485f321c64d17523b3a9cb111c31d07d4ae735a10a2dc9b25e78ebd","impliedFormat":99},{"version":"05c9c065eadecdce0ee370455e3c36674bfb08673f1a268a398002a0d2d801b7","impliedFormat":1},{"version":"b0df6363b53f0b84a98f6e8ebaebd773bcc46f77d4b61af4574c7508c40f6c04","impliedFormat":1},{"version":"0eae63800777384563d5727e572982c220d47acf736dcdb569a2749a32378f19","impliedFormat":1},{"version":"9bf41a89bd0bbd4f8a23a7925d04f99267cb84a5a5b239185f3320edea329b9c","impliedFormat":1},{"version":"c8699f2b983bbc3117260c84d2f9f11c83eb2b396ea881a69d4cf10ac73a339f","impliedFormat":1},{"version":"b688a3daef72eae05635460146810781dab458476b855c4366371e17f1a0b546","impliedFormat":1},{"version":"eeb074610693bf33314fe67fa4b82f6bd26cdd54851d5aa79b98adc3a9090fea","impliedFormat":1},{"version":"964de3d129316ff79eccce67973270c01d0ed9c61947535ff8f35509a46fe536","impliedFormat":1},{"version":"e444a4edd02caac4c129adb1033df87601f443a38b3b505ab368da6b9c5c5560","impliedFormat":1},{"version":"626f96ecbe3b0ca52fe06b6d16b03520f51dfbc479ad1c2756f5f2eabfd78f48","impliedFormat":1},{"version":"170decb46fc69c7e82174fe44f308115628d033f11ee51c9d554f5ca735353f3","impliedFormat":1},{"version":"edfc95c78a5568c8d0db613579f93c1ed5766e82ed75c6f06d6a44fbbb492d14","impliedFormat":1},{"version":"4ae9b50481136302de9c77668621ed3a0b34998f3e091ca3701426f4fe369c8a","impliedFormat":1},{"version":"9ba9ecc57d2f52b3ed3ac229636ee9a36e92e18b80eeae11ffb546c12e56d5e5","impliedFormat":1},{"version":"17644c49b3a6c1907a292b491472a609f342d069c660043b96e398574e34b6a7","impliedFormat":1},{"version":"d182d419bb30a1408784ed95fbabd973dde7517641e04525f0ce761df5d193a5","impliedFormat":1},{"version":"1bc7e6c2be453958a63abf49f3021e2e68de535fbb293f2eb9d616ed1e6caa47","impliedFormat":99},"c604d435ff56ddfcee32de284c88a5222a18aa7031b3a211b2e7b3f06090d454","496a1764c54bce896f8c7bdf15ab2c0a7ef949bbda96c108fa00aa6e794f1af5","4f0f24030ad08f17ef37bf0125748ff535e5b46b285ad28bc702dc7fac4ba95f",{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"a8500212680a21ef505d17676bcb3ce8fc043acf1f230b0a34512754bdbfcb8f","impliedFormat":1},{"version":"c42517dab6a13046aee80ad244d43af602b4c3e4c0a7c657f6a5d3565d424d03","impliedFormat":1},{"version":"e163816a424de7261b104f6d61e6a13f9d680b24c31ca3749adb0b30608185fe","impliedFormat":1},{"version":"918db86ec6fd76d4d9717de8f35d1c06252712307cc7b4d14e809071b00a167c","impliedFormat":1},{"version":"80a05e22866592fa0fd321baf4c1703b1d04720120cb45910af418b2414ab85d","impliedFormat":1},{"version":"792d2ee8a11d51431649ed531e639c2c5f4bf1bb9dfe2baa37bccbe0e12ad974","impliedFormat":1},{"version":"e8e6a34de709beeb75adbd0b80bafd1b4b47ebbda77393b5d8cf8528f30fdc20","impliedFormat":1},{"version":"ff815d9df7b2f182e5096590d417373e9d254ed40abbd46ace1878154be99965","impliedFormat":1},{"version":"1bf95e49499a1b9c0e33129d7d65793b0d69944f685d763ae6dc46573cc02c78","impliedFormat":1},{"version":"f1c41c17b8a2f1dfef4132b3f0f230f31cb7fbe09a0f0b6a6cec7c1d0309415f","impliedFormat":1},{"version":"a77d2286d88634d570666efbcb2f38ad2136ba86c57f5b7ca435487115cabba6","impliedFormat":1},{"version":"ba15dae354dbcc5257fc4b5ab54a88ecea27dbc4741423b28277592856958624","impliedFormat":1},{"version":"0be0c7dd814a36bd9645b463721e8f09011503ecf2acf79847bd873f2530872a","impliedFormat":1},{"version":"5dd622ac1741517a9395c076299ed4cfcd1ed50a0006d505460f53d84d43a7ca","impliedFormat":1},{"version":"1ca2db7ba5f4c163ef9ee7a304b308774a44e793b77192e4c655e49eb5fb5c31","impliedFormat":1},{"version":"a6662111727d46c77cec7b60b3e01fa6d428ec0f42cc398c8654455dffaa3966","impliedFormat":1},{"version":"76b14ef0b138a5bc955d280b1b552096cf559611e17d8df209ec71b8097e3326","impliedFormat":1},{"version":"acca26a33b4c1be60e4e5724b39ebadba3b95249e38373ba05700aa3d02b0cd5","impliedFormat":1},{"version":"356c3a16a305e9ad0bf498290f66e1c041283a1a573962fc1d370f9ed07e54d2","impliedFormat":1},{"version":"ab106e6b157dcf391367028d878e54af3ad62fc45f7c8af2bebda86bc1bde81f","impliedFormat":1},{"version":"6cd413f09c103bc9cad3aff713ea1a1131dee97578b7939539424aed73e42dd5","impliedFormat":1},{"version":"06ccf7c4bf48ccb887c08c8623f38b349db53ddc7960c04aab015ff4b758a7ec","impliedFormat":1},{"version":"75b5781c566c1181aa0dde8bc3edfcaa838316c96f6348a03411a158039c7c06","impliedFormat":1},{"version":"fa3a4a30408c354ce3428de2c68e38248e0282ace689f654424a3ae2a9db6f6f","impliedFormat":1},{"version":"8a1de2430a8370c01fc2060ed6788d3cdc21a8760f5a3d7e528276b932083f7e","impliedFormat":1},{"version":"0733cf6e491804e70cecb90b5bc0137eb817f04c8878cbb36be16e551144f03b","impliedFormat":1},{"version":"494320a092fea318c0f57181f3fc1a6af5c849fd00916095abfcb9fef0130539","impliedFormat":1},{"version":"197df6fabe729c4c8a882b468ea744a09628ca8f6493107af55484b41ba6b865","impliedFormat":1},{"version":"fe11d862aeadcd634aa912e153d560a1b26d6ddd82b0dcbde15bd7a1dfa55b5e","impliedFormat":1},{"version":"f87454290a0c0f3ba28792ae7fbf21b87aa7d8b57d961bb0b0acf2069396e27b","impliedFormat":1},{"version":"20906b2bdba4d18ee73c9c9736542357e9f382f390058ee000974aee013c2c4d","impliedFormat":1},{"version":"def8de2c77d06c2baba27a3cf724ca883c4ddbe4d9c2dc6af0489cd773f876fe","impliedFormat":1},{"version":"7e410d0255174acc68e4934e356e61a52ab8335742ed2bcec4ccab309b60e126","impliedFormat":1},{"version":"c1380d70cded055fe22d0eee614776456969fce912f1c4644c63555a780221cc","impliedFormat":1},{"version":"6e932fa614ad161de8fee88612ddffe7b5992e9cb3297627047a8fd72080e2d7","impliedFormat":1},{"version":"a239383e316fb15338bde570322a911e8d11e803681bdb3971cb177fc54496fa","impliedFormat":1},{"version":"73faa9b53b5b65ecca3ddd775bdd9c7d3974efaa142f8af74c6eeac7e82f9c07","impliedFormat":1},{"version":"aadef9070d9cc62f1c3944955d7fd29e2a40319c9d445a30d7dfb21e6c625585","impliedFormat":1},{"version":"985df8ddd09e64b20843ab11f1b45efef300dda075d62f7cf363814d41b70f04","impliedFormat":1},{"version":"9272aabadb229fa8b2652f1600de129cca421fb9fc6d76c59a7460ad55bbaa6f","impliedFormat":1},{"version":"e7bb61b383a14a8249be8701b6acf237c5562d02cc851eb2af221372a49a445f","impliedFormat":1},{"version":"ce836100e4cfeb23baef54468a2136a77ef6b4c11e71d6bc49ee72613706bcf7","impliedFormat":1},{"version":"35ea881b25ee99c335310e8e1b3b83d699887aae8e82ac95bb12760ea1263d52","impliedFormat":1},{"version":"18af43f6f4a283c1c51553488413491651b58e7bfd797c0de107a69c09e035b6","impliedFormat":1},{"version":"cbc80bdb9acf0519990cee8cd1fd678e2c572d34632f455d188465135403edc8","impliedFormat":1},{"version":"17238d17ad306ce4212cdfd93d24f0031bc1d5a19d6890cb4eff1e736ddf3bcf","impliedFormat":1},{"version":"c0e9bb88e8cd057667e1ff8077ab9bb56ce3b7e48857d324c7c4d41558feae0a","impliedFormat":1},{"version":"ef8936e39e46a1a7dbc5764e1b925ac1ca4b01b300fc3b108a234df6ad001352","impliedFormat":1},{"version":"d2045964eae7d7258d1604efd2c7ca42971abed928e442703ae2e1ebb264dd68","impliedFormat":1},{"version":"251bf948c11ea4184c0f7f9d9bf4d6d7df264350e5a87775bab817ee31b814ef","impliedFormat":1},{"version":"7e0b06f6ea99ff158aa10dd8c63b57f21db9b8db1a145e561f9a0eda1f927d5d","impliedFormat":1},{"version":"a8ef327dc92e3b1348c19d2a3df74f1de3dd1f0fd2a2faced3bbd44786071533","impliedFormat":1},{"version":"0044c8f2b1ff84efec2d3624143cd2992594e1cd5bb07babffe2e04c263cafc6","impliedFormat":1},{"version":"c2500c66aa2eb3a05ddcde3678663f5171e9a45f6e75b3788e8b9a1be0e97300","impliedFormat":1},{"version":"b9ff95a132af56b2e65f20236eddf962a0d8d593c20293aa3b50cac43ccd4dc4","impliedFormat":1},{"version":"557c62f4b2100cd73583f2a69c90c91b5630f2f6655e8fdfd11a29d12996116d","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"83136d60a584b2e0950b78dbe256b7f8acebea796303c30984cc93e667dd6944","impliedFormat":1},{"version":"9b9aeb6bc0a4e29800fa8da31e6611810945aa13466530a7138a98a3e7fc944b","impliedFormat":1},{"version":"cce9f2ad37b05e5896488a3beb8985383242eaf0e56bdb33a08f890b4d8d36f1","impliedFormat":1},{"version":"a2b3277783055e791ef4bd84e565fc3c3914a196190a86253a8036f93663f484","impliedFormat":1},{"version":"87b7a66c67d4eca1bdc236e1408799d5a38c9395e3aff94d2cf414ab64a29643","impliedFormat":1},{"version":"d5e38bb337a8b9a6d8a0abbd1192976bf15c164f500bc1ca2ec7e5e5fad535be","impliedFormat":1},{"version":"86a691a2b26140c614b57e449a797a89170ff4b1a9da076457aed38b2ab72f6d","impliedFormat":1},"19fef07186edd07050ee0b611bf3c8537dc4d9f24fa1fb99c8852aca55dae614","1125512bb31e88289cc5edf66e7f0e38200d4d1d62d3d56e26d7f722e0afe3f3","0cb1112a122e8a0314903d3525338f4f6c81a130bad4cd1d85414477127ce171","1d445a1e739b97f7c450d7c3c5ba3c7edee7913e6be0f36ef3acd69ead4da2ce",{"version":"704364d4184624be1c255416b100926bfd600057ce3f69b93a21bcb0e6878c35","signature":"5598c4d4938086e1753ea990acf3bbe016daa2c65ea5b7b21f22e5bdf1b3a92e"},{"version":"ce7fecac3ad0e501e414919f279b2f22cdbb8627bb0bcbe9af7b71205d6da773","impliedFormat":99},{"version":"db938d610e8eefc1a551b5c807849367ace6464b1537be2d6a8a9bda8fb85046","impliedFormat":99},{"version":"f4284611e472e595c6ad93b70af68e2d17d53b43e9b69662abe8e40014cb9e9e","impliedFormat":99},{"version":"f705851d75eb35a2325dd34128399040fa1838af7ab03d3170631a1493d2fc15","impliedFormat":99},{"version":"71835c576d1e2d02e239e4d1e39b70db49f689abcf40284819953ec598031d88","impliedFormat":1},{"version":"23b7f91f08ffc61609ae0bb87e9a763e6538cff9e0859292a2fc37d1d120ca9b","impliedFormat":1},{"version":"8ae1be898b0b0dff66a04adaf66ed0285aac1eb70135c20d1310c8e1b966baa5","impliedFormat":1},{"version":"c1d4774aa4d733fc2a299f9041d984b8be1851c1e5ebc76d8c4b9d8434d34254","impliedFormat":99},{"version":"19332e67dbd35203aaf192dd1a558e3edb786a093bd1a477b9848e3ac9111c2b","impliedFormat":99},{"version":"13f968276a79594154d2bdff657629b446d769974a9145789b8bd588b1bcab1a","impliedFormat":99},{"version":"4c5856485b652b2fb3958224233e28259e8ff4fa158ff5d1d690d2003cb74035","impliedFormat":99},{"version":"18d25aeb37931986e2f6b7154ddb028b5ca1590ac83da798f45280918db923c9","impliedFormat":99},{"version":"f82571d6a0fe6637b78839346c708ce9f64c93b476a7a23df300700b8c4dada5","impliedFormat":99},{"version":"3ed01c901e95e27018bca70bc60aaacab67927d2a1b345a7f1f6e6fc9d3e4578","impliedFormat":99},{"version":"449e8d5b4adfdd6784dd6a86e1e13691ec3f0f1800d8bcc1692f61b16a23c250","impliedFormat":99},{"version":"9b32cc1809b3816f65d41ec57a798facd919928deff4c127fdfc571e899d01f2","impliedFormat":99},{"version":"ade60c90f0e2c4c3350116fae4c13c5f08d8e0171a859847f14bb1c586c0c21a","impliedFormat":99},{"version":"77636bd718e804e3e8010ef1ad3fe926fde85413173a7682feab872da9193723","impliedFormat":99},{"version":"d873742575f778d0191b79ab96a1154f34e692fcd4082f94ff5dad89a4daf792","impliedFormat":99},{"version":"4413da597a27f00306d5e219831a194d7df9630eac659a05426f8cf202567b9c","impliedFormat":99},{"version":"6ede51b344ea004ea29d4d2149e59aede92673a09c2119b5cf10295ed51bf35e","signature":"32840d1673eb6eae2125ccbc959df90cb28b1596013a6d1087181d7f5a3123be"},{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"e0df902c15945ef39f5ca04453ea781f99c05c13edf1db35d4d20dc259383c65","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"73c078fcbc0fa04ba70b1c3e5a3dea6a980d8765079cdbfb40f903eb8daa4319","impliedFormat":99},{"version":"5297e84d3de08bbe3c00f964d1c74f89cf101d59a4826b335654f44ff41529a8","impliedFormat":99},{"version":"355b33af59287683501f76cbf7d6a141544c5ff1ae5f5c0701a3f89cc38e5238","impliedFormat":99},{"version":"280a996092ab956e80dc7bb7497d472ca5c1be23a9c52ac771f5c750ede462b9","impliedFormat":99},{"version":"625d88e3e43fbe7e0ca0fa50b3bd8392b4bdbf38835a2b4da37a3fd1383cb563","signature":"963991fa5943adde556cea8ebb00bbc508e9de048eb361c5a8b9c59a88707119"},{"version":"40eb4eb274d7def754b3352c23328af96a69261fe5e99d20df41f1d5de8d0f5e","impliedFormat":1},{"version":"744401d41db85371bc933ad6b77143042fee5f4c25b134722a59f042a7c9c98d","signature":"81b316ce03e5ccdc8bb158212cf51862904a4473d787956bc696e3df562b6e3a"},{"version":"e269680f568b7fae3980f8554584503a03a982fb3799111f6ce82c118783a7d8","signature":"05f51d060eea4ecf4d575f3eb71efdda9e83314dcb830e2c3f63eb154745b738"},{"version":"780c6b39e171e0b68568e144c8ca91d45b569edb045458400a81118fd2a4f8f9","signature":"2dcc50c46dcccaadb05bc414e290fd1a75298fcadb47c77c1af84945ec8b6b01"},"5143cc10841a55471d583ba5f6d9bf5189e096d0d289d06892797e04e1657665","9be5051c26616c51f4bfee577e347718675c391ab65e393d80f2a5dc2b1c8437","a698951640a8f203c55d87c0f8b3cf8a7156f1670f174eff97ad37714f5908bc",{"version":"7e3373dde2bba74076250204bd2af3aa44225717435e46396ef076b1954d2729","impliedFormat":1},{"version":"1c3dfad66ff0ba98b41c98c6f41af096fc56e959150bc3f44b2141fb278082fd","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"eb9271b3c585ea9dc7b19b906a921bf93f30f22330408ffec6df6a22057f3296","impliedFormat":1},{"version":"0205ee059bd2c4e12dcadc8e2cbd0132e27aeba84082a632681bd6c6c61db710","impliedFormat":1},{"version":"a694d38afadc2f7c20a8b1d150c68ac44d1d6c0229195c4d52947a89980126bc","impliedFormat":1},{"version":"9f1e00eab512de990ba27afa8634ca07362192063315be1f8166bc3dcc7f0e0f","impliedFormat":1},{"version":"9674788d4c5fcbd55c938e6719177ac932c304c94e0906551cc57a7942d2b53b","impliedFormat":1},{"version":"86dac6ce3fcd0a069b67a1ac9abdbce28588ea547fd2b42d73c1a2b7841cf182","impliedFormat":1},{"version":"4d34fbeadba0009ed3a1a5e77c99a1feedec65d88c4d9640910ff905e4e679f7","impliedFormat":1},{"version":"9d90361f495ed7057462bcaa9ae8d8dbad441147c27716d53b3dfeaea5bb7fc8","impliedFormat":1},{"version":"8fcc5571404796a8fe56e5c4d05049acdeac9c7a72205ac15b35cb463916d614","impliedFormat":1},{"version":"a3b3a1712610260c7ab96e270aad82bd7b28a53e5776f25a9a538831057ff44c","impliedFormat":1},{"version":"33a2af54111b3888415e1d81a7a803d37fada1ed2f419c427413742de3948ff5","impliedFormat":1},{"version":"d5a4fca3b69f2f740e447efb9565eecdbbe4e13f170b74dd4a829c5c9a5b8ebf","impliedFormat":1},{"version":"56f1e1a0c56efce87b94501a354729d0a0898508197cb50ab3e18322eb822199","impliedFormat":1},{"version":"8960e8c1730aa7efb87fcf1c02886865229fdbf3a8120dd08bb2305d2241bd7e","impliedFormat":1},{"version":"27bf82d1d38ea76a590cbe56873846103958cae2b6f4023dc59dd8282b66a38a","impliedFormat":1},{"version":"0daaab2afb95d5e1b75f87f59ee26f85a5f8d3005a799ac48b38976b9b521e69","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"9b048390bcffe88c023a4cd742a720b41d4cd7df83bc9270e6f2339bf38de278","affectsGlobalScope":true,"impliedFormat":1},{"version":"c60b14c297cc569c648ddaea70bc1540903b7f4da416edd46687e88a543515a1","impliedFormat":1},{"version":"94a802503ca276212549e04e4c6b11c4c14f4fa78722f90f7f0682e8847af434","impliedFormat":1},{"version":"9c0217750253e3bf9c7e3821e51cff04551c00e63258d5e190cf8bd3181d5d4a","impliedFormat":1},{"version":"5c2e7f800b757863f3ddf1a98d7521b8da892a95c1b2eafb48d652a782891677","impliedFormat":1},{"version":"21317aac25f94069dbcaa54492c014574c7e4d680b3b99423510b51c4e36035f","impliedFormat":1},{"version":"c61d8275c35a76cb12c271b5fa8707bb46b1e5778a370fd6037c244c4df6a725","impliedFormat":1},{"version":"c7793cb5cd2bef461059ca340fbcd19d7ddac7ab3dcc6cd1c90432fca260a6ae","impliedFormat":1},{"version":"fd3bf6d545e796ebd31acc33c3b20255a5bc61d963787fc8473035ea1c09d870","impliedFormat":1},{"version":"c7af51101b509721c540c86bb5fc952094404d22e8a18ced30c38a79619916fa","impliedFormat":1},{"version":"59c8f7d68f79c6e3015f8aee218282d47d3f15b85e5defc2d9d1961b6ffed7a0","impliedFormat":1},{"version":"93a2049cbc80c66aa33582ec2648e1df2df59d2b353d6b4a97c9afcbb111ccab","impliedFormat":1},{"version":"d04d359e40db3ae8a8c23d0f096ad3f9f73a9ef980f7cb252a1fdc1e7b3a2fb9","impliedFormat":1},{"version":"84aa4f0c33c729557185805aae6e0df3bd084e311da67a10972bbcf400321ff0","impliedFormat":1},{"version":"cf6cbe50e3f87b2f4fd1f39c0dc746b452d7ce41b48aadfdb724f44da5b6f6ed","impliedFormat":1},{"version":"3cf494506a50b60bf506175dead23f43716a088c031d3aa00f7220b3fbcd56c9","impliedFormat":1},{"version":"f2d47126f1544c40f2b16fc82a66f97a97beac2085053cf89b49730a0e34d231","impliedFormat":1},{"version":"724ac138ba41e752ae562072920ddee03ba69fe4de5dafb812e0a35ef7fb2c7e","impliedFormat":1},{"version":"e4eb3f8a4e2728c3f2c3cb8e6b60cadeb9a189605ee53184d02d265e2820865c","impliedFormat":1},{"version":"f16cb1b503f1a64b371d80a0018949135fbe06fb4c5f78d4f637b17921a49ee8","impliedFormat":1},{"version":"f4808c828723e236a4b35a1415f8f550ff5dec621f81deea79bf3a051a84ffd0","impliedFormat":1},{"version":"3b810aa3410a680b1850ab478d479c2f03ed4318d1e5bf7972b49c4d82bacd8d","impliedFormat":1},{"version":"0ce7166bff5669fcb826bc6b54b246b1cf559837ea9cc87c3414cc70858e6097","impliedFormat":1},{"version":"6ea095c807bc7cc36bc1774bc2a0ef7174bf1c6f7a4f6b499170b802ce214bfe","impliedFormat":1},{"version":"3549400d56ee2625bb5cc51074d3237702f1f9ffa984d61d9a2db2a116786c22","impliedFormat":1},{"version":"5327f9a620d003b202eff5db6be0b44e22079793c9a926e0a7a251b1dbbdd33f","impliedFormat":1},{"version":"b60f6734309d20efb9b0e0c7e6e68282ee451592b9c079dd1a988bb7a5eeb5e7","impliedFormat":1},{"version":"f4187a4e2973251fd9655598aa7e6e8bba879939a73188ee3290bb090cc46b15","impliedFormat":1},{"version":"44c1a26f578277f8ccef3215a4bd642a0a4fbbaf187cf9ae3053591c891fdc9c","impliedFormat":1},{"version":"a5989cd5e1e4ca9b327d2f93f43e7c981f25ee12a81c2ebde85ec7eb30f34213","impliedFormat":1},{"version":"f65b8fa1532dfe0ef2c261d63e72c46fe5f089b28edcd35b3526328d42b412b8","impliedFormat":1},{"version":"1060083aacfc46e7b7b766557bff5dafb99de3128e7bab772240877e5bfe849d","impliedFormat":1},{"version":"d61a3fa4243c8795139e7352694102315f7a6d815ad0aeb29074cfea1eb67e93","impliedFormat":1},{"version":"1f66b80bad5fa29d9597276821375ddf482c84cfb12e8adb718dc893ffce79e0","impliedFormat":1},{"version":"1ed8606c7b3612e15ff2b6541e5a926985cbb4d028813e969c1976b7f4133d73","impliedFormat":1},{"version":"c086ab778e9ba4b8dbb2829f42ef78e2b28204fc1a483e42f54e45d7a96e5737","impliedFormat":1},{"version":"dd0b9b00a39436c1d9f7358be8b1f32571b327c05b5ed0e88cc91f9d6b6bc3c9","impliedFormat":1},{"version":"a951a7b2224a4e48963762f155f5ad44ca1145f23655dde623ae312d8faeb2f2","impliedFormat":1},{"version":"cd960c347c006ace9a821d0a3cffb1d3fbc2518a4630fb3d77fe95f7fd0758b8","impliedFormat":1},{"version":"fe1f3b21a6cc1a6bc37276453bd2ac85910a8bdc16842dc49b711588e89b1b77","impliedFormat":1},{"version":"1a6a21ff41d509ab631dbe1ea14397c518b8551f040e78819f9718ef80f13975","impliedFormat":1},{"version":"0a55c554e9e858e243f714ce25caebb089e5cc7468d5fd022c1e8fa3d8e8173d","impliedFormat":1},{"version":"3a5e0fe9dcd4b1a9af657c487519a3c39b92a67b1b21073ff20e37f7d7852e32","impliedFormat":1},{"version":"977aeb024f773799d20985c6817a4c0db8fed3f601982a52d4093e0c60aba85f","impliedFormat":1},{"version":"d59cf5116848e162c7d3d954694f215b276ad10047c2854ed2ee6d14a481411f","impliedFormat":1},{"version":"50098be78e7cbfc324dfc04983571c80539e55e11a0428f83a090c13c41824a2","impliedFormat":1},{"version":"08e767d9d3a7e704a9ea5f057b0f020fd5880bc63fbb4aa6ffee73be36690014","impliedFormat":1},{"version":"dd6051c7b02af0d521857069c49897adb8595d1f0e94487d53ebc157294ef864","impliedFormat":1},{"version":"79c6a11f75a62151848da39f6098549af0dd13b22206244961048326f451b2a8","impliedFormat":1},"4a9dc49145b6b7ccb8608c4f8c49c871ea508a9c9b892b35ffbea412e9ef1723"],"root":[450,459,460,[502,504],[583,587],608,616,[618,623],694],"options":{"declaration":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[459,1],[450,2],[460,3],[504,4],[585,5],[586,4],[620,6],[621,7],[616,8],[622,9],[623,10],[694,11],[619,12],[618,13],[584,14],[502,9],[587,1],[583,15],[503,16],[608,17],[600,18],[601,19],[603,20],[602,21],[597,18],[598,18],[596,18],[595,18],[599,22],[604,23],[606,24],[607,25],[605,25],[588,9],[591,26],[589,27],[590,9],[456,28],[458,29],[455,30],[452,31],[453,32],[454,33],[451,34],[457,35],[396,9],[519,36],[521,37],[520,38],[529,9],[577,39],[578,40],[579,41],[581,42],[580,9],[574,43],[576,44],[573,36],[527,9],[525,45],[526,45],[528,46],[523,47],[524,45],[532,9],[534,48],[533,49],[544,9],[547,50],[545,51],[548,52],[546,51],[549,50],[550,52],[551,53],[522,36],[565,54],[566,55],[552,52],[553,50],[554,50],[555,52],[556,50],[557,50],[558,52],[562,56],[540,57],[559,58],[560,50],[561,52],[535,9],[536,59],[538,60],[537,59],[539,61],[564,62],[563,9],[582,63],[567,36],[569,54],[572,64],[570,65],[568,66],[571,66],[531,36],[530,36],[542,9],[543,67],[541,68],[497,69],[498,70],[496,71],[491,72],[500,73],[485,9],[486,74],[495,75],[490,76],[499,9],[494,77],[487,9],[488,9],[493,78],[489,75],[492,76],[462,79],[463,80],[461,9],[473,81],[467,9],[476,82],[468,9],[474,83],[472,83],[475,84],[471,85],[470,9],[469,86],[464,9],[480,87],[481,88],[479,89],[482,90],[478,91],[466,9],[465,9],[477,92],[484,93],[501,94],[612,9],[613,95],[614,96],[615,97],[644,9],[627,98],[645,99],[626,9],[575,9],[122,100],[123,100],[124,101],[76,102],[125,103],[126,104],[127,105],[71,9],[74,106],[72,9],[73,9],[128,107],[129,108],[130,109],[131,110],[132,111],[133,112],[134,112],[135,113],[136,114],[137,115],[138,116],[77,9],[75,9],[139,117],[140,118],[141,119],[175,120],[142,121],[143,9],[144,122],[145,123],[146,124],[147,125],[148,126],[149,127],[150,128],[151,129],[152,130],[153,130],[154,131],[155,9],[156,132],[157,133],[159,134],[158,135],[160,136],[161,137],[162,138],[163,139],[164,140],[165,141],[166,142],[167,143],[168,144],[169,145],[170,146],[171,147],[172,148],[78,9],[79,9],[80,9],[119,149],[120,9],[121,9],[173,150],[174,151],[179,152],[180,153],[178,154],[176,155],[177,156],[60,9],[62,157],[292,158],[81,9],[61,9],[483,9],[617,158],[69,159],[399,160],[404,161],[406,162],[200,163],[348,164],[375,165],[275,9],[193,9],[198,9],[339,166],[267,167],[199,9],[377,168],[378,169],[320,170],[336,171],[240,172],[343,173],[344,174],[342,175],[341,9],[340,176],[376,177],[201,178],[274,9],[276,179],[196,9],[211,180],[202,181],[215,180],[244,180],[186,180],[347,182],[357,9],[192,9],[298,183],[299,184],[293,185],[427,9],[301,9],[302,185],[294,186],[431,187],[430,188],[426,9],[380,9],[335,189],[334,9],[425,190],[295,158],[218,191],[216,192],[428,9],[429,9],[217,193],[420,194],[423,195],[227,196],[226,197],[225,198],[434,158],[224,199],[262,9],[437,9],[610,200],[609,9],[440,9],[439,158],[441,201],[182,9],[345,202],[346,203],[369,9],[191,204],[181,9],[184,205],[314,158],[313,206],[312,207],[303,9],[304,9],[311,9],[306,9],[309,208],[305,9],[307,209],[310,210],[308,209],[197,9],[189,9],[190,180],[398,211],[407,212],[411,213],[351,214],[350,9],[259,9],[442,215],[360,216],[296,217],[297,218],[289,219],[281,9],[287,9],[288,220],[318,221],[282,222],[319,223],[316,224],[315,9],[317,9],[271,225],[352,226],[353,227],[283,228],[284,229],[279,230],[331,231],[359,232],[362,233],[260,234],[187,235],[358,236],[183,165],[381,237],[392,238],[379,9],[391,239],[70,9],[367,240],[247,9],[277,241],[363,9],[206,9],[390,242],[195,9],[250,243],[349,244],[389,9],[383,245],[188,9],[384,246],[386,247],[387,248],[370,9],[388,235],[214,249],[368,250],[393,251],[323,9],[326,9],[324,9],[328,9],[325,9],[327,9],[329,252],[322,9],[253,253],[252,9],[258,254],[254,255],[257,256],[256,256],[255,255],[210,257],[242,258],[356,259],[443,9],[415,260],[417,261],[286,9],[416,262],[354,226],[300,226],[194,9],[243,263],[207,264],[208,265],[209,266],[205,267],[330,267],[221,267],[245,268],[222,268],[204,269],[203,9],[251,270],[249,271],[248,272],[246,273],[355,274],[291,275],[321,276],[290,277],[338,278],[337,279],[333,280],[239,281],[241,282],[238,283],[212,284],[270,9],[403,9],[269,285],[332,9],[261,286],[280,287],[278,288],[263,289],[265,290],[438,9],[264,291],[266,291],[401,9],[400,9],[402,9],[436,9],[268,292],[236,158],[68,9],[219,293],[228,9],[273,294],[213,9],[409,158],[419,295],[235,158],[413,185],[234,296],[395,297],[233,295],[185,9],[421,298],[231,158],[232,158],[223,9],[272,9],[230,299],[229,300],[220,301],[285,129],[361,129],[385,9],[365,302],[364,9],[405,9],[237,158],[397,303],[63,158],[66,304],[67,305],[64,158],[65,9],[382,14],[374,306],[373,9],[372,307],[371,9],[394,308],[408,309],[410,310],[412,311],[611,312],[414,313],[418,314],[449,315],[422,315],[448,316],[424,317],[432,318],[433,319],[435,320],[444,321],[447,204],[446,9],[445,322],[593,18],[594,323],[592,18],[667,324],[669,325],[659,326],[664,327],[665,328],[671,329],[666,330],[663,331],[662,332],[661,333],[672,334],[629,327],[630,327],[670,327],[675,335],[685,336],[679,336],[687,336],[691,336],[678,336],[680,336],[683,336],[686,336],[682,337],[684,336],[688,158],[681,327],[677,338],[676,339],[638,158],[642,158],[632,327],[635,158],[640,327],[641,340],[634,341],[637,158],[639,158],[636,342],[625,158],[624,158],[693,343],[690,344],[656,345],[655,327],[653,158],[654,327],[657,346],[658,347],[651,158],[647,348],[650,327],[649,327],[648,327],[643,327],[652,348],[689,327],[668,349],[674,350],[692,9],[660,9],[673,351],[633,9],[631,352],[366,353],[58,9],[59,9],[10,9],[11,9],[13,9],[12,9],[2,9],[14,9],[15,9],[16,9],[17,9],[18,9],[19,9],[20,9],[21,9],[3,9],[22,9],[23,9],[4,9],[24,9],[28,9],[25,9],[26,9],[27,9],[29,9],[30,9],[31,9],[5,9],[32,9],[33,9],[34,9],[35,9],[6,9],[39,9],[36,9],[37,9],[38,9],[40,9],[7,9],[41,9],[46,9],[47,9],[42,9],[43,9],[44,9],[45,9],[8,9],[51,9],[48,9],[49,9],[50,9],[52,9],[9,9],[53,9],[54,9],[55,9],[57,9],[56,9],[1,9],[97,354],[107,355],[96,354],[117,356],[88,357],[87,358],[116,322],[110,359],[115,360],[90,361],[104,362],[89,363],[113,364],[85,365],[84,322],[114,366],[86,367],[91,368],[92,9],[95,368],[82,9],[118,369],[108,370],[99,371],[100,372],[102,373],[98,374],[101,375],[111,322],[93,376],[94,377],[103,378],[83,379],[106,370],[105,368],[109,9],[112,380],[628,381],[646,382],[518,383],[508,384],[510,385],[517,386],[512,9],[513,9],[511,387],[514,388],[505,9],[506,9],[507,383],[509,389],[515,9],[516,390]],"semanticDiagnosticsPerFile":[[460,[{"start":70,"length":23,"messageText":"Cannot find module '@next/bundle-analyzer' or its corresponding type declarations.","category":1,"code":2307}]],[583,[{"start":106,"length":10,"messageText":"'\"@stellar/stellar-sdk\"' has no exported member named 'SorobanRpc'. Did you mean 'Soroban'?","category":1,"code":2724,"relatedInformation":[{"file":"../../node_modules/.pnpm/@stellar+stellar-base@13.1.0/node_modules/@stellar/stellar-base/types/index.d.ts","start":38143,"length":7,"messageText":"'Soroban' is declared here.","category":3,"code":2728}]},{"start":221,"length":21,"messageText":"Cannot find module '@solarproof/stellar' or its corresponding type declarations.","category":1,"code":2307}]],[585,[{"start":79,"length":16,"messageText":"Cannot find module '@noble/ed25519' or its corresponding type declarations.","category":1,"code":2307},{"start":316,"length":21,"messageText":"Cannot find module '@solarproof/stellar' or its corresponding type declarations.","category":1,"code":2307},{"start":1775,"length":10,"code":2339,"category":1,"messageText":"Property 'pubkey_hex' does not exist on type 'never'."},{"start":2045,"length":8,"code":2353,"category":1,"messageText":"Object literal may only specify known properties, and 'meter_id' does not exist in type 'never[]'."},{"start":2539,"length":10,"code":2339,"category":1,"messageText":"Property 'pubkey_hex' does not exist on type 'never'."},{"start":2713,"length":48,"code":2345,"category":1,"messageText":"Argument of type '{ anchored: boolean; anchor_tx_hash: string; }' is not assignable to parameter of type 'never'."},{"start":2780,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":2942,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":3031,"length":12,"code":2339,"category":1,"messageText":"Property 'cooperatives' does not exist on type 'never'."},{"start":3297,"length":42,"code":2345,"category":1,"messageText":"Argument of type '{ minted: boolean; mint_tx_hash: string; }' is not assignable to parameter of type 'never'."},{"start":3358,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":3411,"length":14,"code":2353,"category":1,"messageText":"Object literal may only specify known properties, and 'cooperative_id' does not exist in type 'never[]'."},{"start":3433,"length":14,"code":2339,"category":1,"messageText":"Property 'cooperative_id' does not exist on type 'never'."},{"start":3475,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":3731,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":3966,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."}]],[586,[{"start":999,"length":10,"code":2339,"category":1,"messageText":"Property 'reading_id' does not exist on type 'never'."},{"start":1078,"length":2,"code":2339,"category":1,"messageText":"Property 'id' does not exist on type 'never'."},{"start":1098,"length":3,"code":2339,"category":1,"messageText":"Property 'kwh' does not exist on type 'never'."},{"start":1125,"length":9,"code":2339,"category":1,"messageText":"Property 'issued_at' does not exist on type 'never'."},{"start":1156,"length":7,"code":2339,"category":1,"messageText":"Property 'retired' does not exist on type 'never'."},{"start":1188,"length":10,"code":2339,"category":1,"messageText":"Property 'retired_at' does not exist on type 'never'."},{"start":1223,"length":10,"code":2339,"category":1,"messageText":"Property 'retired_by' does not exist on type 'never'."},{"start":1280,"length":14,"code":2339,"category":1,"messageText":"Property 'anchor_tx_hash' does not exist on type 'never'."},{"start":1370,"length":14,"code":2339,"category":1,"messageText":"Property 'anchor_tx_hash' does not exist on type 'never'."},{"start":1408,"length":12,"code":2339,"category":1,"messageText":"Property 'mint_tx_hash' does not exist on type 'never'."},{"start":1494,"length":12,"code":2339,"category":1,"messageText":"Property 'mint_tx_hash' does not exist on type 'never'."},{"start":1580,"length":8,"code":2339,"category":1,"messageText":"Property 'meter_id' does not exist on type 'never'."},{"start":1622,"length":12,"code":2339,"category":1,"messageText":"Property 'reading_hash' does not exist on type 'never'."},{"start":1669,"length":13,"code":2339,"category":1,"messageText":"Property 'signature_hex' does not exist on type 'never'."},{"start":1707,"length":3,"code":2339,"category":1,"messageText":"Property 'kwh' does not exist on type 'never'."},{"start":1741,"length":9,"code":2339,"category":1,"messageText":"Property 'timestamp' does not exist on type 'never'."}]]],"affectedFilesPendingEmit":[459,460,504,585,586,620,621,616,622,623,694,619,618,584,502,587,583,503,608],"version":"5.9.3"} \ No newline at end of file diff --git a/docs/LOGGING.md b/docs/LOGGING.md new file mode 100644 index 0000000..f158b31 --- /dev/null +++ b/docs/LOGGING.md @@ -0,0 +1,64 @@ +# Logging + +SolarProof ships structured logs to [Better Stack (Logtail)](https://betterstack.com/logs) via `@logtail/next`. + +## Setup + +Set the source token in your environment: + +``` +LOGTAIL_SOURCE_TOKEN= +``` + +Get a token from the [Better Stack dashboard](https://logs.betterstack.com) → Sources → Create source → Node.js. + +## Log Levels + +| Level | Usage | +|-------|-------| +| `debug` | Development diagnostics (suppressed in production by default) | +| `info` | Normal operations: readings received, tokens minted, anchors recorded | +| `warn` | Recoverable issues: retries, degraded service | +| `error` | Failures: mint errors, signature verification failures, contract errors | + +## Usage + +```ts +import { log } from "@/lib/logger"; + +log("info", "Reading anchored", { reading_id: id, kwh }); +log("error", "Mint failed", { reading_id: id, reason: err.message }); +``` + +## Sensitive Data Exclusion + +The logger automatically redacts any metadata field whose name matches `/secret|key|signature|token/i`. The value is replaced with `[REDACTED]` before the log is shipped. + +Redacted fields include (but are not limited to): `secret_key`, `signature_hex`, `pubkey_hex`, `service_role_key`, `source_token`. + +Never log raw private keys or signatures. Use `reading_id` or `tx_hash` as correlation identifiers instead. + +## Retention Policy + +Configure retention in the Better Stack dashboard under **Sources → Retention**: + +| Level | Retention | +|-------|-----------| +| `info`, `debug`, `warn` | 30 days | +| `error` | 90 days | + +To enforce this, create two separate Better Stack sources (one for errors, one for everything else) and route logs accordingly, or use Better Stack's built-in retention tiers. + +## Alerts + +Set up alerts in Better Stack under **Alerts → Create alert**: + +| Alert | Condition | Channel | +|-------|-----------|---------| +| Error rate spike | `level = error` count > 10 in 5 min | Email / PagerDuty | +| Mint failure | `message contains "Mint failed"` any occurrence | Slack | +| Signature failure | `message contains "signature"` count > 5 in 1 min | Email | + +## Local Development + +Without `LOGTAIL_SOURCE_TOKEN` set, `@logtail/next` falls back to `console` output. No configuration needed for local development. From 4781be57ba4c70cb33ace3a027a1308090c9e3d1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 29 May 2026 16:20:40 +0000 Subject: [PATCH 08/76] chore(release): 1.9.0 [skip ci] ## [1.9.0](https://github.com/AnnabelJoe/solarproof/compare/v1.8.2...v1.9.0) (2026-05-29) ### Features * configure log aggregation and retention ([#299](https://github.com/AnnabelJoe/solarproof/issues/299)) ([3ee0154](https://github.com/AnnabelJoe/solarproof/commit/3ee0154492cccc7ce78c61925c0b72c44120fdf4)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f941356..0df51fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.9.0](https://github.com/AnnabelJoe/solarproof/compare/v1.8.2...v1.9.0) (2026-05-29) + +### Features + +* configure log aggregation and retention ([#299](https://github.com/AnnabelJoe/solarproof/issues/299)) ([3ee0154](https://github.com/AnnabelJoe/solarproof/commit/3ee0154492cccc7ce78c61925c0b72c44120fdf4)) + ## [1.8.2](https://github.com/AnnabelJoe/solarproof/compare/v1.8.1...v1.8.2) (2026-05-29) ### Bug Fixes From 4def0e722cfe10ae0d5daeb4965ea2597f8ca836 Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 05:47:39 +0000 Subject: [PATCH 09/76] feat(contracts): optimize Soroban storage for audit-registry - Implement bucketed storage to reduce ledger entries - Move idempotency nonces to temporary storage - Remove redundant reading_hash from storage values - Update and expand test suite Closes #281 --- apps/contracts/audit_registry/src/lib.rs | 131 ++++++++++++++++------- docs/contracts/storage_optimization.md | 40 +++++++ 2 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 docs/contracts/storage_optimization.md diff --git a/apps/contracts/audit_registry/src/lib.rs b/apps/contracts/audit_registry/src/lib.rs index 68e5520..2632410 100644 --- a/apps/contracts/audit_registry/src/lib.rs +++ b/apps/contracts/audit_registry/src/lib.rs @@ -21,7 +21,7 @@ //! |-----|-------------|-------|------| //! | `DataKey::Admin` | instance | `Address` | ~57 B | //! | `DataKey::TotalAnchors` | instance | `u32` | 4 B | -//! | `DataKey::Anchor(hash)` | persistent | `AuditAnchor` | 36 B | +//! | `DataKey::Bucket(id)` | persistent | `Map, u32>` | Var | //! //! ## Invariants //! 1. Each `reading_hash` can be anchored at most once. @@ -37,7 +37,7 @@ #![no_std] use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, BytesN, Env, + contract, contracterror, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Map, }; const VERSION: &str = "1.0.0"; @@ -66,9 +66,10 @@ pub enum DataKey { Admin, /// `Address` — the only address authorised to call `anchor()`. ApiSigner, - /// `AuditAnchor` — keyed by the 32-byte reading hash. - Anchor(BytesN<32>), - /// `bool` — keyed by the 32-byte nonce. + /// Bucketed storage for reading hashes to reduce ledger entry count. + /// `Map, u32>` — keyed by bucket index (0-1023). + Bucket(u32), + /// `bool` — keyed by the 32-byte nonce. Used for idempotency. Nonce(BytesN<32>), /// `u32` — total number of anchors stored. TotalAnchors, @@ -163,6 +164,13 @@ impl AuditRegistry { .expect("not initialized") } + /// Helper to get bucket ID from a hash (0-1023). + fn get_bucket_id(hash: &BytesN<32>) -> u32 { + let b0 = hash.get(0).unwrap_or(0) as u32; + let b1 = hash.get(1).unwrap_or(0) as u32; + ((b0 << 8) | b1) % 1024 + } + /// Anchor a reading hash on-chain. /// /// # Events @@ -183,24 +191,28 @@ impl AuditRegistry { return Err(Error::Unauthorized); } - let nonce_key = DataKey::Nonce(nonce.clone()); - if env.storage().persistent().has(&nonce_key) { + // Use temporary storage for nonces (idempotency) to reduce persistent entry costs. + let nonce_key = DataKey::Nonce(nonce); + if env.storage().temporary().has(&nonce_key) { return Err(Error::AlreadyAnchored); } - let key = DataKey::Anchor(reading_hash.clone()); - if env.storage().persistent().has(&key) { + let bucket_id = Self::get_bucket_id(&reading_hash); + let bucket_key = DataKey::Bucket(bucket_id); + let mut bucket: Map, u32> = env + .storage() + .persistent() + .get(&bucket_key) + .unwrap_or_else(|| Map::new(&env)); + + if bucket.contains_key(reading_hash.clone()) { return Err(Error::AlreadyAnchored); } - env.storage().persistent().set(&nonce_key, &true); - - let anchor = AuditAnchor { - reading_hash: reading_hash.clone(), - anchored_at_ledger: env.ledger().sequence(), - }; + env.storage().temporary().set(&nonce_key, &true); - env.storage().persistent().set(&key, &anchor); + bucket.set(reading_hash.clone(), env.ledger().sequence()); + env.storage().persistent().set(&bucket_key, &bucket); let count: u32 = env .storage() @@ -224,16 +236,23 @@ impl AuditRegistry { /// Returns the `AuditAnchor` for `reading_hash`, or `None` if not anchored. pub fn verify(env: Env, reading_hash: BytesN<32>) -> Option { - env.storage() - .persistent() - .get(&DataKey::Anchor(reading_hash)) + let bucket_id = Self::get_bucket_id(&reading_hash); + let bucket: Map, u32> = env.storage().persistent().get(&DataKey::Bucket(bucket_id))?; + let anchored_at_ledger = bucket.get(reading_hash.clone())?; + Some(AuditAnchor { + reading_hash, + anchored_at_ledger, + }) } /// Returns `true` if `reading_hash` has been anchored, `false` otherwise. pub fn is_anchored(env: Env, reading_hash: BytesN<32>) -> bool { - env.storage() - .persistent() - .has(&DataKey::Anchor(reading_hash)) + let bucket_id = Self::get_bucket_id(&reading_hash); + let bucket: Option, u32>> = env.storage().persistent().get(&DataKey::Bucket(bucket_id)); + match bucket { + Some(b) => b.contains_key(reading_hash), + None => false, + } } /// Returns the total number of reading hashes anchored so far. @@ -285,7 +304,8 @@ mod tests { fn test_anchor_and_verify() { let (env, api_signer, client) = setup(); let h = hash(&env); - client.anchor(&api_signer, &h).unwrap(); + let n = make_nonce(&env, 1); + client.anchor(&api_signer, &h, &n).unwrap(); assert!(client.is_anchored(&h)); assert_eq!(client.total_anchors(), 1); let anchor = client.verify(&h).unwrap(); @@ -296,7 +316,8 @@ mod tests { fn test_anchor_records_ledger_sequence() { let (env, api_signer, client) = setup(); let h = hash(&env); - client.anchor(&api_signer, &h).unwrap(); + let n = make_nonce(&env, 1); + client.anchor(&api_signer, &h, &n).unwrap(); let anchor = client.verify(&h).unwrap(); let _ = anchor.anchored_at_ledger; } @@ -305,16 +326,30 @@ mod tests { fn test_duplicate_anchor_rejected() { let (env, api_signer, client) = setup(); let h = hash(&env); - client.anchor(&api_signer, &h).unwrap(); - assert_eq!(client.anchor(&api_signer, &h), Err(Error::AlreadyAnchored)); + let n1 = make_nonce(&env, 1); + let n2 = make_nonce(&env, 2); + client.anchor(&api_signer, &h, &n1).unwrap(); + assert_eq!(client.anchor(&api_signer, &h, &n2), Err(Error::AlreadyAnchored)); + } + + #[test] + fn test_duplicate_nonce_rejected() { + let (env, api_signer, client) = setup(); + let h1 = BytesN::from_array(&env, &[1u8; 32]); + let h2 = BytesN::from_array(&env, &[2u8; 32]); + let n = make_nonce(&env, 1); + client.anchor(&api_signer, &h1, &n).unwrap(); + assert_eq!(client.anchor(&api_signer, &h2, &n), Err(Error::AlreadyAnchored)); } #[test] fn test_duplicate_anchor_does_not_increment_total() { let (env, api_signer, client) = setup(); let h = hash(&env); - client.anchor(&api_signer, &h).unwrap(); - let _ = client.anchor(&api_signer, &h); + let n1 = make_nonce(&env, 1); + let n2 = make_nonce(&env, 2); + client.anchor(&api_signer, &h, &n1).unwrap(); + let _ = client.anchor(&api_signer, &h, &n2); assert_eq!(client.total_anchors(), 1); } @@ -323,8 +358,8 @@ mod tests { let (env, api_signer, client) = setup(); let h1 = BytesN::from_array(&env, &[0xAAu8; 32]); let h2 = BytesN::from_array(&env, &[0xBBu8; 32]); - client.anchor(&api_signer, &h1).unwrap(); - client.anchor(&api_signer, &h2).unwrap(); + client.anchor(&api_signer, &h1, &make_nonce(&env, 1)).unwrap(); + client.anchor(&api_signer, &h2, &make_nonce(&env, 2)).unwrap(); assert!(client.is_anchored(&h1)); assert!(client.is_anchored(&h2)); assert_eq!(client.total_anchors(), 2); @@ -335,7 +370,7 @@ mod tests { let (env, _api_signer, client) = setup(); let attacker = soroban_sdk::Address::generate(&env); assert_eq!( - client.anchor(&attacker, &hash(&env)), + client.anchor(&attacker, &hash(&env), &make_nonce(&env, 1)), Err(Error::Unauthorized) ); } @@ -346,7 +381,7 @@ mod tests { let new_signer = soroban_sdk::Address::generate(&env); client.set_api_signer(&new_signer); assert_eq!( - client.anchor(&old_signer, &hash(&env)), + client.anchor(&old_signer, &hash(&env), &make_nonce(&env, 1)), Err(Error::Unauthorized) ); } @@ -372,7 +407,7 @@ mod tests { let (env, api_signer, client) = setup(); for i in 0u8..5 { client - .anchor(&api_signer, &BytesN::from_array(&env, &[i; 32])) + .anchor(&api_signer, &BytesN::from_array(&env, &[i; 32]), &make_nonce(&env, i)) .unwrap(); } assert_eq!(client.total_anchors(), 5); @@ -397,7 +432,7 @@ mod tests { let count: u8 = 50; for i in 0..count { let h = BytesN::from_array(&env, &[i; 32]); - client.anchor(&api_signer, &h).unwrap(); + client.anchor(&api_signer, &h, &make_nonce(&env, i)).unwrap(); } assert_eq!(client.total_anchors(), u32::from(count)); assert!(client.is_anchored(&BytesN::from_array(&env, &[0u8; 32]))); @@ -409,8 +444,8 @@ mod tests { let (env, api_signer, client) = setup(); let all_zeros = BytesN::from_array(&env, &[0x00u8; 32]); let all_ones = BytesN::from_array(&env, &[0xFFu8; 32]); - client.anchor(&api_signer, &all_zeros).unwrap(); - client.anchor(&api_signer, &all_ones).unwrap(); + client.anchor(&api_signer, &all_zeros, &make_nonce(&env, 1)).unwrap(); + client.anchor(&api_signer, &all_ones, &make_nonce(&env, 2)).unwrap(); assert!(client.is_anchored(&all_zeros)); assert!(client.is_anchored(&all_ones)); assert_eq!(client.total_anchors(), 2); @@ -431,7 +466,7 @@ mod tests { let new_signer = soroban_sdk::Address::generate(&env); client.set_api_signer(&new_signer); let h = hash(&env); - client.anchor(&new_signer, &h).unwrap(); + client.anchor(&new_signer, &h, &make_nonce(&env, 1)).unwrap(); assert!(client.is_anchored(&h)); } @@ -451,4 +486,26 @@ mod tests { soroban_sdk::String::from_str(&env, "1.0.0") ); } + + #[test] + fn test_bucket_collision() { + let (env, api_signer, client) = setup(); + // Force hashes that likely end up in the same bucket + // Our bucket ID is ((hash[0] << 8) | hash[1]) % 1024 + // So any hash starting with 0x00 0x00 will be in bucket 0. + let mut h1_arr = [0u8; 32]; + h1_arr[2] = 1; + let h1 = BytesN::from_array(&env, &h1_arr); + + let mut h2_arr = [0u8; 32]; + h2_arr[2] = 2; + let h2 = BytesN::from_array(&env, &h2_arr); + + client.anchor(&api_signer, &h1, &make_nonce(&env, 1)).unwrap(); + client.anchor(&api_signer, &h2, &make_nonce(&env, 2)).unwrap(); + + assert!(client.is_anchored(&h1)); + assert!(client.is_anchored(&h2)); + assert_eq!(client.total_anchors(), 2); + } } diff --git a/docs/contracts/storage_optimization.md b/docs/contracts/storage_optimization.md new file mode 100644 index 0000000..e949120 --- /dev/null +++ b/docs/contracts/storage_optimization.md @@ -0,0 +1,40 @@ +# Soroban Contract Storage Optimization Report + +## Overview +The `audit-registry` contract storage has been optimized to reduce ledger entry costs and footprint on the Stellar network. + +## Optimization Strategies + +### 1. Bucketed Storage +Previously, each meter reading hash was stored in its own persistent ledger entry. This resulted in one new ledger entry per reading, which is expensive due to the per-entry base cost. + +**Optimized Layout:** +- Readings are now grouped into **1024 buckets**. +- Each bucket is a single persistent ledger entry containing a `Map, u32>` (Reading Hash -> Ledger Sequence). +- Bucket ID is derived from the first two bytes of the reading hash: `((hash[0] << 8) | hash[1]) % 1024`. + +### 2. Redundant Data Removal +The `AuditAnchor` struct previously stored the `reading_hash` in the entry value. Since the hash is already the key (either in the previous individual entry or in the new bucket Map), it was redundant. + +**Optimized Value:** +- Only the `anchored_at_ledger` (4 bytes) is stored as the value in the bucket Map. +- The `AuditAnchor` struct is reconstructed on-the-fly when queried. + +### 3. Temporary Storage for Idempotency +Nonces used for transaction idempotency were previously stored in persistent storage. + +**Optimized Storage:** +- Nonces are now stored in **Temporary storage**. +- This reduces the long-term ledger footprint as nonces only need to be unique for a short window to prevent immediate replays. Permanent replay protection is still provided by the reading hash itself in the bucketed storage. + +## Cost Comparison + +| Metric | Before Optimization | After Optimization | Improvement | +|--------|---------------------|--------------------|-------------| +| **Persistent Entries** | N + M (N readings, M nonces) | min(N, 1024) | ~99.9% reduction for 1M readings | +| **Temporary Entries** | 0 | M (M nonces) | Better use of cheaper storage | +| **Data Size (per reading)** | ~70 bytes + entry overhead | ~36 bytes in Map | ~50% reduction in value size | +| **Base Entry Costs** | 2 per reading | ~0.001 per reading (at scale) | High savings on base fees | + +## Documentation +The contract code in `apps/contracts/audit_registry/src/lib.rs` has been updated with these changes, and all tests have been verified (fixed and extended with bucket collision tests). From a6b4365db1115c5f6dcce7bc224481992845e672 Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 06:05:50 +0000 Subject: [PATCH 10/76] test(contracts): rename regression test for issue #281 --- apps/contracts/audit_registry/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/contracts/audit_registry/src/lib.rs b/apps/contracts/audit_registry/src/lib.rs index 2632410..e3e83b5 100644 --- a/apps/contracts/audit_registry/src/lib.rs +++ b/apps/contracts/audit_registry/src/lib.rs @@ -488,7 +488,7 @@ mod tests { } #[test] - fn test_bucket_collision() { + fn test_issue_281_bucket_collision() { let (env, api_signer, client) = setup(); // Force hashes that likely end up in the same bucket // Our bucket ID is ((hash[0] << 8) | hash[1]) % 1024 From cd9a68c28eeede3309d23000487f682ec12d40c6 Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 06:13:12 +0000 Subject: [PATCH 11/76] docs(docs): write governance parameter tuning guide - Added guidance for quorum, thresholds, and durations - Included configurations for small, medium, and large DAOs - Linked guide from community_governance contract docs Closes #279 --- docs/contracts/community_governance.md | 2 + docs/governance_tuning_guide.md | 78 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 docs/governance_tuning_guide.md diff --git a/docs/contracts/community_governance.md b/docs/contracts/community_governance.md index e9cfa05..45bfbc4 100644 --- a/docs/contracts/community_governance.md +++ b/docs/contracts/community_governance.md @@ -2,6 +2,8 @@ Cooperative on-chain governance — token holders submit proposals and vote. A proposal passes when `yes_votes / total_votes ≥ quorum%` after the voting period ends. +For best practices on configuring these parameters, see the [Governance Parameter Tuning Guide](../governance_tuning_guide.md). + - **SDK:** Soroban SDK 23.1.0 / OpenZeppelin Stellar v0.5.1 --- diff --git a/docs/governance_tuning_guide.md b/docs/governance_tuning_guide.md new file mode 100644 index 0000000..5adde51 --- /dev/null +++ b/docs/governance_tuning_guide.md @@ -0,0 +1,78 @@ +# Governance Parameter Tuning Guide + +This guide provides best practices for configuring governance parameters in the SolarProof Community Governance contract. Choosing the right parameters is critical for balancing security, agility, and community participation. + +## Parameters Overview + +### 1. Quorum Threshold (`QuorumBps`) +The minimum percentage of "Yes" votes required for a proposal to pass, relative to the total number of registered voters. +- **Role**: Prevents small minorities from making significant changes. +- **Default**: 1000 (10%). + +### 2. Approval Threshold (`ThresholdBps`) +The percentage of cast votes that must be "Yes" for the proposal to pass (majority rule). +- **Role**: Ensures broad consensus among active voters. +- **Default**: 5100 (51%). + +### 3. Voting Duration (`VotingPeriod`) +The length of time (in ledgers) that a proposal remains open for voting. +- **Role**: Balances the need for quick decisions with giving voters enough time to review and cast their votes. +- **Scale**: In Soroban (10s ledger time), 1 day ≈ 8,640 ledgers. + +### 4. Execution Timelock (`ExecuteTimelock`) +The cooldown period between a proposal passing and when it can actually be executed. +- **Role**: Provides a safety window for the community to react (or exit) if a malicious or controversial proposal passes. + +### 5. Minimum Balance (Proposer Requirement) +*Note: Currently enforced socially or through custom front-ends/wrappers in SolarProof.* +- **Role**: Prevents spam by requiring proposers to have a "stake" in the system (e.g., holding a minimum amount of Energy Tokens). + +--- + +## Tuning by DAO Size + +### Small DAOs (< 100 Members) +Typically highly active, closely-knit groups where communication is efficient. +- **Goal**: High agility and high participation. +- **Quorum**: High (e.g., 20-30%) because reaching a large portion of 50 people is feasible. +- **Voting Duration**: Short (3-5 days). +- **Timelock**: Minimal (24 hours). + +### Medium DAOs (100 - 1000 Members) +A mix of active contributors and passive observers. +- **Goal**: Balance security with participation. +- **Quorum**: Moderate (e.g., 10-15%). +- **Voting Duration**: Moderate (7 days). +- **Timelock**: Moderate (2-3 days). + +### Large DAOs (1000+ Members) +High degree of voter apathy and diverse interests. +- **Goal**: Prevent gridlock while maintaining security. +- **Quorum**: Low (e.g., 2-5%) to avoid proposals constantly failing due to lack of turnout. +- **Voting Duration**: Long (10-14 days) to ensure enough reach. +- **Timelock**: Long (7 days) for maximum security. + +--- + +## Example Configurations + +| Size | Quorum (BPS) | Threshold (BPS) | Voting Period | Timelock | +| :--- | :--- | :--- | :--- | :--- | +| **Small** | 2500 (25%) | 5100 (51%) | 25,920 ledgers (~3 days) | 8,640 ledgers (~24h) | +| **Medium** | 1000 (10%) | 5100 (51%) | 60,480 ledgers (~7 days) | 17,280 ledgers (~48h) | +| **Large** | 300 (3%) | 6000 (60%) | 120,960 ledgers (~14 days) | 60,480 ledgers (~7 days) | + +--- + +## Tradeoffs and Best Practices + +### Quorum vs. Participation +- **High Quorum**: Highly secure against hostile takeovers but risks "governance gridlock" where nothing passes due to apathy. +- **Low Quorum**: Easy to pass changes, but susceptible to "ninja voting" (small groups passing changes while others aren't looking). + +### Duration vs. Agility +- **Longer Durations**: Better for complex technical changes or high-stakes financial decisions. Give the community time to discuss on social channels. +- **Shorter Durations**: Better for operational tweaks or emergency responses. + +### Timelocks as a Safety Valve +Always use a timelock for protocol upgrades or large fund movements. A 48-72 hour window is generally considered the "goldilocks" zone for medium-sized DAOs, allowing enough time for an "emergency pause" or for users to withdraw their stake if they disagree with the outcome. From f9055f7bd652a4fedd4ca454386452bbe0d8d779 Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 06:05:50 +0000 Subject: [PATCH 12/76] feat(contracts): optimize Soroban storage for audit-registry - Implement bucketed storage to reduce ledger entries - Move idempotency nonces to temporary storage - Remove redundant reading_hash from storage values - Update and expand test suite - Include regression test test_issue_281_bucket_collision Closes #281 --- apps/contracts/audit_registry/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/contracts/audit_registry/src/lib.rs b/apps/contracts/audit_registry/src/lib.rs index 2632410..e3e83b5 100644 --- a/apps/contracts/audit_registry/src/lib.rs +++ b/apps/contracts/audit_registry/src/lib.rs @@ -488,7 +488,7 @@ mod tests { } #[test] - fn test_bucket_collision() { + fn test_issue_281_bucket_collision() { let (env, api_signer, client) = setup(); // Force hashes that likely end up in the same bucket // Our bucket ID is ((hash[0] << 8) | hash[1]) % 1024 From b9e0b9888dcc2d54bc6ff89507d80bcd58d525a5 Mon Sep 17 00:00:00 2001 From: Gina-georgina Date: Sat, 30 May 2026 13:01:03 +0000 Subject: [PATCH 13/76] feat: implement rate limiting on /api/readings #266 --- apps/web/.env.example | 4 ++ apps/web/src/app/api/readings/route.ts | 21 +++++++- apps/web/src/env.ts | 4 ++ apps/web/src/lib/cache.ts | 72 ++++++++++++++++++++------ 4 files changed, 85 insertions(+), 16 deletions(-) diff --git a/apps/web/.env.example b/apps/web/.env.example index b572aaf..1ca1be0 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -17,3 +17,7 @@ MINTER_SECRET_KEY= # Upstash Redis (optional — caching layer for certificate queries) UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io UPSTASH_REDIS_REST_TOKEN=your-token + +# Rate limiting (per meter ID) +READINGS_RATE_LIMIT_PER_MINUTE=60 +READINGS_RATE_LIMIT_WINDOW_SECONDS=60 diff --git a/apps/web/src/app/api/readings/route.ts b/apps/web/src/app/api/readings/route.ts index dcf0076..d2037a7 100644 --- a/apps/web/src/app/api/readings/route.ts +++ b/apps/web/src/app/api/readings/route.ts @@ -5,7 +5,7 @@ import { createServiceClient } from '@/lib/supabase' import { anchorReading, mintCertificates } from '@/lib/stellar' import { computeReadingHash } from '@/lib/crypto' import { kwhToStroops } from '@solarproof/stellar' -import { invalidateCert } from '@/lib/cache' +import { invalidateCert, enforceRateLimit } from '@/lib/cache' function extractErrorMessage(err: unknown): string { if (err instanceof Error) return err.message @@ -45,6 +45,25 @@ export async function POST(req: NextRequest) { } const { meter_id, kwh, timestamp, signature_hex } = parsed.data + const limit = Number(process.env.READINGS_RATE_LIMIT_PER_MINUTE ?? 60) + const windowSeconds = Number(process.env.READINGS_RATE_LIMIT_WINDOW_SECONDS ?? 60) + const rateKey = `rate:readings:${meter_id}` + + const rate = await enforceRateLimit(rateKey, limit, windowSeconds) + if (!rate.allowed) { + return NextResponse.json( + { error: 'Too many requests, please try again later' }, + { + status: 429, + headers: { + 'Retry-After': rate.resetSeconds.toString(), + 'X-RateLimit-Limit': limit.toString(), + 'X-RateLimit-Remaining': rate.remaining.toString(), + }, + } + ) + } + const db = createServiceClient() // Fetch meter + cooperative diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 82a8dff..ce3e6bb 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -5,6 +5,8 @@ export const env = createEnv({ server: { SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), MINTER_SECRET_KEY: z.string().min(56), + READINGS_RATE_LIMIT_PER_MINUTE: z.string().default('60'), + READINGS_RATE_LIMIT_WINDOW_SECONDS: z.string().default('60'), }, client: { NEXT_PUBLIC_SUPABASE_URL: z.string().url(), @@ -23,5 +25,7 @@ export const env = createEnv({ NEXT_PUBLIC_ENERGY_TOKEN_ID: process.env.NEXT_PUBLIC_ENERGY_TOKEN_ID, NEXT_PUBLIC_AUDIT_REGISTRY_ID: process.env.NEXT_PUBLIC_AUDIT_REGISTRY_ID, NEXT_PUBLIC_COMMUNITY_GOVERNANCE_ID: process.env.NEXT_PUBLIC_COMMUNITY_GOVERNANCE_ID, + READINGS_RATE_LIMIT_PER_MINUTE: process.env.READINGS_RATE_LIMIT_PER_MINUTE, + READINGS_RATE_LIMIT_WINDOW_SECONDS: process.env.READINGS_RATE_LIMIT_WINDOW_SECONDS, }, }) diff --git a/apps/web/src/lib/cache.ts b/apps/web/src/lib/cache.ts index 9258175..7fd7c8f 100644 --- a/apps/web/src/lib/cache.ts +++ b/apps/web/src/lib/cache.ts @@ -8,45 +8,63 @@ const { UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN } = process.env const CERT_TTL = 60 // seconds function redisUrl(path: string) { + if (!UPSTASH_REDIS_REST_URL) { + throw new Error('UPSTASH_REDIS_REST_URL is not configured') + } return `${UPSTASH_REDIS_REST_URL}${path}` } -async function redisGet(key: string): Promise { - if (!UPSTASH_REDIS_REST_URL) return null - const res = await fetch(redisUrl(`/get/${encodeURIComponent(key)}`), { - headers: { Authorization: `Bearer ${UPSTASH_REDIS_REST_TOKEN}` }, +async function redisFetch(path: string, options: RequestInit = {}) { + if (!UPSTASH_REDIS_REST_URL) { + throw new Error('UPSTASH_REDIS_REST_URL is not configured') + } + + const res = await fetch(redisUrl(path), { + headers: { + Authorization: `Bearer ${UPSTASH_REDIS_REST_TOKEN}`, + 'Content-Type': 'application/json', + }, cache: 'no-store', + ...options, }) + const json = await res.json() + if (!res.ok) { + throw new Error(json.error || 'Redis request failed') + } + return json +} + +async function redisGet(key: string): Promise { + const json = await redisFetch(`/get/${encodeURIComponent(key)}`) if (json.result == null) return null console.log(`[cache] HIT ${key}`) return JSON.parse(json.result) as T } async function redisSet(key: string, value: unknown, ttl: number): Promise { - if (!UPSTASH_REDIS_REST_URL) return - await fetch(redisUrl(`/set/${encodeURIComponent(key)}`), { + await redisFetch(`/set/${encodeURIComponent(key)}`, { method: 'POST', - headers: { - Authorization: `Bearer ${UPSTASH_REDIS_REST_TOKEN}`, - 'Content-Type': 'application/json', - }, body: JSON.stringify({ value: JSON.stringify(value), ex: ttl }), - cache: 'no-store', }) console.log(`[cache] SET ${key} ttl=${ttl}s`) } async function redisDel(key: string): Promise { - if (!UPSTASH_REDIS_REST_URL) return - await fetch(redisUrl(`/del/${encodeURIComponent(key)}`), { + await redisFetch(`/del/${encodeURIComponent(key)}`, { method: 'POST', - headers: { Authorization: `Bearer ${UPSTASH_REDIS_REST_TOKEN}` }, - cache: 'no-store', }) console.log(`[cache] DEL ${key}`) } +async function redisEval(script: string, keys: string[], args: (string | number)[]): Promise { + const json = await redisFetch('/eval', { + method: 'POST', + body: JSON.stringify({ script, keys, args }), + }) + return json.result as T +} + export function certCacheKey(id: string) { return `cert:${id}` } @@ -64,3 +82,27 @@ export async function setCachedCert(id: string, value: unknown): Promise { export async function invalidateCert(...ids: string[]): Promise { await Promise.all(ids.map((id) => redisDel(certCacheKey(id)))) } + +export async function enforceRateLimit( + key: string, + limit: number, + windowSeconds: number +): Promise<{ allowed: boolean; remaining: number; resetSeconds: number }> { + // Use Lua script for atomic INCR + EXPIRE + const script = ` + local current = redis.call("INCR", KEYS[1]) + if current == 1 then + redis.call("EXPIRE", KEYS[1], ARGV[1]) + end + return {current, redis.call("TTL", KEYS[1])} + ` + const [count, ttl] = await redisEval<[number, number]>(script, [key], [windowSeconds]) + + const remaining = Math.max(limit - count, 0) + + return { + allowed: count <= limit, + remaining, + resetSeconds: ttl < 0 ? 0 : ttl, + } +} From 249dd4fafc5a3540ab2b0d55c10cb7705b1e7e66 Mon Sep 17 00:00:00 2001 From: Gina-georgina Date: Sat, 30 May 2026 13:05:05 +0000 Subject: [PATCH 14/76] feat: add dark mode toggle to settings and enhance theme support #253 --- apps/web/src/app/dashboard/page.tsx | 15 ++------- apps/web/src/app/settings/page.tsx | 48 +++++++++++++++++++++++++++++ apps/web/src/components/navbar.tsx | 1 + 3 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/app/settings/page.tsx diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 718baa5..146e3d7 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -134,10 +134,7 @@ export default function DashboardPage() {

Dashboard

- {/* ------------------------------------------------------------------ */} - {/* Stat cards */} - {/* ------------------------------------------------------------------ */} -
+

Key statistics

@@ -187,10 +184,7 @@ export default function DashboardPage() {
- {/* ------------------------------------------------------------------ */} - {/* Charts */} - {/* ------------------------------------------------------------------ */} -
+

Energy charts

@@ -313,10 +307,7 @@ export default function DashboardPage() {
- {/* ------------------------------------------------------------------ */} - {/* Recent readings table */} - {/* ------------------------------------------------------------------ */} -
+

+

User settings

+
+
+
+
+

Theme

+

+ Use dark mode for better low-light readability. Your preference is saved in localStorage. +

+
+ +
+
+ +
+

+ Current theme: {theme || 'system'} +

+

+ SolarProof stores your theme selection in localStorage under solarproof-theme. +

+
+
+ + ) +} diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 4114dca..1118c6c 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -11,6 +11,7 @@ const links = [ { href: '/meters', label: 'Meters' }, { href: '/certificates', label: 'Certificates' }, { href: '/governance', label: 'Governance' }, + { href: '/settings', label: 'Settings' }, { href: '/verify', label: 'Verify' }, ] From cc85f83d86c9d83d90106b981a03aa07508868d1 Mon Sep 17 00:00:00 2001 From: Gina-georgina Date: Sat, 30 May 2026 13:07:02 +0000 Subject: [PATCH 15/76] feat: implement global and section error boundaries #256 --- apps/web/src/app/dashboard/page.tsx | 22 ++-- apps/web/src/app/layout.tsx | 11 +- apps/web/src/components/error-boundary.tsx | 112 +++++++++++++++++++++ 3 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/components/error-boundary.tsx diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 718baa5..da59036 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -16,6 +16,7 @@ import { import { useTheme } from 'next-themes' import { Zap, Award, Leaf, TrendingUp } from 'lucide-react' import { StatCardSkeleton, ChartSkeleton, TableRowSkeleton } from '@/components/skeleton' +import { SectionErrorBoundary } from '@/components/error-boundary' // --------------------------------------------------------------------------- // Types @@ -134,10 +135,8 @@ export default function DashboardPage() {

Dashboard

- {/* ------------------------------------------------------------------ */} - {/* Stat cards */} - {/* ------------------------------------------------------------------ */} -
+ +

Key statistics

@@ -186,11 +185,10 @@ export default function DashboardPage() { ) : null}
+ - {/* ------------------------------------------------------------------ */} - {/* Charts */} - {/* ------------------------------------------------------------------ */} -
+ +

Energy charts

@@ -312,11 +310,10 @@ export default function DashboardPage() { )}
+
- {/* ------------------------------------------------------------------ */} - {/* Recent readings table */} - {/* ------------------------------------------------------------------ */} -
+ +

+
) } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 71b9e96..c6c6b81 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google' import './globals.css' import { Providers } from './providers' import { Navbar } from '@/components/navbar' +import { GlobalErrorBoundary } from '@/components/error-boundary' import { Analytics } from '@vercel/analytics/next' import { SpeedInsights } from '@vercel/speed-insights/next' @@ -24,10 +25,12 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - - -
{children}
-
+ + + +
{children}
+
+
diff --git a/apps/web/src/components/error-boundary.tsx b/apps/web/src/components/error-boundary.tsx new file mode 100644 index 0000000..80e5784 --- /dev/null +++ b/apps/web/src/components/error-boundary.tsx @@ -0,0 +1,112 @@ +'use client' + +import { Component, type ErrorInfo, type ReactNode } from 'react' +import { AlertTriangle, RefreshCcw } from 'lucide-react' +import * as Sentry from '@sentry/nextjs' + +interface SectionErrorBoundaryProps { + sectionName: string + children: ReactNode +} + +interface SectionErrorBoundaryState { + hasError: boolean + error?: Error +} + +export class SectionErrorBoundary extends Component { + state: SectionErrorBoundaryState = { hasError: false } + + static getDerivedStateFromError(error: Error): SectionErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + Sentry.captureException(error, { + extra: { + section: this.props.sectionName, + componentStack: info.componentStack, + }, + }) + console.error(`Dashboard section error: ${this.props.sectionName}`, error, info) + } + + reset = () => { + this.setState({ hasError: false, error: undefined }) + } + + render() { + if (this.state.hasError) { + return ( +
+
+ ) + } + + return this.props.children + } +} + +interface GlobalErrorBoundaryProps { + children: ReactNode +} + +export class GlobalErrorBoundary extends Component { + state: SectionErrorBoundaryState = { hasError: false } + + static getDerivedStateFromError(error: Error): SectionErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + Sentry.captureException(error, { + extra: { + type: 'global', + componentStack: info.componentStack, + }, + }) + console.error('Global application error:', error, info) + } + + reset = () => { + this.setState({ hasError: false, error: undefined }) + } + + render() { + if (this.state.hasError) { + return ( +
+
+ ) + } + + return this.props.children + } +} From 4d2bde06d7e6cf59102504433c0c59a8bc6fc5d1 Mon Sep 17 00:00:00 2001 From: Gina-georgina Date: Sat, 30 May 2026 13:07:49 +0000 Subject: [PATCH 16/76] feat: add pagination and filtering to certificates #258 --- apps/web/src/app/api/certificates/route.ts | 69 ++++++ apps/web/src/app/certificates/page.tsx | 272 +++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 apps/web/src/app/api/certificates/route.ts create mode 100644 apps/web/src/app/certificates/page.tsx diff --git a/apps/web/src/app/api/certificates/route.ts b/apps/web/src/app/api/certificates/route.ts new file mode 100644 index 0000000..eecddea --- /dev/null +++ b/apps/web/src/app/api/certificates/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createServiceClient } from '@/lib/supabase' + +const CertificatesQuerySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(50).default(10), + status: z.enum(['active', 'retired']).optional(), + meterId: z.string().uuid().optional(), + startDate: z.string().optional(), + endDate: z.string().optional(), +}) + +export async function GET(req: NextRequest) { + const { searchParams } = req.nextUrl + const parsed = CertificatesQuerySchema.parse({ + page: searchParams.get('page'), + pageSize: searchParams.get('pageSize'), + status: searchParams.get('status') || undefined, + meterId: searchParams.get('meterId') || undefined, + startDate: searchParams.get('startDate') || undefined, + endDate: searchParams.get('endDate') || undefined, + }) + + const db = createServiceClient() + let query = db + .from('certificates') + .select('*, readings!inner(meter_id)', { count: 'exact' }) + + if (parsed.status === 'active') { + query = query.eq('retired', false) + } + + if (parsed.status === 'retired') { + query = query.eq('retired', true) + } + + if (parsed.startDate) { + query = query.gte('issued_at', parsed.startDate) + } + + if (parsed.endDate) { + query = query.lte('issued_at', parsed.endDate) + } + + if (parsed.meterId) { + query = query.eq('readings.meter_id', parsed.meterId) + } + + const start = (parsed.page - 1) * parsed.pageSize + const end = parsed.page * parsed.pageSize - 1 + const { data, count, error } = await query.range(start, end) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + const certificates = (data ?? []).map((certificate: any) => ({ + ...certificate, + meter_id: certificate.readings?.meter_id ?? null, + })) + + return NextResponse.json({ + certificates, + total: count ?? 0, + page: parsed.page, + pageSize: parsed.pageSize, + }) +} diff --git a/apps/web/src/app/certificates/page.tsx b/apps/web/src/app/certificates/page.tsx new file mode 100644 index 0000000..8559f49 --- /dev/null +++ b/apps/web/src/app/certificates/page.tsx @@ -0,0 +1,272 @@ +'use client' + +import { useMemo, useState, useEffect, type FormEvent } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useRouter, useSearchParams } from 'next/navigation' +import { ArrowLeft, ArrowRight, Filter, Zap } from 'lucide-react' + +interface Certificate { + id: string + meter_id: string | null + reading_id: string + reading_hash: string + mint_tx_hash: string + anchor_tx_hash: string + kwh: number + issued_at: string + retired: boolean + retired_at: string | null + retired_by: string | null +} + +interface CertificateResponse { + certificates: Certificate[] + total: number + page: number + pageSize: number +} + +function normalizeSearchParams(searchParams: URLSearchParams) { + return { + page: Number(searchParams.get('page') ?? 1), + pageSize: Number(searchParams.get('pageSize') ?? 10), + status: searchParams.get('status') ?? '', + meterId: searchParams.get('meterId') ?? '', + startDate: searchParams.get('startDate') ?? '', + endDate: searchParams.get('endDate') ?? '', + } +} + +export default function CertificatesPage() { + const router = useRouter() + const searchParams = useSearchParams() + const [formState, setFormState] = useState(() => normalizeSearchParams(new URLSearchParams())) + + const params = useMemo(() => normalizeSearchParams(searchParams ?? new URLSearchParams()), [searchParams]) + + useEffect(() => { + setFormState(params) + }, [params]) + + const queryString = useMemo(() => { + const next = new URLSearchParams() + next.set('page', String(params.page)) + next.set('pageSize', String(params.pageSize)) + if (params.status) next.set('status', params.status) + if (params.meterId) next.set('meterId', params.meterId) + if (params.startDate) next.set('startDate', params.startDate) + if (params.endDate) next.set('endDate', params.endDate) + return next.toString() + }, [params]) + + const { + data, + isLoading, + error, + } = useQuery({ + queryKey: ['certificates', queryString], + queryFn: async () => { + const response = await fetch(`/api/certificates?${queryString}`) + if (!response.ok) { + throw new Error('Failed to load certificates') + } + return response.json() + }, + keepPreviousData: true, + }) + + function applyFilters(event: FormEvent) { + event.preventDefault() + const next = new URLSearchParams() + next.set('page', '1') + next.set('pageSize', String(formState.pageSize)) + if (formState.status) next.set('status', formState.status) + if (formState.meterId) next.set('meterId', formState.meterId) + if (formState.startDate) next.set('startDate', formState.startDate) + if (formState.endDate) next.set('endDate', formState.endDate) + router.push(`/certificates?${next.toString()}`) + } + + function updatePage(page: number) { + const next = new URLSearchParams(searchParams ?? new URLSearchParams()) + next.set('page', String(page)) + router.push(`/certificates?${next.toString()}`) + } + + return ( +
+
+
+

Certificates

+

+ Browse certificates with pagination, filtering, and status controls. +

+
+
+
+
+ +
+
+ + + + + +
+ +
+ + +
+ +
+ +

+ Showing {data?.certificates.length ?? 0} of {data?.total ?? 0} certificates. +

+
+
+ + {error && ( +

+ Unable to load certificates. Please try again. +

+ )} + +
+
+ + + + + + + + + + + + + {isLoading ? ( + Array.from({ length: params.pageSize }).map((_, index) => ( + + + )) + ) : data && data.certificates.length > 0 ? ( + data.certificates.map((certificate) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
IDMeter IDStatuskWhIssuedAnchor tx
+
{certificate.id}{certificate.meter_id ?? 'N/A'} + + {certificate.retired ? 'Retired' : 'Active'} + + {certificate.kwh} + {new Date(certificate.issued_at).toLocaleDateString()} + {certificate.anchor_tx_hash}
+ No certificates match the current filter. +
+
+
+ +
+ + + Page {params.page} of {Math.max(1, Math.ceil((data?.total ?? 0) / params.pageSize))} + + +
+
+ ) +} From b0ac31ba9c7ff96f414777c4c0459fdd46084b9a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 13:19:53 +0000 Subject: [PATCH 17/76] chore(release): 1.10.0 [skip ci] ## [1.10.0](https://github.com/AnnabelJoe/solarproof/compare/v1.9.0...v1.10.0) (2026-05-30) ### Features * add pagination and filtering to certificates [#258](https://github.com/AnnabelJoe/solarproof/issues/258) ([4d2bde0](https://github.com/AnnabelJoe/solarproof/commit/4d2bde06d7e6cf59102504433c0c59a8bc6fc5d1)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df51fe..1098a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.10.0](https://github.com/AnnabelJoe/solarproof/compare/v1.9.0...v1.10.0) (2026-05-30) + +### Features + +* add pagination and filtering to certificates [#258](https://github.com/AnnabelJoe/solarproof/issues/258) ([4d2bde0](https://github.com/AnnabelJoe/solarproof/commit/4d2bde06d7e6cf59102504433c0c59a8bc6fc5d1)) + ## [1.9.0](https://github.com/AnnabelJoe/solarproof/compare/v1.8.2...v1.9.0) (2026-05-29) ### Features From f4a94b27b77738d76afe7ad86ef4cd1d18e9933b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 13:21:33 +0000 Subject: [PATCH 18/76] chore(release): 1.11.0 [skip ci] ## [1.11.0](https://github.com/AnnabelJoe/solarproof/compare/v1.10.0...v1.11.0) (2026-05-30) ### Features * implement global and section error boundaries [#256](https://github.com/AnnabelJoe/solarproof/issues/256) ([cc85f83](https://github.com/AnnabelJoe/solarproof/commit/cc85f83d86c9d83d90106b981a03aa07508868d1)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1098a81..39f6ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.11.0](https://github.com/AnnabelJoe/solarproof/compare/v1.10.0...v1.11.0) (2026-05-30) + +### Features + +* implement global and section error boundaries [#256](https://github.com/AnnabelJoe/solarproof/issues/256) ([cc85f83](https://github.com/AnnabelJoe/solarproof/commit/cc85f83d86c9d83d90106b981a03aa07508868d1)) + ## [1.10.0](https://github.com/AnnabelJoe/solarproof/compare/v1.9.0...v1.10.0) (2026-05-30) ### Features From 46773090d495a6768d9ae503476dfdd3038d38fd Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 13:22:49 +0000 Subject: [PATCH 19/76] chore(release): 1.12.0 [skip ci] ## [1.12.0](https://github.com/AnnabelJoe/solarproof/compare/v1.11.0...v1.12.0) (2026-05-30) ### Features * add dark mode toggle to settings and enhance theme support [#253](https://github.com/AnnabelJoe/solarproof/issues/253) ([249dd4f](https://github.com/AnnabelJoe/solarproof/commit/249dd4fafc5a3540ab2b0d55c10cb7705b1e7e66)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f6ef9..3fa56cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.12.0](https://github.com/AnnabelJoe/solarproof/compare/v1.11.0...v1.12.0) (2026-05-30) + +### Features + +* add dark mode toggle to settings and enhance theme support [#253](https://github.com/AnnabelJoe/solarproof/issues/253) ([249dd4f](https://github.com/AnnabelJoe/solarproof/commit/249dd4fafc5a3540ab2b0d55c10cb7705b1e7e66)) + ## [1.11.0](https://github.com/AnnabelJoe/solarproof/compare/v1.10.0...v1.11.0) (2026-05-30) ### Features From 580a52cea920f2827f8b4e502ebaf108792c509c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 14:14:18 +0000 Subject: [PATCH 20/76] chore(release): 1.13.0 [skip ci] ## [1.13.0](https://github.com/AnnabelJoe/solarproof/compare/v1.12.0...v1.13.0) (2026-05-30) ### Features * implement rate limiting on /api/readings [#266](https://github.com/AnnabelJoe/solarproof/issues/266) ([b9e0b98](https://github.com/AnnabelJoe/solarproof/commit/b9e0b9888dcc2d54bc6ff89507d80bcd58d525a5)) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa56cd..4fae4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.13.0](https://github.com/AnnabelJoe/solarproof/compare/v1.12.0...v1.13.0) (2026-05-30) + +### Features + +* implement rate limiting on /api/readings [#266](https://github.com/AnnabelJoe/solarproof/issues/266) ([b9e0b98](https://github.com/AnnabelJoe/solarproof/commit/b9e0b9888dcc2d54bc6ff89507d80bcd58d525a5)) + ## [1.12.0](https://github.com/AnnabelJoe/solarproof/compare/v1.11.0...v1.12.0) (2026-05-30) ### Features From 1b86929104a49fadd49b6894eb43c639f940b283 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 14:17:30 +0000 Subject: [PATCH 21/76] chore(release): 1.14.0 [skip ci] ## [1.14.0](https://github.com/AnnabelJoe/solarproof/compare/v1.13.0...v1.14.0) (2026-05-30) ### Features * **contracts:** optimize Soroban storage for audit-registry ([4def0e7](https://github.com/AnnabelJoe/solarproof/commit/4def0e722cfe10ae0d5daeb4965ea2597f8ca836)), closes [#281](https://github.com/AnnabelJoe/solarproof/issues/281) ### Documentation * **docs:** write governance parameter tuning guide ([cd9a68c](https://github.com/AnnabelJoe/solarproof/commit/cd9a68c28eeede3309d23000487f682ec12d40c6)), closes [#279](https://github.com/AnnabelJoe/solarproof/issues/279) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fae4b4..2df72dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.14.0](https://github.com/AnnabelJoe/solarproof/compare/v1.13.0...v1.14.0) (2026-05-30) + +### Features + +* **contracts:** optimize Soroban storage for audit-registry ([4def0e7](https://github.com/AnnabelJoe/solarproof/commit/4def0e722cfe10ae0d5daeb4965ea2597f8ca836)), closes [#281](https://github.com/AnnabelJoe/solarproof/issues/281) + +### Documentation + +* **docs:** write governance parameter tuning guide ([cd9a68c](https://github.com/AnnabelJoe/solarproof/commit/cd9a68c28eeede3309d23000487f682ec12d40c6)), closes [#279](https://github.com/AnnabelJoe/solarproof/issues/279) + ## [1.13.0](https://github.com/AnnabelJoe/solarproof/compare/v1.12.0...v1.13.0) (2026-05-30) ### Features From 52e376cbaf526d124bed1d44fd18c886da373309 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 30 May 2026 14:18:00 +0000 Subject: [PATCH 22/76] chore(release): 1.15.0 [skip ci] ## [1.15.0](https://github.com/AnnabelJoe/solarproof/compare/v1.14.0...v1.15.0) (2026-05-30) ### Features * **contracts:** optimize Soroban storage for audit-registry ([f9055f7](https://github.com/AnnabelJoe/solarproof/commit/f9055f7bd652a4fedd4ca454386452bbe0d8d779)), closes [#281](https://github.com/AnnabelJoe/solarproof/issues/281) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df72dc..3a4be2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.15.0](https://github.com/AnnabelJoe/solarproof/compare/v1.14.0...v1.15.0) (2026-05-30) + +### Features + +* **contracts:** optimize Soroban storage for audit-registry ([f9055f7](https://github.com/AnnabelJoe/solarproof/commit/f9055f7bd652a4fedd4ca454386452bbe0d8d779)), closes [#281](https://github.com/AnnabelJoe/solarproof/issues/281) + ## [1.14.0](https://github.com/AnnabelJoe/solarproof/compare/v1.13.0...v1.14.0) (2026-05-30) ### Features From 8281f0867d5d420b5b501b80d74d07326f8e14bd Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 14:40:14 +0000 Subject: [PATCH 23/76] docs: standardize CHANGELOG.md and update PR template #312 --- .github/PULL_REQUEST_TEMPLATE.md | 1 + CHANGELOG.md | 262 ++++++++----------------------- 2 files changed, 70 insertions(+), 193 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3ea4c78..ea0376a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,5 +14,6 @@ Closes # - [ ] Tests pass - [ ] No new lint warnings - [ ] Docs updated if needed +- [ ] CHANGELOG.md updated - [ ] PR targets `develop` - [ ] Supabase queries audited for SQL injection (no raw SQL, parameterized methods used) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df51fe..4ec740d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,193 +1,3 @@ -## [1.9.0](https://github.com/AnnabelJoe/solarproof/compare/v1.8.2...v1.9.0) (2026-05-29) - -### Features - -* configure log aggregation and retention ([#299](https://github.com/AnnabelJoe/solarproof/issues/299)) ([3ee0154](https://github.com/AnnabelJoe/solarproof/commit/3ee0154492cccc7ce78c61925c0b72c44120fdf4)) - -## [1.8.2](https://github.com/AnnabelJoe/solarproof/compare/v1.8.1...v1.8.2) (2026-05-29) - -### Bug Fixes - -* use checked arithmetic in energy_token to prevent overflow ([#277](https://github.com/AnnabelJoe/solarproof/issues/277)) ([f526e26](https://github.com/AnnabelJoe/solarproof/commit/f526e268b3b8ee607e55248554d9ec7369831332)) - -## [1.8.1](https://github.com/AnnabelJoe/solarproof/compare/v1.8.0...v1.8.1) (2026-05-29) - -### Bug Fixes - -* add replay attack protection to audit_registry contract ([#280](https://github.com/AnnabelJoe/solarproof/issues/280)) ([b4e10b7](https://github.com/AnnabelJoe/solarproof/commit/b4e10b7e45157b6c98a2da83a2940d069b9d986d)) - -## [1.8.0](https://github.com/AnnabelJoe/solarproof/compare/v1.7.1...v1.8.0) (2026-05-29) - -### Features - -* automate Stellar Testnet faucet funding in CI ([#303](https://github.com/AnnabelJoe/solarproof/issues/303)) ([25598db](https://github.com/AnnabelJoe/solarproof/commit/25598db91671f6fe7ce19ec58b84b8e52be73517)) - -### Documentation - -* **adr:** add ADR-005 monorepo structure and ADR-006 certificate retirement model ([d7c679a](https://github.com/AnnabelJoe/solarproof/commit/d7c679aff638315c81a9ed8444b24bf5fa4c02ad)), closes [#311](https://github.com/AnnabelJoe/solarproof/issues/311) - -## [1.7.1](https://github.com/AnnabelJoe/solarproof/compare/v1.7.0...v1.7.1) (2026-05-29) - -### Bug Fixes - -* implement CSRF protection for state-changing API endpoints ([#335](https://github.com/AnnabelJoe/solarproof/issues/335)) ([2c6c8c8](https://github.com/AnnabelJoe/solarproof/commit/2c6c8c876dd56d55175a52eb35e7c55c9821efb8)) - -## [1.7.0](https://github.com/AnnabelJoe/solarproof/compare/v1.6.0...v1.7.0) (2026-05-28) - -### Features - -* **#145:** add Stellar explorer deep links for all on-chain transactions ([ccb13da](https://github.com/AnnabelJoe/solarproof/commit/ccb13da0009895ef035d757374743c9c48e01253)), closes [#145](https://github.com/AnnabelJoe/solarproof/issues/145) - -### Documentation - -* add JSDoc to all public API functions ([#316](https://github.com/AnnabelJoe/solarproof/issues/316)) ([eca0923](https://github.com/AnnabelJoe/solarproof/commit/eca092377c187939d87c11badd06ea8ad06e433f)) -* complete OpenAPI 3.0 spec for all API endpoints ([#307](https://github.com/AnnabelJoe/solarproof/issues/307)) ([e15d906](https://github.com/AnnabelJoe/solarproof/commit/e15d9065f074f1cc04ebd546fbbc3400b24ab29d)) -* document public verifier API for third-party integrations ([#313](https://github.com/AnnabelJoe/solarproof/issues/313)) ([9eb74ff](https://github.com/AnnabelJoe/solarproof/commit/9eb74ff518784611745ea27a1bc6c016b5086a06)) - -## [1.6.0](https://github.com/AnnabelJoe/solarproof/compare/v1.5.0...v1.6.0) (2026-05-28) - -### Features - -* implement SEP-41 approve/allowance/transfer_from ([#286](https://github.com/AnnabelJoe/solarproof/issues/286)) ([e3c6fce](https://github.com/AnnabelJoe/solarproof/commit/e3c6fcec3bb7bc1b11178bae340749d944836123)) - -### Documentation - -* document Ed25519 meter signing protocol and key lifecycle ([#309](https://github.com/AnnabelJoe/solarproof/issues/309)) ([cbfd1be](https://github.com/AnnabelJoe/solarproof/commit/cbfd1be6e720bc1c19c08d0597259576e8bf6b77)) - -## [1.5.0](https://github.com/AnnabelJoe/solarproof/compare/v1.4.0...v1.5.0) (2026-05-28) - -### Features - -* **api:** add Idempotency-Key header support to readings API ([e4bccbe](https://github.com/AnnabelJoe/solarproof/commit/e4bccbe3fe16471654445d4dd1a9a5f8fb323030)), closes [#267](https://github.com/AnnabelJoe/solarproof/issues/267) - -## [1.4.0](https://github.com/AnnabelJoe/solarproof/compare/v1.3.0...v1.4.0) (2026-05-28) - -### Features - -* **observability:** add OpenTelemetry APM instrumentation ([670c77a](https://github.com/AnnabelJoe/solarproof/commit/670c77a9dc90d7fe7475cde001c451725797243a)), closes [#291](https://github.com/AnnabelJoe/solarproof/issues/291) - -### Documentation - -* enhance developer onboarding guide ([ff7de25](https://github.com/AnnabelJoe/solarproof/commit/ff7de25516744e97733fc8fd724fc066a6bcacd9)), closes [#308](https://github.com/AnnabelJoe/solarproof/issues/308) - -## [1.3.0](https://github.com/AnnabelJoe/solarproof/compare/v1.2.0...v1.3.0) (2026-05-28) - -### Features - -* add /api/health and /api/ready endpoints ([#275](https://github.com/AnnabelJoe/solarproof/issues/275)) ([4f761a4](https://github.com/AnnabelJoe/solarproof/commit/4f761a428a2c3a9cb239d8ee1c2f7150d73acfe1)) - -### Documentation - -* document pnpm --frozen-lockfile requirement ([#302](https://github.com/AnnabelJoe/solarproof/issues/302)) ([4072e11](https://github.com/AnnabelJoe/solarproof/commit/4072e11a05c90cf952132bfd1b7b96d514021f1b)) - -## [1.2.0](https://github.com/AnnabelJoe/solarproof/compare/v1.1.0...v1.2.0) (2026-05-28) - -### Features - -* add governance voting UI ([#265](https://github.com/AnnabelJoe/solarproof/issues/265)) ([b59d23a](https://github.com/AnnabelJoe/solarproof/commit/b59d23a57fe4ba0c2e671f9ba4022ab2ea027ebb)) - -## [1.1.0](https://github.com/AnnabelJoe/solarproof/compare/v1.0.0...v1.1.0) (2026-05-28) - -### Features - -* responsive dashboard, certificate detail page, toast notifications, and accessibility improvements ([704c0a5](https://github.com/AnnabelJoe/solarproof/commit/704c0a5dc414eaeb4d15da15d4579a10f1bc076a)) - -## 1.0.0 (2026-05-19) - -### Features - -* **#12:** add toast notification system for transaction feedback ([c3ae97c](https://github.com/AnnabelJoe/solarproof/commit/c3ae97c0a0ff2bf5adf9115aa15e890686d85002)), closes [#12](https://github.com/AnnabelJoe/solarproof/issues/12) -* **#13:** implement certificate retirement flow in the UI ([093620a](https://github.com/AnnabelJoe/solarproof/commit/093620afbd16c89f5f0b3f3b4b092a6c6858d4f1)), closes [#13](https://github.com/AnnabelJoe/solarproof/issues/13) -* **#14:** add chart visualizations for energy generation over time ([5a35ae7](https://github.com/AnnabelJoe/solarproof/commit/5a35ae7263925dc9af3e7f1fc4467485838136ca)), closes [#14](https://github.com/AnnabelJoe/solarproof/issues/14) -* add /certificate/[id] chain-of-custody detail page ([cb622a8](https://github.com/AnnabelJoe/solarproof/commit/cb622a8eb3b7977627674f1772a1eedc6f3ca981)) -* add audit registry deduplication + local Soroban integration scripts ([3db8f31](https://github.com/AnnabelJoe/solarproof/commit/3db8f31344c4929d1b7c30b2b5d7d047e75ef521)) -* add automated CodeQL security scanning ([#88](https://github.com/AnnabelJoe/solarproof/issues/88)) ([8021a4f](https://github.com/AnnabelJoe/solarproof/commit/8021a4f4da2650681931c97af63a07069b7dedd6)) -* add copy-to-clipboard functionality for IDs and hashes ([c35dd6a](https://github.com/AnnabelJoe/solarproof/commit/c35dd6aec0ebf160773d37fca1d0b9fe6e089a7f)), closes [#23](https://github.com/AnnabelJoe/solarproof/issues/23) -* add Docker Compose setup for local development ([#82](https://github.com/AnnabelJoe/solarproof/issues/82)) ([6c6fb8b](https://github.com/AnnabelJoe/solarproof/commit/6c6fb8be3ec44d352dc8535251c00adb0d5379d8)) -* add npm and cargo audit to CI pipeline ([#91](https://github.com/AnnabelJoe/solarproof/issues/91)) ([24549f0](https://github.com/AnnabelJoe/solarproof/commit/24549f054f0dc39da01ac3f36ca4d6bf2d6da508)) -* add OpenAPI spec, /api/docs endpoint, Swagger UI, and CI validation ([#107](https://github.com/AnnabelJoe/solarproof/issues/107)) ([ed079cf](https://github.com/AnnabelJoe/solarproof/commit/ed079cf9d41213e5165e344235af97c9fa30f322)) -* add POST /api/readings/batch endpoint ([514e01f](https://github.com/AnnabelJoe/solarproof/commit/514e01f2765186c0ee1ce90da6f2590f5cfaa29b)) -* add real-time dashboard updates with WebSocket support ([091499c](https://github.com/AnnabelJoe/solarproof/commit/091499c5f355201c159d4afa13bb45d1f5c6bb77)), closes [#9](https://github.com/AnnabelJoe/solarproof/issues/9) -* add Redis/Upstash caching for certificate queries ([#43](https://github.com/AnnabelJoe/solarproof/issues/43)) ([a7f7583](https://github.com/AnnabelJoe/solarproof/commit/a7f7583f4036a1e32010ebfab1a94173b2769898)) -* add Vercel Analytics and Speed Insights ([#94](https://github.com/AnnabelJoe/solarproof/issues/94)) ([e79473c](https://github.com/AnnabelJoe/solarproof/commit/e79473cfa2752867a1a4bd5efde09f66d4e878b3)) -* **api:** append-only audit_log for operator actions ([767e161](https://github.com/AnnabelJoe/solarproof/commit/767e16117bef0576b71fc6ec5004789a39769471)), closes [#44](https://github.com/AnnabelJoe/solarproof/issues/44) -* **api:** implement CORS policy for API routes ([235e9b8](https://github.com/AnnabelJoe/solarproof/commit/235e9b8046fc1af0d131c146a72bcfc977c52aee)), closes [#46](https://github.com/AnnabelJoe/solarproof/issues/46) -* **api:** implement Ed25519 signature verification in POST /api/readings ([#26](https://github.com/AnnabelJoe/solarproof/issues/26)) ([6f92870](https://github.com/AnnabelJoe/solarproof/commit/6f928706880f1e686251c4ccde2656180963f976)) -* **api:** implement idempotency for meter reading submissions ([#28](https://github.com/AnnabelJoe/solarproof/issues/28)) ([8c8ebc2](https://github.com/AnnabelJoe/solarproof/commit/8c8ebc20392fe015f2aea6903523a1a96ea1d3fb)) -* **api:** implement webhook notifications for certificate events ([#38](https://github.com/AnnabelJoe/solarproof/issues/38)) ([b2a5323](https://github.com/AnnabelJoe/solarproof/commit/b2a5323ee4f15b2e36263fbad7c2c128ccbad911)) -* **api:** retry with exponential backoff for anchor/mint transactions ([89294a1](https://github.com/AnnabelJoe/solarproof/commit/89294a132c7b5551fca7aff3f0d50d52cf44d763)), closes [#31](https://github.com/AnnabelJoe/solarproof/issues/31) -* **auth:** implement JWT + Supabase Auth for operator routes ([#40](https://github.com/AnnabelJoe/solarproof/issues/40)) ([94311fc](https://github.com/AnnabelJoe/solarproof/commit/94311fc9129580848ea0c1a3cd30b057b1dea687)) -* **backup:** daily pg_dump to S3 with 30-day retention and Slack alerts ([#90](https://github.com/AnnabelJoe/solarproof/issues/90)) ([bb7d2de](https://github.com/AnnabelJoe/solarproof/commit/bb7d2deeb7b3606ec8f80288526db678673e3e49)) -* bitmap vote storage for community_governance ([#71](https://github.com/AnnabelJoe/solarproof/issues/71)) ([6b9378d](https://github.com/AnnabelJoe/solarproof/commit/6b9378d6ff6ef975fd72e9a0f9abc3a1fd6f7433)) -* configurable quorum_bps and threshold_bps in community_governance ([#64](https://github.com/AnnabelJoe/solarproof/issues/64)) ([9cb685b](https://github.com/AnnabelJoe/solarproof/commit/9cb685b1bf742412e25e9d267b35ec5fceab2fda)) -* configure Vercel preview deployments for every PR ([#78](https://github.com/AnnabelJoe/solarproof/issues/78)) ([ad997f7](https://github.com/AnnabelJoe/solarproof/commit/ad997f7d27153d247e503636c37784ce8aa408ec)) -* **contracts:** add multisig_admin contract for 2-of-3 admin ops ([#69](https://github.com/AnnabelJoe/solarproof/issues/69)) ([740383b](https://github.com/AnnabelJoe/solarproof/commit/740383b4d550e3fda0ad82a550eb8df780c4f297)) -* **contracts:** add version tracking and migration support ([#70](https://github.com/AnnabelJoe/solarproof/issues/70)) ([b057beb](https://github.com/AnnabelJoe/solarproof/commit/b057beb8a0f86801d15285bc6b8b280aa7b0f68d)) -* **contracts:** cargo-fuzz targets for mint, anchor, vote ([30ffc81](https://github.com/AnnabelJoe/solarproof/commit/30ffc814d321a78282e2f9991e323b0d46d68cd0)), closes [#67](https://github.com/AnnabelJoe/solarproof/issues/67) -* **contracts:** emit events for mint/retire/anchor/propose/vote ([b4cc652](https://github.com/AnnabelJoe/solarproof/commit/b4cc652a828639cd1a29e26e1d98c375912ae832)), closes [#60](https://github.com/AnnabelJoe/solarproof/issues/60) -* cursor-based paginated certificate list at /certificates ([1d55113](https://github.com/AnnabelJoe/solarproof/commit/1d55113b5cd575a6a4708ba810b71c04a92b3f6f)) -* **energy_token:** implement retire() for REC compliance ([#54](https://github.com/AnnabelJoe/solarproof/issues/54)) ([842042a](https://github.com/AnnabelJoe/solarproof/commit/842042ac24abb3079172a119e59ecefcb98ef8ce)) -* env var validation at startup with @t3-oss/env-nextjs ([#79](https://github.com/AnnabelJoe/solarproof/issues/79)) ([f386b18](https://github.com/AnnabelJoe/solarproof/commit/f386b18c3fdebf5d6ab3b853c97d31c6dd2df310)) -* **governance:** add contract upgrade mechanism with 48h timelock ([f335d8c](https://github.com/AnnabelJoe/solarproof/commit/f335d8c8627abf522f3e121f6705b18905a1eb5b)), closes [#55](https://github.com/AnnabelJoe/solarproof/issues/55) -* **governance:** add proposal execution timelock ([#65](https://github.com/AnnabelJoe/solarproof/issues/65)) ([e2ed39e](https://github.com/AnnabelJoe/solarproof/commit/e2ed39eb8c4ac5e636157646718ec4f3c40b4907)) -* **health:** comprehensive health check endpoint with DB + Stellar RPC checks ([#45](https://github.com/AnnabelJoe/solarproof/issues/45)) ([c0ebc47](https://github.com/AnnabelJoe/solarproof/commit/c0ebc471243bd4b80aa56a0148c80016496a527a)) -* implement POST /api/certificates/[id]/retire ([#34](https://github.com/AnnabelJoe/solarproof/issues/34)) ([1d7e055](https://github.com/AnnabelJoe/solarproof/commit/1d7e055641e9dfa96ba0e7ba55fbd976074273d1)) -* implement SEP-41 token interface compliance for energy_token ([#61](https://github.com/AnnabelJoe/solarproof/issues/61)) ([d9788f9](https://github.com/AnnabelJoe/solarproof/commit/d9788f9690294cac690219b1d6555c9d63cce6a1)) -* implement Supabase RLS for multi-operator isolation ([#36](https://github.com/AnnabelJoe/solarproof/issues/36)) ([5e7bdf2](https://github.com/AnnabelJoe/solarproof/commit/5e7bdf2d6f677501f07f43f6384531fa04af4419)) -* implement token transfer pause mechanism ([#66](https://github.com/AnnabelJoe/solarproof/issues/66)) ([e7fc62a](https://github.com/AnnabelJoe/solarproof/commit/e7fc62a36670474926325bcd7c8ab7e8ab64ab58)) -* **infra:** set up staging environment on Vercel ([#89](https://github.com/AnnabelJoe/solarproof/issues/89)) ([7ad4dae](https://github.com/AnnabelJoe/solarproof/commit/7ad4daeecc306b586397c6b50784d316e0cff15a)) -* initial SolarProof — cryptographic renewable energy certification ([404fce6](https://github.com/AnnabelJoe/solarproof/commit/404fce6acdbc814b42293fd2328152291caf3833)) -* integrate Sentry error monitoring in Next.js app ([#83](https://github.com/AnnabelJoe/solarproof/issues/83)) ([da4fe77](https://github.com/AnnabelJoe/solarproof/commit/da4fe77ff4bbaa0eff8ac552817cc3c04e942b17)) -* load Stellar signing key from AWS Secrets Manager with rotation support ([7b81c1a](https://github.com/AnnabelJoe/solarproof/commit/7b81c1a2e8a1d669a2b0cad467dc8b296af9be59)), closes [#50](https://github.com/AnnabelJoe/solarproof/issues/50) -* **meters:** add meter management UI and API routes ([e379c87](https://github.com/AnnabelJoe/solarproof/commit/e379c87458715989fac4006598d3986d565560d0)), closes [#18](https://github.com/AnnabelJoe/solarproof/issues/18) -* **migrations:** add rollback scripts and operator_sessions migration ([#37](https://github.com/AnnabelJoe/solarproof/issues/37)) ([d0e432c](https://github.com/AnnabelJoe/solarproof/commit/d0e432cb132c442839ab4674df3454d57c6bb585)) -* mobile responsive, skeleton loaders, dark mode, ARIA a11y ([ae45c53](https://github.com/AnnabelJoe/solarproof/commit/ae45c535882691c29b5686483599448d19d42ad1)) -* **monitoring:** add uptime checks for /api/health and /verify ([#84](https://github.com/AnnabelJoe/solarproof/issues/84)) ([cd5d44e](https://github.com/AnnabelJoe/solarproof/commit/cd5d44e0161b6b23def3770d2761aa140d2acdd7)) -* optimize audit_registry storage — hash-only on-chain ([#59](https://github.com/AnnabelJoe/solarproof/issues/59)) ([6f233cd](https://github.com/AnnabelJoe/solarproof/commit/6f233cda8b2e0aebbf56bb046c2cbf74a3058dff)) -* overflow protection for energy_token mint arithmetic ([#51](https://github.com/AnnabelJoe/solarproof/issues/51)) ([9857d35](https://github.com/AnnabelJoe/solarproof/commit/9857d356cbdb2142e22005a8eede107fe89f6edb)) -* pin Rust toolchain and harden CI for Soroban contracts ([#77](https://github.com/AnnabelJoe/solarproof/issues/77)) ([d61094a](https://github.com/AnnabelJoe/solarproof/commit/d61094a4469aa22adf81fe4180d8186466e2f9b9)) -* pre-flight account and trustline checks before mint ([7de0635](https://github.com/AnnabelJoe/solarproof/commit/7de06358546a1fcfece5385c9a9397c4ec8b7c80)) -* **queue:** async Stellar transaction queue with job status API ([0fc6c1d](https://github.com/AnnabelJoe/solarproof/commit/0fc6c1d525483ae422585c51f5c5069d796c37ab)), closes [#42](https://github.com/AnnabelJoe/solarproof/issues/42) -* rate limiting, meter name, tracer-sim, verify chain-of-custody ([5b2f413](https://github.com/AnnabelJoe/solarproof/commit/5b2f413aba7ac18fc7bacca90cab0f89ec2da64e)), closes [#27](https://github.com/AnnabelJoe/solarproof/issues/27) [#30](https://github.com/AnnabelJoe/solarproof/issues/30) [#32](https://github.com/AnnabelJoe/solarproof/issues/32) [#35](https://github.com/AnnabelJoe/solarproof/issues/35) -* resolve issues [#10](https://github.com/AnnabelJoe/solarproof/issues/10) [#15](https://github.com/AnnabelJoe/solarproof/issues/15) [#16](https://github.com/AnnabelJoe/solarproof/issues/16) [#17](https://github.com/AnnabelJoe/solarproof/issues/17) — i18n, governance form, voting UI, verify stepper ([b7ecc3b](https://github.com/AnnabelJoe/solarproof/commit/b7ecc3b023a61aa935f46aae1e071e952050b9ea)) -* **scripts:** add idempotent deploy-testnet and deploy-mainnet scripts ([e7c4855](https://github.com/AnnabelJoe/solarproof/commit/e7c4855a584b8e5c9e878847436a9736d7799770)), closes [#63](https://github.com/AnnabelJoe/solarproof/issues/63) -* **stellar:** add 10s timeout and circuit breaker to all RPC calls ([866fffe](https://github.com/AnnabelJoe/solarproof/commit/866fffe5e0caddebcbc6ed3ed355deaaff198421)), closes [#41](https://github.com/AnnabelJoe/solarproof/issues/41) -* structured log aggregation via Logtail (Better Stack) ([23bc6ac](https://github.com/AnnabelJoe/solarproof/commit/23bc6acd212628b8db4385eeae8c35259e057f70)), closes [#92](https://github.com/AnnabelJoe/solarproof/issues/92) -* structured logging, API versioning, pagination, governance tests ([7b74132](https://github.com/AnnabelJoe/solarproof/commit/7b741323a974253f90206aef4b5115bd74638504)), closes [#33](https://github.com/AnnabelJoe/solarproof/issues/33) [#39](https://github.com/AnnabelJoe/solarproof/issues/39) [#47](https://github.com/AnnabelJoe/solarproof/issues/47) [#58](https://github.com/AnnabelJoe/solarproof/issues/58) -* Supabase IaC migrations and CI validation ([#95](https://github.com/AnnabelJoe/solarproof/issues/95)) ([302f23e](https://github.com/AnnabelJoe/solarproof/commit/302f23ee8dec27c6a5ffdea0bbfd7dff369d981a)) -* **web:** add custom 404 and 500 error pages ([d8d9895](https://github.com/AnnabelJoe/solarproof/commit/d8d98958bdbdae336dd5127183ae6ac311ce34b2)), closes [#22](https://github.com/AnnabelJoe/solarproof/issues/22) -* **web:** make contract addresses and network config env-configurable ([#62](https://github.com/AnnabelJoe/solarproof/issues/62)) ([b393066](https://github.com/AnnabelJoe/solarproof/commit/b393066d14b9ede145da192deac3e80a7577b92a)) - -### Bug Fixes - -* **#11:** persist Freighter wallet connection state across page refreshes ([32045d4](https://github.com/AnnabelJoe/solarproof/commit/32045d487cae237ca191e49542196a35b80776c7)), closes [#11](https://github.com/AnnabelJoe/solarproof/issues/11) -* **a11y:** improve keyboard accessibility on verify page ([1751ba4](https://github.com/AnnabelJoe/solarproof/commit/1751ba41c1f0d88d9bc0cc84b653991ca304256f)) -* add error boundaries to isolate component failures ([39f715b](https://github.com/AnnabelJoe/solarproof/commit/39f715b23df292e1f23238bbf37d93e85a2c7484)) -* add input validation to verify and retire API routes ([#29](https://github.com/AnnabelJoe/solarproof/issues/29)) ([4ea58a6](https://github.com/AnnabelJoe/solarproof/commit/4ea58a623b5efbbbba1bff7d023ed65893d40543)) -* add spinner and disable buttons during form submission ([7ef2928](https://github.com/AnnabelJoe/solarproof/commit/7ef292884c30c921125e35f8d5f5a6ed64608c1a)), closes [#21](https://github.com/AnnabelJoe/solarproof/issues/21) -* **audit-registry:** add access control to anchor() ([a042f18](https://github.com/AnnabelJoe/solarproof/commit/a042f187585493850aee021b4c4c329c9451b532)), closes [#52](https://github.com/AnnabelJoe/solarproof/issues/52) -* cargo fmt, Rust 1.88.0 toolchain, remove pnpm version conflict ([ea5a393](https://github.com/AnnabelJoe/solarproof/commit/ea5a3939b2bb065a42f88c130c4be4261412f545)) -* **ci:** commit pnpm lockfile for frozen-lockfile enforcement ([#86](https://github.com/AnnabelJoe/solarproof/issues/86)) ([0752cbd](https://github.com/AnnabelJoe/solarproof/commit/0752cbd2c335ffe4ff2dc3160c35565651da2dfa)) -* **governance:** add reentrancy guard to vote() ([0e051d2](https://github.com/AnnabelJoe/solarproof/commit/0e051d28111f263df7595da3bb9c15d592fb42a4)), closes [#53](https://github.com/AnnabelJoe/solarproof/issues/53) -* regenerate lockfile, bump rust-toolchain.toml to 1.88.0 ([158d740](https://github.com/AnnabelJoe/solarproof/commit/158d740e4f676589fd5e5e10d7b180ef56c3842a)) -* remove empty with: blocks from pnpm/action-setup steps ([b8ebced](https://github.com/AnnabelJoe/solarproof/commit/b8ebced91c374231093bde16ca98f20c65030669)) -* resolve all build, type, and lint errors ([8a67ea0](https://github.com/AnnabelJoe/solarproof/commit/8a67ea0c1af167b756621ca9e683f5b763042973)) -* resolve issues [#19](https://github.com/AnnabelJoe/solarproof/issues/19), [#20](https://github.com/AnnabelJoe/solarproof/issues/20), [#24](https://github.com/AnnabelJoe/solarproof/issues/24), [#25](https://github.com/AnnabelJoe/solarproof/issues/25) ([78448aa](https://github.com/AnnabelJoe/solarproof/commit/78448aa04a1460fe8756316666862022d11ed900)) -* secrets management - placeholder .env.example + secret scanning ([#85](https://github.com/AnnabelJoe/solarproof/issues/85)) ([17435bd](https://github.com/AnnabelJoe/solarproof/commit/17435bd161268aa661935ec2eb5f2cf576f9b0d4)) - -### Documentation - -* add API reference for all endpoints ([#96](https://github.com/AnnabelJoe/solarproof/issues/96)) ([d66dbe4](https://github.com/AnnabelJoe/solarproof/commit/d66dbe44ef4cb0e58a117be8836b67c625d68717)) -* add contract deployment guide and deployments.md ([#108](https://github.com/AnnabelJoe/solarproof/issues/108)) ([e084e5e](https://github.com/AnnabelJoe/solarproof/commit/e084e5e9d88c951aa981fa097b58ea55dc851af5)) -* add descriptions and examples to .env.example ([#105](https://github.com/AnnabelJoe/solarproof/issues/105)) ([e834f15](https://github.com/AnnabelJoe/solarproof/commit/e834f15888be68aff8ed82a2b87c824f03d0e29a)) -* add developer onboarding guide ([#97](https://github.com/AnnabelJoe/solarproof/issues/97)) ([3b77c50](https://github.com/AnnabelJoe/solarproof/commit/3b77c50d1e260cd09a589e7176c826c9ed1acb22)) -* add end-user guide for public verifier ([#106](https://github.com/AnnabelJoe/solarproof/issues/106)) ([a151440](https://github.com/AnnabelJoe/solarproof/commit/a151440df3a6c5e490b8aefb56d34de3f227a2e2)) -* **adr:** add ADR template, index, and 4 ADRs ([#99](https://github.com/AnnabelJoe/solarproof/issues/99)) ([28db52c](https://github.com/AnnabelJoe/solarproof/commit/28db52cfe55c76627cf8893adcffed5b89dc6905)) -* **contracts:** add interface and error code docs for all three contracts ([b6d39fd](https://github.com/AnnabelJoe/solarproof/commit/b6d39fdb384eb72ed8c619fb6b5b29ddb8ce32e6)), closes [#98](https://github.com/AnnabelJoe/solarproof/issues/98) -* **contracts:** add NatSpec-style doc comments to all public functions ([#68](https://github.com/AnnabelJoe/solarproof/issues/68)) ([ed9a056](https://github.com/AnnabelJoe/solarproof/commit/ed9a05610dcf8e14f172fbbc0cc717b74b8c6c7e)) -* expand CONTRIBUTING.md with branch naming, commit format, PR checklist, and review expectations ([b26a136](https://github.com/AnnabelJoe/solarproof/commit/b26a136d1b76536be00a630c3e4d5e4786126065)), closes [#100](https://github.com/AnnabelJoe/solarproof/issues/100) -* prepare contracts for security audit ([#75](https://github.com/AnnabelJoe/solarproof/issues/75)) ([295766b](https://github.com/AnnabelJoe/solarproof/commit/295766b5e8196f281cddcb483d6acb45bcbf736f)) - # Changelog All notable changes to this project will be documented in this file. @@ -204,14 +14,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- feat(contracts): implement certificate retirement in energy_token (4a22103) +- feat(contracts): optimize Soroban storage for audit-registry (f9055f7) - STRIDE-based threat model (`docs/THREAT_MODEL.md`) covering 13 attack vectors across all 6 STRIDE categories (#110) - TSDoc comments and inline explanations on all exported functions in `packages/stellar/src/index.ts`, `apps/web/src/lib/stellar.ts`, and `apps/web/src/lib/crypto.ts` (#103) - Vitest unit tests for `buildTransaction`, `anchorReading` (build_anchor_tx), `mintCertificates` (build_mint_tx), and `retireCertificate` (build_retire_tx) with mocked Stellar RPC (#118) ---- +## [1.9.0] - 2026-05-29 +### Added +- configure log aggregation and retention ([#299](https://github.com/AnnabelJoe/solarproof/issues/299)) + +## [1.8.2] - 2026-05-29 +### Fixed +- use checked arithmetic in energy_token to prevent overflow ([#277](https://github.com/AnnabelJoe/solarproof/issues/277)) -## [1.0.0] — 2026-04-21 +## [1.8.1] - 2026-05-29 +### Fixed +- add replay attack protection to audit_registry contract ([#280](https://github.com/AnnabelJoe/solarproof/issues/280)) + +## [1.8.0] - 2026-05-29 +### Added +- automate Stellar Testnet faucet funding in CI ([#303](https://github.com/AnnabelJoe/solarproof/issues/303)) +- **adr:** add ADR-005 monorepo structure and ADR-006 certificate retirement model ([#311](https://github.com/AnnabelJoe/solarproof/issues/311)) + +## [1.7.1] - 2026-05-29 +### Fixed +- implement CSRF protection for state-changing API endpoints ([#335](https://github.com/AnnabelJoe/solarproof/issues/335)) + +## [1.7.0] - 2026-05-28 +### Added +- **#145:** add Stellar explorer deep links for all on-chain transactions ([#145](https://github.com/AnnabelJoe/solarproof/issues/145)) +- add JSDoc to all public API functions ([#316](https://github.com/AnnabelJoe/solarproof/issues/316)) +- complete OpenAPI 3.0 spec for all API endpoints ([#307](https://github.com/AnnabelJoe/solarproof/issues/307)) +- document public verifier API for third-party integrations ([#313](https://github.com/AnnabelJoe/solarproof/issues/313)) + +## [1.6.0] - 2026-05-28 +### Added +- implement SEP-41 approve/allowance/transfer_from ([#286](https://github.com/AnnabelJoe/solarproof/issues/286)) +- document Ed25519 meter signing protocol and key lifecycle ([#309](https://github.com/AnnabelJoe/solarproof/issues/309)) + +## [1.5.0] - 2026-05-28 +### Added +- **api:** add Idempotency-Key header support to readings API ([#267](https://github.com/AnnabelJoe/solarproof/issues/267)) + +## [1.4.0] - 2026-05-28 +### Added +- **observability:** add OpenTelemetry APM instrumentation ([#291](https://github.com/AnnabelJoe/solarproof/issues/291)) +- enhance developer onboarding guide ([#308](https://github.com/AnnabelJoe/solarproof/issues/308)) + +## [1.3.0] - 2026-05-28 +### Added +- add /api/health and /api/ready endpoints ([#275](https://github.com/AnnabelJoe/solarproof/issues/275)) +- document pnpm --frozen-lockfile requirement ([#302](https://github.com/AnnabelJoe/solarproof/issues/302)) + +## [1.2.0] - 2026-05-28 +### Added +- add governance voting UI ([#265](https://github.com/AnnabelJoe/solarproof/issues/265)) + +## [1.1.0] - 2026-05-28 +### Added +- responsive dashboard, certificate detail page, toast notifications, and accessibility improvements (704c0a5) +## [1.0.0] - 2026-04-21 ### Added - End-to-end cryptographic proof pipeline: Ed25519 meter signing → on-chain anchor → certificate minting → retirement - Three Soroban smart contracts: `energy_token` (SEP-41), `audit_registry`, `community_governance` @@ -228,5 +92,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Architecture Decision Records (`docs/adr/`) - API reference (`docs/API.md`), deployment guide (`docs/DEPLOYMENT.md`), onboarding guide (`docs/ONBOARDING.md`) -[Unreleased]: https://github.com/AnnabelJoe/solarproof/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/AnnabelJoe/solarproof/compare/v1.9.0...HEAD +[1.9.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.8.2...v1.9.0 +[1.8.2]: https://github.com/AnnabelJoe/solarproof/compare/v1.8.1...v1.8.2 +[1.8.1]: https://github.com/AnnabelJoe/solarproof/compare/v1.8.0...v1.8.1 +[1.8.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.7.1...v1.8.0 +[1.7.1]: https://github.com/AnnabelJoe/solarproof/compare/v1.7.0...v1.7.1 +[1.7.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.6.0...v1.7.0 +[1.6.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.5.0...v1.6.0 +[1.5.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.4.0...v1.5.0 +[1.4.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.3.0...v1.4.0 +[1.3.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.2.0...v1.3.0 +[1.2.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/AnnabelJoe/solarproof/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/AnnabelJoe/solarproof/releases/tag/v1.0.0 From 88e03de9e99e46f35ab0002ed14451b23974fb53 Mon Sep 17 00:00:00 2001 From: BECKY Date: Sat, 30 May 2026 14:40:50 +0000 Subject: [PATCH 24/76] security: add dependency scanning to CI and auto-issue creation #293 --- .github/workflows/audit.yml | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index a1ad2f7..e951a4a 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -11,6 +11,7 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: npm-audit: @@ -42,6 +43,20 @@ jobs: repo: context.repo.repo, body: `### ${status} pnpm audit\n\`\`\`\n${output.slice(0, 6000)}\n\`\`\`` }); + - name: Create Issue on failure (Scheduled) + if: github.event_name == 'schedule' && steps.npm_audit.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('/tmp/npm-audit.txt', 'utf8'); + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '🚨 Weekly pnpm audit failed: High-severity vulnerabilities detected', + body: `The weekly pnpm audit found high-severity vulnerabilities in JS dependencies.\n\n### Audit Output:\n\`\`\`\n${output.slice(0, 6000)}\n\`\`\``, + labels: ['security', 'devops'] + }); - name: Fail if audit found high/critical issues if: steps.npm_audit.outcome == 'failure' run: exit 1 @@ -54,7 +69,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: "1.85.0" + toolchain: "1.88.0" - uses: Swatinem/rust-cache@v2 with: workspaces: apps/contracts @@ -79,6 +94,20 @@ jobs: repo: context.repo.repo, body: `### ${status} cargo audit\n\`\`\`\n${output.slice(0, 6000)}\n\`\`\`` }); + - name: Create Issue on failure (Scheduled) + if: github.event_name == 'schedule' && steps.cargo_audit.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('/tmp/cargo-audit.txt', 'utf8'); + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '🚨 Weekly cargo audit failed: Vulnerabilities detected', + body: `The weekly cargo audit found vulnerabilities in Rust dependencies.\n\n### Audit Output:\n\`\`\`\n${output.slice(0, 6000)}\n\`\`\``, + labels: ['security', 'devops'] + }); - name: Fail if audit found vulnerabilities if: steps.cargo_audit.outcome == 'failure' run: exit 1 From 7698660b032ca9a345cee0f37e46122e6b243ab3 Mon Sep 17 00:00:00 2001 From: ZAINAB Date: Sat, 30 May 2026 17:07:40 +0000 Subject: [PATCH 25/76] docs: create SECURITY.md with vulnerability disclosure policy (#314) --- SECURITY.md | 76 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index d602b43..a6e034f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,73 +1,87 @@ # Security Policy +SolarProof takes the security of our renewable energy infrastructure seriously. We appreciate the efforts of security researchers who help us maintain the integrity of our cryptographic proofs. + ## Supported Versions +We provide security updates for the following versions: + | Version | Supported | |---|---| | `main` branch | ✅ | | Older releases | ❌ | -We only provide security fixes for the current `main` branch. - --- ## Reporting a Vulnerability **Please do not open a public GitHub issue for security vulnerabilities.** -Report vulnerabilities by emailing: +If you discover a potential security issue, please report it to us via email: + +**[security@solarproof.dev](mailto:security@solarproof.dev)** -**security@solarproof.dev** +To help us address the issue quickly, please include: -Include as much detail as possible: +- A detailed description of the vulnerability and its potential impact. +- Step-by-step instructions to reproduce the issue (or a proof-of-concept). +- The affected component(s) (API, smart contracts, frontend, scripts). +- Any suggested remediations or mitigations. -- A description of the vulnerability and its potential impact -- Steps to reproduce or a proof-of-concept -- Affected component(s) (API, smart contracts, frontend, scripts) -- Any suggested mitigations +### Response Timeline -We will acknowledge your report within **48 hours** and aim to provide a resolution timeline within **7 days**. +- **Acknowledgment:** Within 48 hours of receipt. +- **Initial Evaluation:** Within 7 days of acknowledgment. +- **Resolution:** We aim to resolve critical issues within 14-21 days. --- ## Disclosure Process -1. You report the vulnerability privately to `security@solarproof.dev` -2. We acknowledge receipt within **48 hours** -3. We investigate and develop a fix (target: within 14 days for critical issues) -4. We coordinate a release date with you before public disclosure -5. We publish a security advisory and credit the reporter (unless you prefer to remain anonymous) +We follow [coordinated vulnerability disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure) and ask that you do the same. -We follow [responsible disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure) and ask that you do the same — please allow us reasonable time to patch before any public disclosure. +1. **Report:** You report the vulnerability privately to our security email. +2. **Evaluation:** We verify the issue and assess the risk. +3. **Fix:** We develop and test a security patch. +4. **Coordination:** We coordinate a release date with you. +5. **Disclosure:** We publish a security advisory and credit you for the discovery. --- ## Scope -The following are **in scope**: +### In Scope -- `POST /api/readings` — Ed25519 signature verification bypass -- `POST /api/certificates/[id]/retire` — unauthorized retirement -- `GET /api/verify` — data leakage or manipulation -- Soroban smart contracts (`energy_token`, `audit_registry`, `community_governance`) -- Authentication and authorization logic -- Supabase RLS policy bypasses +- **Meter Proofs:** Ed25519 signature verification bypasses. +- **Certificate Lifecycle:** Unauthorized retirement or minting of tokens. +- **Chain of Custody:** Data manipulation in the `/api/verify` or anchor registry. +- **Smart Contracts:** Vulnerabilities in `energy_token`, `audit_registry`, or `community_governance`. +- **Infrastructure:** Supabase RLS policy bypasses or authentication flaws. -The following are **out of scope**: +### Out of Scope -- Stellar testnet infrastructure (report to Stellar Foundation) -- Third-party dependencies (report upstream; we will patch promptly when fixes are available) -- Social engineering or phishing attacks -- Denial-of-service attacks without a demonstrated security impact +- Vulnerabilities in the Stellar network itself (please report to the [Stellar Foundation](https://stellar.org/security)). +- Attacks requiring physical access to a meter device (unless the attack scales to other devices). +- Social engineering, phishing, or denial-of-service (DoS) attacks. +- Third-party library vulnerabilities (unless they result from our specific usage). --- ## Bug Bounty -SolarProof does not currently operate a paid bug bounty program. We do publicly credit all responsible disclosures in our security advisories. +SolarProof does not currently operate a paid bug bounty program. However, we are happy to: + +- Publicly credit researchers in our security advisories. +- Provide a letter of appreciation for significant findings. +- Offer early access to upcoming features. --- -## PGP Key +## Encrypted Communication (PGP) + +For sensitive reports, you may use our PGP key to encrypt your email. + +**Fingerprint:** `8F3E 4D2A 1B9C 7E6D 5F4A 3B2C 1D0E 9F8A 7B6C 5D4E` (Placeholder) +**Public Key:** A link to the full public key will be provided here once the project reaches production. -A PGP key for encrypted communication will be published here once the project reaches production. In the meantime, please use the email above. +In the meantime, standard email to `security@solarproof.dev` is the preferred channel. From ce744ea7d953fd285997e0de54d2d4024adec177 Mon Sep 17 00:00:00 2001 From: ZAINAB Date: Sat, 30 May 2026 17:08:37 +0000 Subject: [PATCH 26/76] ops: configure Redis persistence and eviction policy (#294) --- docker-compose.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c6c17eb..500d375 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,9 @@ services: depends_on: supabase-db: condition: service_healthy - redis: + redis-cache: + condition: service_healthy + redis-queue: condition: service_healthy volumes: - ./apps/web:/app/apps/web @@ -44,12 +46,27 @@ services: retries: 5 start_period: 20s - redis: + redis-cache: image: redis:7-alpine + command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru ports: - "6379:6379" volumes: - - redis_data:/data + - redis_cache_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + redis-queue: + image: redis:7-alpine + command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction + ports: + - "6380:6379" + volumes: + - redis_queue_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s @@ -59,4 +76,5 @@ services: volumes: supabase_data: - redis_data: + redis_cache_data: + redis_queue_data: From ce90af9f4fe98eae5ab2897d8d4eaac96d04a3a3 Mon Sep 17 00:00:00 2001 From: ZAINAB Date: Sat, 30 May 2026 17:09:04 +0000 Subject: [PATCH 27/76] feat: implement meter key revocation mechanism (#339) --- .../src/app/api/meters/[id]/revoke/route.ts | 67 ++++++++++++++++--- apps/web/src/app/api/readings/batch/route.ts | 5 +- apps/web/src/app/api/readings/route.ts | 9 +-- apps/web/src/lib/audit.ts | 1 + .../20260530000009_meter_revocation.sql | 14 ++++ 5 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 supabase/migrations/20260530000009_meter_revocation.sql diff --git a/apps/web/src/app/api/meters/[id]/revoke/route.ts b/apps/web/src/app/api/meters/[id]/revoke/route.ts index 434ff6f..25add8d 100644 --- a/apps/web/src/app/api/meters/[id]/revoke/route.ts +++ b/apps/web/src/app/api/meters/[id]/revoke/route.ts @@ -1,22 +1,73 @@ import { NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' import { createServiceClient } from '@/lib/supabase' +import { requireAuth, isAuthError } from '@/lib/auth' +import { auditLog } from '@/lib/audit' -/** PATCH /api/meters/[id]/revoke — set meter active=false */ -export async function PATCH( - _req: NextRequest, +const RevokeSchema = z.object({ + reason: z.string().min(1).max(500), +}) + +/** + * POST /api/meters/[id]/revoke + * + * Revokes a meter's public key. Revoked meters can no longer submit readings. + * This action is permanent and recorded in the audit log. + * + * Requires operator JWT. + */ +export async function POST( + req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = await requireAuth(req) + if (isAuthError(auth)) return auth + const { id } = await params + const body = await req.json().catch(() => ({})) + const parsed = RevokeSchema.safeParse(body) + + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) + } + const db = createServiceClient() + const now = new Date().toISOString() const { data, error } = await db .from('meters') - .update({ active: false }) + .update({ + active: false, + revoked_at: now, + revocation_reason: parsed.data.reason + }) .eq('id', id) - .select() + .is('revoked_at', null) + .select('id, serial_number, pubkey_hex') .single() - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - if (!data) return NextResponse.json({ error: 'Meter not found' }, { status: 404 }) - return NextResponse.json(data) + if (error) { + if (error.code === 'PGRST116') { + return NextResponse.json({ error: 'Meter not found or already revoked' }, { status: 404 }) + } + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + // Audit logging + await auditLog(req, { + operator_id: auth.user.id, + action: 'meter.revoke', + resource_id: id, + metadata: { + serial_number: data.serial_number, + pubkey_hex: data.pubkey_hex, + reason: parsed.data.reason, + } + }) + + return NextResponse.json({ + message: 'Meter revoked successfully', + id: data.id, + revoked_at: now + }) } diff --git a/apps/web/src/app/api/readings/batch/route.ts b/apps/web/src/app/api/readings/batch/route.ts index f07a6f9..044ca7f 100644 --- a/apps/web/src/app/api/readings/batch/route.ts +++ b/apps/web/src/app/api/readings/batch/route.ts @@ -55,9 +55,10 @@ type MeterRow = { id: string; pubkey_hex: string; cooperative_id: string; cooper const meterIds = [...new Set(readings.map(r => r.meter_id))] const { data: meters } = await db .from('meters') - .select('id, pubkey_hex, cooperative_id, cooperatives(admin_address)') + .select('id, pubkey_hex, cooperative_id, revoked_at, cooperatives(admin_address)') .in('id', meterIds) - .eq('active', true) as { data: MeterRow[] | null } + .eq('active', true) + .is('revoked_at', null) as { data: (MeterRow & { revoked_at: string | null })[] | null } const meterMap = new Map((meters ?? []).map(m => [m.id, m])) diff --git a/apps/web/src/app/api/readings/route.ts b/apps/web/src/app/api/readings/route.ts index 880aef3..53efa19 100644 --- a/apps/web/src/app/api/readings/route.ts +++ b/apps/web/src/app/api/readings/route.ts @@ -162,14 +162,15 @@ export async function POST(req: NextRequest) { // Fetch meter + cooperative const { data: meter } = await db .from('meters') - .select('id, pubkey_hex, cooperative_id, cooperatives(admin_address)') + .select('id, pubkey_hex, cooperative_id, revoked_at, cooperatives(admin_address)') .eq('id', meter_id) .eq('active', true) - .single() as { data: { id: string; pubkey_hex: string; cooperative_id: string; cooperatives: { admin_address: string } | null } | null } + .is('revoked_at', null) + .single() as { data: { id: string; pubkey_hex: string; cooperative_id: string; revoked_at: string | null; cooperatives: { admin_address: string } | null } | null } if (!meter) { - log.warn('readings.post.meter_not_found', { meter_id }) - return NextResponse.json({ error: 'Meter not found or inactive' }, { status: 404 }) + log.warn('readings.post.meter_not_found_or_revoked', { meter_id }) + return NextResponse.json({ error: 'Meter not found, inactive, or revoked' }, { status: 404 }) } // Rate limit: 60 requests/minute per meter public key diff --git a/apps/web/src/lib/audit.ts b/apps/web/src/lib/audit.ts index 89f2df4..7605de5 100644 --- a/apps/web/src/lib/audit.ts +++ b/apps/web/src/lib/audit.ts @@ -6,6 +6,7 @@ export type AuditAction = | 'certificate.retire' | 'meter.register' | 'meter.deactivate' + | 'meter.revoke' interface AuditEntry { operator_id: string diff --git a/supabase/migrations/20260530000009_meter_revocation.sql b/supabase/migrations/20260530000009_meter_revocation.sql new file mode 100644 index 0000000..5648c61 --- /dev/null +++ b/supabase/migrations/20260530000009_meter_revocation.sql @@ -0,0 +1,14 @@ +-- Migration 009: meter key revocation +-- Adds fields to track revocation of compromised meter keys. + +ALTER TABLE meters +ADD COLUMN revoked_at timestamptz, +ADD COLUMN revocation_reason text; + +-- Index for performance when checking active meters +CREATE INDEX idx_meters_active_revoked ON meters (id) WHERE active = true AND revoked_at IS NULL; + +-- Update existing audit_action enum if it exists (Supabase/Postgres) +-- Note: In Supabase, we often use text for action, but let's check if it's an enum. +-- Based on apps/web/src/lib/audit.ts, it seems to be handled in application logic, +-- but the database table might have a check constraint or just text. From 3691090e70bd8113ecb542f6ffef90ac9d753e10 Mon Sep 17 00:00:00 2001 From: ZAINAB Date: Sat, 30 May 2026 17:09:30 +0000 Subject: [PATCH 28/76] test: add snapshot tests for React components and fix Navbar (#328) --- apps/web/package.json | 1 + .../__snapshots__/snapshot.test.tsx.snap | 237 ++++++++++ .../__tests__/components/snapshot.test.tsx | 115 ++++- apps/web/src/components/navbar.tsx | 40 +- pnpm-lock.yaml | 405 +++++++++++++++++- 5 files changed, 769 insertions(+), 29 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 68af2c9..d8f80db 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,6 +25,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.577.0", "next": "15.5.18", + "next-intl": "^4.13.0", "next-themes": "^0.4.4", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/apps/web/src/__tests__/components/__snapshots__/snapshot.test.tsx.snap b/apps/web/src/__tests__/components/__snapshots__/snapshot.test.tsx.snap index 43730e9..4214805 100644 --- a/apps/web/src/__tests__/components/__snapshots__/snapshot.test.tsx.snap +++ b/apps/web/src/__tests__/components/__snapshots__/snapshot.test.tsx.snap @@ -1,5 +1,111 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Copy components snapshots > CopyButton renders correctly 1`] = ` + +`; + +exports[`Copy components snapshots > CopyableText renders correctly (mono) 1`] = ` + + + 0x1234567890 + + + +`; + +exports[`Copy components snapshots > CopyableText renders correctly (non-mono) 1`] = ` + + + test + + + +`; + +exports[`LanguageSwitcher snapshots > LanguageSwitcher renders correctly 1`] = ` +
+ +
+`; + exports[`MeterReadingRow snapshots > pending (unverified) reading row renders correctly 1`] = ` @@ -70,6 +176,137 @@ exports[`MeterReadingRow snapshots > verified reading row renders correctly 1`]
`; +exports[`Navbar snapshots > Navbar renders correctly 1`] = ` + +`; + exports[`Skeleton components snapshots > ChartSkeleton renders correctly (no title) 1`] = `
({ + usePathname: () => '/', + useRouter: () => ({ + refresh: vi.fn(), + push: vi.fn(), + replace: vi.fn(), + }), + useTransition: () => [false, vi.fn()], +})) + +vi.mock('next-themes', () => ({ + useTheme: () => ({ + resolvedTheme: 'light', + setTheme: vi.fn(), + }), +})) + +vi.mock('@/hooks/useWallet', () => ({ + useWallet: () => ({ + address: 'GABC...XYZ', + connected: true, + loading: false, + connect: vi.fn(), + disconnect: vi.fn(), + }), +})) + +vi.mock('@/env', () => ({ + env: { + NEXT_PUBLIC_STELLAR_NETWORK: 'testnet', + }, +})) + +vi.mock('next-intl', () => ({ + useTranslations: (namespace: string) => (key: string) => `${namespace}.${key}`, +})) + +// Mock lucide-react to avoid random IDs in snapshots +vi.mock('lucide-react', async () => { + const actual = await vi.importActual('lucide-react') + return { + ...actual, + Sun: () =>
, + Moon: () =>
, + Menu: () =>
, + X: () =>
, + Wallet: () =>
, + LogOut: () =>
, + Copy: () =>
, + Check: () =>
, + CheckCircle: () =>
, + XCircle: () =>
, + Loader2: () =>
, + } +}) describe('Skeleton components snapshots', () => { it('Skeleton renders correctly', () => { @@ -110,3 +165,45 @@ describe('MeterReadingRow snapshots', () => { expect(container.firstChild).toMatchSnapshot() }) }) + +describe('Copy components snapshots', () => { + it('CopyButton renders correctly', () => { + const { container } = render() + expect(container.firstChild).toMatchSnapshot() + }) + + it('CopyableText renders correctly (mono)', () => { + const { container } = render() + expect(container.firstChild).toMatchSnapshot() + }) + + it('CopyableText renders correctly (non-mono)', () => { + const { container } = render() + expect(container.firstChild).toMatchSnapshot() + }) +}) + +describe('LanguageSwitcher snapshots', () => { + it('LanguageSwitcher renders correctly', () => { + const { container } = render() + expect(container.firstChild).toMatchSnapshot() + }) +}) + +describe('Toast components snapshots', () => { + it('Empty ToastContainer renders nothing', () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + }) +}) + +describe('Navbar snapshots', () => { + it('Navbar renders correctly', () => { + const { container } = render() + expect(container.firstChild).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index b0936ef..69d980d 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -7,11 +7,24 @@ import { useTheme } from 'next-themes' import { useEffect, useRef, useState } from 'react' import { useWallet } from '@/hooks/useWallet' import { env } from '@/env' +import { CopyButton } from '@/components/copy-button' +import { LanguageSwitcher } from '@/components/language-switcher' + +import { useTranslations } from 'next-intl' +import type { Locale } from '@/lib/locales' interface NavbarProps { locale: Locale } +const links = [ + { href: '/', labelKey: 'dashboard' }, + { href: '/meters', labelKey: 'meters' }, + { href: '/certificates', labelKey: 'certificates' }, + { href: '/governance', labelKey: 'governance' }, + { href: '/verify', labelKey: 'verify' }, +] + const network = env.NEXT_PUBLIC_STELLAR_NETWORK function NetworkBadge() { @@ -37,7 +50,8 @@ function NetworkBadge() { ) } -export function Navbar() { +export function Navbar({ locale }: NavbarProps) { + const t = useTranslations('nav') const pathname = usePathname() const { resolvedTheme, setTheme } = useTheme() const [mounted, setMounted] = useState(false) @@ -51,6 +65,10 @@ export function Navbar() { setMounted(true) }, []) + function toggleTheme() { + setTheme(resolvedTheme === 'dark' ? 'light' : 'dark') + } + // Close menu on route change useEffect(() => { setMenuOpen(false) }, [pathname]) @@ -126,7 +144,7 @@ export function Navbar() { : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100' }`} > - {l.label} + {t(l.labelKey as any)} ) })} @@ -148,7 +166,7 @@ export function Navbar() {
- -
{/* Mobile menu */} @@ -235,7 +243,7 @@ export function Navbar() { : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100' }`} > - {l.label} + {t(l.labelKey as any)} ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d223418..f44cde9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: next: specifier: 15.5.18 version: 15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next-intl: + specifier: ^4.13.0 + version: 4.13.0(next@15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(typescript@5.9.3) next-themes: specifier: ^0.4.4 version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -141,7 +144,7 @@ importers: devDependencies: tsup: specifier: ^8.3.5 - version: 8.5.1(postcss@8.5.14)(typescript@5.9.3) + version: 8.5.1(@swc/core@1.15.40)(postcss@8.5.14)(typescript@5.9.3) typescript: specifier: ^5.6.3 version: 5.9.3 @@ -638,6 +641,18 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@formatjs/fast-memoize@3.1.5': + resolution: {integrity: sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==} + + '@formatjs/icu-messageformat-parser@3.5.10': + resolution: {integrity: sha512-XeJihYLy1lCe19xfK1KWKG/betBOK2rB0luL8lSkjfvJj0zP+LTJvkC+RKd0jsFI8mWxN71LrarHSrEXE8xxOQ==} + + '@formatjs/icu-skeleton-parser@2.1.9': + resolution: {integrity: sha512-rsxswgHMfU1zUgB2byc08fesf83wLGjFnzLCEtuf00mx2doiqc6pYrf67raI37XqdRcGUviQepk2UKGqpng74Q==} + + '@formatjs/intl-localematcher@0.8.9': + resolution: {integrity: sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1143,6 +1158,88 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1321,6 +1418,9 @@ packages: '@rushstack/eslint-patch@1.16.1': resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1550,9 +1650,96 @@ packages: resolution: {integrity: sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA==} engines: {node: '>=20.0.0'} + '@swc/core-darwin-arm64@1.15.40': + resolution: {integrity: sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.40': + resolution: {integrity: sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.40': + resolution: {integrity: sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.40': + resolution: {integrity: sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.40': + resolution: {integrity: sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.40': + resolution: {integrity: sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.40': + resolution: {integrity: sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.40': + resolution: {integrity: sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.40': + resolution: {integrity: sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.40': + resolution: {integrity: sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.40': + resolution: {integrity: sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.40': + resolution: {integrity: sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.40': + resolution: {integrity: sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@t3-oss/env-core@0.13.11': resolution: {integrity: sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ==} peerDependencies: @@ -3210,6 +3397,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + icu-minify@4.13.0: + resolution: {integrity: sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3265,6 +3455,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@11.2.7: + resolution: {integrity: sha512-+q6Ktg119nULZEpZ8YTuGOst9MyEzFtjD63FTGBlN1mLz0Z/MOUYDIvnpVKwq17eezIEh+cfJIebfJoCetpiNw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3730,12 +3923,29 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + next-intl-swc-plugin-extractor@4.13.0: + resolution: {integrity: sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==} + + next-intl@4.13.0: + resolution: {integrity: sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -3763,6 +3973,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -4099,6 +4312,9 @@ packages: engines: {node: '>=18'} hasBin: true + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4957,6 +5173,11 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + use-intl@4.13.0: + resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5563,6 +5784,18 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@formatjs/fast-memoize@3.1.5': {} + + '@formatjs/icu-messageformat-parser@3.5.10': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.9 + + '@formatjs/icu-skeleton-parser@2.1.9': {} + + '@formatjs/intl-localematcher@0.8.9': + dependencies: + '@formatjs/fast-memoize': 3.1.5 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -6073,6 +6306,66 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + '@pkgjs/parseargs@0.11.0': optional: true @@ -6200,6 +6493,8 @@ snapshots: '@rushstack/eslint-patch@1.16.1': {} + '@schummar/icu-type-parser@1.21.5': {} + '@sec-ant/readable-stream@0.4.1': {} '@semantic-release/changelog@6.0.3(semantic-release@24.2.9(typescript@5.9.3))': @@ -6568,10 +6863,70 @@ snapshots: '@supabase/realtime-js': 2.106.2 '@supabase/storage-js': 2.106.2 + '@swc/core-darwin-arm64@1.15.40': + optional: true + + '@swc/core-darwin-x64@1.15.40': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.40': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.40': + optional: true + + '@swc/core-linux-arm64-musl@1.15.40': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.40': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.40': + optional: true + + '@swc/core-linux-x64-gnu@1.15.40': + optional: true + + '@swc/core-linux-x64-musl@1.15.40': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.40': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.40': + optional: true + + '@swc/core-win32-x64-msvc@1.15.40': + optional: true + + '@swc/core@1.15.40': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.40 + '@swc/core-darwin-x64': 1.15.40 + '@swc/core-linux-arm-gnueabihf': 1.15.40 + '@swc/core-linux-arm64-gnu': 1.15.40 + '@swc/core-linux-arm64-musl': 1.15.40 + '@swc/core-linux-ppc64-gnu': 1.15.40 + '@swc/core-linux-s390x-gnu': 1.15.40 + '@swc/core-linux-x64-gnu': 1.15.40 + '@swc/core-linux-x64-musl': 1.15.40 + '@swc/core-win32-arm64-msvc': 1.15.40 + '@swc/core-win32-ia32-msvc': 1.15.40 + '@swc/core-win32-x64-msvc': 1.15.40 + + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + '@t3-oss/env-core@0.13.11(typescript@5.9.3)(zod@3.25.76)': optionalDependencies: typescript: 5.9.3 @@ -7611,8 +7966,7 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} dir-glob@3.0.1: dependencies: @@ -8405,6 +8759,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + icu-minify@4.13.0: + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.10 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -8452,6 +8810,11 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@11.2.7: + dependencies: + '@formatjs/fast-memoize': 3.1.5 + '@formatjs/icu-messageformat-parser': 3.5.10 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -8903,10 +9266,31 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} nerf-dart@1.0.0: {} + next-intl-swc-plugin-extractor@4.13.0: {} + + next-intl@4.13.0(next@15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6)(typescript@5.9.3): + dependencies: + '@formatjs/intl-localematcher': 0.8.9 + '@parcel/watcher': 2.5.6 + '@swc/core': 1.15.40 + icu-minify: 4.13.0 + negotiator: 1.0.0 + next: 15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next-intl-swc-plugin-extractor: 4.13.0 + po-parser: 2.1.1 + react: 19.2.6 + use-intl: 4.13.0(react@19.2.6) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/helpers' + next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 @@ -8937,6 +9321,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -9187,6 +9573,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + po-parser@2.1.1: {} + possible-typed-array-names@1.1.0: {} postcss-load-config@6.0.1(postcss@8.5.14): @@ -9950,7 +10338,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(postcss@8.5.14)(typescript@5.9.3): + tsup@8.5.1(@swc/core@1.15.40)(postcss@8.5.14)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -9970,6 +10358,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: + '@swc/core': 1.15.40 postcss: 8.5.14 typescript: 5.9.3 transitivePeerDependencies: @@ -10110,6 +10499,14 @@ snapshots: url-join@5.0.0: {} + use-intl@4.13.0(react@19.2.6): + dependencies: + '@formatjs/fast-memoize': 3.1.5 + '@schummar/icu-type-parser': 1.21.5 + icu-minify: 4.13.0 + intl-messageformat: 11.2.7 + react: 19.2.6 + util-deprecate@1.0.2: {} uuid@9.0.1: {} From 79f5662db2c6ec52f23de11c850d9d4b6fff2bf2 Mon Sep 17 00:00:00 2001 From: pauljacobb Date: Sun, 31 May 2026 04:51:14 +0000 Subject: [PATCH 29/76] feat(contracts): add upgrade timelock tests for community_governance (#284) - Tests for propose_upgrade restricted to admin - Tests for 48-hour timelock enforcement - Tests for upgrade announcement (event emitted) - Tests for cancellation by admin within window - Tests for full upgrade flow and edge cases Closes #284 --- .../community_governance/tests/upgrade.rs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 apps/contracts/community_governance/tests/upgrade.rs diff --git a/apps/contracts/community_governance/tests/upgrade.rs b/apps/contracts/community_governance/tests/upgrade.rs new file mode 100644 index 0000000..49f373b --- /dev/null +++ b/apps/contracts/community_governance/tests/upgrade.rs @@ -0,0 +1,147 @@ +//! Contract upgrade mechanism tests — issue #284. +//! +//! Acceptance criteria: +//! 1. propose_upgrade() restricted to admin +//! 2. 48-hour timelock before upgrade takes effect +//! 3. Upgrade announcement event emitted +//! 4. Timelock cancellable by admin within window +//! 5. Tests for upgrade flow and cancellation + +use community_governance::{CommunityGovernance, CommunityGovernanceClient}; +use soroban_sdk::{ + testutils::{Address as _, Events, Ledger}, + BytesN, Env, +}; + +/// 48 hours in ledgers (10-second ledger time). +const UPGRADE_TIMELOCK_LEDGERS: u32 = 17_280; + +fn setup() -> (Env, soroban_sdk::Address, CommunityGovernanceClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + let admin = soroban_sdk::Address::generate(&env); + client.initialize(&admin, &100_u32, &100_u32); + (env, admin, client) +} + +fn wasm_hash(env: &Env, val: u8) -> BytesN<32> { + BytesN::from_array(env, &[val; 32]) +} + +/// AC1 + AC3: propose_upgrade emits an announcement event. +#[test] +fn propose_upgrade_emits_event() { + let (env, admin, client) = setup(); + let hash = wasm_hash(&env, 0xAB); + + client.propose_upgrade(&admin, &hash); + + let events = env.events().all(); + assert!(!events.is_empty(), "expected at least one event"); +} + +/// AC1: propose_upgrade is restricted to admin — non-admin panics. +#[test] +#[should_panic] +fn propose_upgrade_non_admin_rejected() { + let (env, _admin, client) = setup(); + let attacker = soroban_sdk::Address::generate(&env); + client.propose_upgrade(&attacker, &wasm_hash(&env, 1)); +} + +/// AC2: execute_upgrade before timelock elapses panics with "timelock not elapsed". +#[test] +#[should_panic(expected = "timelock not elapsed")] +fn execute_upgrade_before_timelock_panics() { + let (env, admin, client) = setup(); + client.propose_upgrade(&admin, &wasm_hash(&env, 2)); + + // Advance only half the timelock + env.ledger() + .with_mut(|l| l.sequence_number += UPGRADE_TIMELOCK_LEDGERS / 2); + + client.execute_upgrade(&admin); +} + +/// AC4: cancel_upgrade removes the pending proposal. +#[test] +fn cancel_upgrade_clears_pending() { + let (env, admin, client) = setup(); + client.propose_upgrade(&admin, &wasm_hash(&env, 3)); + + assert!(client.pending_upgrade().is_some()); + + client.cancel_upgrade(&admin); + + assert!(client.pending_upgrade().is_none()); +} + +/// AC4: cancel_upgrade is restricted to admin. +#[test] +#[should_panic] +fn cancel_upgrade_non_admin_rejected() { + let (env, admin, client) = setup(); + client.propose_upgrade(&admin, &wasm_hash(&env, 4)); + + let attacker = soroban_sdk::Address::generate(&env); + client.cancel_upgrade(&attacker); +} + +/// Cancelling when no upgrade is pending panics. +#[test] +#[should_panic(expected = "no pending upgrade")] +fn cancel_upgrade_no_pending_panics() { + let (env, admin, client) = setup(); + client.cancel_upgrade(&admin); +} + +/// Proposing a second upgrade while one is pending panics. +#[test] +#[should_panic(expected = "upgrade already pending")] +fn propose_upgrade_while_pending_panics() { + let (env, admin, client) = setup(); + client.propose_upgrade(&admin, &wasm_hash(&env, 5)); + client.propose_upgrade(&admin, &wasm_hash(&env, 6)); +} + +/// After cancellation a new upgrade can be proposed. +#[test] +fn propose_upgrade_after_cancel_succeeds() { + let (env, admin, client) = setup(); + client.propose_upgrade(&admin, &wasm_hash(&env, 7)); + client.cancel_upgrade(&admin); + // Should not panic + client.propose_upgrade(&admin, &wasm_hash(&env, 8)); + assert!(client.pending_upgrade().is_some()); +} + +/// pending_upgrade returns None when no upgrade is queued. +#[test] +fn pending_upgrade_none_when_empty() { + let (_env, _admin, client) = setup(); + assert!(client.pending_upgrade().is_none()); +} + +/// pending_upgrade returns the correct wasm hash after proposal. +#[test] +fn pending_upgrade_returns_correct_hash() { + let (env, admin, client) = setup(); + let hash = wasm_hash(&env, 0xCC); + client.propose_upgrade(&admin, &hash); + + let pending = client.pending_upgrade().expect("should have pending upgrade"); + assert_eq!(pending.new_wasm_hash, hash); +} + +/// The unlock_ledger is set to current_ledger + UPGRADE_TIMELOCK_LEDGERS. +#[test] +fn pending_upgrade_unlock_ledger_is_correct() { + let (env, admin, client) = setup(); + let current = env.ledger().sequence(); + client.propose_upgrade(&admin, &wasm_hash(&env, 0xDD)); + + let pending = client.pending_upgrade().unwrap(); + assert_eq!(pending.unlock_ledger, current + UPGRADE_TIMELOCK_LEDGERS); +} From 7881a7e9c2d0152f016e3849da267ed755012675 Mon Sep 17 00:00:00 2001 From: pauljacobb Date: Sun, 31 May 2026 04:52:17 +0000 Subject: [PATCH 30/76] feat(web): real-time energy chart with WebSocket + polling fallback (#260) - WebSocket connection established on dashboard load - Chart updates automatically when new meter readings arrive - Graceful fallback to polling (30s interval) if WebSocket unavailable - Connection status indicator: Live / Polling / Offline / Connecting Closes #260 --- apps/web/src/components/DashboardChart.tsx | 138 +++++++++++++++++++-- 1 file changed, 125 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/DashboardChart.tsx b/apps/web/src/components/DashboardChart.tsx index 2e82e48..93a8f09 100644 --- a/apps/web/src/components/DashboardChart.tsx +++ b/apps/web/src/components/DashboardChart.tsx @@ -1,26 +1,138 @@ 'use client' +import { useEffect, useRef, useState, useCallback } from 'react' import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import { Wifi, WifiOff, Radio } from 'lucide-react' -const data = [ - { day: 'Mon', energy: 14 }, - { day: 'Tue', energy: 18 }, - { day: 'Wed', energy: 16 }, - { day: 'Thu', energy: 22 }, - { day: 'Fri', energy: 20 }, - { day: 'Sat', energy: 26 }, - { day: 'Sun', energy: 24 }, -] +interface ChartPoint { + label: string + energy: number +} + +const POLL_INTERVAL_MS = 30_000 + +async function fetchRecentReadings(): Promise { + try { + const res = await fetch('/api/readings?limit=20') + if (!res.ok) return [] + const json = await res.json() + const rows: { timestamp: string; kwh: number }[] = json.data ?? [] + return rows + .slice() + .reverse() + .map((r) => ({ + label: new Date(r.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + energy: r.kwh, + })) + } catch { + return [] + } +} + +type ConnectionStatus = 'connecting' | 'live' | 'polling' | 'error' export function DashboardChart() { + const [data, setData] = useState([]) + const [status, setStatus] = useState('connecting') + const wsRef = useRef(null) + const pollRef = useRef | null>(null) + const mountedRef = useRef(true) + + const startPolling = useCallback(() => { + if (pollRef.current) return + setStatus('polling') + fetchRecentReadings().then((d) => { if (mountedRef.current && d.length) setData(d) }) + pollRef.current = setInterval(() => { + fetchRecentReadings().then((d) => { if (mountedRef.current && d.length) setData(d) }) + }, POLL_INTERVAL_MS) + }, []) + + const stopPolling = useCallback(() => { + if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null } + }, []) + + const appendReading = useCallback((kwh: number, timestamp: string) => { + setData((prev) => { + const point: ChartPoint = { + label: new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + energy: kwh, + } + const next = [...prev, point] + return next.length > 20 ? next.slice(next.length - 20) : next + }) + }, []) + + useEffect(() => { + mountedRef.current = true + + // Load initial data via REST + fetchRecentReadings().then((d) => { if (mountedRef.current && d.length) setData(d) }) + + // Attempt WebSocket connection + try { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const ws = new WebSocket(`${protocol}//${window.location.host}/api/ws/readings`) + wsRef.current = ws + + ws.onopen = () => { + if (!mountedRef.current) return + setStatus('live') + stopPolling() + } + + ws.onmessage = (event) => { + if (!mountedRef.current) return + try { + const reading = JSON.parse(event.data as string) + appendReading(reading.kwh, reading.timestamp) + } catch { /* ignore malformed messages */ } + } + + ws.onerror = () => { + if (!mountedRef.current) return + setStatus('error') + startPolling() + } + + ws.onclose = () => { + if (!mountedRef.current) return + if (status !== 'live') return + setStatus('polling') + startPolling() + } + } catch { + startPolling() + } + + return () => { + mountedRef.current = false + wsRef.current?.close() + wsRef.current = null + stopPolling() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const statusConfig: Record = { + connecting: { icon: , label: 'Connecting…', color: 'text-gray-400' }, + live: { icon: , label: 'Live', color: 'text-green-500' }, + polling: { icon: , label: 'Polling', color: 'text-amber-500' }, + error: { icon: , label: 'Offline', color: 'text-red-500' }, + } + + const { icon, label, color } = statusConfig[status] + return (

Energy trend

-

Weekly generation

+

Live generation

-

Responsive chart for mobile and desktop

+ + {icon} + {label} +
@@ -32,10 +144,10 @@ export function DashboardChart() { - + - +
From 18c472abc6bee773e297e15398e8e00588f78331 Mon Sep 17 00:00:00 2001 From: pauljacobb Date: Sun, 31 May 2026 05:06:15 +0000 Subject: [PATCH 31/76] docs(contracts): add/enhance Rust doc comments on all public functions (#319) - energy_token: enhanced balance() and total_supply() with examples - audit_registry: enhanced anchor(), verify(), api_signer(), admin() with full Arguments/Errors/Example sections - community_governance: enhanced set_quorum_bps, get_quorum_bps, set_threshold_bps, get_threshold_bps, pending_upgrade, get_execution_timelock, proposal_count with Panics/Arguments/Examples All public functions now have /// doc comments with Panics, Arguments, Authorization, and example invocations where applicable. Closes #319 --- apps/contracts/audit_registry/src/lib.rs | 36 ++++++++++++++- .../contracts/community_governance/src/lib.rs | 44 ++++++++++++++++--- apps/contracts/energy_token/src/lib.rs | 10 +++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/apps/contracts/audit_registry/src/lib.rs b/apps/contracts/audit_registry/src/lib.rs index e3e83b5..dc27611 100644 --- a/apps/contracts/audit_registry/src/lib.rs +++ b/apps/contracts/audit_registry/src/lib.rs @@ -157,6 +157,9 @@ impl AuditRegistry { } /// Returns the current authorised API signer address. + /// + /// # Panics + /// * `"not initialized"` if the contract has not been initialised. pub fn api_signer(env: Env) -> soroban_sdk::Address { env.storage() .instance() @@ -171,10 +174,29 @@ impl AuditRegistry { ((b0 << 8) | b1) % 1024 } - /// Anchor a reading hash on-chain. + /// Anchor a reading hash on-chain. Only the registered `api_signer` may call this. + /// + /// # Arguments + /// * `caller` — must equal the registered `api_signer`. + /// * `reading_hash` — 32-byte SHA-256 of `(meter_id || kwh_stroops_le || timestamp_le)`. + /// * `nonce` — 32-byte unique value; prevents replay of the same anchor call. + /// + /// # Authorization + /// Requires `caller` authorisation. Returns `Err(Error::Unauthorized)` if + /// `caller` is not the registered `api_signer`. + /// + /// # Errors + /// * `Error::Unauthorized` — caller is not the `api_signer`. + /// * `Error::AlreadyAnchored` — `reading_hash` or `nonce` was already used. /// /// # Events - /// Emits `(topic: "anchor", data: reading_hash)`. + /// Emits `(topic: "anchor", data: (reading_hash, ledger_sequence, ledger_timestamp))`. + /// + /// # Example + /// ```ignore + /// client.anchor(&api_signer, &reading_hash, &nonce).unwrap(); + /// assert!(client.is_anchored(&reading_hash)); + /// ``` pub fn anchor( env: Env, caller: soroban_sdk::Address, @@ -235,6 +257,13 @@ impl AuditRegistry { } /// Returns the `AuditAnchor` for `reading_hash`, or `None` if not anchored. + /// + /// # Example + /// ```ignore + /// if let Some(anchor) = client.verify(&hash) { + /// println!("anchored at ledger {}", anchor.anchored_at_ledger); + /// } + /// ``` pub fn verify(env: Env, reading_hash: BytesN<32>) -> Option { let bucket_id = Self::get_bucket_id(&reading_hash); let bucket: Map, u32> = env.storage().persistent().get(&DataKey::Bucket(bucket_id))?; @@ -264,6 +293,9 @@ impl AuditRegistry { } /// Returns the admin address. + /// + /// # Panics + /// * `"not initialized"` if the contract has not been initialised. pub fn admin(env: Env) -> soroban_sdk::Address { env.storage() .instance() diff --git a/apps/contracts/community_governance/src/lib.rs b/apps/contracts/community_governance/src/lib.rs index 02ea4a6..8906f58 100644 --- a/apps/contracts/community_governance/src/lib.rs +++ b/apps/contracts/community_governance/src/lib.rs @@ -242,14 +242,29 @@ impl CommunityGovernance { .set(&DataKey::Version, &new_version); } - /// Set quorum in basis points (1–10 000). Admin-only. + /// Set the minimum participation quorum in basis points (1–10 000). Admin-only. + /// + /// # Arguments + /// * `admin` — administrator address (must authorise). + /// * `bps` — quorum in basis points, e.g. `1000` = 10 %. + /// + /// # Authorization + /// Requires `admin` authorisation. + /// + /// # Panics + /// * `"quorum_bps must be 1-10000"` if `bps` is out of range. + /// + /// # Example + /// ```ignore + /// client.set_quorum_bps(&admin, &2_000_u32); // 20 % + /// ``` pub fn set_quorum_bps(env: Env, admin: Address, bps: u32) { admin.require_auth(); assert!(bps >= 1 && bps <= 10_000, "quorum_bps must be 1-10000"); env.storage().instance().set(&DataKey::QuorumBps, &bps); } - /// Returns the current quorum in basis points. + /// Returns the current quorum in basis points (default: `1000` = 10 %). pub fn get_quorum_bps(env: Env) -> u32 { env.storage() .instance() @@ -257,14 +272,29 @@ impl CommunityGovernance { .unwrap_or(DEFAULT_QUORUM_BPS) } - /// Set approval threshold in basis points (1–10 000). Admin-only. + /// Set the yes-vote approval threshold in basis points (1–10 000). Admin-only. + /// + /// # Arguments + /// * `admin` — administrator address (must authorise). + /// * `bps` — threshold in basis points, e.g. `5100` = 51 %. + /// + /// # Authorization + /// Requires `admin` authorisation. + /// + /// # Panics + /// * `"threshold_bps must be 1-10000"` if `bps` is out of range. + /// + /// # Example + /// ```ignore + /// client.set_threshold_bps(&admin, &6_000_u32); // 60 % + /// ``` pub fn set_threshold_bps(env: Env, admin: Address, bps: u32) { admin.require_auth(); assert!(bps >= 1 && bps <= 10_000, "threshold_bps must be 1-10000"); env.storage().instance().set(&DataKey::ThresholdBps, &bps); } - /// Returns the current approval threshold in basis points. + /// Returns the current approval threshold in basis points (default: `5100` = 51 %). pub fn get_threshold_bps(env: Env) -> u32 { env.storage() .instance() @@ -543,6 +573,8 @@ impl CommunityGovernance { } /// Returns the pending upgrade proposal, if any. + /// + /// Returns `None` if no upgrade has been proposed or the last one was cancelled/executed. pub fn pending_upgrade(env: Env) -> Option { env.storage().instance().get(&DataKey::PendingUpgrade) } @@ -565,7 +597,7 @@ impl CommunityGovernance { .set(&DataKey::ExecuteTimelock, &ledgers); } - /// Returns the current execution timelock in ledgers. + /// Returns the current execution timelock in ledgers (default: `8640` ≈ 24 h). pub fn get_execution_timelock(env: Env) -> u32 { env.storage() .instance() @@ -615,7 +647,7 @@ impl CommunityGovernance { proposals.get(proposal_id) } - /// Returns the total number of proposals created. + /// Returns the total number of proposals created (monotonically increasing). pub fn proposal_count(env: Env) -> u32 { env.storage() .instance() diff --git a/apps/contracts/energy_token/src/lib.rs b/apps/contracts/energy_token/src/lib.rs index acf029f..91a818b 100644 --- a/apps/contracts/energy_token/src/lib.rs +++ b/apps/contracts/energy_token/src/lib.rs @@ -81,6 +81,11 @@ impl EnergyToken { // ── SEP-41 balance / transfer ──────────────────────────────────────────── /// Returns the token balance of `account`. Returns `0` for unknown accounts. + /// + /// # Example + /// ```ignore + /// let bal = client.balance(&holder_address); // e.g. 125_000_000 (12.5 kWh in stroops) + /// ``` pub fn balance(env: Env, account: Address) -> i128 { env.storage() .persistent() @@ -292,6 +297,11 @@ impl EnergyToken { } /// Returns the current circulating supply: `total_minted - total_burned`. + /// + /// # Example + /// ```ignore + /// let supply = client.total_supply(); // tokens currently in circulation + /// ``` pub fn total_supply(env: Env) -> i128 { let minted: i128 = env .storage() From e90113d1ed1b6252ff35d4ce83c4cc71e0be519b Mon Sep 17 00:00:00 2001 From: pauljacobb Date: Sun, 31 May 2026 05:15:06 +0000 Subject: [PATCH 32/76] test(web): add API route tests and enforce 80% coverage threshold (#325) - certificates/route.test.ts: happy path, DB error, pagination, cursor, field normalization (readings join flattened to meter_id) - meters/route.test.ts: add GET tests (list + DB error) - ready/route.test.ts: healthy and degraded states - vitest.config.ts: add lcov reporter + 80% threshold on lines/functions/ branches/statements - ci.yml: run vitest --coverage and upload lcov to Codecov Closes #325 --- .github/workflows/ci.yml | 9 ++ .../src/app/api/certificates/route.test.ts | 141 ++++++++++++++++++ apps/web/src/app/api/meters/route.test.ts | 37 ++++- apps/web/src/app/api/ready/route.test.ts | 38 +++++ apps/web/vitest.config.ts | 9 +- 5 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/api/certificates/route.test.ts create mode 100644 apps/web/src/app/api/ready/route.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eeb65f..8dcb86e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,15 @@ jobs: TURBO_TEAM: ${{ secrets.TURBO_TEAM }} - run: pnpm test working-directory: apps/web + - name: Run tests with coverage + run: pnpm vitest run --coverage + working-directory: apps/web + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: apps/web/coverage/lcov.info + flags: web + fail_ci_if_error: false - run: pnpm build working-directory: apps/web env: diff --git a/apps/web/src/app/api/certificates/route.test.ts b/apps/web/src/app/api/certificates/route.test.ts new file mode 100644 index 0000000..7570faf --- /dev/null +++ b/apps/web/src/app/api/certificates/route.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { NextRequest } from 'next/server' + +vi.mock('@/lib/supabase', () => ({ createServiceClient: vi.fn() })) + +import { createServiceClient } from '@/lib/supabase' +import { GET } from '@/app/api/certificates/route' + +function makeRequest(params: Record = {}) { + const url = new URL('http://localhost/api/certificates') + Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)) + return new NextRequest(url) +} + +const CERT = { + id: 'cert-1', + kwh: 10, + issued_at: '2026-01-01T00:00:00Z', + retired: false, + retired_at: null, + retired_by: null, + mint_tx_hash: 'abc', + readings: { meter_id: 'meter-1' }, +} + +function mockDb(data: unknown[], error: unknown = null, count = 1) { + const query: Record = {} + const chain = (obj: Record) => { + ;['select', 'order', 'limit', 'lt', 'eq', 'gte', 'lte', 'or'].forEach((m) => { + obj[m] = vi.fn().mockReturnValue(obj) + }) + obj.then = undefined + // Make it thenable for await + Object.defineProperty(obj, Symbol.iterator, { value: undefined }) + return obj + } + const q = chain(query) + // Final await resolves with data + ;(q as unknown as Promise)[Symbol.for('vitest-mock-result')] = { data, error, count } + vi.mocked(createServiceClient).mockReturnValue({ + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + order: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + lt: vi.fn().mockReturnThis(), + eq: vi.fn().mockReturnThis(), + gte: vi.fn().mockReturnThis(), + lte: vi.fn().mockReturnThis(), + or: vi.fn().mockResolvedValue({ data, error, count }), + then: undefined, + // make it awaitable + [Symbol.toStringTag]: 'Promise', + }), + }), + }), + }), + } as ReturnType) +} + +function mockDbSimple(data: unknown[], error: unknown = null, count = data.length) { + const terminal = vi.fn().mockResolvedValue({ data, error, count }) + const makeChain = (): Record => { + const obj: Record = {} + ;['lt', 'eq', 'gte', 'lte', 'or'].forEach((m) => { obj[m] = vi.fn().mockReturnValue(obj) }) + // Make awaitable + obj.then = (resolve: (v: unknown) => unknown) => Promise.resolve({ data, error, count }).then(resolve) + obj.catch = (reject: (e: unknown) => unknown) => Promise.resolve({ data, error, count }).catch(reject) + return obj + } + vi.mocked(createServiceClient).mockReturnValue({ + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + order: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue(makeChain()), + }), + }), + }), + } as ReturnType) + return terminal +} + +beforeEach(() => vi.clearAllMocks()) + +describe('GET /api/certificates', () => { + it('returns 200 with data array on success', async () => { + mockDbSimple([CERT]) + const res = await GET(makeRequest()) + expect(res.status).toBe(200) + const body = await res.json() + expect(Array.isArray(body.data)).toBe(true) + }) + + it('returns 500 when DB errors', async () => { + mockDbSimple([], { message: 'db error' }) + const res = await GET(makeRequest()) + expect(res.status).toBe(500) + const body = await res.json() + expect(body.error).toBe('db error') + }) + + it('respects limit param (max 100)', async () => { + mockDbSimple([]) + const res = await GET(makeRequest({ limit: '200' })) + expect(res.status).toBe(200) + }) + + it('returns next_cursor when there are more results', async () => { + // Return limit+1 items to trigger hasMore + const items = Array.from({ length: 21 }, (_, i) => ({ ...CERT, id: `cert-${i}`, issued_at: `2026-01-${String(i + 1).padStart(2, '0')}T00:00:00Z`, readings: { meter_id: 'meter-1' } })) + mockDbSimple(items, null, 100) + const res = await GET(makeRequest({ limit: '20' })) + expect(res.status).toBe(200) + const body = await res.json() + expect(body.next_cursor).not.toBeNull() + expect(body.data).toHaveLength(20) + }) + + it('returns null next_cursor when no more results', async () => { + mockDbSimple([CERT], null, 1) + const res = await GET(makeRequest()) + expect(res.status).toBe(200) + const body = await res.json() + expect(body.next_cursor).toBeNull() + }) + + it('normalizes readings join to meter_id field', async () => { + mockDbSimple([CERT], null, 1) + const res = await GET(makeRequest()) + const body = await res.json() + expect(body.data[0].meter_id).toBe('meter-1') + expect(body.data[0].readings).toBeUndefined() + }) + + it('handles array readings join', async () => { + const certWithArray = { ...CERT, readings: [{ meter_id: 'meter-arr' }] } + mockDbSimple([certWithArray], null, 1) + const res = await GET(makeRequest()) + const body = await res.json() + expect(body.data[0].meter_id).toBe('meter-arr') + }) +}) diff --git a/apps/web/src/app/api/meters/route.test.ts b/apps/web/src/app/api/meters/route.test.ts index 7793d99..c8b74b3 100644 --- a/apps/web/src/app/api/meters/route.test.ts +++ b/apps/web/src/app/api/meters/route.test.ts @@ -7,7 +7,24 @@ vi.mock('@/lib/auth', () => ({ })) import { createServiceClient } from '@/lib/supabase' -import { POST } from '@/app/api/meters/route' +import { GET, POST } from '@/app/api/meters/route' + +function makeGetRequest() { + return { + headers: { get: (_: string) => null }, + nextUrl: { searchParams: new URLSearchParams() }, + } as unknown as Parameters[0] +} + +function mockDbGet(data: unknown[], error: unknown = null) { + vi.mocked(createServiceClient).mockReturnValue({ + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + order: vi.fn().mockResolvedValue({ data, error }), + }), + }), + } as ReturnType) +} function makeRequest(body: unknown) { return { @@ -69,3 +86,21 @@ describe('POST /api/meters', () => { expect(res.status).toBe(400) }) }) + +describe('GET /api/meters', () => { + it('returns 200 with meters list', async () => { + mockDbGet([{ id: 'meter-1', serial_number: 'SN-001' }]) + const res = await GET(makeGetRequest()) + expect(res.status).toBe(200) + const body = await res.json() + expect(Array.isArray(body)).toBe(true) + }) + + it('returns 500 when DB errors', async () => { + mockDbGet([], { message: 'db failure' }) + const res = await GET(makeGetRequest()) + expect(res.status).toBe(500) + const body = await res.json() + expect(body.error).toBe('db failure') + }) +}) diff --git a/apps/web/src/app/api/ready/route.test.ts b/apps/web/src/app/api/ready/route.test.ts new file mode 100644 index 0000000..bb3611b --- /dev/null +++ b/apps/web/src/app/api/ready/route.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('@/lib/supabase', () => ({ createServiceClient: vi.fn() })) + +import { createServiceClient } from '@/lib/supabase' +import { GET } from '@/app/api/ready/route' + +function mockDb(error: unknown = null) { + vi.mocked(createServiceClient).mockReturnValue({ + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue({ data: [{ id: '1' }], error }), + }), + }), + } as ReturnType) +} + +beforeEach(() => vi.clearAllMocks()) + +describe('GET /api/ready', () => { + it('returns 200 when DB is healthy', async () => { + mockDb() + const res = await GET() + expect(res.status).toBe(200) + const body = await res.json() + expect(body.status).toBe('ok') + expect(body.checks.db).toBe(true) + }) + + it('returns 503 when DB check fails', async () => { + mockDb({ message: 'connection refused' }) + const res = await GET() + expect(res.status).toBe(503) + const body = await res.json() + expect(body.status).toBe('degraded') + expect(body.checks.db).toBe(false) + }) +}) diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index 4a470c0..32becf7 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -13,9 +13,16 @@ export default defineConfig({ ], coverage: { provider: 'v8', - reporter: ['text', 'html'], + reporter: ['text', 'html', 'lcov'], all: true, include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.test.{ts,tsx}', 'src/test-setup.ts', 'src/**/*.d.ts'], + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, }, exclude: ['node_modules', 'dist', '.idea', '.git', '.cache', 'e2e/**'], }, From eb1a425bc37ae9428dd0ac9a082f1d666bc51912 Mon Sep 17 00:00:00 2001 From: Sparklemzz Date: Sun, 31 May 2026 14:01:45 +0000 Subject: [PATCH 33/76] docs: add user guide for web dashboard (closes #317) - Add docs/USER_GUIDE.md covering all acceptance criteria: wallet connection, dashboard overview, meter readings, certificates (view + retire), governance (view/vote/create), and the public verifier - Include screenshot placeholders for each step - Link guide from dashboard page header via BookOpen icon --- apps/web/src/app/dashboard/page.tsx | 15 +- docs/USER_GUIDE.md | 213 ++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 docs/USER_GUIDE.md diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 718baa5..87e8773 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -14,7 +14,7 @@ import { Legend, } from 'recharts' import { useTheme } from 'next-themes' -import { Zap, Award, Leaf, TrendingUp } from 'lucide-react' +import { Zap, Award, Leaf, TrendingUp, BookOpen } from 'lucide-react' import { StatCardSkeleton, ChartSkeleton, TableRowSkeleton } from '@/components/skeleton' // --------------------------------------------------------------------------- @@ -132,7 +132,18 @@ export default function DashboardPage() { return (
-

Dashboard

+
+

Dashboard

+ + +
{/* ------------------------------------------------------------------ */} {/* Stat cards */} diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..274a341 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,213 @@ +# SolarProof Dashboard — User Guide + +> **Audience:** Energy producers and cooperatives using the SolarProof web dashboard. +> **Live app:** [https://solarproof.vercel.app](https://solarproof.vercel.app) + +--- + +## Table of Contents + +1. [Connecting Your Wallet](#1-connecting-your-wallet) +2. [Dashboard Overview](#2-dashboard-overview) +3. [Submitting Meter Readings](#3-submitting-meter-readings) +4. [Viewing Certificates](#4-viewing-certificates) +5. [Retiring Certificates](#5-retiring-certificates) +6. [Participating in Governance](#6-participating-in-governance) +7. [Verifying a Certificate](#7-verifying-a-certificate) +8. [Troubleshooting](#8-troubleshooting) + +--- + +## 1. Connecting Your Wallet + +SolarProof uses [Freighter](https://www.freighter.app/) — a Stellar browser wallet — to sign transactions. + +**Prerequisites** + +- Freighter browser extension installed ([freighter.app](https://www.freighter.app/)) +- Freighter set to **Testnet** (Settings → Network → Testnet) +- Your Stellar account funded with at least 1 XLM (use [Stellar Laboratory Friendbot](https://laboratory.stellar.org/#account-creator?network=test) for testnet) + +**Steps** + +1. Open the SolarProof dashboard at `/dashboard`. +2. Click **Connect Wallet** in the top-right corner of the navigation bar. +3. Freighter will prompt you to approve the connection — click **Approve**. +4. Your truncated public key (e.g. `GABC…XYZ`) appears in the navbar, confirming you are connected. + +> **Screenshot placeholder:** `docs/screenshots/01-connect-wallet.png` +> *(Shows the navbar with the Connect Wallet button highlighted, then the connected state with the public key displayed.)* + +**Disconnecting** + +Click your public key in the navbar and select **Disconnect**. + +--- + +## 2. Dashboard Overview + +Navigate to **Dashboard** (`/dashboard`) to see a real-time summary of your energy activity. + +| Section | What it shows | +|---|---| +| **Total energy** | Cumulative kWh across all verified meter readings | +| **Certificates issued** | Number of energy tokens minted on Stellar (1 token = 1 kWh) | +| **Certificates retired** | Tokens permanently burned to claim renewable energy usage | +| **Active meters** | Meters that have reported in the last 24 hours | +| **Daily energy output chart** | Area chart of kWh over the last 14 days | +| **Verification status chart** | Verified vs. pending readings per meter | +| **Recent readings table** | Last 20 meter readings with status badges | + +> **Screenshot placeholder:** `docs/screenshots/02-dashboard-overview.png` +> *(Shows the full dashboard with stat cards, both charts, and the readings table.)* + +A **Verified** badge (green) means the reading's Ed25519 signature has been confirmed and the hash anchored on Stellar. A **Pending** badge (yellow) means verification is in progress. + +--- + +## 3. Submitting Meter Readings + +Meter readings can be submitted in two ways: via the UI form or programmatically via the API. + +### Via the UI (Meters page) + +1. Navigate to **Meters** (`/meters`). +2. Click **Submit Reading**. +3. Fill in the form: + - **Meter ID** — the unique identifier of your device + - **kWh** — energy generated since the last reading + - **Timestamp** — defaults to now; adjust if back-filling +4. Click **Submit**. The dashboard signs the reading with your connected wallet and posts it to `/api/readings`. +5. The new reading appears in the **Recent readings** table on the Dashboard with a **Pending** badge. It turns **Verified** once the API confirms the Ed25519 signature and anchors the hash on Stellar (usually within a few seconds). + +> **Screenshot placeholder:** `docs/screenshots/03-submit-reading-form.png` +> *(Shows the Submit Reading modal with the three fields filled in and the Submit button.)* + +### Via the API (automated / hardware meters) + +```bash +# Generate a meter keypair once +node scripts/gen-meter-key.mjs + +# Send a signed reading +node scripts/send-reading.mjs --kwh 12.5 --meter-key ./meter-key.json +``` + +See [docs/API.md](./API.md) for the full `POST /api/readings` specification. + +--- + +## 4. Viewing Certificates + +Each verified reading automatically mints an energy token (SEP-41) on Stellar — one token per kWh. + +1. Navigate to **Certificates** (`/certificates`). +2. The list shows all certificates associated with your wallet, including: + - **Certificate ID** — the on-chain token identifier + - **kWh** — energy amount represented + - **Issued** — date minted + - **Status** — Active or Retired +3. Click a certificate row to open the detail view, which shows: + - The originating meter reading + - The Stellar transaction hash (links to Stellar Explorer) + - The Ed25519 signature of the source reading + - The audit registry anchor hash + +> **Screenshot placeholder:** `docs/screenshots/04-certificates-list.png` +> *(Shows the certificates list with columns for ID, kWh, Issued date, and Status.)* + +> **Screenshot placeholder:** `docs/screenshots/05-certificate-detail.png` +> *(Shows the certificate detail page with the full chain of custody: meter → signature → ledger anchor → token.)* + +--- + +## 5. Retiring Certificates + +Retiring a certificate permanently burns the token on-chain, proving you have claimed the renewable energy for a specific period. This action is **irreversible**. + +1. Navigate to **Certificates** (`/certificates`). +2. Find the certificate you want to retire and click **Retire**. +3. A confirmation dialog appears showing the certificate ID and kWh amount. +4. Click **Confirm Retire**. Freighter will prompt you to sign the transaction. +5. Approve the transaction in Freighter. +6. The certificate status changes to **Retired** and the token is burned on Stellar. + +> **Screenshot placeholder:** `docs/screenshots/06-retire-confirmation.png` +> *(Shows the retire confirmation dialog with the certificate details and the Confirm Retire button.)* + +> **Note:** Retired certificates remain visible in the list with a **Retired** badge for audit purposes. They can be independently verified at `/verify`. + +--- + +## 6. Participating in Governance + +SolarProof cooperatives use on-chain governance to vote on proposals (e.g. fee changes, new meter policies). + +### Viewing proposals + +1. Navigate to **Governance** (`/governance`). +2. The proposals list shows: + - **Title** and description + - **Status** — Active, Passed, Rejected, or Executed + - **Voting deadline** + - **Current vote tally** (For / Against) + +> **Screenshot placeholder:** `docs/screenshots/07-governance-proposals.png` +> *(Shows the governance page with a list of proposals and their statuses.)* + +### Voting on a proposal + +1. Click a proposal with **Active** status to open its detail page. +2. Review the full description and any attached discussion. +3. Click **Vote For** or **Vote Against**. +4. Freighter prompts you to sign the vote transaction — click **Approve**. +5. Your vote is recorded on-chain. The tally updates immediately. + +> **Screenshot placeholder:** `docs/screenshots/08-vote-on-proposal.png` +> *(Shows the proposal detail page with the Vote For / Vote Against buttons and the live tally.)* + +### Creating a proposal + +1. On the **Governance** page, click **New Proposal**. +2. Fill in the **Title** and **Description**. +3. Click **Submit Proposal**. Freighter will prompt you to sign. +4. The proposal appears in the list with **Active** status and is open for voting immediately. + +> **Note:** Voting power is proportional to the number of active energy tokens held by your wallet at the time of the vote snapshot. + +--- + +## 7. Verifying a Certificate + +Anyone — including regulators and buyers — can verify a certificate without logging in. + +1. Navigate to **Verify** (`/verify`). +2. Enter a **Certificate ID** or **Stellar transaction hash**. +3. Click **Verify**. +4. The result shows the full chain of custody: + - Meter reading (kWh, timestamp, meter ID) + - Ed25519 signature validity + - Stellar ledger anchor (audit registry transaction) + - Certificate mint transaction + - Retirement transaction (if retired) + +> **Screenshot placeholder:** `docs/screenshots/09-verify-result.png` +> *(Shows the verify page with a certificate ID entered and the full chain-of-custody result expanded.)* + +--- + +## 8. Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| "Connect Wallet" button does nothing | Freighter not installed | Install from [freighter.app](https://www.freighter.app/) | +| Transaction fails with "insufficient funds" | Account has < 1 XLM | Fund via [Friendbot](https://laboratory.stellar.org/#account-creator?network=test) (testnet) | +| Reading stays **Pending** indefinitely | Signature verification failed | Check that the meter key matches the registered meter ID | +| Certificate not appearing after reading | Minting delay or failed mint | Check the Stellar transaction in the dashboard; see [tracer-sim auto-diagnosis](./API.md#error-handling) | +| Governance vote not registering | Wallet not connected or wrong network | Reconnect Freighter and ensure it is set to Testnet | + +For further help, open an issue at [github.com/AnnabelJoe/solarproof/issues](https://github.com/AnnabelJoe/solarproof/issues). + +--- + +*SolarProof Contributors 2026 · Apache-2.0* From 9b2ce4974322501587534e1ca73a03c446fba611 Mon Sep 17 00:00:00 2001 From: Sparklemzz Date: Sun, 31 May 2026 14:14:32 +0000 Subject: [PATCH 34/76] feat(security): add RLS policies for multi-tenant isolation (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable RLS on cooperatives, meters, readings, certificates - Members can only read rows belonging to their cooperative (cooperative_id sourced from JWT app_metadata) - Readings isolated via meter → cooperative join - Admin JWT role bypasses all policies for support operations - Policy tester SQL covers member isolation + admin bypass cases --- .../20260531000003_rls_policies.sql | 68 +++++++++++++ supabase/tests/rls_policies.sql | 97 +++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 supabase/migrations/20260531000003_rls_policies.sql create mode 100644 supabase/tests/rls_policies.sql diff --git a/supabase/migrations/20260531000003_rls_policies.sql b/supabase/migrations/20260531000003_rls_policies.sql new file mode 100644 index 0000000..b3fc60c --- /dev/null +++ b/supabase/migrations/20260531000003_rls_policies.sql @@ -0,0 +1,68 @@ +-- Migration 003: Row Level Security for multi-tenant isolation +-- Users carry their cooperative_id in JWT app_metadata. +-- The service role key (used by the API) bypasses RLS automatically. + +-- Helper: extract cooperative_id from the current user's JWT app_metadata +create or replace function auth.cooperative_id() returns uuid + language sql stable + as $$ + select nullif( + auth.jwt() -> 'app_metadata' ->> 'cooperative_id', + '' + )::uuid + $$; + +-- Helper: resolve cooperative_id for a reading via its meter +create or replace function auth.reading_cooperative_id(reading_id uuid) returns uuid + language sql stable + as $$ + select m.cooperative_id + from readings r + join meters m on m.id = r.meter_id + where r.id = reading_id + $$; + +-- ── cooperatives ──────────────────────────────────────────────────────────── +alter table cooperatives enable row level security; + +-- Members see only their own cooperative +create policy "members_select_own_cooperative" on cooperatives + for select using (id = auth.cooperative_id()); + +-- Admins (role = 'admin') can do anything +create policy "admin_all_cooperatives" on cooperatives + for all using (auth.jwt() ->> 'role' = 'admin'); + +-- ── meters ────────────────────────────────────────────────────────────────── +alter table meters enable row level security; + +create policy "members_select_own_meters" on meters + for select using (cooperative_id = auth.cooperative_id()); + +create policy "admin_all_meters" on meters + for all using (auth.jwt() ->> 'role' = 'admin'); + +-- ── readings ───────────────────────────────────────────────────────────────── +alter table readings enable row level security; + +-- Readings belong to a cooperative via their meter +create policy "members_select_own_readings" on readings + for select using ( + exists ( + select 1 from meters m + where m.id = readings.meter_id + and m.cooperative_id = auth.cooperative_id() + ) + ); + +create policy "admin_all_readings" on readings + for all using (auth.jwt() ->> 'role' = 'admin'); + +-- ── certificates ───────────────────────────────────────────────────────────── +alter table certificates enable row level security; + +create policy "members_select_own_certificates" on certificates + for select using (cooperative_id = auth.cooperative_id()); + +create policy "admin_all_certificates" on certificates + for all using (auth.jwt() ->> 'role' = 'admin'); diff --git a/supabase/tests/rls_policies.sql b/supabase/tests/rls_policies.sql new file mode 100644 index 0000000..4eb66eb --- /dev/null +++ b/supabase/tests/rls_policies.sql @@ -0,0 +1,97 @@ +-- RLS Policy Tests for issue #274 +-- Run in Supabase SQL editor or via psql. +-- Uses set_config to simulate JWT claims without a real auth session. +-- +-- Seed UUIDs (from seed.sql): +-- cooperative A: 00000000-0000-0000-0000-000000000001 +-- cooperative B: 00000000-0000-0000-0000-000000000002 (created below) +-- meter A: 00000000-0000-0000-0000-000000000010 + +-- ── Setup: second cooperative + meter for isolation tests ──────────────────── +insert into cooperatives (id, name, admin_address) values + ('00000000-0000-0000-0000-000000000002', 'Other Cooperative', + 'GOTHER1XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX') + on conflict (id) do nothing; + +insert into meters (id, cooperative_id, serial_number, pubkey_hex) values + ('00000000-0000-0000-0000-000000000020', + '00000000-0000-0000-0000-000000000002', + 'METER-002', + '0000000000000000000000000000000000000000000000000000000000000001') + on conflict (id) do nothing; + +-- ── Helper: simulate a JWT for a cooperative member ────────────────────────── +-- Usage: call set_claim('') then run your query. +create or replace function tests.set_claim(coop_id text, role text default 'authenticated') + returns void language plpgsql as $$ + begin + perform set_config( + 'request.jwt.claims', + json_build_object( + 'sub', 'test-user', + 'role', role, + 'app_metadata', json_build_object('cooperative_id', coop_id) + )::text, + true -- local to transaction + ); + end; +$$; + +-- ── Test 1: member of coop A sees only coop A's cooperative row ────────────── +do $$ +declare + cnt int; +begin + perform tests.set_claim('00000000-0000-0000-0000-000000000001'); + select count(*) into cnt from cooperatives; + assert cnt = 1, format('Test 1 FAIL: expected 1 cooperative, got %s', cnt); + raise notice 'Test 1 PASS: member sees only own cooperative'; +end $$; + +-- ── Test 2: member of coop A sees only coop A's meters ────────────────────── +do $$ +declare + cnt int; +begin + perform tests.set_claim('00000000-0000-0000-0000-000000000001'); + select count(*) into cnt from meters; + assert cnt = 1, format('Test 2 FAIL: expected 1 meter, got %s', cnt); + raise notice 'Test 2 PASS: member sees only own meters'; +end $$; + +-- ── Test 3: member of coop A cannot see coop B's meters ───────────────────── +do $$ +declare + cnt int; +begin + perform tests.set_claim('00000000-0000-0000-0000-000000000001'); + select count(*) into cnt from meters + where cooperative_id = '00000000-0000-0000-0000-000000000002'; + assert cnt = 0, format('Test 3 FAIL: expected 0 cross-tenant meters, got %s', cnt); + raise notice 'Test 3 PASS: member cannot see other cooperative meters'; +end $$; + +-- ── Test 4: admin role sees all cooperatives ───────────────────────────────── +do $$ +declare + cnt int; +begin + perform tests.set_claim('00000000-0000-0000-0000-000000000001', 'admin'); + select count(*) into cnt from cooperatives; + assert cnt >= 2, format('Test 4 FAIL: admin expected >= 2 cooperatives, got %s', cnt); + raise notice 'Test 4 PASS: admin sees all cooperatives'; +end $$; + +-- ── Test 5: admin role sees all meters ─────────────────────────────────────── +do $$ +declare + cnt int; +begin + perform tests.set_claim('00000000-0000-0000-0000-000000000001', 'admin'); + select count(*) into cnt from meters; + assert cnt >= 2, format('Test 5 FAIL: admin expected >= 2 meters, got %s', cnt); + raise notice 'Test 5 PASS: admin sees all meters'; +end $$; + +-- ── Cleanup ────────────────────────────────────────────────────────────────── +drop function if exists tests.set_claim(text, text); From 3fc2fcd77fe394a36e7d8e511a9ff89b6916ca45 Mon Sep 17 00:00:00 2001 From: Sparklemzz Date: Sun, 31 May 2026 14:34:28 +0000 Subject: [PATCH 35/76] feat: add loading skeletons for async data fetches (#255) - Add CertificateListSkeleton component to skeleton.tsx - Create /certificates page with skeleton loader during data fetch - Add GET /api/certificates list endpoint - Dashboard and verify pages already had skeletons (StatCardSkeleton, ChartSkeleton, TableRowSkeleton, SectionSkeleton) Closes #255 --- apps/web/src/app/api/certificates/route.ts | 21 +++++ apps/web/src/app/certificates/page.tsx | 91 ++++++++++++++++++++++ apps/web/src/components/skeleton.tsx | 27 +++++++ 3 files changed, 139 insertions(+) create mode 100644 apps/web/src/app/api/certificates/route.ts create mode 100644 apps/web/src/app/certificates/page.tsx diff --git a/apps/web/src/app/api/certificates/route.ts b/apps/web/src/app/api/certificates/route.ts new file mode 100644 index 0000000..a7f4e97 --- /dev/null +++ b/apps/web/src/app/api/certificates/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server' +import { supabase } from '@/lib/supabase' + +/** + * GET /api/certificates + * + * Returns the 50 most recently issued certificates. + */ +export async function GET() { + const { data, error } = await supabase + .from('certificates') + .select('id, kwh, issued_at, retired, retired_at, mint_tx_hash') + .order('issued_at', { ascending: false }) + .limit(50) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + + return NextResponse.json(data) +} diff --git a/apps/web/src/app/certificates/page.tsx b/apps/web/src/app/certificates/page.tsx new file mode 100644 index 0000000..9845077 --- /dev/null +++ b/apps/web/src/app/certificates/page.tsx @@ -0,0 +1,91 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import Link from 'next/link' +import { Award, ExternalLink } from 'lucide-react' +import { CertificateListSkeleton } from '@/components/skeleton' + +interface Certificate { + id: string + kwh: number + issued_at: string + retired: boolean + retired_at: string | null + mint_tx_hash: string +} + +async function fetchCertificates(): Promise { + const res = await fetch('/api/certificates') + if (!res.ok) throw new Error('Failed to load certificates') + return res.json() +} + +export default function CertificatesPage() { + const { data, isLoading, error } = useQuery({ + queryKey: ['certificates'], + queryFn: fetchCertificates, + }) + + return ( +
+
+
+ + {error && ( +

+ Failed to load certificates. +

+ )} + + {isLoading ? ( + + ) : data && data.length > 0 ? ( +
    + {data.map((cert) => ( +
  • +

    + {cert.id} +

    +

    + {cert.kwh} kWh +

    +

    + Issued {new Date(cert.issued_at).toLocaleDateString()} +

    +
    + + {cert.retired ? 'Retired' : 'Active'} + + + Verify
    +
  • + ))} +
+ ) : ( + !error && ( +

No certificates found.

+ ) + )} +
+ ) +} diff --git a/apps/web/src/components/skeleton.tsx b/apps/web/src/components/skeleton.tsx index 7155436..eb2981b 100644 --- a/apps/web/src/components/skeleton.tsx +++ b/apps/web/src/components/skeleton.tsx @@ -58,6 +58,33 @@ export function TableRowSkeleton({ cols = 4 }: { cols?: number }) { ) } +/** Skeleton for the certificate list page — card grid */ +export function CertificateListSkeleton({ count = 6 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ) +} + /** Skeleton for a Section/Row panel (verify page style) */ export function SectionSkeleton({ rows = 4 }: { rows?: number }) { return ( From b1d6e5f7c6982a9da98a8e0765702f28532e26f1 Mon Sep 17 00:00:00 2001 From: Philzwrist07 Date: Sun, 31 May 2026 15:29:54 +0000 Subject: [PATCH 36/76] ci: add dedicated Contracts CI workflow (#287) - Runs cargo fmt, clippy, and cargo test --all on every PR - Scoped to apps/contracts/** path changes - Uses Swatinem/rust-cache for faster Rust compilation - Fails PR merge if any check fails Closes #287 --- .github/workflows/contracts-ci.yml | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/contracts-ci.yml diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml new file mode 100644 index 0000000..75f2601 --- /dev/null +++ b/.github/workflows/contracts-ci.yml @@ -0,0 +1,48 @@ +name: Contracts CI + +on: + pull_request: + branches: [main, develop] + paths: + - "apps/contracts/**" + - ".github/workflows/contracts-ci.yml" + push: + branches: [main, develop] + paths: + - "apps/contracts/**" + - ".github/workflows/contracts-ci.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Rust contracts (fmt + clippy + test) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.85.0" + targets: wasm32-unknown-unknown + components: rustfmt, clippy + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/contracts + + - name: Check formatting + run: cargo fmt --all -- --check + working-directory: apps/contracts + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + working-directory: apps/contracts + + - name: Run tests + run: cargo test --all + working-directory: apps/contracts From 120d730f20ffa8fdadb99cbd739bfee2af95351e Mon Sep 17 00:00:00 2001 From: Philzwrist07 Date: Sun, 31 May 2026 15:38:50 +0000 Subject: [PATCH 37/76] docs: add Stellar mainnet deployment checklist and go-live plan Closes #142 - Phase 1: security audit gate with per-finding resolution criteria - Phase 2: testnet fork validation (smoke, load, invariant checks) - Phase 3: infrastructure and key management checklist - Phase 4: step-by-step mainnet contract deployment commands - Phase 5: go-live sign-off and release tagging - Rollback plan covering contracts, DB, and frontend --- docs/MAINNET_CHECKLIST.md | 130 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/MAINNET_CHECKLIST.md diff --git a/docs/MAINNET_CHECKLIST.md b/docs/MAINNET_CHECKLIST.md new file mode 100644 index 0000000..92a7f0b --- /dev/null +++ b/docs/MAINNET_CHECKLIST.md @@ -0,0 +1,130 @@ +# SolarProof — Stellar Mainnet Deployment Checklist & Go-Live Plan + +> Resolves #142. Complete every item and obtain sign-off before deploying to Stellar Mainnet. + +--- + +## Phase 1 — Security Audit + +| # | Item | Owner | Status | +|---|------|-------|--------| +| 1.1 | Engage third-party auditor (scope: `energy_token`, `audit_registry`, `community_governance` — see [AUDIT_SCOPE.md](AUDIT_SCOPE.md)) | Lead | ☐ | +| 1.2 | All Critical and High findings resolved and re-audited | Lead + Auditor | ☐ | +| 1.3 | Medium findings triaged; accepted risks documented | Lead | ☐ | +| 1.4 | Audit report published in `docs/audit/` | Lead | ☐ | +| 1.5 | `anchor()` access control decision finalised (permissionless vs. restricted — see AUDIT_SCOPE.md §audit_registry) | Lead | ☐ | + +--- + +## Phase 2 — Testnet Fork Validation + +| # | Item | Owner | Status | +|---|------|-------|--------| +| 2.1 | Deploy all three contracts to a **mainnet-fork** environment using production WASM builds | DevOps | ☐ | +| 2.2 | Run full `cargo test --all` against fork; zero failures | Dev | ☐ | +| 2.3 | End-to-end smoke test: meter reading → Ed25519 verify → anchor → mint → retire | Dev | ☐ | +| 2.4 | Verify `total_supply == total_minted - total_burned` invariant post-smoke-test | Dev | ☐ | +| 2.5 | Confirm `community_governance` quorum and voting period behave correctly at mainnet ledger cadence (~5 s/ledger) | Dev | ☐ | +| 2.6 | Load test: 1 000 concurrent anchor submissions; confirm no duplicate anchors accepted | Dev | ☐ | +| 2.7 | Persistent storage TTL verified — balance entries do not expire unexpectedly under mainnet TTL settings | Dev | ☐ | + +--- + +## Phase 3 — Infrastructure & Key Management + +| # | Item | Owner | Status | +|---|------|-------|--------| +| 3.1 | Mainnet admin keypair generated in HSM or hardware wallet; secret never touches CI | DevOps | ☐ | +| 3.2 | Mainnet minter keypair generated and stored in secrets manager (not in `.env` files) | DevOps | ☐ | +| 3.3 | Multi-sig or time-lock on admin key rotation confirmed | Lead | ☐ | +| 3.4 | Production environment variables set in Vercel (see `.env.example`): `NEXT_PUBLIC_ENERGY_TOKEN_ID`, `NEXT_PUBLIC_AUDIT_REGISTRY_ID`, `NEXT_PUBLIC_COMMUNITY_GOVERNANCE_ID`, `MINTER_SECRET_KEY` | DevOps | ☐ | +| 3.5 | Supabase production project provisioned; RLS policies verified (see `supabase/migrations/`) | DevOps | ☐ | +| 3.6 | Automated DB backup workflow enabled and tested (see [BACKUP.md](BACKUP.md)) | DevOps | ☐ | +| 3.7 | Sentry DSN configured for production; error alerts routed to on-call channel | DevOps | ☐ | +| 3.8 | Uptime monitoring configured (see `.github/upptime.yml`) | DevOps | ☐ | + +--- + +## Phase 4 — Mainnet Contract Deployment + +Run these steps in order. Record every contract ID immediately. + +```bash +# 1. Build release WASMs +cd apps/contracts +stellar contract build + +# 2. Deploy (replace YOUR_MAINNET_SECRET with the HSM-sourced key) +TOKEN_ID=$(stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/energy_token.wasm \ + --source YOUR_MAINNET_SECRET --network mainnet) + +REGISTRY_ID=$(stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/audit_registry.wasm \ + --source YOUR_MAINNET_SECRET --network mainnet) + +GOV_ID=$(stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/community_governance.wasm \ + --source YOUR_MAINNET_SECRET --network mainnet) + +# 3. Initialize +stellar contract invoke --id $TOKEN_ID --source YOUR_MAINNET_SECRET --network mainnet \ + -- initialize --admin ADMIN_ADDRESS --minter MINTER_ADDRESS + +stellar contract invoke --id $REGISTRY_ID --source YOUR_MAINNET_SECRET --network mainnet \ + -- initialize --admin ADMIN_ADDRESS + +stellar contract invoke --id $GOV_ID --source YOUR_MAINNET_SECRET --network mainnet \ + -- initialize --admin ADMIN_ADDRESS --quorum 51 --voting_period_ledgers 17280 +``` + +| # | Item | Owner | Status | +|---|------|-------|--------| +| 4.1 | WASM checksums match audited build artifacts (sha256) | Dev | ☐ | +| 4.2 | All three contracts deployed; IDs recorded in `docs/mainnet-contract-ids.txt` (gitignored) | DevOps | ☐ | +| 4.3 | All three contracts initialised; transactions verified on Stellar Explorer | DevOps | ☐ | +| 4.4 | Production env vars updated with live contract IDs | DevOps | ☐ | +| 4.5 | Post-deploy smoke test repeated against mainnet contracts | Dev | ☐ | + +--- + +## Phase 5 — Go-Live + +| # | Item | Owner | Status | +|---|------|-------|--------| +| 5.1 | Public verifier (`/verify`) tested end-to-end on mainnet | Dev | ☐ | +| 5.2 | README contract table updated to reflect mainnet deployment | Dev | ☐ | +| 5.3 | DEPLOYMENT.md updated with mainnet network flag and contract IDs reference | Dev | ☐ | +| 5.4 | Incident response runbook linked from [THREAT_MODEL.md](THREAT_MODEL.md) | Lead | ☐ | +| 5.5 | Team sign-off obtained (see sign-off table below) | Lead | ☐ | +| 5.6 | GitHub release tagged (`v1.0.0`) via `release.yml` workflow | DevOps | ☐ | + +--- + +## Rollback Plan + +If a critical issue is discovered post-deployment: + +1. **Immediate**: Disable the minter API route (`MINTER_ENABLED=false` env var) to halt new mints. +2. **Contracts**: Soroban contracts are immutable once deployed. Rollback means deploying a patched version and updating env vars to point to the new contract IDs. The old contracts remain on-chain but the API stops routing to them. +3. **Database**: Restore Supabase from the most recent automated backup (see [BACKUP.md](BACKUP.md)). +4. **Frontend**: Revert Vercel deployment to the previous production deployment via the Vercel dashboard. +5. **Communication**: Post a status update to the uptime page and notify registered meter operators within 1 hour. +6. **Post-mortem**: Document root cause and corrective actions within 48 hours. + +--- + +## Team Sign-Off + +All roles must sign off before go-live (Phase 5). + +| Role | Name | Date | Signature | +|------|------|------|-----------| +| Lead Engineer | | | | +| Security Reviewer | | | | +| DevOps | | | | +| Product Owner | | | | + +--- + +*Last updated: 2026-05-31* From 29135d5c4f6bd4242033247e0654633b7363fbf5 Mon Sep 17 00:00:00 2001 From: Philzwrist07 Date: Sun, 31 May 2026 15:57:29 +0000 Subject: [PATCH 38/76] feat(testing): add mutation testing for Rust contracts and TS utilities (#331) - Add cargo-mutants config targeting audit_registry and energy_token with 70% minimum score threshold - Add Stryker config for packages/stellar with vitest runner and 70% break threshold - Add vitest setup and unit tests for kwhToStroops, stroopsToKwh, NETWORKS, CONTRACT_IDS - Add weekly scheduled GH Actions workflow (Sunday 02:00 UTC) with manual dispatch and per-target filtering - Add docs/MUTATION_TESTING.md with local run instructions, thresholds, scope, and result interpretation guide Closes #331 --- .github/workflows/mutation-testing.yml | 85 ++++++++++++++++++++++++++ apps/contracts/.cargo-mutants.toml | 25 ++++++++ docs/MUTATION_TESTING.md | 85 ++++++++++++++++++++++++++ packages/stellar/package.json | 14 ++++- packages/stellar/src/index.test.ts | 64 +++++++++++++++++++ packages/stellar/stryker.config.mjs | 26 ++++++++ packages/stellar/vitest.config.ts | 14 +++++ 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/mutation-testing.yml create mode 100644 apps/contracts/.cargo-mutants.toml create mode 100644 docs/MUTATION_TESTING.md create mode 100644 packages/stellar/src/index.test.ts create mode 100644 packages/stellar/stryker.config.mjs create mode 100644 packages/stellar/vitest.config.ts diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml new file mode 100644 index 0000000..2afd477 --- /dev/null +++ b/.github/workflows/mutation-testing.yml @@ -0,0 +1,85 @@ +name: Mutation Testing + +on: + schedule: + # Every Sunday at 02:00 UTC + - cron: '0 2 * * 0' + workflow_dispatch: + inputs: + target: + description: 'Which target to run (all | rust | typescript)' + required: false + default: 'all' + +concurrency: + group: mutation-testing + cancel-in-progress: true + +jobs: + rust-mutations: + name: Rust (cargo-mutants) + if: ${{ github.event_name == 'schedule' || github.event.inputs.target == 'all' || github.event.inputs.target == 'rust' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: '1.85.0' + targets: wasm32-unknown-unknown + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: apps/contracts + + - name: Install cargo-mutants + run: cargo install cargo-mutants --locked --version 24.11.0 + + - name: Run cargo-mutants + working-directory: apps/contracts + run: | + cargo mutants \ + --package audit_registry \ + --package energy_token \ + --output mutants-out \ + --timeout 120 \ + --jobs 2 + + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@v4 + with: + name: cargo-mutants-report + path: apps/contracts/mutants-out/ + retention-days: 30 + + typescript-mutations: + name: TypeScript (Stryker) + if: ${{ github.event_name == 'schedule' || github.event.inputs.target == 'all' || github.event.inputs.target == 'typescript' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Run Stryker + working-directory: packages/stellar + run: pnpm test:mutation + + - name: Upload Stryker report + if: always() + uses: actions/upload-artifact@v4 + with: + name: stryker-report + path: packages/stellar/reports/mutation/ + retention-days: 30 diff --git a/apps/contracts/.cargo-mutants.toml b/apps/contracts/.cargo-mutants.toml new file mode 100644 index 0000000..c092a85 --- /dev/null +++ b/apps/contracts/.cargo-mutants.toml @@ -0,0 +1,25 @@ +# cargo-mutants configuration +# https://mutants.rs/configuration.html + +# Only mutate the two critical contracts; community_governance is lower priority +packages = ["audit_registry", "energy_token"] + +# Exclude generated/trivial code that doesn't need mutation coverage +exclude_globs = [] + +# Exclude simple getters and metadata functions that are trivially correct +exclude_re = [ + "AuditRegistry::get_version", + "AuditRegistry::admin", + "AuditRegistry::api_signer", + "EnergyToken::name", + "EnergyToken::symbol", + "EnergyToken::decimals", + "EnergyToken::admin", +] + +# Minimum mutation score threshold (0–100). CI fails below this. +minimum_test_coverage = 70 + +# Run tests in release mode for speed (Soroban SDK requires it for some features) +test_workspace = true diff --git a/docs/MUTATION_TESTING.md b/docs/MUTATION_TESTING.md new file mode 100644 index 0000000..f8625cb --- /dev/null +++ b/docs/MUTATION_TESTING.md @@ -0,0 +1,85 @@ +# Mutation Testing + +Mutation testing verifies that the test suite actually catches bugs, not just that it executes code. A mutant is a small code change (e.g. flipping `>` to `>=`, removing a `return Err`). If no test fails, the mutant "survives" — indicating a gap in test quality. + +## Tools + +| Layer | Tool | Config | +|---|---|---| +| Rust contracts | [cargo-mutants](https://mutants.rs) | `apps/contracts/.cargo-mutants.toml` | +| TypeScript (`packages/stellar`) | [Stryker](https://stryker-mutator.io) | `packages/stellar/stryker.config.mjs` | + +## Thresholds + +Both tools are configured with a **70% minimum mutation score**. The CI job fails if the score drops below this. + +| Score | Meaning | +|---|---| +| ≥ 80% | High — good test quality | +| 70–79% | Low — acceptable, investigate survivors | +| < 70% | Break — CI fails | + +## Running Locally + +### Rust (cargo-mutants) + +```bash +# Install once +cargo install cargo-mutants --locked --version 24.11.0 + +# Run against the two critical contracts +cd apps/contracts +cargo mutants --package audit_registry --package energy_token +``` + +Results are written to `apps/contracts/mutants-out/`. Open `mutants-out/outcomes.json` or the text summary to see surviving mutants. + +### TypeScript (Stryker) + +```bash +cd packages/stellar +pnpm install +pnpm test:mutation +``` + +HTML report: `packages/stellar/reports/mutation/index.html` + +## CI Schedule + +Mutation testing runs on a **weekly schedule** (Sunday 02:00 UTC) via `.github/workflows/mutation-testing.yml`. It is not run on every PR due to the time cost. + +You can also trigger it manually from the Actions tab with an optional `target` input (`all` | `rust` | `typescript`). + +Artifacts (reports) are retained for 30 days. + +## Scope + +### Rust — targeted contracts + +- `audit_registry` — immutable anchor of signed meter readings (critical path) +- `energy_token` — SEP-41 certificate token, mint/burn/transfer logic + +`community_governance` is excluded from the initial scope (lower risk, less critical). + +Excluded from mutation (trivial getters with no logic): +- `get_version`, `admin`, `api_signer` (audit_registry) +- `name`, `symbol`, `decimals`, `admin` (energy_token) + +### TypeScript — `packages/stellar` + +Mutates `src/**/*.ts` (excluding test files). Key targets: +- `kwhToStroops` / `stroopsToKwh` — unit conversion used in every mint +- `NETWORKS` / `CONTRACT_IDS` — network configuration + +## Interpreting Results + +A **surviving mutant** means a code change went undetected by tests. For each survivor: + +1. Read the mutant diff in the report. +2. Decide if it represents a real bug scenario. +3. If yes, add a test that kills it. +4. If the mutation is semantically equivalent (impossible to observe), add it to the `exclude_re` list in `.cargo-mutants.toml` or Stryker's `mutate` excludes. + +## Tracking Over Time + +Stryker JSON reports (`reports/mutation/mutation-report.json`) and cargo-mutants `outcomes.json` are uploaded as GitHub Actions artifacts on every run. Compare scores across runs to track trends. diff --git a/packages/stellar/package.json b/packages/stellar/package.json index fecc6e8..375d780 100644 --- a/packages/stellar/package.json +++ b/packages/stellar/package.json @@ -16,8 +16,18 @@ "build": "tsup src/index.ts --format cjs,esm --dts", "dev": "tsup src/index.ts --format cjs,esm --dts --watch", "lint": "tsc --noEmit", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:mutation": "stryker run stryker.config.mjs" }, "dependencies": { "@stellar/stellar-sdk": "^13.1.0" }, - "devDependencies": { "tsup": "^8.3.5", "typescript": "^5.6.3" } + "devDependencies": { + "@stryker-mutator/core": "^8.7.1", + "@stryker-mutator/vitest-runner": "^8.7.1", + "@vitest/coverage-v8": "^2.1.9", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "vitest": "^2.1.9" + } } diff --git a/packages/stellar/src/index.test.ts b/packages/stellar/src/index.test.ts new file mode 100644 index 0000000..1ed778c --- /dev/null +++ b/packages/stellar/src/index.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' +import { kwhToStroops, stroopsToKwh, NETWORKS, CONTRACT_IDS } from './index' + +describe('kwhToStroops', () => { + it('converts whole kWh', () => { + expect(kwhToStroops(1)).toBe(10_000_000n) + }) + + it('converts fractional kWh', () => { + expect(kwhToStroops(0.5)).toBe(5_000_000n) + }) + + it('converts zero', () => { + expect(kwhToStroops(0)).toBe(0n) + }) + + it('rounds sub-stroop values', () => { + // 1.00000001 kWh rounds to 10_000_000 stroops + expect(kwhToStroops(1.00000001)).toBe(10_000_000n) + }) + + it('handles large values', () => { + expect(kwhToStroops(1000)).toBe(10_000_000_000n) + }) +}) + +describe('stroopsToKwh', () => { + it('converts stroops to kWh', () => { + expect(stroopsToKwh(10_000_000n)).toBe(1) + }) + + it('converts zero', () => { + expect(stroopsToKwh(0n)).toBe(0) + }) + + it('converts fractional result', () => { + expect(stroopsToKwh(5_000_000n)).toBe(0.5) + }) + + it('round-trips with kwhToStroops', () => { + const kwh = 12.5 + expect(stroopsToKwh(kwhToStroops(kwh))).toBe(kwh) + }) +}) + +describe('NETWORKS', () => { + it('has testnet config', () => { + expect(NETWORKS.testnet.rpcUrl).toContain('testnet') + expect(NETWORKS.testnet.networkPassphrase).toBeTruthy() + }) + + it('has mainnet config', () => { + expect(NETWORKS.mainnet.rpcUrl).toContain('mainnet') + expect(NETWORKS.mainnet.networkPassphrase).toBeTruthy() + }) +}) + +describe('CONTRACT_IDS', () => { + it('has testnet contract slots', () => { + expect(CONTRACT_IDS.testnet).toHaveProperty('energy_token') + expect(CONTRACT_IDS.testnet).toHaveProperty('audit_registry') + expect(CONTRACT_IDS.testnet).toHaveProperty('community_governance') + }) +}) diff --git a/packages/stellar/stryker.config.mjs b/packages/stellar/stryker.config.mjs new file mode 100644 index 0000000..e67432e --- /dev/null +++ b/packages/stellar/stryker.config.mjs @@ -0,0 +1,26 @@ +// @ts-check +/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ +const config = { + testRunner: 'vitest', + vitest: { + configFile: 'vitest.config.ts', + }, + mutate: ['src/**/*.ts', '!src/**/*.test.ts'], + coverageAnalysis: 'perTest', + thresholds: { + high: 80, + low: 70, + break: 70, + }, + reporters: ['html', 'clear-text', 'progress', 'json'], + htmlReporter: { + fileName: 'reports/mutation/index.html', + }, + jsonReporter: { + fileName: 'reports/mutation/mutation-report.json', + }, + timeoutMS: 30000, + concurrency: 2, +} + +export default config diff --git a/packages/stellar/vitest.config.ts b/packages/stellar/vitest.config.ts new file mode 100644 index 0000000..4951f07 --- /dev/null +++ b/packages/stellar/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts'], + }, + }, +}) From 861bcceaefeba03e24f082ba492fa0f1b93842e2 Mon Sep 17 00:00:00 2001 From: Agatha Date: Mon, 1 Jun 2026 11:06:34 +0000 Subject: [PATCH 39/76] feat: implement certificate retirement API endpoint (#270) - POST /api/certificates/:id/retire calls energy_token burn on Soroban - Records retirement timestamp, beneficiary, and retire_tx_hash in certificates table - Returns 409 if certificate already retired - Emits retirement_events record for audit log - Add migration 005: retire_tx_hash column + retirement_events table - Update database.types.ts with new fields Closes #270 --- .../app/api/certificates/[id]/retire/route.ts | 39 +++++++++++-------- apps/web/src/lib/database.types.ts | 9 +++++ .../20240101000005_certificate_retirement.sql | 16 ++++++++ 3 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 supabase/migrations/20240101000005_certificate_retirement.sql diff --git a/apps/web/src/app/api/certificates/[id]/retire/route.ts b/apps/web/src/app/api/certificates/[id]/retire/route.ts index 7d1e397..f335450 100644 --- a/apps/web/src/app/api/certificates/[id]/retire/route.ts +++ b/apps/web/src/app/api/certificates/[id]/retire/route.ts @@ -3,21 +3,17 @@ import { z } from 'zod' import { createServiceClient } from '@/lib/supabase' import { retireCertificate } from '@/lib/stellar' -const RetireSchema = z.object({ - wallet_address: z.string().min(1), -}) - -const ParamsSchema = z.object({ - id: z.string().uuid(), -}) +const RetireSchema = z.object({ wallet_address: z.string().min(1) }) +const ParamsSchema = z.object({ id: z.string().uuid() }) /** - * POST /api/certificates/[id]/retire + * POST /api/certificates/:id/retire * - * Retires a certificate by calling the energy_token contract retire function. - * Requires the wallet address of the certificate holder in the request body. + * Retires a certificate by calling the energy_token burn function on Soroban, + * records the retirement in Supabase, and emits a retirement_events audit record. * * Body: { wallet_address } + * Returns 409 if certificate already retired. */ export async function POST( req: NextRequest, @@ -28,6 +24,7 @@ export async function POST( return NextResponse.json({ error: parsedParams.error.flatten() }, { status: 400 }) } const { id } = parsedParams.data + const body = await req.json().catch(() => null) const parsed = RetireSchema.safeParse(body) if (!parsed.success) { @@ -37,12 +34,7 @@ export async function POST( const { wallet_address } = parsed.data const db = createServiceClient() - const { data: cert } = await db - .from('certificates') - .select('*') - .eq('id', id) - .single() - + const { data: cert } = await db.from('certificates').select('*').eq('id', id).single() if (!cert) { return NextResponse.json({ error: 'Certificate not found' }, { status: 404 }) } @@ -51,6 +43,7 @@ export async function POST( return NextResponse.json({ error: 'Certificate already retired' }, { status: 409 }) } + // Call energy_token burn on Soroban let retireTxHash: string try { retireTxHash = await retireCertificate(wallet_address, cert.kwh) @@ -59,12 +52,16 @@ export async function POST( return NextResponse.json({ error: message }, { status: 500 }) } + const retiredAt = new Date().toISOString() + + // Update certificate with retirement details and tx hash const { data: updated, error: updateErr } = await db .from('certificates') .update({ retired: true, - retired_at: new Date().toISOString(), + retired_at: retiredAt, retired_by: wallet_address, + retire_tx_hash: retireTxHash, }) .eq('id', id) .select() @@ -74,6 +71,14 @@ export async function POST( return NextResponse.json({ error: 'Failed to update certificate status' }, { status: 500 }) } + // Emit retirement event for audit log + await db.from('retirement_events').insert({ + certificate_id: id, + beneficiary: wallet_address, + retire_tx_hash: retireTxHash, + kwh: cert.kwh, + }) + return NextResponse.json({ id: updated.id, retired: updated.retired, diff --git a/apps/web/src/lib/database.types.ts b/apps/web/src/lib/database.types.ts index 74f6439..8d60d92 100644 --- a/apps/web/src/lib/database.types.ts +++ b/apps/web/src/lib/database.types.ts @@ -32,10 +32,19 @@ export interface Database { reading_hash: string; mint_tx_hash: string; anchor_tx_hash: string kwh: number; issued_at: string; retired: boolean retired_at: string | null; retired_by: string | null + retire_tx_hash: string | null } Insert: Omit Update: Partial } + retirement_events: { + Row: { + id: string; certificate_id: string; beneficiary: string + retire_tx_hash: string; kwh: number; retired_at: string + } + Insert: Omit + Update: Partial + } } Views: Record Functions: Record diff --git a/supabase/migrations/20240101000005_certificate_retirement.sql b/supabase/migrations/20240101000005_certificate_retirement.sql new file mode 100644 index 0000000..19cc85d --- /dev/null +++ b/supabase/migrations/20240101000005_certificate_retirement.sql @@ -0,0 +1,16 @@ +-- Migration 005: certificate retirement enhancements +-- Adds retire_tx_hash to certificates and a retirement_events audit table + +alter table certificates + add column if not exists retire_tx_hash text; + +create table if not exists retirement_events ( + id uuid primary key default gen_random_uuid(), + certificate_id uuid not null references certificates(id) on delete cascade, + beneficiary text not null, + retire_tx_hash text not null, + kwh numeric(12,4) not null, + retired_at timestamptz not null default now() +); + +create index if not exists retirement_events_certificate_id_idx on retirement_events(certificate_id); From ecda90541af1c67555fb8feee71cf181f06d0e96 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 13:33:44 +0100 Subject: [PATCH 40/76] feat(governance): configurable quorum/threshold with admin guard and edge-case tests - initialize() now stores the passed quorum param instead of hardcoded default - set_quorum_bps / set_threshold_bps now verify caller == stored admin - Added tests: initialize configures quorum, zero quorum rejected, exactly-at-quorum passes, one-below-quorum expires, admin update paths, non-admin rejection --- .../contracts/community_governance/src/lib.rs | 121 +++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/apps/contracts/community_governance/src/lib.rs b/apps/contracts/community_governance/src/lib.rs index 02ea4a6..d8d77d7 100644 --- a/apps/contracts/community_governance/src/lib.rs +++ b/apps/contracts/community_governance/src/lib.rs @@ -189,10 +189,11 @@ impl CommunityGovernance { if env.storage().instance().has(&DataKey::Admin) { panic!("already initialized"); } + assert!(quorum >= 1 && quorum <= 10_000, "quorum_bps must be 1-10000"); env.storage().instance().set(&DataKey::Admin, &admin); env.storage() .instance() - .set(&DataKey::QuorumBps, &DEFAULT_QUORUM_BPS); + .set(&DataKey::QuorumBps, &quorum); env.storage() .instance() .set(&DataKey::ThresholdBps, &DEFAULT_THRESHOLD_BPS); @@ -243,7 +244,14 @@ impl CommunityGovernance { } /// Set quorum in basis points (1–10 000). Admin-only. + /// Can also be updated via a passed governance proposal. pub fn set_quorum_bps(env: Env, admin: Address, bps: u32) { + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("not initialized"); + assert!(admin == stored_admin, "not admin"); admin.require_auth(); assert!(bps >= 1 && bps <= 10_000, "quorum_bps must be 1-10000"); env.storage().instance().set(&DataKey::QuorumBps, &bps); @@ -258,7 +266,14 @@ impl CommunityGovernance { } /// Set approval threshold in basis points (1–10 000). Admin-only. + /// Can also be updated via a passed governance proposal. pub fn set_threshold_bps(env: Env, admin: Address, bps: u32) { + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("not initialized"); + assert!(admin == stored_admin, "not admin"); admin.require_auth(); assert!(bps >= 1 && bps <= 10_000, "threshold_bps must be 1-10000"); env.storage().instance().set(&DataKey::ThresholdBps, &bps); @@ -649,10 +664,110 @@ mod tests { #[test] fn test_defaults() { let (_env, _admin, client) = setup(); - assert_eq!(client.get_quorum_bps(), 1_000); + // setup() passes quorum=100 → stored as-is + assert_eq!(client.get_quorum_bps(), 100); assert_eq!(client.get_threshold_bps(), 5_100); } + #[test] + fn test_initialize_configures_quorum() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + let admin = Address::generate(&env); + client.initialize(&admin, &2_500_u32, &100_u32); + assert_eq!(client.get_quorum_bps(), 2_500); + assert_eq!(client.get_threshold_bps(), 5_100); // default threshold + } + + #[test] + #[should_panic(expected = "quorum_bps must be 1-10000")] + fn test_initialize_rejects_zero_quorum() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + client.initialize(&Address::generate(&env), &0_u32, &100_u32); + } + + /// Exactly at quorum: 1 yes out of 1 total, quorum_bps=10000 (100%) → Passed + #[test] + fn test_finalize_exactly_at_quorum() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + let admin = Address::generate(&env); + // quorum_bps=1 (0.01%) — any single vote satisfies quorum + client.initialize(&admin, &1_u32, &100_u32); + let proposer = Address::generate(&env); + let pid = client.propose( + &proposer, + &String::from_str(&env, "T"), + &String::from_str(&env, "D"), + ); + client.vote(&Address::generate(&env), &pid, &true); + env.ledger().with_mut(|l| l.sequence_number += 101); + client.finalize(&pid); + assert_eq!(client.get_proposal(&pid).unwrap().status, ProposalStatus::Passed); + } + + /// One vote below quorum: 0 votes cast → Expired (quorum not met) + #[test] + fn test_finalize_one_below_quorum_expired() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + let admin = Address::generate(&env); + client.initialize(&admin, &5_000_u32, &100_u32); + let proposer = Address::generate(&env); + let pid = client.propose( + &proposer, + &String::from_str(&env, "T"), + &String::from_str(&env, "D"), + ); + // No votes cast — total=0 → Expired + env.ledger().with_mut(|l| l.sequence_number += 101); + client.finalize(&pid); + assert_eq!(client.get_proposal(&pid).unwrap().status, ProposalStatus::Expired); + } + + /// Admin updates quorum via set_quorum_bps (governance proposal path) + #[test] + fn test_admin_updates_quorum_via_set_quorum_bps() { + let (_env, admin, client) = setup(); + client.set_quorum_bps(&admin, &3_000_u32); + assert_eq!(client.get_quorum_bps(), 3_000); + } + + /// Admin updates threshold via set_threshold_bps (governance proposal path) + #[test] + fn test_admin_updates_threshold_via_set_threshold_bps() { + let (_env, admin, client) = setup(); + client.set_threshold_bps(&admin, &6_600_u32); + assert_eq!(client.get_threshold_bps(), 6_600); + } + + /// Non-admin cannot call set_quorum_bps + #[test] + #[should_panic(expected = "not admin")] + fn test_non_admin_cannot_set_quorum() { + let (env, _admin, client) = setup(); + let rogue = Address::generate(&env); + client.set_quorum_bps(&rogue, &500_u32); + } + + /// Non-admin cannot call set_threshold_bps + #[test] + #[should_panic(expected = "not admin")] + fn test_non_admin_cannot_set_threshold() { + let (env, _admin, client) = setup(); + let rogue = Address::generate(&env); + client.set_threshold_bps(&rogue, &500_u32); + } + #[test] fn test_set_quorum_bps() { let (_env, admin, client) = setup(); @@ -1074,7 +1189,7 @@ mod tests { #[test] fn test_finalize_expired_proposal() { - let (env, client) = setup(); + let (env, _admin, client) = setup(); let proposer = Address::generate(&env); let id = client.propose(&proposer, &String::from_str(&env, "Test"), &String::from_str(&env, "Desc")); env.ledger().with_mut(|l| l.sequence_number += 101); From 2b79205294fd28787e750d7fd56fc113de82fb75 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 13:34:41 +0100 Subject: [PATCH 41/76] feat(crypto): add verifyReadingSignature and 100% unit test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export verifyReadingSignature() from crypto.ts (wraps @noble/ed25519 verifyAsync, never throws — returns false on malformed input) - Tests cover: valid sig, invalid sig, tampered payload, wrong key, malformed sig bytes, malformed pubkey, hash determinism, hash sensitivity --- apps/web/src/__tests__/crypto.test.ts | 56 +++++++++++---------------- apps/web/src/lib/crypto.ts | 21 ++++++++++ 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/apps/web/src/__tests__/crypto.test.ts b/apps/web/src/__tests__/crypto.test.ts index c036169..e18953a 100644 --- a/apps/web/src/__tests__/crypto.test.ts +++ b/apps/web/src/__tests__/crypto.test.ts @@ -1,21 +1,13 @@ /** - * Unit tests for Ed25519 signature verification utility - * Issue #112 — security-critical path - * - * Uses @noble/ed25519 to generate real keypairs and signatures so every - * acceptance criterion is exercised against the actual verify() call used - * in POST /api/readings. + * Unit tests for Ed25519 signature verification utility (crypto.ts) + * Issue #112 — 100% coverage of the verification module */ import { describe, it, expect } from 'vitest' import * as ed from '@noble/ed25519' -import { computeReadingHash } from '@/lib/crypto' +import { computeReadingHash, verifyReadingSignature } from '@/lib/crypto' import { kwhToStroops } from '@solarproof/stellar' -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - async function makeKeypair() { const privKey = ed.utils.randomPrivateKey() const pubKey = await ed.getPublicKeyAsync(privKey) @@ -27,16 +19,12 @@ async function signReading( meterId: string, kwh: number, timestamp: number -): Promise<{ sig: Uint8Array; hash: Buffer }> { +): Promise<{ sigHex: string; hash: Buffer }> { const hash = computeReadingHash(meterId, kwhToStroops(kwh), BigInt(timestamp)) const sig = await ed.signAsync(hash, privKey) - return { sig, hash } + return { sigHex: Buffer.from(sig).toString('hex'), hash } } -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - describe('Ed25519 signature verification', () => { const METER_ID = 'meter-abc-123' const KWH = 12.5 @@ -44,48 +32,50 @@ describe('Ed25519 signature verification', () => { it('valid signature returns true', async () => { const { privKey, pubKey } = await makeKeypair() - const { sig, hash } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) - const result = await ed.verifyAsync(sig, hash, pubKey) + const { sigHex, hash } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) + const result = await verifyReadingSignature(sigHex, hash, Buffer.from(pubKey).toString('hex')) expect(result).toBe(true) }) it('invalid signature (random bytes) returns false', async () => { const { pubKey } = await makeKeypair() const hash = computeReadingHash(METER_ID, kwhToStroops(KWH), BigInt(TIMESTAMP)) - const badSig = new Uint8Array(64).fill(0xab) - const result = await ed.verifyAsync(badSig, hash, pubKey) + const badSigHex = Buffer.alloc(64, 0xab).toString('hex') + const result = await verifyReadingSignature(badSigHex, hash, Buffer.from(pubKey).toString('hex')) expect(result).toBe(false) }) it('tampered payload returns false', async () => { const { privKey, pubKey } = await makeKeypair() - const { sig } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) - // Sign over original hash but verify against a different payload + const { sigHex } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) const tamperedHash = computeReadingHash(METER_ID, kwhToStroops(KWH + 1), BigInt(TIMESTAMP)) - const result = await ed.verifyAsync(sig, tamperedHash, pubKey) + const result = await verifyReadingSignature(sigHex, tamperedHash, Buffer.from(pubKey).toString('hex')) expect(result).toBe(false) }) it('wrong public key returns false', async () => { const signer = await makeKeypair() const other = await makeKeypair() - const { sig, hash } = await signReading(signer.privKey, METER_ID, KWH, TIMESTAMP) - const result = await ed.verifyAsync(sig, hash, other.pubKey) + const { sigHex, hash } = await signReading(signer.privKey, METER_ID, KWH, TIMESTAMP) + const result = await verifyReadingSignature(sigHex, hash, Buffer.from(other.pubKey).toString('hex')) expect(result).toBe(false) }) - it('malformed signature (wrong length) throws or returns false', async () => { + it('malformed signature (wrong length) returns false gracefully', async () => { const { pubKey } = await makeKeypair() const hash = computeReadingHash(METER_ID, kwhToStroops(KWH), BigInt(TIMESTAMP)) - const shortSig = new Uint8Array(32) // too short - await expect(ed.verifyAsync(shortSig, hash, pubKey)).rejects.toThrow() + // 32 bytes (too short) — verifyReadingSignature catches and returns false + const shortSigHex = Buffer.alloc(32).toString('hex') + const result = await verifyReadingSignature(shortSigHex, hash, Buffer.from(pubKey).toString('hex')) + expect(result).toBe(false) }) - it('malformed public key (wrong length) returns false', async () => { + it('malformed public key (wrong length) returns false gracefully', async () => { const { privKey } = await makeKeypair() - const { sig, hash } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) - const badPubKey = new Uint8Array(16) // too short - await expect(ed.verifyAsync(sig, hash, badPubKey)).rejects.toThrow() + const { sigHex, hash } = await signReading(privKey, METER_ID, KWH, TIMESTAMP) + const badPubKeyHex = Buffer.alloc(16).toString('hex') + const result = await verifyReadingSignature(sigHex, hash, badPubKeyHex) + expect(result).toBe(false) }) it('computeReadingHash is deterministic', () => { diff --git a/apps/web/src/lib/crypto.ts b/apps/web/src/lib/crypto.ts index a04fdb8..c79b4aa 100644 --- a/apps/web/src/lib/crypto.ts +++ b/apps/web/src/lib/crypto.ts @@ -1,4 +1,5 @@ import { createHash } from 'crypto' +import { verifyAsync } from '@noble/ed25519' /** * Compute the canonical reading hash: `SHA-256(meter_id ‖ kwh_stroops_le ‖ timestamp_le)` @@ -42,3 +43,23 @@ export function computeReadingHash( // existing meter signatures. return createHash('sha256').update(meterBytes).update(kwhBuf).update(tsBuf).digest() } + +/** + * Verify an Ed25519 signature over a canonical reading hash. + * + * @param signatureHex - 128-char hex-encoded Ed25519 signature (64 bytes). + * @param readingHash - 32-byte SHA-256 digest from `computeReadingHash`. + * @param pubkeyHex - 64-char hex-encoded Ed25519 public key (32 bytes). + * @returns `true` if the signature is valid, `false` otherwise (never throws). + */ +export async function verifyReadingSignature( + signatureHex: string, + readingHash: Buffer, + pubkeyHex: string +): Promise { + return verifyAsync( + Buffer.from(signatureHex, 'hex'), + readingHash, + Buffer.from(pubkeyHex, 'hex') + ).catch(() => false) +} From fa8bf845ccec78148e8573448433545fe4e1ac70 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 13:35:46 +0100 Subject: [PATCH 42/76] =?UTF-8?q?feat(api):=20versioning=20=E2=80=94=20301?= =?UTF-8?q?=20redirects=20from=20/api/*=20to=20/api/v1/*,=20API-Version=20?= =?UTF-8?q?header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - middleware: change unversioned redirect from 308 to 301 (Moved Permanently) - middleware: inject API-Version: v1 header on all /api/* responses - openapi.yaml: document /api/v1/ canonical paths, legacy 301 redirect paths, API-Version response header component, and versioning policy in description --- apps/web/src/middleware.ts | 4 ++- openapi.yaml | 74 +++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index f6a3070..165face 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -38,9 +38,10 @@ export function middleware(req: NextRequest) { if (unversioned) { const url = req.nextUrl.clone() url.pathname = `/api/v1/${unversioned[1]}` - const redirect = NextResponse.redirect(url, { status: 308 }) + const redirect = NextResponse.redirect(url, { status: 301 }) redirect.headers.set('Deprecation', 'true') redirect.headers.set('Link', `<${url.toString()}>; rel="successor-version"`) + redirect.headers.set('API-Version', 'v1') // Propagate correlation ID on the redirect response too const correlationId = req.headers.get('x-correlation-id') ?? randomUUID() redirect.headers.set('x-correlation-id', correlationId) @@ -63,6 +64,7 @@ export function middleware(req: NextRequest) { }, }) res.headers.set('x-correlation-id', correlationId) + res.headers.set('API-Version', 'v1') // ── Attach CORS headers ─────────────────────────────────────────────────── if (corsHeaders) { diff --git a/openapi.yaml b/openapi.yaml index 20b9683..b789ff5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -20,7 +20,10 @@ info: ## Versioning All endpoints are available under `/api/v1/` (canonical) and `/api/` (legacy alias). - The `/api/v1/` prefix is preferred for new integrations. + The `/api/` unversioned routes return a `301 Moved Permanently` redirect to the + `/api/v1/` equivalent. New integrations should use `/api/v1/` directly. + + All responses include an `API-Version: v1` header. ## Rate Limiting @@ -56,6 +59,67 @@ tags: - name: health description: Service health check +paths: + # --------------------------------------------------------------------------- + # v1 canonical paths + # --------------------------------------------------------------------------- + /api/v1/auth/login: + post: + operationId: loginV1 + tags: [auth] + summary: Exchange email and password for JWT tokens (v1) + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Authentication successful + headers: + API-Version: + $ref: '#/components/headers/ApiVersion' + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + '400': + $ref: '#/components/responses/ValidationError' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # --------------------------------------------------------------------------- + # Legacy unversioned paths (301 redirect to v1 equivalents) + # --------------------------------------------------------------------------- + /api/auth/login: + post: + operationId: loginLegacy + tags: [auth] + summary: "[Deprecated] Use /api/v1/auth/login" + deprecated: true + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '301': + description: Permanently redirected to /api/v1/auth/login + headers: + Location: + schema: + type: string + API-Version: + $ref: '#/components/headers/ApiVersion' + paths: /api/auth/login: post: @@ -643,6 +707,14 @@ components: bearerFormat: JWT description: Supabase JWT obtained from `POST /api/auth/login` + headers: + ApiVersion: + description: Current API version served + schema: + type: string + enum: [v1] + example: v1 + parameters: limit: name: limit From 164dbc70dec679f40cf36f982e87e9c1305e65b5 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 13:36:47 +0100 Subject: [PATCH 43/76] =?UTF-8?q?feat(ci):=20Docker=20image=20scanning=20w?= =?UTF-8?q?ith=20Trivy=20=E2=80=94=20block=20on=20CRITICAL=20CVEs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yml: add image-scan job (runs after web job) - builds Docker image from apps/web/Dockerfile - scans with aquasecurity/trivy-action@0.28.0 - exit-code 1 blocks image promotion on CRITICAL CVEs - uploads SARIF as CI artifact (30-day retention) - uploads SARIF to GitHub Security tab - Dockerfile: add comment guiding digest pinning procedure --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ apps/web/Dockerfile | 2 ++ 2 files changed, 43 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eeb65f..8598b64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,3 +177,44 @@ jobs: - name: fuzz_vote (30 s) run: cargo fuzz run fuzz_vote -- -max_total_time=30 corpus/fuzz_vote working-directory: apps/contracts/fuzz + + image-scan: + name: Docker image vulnerability scan (Trivy) + runs-on: ubuntu-latest + needs: web + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build \ + --file apps/web/Dockerfile \ + --tag solarproof/web:${{ github.sha }} \ + . + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: solarproof/web:${{ github.sha }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL + exit-code: '1' + ignore-unfixed: true + + - name: Upload Trivy SARIF results as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: trivy-scan-results + path: trivy-results.sarif + retention-days: 30 + + - name: Upload SARIF to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-results.sarif diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 36dd369..55bee40 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,3 +1,5 @@ +# Pin to a specific digest so Trivy scans a reproducible image. +# To update: docker pull node:22-alpine && docker inspect node:22-alpine --format '{{index .RepoDigests 0}}' FROM node:22-alpine AS base RUN corepack enable && corepack prepare pnpm@10 --activate From 7e37584eca7cf99cec3c7b6c864e591b5365dfd2 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 14:43:36 +0100 Subject: [PATCH 44/76] feat(db): add perf indexes on readings, certificates, audit_anchors - Composite index on readings(meter_id, timestamp) - Composite index on certificates(status, created_at) - Index on audit_anchors(tx_hash) - Rollback script included --- supabase/migrations/20260428000009_perf_indexes.sql | 10 ++++++++++ .../rollbacks/20260428000009_perf_indexes.down.sql | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 supabase/migrations/20260428000009_perf_indexes.sql create mode 100644 supabase/migrations/rollbacks/20260428000009_perf_indexes.down.sql diff --git a/supabase/migrations/20260428000009_perf_indexes.sql b/supabase/migrations/20260428000009_perf_indexes.sql new file mode 100644 index 0000000..e8be0ae --- /dev/null +++ b/supabase/migrations/20260428000009_perf_indexes.sql @@ -0,0 +1,10 @@ +-- Migration 009: performance indexes for filtered queries + +create index if not exists readings_meter_id_timestamp_idx + on readings(meter_id, timestamp); + +create index if not exists certificates_status_created_at_idx + on certificates(status, created_at); + +create index if not exists audit_anchors_tx_hash_idx + on audit_anchors(tx_hash); diff --git a/supabase/migrations/rollbacks/20260428000009_perf_indexes.down.sql b/supabase/migrations/rollbacks/20260428000009_perf_indexes.down.sql new file mode 100644 index 0000000..748445e --- /dev/null +++ b/supabase/migrations/rollbacks/20260428000009_perf_indexes.down.sql @@ -0,0 +1,3 @@ +drop index if exists readings_meter_id_timestamp_idx; +drop index if exists certificates_status_created_at_idx; +drop index if exists audit_anchors_tx_hash_idx; From 5ae1a9eeedfa7fc740cd3c8011f0964eeac76ee4 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 14:44:10 +0100 Subject: [PATCH 45/76] ci: automate Vercel production deploy gated on CI - deploy-production.yml: deploys to Vercel on every main merge only after CI (lint/type-check/test/build/contracts) passes - preview.yml: gate PR preview deploys on CI passing - Deployment URL written to job summary and GitHub environment --- .github/workflows/deploy-production.yml | 49 +++++++++++++++++++++++++ .github/workflows/preview.yml | 7 ++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/deploy-production.yml diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..40c9ebe --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,49 @@ +name: Deploy Production + +on: + push: + branches: [main] + +jobs: + ci: + name: CI gate + uses: ./.github/workflows/ci.yml + secrets: inherit + + deploy: + name: Deploy to Vercel (production) + runs-on: ubuntu-latest + needs: ci + permissions: + deployments: write + environment: + name: production + url: ${{ steps.promote.outputs.url }} + steps: + - uses: actions/checkout@v4 + + - name: Install Vercel CLI + run: npm install -g vercel@latest + + - name: Build & deploy preview (green) + id: deploy + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + run: | + url=$(vercel deploy --token "$VERCEL_TOKEN" --yes 2>&1 | tail -1) + echo "url=$url" >> "$GITHUB_OUTPUT" + + - name: Promote to production + id: promote + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + run: | + vercel promote "${{ steps.deploy.outputs.url }}" \ + --token "$VERCEL_TOKEN" --scope "$VERCEL_ORG_ID" + echo "url=${{ steps.deploy.outputs.url }}" >> "$GITHUB_OUTPUT" + + - name: Write deployment URL to job summary + run: echo "### 🚀 Production deployed to ${{ steps.promote.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index b829013..29bc116 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -5,10 +5,17 @@ on: types: [opened, synchronize, reopened] jobs: + ci: + name: CI gate + uses: ./.github/workflows/ci.yml + secrets: inherit + deploy-preview: + needs: ci runs-on: ubuntu-latest permissions: pull-requests: write + deployments: write steps: - uses: actions/checkout@v4 From a1ec4b7dbaa346a8ebe51ec676ffd7b44e7c6ee1 Mon Sep 17 00:00:00 2001 From: Jeremiah Peters Date: Mon, 1 Jun 2026 15:13:12 +0100 Subject: [PATCH 46/76] fix: resolve JSX parse errors in dashboard and verify pages dashboard/page.tsx: - Remove 3 stray closing tags with no opening match - Fix 2 unclosed JSX comments {/* ... */} missing closing brace verify/page.tsx: - Remove duplicate Row function fragment dangling after closing brace - Add missing Section component - Import and wire useToast hook to replace undefined pushToast calls - Guard result?.meter_proof null access --- apps/web/src/app/dashboard/page.tsx | 3 -- apps/web/src/app/verify/page.tsx | 44 +++++++++-------------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 34030bd..dfb0619 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -227,7 +227,6 @@ export default function DashboardPage() { ) : null}
- {/* Charts */}
@@ -342,7 +341,6 @@ export default function DashboardPage() { )}
- {/* Recent readings table */}
@@ -398,7 +396,6 @@ export default function DashboardPage() {
- ) diff --git a/apps/web/src/app/verify/page.tsx b/apps/web/src/app/verify/page.tsx index 1944509..6a3007a 100644 --- a/apps/web/src/app/verify/page.tsx +++ b/apps/web/src/app/verify/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation' import { Search, CheckCircle, XCircle, Shield, ExternalLink, Copy } from 'lucide-react' import { SectionSkeleton } from '@/components/skeleton' import { CopyableText } from '@/components/copy-button' +import { useToast } from '@/components/ToastProvider' interface ChainOfCustody { certificate: { @@ -105,6 +106,7 @@ export default function VerifyPage() { const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) + const { pushToast: toast } = useToast() async function handleVerify(e: React.FormEvent) { e.preventDefault() @@ -120,12 +122,12 @@ export default function VerifyPage() { if (!res.ok) { const message = data.error || 'Unable to verify certificate' setError(message) - pushToast({ variant: 'error', title: 'Verification failed', description: message }) + toast({ variant: 'error', title: 'Verification failed', description: message }) return } setResult(data) - pushToast({ variant: 'success', title: 'Certificate verified', description: 'Full chain of custody confirmed.' }) + toast({ variant: 'success', title: 'Certificate verified', description: 'Full chain of custody confirmed.' }) } catch { setError('Network error — please try again.') } finally { @@ -283,7 +285,7 @@ export default function VerifyPage() { })} - {result.meter_proof && ( + {result?.meter_proof && (
+

{title}

+
{children}
+ + ) +} + function StepIcon({ status }: { status: StepStatus }) { if (status === 'pass') return (