fix(grid-wallet-prod): npm ci --ignore-scripts so the image can build - #746
Merged
Conversation
…idebar) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…API panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he layout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…case Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… resize math Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the "Save a passkey?" interstitial sheet — tapping Continue with passkey now starts the WebAuthn create ceremony directly, which also keeps the tap's user activation for the system dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…screen Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd signed cash-out (Phase 2) (#740) ## Summary Wires `components/grid-wallet-prod` from fully-scripted to the **real Grid API** (public sandbox), per the Phase 2 spec (`docs/superpowers/specs/2026-07-22-grid-wallet-prod-live-api-design.md`). The API panel now renders the requests/responses that actually went over the wire. **The full money loop is live-verified end to end:** passkey sign-in → Add $20 (real platform on-ramp, `COMPLETED`) → Cash out $5 (quote → browser-signed `Grid-Wallet-Signature` execute → polled to `COMPLETED` in ~62s), with the displayed balance tracking the real account throughout. ### What's in here - **Server proxy** (`/api/grid/[...path]`): allow-listed paths only, Basic-auth injected server-side, `{request,response}` envelopes with redacted auth — the panel logs real traffic. - **Browser crypto** (`gridCrypto.ts`): TEK keygen, HPKE encrypt/decrypt, Turnkey stamps via `@turnkey/*` — session keys never leave the client; cross-checked against `scripts/embedded-wallet-sign.js`. - **Real auth** (`gridSession.ts`): first-run EMAIL_OTP bootstrap (magic `000000` in sandbox) → registers a real Grid `PASSKEY` credential via WebAuthn → returning sign-ins are pure passkey (challenge → `.get()` → verify). - **Webhook receiver** (`/api/webhooks`): `X-Grid-Signature` P-256 verification against `GRID_WEBHOOK_PUBKEY`, event ring buffer + poll route. - **Live money**: balance/activity reads, Add money via the real platform on-ramp (the customer-wallet sandbox fund endpoint mints unspendable book balance — discovered live), outbound quote/stamp/execute/poll with completion checkmarks gated on `COMPLETED`. - 38 vitest tests across the pure modules; every task browser-verified against the live sandbox. ### Env contract `GRID_CLIENT_ID/SECRET`, `GRID_API_BASE_URL`, `GRID_CUSTOMER_ID`, `GRID_WEBHOOK_PUBKEY` in `.env.local` (git-ignored). Prod cutover = swap those values; first tap re-runs passkey registration against the production customer. ### Tracked pre-prod follow-ups (not in this PR) - ~~Gate `GET /api/webhooks/events`~~ — route removed; deliveries are pushed over SSE instead of polled. - Proxy customer-scoping before production keys (today it's a single-customer sandbox gateway). - ~~Wire real activity rows onto the phone~~ — done below. - Phone-side failure states for execute errors (panel-only today). - Report upstream: `POST /quotes` returns undocumented 200 for platform-sourced quotes. --- ## Update — funding flows, webhook-driven arrivals, sandbox gating A second pass over the same component. Everything below was verified against the live sandbox in a browser; the API panel now shows **only** real traffic. ### Auth follows the documented bootstrap order - Sign-in leads with the credential a Global Account is actually born with (`EMAIL_OTP`), with the entry step prefilled from the credential's own `nickname` — no invented address. - A passkey is added **later**, from the wallet, as its own signed action: one device ceremony authorized by the live session (it used to register *and* immediately authenticate, so a single tap asked for approval twice). - The CTA advertises the credential `signIn` will really use. Passkeys are device-bound, so a passkey listed on the account but registered elsewhere no longer sends this browser into an assertion it can't satisfy. - A lapsed session (15 min) can now re-authenticate in place: the OTP sheet has a surface on the wallet screen, and the WebAuthn call runs inside the tap's user activation instead of behind the re-auth round-trip. ### Funding uses the endpoints Grid actually supports - **Add money → country → deposit instructions**: the customer's own account/routing/rails/reference, read from `GET /customers/internal-accounts`. (The EUR block is a clearly-marked placeholder IBAN in `data/placeholderDeposit.ts` until the customer has an EUR account — the read already returns one section per fiat account, so a real one appears with no code change.) - **…or "add an account"** to pull from. A pull quote sources the **ExternalAccount** — the previous body pointed at the platform's internal account. Grid rejects `/execute` for such a quote (*"funds must be pushed … use POST /sandbox/send"*), so the quote is created and left pending, and `POST /sandbox/send` stands in for the push. Amounts here are USD cents, not USDB micro-units. - **Add from crypto**: Base / Solana / Ethereum, network → amount → the real Grid-provisioned deposit address for that chain. - Saved banks are seeded from `GET /customers/external-accounts` (filtered to USD/EUR, ACTIVE); selecting one quotes against that account instead of re-creating it — which also fixes the stale-recipient gap noted in #740's original follow-ups. ### Webhooks - **`X-Grid-Signature` parsing was rejecting every real delivery.** Grid sends `{"v":"1","s":"…"}` (or a bare base64 header); we required `{"version":1,"signature":"…"}`, so verification never ran and everything 401'd. The earlier tests signed using our own assumed shape, so they passed — they now assert the documented formats. - Verified deliveries **stream to the panel over SSE** (`/api/webhooks/stream`); nothing polls. The bus is pinned to `globalThis` because Next gives each route handler its own module instance — the publisher and subscriber were otherwise in different registries. - **An arrival, not a tap, is what says money landed**: `INCOMING_PAYMENT.COMPLETED` drives the toast and Activity row from its payload and re-reads the balance from the API. Confirming an add no longer claims anything. ### Panel, wallet, packaging - The panel shows only real requests: every synthesized call and response body is gone (including a fabricated `POST https://your-app.com/webhooks/grid` and ~330 lines of stubbed quote/transaction/session responses). Flows with no client call behind them log nothing. - Activity renders the real `GET /transactions` history (EXPIRED quotes filtered out); the balance hero shows **available** with the account **total** beneath, since sandbox book balance runs ahead of spendable. - Bank picker trimmed to the US + euro area (ACH + SEPA); APY 3%. - `Dockerfile` + `.dockerignore` on `output: 'standalone'`. **`NEXT_PUBLIC_GRID_SANDBOX` is a build arg** (NEXT_PUBLIC_* is inlined at build time) and gates the sandbox-only "simulate funding" affordance — a production image must be built without it. - `scripts/ensure-business-customer.mjs`: idempotent `BUSINESS` customer provisioning, writes `GRID_BUSINESS_CUSTOMER_ID` to `.env.local`. 60 vitest tests (was 38). ### Still open - A received webhook only drives the *add* path; `INCOMING_PAYMENT.PENDING` and `OUTGOING_PAYMENT.*` are logged but don't touch the wallet. - The sandbox funding stand-in for the *instructions* screen still runs the platform on-ramp (no quote exists there to `/sandbox/send`). - USDB-native funding over Spark works (`REALTIME_FUNDING` + `cryptoNetwork: SPARK`, rate 1.0, verified) but isn't offered in the UI — both current paths convert. - `docker build` unverified: the daemon wasn't running here. The standalone server itself was run and exercised (page, webhook, SSE, proxy → Grid all responding). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
….example The README still described sign-in as "passkey only" and the live API as "Phase 2, planned" — both contradicted by the code in this branch. It now documents what the app actually does: EMAIL_OTP-first auth with a passkey added later, the push/pull split for funding (and why a pull quote can't be executed), webhook-driven arrival reporting over SSE with the ngrok setup, the sandbox flag and Docker build, and the sandbox quirks worth knowing (skipped email delivery, book-vs-spendable balance). .env.example documents the env contract the README refers to; .env.local stays git-ignored and out of the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2
…ents Every connected panel already received every delivery — the demo drives one hardcoded customer, so there is no per-client filtering and pushEvent walks all subscribers. Verified with three concurrent clients: all three got the event, and one disconnecting left the others streaming. What was missing is replay. A client only saw events that arrived while it was connected, so an EventSource auto-reconnect after a network blip silently dropped anything that landed in the gap. Each frame now carries an `id:` (a per-process sequence). EventSource echoes it back as `Last-Event-ID` on reconnect, and the stream replays events after that point. A FRESH client replays nothing: it has no id, and rendering old deliveries would date them to the moment of receipt. Catch-up must neither gap nor duplicate, which rules out both naive orderings — replay-then-subscribe loses whatever lands in between, and subscribe-then-replay sends that same event twice (live, then again from the backlog). So the stream subscribes immediately but HOLDS live events, replays the backlog, then flushes the held ones the replay didn't already cover. Verified by reconnecting while deliveries stream in: no gaps, no duplicates. The connect handshake comment now reports `clients=N` — invisible to EventSource, visible in `curl -N`, useful when checking that every panel is attached. Tests cover the bus contract: fan-out to every subscriber, survival of a broken one, the seq/eventsSince replay windows, and the ring dropping the overflow a long-gone client can no longer catch up on. Limits unchanged and documented in the module: the bus is per server process (with replicas, only the instance that received a delivery fans it out), and the ring buffer holds the last 50 events. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2
… stream Self-review follow-up. /api/webhooks/stream is unauthenticated and carries whatever Grid delivered — transaction ids, amounts, and counterpartyInformation, which per Grid's schema can include a counterparty's name, birth date and nationality. That is the same exposure the polling route was deleted for earlier in this stack; the SSE replacement inherited it. Reject an EventSource opened by another site (Sec-Fetch-Site: cross-site), so a page on someone else's origin can't attach to a panel running on your machine. Deliberately narrow: only an explicit cross-site is refused, so `curl -N` and the verification scripts keep working. This is NOT access control, and the comment says so: anyone who can reach the host can still read the stream, which behind a tunnel means anyone with the URL. Gating it properly (session cookie, or not exposing it past localhost) stays a pre-prod requirement alongside proxy customer-scoping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Contributor
Greptile SummaryAligns the container dependency installation with the component’s existing Vercel installation behavior.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| components/grid-wallet-prod/Dockerfile | Updates the dependency stage to skip lifecycle scripts, matching the component’s documented deployment installation behavior. |
Reviews (3): Last reviewed commit: "fix(grid-wallet-prod): npm ci --ignore-s..." | Re-trigger Greptile
Contributor
Author
|
@greptileai review No new commits since your last review — re-triggering for a fresh pass alongside the rest of the stack. |
The Dockerfile added in #740 has never produced an image. `npm ci` dies in the deps stage: the seven @central-icons-react packages each run a postinstall license-check.js that hard-fails unless CENTRAL_LICENSE_KEY is in the environment, and that variable appears nowhere in this repo. npm error path /app/node_modules/@central-icons-react/round-filled-radius-2-stroke-2 npm error command sh -c node ./license-check.js npm error Error: Central Icons license key is not set. vercel.json for this same component already sets `installCommand: npm install --ignore-scripts`, which is why the Vercel deployments work and the container build never did. This aligns the Dockerfile with the install behavior the component is actually developed and shipped against. No dependency here needs a postinstall to produce a working build. Verified linux/arm64 end to end: image builds (332MB), /app/server.js is the entrypoint, NEXT_PUBLIC_GRID_SANDBOX inlines into the client bundle, the server boots and serves / with 200 carrying no Grid credentials, and an unsigned POST /api/webhooks is rejected 401 by the signature check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mariano-macri
force-pushed
the
grid-wallet-prod-dockerfile-fix
branch
from
July 27, 2026 18:26
9549323 to
5b58855
Compare
mariano-macri
changed the base branch from
07-25-feat_grid-wallet-prod_payouts_reach_every_corridor_only_funding_is_rail-limited
to
07-24-fix_grid-wallet-prod_replay_missed_webhooks_to_reconnecting_sse_clients
July 27, 2026 18:26
Contributor
Author
|
@greptileai review Rebased onto |
pengying
previously approved these changes
Jul 27, 2026
pengying
force-pushed
the
07-24-fix_grid-wallet-prod_replay_missed_webhooks_to_reconnecting_sse_clients
branch
3 times, most recently
from
July 27, 2026 21:57
7172ecc to
63fb4ad
Compare
Base automatically changed from
07-24-fix_grid-wallet-prod_replay_missed_webhooks_to_reconnecting_sse_clients
to
main
July 27, 2026 22:01
mariano-macri
enabled auto-merge (squash)
July 28, 2026 15:35
Stacked on #746 (which makes the image buildable at all). Builds `components/grid-wallet-prod` and pushes it to the ECR repo vended by `tooling-infra` (`terraform/apps/grid-wallet-prod`), so the app can be deployed to the internal tooling EKS cluster behind Okta. ## What it does On pushes to `main` touching `components/grid-wallet-prod/**`: assume `github-actions-grid-wallet-prod` via GitHub OIDC, build `linux/arm64`, push an immutable `sha-<gitsha>` tag. ## Three things worth reviewing **Landing order is mandatory.** The path filter includes the workflow's own file, so merging this before the component stack reaches `main` triggers a run against a `main` with no `components/grid-wallet-prod/` directory, and the build fails on a missing working directory. **Land the component stack first, this second.** **`linux/arm64`** because the tooling-prod cluster is Graviton (m8g). No other workflow in this repo uses an ARM label yet — if the first run sits in `queued`, GitHub-hosted ARM runners aren't enabled for the org and the fallback is `setup-qemu` + `setup-buildx` on `ubuntu-latest` (noted inline). ARC runners are *not* an option here: they have no Docker daemon. **The sandbox flag is a build arg, not config.** `NEXT_PUBLIC_*` is inlined by Next at build time, so `--build-arg NEXT_PUBLIC_GRID_SANDBOX=true` is written literally in the workflow and has **no default** in the Dockerfile. A future production image would be a separate workflow against a separate ECR repo and cannot inherit the sandbox affordance by accident. ## Idempotency The ECR repo is `IMMUTABLE`-tagged, so re-pushing an existing tag is a hard failure. The build is guarded by a `describe-images` check that distinguishes *"tag absent"* from *"the call failed"* — a bare `if aws ... >/dev/null 2>&1` is exempt from `set -e` and would treat an IAM error as a missing image, then die on the immutable push. Only `ImageNotFoundException` means "go build"; anything else fails loudly. No `workflow_dispatch`: the IAM trust policy pins the `sub` claim to `refs/heads/main`, so a dispatch from any other ref could not authenticate. ## Depends on `tooling-infra` PR (ECR repo + `github-actions-grid-wallet-prod` role) must be applied before this runs, or the first push has nothing to authenticate against. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
pengying
approved these changes
Jul 28, 2026
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.
The Dockerfile added in #740 has never produced an image
npm cidies in the deps stage. The seven@central-icons-reactpackages each run a postinstalllicense-check.jsthat hard-fails unlessCENTRAL_LICENSE_KEYis set, and that variable appears nowhere in this repo:#740 noted the build as unverified ("the daemon wasn't running here"). It's not merely unverified — it cannot succeed as written.
Why
--ignore-scriptsis the right fix herecomponents/grid-wallet-prod/vercel.jsonalready sets:{ "installCommand": "npm install --ignore-scripts" }That's why the Vercel deployments of this component work and the container build never did. This aligns the Dockerfile with the install behavior the component is actually developed and shipped against. No dependency here needs a postinstall to produce a working build.
Verified end to end (linux/arm64)
/app/server.jsis the entrypoint (flat, not nested — this component has its own lockfile and the repo root has noworkspaces)NEXT_PUBLIC_GRID_SANDBOXinlines into the client bundle/with 200 carrying no Grid credentialsPOST /api/webhooksis rejected 401 by the signature checkScope
Dockerfile only. Splitting this out from the CI-workflow PR that stacks on top of it, since this is a bug fix you want whether or not the tooling-cluster deployment ever happens.
🤖 Generated with Claude Code