diff --git a/.env.example b/.env.example index 981bee4..b80c96a 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,5 @@ NEXT_PUBLIC_INDEXER_URL=http://localhost:3001 # USDC token contract (Stellar Asset Contract) used for locks, and its issuer shown in the UI. NEXT_PUBLIC_USDC_CONTRACT_ID= NEXT_PUBLIC_USDC_ISSUER= +# Feature flag: enable direct-recipient locks (create_direct_lock). Default false. +NEXT_PUBLIC_ENABLE_DIRECT_PAYMENTS=false diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7e2c66a..4332438 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,12 +1,19 @@ -# Playwright happy path (request -> send -> claim -> verify) against a dedicated, throwaway -# testnet contract (roadmap M3-19). Signing uses a test-only keypair adapter -# (lib/wallet/test-adapter.ts), active only when NEXT_PUBLIC_E2E_TEST_MODE=1 -- never in a real -# deployment. The indexer is built from kinlock-sdk's own Dockerfile and run against a -# throwaway Postgres container, mirroring the pattern already proven in kinlock-sdk's own CI -# (ci.yml's indexer-image job). +# On pull requests, run the browser privacy test without signing secrets. Run the signed +# request -> send -> claim -> verify flow only after reviewed code is pushed to protected main. +# The test-only keypair adapter (lib/wallet/test-adapter.ts) is active only when +# NEXT_PUBLIC_E2E_TEST_MODE=1 and is never enabled in the pull-request job or a deployment. +# The indexer is built from kinlock-sdk's Dockerfile and run against throwaway Postgres. +# +# Only the signed main-branch run needs the indexer (/send lists payees from it). The pull-request +# privacy test opens /claim/{id}, which reads chain state over RPC, so it runs without the +# indexer. Keeping the indexer off the PR path matters because RPC only serves about 7 days of +# events: once E2E_START_LEDGER ages out of that window the indexer exits at startup, which would +# fail every PR for a reason unrelated to the change under review. name: e2e on: pull_request: + push: + branches: [main] permissions: contents: read env: @@ -25,19 +32,18 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' with: repository: Kinlock-Org/kinlock-sdk path: kinlock-sdk-src - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' with: repository: Kinlock-Org/kinlock-registry - # TEMPORARY: the fixture payee this job needs is still on an unmerged branch - # (kinlock-registry PR #38, self-authored, needs the owner's own review/merge -- - # AGENTS.md's self-approval rule). Revert to the default (main) once it merges. - ref: feat/e2e-fixture-payee path: kinlock-registry-src - name: Start Postgres and build the indexer image + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | docker network create ci docker run -d --name pg --network ci -e POSTGRES_PASSWORD=ci postgres:17 @@ -53,6 +59,7 @@ jobs: rm -rf kinlock-sdk-src - name: Migrate and start the indexer against the fixture contract + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | docker run --rm --network ci -e DATABASE_URL=postgres://postgres:ci@pg:5432/postgres \ --entrypoint node kinlock-indexer:e2e dist/db/migrate.js @@ -68,6 +75,7 @@ jobs: kinlock-indexer:e2e - name: Wait for the indexer to ingest and registry-join the fixture payee + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | # Event ingestion and the registry join run on separate schedules (the join re-runs # every REGISTRY_SYNC_MS, 60s by default, not on every new row) -- checking for "any @@ -92,7 +100,23 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm exec playwright install --with-deps chromium - - run: pnpm e2e + - name: Run privacy E2E on pull requests (no signing secrets) + if: github.event_name == 'pull_request' + run: pnpm exec playwright test e2e/claim-fragment-privacy.spec.ts + env: + NEXT_PUBLIC_STELLAR_NETWORK: testnet + NEXT_PUBLIC_RPC_URLS: ${{ env.E2E_RPC_URL }} + NEXT_PUBLIC_CONTRACT_ID: ${{ env.E2E_CONTRACT_ID }} + NEXT_PUBLIC_INDEXER_URL: http://localhost:3001 + NEXT_PUBLIC_USDC_CONTRACT_ID: ${{ env.E2E_TOKEN_ID }} + NEXT_PUBLIC_USDC_ISSUER: ${{ env.E2E_TOKEN_ISSUER }} + # Lock 1 on this job's dedicated fixture contract (the test default 11 is on the + # standing testnet contract). + E2E_PRIVACY_TEST_LOCK_ID: "1" + + - name: Run full signed testnet E2E on protected main + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: pnpm e2e env: NEXT_PUBLIC_E2E_TEST_MODE: "1" NEXT_PUBLIC_STELLAR_NETWORK: testnet @@ -103,8 +127,6 @@ jobs: NEXT_PUBLIC_USDC_ISSUER: ${{ env.E2E_TOKEN_ISSUER }} E2E_SENDER_SECRET: ${{ secrets.E2E_SENDER_SECRET }} E2E_PAYEE_SECRET: ${{ secrets.E2E_PAYEE_SECRET }} - # claim-fragment-privacy.spec.ts's default (lock 11) is on the standing testnet - # contract, not this job's dedicated fixture one -- lock 1 there is the equivalent. E2E_PRIVACY_TEST_LOCK_ID: "1" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: ${{ !cancelled() }} diff --git a/DRIPS_WAVE.md b/DRIPS_WAVE.md index 7d251d8..61ea028 100644 --- a/DRIPS_WAVE.md +++ b/DRIPS_WAVE.md @@ -12,7 +12,7 @@ This document provides a single-page executive and technical summary for the Dri **Kinlock App** is the user-facing web portal allowing senders to create locks, payees to issue payment requests and claim funds, and public observers to verify institutional receipts on Stellar. -- **Real-World Problem:** International payments for education, housing, and contracts suffer from lack of trust, opaque fee deductions, and custodial vulnerability. Kinlock provides a non-custodial, scheduled escrow interface directly bound to Soroban smart contracts. +- **Real-World Problem:** Payments for education and housing can be difficult to reconcile and verify, especially when sender and payee are far apart. Kinlock provides a non-custodial way to lock USDC for a verified institution or any Stellar address, with scheduled releases and public on-chain receipts. The product is market-agnostic and launches market by market; it does not claim that a receipt proves a service was delivered. - **Frontend Architecture & Stellar Integration:** - **Framework:** Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS. - **Wallet Integration:** Swappable multi-wallet support via `@creit.tech/stellar-wallets-kit` (Freighter, xBull, Albedo, Lobstr). @@ -21,9 +21,9 @@ This document provides a single-page executive and technical summary for the Dri - Reference identifiers live exclusively in URL hash fragments (`#r=...&s=...`), strictly preventing client credentials and private references from reaching web servers, analytics tools, or access logs. - Nonce-enforced Content Security Policy (CSP) and site-wide `Referrer-Policy: no-referrer`. Zero third-party analytics scripts on sensitive claim paths. - **Universal & Market-Agnostic Core:** - - 100% externalized internationalization strings (`messages/en.json`). - - Dynamic currency and date formatting using native `Intl` APIs. - - Zero hardcoded national currencies or assumptions; payee metadata is fetched from canonical registry definitions. + - User-visible strings are externalized in the English message file; translated language packs are not yet provided. + - Number, currency, and date formatting use native `Intl` APIs. + - Country and local-currency values come from payee registry data. The sender currently displays USD only because the app has no live FX-rate provider. --- @@ -45,7 +45,7 @@ This document provides a single-page executive and technical summary for the Dri - [x] **Hosted under Organization:** [`Kinlock-Org`](https://github.com/Kinlock-Org) - [x] **Permissive License:** Apache-2.0 ([`LICENSE`](LICENSE)) -- [x] **Branch Protection:** Enforced on `main` (peer reviews required, required checks passing) +- [x] **Branch Protection:** The live `main` rules require one approval and the `ci`, `docs-in-sync`, `e2e`, and `roadmap-check` status checks (checked 2026-11-06). - [x] **Continuous Integration (CI):** Green GitHub Actions validating Biome linting, TypeScript `tsc --noEmit`, Vitest test runner, and Next.js production build - [x] **Security Hardening:** - CodeQL SAST scanning (Actions, JavaScript/TypeScript) @@ -53,23 +53,21 @@ This document provides a single-page executive and technical summary for the Dri - Strict input sanitization with Zod - Hardened headers: CSP with dynamic script-src hashes, HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` - [x] **Testing Rigor:** - - 16 test files with 187 passing unit and integration tests - - Automated tests validating CSP headers, schedule calculation, refund triggers, receipt validation, inline string exclusions, and country-agnostic neutrality + - 16 test files and 197 passing tests on the reviewed working branch (`pnpm test`, run 2026-11-06) + - Automated tests cover CSP headers, schedule calculation, receipt and claim behavior, inline strings, and country-agnostic neutrality. Pull requests run the browser claim-fragment privacy test without signing credentials; the full testnet request → send → claim → verify Playwright flow runs only on protected-main pushes. The full flow previously passed in CI (`M3-19`); the updated workflow still needs a real fork-PR and protected-main run. - [x] **Roadmap & Progress Tracking:** Every PR tracked and enforced via [`ROADMAP.md`](ROADMAP.md) --- ## 4. Drips Wave Task Queue & Issue Board -Issues for external contributors follow a strict three-tier complexity mapping aligned with Drips Wave points: -- **Trivial (100 points):** Copy updates, accessibility improvements, ARIA landmarks, and UI state polish. -- **Medium (150 points):** Client-side export conveniences, locale selector enhancements, and transaction receipt formatting components. -- **High (200 points):** Comprehensive end-to-end integration workflows (Playwright happy-path test harnesses) and multi-wallet pairing fallbacks. +The live issue tracker is authoritative for available work; tasks and point values change as issues are completed or program guidance changes. Before starting, contributors should confirm an issue is still open, assigned as required, mapped to a current roadmap item, and within the safe contribution scope. Contract fund logic and other security-sensitive changes require maintainer review and are not ordinary Wave tasks. -> **Task Safety Rule:** Under `AGENTS.md`, money-moving state transitions must never read from intermediary Postgres services, and URL fragment references must never be logged or sent off-device. Wave tasks strictly reinforce these guardrails. +> **Task safety:** Money-moving decisions read chain state, not Postgres. Claim-link references stay in the browser and must never be logged or sent to a server. Follow the repository contribution guide and issue acceptance criteria. -- **Active Bounty Issues:** [Kinlock App Wave Issues](https://github.com/Kinlock-Org/kinlock-app/issues?q=is%3Aissue+is%3Aopen+label%3Adrips-wave%2Cwave) -- **Contribution Guide:** [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`docs/wave-issue-format.md`](https://github.com/Kinlock-Org/.github/blob/main/docs/wave-issue-format.md) +- **Open issues:** [Kinlock App issues](https://github.com/Kinlock-Org/kinlock-app/issues?q=is%3Aissue+is%3Aopen) +- **Contribution guide:** [`CONTRIBUTING.md`](CONTRIBUTING.md) and the [Wave issue format](https://github.com/Kinlock-Org/.github/blob/main/docs/wave-issue-format.md) +- **Current implementation and remaining tasks:** [`ROADMAP.md`](ROADMAP.md) --- diff --git a/README.md b/README.md index 21d24c1..b00a6e9 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,10 @@ A 25-second overview of the flow — lock USDC for a verified payee, the payee c | `/claim/[id]` | Payee verifies the reference against the on-chain `ref_hash`, then releases or declines | | `/locks/[id]` | Lock detail with refund, shown only when the contract allows it | | `/r/[txHash]/[eventIndex]`, `/verify` | Receipts rebuilt from chain data. "Payment to verified payee" — nothing more | -| `/payee`, `/attester` | **Stubs** (`M3-11`, `M3-12`) | +| `/payee` | Payee dashboard: connect a wallet, view and filter locks addressed to the payee, and open a lock's claim page. List data comes from the indexer; actions read chain state. | +| `/attester` | Placeholder page; the read-only account-check tooling is not implemented yet (`M3-12`). | -> **Status: the core money flows are real; the dashboard isn't.** Claim, decline, refund, receipts and verify read the **chain** through the SDK, never the indexer. `/send` is `M3-06` in progress, the Playwright happy path (`M3-19`) is still a `test.fixme` stub, and there is no live FX rate yet, so amounts show **USD only with a note**. Full state in `ROADMAP.md` (Phase 4: 11 `DONE`, 6 `IN PROGRESS`, 9 `TODO`). +> **Status:** the sender, claim, lock-detail, payee dashboard and receipt flows are implemented. Money-moving actions and receipt verification read chain state through the SDK; the payee dashboard uses indexer data for lists. The Playwright request → send → claim → verify test is implemented, and the roadmap records it as passing in CI (`M3-19`). Local E2E runs need the dedicated testnet setup and signing secrets. There is no live FX-rate provider yet, so the app shows USD only with an explanatory note. See `ROADMAP.md` for current status and remaining work. ## The rules this repo exists to enforce @@ -39,22 +40,22 @@ A 25-second overview of the flow — lock USDC for a verified payee, the payee c - **Nothing is hard-coded to a country.** Country and `local_currency` come from registry data. Every user-visible string lives in `messages/en.json`, keys are type-checked, and a test fails the build on an inline string; `Intl` handles numbers, currencies and dates (`M3-23` done — other locales are deferred, not missing by accident). - **Only `lib/sdk.ts` talks to the contract.** Wallets go through `lib/wallet/` using the Stellar Wallets Kit, so Freighter, xBull, Albedo and Lobstr are swappable. -One caveat worth knowing: until the app is wired to the deployed indexer, `/send` and `/request` offer the payees in `lib/payees/known.ts` — a checked-in snapshot of the three testnet fixture payees (KE, NG, PH). It is not trusted for display: each entry's `payee_id` and `meta_hash` are re-derived locally and matched against a live `getPayee` chain read, and status and payout address always come from chain. +Payee discovery in `/send`, `/request`, and `/payee` reads from the configured indexer. If the indexer is not configured or is unavailable, the pages display an honest unavailable state (`pages.*.indexerDown`). Money-moving actions and receipt verification read chain state directly through the SDK (hard rule 3: chain is truth). ## Quick start ``` pnpm install pnpm dev -pnpm lint && pnpm typecheck && pnpm test # biome, tsc, vitest (13 files, 69 cases) -pnpm e2e # placeholder until M3-19 +pnpm lint && pnpm typecheck && pnpm test +pnpm e2e # Playwright; requires configured testnet E2E setup and secrets ``` Next.js 16 (App Router), React 19, TypeScript 7, Tailwind 4. Node 20 in CI. Configuration is Zod-validated in `lib/config.ts`. Required: `NEXT_PUBLIC_STELLAR_NETWORK`, `NEXT_PUBLIC_RPC_URLS`, `NEXT_PUBLIC_CONTRACT_ID`, `NEXT_PUBLIC_USDC_CONTRACT_ID`, `NEXT_PUBLIC_USDC_ISSUER`. `NEXT_PUBLIC_INDEXER_URL` is optional — only list views need it, and the deployed testnet app currently runs without it. Copy `.env.example` to `.env.local`; never commit values. -Deploys to Vercel are currently **manual** (`vercel --prod`) — there's no deploy workflow, so a merged PR does not go live by itself. +Vercel deploys are currently **manual** (`vercel --prod`); a merged PR does not go live by itself. ## Maintainers diff --git a/ROADMAP.md b/ROADMAP.md index 98976da..b74a503 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ | | | |---|---| -| **Last updated** | 2026-11-01 (fix/scorecard-hardening: M3-33 DONE) | +| **Last updated** | 2026-11-09 (fix/prelaunch-hardening: M3-42..M3-44 DONE, M3-45 TODO) | | **Docs baseline** | v0.3, worldwide scope (`PRD.md`, `ARCHITECTURE.md`, `ARCHITECTURE_ESSENTIALS.md`, `AGENTS.md`, `CLAUDE.md`, `project_structure.md`) | | **Current phase** | Phase 0 (Foundations) → starting Phase 1 (M0 Validate) | | **Readiness** | See [§3](#3-progress-snapshot) | @@ -131,7 +131,7 @@ Readiness % = `DONE ÷ (all rows − DEFERRED − DROPPED)`. Conditional rows (P | 1 M0 Validate | 17 | 1 | 6 | 10 | 0 | 0 | 0 | 6% | | 2 Contract + registry | 41 | 24 | 3 | 14 | 0 | 0 | 0 | 59% | | 3 SDK + indexer | 21 | 14 | 3 | 4 | 0 | 0 | 0 | 67% | -| 4 App | 34 | 25 | 6 | 3 | 0 | 0 | 0 | 74% | +| 4 App | 45 | 34 | 7 | 4 | 0 | 0 | 0 | 76% | | 5 Testnet pilot | 7 | 0 | 0 | 7 | 0 | 0 | 0 | 0% | | 6 Pre-mainnet features | 10 | 0 | 0 | 10 | 0 | 0 | 0 | 0% | | 7 Hardening | 26 | 0 | 0 | 26 | 0 | 0 | 0 | 0% | @@ -139,7 +139,7 @@ Readiness % = `DONE ÷ (all rows − DEFERRED − DROPPED)`. Conditional rows (P | 9 Conditional ramp | 9 | 0 | 0 | 0 | 0 | 9 | 0 | n/a | | 10 Wave + community | 10 | 5 | 1 | 4 | 0 | 0 | 0 | 50% | | 11 Deferred parking lot | 16 | 0 | 0 | 0 | 0 | 16 | 0 | n/a | -| **All** | **218** | **85** | **20** | **88** | **0** | **25** | **0** | **44%** | +| **All** | **229** | **94** | **21** | **89** | **0** | **25** | **0** | **46%** | --- @@ -331,6 +331,17 @@ Readiness % = `DONE ÷ (all rows − DEFERRED − DROPPED)`. Conditional rows (P | M3-32 | `roadmap-check.yml` exempts Dependabot (`if: github.actor != 'dependabot[bot]'`): Dependabot has no way to know AGENTS.md §12's ROADMAP.md convention and can't be configured to edit it, so every Dependabot PR opened after `M3-31` was failing this required check for a reason unrelated to the dependency bump itself | app | P1 | DONE | M3-31 | `roadmap-check` reports success (skipped) on a real Dependabot PR; confirmed via GitHub's own status-checks docs that a job skipped by a job-level `if` reports "Success" and does not block a required check, not assumed | | M3-33 | Scorecard remediation, Tier 2: switch CodeQL from GitHub's default setup to advanced codeql.yml; pin malware-scan.yml action SHAs and semgrep version; remove downloadThenRun curl pipe in e2e.yml; enrich SECURITY.md with disclosure process and response timelines | app | P2 | DONE | M3-31 | SAST confirmed 10/10 on Scorecard; malware-scan actions pinned by commit SHA; downloadThenRun pattern removed; full test suite passes | | M3-34 | Universal payments UI support: add dual-mode selector (Verified payee vs Any Stellar address) in /send, wire direct recipient to preflight and createLock, update receipts and lock views to show "Payment to recipient" for direct locks | app | P0 | DONE | M2-28 | ADR-0031; Biome lint passes; 167 tests pass; production build succeeds | +| M3-35 | Make the required E2E check safe and actionable for external fork PRs, where GitHub does not expose testnet signing secrets | app | P1 | IN PROGRESS | M3-19 | A real fork PR completes required checks without exposing signing secrets to untrusted code; the testnet happy-path still runs in a trusted context and cannot be silently skipped | +| M3-36 | Revert mock payee fallback on indexer downtime: show unavailable state (`payees = null`) and delete mock-data.ts | app | P0 | DONE | M3-26 | Prelaunch review Item 2; hard rule 11 and hard rule 3 respected; `listPayees()` throws on indexer failure / unset URL; `mock-data.ts` and `NEXT_PUBLIC_USE_MOCK_DATA` removed; tests verify indexer downtime yields unavailable state | +| M3-37 | Gate direct-mode UI behind `NEXT_PUBLIC_ENABLE_DIRECT_PAYMENTS` (default false) until deployed contract supports `create_direct_lock` | app | P0 | DONE | M3-34 | Prelaunch review Item 3; direct mode hidden by default to prevent simulation errors on pre-ADR-0031 deployed contract; mode toggle suppressed when flag is false; unit tests verify flag parsing and default | +| M3-38 | Prevent double payment and preserve claim link on pending tx | app | P0 | DONE | M3-06, M2-30 | Prelaunch review Item 4; save pending submission to local storage before submit; on TX_PENDING enter pending state with txHash and poll getTransaction without re-enabling Send; on confirmation build and store claim link in claimLinks.v1 and clean up pending; on failure return to review; unit tests verify pending submission helpers, SDK pollTransaction, and hook lifecycle | +| M3-39 | Preflight verdict: require acknowledgement on unknown block check | app | P0 | DONE | M3-06, M2-31 | Prelaunch review Item 7; `verdict()` returns `needsAcknowledgement` when any `severity === "block"` check has `status === "unknown"`; unit tests verify unknown block checks do not pass automatically; 202 tests pass | +| M3-40 | Apply nonce CSP site-wide to protect stored claim links across all application routes | app | P0 | DONE | M3-14 | Prelaunch review Item 8; proxy matcher applies nonce CSP site-wide excluding static assets and images; connect-src covers self, rpcOrigins, indexer origin, https://*.stellar.org, and https://albedo.link; tests verify connect-src and policy; 203 tests pass | +| M3-41 | Vendor @kinlock/sdk tarball to pin sha512 integrity in pnpm-lock.yaml | app | P0 | DONE | M3-03 | Prelaunch review Item 9; vendor kinlock-sdk-0.3.2.tgz in vendor/ with file: dependency in package.json; pnpm-lock.yaml pins sha512 integrity; 203 tests pass; build clean | +| M3-42 | Make the pending-send fix (M3-38) real: vendor `@kinlock/sdk` 0.3.3 (the 0.3.2 tarball lacked `TX_PENDING` and the trustline fix), never default a claim link to lock 1 when the lock id can't be read, resume a pending send after a reload, and re-export the SDK's own `KinlockError` so `instanceof` checks match SDK errors again | app | P0 | DONE | M3-38, M3-41, M2-33 | Vendored tarball contains `TX_PENDING` and `trustLine()` handling with a `sha512` in the lockfile; `confirmedLockId` returns null instead of a default; reload of `/send` with a pending record shows the pending screen and keeps the record on timeout (checked in a production build); `lib.KinlockError` is the SDK class; 206 tests pass | +| M3-43 | Render every route per request so the site-wide nonce CSP (M3-40) doesn't block prerendered pages | app | P0 | DONE | M3-40 | `pnpm build` lists every page as dynamic; in `pnpm start`, `/` and `/attester` load with every script carrying the request nonce and no CSP violations (previously every script on `/` was blocked) | +| M3-44 | Run the pull-request privacy E2E without the indexer: it only reads chain state, and the indexer exits at startup once `E2E_START_LEDGER` ages out of RPC's ~7-day event window, which failed every PR | app | P0 | DONE | M3-35 | Indexer steps in `e2e.yml` run only on pushes to `main`; `claim-fragment-privacy.spec.ts` passes locally against a production build with no indexer running and lock 1 on the fixture contract | +| M3-45 | Keep the signed main-branch E2E working past RPC retention: the fixture contract's `PayeeRegistered` event (ledger 5421502) is older than testnet RPC history, so its indexer can't ingest the fixture payee. Redeploy and register a fresh fixture contract per run (or on a schedule shorter than retention) | app | P0 | TODO | M3-19 | The `main` E2E run ingests the fixture payee from a contract deployed within RPC retention and passes the request, send, claim, verify flow | --- @@ -542,6 +553,16 @@ Newest first. One entry per PR. Required for every contribution (see §2). | Date | PR / ref | Repo | Rows touched | Summary | |---|---|---|---|---| +| 2026-11-09 | `fix/prelaunch-hardening` (#91) | app | M3-44 DONE, M3-45 TODO (new rows) | Fix the failing `e2e` check on PRs. The indexer exited at startup with `startLedger must be within the ledger range: 5511598 - 5632557`: `E2E_START_LEDGER` (5421502) has aged out of testnet RPC's ~7-day event history. The PR job only runs the claim-fragment privacy test, which reads chain state and doesn't need the indexer, so the indexer steps now run only on `main` pushes. Verified locally: the privacy test passes against a production build with no indexer. The signed `main` run still needs a fixture contract inside RPC retention (M3-45). Progress counts computed with `scripts/roadmap-progress` on a copy and updated by hand. | +| 2026-11-09 | `fix/prelaunch-hardening` | app | M3-42 DONE, M3-43 DONE (new rows) | Follow-up after verifying the pre-launch review fixes. M3-38 and M3-40 were marked DONE before they worked: the app still vendored SDK 0.3.2 (no `TX_PENDING`, no trustline fix), a pending send could build a claim link to lock 1, pending sends were never resumed after a reload, the app's own `KinlockError` broke `instanceof` in the request form, and the nonce CSP blocked every script on prerendered pages. This PR vendors SDK 0.3.3, removes the lock-id default, resumes pending sends on load, re-exports the SDK error class, and forces dynamic rendering at the root layout. `pnpm lint`, `pnpm typecheck`, `pnpm test` (206/206), `pnpm build` run and green; `/`, `/attester` and pending recovery on `/send` checked in a browser against `pnpm start`. Progress counts computed with `scripts/roadmap-progress` on a copy (it can't parse this file's column-padded §3 header) and updated by hand. | +| 2026-11-08 | `fix/sdk-vendor-integrity` | app | M3-41 DONE (new row) | Fix PRELAUNCH_REVIEW Item 9: vendor @kinlock/sdk tarball in vendor/ to pin sha512 integrity in pnpm-lock.yaml (203/203 tests pass, build clean). | +| 2026-11-08 | `feat/mock-payee-fallback` (Item 8 fix) | app | M3-40 DONE (new row) | Fix PRELAUNCH_REVIEW Item 8 (app): apply nonce CSP site-wide to protect stored claim links across all application routes, include indexer origin in originsOf, and cover wildcard stellar.org and albedo.link in connect-src (203/203 tests pass). | +| 2026-11-08 | `fix/preflight-verdict-unknown-ack` | app | M3-39 DONE (new row) | Fix PRELAUNCH_REVIEW Item 7 (App): require sender acknowledgement when a block check returns unknown in preflight verdict (202/202 tests pass). | +| 2026-11-08 | `feat/mock-payee-fallback` (Item 4 fix) | app | M3-38 DONE (new row) | Fix PRELAUNCH_REVIEW Item 4 (app): prevent double payment and preserve claim link on pending tx. Save pending submission before submit; on TX_PENDING enter pending state with txHash and poll getTransaction without re-enabling Send button; on confirmation build and save claim link in claimLinks.v1 and remove pending; on permanent failure return to review; added Pending UI with externalized messages; unit tests verify pending submission helpers and SDK pollTransaction (202/202 tests pass, Biome clean, Next.js build clean). | +| 2026-11-08 | `feat/mock-payee-fallback` (Item 3 fix) | app | M3-37 DONE (new row) | Hide direct mode in kinlock-app behind `NEXT_PUBLIC_ENABLE_DIRECT_PAYMENTS` flag (default false) until deployed contract supports `create_direct_lock` (Item 3 from PRELAUNCH_REVIEW.md). In `/send`, mode toggle is suppressed and forced to verified when flag is false; unit tests verify flag parsing and default; lint, typecheck, tests, and build pass. | +| 2026-11-08 | `fix/revert-mock-payee-fallback` (Item 2 fix) | app | M3-36 DONE, M3-26 updated | Reverted mock payee fallback on indexer downtime: `listPayees()` throws when unconfigured or on fetch failure, server pages catch and set `payees = null` (rendering `pages.*.indexerDown`), deleted `lib/indexer/mock-data.ts`, removed `NEXT_PUBLIC_USE_MOCK_DATA`, and removed mock fallback tests. Restores country-agnostic core (hard rule 11) and chain is truth (hard rule 3). All 195 tests pass, Biome clean, Next.js build succeeds. | +| 2026-11-06 | `feat/mock-payee-fallback` (README and Wave-readiness review) | app | M3-35 IN PROGRESS | Updated the app README and Drips Wave summary to match current functionality and tests; corrected stale counts and placeholder claims. Live branch-protection and fork PR checks exposed a blocker: the required E2E job failed because GitHub withholds signing secrets from fork PRs. Updated the workflow to run the no-secret privacy test for every PR and the full signed testnet flow only on protected-main pushes; removed the stale registry-fixture branch pin. M3-35 remains IN PROGRESS pending a real fork-PR run and trusted-main E2E verification. | +| 2026-11-05 | `feat/mock-payee-fallback` | app | M3-26 (status unchanged, DONE) | Added active fixture mock payee fallback data for `/send`, `/request`, and `/payee` so verified payee selection and locked payment flows work even when the external indexer is unconfigured or unreachable. Falls back to mock payees if indexer returns error/404 or `NEXT_PUBLIC_INDEXER_URL` is unset, or if `NEXT_PUBLIC_USE_MOCK_DATA=true`. All 197 tests pass. | | 2026-11-01 | `fix/scorecard-hardening` | app | M3-33 DONE | OpenSSF Scorecard hardening: pinned malware-scan.yml actions to commit SHAs and semgrep to 1.136.0 (Pinned-Dependencies); removed curl-pipe downloadThenRun pattern in e2e.yml; enriched SECURITY.md with vulnerability reporting instructions and response/disclosure timelines. All 187 tests pass. | | 2026-11-01 | `docs/drips-wave-application` | app | no row changes | Added dedicated DRIPS_WAVE.md application and review summary document for the Drips Wave Stellar program review panel, detailing Next.js web application architecture, live Vercel testnet deployment, strict privacy boundaries, testing hygiene, and maintainers. | | 2026-11-01 | `feat/route-loading-error-states` | app | M3-27 DONE | Closes #37: added route-level designed states (app/loading.tsx, app/locks/loading.tsx, app/payee/loading.tsx, app/error.tsx with autofocus retry, and honest app/not-found.tsx); added accessible Spinner and StatePanel in components/ui/; externalized strings to messages/en.json. | diff --git a/app/layout.tsx b/app/layout.tsx index 435612a..e1eb61b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,13 @@ import "./globals.css"; const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit", display: "swap" }); +/** + * [sec] Every page is rendered per request. proxy.ts sends a nonce-based CSP on every route, and a + * prerendered (static) page can't carry that request's nonce, so the browser would block all of + * its scripts and the page would never hydrate. + */ +export const dynamic = "force-dynamic"; + export const metadata = { title: t("app.name"), description: t("app.tagline"), diff --git a/app/send/SendForm.tsx b/app/send/SendForm.tsx index c5939a1..c5a2170 100644 --- a/app/send/SendForm.tsx +++ b/app/send/SendForm.tsx @@ -7,7 +7,7 @@ import { DateTimeDisplay } from "@/components/time/DateTimeDisplay"; import { ASSET_CODE, MAX_TRANCHES } from "@/lib/constants"; import { shortAddress } from "@/lib/i18n/format"; import { hasMessage, t } from "@/lib/i18n/messages"; -import { fromBaseUnits } from "@/lib/sdk"; +import { fromBaseUnits, publicConfig } from "@/lib/sdk"; import { idsOf, localCurrencyOf, preflightKey } from "./send-logic"; import { type SendForm as Form, useSendForm } from "./useSendForm"; @@ -18,9 +18,24 @@ export interface PayeeOption { } /** The sender flow. Markup only; state lives in useSendForm. Roadmap M3-06. */ -export function SendForm({ payees, issuer }: { payees: PayeeOption[]; issuer: string }) { - const f = useSendForm(idsOf(payees)); - return f.stage === "done" ? : ; +export function SendForm({ + payees, + issuer, + enableDirectPayments, +}: { + payees: PayeeOption[]; + issuer: string; + enableDirectPayments?: boolean; +}) { + const directEnabled = enableDirectPayments ?? publicConfig().NEXT_PUBLIC_ENABLE_DIRECT_PAYMENTS; + const f = useSendForm(idsOf(payees), directEnabled); + if (f.stage === "done") { + return ; + } + if (f.stage === "pending") { + return ; + } + return ; } function Editor({ f, payees, issuer }: { f: Form; payees: PayeeOption[]; issuer: string }) { @@ -46,28 +61,30 @@ function Editor({ f, payees, issuer }: { f: Form; payees: PayeeOption[]; issuer: ) : null} {f.unknownPayee ?

{t("pages.send.prefillUnknownPayee")}

: null} -
- - -
+ {f.directEnabled ? ( +
+ + +
+ ) : null} - {f.mode === "verified" ? ( + {f.mode === "verified" || !f.directEnabled ? (