Share the browser half from shared/, and drop templated views - #2
Merged
Conversation
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.
Groundwork for
php-js,python-flask-js,ruby-sinatra-jsandjava-springboot-js. Thecurrent scheme does not survive them: six apps × five files is thirty copies kept identical by
hand, and the one line that was allowed to differ — the config injection — would need six
template dialects and six exceptions in the CI diff. Both problems are removed here, before any
new app is written.
shared/ is the source of truth
The copies stay committed, so nothing about an app directory changes:
cd go-js && go run .still works with no pre-step and the release archives are the same shape. A frontend change is
now: edit
shared/, run the script, commit both.CI stops diffing pairs of files and instead runs the script and fails on
git diff --exit-code.That check has no per-file list and no exceptions, so adding a language costs one line in the
script and nothing in CI — the two arrays at the top of
sync-shared.share the wholeconfiguration.
nextjs/is in the second array: its scripts and views are React components, soit takes only the stylesheet, and that one file is what keeps the three from looking different.
Every shared file gained a header line saying where the source of truth is, so it travels into
the copies — which is exactly where someone about to edit the wrong file will read it.
No templated HTML anywhere
checkout.htmlandresult.htmlare now served byte for byte as they sit on disk. The servergenerates
config.jsinstead, and the page loads it with an ordinary<script src>:GET {prefix}/config.jswindow.CONFIG = {basePath, sdkUrl, endpointId, amount, currency, ephemeralTicket}GET {prefix}/result-config.jssdkUrland without a ticketTwo endpoints rather than one so the result page does not mint a payment ticket it will never
use. Both are
Cache-Control: no-store— the ticket is single-use.go-jsloseshtml/templateentirely;nodejs-express-jslosesrenderPageand theCONFIG_LINEregex, and with it the footgun its own CLAUDE.md documented, where a plainreplace('__CONFIG__', …)hit the explanatory comment instead of the script tag.The gateway-failure path moved with it. A failed ticket call used to make
GET /answer502.
config.jshas to stay valid JavaScript, so it now emitserrorin place ofephemeralTicket, andcheckout.jstreats a missing ticket as terminal: the reason goes to theconsole, the payer gets copy they can act on, and the button says "Reload to try again" instead
of inviting a click that cannot work. That last part also fixes a pre-existing bug — a failed
SDK script load used to enable the button.
One 3DS return for every language
redirect_urlnow points at{prefix}/result/callbackin all three apps:POST {prefix}/result/callbackverifiessha1(status + orderid + merchant_order + MERCHANT_CONTROL), answers403on a mismatch, and otherwise303s to{prefix}/resultwith those same four parameters in the query;
GET {prefix}/resultverifies them again with the same function before serving anything.Forwarding the gateway's own
controlis what makes this work with no second signing scheme andno server-side state: the browser carries the identifiers but cannot forge them, and an edited
URL gets a
403rather than a page that polls somebody else's order.result.htmlreads theorder from
location.search, so it needs nothing injected either.This also removes the special case
nextjs/had: it drops thehttpOnlycookie andreturn-cookie.tsand does exactly what the other two do. The one remaining difference isdocumented — a React page cannot set a status code without the experimental
forbidden(), so afailed check there renders the empty page instead of a
403. Refusing to show the order is thepart that matters.
nodejs-express-js/deploy/nginx.confneeded a fix that is easy to miss: its\.js$rule servedassets off the disk and would have swallowed
config.jsandresult-config.js, breaking everypayment. It now names the three real static files one by one, with a comment saying why.
Verification
All three apps were run against the sandbox and compared, not just built.
config.jsongo-jsandnodejs-express-js:200,text/javascript,no-store, the samesix keys, a fresh 731-byte ticket per request. The checkout page renders identically to
before, the three iframes are created from it, the theme toggle still reaches them, and the
pay button enables on
onReady.303to the identical URL in allthree (the Go redirect builds the query by hand rather than with
url.Values.Encode, whichsorts, so the payer sees the same address everywhere); a tampered POST gives
403; a tamperedGET /resultgives403in Go and Express and the empty page in Next; a bareGET /resultshows "nothing to show". Following a valid signed URL polls the real order and renders the
full approved panel — amount,
VISA •••• 4448,ANNA WEBER, reference, approval code.API_URLpointed at a closed port:config.jsstill returnsvalid JavaScript with
error, the page shows "This page could not be prepared for a payment",all three fields take the error ring, the button is disabled, the SDK is never loaded, and the
console carries
no ephemeralTicket: fetch failed../scripts/sync-shared.sh && git diff --exit-codeon a clean tree — what CI runs — passes.gofmt -l,go vet,go build(in agolang:1.24container: Go is not installed on thismachine);
npm ci,node --check,npm run build;yarn lint,yarn build.Two gaps worth stating. Entering a card by hand into the cross-origin iframes stopped working
through the browser automation partway through, so the tokenize → Sale → status leg was not
re-driven by hand on
go-jsthis time; it is unchanged code, and the result panel was verifiedagainst a real approved order from the gateway. And the local
.envstill has no genuineMERCHANT_CONTROL, so the callback checks were exercised with a locally computed signature, asin the previous PR.