Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,76 @@ grid receiver lookup-uma '[email protected]'
grid receiver lookup-account <accountId>
```

### Cards

```bash
# List cards
grid cards list [--cardholder-id <id>] [--state ACTIVE]

# Get a card
grid cards get <cardId>

# Issue a virtual card
grid cards create \
--cardholder-id <customerId> \
--funding-sources "InternalAccount:1,InternalAccount:2"

# Freeze / unfreeze / close, or replace funding sources
grid cards update <cardId> --state FROZEN
grid cards update <cardId> --state ACTIVE
grid cards update <cardId> --state CLOSED
grid cards update <cardId> --funding-sources "InternalAccount:3"

# Reveal card details — prints a short-lived panEmbedUrl to render in an iframe.
# Do not store or log it.
grid cards reveal <cardId>
```

`cards update` (and the signed `auth` operations below) use Grid's signed-retry
flow: the first call returns a `202` challenge with a `payloadToSign`. Sign it
with your embedded-wallet key (e.g. via `scripts/embedded-wallet-sign.js`) and
re-run the command with `--wallet-signature <stamp> --request-id <id>` to
complete. The CLI forwards those as the `Grid-Wallet-Signature` / `Request-Id`
headers — it does not compute the stamp for you.

### Auth

```bash
# Credentials
grid auth credentials list --account-id <internalAccountId>
grid auth credentials create --type OAUTH --account-id <id> --oidc-token <token>
grid auth credentials challenge <credentialId> # e.g. resend an OTP
grid auth credentials verify <credentialId> --type EMAIL_OTP \
--encrypted-otp-bundle '<hpke-bundle>' \
--wallet-signature <stamp> --request-id <id>
grid auth credentials revoke <credentialId> --wallet-signature <stamp> --request-id <id>

# Delegated signing keys
grid auth delegated-keys list --account-id <id>
grid auth delegated-keys get <delegatedKeyId>
grid auth delegated-keys create \
--card-id <cardId> --internal-account-id <id> --nickname "Card key" \
--spending-limit USD:5000 --spending-limit EUR:4000 \
--wallet-signature <stamp> --request-id <id>
grid auth delegated-keys revoke <delegatedKeyId> # no signature needed

# Sessions
grid auth sessions list --account-id <id>
grid auth sessions refresh <sessionId> --client-public-key <hex> \
--wallet-signature <stamp> --request-id <id>
grid auth sessions revoke <sessionId> --wallet-signature <stamp> --request-id <id>
```

Passkey (WebAuthn) create/verify accept the attestation/assertion as JSON
(`--attestation` / `--assertion`) that you produce client-side; the CLI cannot
run WebAuthn itself.

Some operations need more than one signed retry — notably `auth delegated-keys
create` has two successive signed legs (a single `--wallet-signature` retry
stops at the second `202` and leaves the key `PENDING`). Run the command once
per signed leg, supplying the next `--wallet-signature` / `--request-id` each
time, until it returns the created key.

### Sandbox Testing

```bash
Expand Down
35 changes: 27 additions & 8 deletions cli/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class GridClient {
options?: {
params?: Record<string, string | number | boolean | undefined>;
body?: unknown;
headers?: Record<string, string | undefined>;
}
): Promise<ApiResponse<T>> {
const url = this.buildUrl(path, options?.params);
Expand All @@ -76,6 +77,12 @@ export class GridClient {
fetchOptions.body = JSON.stringify(options.body);
}

if (options?.headers) {
for (const [key, value] of Object.entries(options.headers)) {
if (value !== undefined) headers[key] = value;
}
}

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
Expand Down Expand Up @@ -134,20 +141,32 @@ export class GridClient {

async get<T>(
path: string,
params?: Record<string, string | number | boolean | undefined>
params?: Record<string, string | number | boolean | undefined>,
headers?: Record<string, string | undefined>
): Promise<ApiResponse<T>> {
return this.request<T>("GET", path, { params });
return this.request<T>("GET", path, { params, headers });
}

async post<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
return this.request<T>("POST", path, { body });
async post<T>(
path: string,
body?: unknown,
headers?: Record<string, string | undefined>
): Promise<ApiResponse<T>> {
return this.request<T>("POST", path, { body, headers });
}

async patch<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
return this.request<T>("PATCH", path, { body });
async patch<T>(
path: string,
body?: unknown,
headers?: Record<string, string | undefined>
): Promise<ApiResponse<T>> {
return this.request<T>("PATCH", path, { body, headers });
}

async delete<T>(path: string): Promise<ApiResponse<T>> {
return this.request<T>("DELETE", path);
async delete<T>(
path: string,
headers?: Record<string, string | undefined>
): Promise<ApiResponse<T>> {
return this.request<T>("DELETE", path, { headers });
}
}
Loading
Loading