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 ?