Repository navigation
Conversation
…nd verify endpoints Adds generic SEP-10 infrastructure (ADR 0005 step 4, issue DocForum#77): before a patient or facility wallet can be trusted as payer or payee, the backend must prove the caller controls that Stellar address. - GET /payments/sep10/challenge?account=G… — authenticated + verified email; full strkey checksum validation; builds a server-signed challenge via @stellar/stellar-sdk WebAuth helpers (Manage Data nonce, 5-minute time bounds, home domain + web_auth_domain) and records the issuance binding (userId, account, nonce, expiresAt). - POST /payments/sep10/verify — walks the SEP-10 server verification list (structure, sequence 0, server source, time bounds, operation shape, server signature), then client signatures: multisig weight thresholds loaded from Horizon via verifyChallengeTxThreshold, or the spec's master-key rule for unfunded accounts via verifyChallengeTxSigners. Bound to the authenticated user and account; nonce consumed with ONE conditional UPDATE so concurrent replays can't both succeed. - New Sep10Challenge model (unique nonce, userId, account, expiresAt, consumedAt, createdAt, index on expiresAt) + migration. - Dedicated SEP10_SIGNING_SECRET config, separate from payer/releaser keys, validated at startup (names vars, never secrets), documented in .env.example. - Horizon access sits behind an injectable seam, so 15 unit + 18 integration tests cover happy path, replay, concurrency, expiry, wrong signer, multisig thresholds, unfunded accounts, user/account binding and tampering with zero network. Docs: ARCHITECTURE.md §4.10, ARCHITECTURE_ESSENTIALS.md data-model map, ROADMAP.md Phase 5.5 + changelog, docs/api/README.md endpoint reference. Closes DocForum#77 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
…place typed-in addresses) Replaces the facility-only, typed-in wallet address with SEP-10-verified owner-scoped links (issue DocForum#78, ADR 0005 step 4), stacked on the DocForum#77 SEP-10 branch. - Schema: WalletLink generalized to ownerType (patient | facility) + ownerId with verificationMethod ('sep10' | 'unverified_legacy'), verifiedAt, replacedAt and a unique activeOwnerKey sentinel — one ACTIVE link per owner at the DB level (Prisma cannot express a partial unique index; the sentinel's unique index can, and survives migrate dev). Re-linking deactivates instead of mutating/deleting. PaymentIntent gains fundedWalletAddress, the payee snapshot written once at funding. - Migration: hand-ordered so it is safe on populated tables; existing facility rows backfill to active unverified_legacy links (proven by a migration test that inserts an old-shape row, applies only this migration, and asserts the backfill + constraint participation). - POST /payments/wallet-link: strict body { transaction } only — a raw address is a 400. Owner resolved from the caller's own profile (403 otherwise); address derived from the verified challenge; link creation and the SEP-10 nonce consumption commit in ONE transaction (verifySep10Challenge now takes a transaction client), so a failed verification creates no link and burns no nonce. - Patient link responses carry ADR 0005's on-chain privacy notice verbatim (unit-tested against the ADR file so it cannot drift). - Funding guard: fundPaymentIntent refuses non-sep10 facility links after WALLET_LINK_UNVERIFIED_FUNDING_CUTOFF (default 2027-01-01, startup-validated, documented in .env.example) with 409 WALLET_LINK_UNVERIFIED, and snapshots fundedWalletAddress so re-linking never rewrites an escrow's payee. - Tests: 8 unit + 12 integration (fake Horizon + fake escrow SDK — no network): happy paths both roles, strict body, failed/replayed/wrong- user verification, DB constraint + concurrent creates, re-link escrow isolation, cutoff refusal/allowance, migration backfill. - Docs: ARCHITECTURE.md §4.11, ARCHITECTURE_ESSENTIALS payments section, docs/api/README.md wallet-link reference, ROADMAP Phase 5.5 + changelog. Closes DocForum#78 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
@affy-exe Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
CI is failing on this PR. Typecheck fails: Please push a fix and CI will re-run. Before pushing, run |
|
CI is red on typecheck: the controllers call service exports that don't exist in this branch, so it looks like part of the change didn't get committed:
This overlaps with #148 (SEP-10 server), so it may depend on it. If so, say so in the description, or include the missing pieces here. It also conflicts with |
|
@affy-exe Thanks for the PR. It can't be merged yet. To unblock it: CI is failing. Here's what broke:
Please fix these errors and run the same steps locally before pushing ( Merge conflicts: this branch conflicts with Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge. |
|
@affy-exe CI is failing on this branch against the latest Backend (typecheck + test + build) The new |
|
@affy-exe This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@affy-exe This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@affy-exe This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
feat(payments): verified wallet links for patients and facilities (replace typed-in addresses)
Closes #78
What changed and why
WalletLinkwas facility-only and stored whatever address a facility typed in — a typo or someone else's address became the escrow payee. ADR 0005 replaces it with SEP-10-verified links for facilities (payees) and patients (payers in wallet mode).Sep10Challengesibling,WalletLinkrewrite): owner generalized toownerType(patient|facility) +ownerId(polymorphic, no FK — same shape asFulfillmentRecord, ownership validated in the service), plusverificationMethod(sep10|unverified_legacy),verifiedAt,replacedAt, andactiveOwnerKey. One active link per owner is enforced at the DB level by the unique index onactiveOwnerKey(set to"<ownerType>:<ownerId>"while active,NULLonce replaced — Prisma's DSL cannot express a partialWHERE replacedAt IS NULLindex, and a raw-SQL one would be dropped as drift by the nextprisma migrate dev; Postgres treats NULLs as distinct, so replaced rows coexist freely). Re-linking deactivates instead of mutating/deleting — history preserved.PaymentIntent.fundedWalletAddress: snapshot of the payee the escrow was actually funded to, written once with theescrowedtransition.POST /payments/wallet-link: body is strict{ transaction }— a rawstellarPublicKey/accountfield is a 400. The owner is resolved from the caller's own profile (patient→theirPatientProfile, facility→theirFacilityProfile; doctor/admin → 403). The address is derived from the verified challenge account, never read from input. Link creation + SEP-10 nonce consumption commit in one transaction (verifySep10Challengegained an optional transaction client): failed verification → no link and no burned nonce; a lost one-active-link race → nonce restored.docs/adr/0005-non-custodial-patient-payments.md.WALLET_LINK_UNVERIFIED_FUNDING_CUTOFF(own env var, startup-validated, documented in.env.example, default2027-01-01T00:00:00Z), funding a facility whose active link isunverified_legacyfails with409 WALLET_LINK_UNVERIFIED; before the cutoff legacy links still work;sep10links always work.fundPaymentIntentreads the owner's active link and snapshots its address; release/refund never consult the link table (they useescrowId), so replacing a link cannot touch an escrow already funded to the old address — integration-tested end-to-end.WalletLink write-path audit (reviewer check: no verified link without passing SEP-10)
Repo-wide grep for
walletLink.create|update|upsert|delete,createWalletLink,upsertWalletLink,verificationMethod:payments/repositories:createWalletLinkverificationMethod: 'sep10'(literal). Sole caller:services/wallet-link.ts→ inside the transaction, afterverifySep10Challengeresolves. Not exported as a service; no other module imports it.payments/repositories:deactivateActiveWalletLinkreplacedAt+ clears sentinel) — can never mint a link or change an address/method.upsertWalletLink(facilityId, address)(the typed-in path)linkWallet(facilityId, publicKey)WalletLink(gen-platform-identities.mjswrites.envonly;mark-email-verified.mjsis auth; no seeds exist).unverified_legacy(+ owner/active fields), neversep10.tests/integration/wallet-link,tests/testnet-integration/payments)unverified_legacypayee directly, with a comment — no product service can create one.Open design decisions
2027-01-01T00:00:00Z. Fail-closed with a defined grace window: deployments get a real deadline without needing to remember the var, but "never enforce" can't be the default. A malformed value stops boot (names the var, never a value); the funding service is separately fail-closed on a NaN cutoff (500 PAYMENTS_MISCONFIGURED).FacilityProfile.userIdis unique today (1:1), so "authorised for that facility" = the facility's own user; the moment multi-user facilities arrive, an authorisation table is needed. Deliberate, noted in code.PaymentIntent.fundedWalletAddressrecords the literal payee string handed to the SDK (what the chain knows), rather than a FK to a link row; link rows are immutable anyway, but the snapshot survives any future link-table changes and needs no join. Historical pre-[Phase 5.5] Verified wallet links for patients and facilities (replace typed-in addresses) #78 intents haveNULL(their payee was never recorded — noted in the migration).ownerType+ownerIdwithout FK — mandated by the issue; inherits theFulfillmentRecord§6.1.3 caveat (service-level ownership checks, no referential integrity on owner deletion). Existing profiles are never deleted by any flow; documented in the schema.migrate devdrift footgun). Same guarantee, portable, survives tooling.Proposedwhile its step 4 (this work) is implemented — the issue assigned here sanctions building it and no factual conflict with the ADR was found (ADR unedited, as instructed). If the ADR is revised, the notice constant's test will catch any wording change.