Repository navigation
fix(sep10,ci): self-consistent SEP-10 domains & token expiry; CI migration gate (#671 #673) - #852
Merged
Cjay-Cyber-2 merged 1 commit intoOct 19, 2026
Conversation
…add CI migration gate Issue mergepay#673: two strictness defects around SEP-10 auth. - home_domain used one grammar that required a dotted FQDN, so it rejected the single-label / with-port values that config.SEP10_HOME_DOMAIN falls back to (hostOf(API_PUBLIC_URL) -> "localhost:4000"). A client that obeyed the server's own mismatch error was then rejected for the shape of the domain it was told to send. Extracted a shared grammar in src/lib/sep10-domain.ts: home domains and boot config accept a hostname with an optional port; client_domain keeps the strict FQDN rule (it names the origin of the client's stellar.toml and has no such fallback). - ACCESS_TOKEN_TTL_SECONDS and TOKEN_EXPIRY_MARGIN_SECONDS were validated independently, so a margin >= TTL boots cleanly and then rejects every access token the moment it is minted. Added a cross-field refine. Issue mergepay#671: add a CI migration job. build-test has no DB and integration provisions schema with `db push`, so nothing ever ran `migrate deploy` — the migration history had 5 broken steps and 2 schema tables never created by any migration. Neutralized the superseded steps to documented no-ops (safe: deploy has always failed at the first one, and db push writes no checksum), added a `migrate diff`-generated reconciliation migration, and a `migration` job asserting validate + deploy + status + zero drift.
|
@Teescom 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! 🚀 |
Cjay-Cyber-2
approved these changes
Oct 19, 2026
Cjay-Cyber-2
left a comment
Collaborator
There was a problem hiding this comment.
Looks good — approving.
Cjay-Cyber-2
approved these changes
Oct 19, 2026
Cjay-Cyber-2
left a comment
Collaborator
There was a problem hiding this comment.
Looks good — approving.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One PR closing two assigned issues:
home_domaingrammar that rejected the value the server itself falls back to, and an access‑token lifetime that could be configured shorter than its own expiry margin.migrationjob that runsprisma migrate deployfor the first time and asserts the migrations converge onschema.prisma, plus the migration‑history repair that makes such a gate able to pass.The challenge verification logic (
src/services/sep10.ts) was already strict and well covered; these fixes are the two seams around it that were provably broken, plus the migration gate.#673 — SEP‑10 domain & token‑expiry strictness
1.
home_domainrejected the server's own fallback domain/auth/verifytakes an optionalhome_domainand, when it differs from the configured domain, returns an error namingconfig.SEP10_HOME_DOMAIN(src/routes/auth.ts).SEP10_HOME_DOMAINdefaults tonew URL(API_PUBLIC_URL).host, which keeps the port and in local/container dev is a single label —hostOfeven hard‑codeslocalhost:4000as its fallback.The request schema used one regex for all three domain fields that required a dotted FQDN and forbade a port:
Measured on the base commit
26c6ff4:localhost:4000→ false,api.mergepay.app→ true. So a client that obeyed the server's own error message ("sendhome_domain=localhost:4000") was then rejected for the shape of that value.Fix — src/lib/sep10-domain.ts defines two grammars:
sep10DomainSchema(home domains + boot configSEP10_HOME_DOMAIN/WEB_AUTH_DOMAIN): a hostname with an optional port1‑65535; still refuses a URL (http://…), underscores, empty/hyphen‑edge labels, a bad port, and >253 chars. Accepting a port is not a weakening — the value only ever has to equal the server's own configured domain, and signature verification binds the challenge to it.clientDomainSchema(client_domain/clientDomain): keeps main's exact strict FQDN rule. A client domain names the origin itsstellar.tomlis fetched from, so a single label orhost:porthas nothing to resolve. This field's strictness was deliberately not relaxed — the testtests/schemas/auth.test.tsnow pins both halves of that split.2. A token lifetime shorter than its own expiry margin
verifyTokenrejects a token whose remaining lifetime is belowTOKEN_EXPIRY_MARGIN_SECONDS, whilesignTokensetsexp = now + ACCESS_TOKEN_TTL_SECONDS(src/services/jwt.ts). On base the two numbers were validated independently (config.ts:79and:85), and the only cross‑field.refinewas the pre‑existing HORIZON‑URL/network check. SoACCESS_TOKEN_TTL_SECONDS=25with the default 30s margin boots cleanly and then rejects every access token the instant it is issued — a total auth outage with no other failure to point at.Fix — a cross‑field refine:
ACCESS_TOKEN_TTL_SECONDS > TOKEN_EXPIRY_MARGIN_SECONDS, reported on theACCESS_TOKEN_TTL_SECONDSpath.#671 — CI migration status check
ci.ymlhad two jobs:build-test(no database) andintegration, which provisions its schema withprisma db push— anddb pushbuilds the database straight fromschema.prismaand never readsprisma/migrations/. Nothing in CI ever ranprisma migrate deploy, so the migration history was free to be undeployable and to drift from the schema.Reproduced on an untouched checkout of the base
prisma migrate deployagainst a fresh Postgres aborts at the 3rd of 38 migrations:Enumerating the full set (drop/recreate/no‑op loop, every file restored afterwards), five migrations cannot be deployed and two schema tables are never created by any migration:
20260724000000_add_webhooks"Group"— builds PascalCase tables that match no model20260728000000_add_audit_log_group_idaudit_logs.group_id— re‑adds a column an earlier migration created20260728010000_add_idempotency_key_statusidempotency_keys.user_id20260729000000_add_idempotency_key_uniqueness"IdempotencyKey"— a table the schema never declares20260729000000_idempotency_key_lifecycleidempotency_keys.user_idtreasury_approvals,status_history, and the withdrawal/anchor retry columns thatschema.prismadeclaresThe repair
20260828000000_webhook_delivery_queue,20260728000000_scope_idempotency_keys,20260801000000_idempotency_key_lifecycle_state). They are rewritten as documented no‑ops, keeping the directories so nothing is renumbered. This matches the repo's own established style (reconcile_worker_lease_columns,settlement_job_statealready converge defensively withIF NOT EXISTS).20260929000000_reconcile_schema_drift— is the literal output ofprisma migrate diff --from-migrations … --to-schema-datamodel … --shadow-database-url … --script, so by construction it is exactly the SQL that makes a deploy‑built database equalschema.prisma: drops the drift the schema does not declare, adds the objects it does.Why rewriting history here is safe:
migrate deployhas always failed atadd_webhooks, so no database can hold a successful_prisma_migrationschecksum for any of these, and every real environment is built withdb push, which writes no checksum at all. Thejob_*columns dropped fromsettlementsare declared only onAnchorSessionin the schema and referenced nowhere insrc/; the onesrc/mention of the renamed index is a doc‑comment example.The gate
A new
migrationjob (postgres:16 service, same healthcheck pattern asintegration) runs, on a fresh DB:prisma validate→migrate deploy→prisma migrate status→prisma migrate diff … --exit-code. Addedprisma:validateandprisma:check-driftscripts for local use; the requiredDATABASE_URLis documented in the job comment. It is not loosening anything — it adds a gate where there was none.Verification (local replicas of all CI jobs, on the rebased head)
Mutation proofs that the gates are not vacuous:
20260929000000_reconcile_schema_driftmakesprisma:check-driftreturn exit 2 and flagstreasury_approvals/status_history— so the reconcile migration is load‑bearing.drift_probetable makes the same check return exit 2, and dropping it returns it to 0.26c6ff4:migrate deployexits 1 (P3018/42P01); the old domain grammar rejectslocalhost:4000; config has no TTL>margin refine.tests/sep10-domain-config.test.ts(45 tests) cover the grammar accept/reject boundaries (253/254, 63/64 label, ports), thehostOf‑echo invariant, boot‑time config validation, the TTL>margin refusals, and the home/client two‑grammar split.Required environment
migrationjob setsDATABASE_URLfor its own service; no new secrets.prisma:check-driftreads$DATABASE_URL(must point at an already‑deployed database).