Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
|
| post("/{cardId}/reveal") { | ||
| try { | ||
| val cardId = call.cardId() | ||
| // Optional body: { "cssUrl": "https://..." } styles this one reveal. | ||
| val body = call.receiveText().ifBlank { "{}" } | ||
| Log.gridRequest("cards.reveal", "$cardId $body") | ||
| val response = GridRest.request("POST", "/cards/${enc(cardId)}/reveal", body) | ||
| Log.gridResponse("cards.reveal", "status=${response.status} (URL redacted)") | ||
| call.respondText(response.body, ContentType.Application.Json, HttpStatusCode.fromValue(response.status)) |
There was a problem hiding this comment.
Card routes lack authorization
The new reveal and card-management routes accept caller-selected card IDs without authenticating the caller or checking ownership, then execute the requests using the server's Grid credentials. Because the sample is designed to be exposed through a tunnel and allows requests from any origin, an untrusted caller can enumerate cards, obtain a reveal URL containing PAN, CVV, and expiry, or freeze, close, and re-limit another card. Add an authorization boundary before exposing these operations; X-Session-Id only routes webhooks and is not an access-control mechanism.
How this was verified: The routes forward unvalidated IDs under account-level credentials, while the application installs no authentication middleware and documents public tunnel exposure.
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/kotlin/src/main/kotlin/com/grid/sample/routes/Cards.kt
Line: 56-64
Comment:
**Card routes lack authorization**
The new reveal and card-management routes accept caller-selected card IDs without authenticating the caller or checking ownership, then execute the requests using the server's Grid credentials. Because the sample is designed to be exposed through a tunnel and allows requests from any origin, an untrusted caller can enumerate cards, obtain a reveal URL containing PAN, CVV, and expiry, or freeze, close, and re-limit another card. Add an authorization boundary before exposing these operations; `X-Session-Id` only routes webhooks and is not an access-control mechanism.
**How this was verified:** The routes forward unvalidated IDs under account-level credentials, while the application installs no authentication middleware and documents public tunnel exposure.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed. Every per-card route (get, patch, reveal, transactions, and the sandbox simulators) now returns 403 unless the card was issued from the caller's X-Session-Id. I also removed the unscoped GET /api/cards list. Verified: reveal and PATCH from another session return 403, and the issuing session still works end to end. This is a sample, so it has no real user auth. The scoping is enough to stop other visitors to a tunneled deployment from reaching a card they didn't create.
| patch("/{cardId}") { | ||
| proxy("cards.update") { | ||
| val body = call.receiveText() | ||
| Log.incoming("PATCH", "/api/cards/${call.cardId()}", body) | ||
| GridRest.request("PATCH", "/cards/${enc(call.cardId())}", body) |
There was a problem hiding this comment.
The generic card create and update proxies log complete request bodies without redaction. Both request schemas permit threeDSecurePassword, so a client using that supported field writes the cardholder's 3-D Secure credential into application logs. Avoid logging these bodies or redact sensitive fields first.
How this was verified: The logging helper only truncates text, while these routes pass complete create and update bodies containing a supported password field to it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/kotlin/src/main/kotlin/com/grid/sample/routes/Cards.kt
Line: 46-50
Comment:
**Card passwords reach logs**
The generic card create and update proxies log complete request bodies without redaction. Both request schemas permit `threeDSecurePassword`, so a client using that supported field writes the cardholder's 3-D Secure credential into application logs. Avoid logging these bodies or redact sensitive fields first.
**How this was verified:** The logging helper only truncates text, while these routes pass complete create and update bodies containing a supported password field to it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed. The card create and update routes no longer log request bodies. The reveal route logs only the card ID and the optional cssUrl body, and never the response URL.
| // Issuance is fee-bearing and irreversible, so one key per attempt: a retry of | ||
| // the same request returns the card from the first call instead of a second card. | ||
| const idempotencyKey = useRef(randomUUID()) |
There was a problem hiding this comment.
The idempotency key is created once per component mount rather than once per request payload. If issuance fails validation and the user edits the JSON before retrying, the materially different request reuses the old key and Grid returns an idempotency conflict. This leaves the flow unable to issue the corrected card without remounting. Retain the key only for retries of an unchanged payload and rotate it when the body changes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/steps/cards/IssueCard.tsx
Line: 19-21
Comment:
**Edited retries reuse keys**
The idempotency key is created once per component mount rather than once per request payload. If issuance fails validation and the user edits the JSON before retrying, the materially different request reuses the old key and Grid returns an idempotency conflict. This leaves the flow unable to issue the corrected card without remounting. Retain the key only for retries of an unchanged payload and rotate it when the body changes.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed. The key now rotates whenever the body changes. An unchanged retry reuses the key and gets the first card back.
| const pollUntilChanged = async (before: CardTransaction[]) => { | ||
| const signature = (list: CardTransaction[]) => list.map((t) => `${t.id}:${t.status}:${t.settledAmount?.amount}`).join('|') | ||
| const start = signature(before) | ||
| for (let i = 0; i < 15; i++) { | ||
| await new Promise((r) => setTimeout(r, 1000)) | ||
| const next = await refresh() | ||
| if (signature(next) !== start) return | ||
| } |
There was a problem hiding this comment.
Polling stops as soon as the transaction list changes, even though the added delayed-pull preset explicitly takes about 30 seconds to reach its final state. The first appearance of the new transaction therefore ends polling before its later status transition, leaving the UI stale until the user manually refreshes. Continue polling while the relevant transaction is pending or authorized.
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/steps/cards/CardTransactions.tsx
Line: 68-75
Comment:
**Polling stops too early**
Polling stops as soon as the transaction list changes, even though the added delayed-pull preset explicitly takes about 30 seconds to reach its final state. The first appearance of the new transaction therefore ends polling before its later status transition, leaving the UI stale until the user manually refreshes. Continue polling while the relevant transaction is pending or authorized.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Partly valid. I watched the -005 preset in sandbox for 45 seconds: it stays AUTHORIZED the whole time, and the spec has no pending card-transaction status, so there was no later transition to poll for. The real bug was the preset itself. Its descriptor was 23 characters, and sandbox truncates descriptors to 22 before decisioning, so the suffix got cut off. I replaced that preset with -003 (CARD_NOT_ACTIVE) and capped the descriptor input at 22 characters.
There was a problem hiding this comment.
That’s a fair correction. With no PENDING card-transaction state and -005 remaining AUTHORIZED, stopping when the new transaction first appears is not stale-state behavior here. The descriptor truncation explains why the intended delayed-pull scenario never occurred; switching to the 22-character-safe -003 preset and enforcing maxLength={22} fixes the actual issue. I’m withdrawing the polling finding.
| <svg viewBox="0 0 24 24" className="h-5 w-5 text-white/70" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"> | ||
| <path d="M8.5 8.5a5 5 0 0 1 0 7" /> | ||
| <path d="M12 6a8.5 8.5 0 0 1 0 12" /> | ||
| <path d="M15.5 3.5a12 12 0 0 1 0 17" /> | ||
| </svg> |
There was a problem hiding this comment.
The newly added contactless and frozen-card icons, along with the lock icon in RevealCard.tsx, use SVG paths without the required vector-effect: non-scaling-stroke wrapper. The repository directive requires that property on all icon SVG paths. Add the prescribed wrapper styling before merging.
Context Used: Central icon library import conventions (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/components/CardVisual.tsx
Line: 70-74
Comment:
**Icons violate SVG requirement**
The newly added contactless and frozen-card icons, along with the lock icon in `RevealCard.tsx`, use SVG paths without the required `vector-effect: non-scaling-stroke` wrapper. The repository directive requires that property on all icon SVG paths. Add the prescribed wrapper styling before merging.
**Context Used:** Central icon library import conventions ([source](https://github.com/lightsparkdev/grid-api/blob/main/components/grid-visualizer/.cursor/rules/central-icons.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Not applying this one. That rule lives in components/grid-visualizer/.cursor/rules and covers that app's central-icons imports. The samples frontend doesn't use the central icon library. These are small inline SVGs rendered at a fixed size, so non-scaling-stroke doesn't change anything.
There was a problem hiding this comment.
You're right — that rule is scoped to components/grid-visualizer/ and its central icon imports, not the samples frontend. These inline SVGs render at fixed dimensions, so vector-effect: non-scaling-stroke is not needed here. I’m withdrawing this comment and won’t flag the contactless, frozen-card, or lock icons for that requirement.
a2f66b0 to
3973de8
Compare
Adds a Card Issuing flow: create a cardholder, fund their USD internal account, issue a virtual card, reveal it in the processor iframe (with an optional cssUrl), simulate authorizations/clearings/returns, and freeze, re-limit, or close the card. The pinned SDK predates the current card and external-account shapes, so those routes call the REST API through a small GridRest helper. This also fixes the sample build on main, which referenced SDK types that 1.7.1 lacks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3973de8 to
a3670b3
Compare

What
Adds a Card Issuing flow to the Kotlin sample app.
kycStatus: APPROVED.POST /sandbox/internal-accounts/{id}/fund.POST /cards. The request sends anIdempotency-Key.-002insufficient funds,-003card not active). Each row can settle, settle with a 20% tip, expire the authorization, or refund. The list pollsGET /transactions?type=CARD&cardId=…until the simulated event lands.POST /cards/{id}/revealrenders the processor iframe with an expiry countdown. An optionalcssUrlfield sends a stylesheet for that reveal. A sample stylesheet is served at/card-reveal.css. The reveal URL is never logged.maxSpendPerTransaction, and close the card after a confirmation step.The card renders as a styled card visual with the status, last 4 digits, and expiry. It shows a frozen overlay when frozen and turns grey when closed.
Access control
The backend holds the platform credentials. So every per-card route (
GET,PATCH, reveal, transactions, simulators) returns403unless the card was issued from the caller'sX-Session-Id. There's no unscoped card list route. Card create and update bodies aren't logged, because they can carrythreeDSecurePassword. The reveal URL is never logged, and the iframe usessandbox="allow-scripts".Why the card and external-account routes skip the SDK
The pinned
lightspark-grid-kotlin1.7.1 has no card support. The latest release, 1.9.0, still sendscardholderIdandfundingSources[]and has no reveal. Moving to 1.8.0 or 1.9.0 also breaks the customer, quote, and auth-credential routes.So the card routes call the REST API through a small
GridResthelper. It uses the same credentials and base URL as the SDK client. The existing KYC-link route already made raw calls, and now uses the same helper.This also fixes the sample build on
main.ExternalAccounts.ktreferencedArbitrumWalletInfo,PlasmaWalletInfo,bankAccountType, and the new INR fields, none of which exist in SDK 1.7.1. That route now forwards the spec-shaped body toPOST /customers/external-accounts.Testing
Ran the flow against the dev sandbox with Playwright:
Issue a card, reveal it, and authorize $12.50. Settle it with a tip ($15.00
SETTLED), then refund it.Authorize with the
-002descriptor and getDECLINED.Freeze the card, then authorize again and get
DECLINED.Create an MXN external account through the rewritten route: 201.
From a different session, reveal and PATCH a card: both return 403.
Render the reveal iframe with
sandbox="allow-scripts"only: the PAN, expiry, and CVV still render.Reveal with
cssUrlpointing at the sample stylesheet on jsDelivr: the iframe loads it, and the revealed card matches the card visual.The stylesheet field defaults to the jsDelivr copy of
samples/frontend/public/card-reveal.cssonmain. That URL resolves once this PR merges. When the app is served over HTTPS (for example behind ngrok), the field defaults to the app's own/card-reveal.css.🤖 Generated with Claude Code