Skip to content

fix(grid-wallet-prod): npm ci --ignore-scripts so the image can build - #746

Merged
mariano-macri merged 19 commits into
mainfrom
grid-wallet-prod-dockerfile-fix
Jul 28, 2026
Merged

fix(grid-wallet-prod): npm ci --ignore-scripts so the image can build#746
mariano-macri merged 19 commits into
mainfrom
grid-wallet-prod-dockerfile-fix

Conversation

@mariano-macri

Copy link
Copy Markdown
Contributor

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 set, 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.
          Please set CENTRAL_LICENSE_KEY in your environment

#740 noted the build as unverified ("the daemon wasn't running here"). It's not merely unverified — it cannot succeed as written.

Why --ignore-scripts is the right fix here

components/grid-wallet-prod/vercel.json already 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)

  • image builds, 332MB
  • /app/server.js is the entrypoint (flat, not nested — this component has its own lockfile and the repo root has no workspaces)
  • NEXT_PUBLIC_GRID_SANDBOX inlines into the client bundle
  • the server boots and serves / with 200 carrying no Grid credentials
  • an unsigned POST /api/webhooks is rejected 401 by the signature check

Scope

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

pengying and others added 16 commits July 21, 2026 13:36
…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
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 28, 2026 4:06pm
grid-wallet-demo Ignored Ignored Preview Jul 28, 2026 4:06pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Aligns the container dependency installation with the component’s existing Vercel installation behavior.

  • Adds --ignore-scripts to npm ci so Central Icons license-check postinstall scripts do not prevent image builds.
  • Documents why lifecycle scripts are skipped for this component.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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

@mariano-macri

Copy link
Copy Markdown
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
mariano-macri force-pushed the grid-wallet-prod-dockerfile-fix branch from 9549323 to 5b58855 Compare July 27, 2026 18:26
@mariano-macri
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
@mariano-macri

Copy link
Copy Markdown
Contributor Author

@greptileai review

Rebased onto 07-24-fix_...replay_missed_webhooks (#742) so this PR no longer sits on top of a draft — commit is now 5b58855, content unchanged (1 file, +8/-1).

pengying
pengying previously approved these changes Jul 27, 2026
@pengying
pengying force-pushed the 07-24-fix_grid-wallet-prod_replay_missed_webhooks_to_reconnecting_sse_clients branch 3 times, most recently from 7172ecc to 63fb4ad Compare July 27, 2026 21:57
Base automatically changed from 07-24-fix_grid-wallet-prod_replay_missed_webhooks_to_reconnecting_sse_clients to main July 27, 2026 22:01
@pengying
pengying dismissed their stale review July 27, 2026 22:01

The base branch was changed.

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>
@mariano-macri
mariano-macri merged commit 047bec9 into main Jul 28, 2026
7 checks passed
@mariano-macri
mariano-macri deleted the grid-wallet-prod-dockerfile-fix branch July 28, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants