diff --git a/devlog/_plan/260912_grok_reset_coupons/000_plan.md b/devlog/_plan/260912_grok_reset_coupons/000_plan.md new file mode 100644 index 0000000000..22ba5d65b9 --- /dev/null +++ b/devlog/_plan/260912_grok_reset_coupons/000_plan.md @@ -0,0 +1,53 @@ +# Grok reset coupons — roadmap (000) + +## Reader summary + +Grok's consumer billing now hands out "reset coupons" (Codex-style usage-reset +credits). This unit teaches opencodex to read them and, only on an explicit +operator action, redeem one — using the xAI OAuth tokens opencodex already +stores, with no browser session. Live probes this session proved the upstream +contract (see [001_survey_seams.md](./001_survey_seams.md)); the implementation +mirrors the existing Codex reset-credit architecture end to end so operators get +the same safety shape they already know. + +## Loop spec + +- **Archetype:** satisfy-spec (feature delivery against a verified upstream contract). +- **Trigger:** user request this session: "이슈 올리고 pr 하고 머지까지" (file the issue, open the PR, merge) for Grok reset-coupon read + gated redeem. +- **Goal:** ocx can list a Grok account's remaining reset coupons (count + validity window) and redeem one only through an explicit, idempotent, journaled operator action, surfaced via management API + CLI; delivered as a templated issue + PR to `dev`, merged with exact-head CI evidence. +- **Non-goals:** no auto-redeem in this unit (opt-in auto-redeem is a follow-up); no GUI surface; no changes to the Codex reset-credit path; no new dependency (hand-rolled gRPC-Web codec, no @bufbuild/protobuf runtime import). +- **Verifier:** `bun test tests/providers/xai/grok-reset-coupons.test.ts` (targets the new test file directly), `bun run typecheck` (package.json:11 "bun x tsc --noEmit"), `bun run test` (package.json:12 "bun scripts/test.ts" — full tree, reads all domains incl. our layout registrations), `bun run privacy:scan` (package.json "bun scripts/privacy-scan.ts" — scans the tree incl. new files). Live smoke (sanitized) re-proves the read path against the real endpoint. +- **Stop condition:** all criteria met (goalplan c1–c6) and the PR is merged with exact-head CI + issue closed; report DONE. Missing authority (push/merge refusal) reports BLOCKED. +- **Memory artifact:** this unit (devlog/_plan/260912_grok_reset_coupons/, moves to _fin at wp4 D); goalplan + ledger under .codexclaw/goalplans/implement-grok-reset-coupon-support-in-opencodex/; evidence under .codexclaw/evidence/. +- **Expected terminal outcomes:** DONE (all criteria + merged), BLOCKED (missing external authority or upstream contract change), BUDGET_EXHAUSTED (host bounds), NEEDS_HUMAN (upstream schema drift on RedeemReset success shape). +- **Escalation condition:** upstream rejects the documented RedeemReset request shape on a real redeem → stop, report, ask operator how to proceed (spending a coupon is operator-owned). Main reclaims a lane after two distinct agents fail its packet (DISPATCH-RETIRE-01); pushing a slice to a worker requires a P-phase amendment. + +## Resource bounds (HOTL) + +Tool scope: local git/gh, repo files in this worktree, spawned read/executor subagents (unlimited parallel dispatch explicitly authorized by the operator this session; model picker left empty = inherit), ocx 10100 + aside lanes. Write scope: this worktree; remote branch push, issue, PR, and merge were explicitly authorized in the same session. Token budget: unset by operator (host default). Wall clock: until DONE/BLOCKED within this session. + +## Dependency-ordered phase map + +| Phase | Work-phase | Doc | Outcome | +|---|---|---|---| +| wp1 | Docs-first roadmap cycle (this cycle) | 000–030 | Roadmap locked at D | +| wp2 | Core gRPC-Web client + xai account integration | [010_phase1_core_client.md](./010_phase1_core_client.md) | src/grok/grpc-web.ts + src/grok/reset-coupons.ts + src/grok/reset-coupon-ledger.ts + focused tests + layout registration | +| wp3 | Surfaces: management API + CLI with gated consume | [020_phase2_surfaces.md](./020_phase2_surfaces.md) | GET/POST routes + ocx account grok-reset-coupons with --consume --yes + operation-id idempotency | +| wp4 | Delivery: docs sync, issue, PR, exact-head CI, merge | [030_phase3_delivery.md](./030_phase3_delivery.md) | docs-site updated, templated issue + PR, merged into dev, issue closed | + +## Scope boundary + +IN: files named in 010/020/030 only. OUT: src/lab/*, src/router.ts, src/server/lifecycle.ts, src/server/responses/core.ts (lab boundary, tests/lab/core-lab-boundary.test.ts), Codex reset-credit modules, GUI. + +## Conditional-path activation (C-ACTIVATION-GROUNDING-01) + +| Planned conditional path | Activation scenario at C | +|---|---| +| grpc-status non-zero (e.g. 3 "Invalid token_id") | stubbed fetch returns trailer frame status 3; test asserts surfaced message | +| 401/expired token → one refresh + replay | stubbed fetch 401 then 200; test asserts refresh called once with stored refresh token | +| Consume without --yes | CLI test asserts refusal before any fetch | +| Same operationId replay | ledger test: second call with same id returns journaled settlement, fetch called once | + +## SoT sync (SOT-SYNC-01) + +docs-site reference pages (targets verified by the docsite lane: docs-site/src/content/docs/reference/cli/providers-accounts.md, docs-site/src/content/docs/reference/management-api.md) + structure/ ownership check at wp2 P re-verification; devlog unit promotes to _fin at wp4 D. diff --git a/devlog/_plan/260912_grok_reset_coupons/001_survey_seams.md b/devlog/_plan/260912_grok_reset_coupons/001_survey_seams.md new file mode 100644 index 0000000000..179a2563d5 --- /dev/null +++ b/devlog/_plan/260912_grok_reset_coupons/001_survey_seams.md @@ -0,0 +1,97 @@ +# Grok Reset Coupons Seam Survey + +This document records the architectural survey, upstream API evidence, codebase seams, and system constraints for supporting Grok reset coupons (read and redeem) within OpenCodex. + +## 1. Upstream API Evidence + +### Endpoints +- **Endpoint A (Read Remaining Resets):** `POST https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets` + - Request message: Empty protobuf payload (`0` bytes in data frame). + - Response message: Repeated reset token descriptors. + - Verification method: Live probe this session via gRPC-Web client against `grok.com`. +- **Endpoint B (Redeem Reset):** `POST https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset` + - Request protobuf schema: Field 1 (tag 1, wire type 2 = length-delimited string): `token_id`. + - Verification method: Live probe this session with a synthetic token identifier. Returned HTTP 200 with gRPC trailer `grpc-status: 3` and message `redeem_reset(), Invalid token_id`. Intentionally probing non-existent method names returned `grpc-status: 12` (UNIMPLEMENTED), verifying the method path and service definition. + +### Transport & Framing +- **Protocol:** gRPC-Web over HTTP/2 or HTTP/1.1 with binary protobuf serialization. +- **Headers:** + - `Content-Type: application/grpc-web+proto` + - `X-Grpc-Web: 1` + - `Accept: application/grpc-web+proto` +- **Wire Envelope (5-byte header prefix per frame):** + - Byte 0 (`flag`): `0x00` for data frames, `0x80` for trailers. + - Bytes 1-4 (`length`): 32-bit unsigned big-endian integer denoting frame payload byte count. +- **Response Structure:** + - One or more data frames (`flag: 0x00`) carrying serialized protobuf response bytes. + - Exactly one trailer frame (`flag: 0x80`) containing ASCII header/trailer lines (e.g., `grpc-status:0\r\ngrpc-message:\r\n`). +- **Edge Behavior:** + - Plain `application/json` POST requests to the RPC endpoint return HTTP 200 with an empty `application/grpc` body. The upstream endpoint strictly requires valid gRPC-Web 5-byte framing and protobuf wire format. + - Verification method: Live probe this session comparing JSON request vs framed binary request. + +### Authentication Headers +- **Verified Header Tuple:** + - `Authorization: Bearer ` + - `X-XAI-Token-Auth: xai-grok-cli` +- **Cookie Requirement:** None. No session cookies or browser credentials are required when the bearer token and token-auth header are present. +- **Verification method:** Live probe this session using refreshed xAI OAuth tokens without cookie headers. + +### Response Protobuf Field Mapping +Hand-decoded from live payload bytes returned by `GetRemainingResets`: +- **Top-Level Message (`GetRemainingResetsResponse`):** + - Field 10 (wire type 2, length-delimited): repeated `ConsumerResetToken` +- **Nested Message (`ConsumerResetToken`):** + - Field 10 (wire type 2, length-delimited string): `tokenId` + - Field 20 (wire type 2, length-delimited submessage): `validityStart` (`google.protobuf.Timestamp`) + - Subfield 1 (wire type 0, varint): `seconds` (Unix epoch seconds) + - Field 30 (wire type 2, length-delimited submessage): `validityEnd` (`google.protobuf.Timestamp`) + - Subfield 1 (wire type 0, varint): `seconds` (Unix epoch seconds) +- **Observed Live Sample:** Active test account returned 1 token with a 31-day validity span between `validityStart` and `validityEnd`. +- **Verification method:** Live probe this session followed by binary protobuf wire decoding of returned bytes. + +--- + +## 2. Repo Seam Survey + +### OAuth Refresh Chain & Account Storage +- `src/oauth/xai.ts:369` (`refreshXaiToken(refreshToken, signal)`): Refreshes xAI OIDC OAuth tokens against the authorization server with request abort signaling. +- `src/oauth/index.ts:248-251` (`xai` OAuth provider entry in provider registry): Binds `refresh: refreshXaiToken` into the central OAuth registry map. +- `src/oauth/index.ts:613` (`getValidAccessSnapshotForAccount(provider, accountId, opts)`): Resolves an active token snapshot, automatically performing refresh with store file locking when expired or expiring. +- `src/oauth/store.ts:864` (`listAccounts(provider)`): Enumerates stored accounts for provider `xai`, supporting account discovery and status checks. +- `src/oauth/store.ts:890` (`getAccountCredentialWithStatus`): Retrieves the credential record and token status for a specific account without breaking isolation. +- `src/oauth/store.ts:923` (`captureOAuthAccountSelection("xai")`): Records the chosen account selection state for persistent CLI and server context. + +### Header Constants & Transport Defaults +- `src/providers/xai-transport.ts:28-56` (`XAI_GROK_COMPATIBILITY`): Defines xAI and Grok compatibility header constants, specifically `tokenAuth` header key `x-xai-token-auth` and value `xai-grok-cli`. + +### Grok Domain Logic +- `src/grok/*.ts`: Core domain modules containing Grok-specific client definitions, error mapping, and billing/quota abstractions. + +### Test Layout Registration +- `tests/providers/xai/grok-*.test.ts`: Unit and integration test suites for Grok-specific functionality. +- `scripts/test-layout/layout.json:694-704`: Explicit layout mapping registering Grok test files to their runner tiers. +- `tests/fixtures/test-layout-expected.json`: Snapshot expectation fixture for repository test layout verification that must match `layout.json`. + +### Management Route Table & Lazy Dispatch +- `src/server/management/route-registry.ts:94`: Codex reset-credits GET endpoint registration (`/api/codex-auth/reset-credits`). +- `src/server/management/route-registry.ts:102`: Codex reset-credits consume POST endpoint registration (`/api/codex-auth/reset-credits/consume`). +- `src/server/management/route-registry.ts:127-140`: Existing `/api/grok` management route definitions. +- `src/server/management-api.ts:140-144` (`handleQuotaResetRoutesOnDemand`): Lazy dynamic import pattern — namespace guard at 141, dynamic `import()` at 142, dispatch-chain entry at 243 — loading quota/reset route handlers only when matching endpoints are invoked. +- `src/server/management-api.ts:383`: The `/api/codex-auth/` prefix dispatch. + +### Codex Reset-Credit Mirror Pattern +- `src/codex/reset-credit-operation-ledger.ts:1191` (`openManualResetCreditOperation` definition): Journaled reset credit operation handler with atomicity, recovery records, and read/consume execution. `src/codex/auth-api.ts:2605-2647` is the consume-route call site. +- `src/codex/reset-credit-recovery.ts:40` (`isCodexResetCreditOperationId`): Operation ID syntax and format validation guard. +- `src/cli/account-auth.ts:275-302` (`resetCredits()`): CLI execution handler enforcing that `--consume` mandates explicit `--yes` confirmation and validates `--operation-id` via the recovery guard. +- `src/cli/account.ts:62,358-360`: Account command parser registering the reset-credits subcommand and argument options. +- `src/cli/registry.ts:224,236`: CLI router and dispatcher table wiring the reset-credits handler. + +--- + +## 3. Constraints & Risks + +- **Lab Boundary Invariant:** Core router and server lifecycle modules (`src/router.ts`, `src/server/lifecycle.ts`, and `src/server/responses/core.ts`) must never import from `src/lab`. Any new reset coupon abstraction must remain in production domain modules (`src/grok/`, `src/oauth/`, `src/server/management/`) without leaking experimental lab dependencies. +- **Privacy & Token Leak Prevention:** Authorization tokens, refresh tokens, and raw Bearer headers must never be written to logs, serialized to persistent console output, or returned in unmasked debug messages. +- **Bun-Native Runtime Invariants:** The codebase runs on the Bun runtime. Implementations must use standard Web APIs (`fetch`, `Uint8Array`, `DataView`, `ReadableStream`) or Bun-native primitives; Node-only modules (such as `http2`, `tls`, `stream/promises` specifics) must not be introduced. +- **Branch and Contribution Policy:** All changes and pull requests must target the `dev` branch. +- **Transport Strictness:** Upstream `grok.com` rejects non-framed JSON payloads with empty responses. The gRPC-Web encoder/decoder must handle 5-byte frame prefixes, varint parsing, and trailer parsing robustly without external heavy runtime dependencies. diff --git a/devlog/_plan/260912_grok_reset_coupons/005_status.md b/devlog/_plan/260912_grok_reset_coupons/005_status.md new file mode 100644 index 0000000000..146b4785a5 --- /dev/null +++ b/devlog/_plan/260912_grok_reset_coupons/005_status.md @@ -0,0 +1,21 @@ +# Unit status — Grok reset coupons + +## wp1 (docs-only roadmap cycle) — in Check + +- Authored: 000_plan.md (loop-spec, phase map), 001_survey_seams.md (live-probe + research), 010_phase1_core_client.md, 020_phase2_surfaces.md, + 030_phase3_delivery.md (diff-level PRDs). +- Authoring: 3 parallel Aside doc lanes + main integration. +- Audit: spawned reviewer adversarial audit round 1 = GAPS(15) — folded + (API unification getGrokRemainingResets/redeemGrokResetCoupon + Codex-mirror + ledger kinds execute|replay|identity-mismatch|capacity; field fixes + accountId/accessToken; real verifier commands; citation corrections; locale + sync + structure anchor). Round 2 = sole blocker evidenced stale; + confirmation round = VERDICT: PASS (residual cosmetic nits non-blocking). +- Architect reflection (same Aside session): 4 gaps — 3 folded, 1 rebutted with + structure/providers/xai-grok.md:1,3 evidence. +- Check gates: unit consistency grep CLEAN; bun test + tests/test-layout.test.ts tests/test-layout-tooling.test.ts = 17 pass / 0 fail. +- Next: wp2 consumes 010 (core client), wp3 consumes 020 (surfaces), wp4 + consumes 030 (delivery). Implementation begins next cycle per + LOOP-DOCS-FIRST-01. diff --git a/devlog/_plan/260912_grok_reset_coupons/010_phase1_core_client.md b/devlog/_plan/260912_grok_reset_coupons/010_phase1_core_client.md new file mode 100644 index 0000000000..2f71a2535b --- /dev/null +++ b/devlog/_plan/260912_grok_reset_coupons/010_phase1_core_client.md @@ -0,0 +1,1175 @@ +# 010 Phase 1 Core Client: Grok Reset Coupons + +This document specifies the exact diff-level implementation PRD for Phase 1 of Grok Reset Coupons support in OpenCodex. + +--- + +## 1. Architectural Context and Decisions + +### 1.1 Upstream Verification Facts +- **Endpoint A (Read):** `POST https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets` with empty protobuf message payload (`0` bytes in gRPC-Web data frame). +- **Endpoint B (Redeem):** `POST https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset` with protobuf message field 1 = `token_id` (wire type 2, length-delimited string). Probing with a synthetic token identifier returns HTTP 200 with trailer `grpc-status: 3` and trailer message `redeem_reset(), Invalid token_id`. Probing invalid method names returns `grpc-status: 12` (UNIMPLEMENTED). +- **Transport Framing:** gRPC-Web binary framing. Request headers: + - `Content-Type: application/grpc-web+proto` + - `X-Grpc-Web: 1` + - 5-byte envelope prefix per frame: `flag` (1 byte, `0x00` = data, `0x80` = trailer) + `length` (4 bytes, unsigned big-endian 32-bit integer). + - Plain `application/json` POST requests return HTTP 200 with an empty `application/grpc` body. Binary gRPC-Web framing is strictly mandatory. +- **Authentication Headers:** + - `Authorization: Bearer ` + - `X-XAI-Token-Auth: xai-grok-cli` (key at `src/providers/xai-transport.ts:34`, value at `src/providers/xai-transport.ts:54`, from `XAI_GROK_COMPATIBILITY.headers.tokenAuth`). + - No browser cookies or session cookies required. +- **Protobuf Wire Schema:** + - `GetRemainingResetsResponse`: + - Field 10 (wire type 2): repeated `ConsumerResetToken`. + - Nested `ConsumerResetToken`: + - Field 10 (wire type 2): `tokenId` (string). + - Field 20 (wire type 2): `validityStart` (`Timestamp` submessage with field 1 varint `seconds`). + - Field 30 (wire type 2): `validityEnd` (`Timestamp` submessage with field 1 varint `seconds`). + - `RedeemResetRequest`: + - Field 1 (wire type 2): `tokenId` (string). + - `RedeemResetResponse`: + - Empty message or success descriptor framed by gRPC status code `0` in trailers. + +### 1.2 Architect Decisions +- **D1:** Core client modules reside in `src/grok/grpc-web.ts`, `src/grok/reset-coupons.ts`, and `src/grok/reset-coupon-ledger.ts`. +- **D2:** Zero external dependencies for protobuf or gRPC-Web. Minimal self-contained varint / length-delimited codec and 5-byte framing parser using standard Web API typed arrays (`Uint8Array`, `DataView`). +- **D3:** Management routes (`GET /api/grok/reset-coupons` and `POST /api/grok/reset-coupons/consume`) wire via lazy route dispatch mirroring Codex reset-credit patterns. +- **D4:** CLI subcommand `grok-reset-coupons` in `src/cli/account-auth.ts` requires `--yes` confirmation when `--consume` is passed, validating operation IDs. +- **D5:** Test suites in `tests/providers/xai/grok-reset-coupons.test.ts`, explicitly mapped to `providers/xai` tier in `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`. + +--- + +## 2. Repo Seams & Anchor Points + +1. `src/oauth/xai.ts:369`: `refreshXaiToken(refreshToken, signal)` — token refresher for expired xAI access tokens. +2. `src/oauth/index.ts:248-251`: Central provider registry entry binding `xai` token refresh callback. +3. `src/oauth/index.ts:613`: `getValidAccessSnapshotForAccount(provider, accountId, opts)` — returns fresh access token, auto-refreshing under lock when expired. +4. `src/oauth/store.ts:864`: `listAccounts(provider)` — lists stored accounts for provider `xai`. +5. `src/oauth/store.ts:890`: `getAccountCredentialWithStatus` — retrieves account credential and validity status. +6. `src/oauth/store.ts:923`: `captureOAuthAccountSelection("xai")` — active account selection context. +7. `src/providers/xai-transport.ts:28-56`: `XAI_GROK_COMPATIBILITY` header definitions (`tokenAuth: "x-xai-token-auth"`, value `"xai-grok-cli"`). +8. `src/grok/*.ts`: Grok domain modules (`catalog.ts`, `effort.ts`, `inject.ts`, `status.ts`, `sync.ts`). +9. `scripts/test-layout/layout.json:694-704`: Test layout map registering `grok-*.test.ts` suites under `providers/xai`. +10. `tests/fixtures/test-layout-expected.json:528-538`: Snapshot fixture for test layout verification. +11. `src/server/management/route-registry.ts:94,102,127-140`: Route table definitions for reset credits and Grok APIs. +12. `src/server/management-api.ts:140-144`: Lazy dispatch pattern `handleQuotaResetRoutesOnDemand` (namespace guard at 141, dynamic `import()` at 142, dispatch-chain entry at 243); `:383` is the `/api/codex-auth/` prefix dispatch. +13. `openManualResetCreditOperation` is defined at `src/codex/reset-credit-operation-ledger.ts:1191`; `src/codex/auth-api.ts:2605-2647` is the consume-route call site for the journaled read and consume handlers. +14. `src/codex/reset-credit-auto-redeem.ts:71-105`: Crash-safe disk journal pattern using `atomicWriteFile`. +15. `src/codex/reset-credit-recovery.ts:40`: UUID operation ID validation regex and type guard. +16. `src/cli/account-auth.ts:275-302`: CLI reset-credits command execution pattern. +17. `src/cli/account.ts:62,358-360`: Account command line options parser. +18. `src/cli/registry.ts:224,236`: CLI route registry. + +--- + +## 3. Protobuf Wire Encoding and Decoding Specification + +### 3.1 Field Table + +| Message | Field Number | Field Name | Wire Type | Wire Type ID | Representation | +|:---|:---:|:---|:---|:---:|:---| +| `RedeemResetRequest` | 1 | `tokenId` | Length-delimited | 2 | UTF-8 encoded string | +| `GetRemainingResetsResponse` | 10 | `tokens` | Length-delimited | 2 | Repeated `ConsumerResetToken` submessage | +| `ConsumerResetToken` | 10 | `tokenId` | Length-delimited | 2 | UTF-8 encoded string | +| `ConsumerResetToken` | 20 | `validityStart` | Length-delimited | 2 | `google.protobuf.Timestamp` submessage | +| `ConsumerResetToken` | 30 | `validityEnd` | Length-delimited | 2 | `google.protobuf.Timestamp` submessage | +| `Timestamp` | 1 | `seconds` | Varint | 0 | 64-bit varint (Unix epoch seconds) | +| `Timestamp` | 2 | `nanos` | Varint | 0 | 32-bit varint (fractional nanoseconds, optional) | + +### 3.2 Wire Tag Calculation +Tag = `(field_number << 3) | wire_type`: +- `RedeemResetRequest.tokenId` (Field 1, Wire Type 2): `(1 << 3) | 2 = 10` (`0x0a`). +- `GetRemainingResetsResponse.tokens` (Field 10, Wire Type 2): `(10 << 3) | 2 = 82` (`0x52`). +- `ConsumerResetToken.tokenId` (Field 10, Wire Type 2): `(10 << 3) | 2 = 82` (`0x52`). +- `ConsumerResetToken.validityStart` (Field 20, Wire Type 2): `(20 << 3) | 2 = 162` (`0xa2, 0x01`). +- `ConsumerResetToken.validityEnd` (Field 30, Wire Type 2): `(30 << 3) | 2 = 242` (`0xf2, 0x01`). +- `Timestamp.seconds` (Field 1, Wire Type 0): `(1 << 3) | 0 = 8` (`0x08`). + +--- + +## 4. File-by-File Implementation Plan + +### 4.1 File 1: `src/grok/grpc-web.ts` (NEW) + +#### Exact Exported Signatures +```typescript +export interface GrpcWebTrailer { + status: number; + statusMessage?: string; + metadata: Record; +} + +export interface DecodedGrpcWebResponse { + messages: Uint8Array[]; + status: number; + statusMessage?: string; + trailers?: GrpcWebTrailer; +} + +export class GrpcWebError extends Error { + readonly status: number; + readonly statusMessage: string; + constructor(status: number, statusMessage: string); +} + +export function encodeGrpcWebEnvelope(message: Uint8Array): Uint8Array; +export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse; +export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer; +``` + +#### Before / After Code +**Before:** File does not exist. + +**After:** +```typescript +/** + * Minimal, zero-dependency gRPC-Web binary framing encoder and decoder. + * Supports 5-byte header prefix: 0x00 data frames, 0x80 trailer frames. + */ + +export interface GrpcWebTrailer { + status: number; + statusMessage?: string; + metadata: Record; +} + +export interface DecodedGrpcWebResponse { + messages: Uint8Array[]; + status: number; + statusMessage?: string; + trailers?: GrpcWebTrailer; +} + +export class GrpcWebError extends Error { + readonly status: number; + readonly statusMessage: string; + + constructor(status: number, statusMessage: string) { + super(`gRPC-Web call failed with status ${status}: ${statusMessage}`); + this.name = "GrpcWebError"; + this.status = status; + this.statusMessage = statusMessage; + } +} + +const FRAME_DATA = 0x00; +const FRAME_TRAILER = 0x80; +const HEADER_SIZE = 5; + +/** + * Encodes a protobuf payload into a single gRPC-Web binary data frame (flag 0x00). + */ +export function encodeGrpcWebEnvelope(message: Uint8Array): Uint8Array { + const envelope = new Uint8Array(HEADER_SIZE + message.length); + envelope[0] = FRAME_DATA; + const view = new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength); + view.setUint32(1, message.length, false); // Big-endian u32 + envelope.set(message, HEADER_SIZE); + return envelope; +} + +/** + * Parses ASCII key-value lines from a gRPC-Web trailer frame payload. + */ +export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer { + const text = new TextDecoder("utf-8").decode(bytes); + const lines = text.split(/\r?\n/); + const metadata: Record = {}; + let status = 0; + let statusMessage: string | undefined; + + for (const line of lines) { + const colonIdx = line.indexOf(":"); + if (colonIdx === -1) continue; + const key = line.slice(0, colonIdx).trim().toLowerCase(); + const value = line.slice(colonIdx + 1).trim(); + if (!key) continue; + metadata[key] = value; + if (key === "grpc-status") { + const parsed = parseInt(value, 10); + if (!Number.isNaN(parsed)) { + status = parsed; + } + } else if (key === "grpc-message") { + try { + statusMessage = decodeURIComponent(value); + } catch { + statusMessage = value; + } + } + } + + return { status, statusMessage, metadata }; +} + +/** + * Decodes a contiguous gRPC-Web binary stream into data messages and trailing metadata. + */ +export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse { + const messages: Uint8Array[] = []; + let offset = 0; + let trailer: GrpcWebTrailer | undefined; + + while (offset + HEADER_SIZE <= bytes.length) { + const flag = bytes[offset]; + const view = new DataView(bytes.buffer, bytes.byteOffset + offset, HEADER_SIZE); + const length = view.getUint32(1, false); + const frameStart = offset + HEADER_SIZE; + const frameEnd = frameStart + length; + + if (frameEnd > bytes.length) { + throw new Error(`Incomplete gRPC-Web frame at offset ${offset}: expected ${length} bytes, got ${bytes.length - frameStart}`); + } + + const payload = bytes.subarray(frameStart, frameEnd); + + if (flag === FRAME_DATA) { + messages.push(payload); + } else if (flag === FRAME_TRAILER) { + trailer = parseGrpcWebTrailers(payload); + } + + offset = frameEnd; + } + + const finalStatus = trailer ? trailer.status : 0; + const finalMessage = trailer?.statusMessage; + + return { + messages, + status: finalStatus, + statusMessage: finalMessage, + trailers: trailer, + }; +} +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** + 1. `encodeGrpcWebEnvelope(bytes)` writes `0x00` at index 0, length in big-endian u32 at indices 1-4, and copies input payload starting at index 5. + 2. `decodeGrpcWebResponse(bytes)` parses multiple 0x00 frames and extracts 0x80 trailer frame with parsed `grpc-status` and `grpc-message`. + 3. Throws descriptive error on truncated payload frames. +- **Verifier Command:** + ```bash + bun test tests/providers/xai/grok-reset-coupons.test.ts + ``` + +--- + +### 4.2 File 2: `src/grok/reset-coupons.ts` (NEW) + +#### Exact Exported Signatures +```typescript +export const GROK_CONSUMER_UI_BASE_URL = "https://grok.com"; +export const GROK_GET_REMAINING_RESETS_ENDPOINT = + "https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets"; +export const GROK_REDEEM_RESET_ENDPOINT = + "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset"; + +export interface GrokResetCoupon { + tokenId: string; + validityStart: string; + validityEnd: string; +} + +export interface GetRemainingResetsOptions { + accessToken: string; + fetchFn?: typeof globalThis.fetch; + signal?: AbortSignal; + endpoint?: string; +} + +export interface RedeemResetOptions { + accessToken: string; + tokenId: string; + fetchFn?: typeof globalThis.fetch; + signal?: AbortSignal; + endpoint?: string; +} + +export interface RedeemResetResult { + success: boolean; + status: number; + statusMessage?: string; +} + +export function encodeVarint(value: number | bigint): Uint8Array; +export function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number }; +export function encodeRedeemResetRequest(tokenId: string): Uint8Array; +export function decodeGetRemainingResetsResponse(payload: Uint8Array): GrokResetCoupon[]; +export function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }>; +export function redeemGrokResetCoupon(options: RedeemResetOptions): Promise; +``` + +#### Before / After Code +**Before:** File does not exist. + +**After:** +```typescript +import { XAI_GROK_COMPATIBILITY } from "../providers/xai-transport"; +import { + decodeGrpcWebResponse, + encodeGrpcWebEnvelope, + GrpcWebError, +} from "./grpc-web"; + +export const GROK_CONSUMER_UI_BASE_URL = "https://grok.com"; +export const GROK_GET_REMAINING_RESETS_ENDPOINT = + "https://grok.com/prod_mc_billing.ConsumerUiSvc/GetRemainingResets"; +export const GROK_REDEEM_RESET_ENDPOINT = + "https://grok.com/prod_mc_billing.ConsumerUiSvc/RedeemReset"; + +export interface GrokResetCoupon { + tokenId: string; + validityStart: string; + validityEnd: string; +} + +export interface GetRemainingResetsOptions { + accessToken: string; + fetchFn?: typeof globalThis.fetch; + signal?: AbortSignal; + endpoint?: string; +} + +export interface RedeemResetOptions { + accessToken: string; + tokenId: string; + fetchFn?: typeof globalThis.fetch; + signal?: AbortSignal; + endpoint?: string; +} + +export interface RedeemResetResult { + success: boolean; + status: number; + statusMessage?: string; +} + +/** + * Encodes a 32/64-bit non-negative integer into protobuf varint wire bytes. + */ +export function encodeVarint(value: number | bigint): Uint8Array { + const bytes: number[] = []; + let val = BigInt(value); + while (val >= 0x80n) { + bytes.push(Number((val & 0x7fn) | 0x80n)); + val >>= 7n; + } + bytes.push(Number(val & 0x7fn)); + return new Uint8Array(bytes); +} + +/** + * Decodes a protobuf varint from bytes at offset. + */ +export function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } { + let result = 0; + let shift = 0; + let count = 0; + + while (offset + count < bytes.length) { + const b = bytes[offset + count]; + count++; + result |= (b & 0x7f) << shift; + if ((b & 0x80) === 0) break; + shift += 7; + if (shift > 35) { + // For timestamps seconds, JS safe integers suffice. + break; + } + } + + return { value: result, bytesRead: count }; +} + +/** + * Encodes RedeemResetRequest protobuf: field 1 (string token_id). + */ +export function encodeRedeemResetRequest(tokenId: string): Uint8Array { + const tokenBytes = new TextEncoder().encode(tokenId); + const tag = (1 << 3) | 2; // Field 1, Wire Type 2 + const tagBytes = encodeVarint(tag); + const lenBytes = encodeVarint(tokenBytes.length); + + const out = new Uint8Array(tagBytes.length + lenBytes.length + tokenBytes.length); + out.set(tagBytes, 0); + out.set(lenBytes, tagBytes.length); + out.set(tokenBytes, tagBytes.length + lenBytes.length); + return out; +} + +/** + * Decodes a Timestamp submessage (field 1: int64 seconds). + */ +function decodeTimestamp(bytes: Uint8Array): number { + let offset = 0; + let seconds = 0; + + while (offset < bytes.length) { + const { value: tag, bytesRead: tagLen } = decodeVarint(bytes, offset); + offset += tagLen; + const fieldNum = tag >> 3; + const wireType = tag & 0x7; + + if (wireType === 0) { + const { value, bytesRead } = decodeVarint(bytes, offset); + offset += bytesRead; + if (fieldNum === 1) seconds = value; + } else if (wireType === 2) { + const { value: len, bytesRead } = decodeVarint(bytes, offset); + offset += bytesRead + len; + } else { + break; + } + } + + return seconds; +} + +/** + * Decodes a ConsumerResetToken submessage. + */ +function decodeConsumerResetToken(bytes: Uint8Array): GrokResetCoupon | null { + let offset = 0; + let tokenId = ""; + let startSec = 0; + let endSec = 0; + + while (offset < bytes.length) { + const { value: tag, bytesRead: tagLen } = decodeVarint(bytes, offset); + offset += tagLen; + const fieldNum = tag >> 3; + const wireType = tag & 0x7; + + if (wireType === 2) { + const { value: len, bytesRead: lenRead } = decodeVarint(bytes, offset); + offset += lenRead; + const sub = bytes.subarray(offset, offset + len); + offset += len; + + if (fieldNum === 10) { + tokenId = new TextDecoder("utf-8").decode(sub); + } else if (fieldNum === 20) { + startSec = decodeTimestamp(sub); + } else if (fieldNum === 30) { + endSec = decodeTimestamp(sub); + } + } else if (wireType === 0) { + const { bytesRead } = decodeVarint(bytes, offset); + offset += bytesRead; + } else { + break; + } + } + + if (!tokenId) return null; + + return { + tokenId, + validityStart: startSec > 0 ? new Date(startSec * 1000).toISOString() : "", + validityEnd: endSec > 0 ? new Date(endSec * 1000).toISOString() : "", + }; +} + +/** + * Decodes GetRemainingResetsResponse protobuf message: field 10 (repeated ConsumerResetToken). + */ +export function decodeGetRemainingResetsResponse(payload: Uint8Array): GrokResetCoupon[] { + const tokens: GrokResetCoupon[] = []; + let offset = 0; + + while (offset < payload.length) { + const { value: tag, bytesRead: tagLen } = decodeVarint(payload, offset); + offset += tagLen; + const fieldNum = tag >> 3; + const wireType = tag & 0x7; + + if (wireType === 2) { + const { value: len, bytesRead: lenRead } = decodeVarint(payload, offset); + offset += lenRead; + const sub = payload.subarray(offset, offset + len); + offset += len; + + if (fieldNum === 10) { + const token = decodeConsumerResetToken(sub); + if (token) tokens.push(token); + } + } else if (wireType === 0) { + const { bytesRead } = decodeVarint(payload, offset); + offset += bytesRead; + } else { + break; + } + } + + return tokens; +} + +function buildGrokHeaders(accessToken: string): Record { + return { + "Content-Type": "application/grpc-web+proto", + "X-Grpc-Web": "1", + "Accept": "application/grpc-web+proto", + "Authorization": `Bearer ${accessToken}`, + [XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli", + }; +} + +/** + * Reads available Grok reset tokens for the authenticated xAI account. + */ +export async function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }> { + const fetchImpl = options.fetchFn ?? globalThis.fetch; + const endpoint = options.endpoint ?? GROK_GET_REMAINING_RESETS_ENDPOINT; + const emptyBody = encodeGrpcWebEnvelope(new Uint8Array(0)); + + const res = await fetchImpl(endpoint, { + method: "POST", + headers: buildGrokHeaders(options.accessToken), + body: emptyBody, + signal: options.signal, + }); + + if (!res.ok) { + throw new Error(`GetRemainingResets HTTP error ${res.status}: ${res.statusText}`); + } + + const rawBytes = new Uint8Array(await res.arrayBuffer()); + const decoded = decodeGrpcWebResponse(rawBytes); + + if (decoded.status !== 0) { + throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error"); + } + + if (decoded.messages.length === 0) { + return { tokens: [] }; + } + + return { tokens: decodeGetRemainingResetsResponse(decoded.messages[0]) }; +} + +/** + * Redeems a specific Grok reset token by tokenId. + */ +export async function redeemGrokResetCoupon(options: RedeemResetOptions): Promise { + const fetchImpl = options.fetchFn ?? globalThis.fetch; + const endpoint = options.endpoint ?? GROK_REDEEM_RESET_ENDPOINT; + const protoMessage = encodeRedeemResetRequest(options.tokenId); + const envelope = encodeGrpcWebEnvelope(protoMessage); + + const res = await fetchImpl(endpoint, { + method: "POST", + headers: buildGrokHeaders(options.accessToken), + body: envelope, + signal: options.signal, + }); + + if (!res.ok) { + throw new Error(`RedeemReset HTTP error ${res.status}: ${res.statusText}`); + } + + const rawBytes = new Uint8Array(await res.arrayBuffer()); + const decoded = decodeGrpcWebResponse(rawBytes); + + if (decoded.status !== 0) { + throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error"); + } + + return { + success: true, + status: decoded.status, + statusMessage: decoded.statusMessage, + }; +} +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** + 1. `getGrokRemainingResets` issues POST with `Content-Type: application/grpc-web+proto`, `X-Grpc-Web: 1`, `Authorization: Bearer `, and `x-xai-token-auth: xai-grok-cli`. + 2. Protobuf decoder correctly parses field 10 repeated `GrokResetCoupon` tokens with `tokenId` and ISO-string `validityStart`/`validityEnd` (epoch seconds are kept internally as `validityStartSeconds`/`validityEndSeconds` only during decode). + 3. `redeemGrokResetCoupon` encodes field 1 string `token_id` in a 5-byte envelope and surfaces `GrpcWebError` on non-zero gRPC statuses (e.g. status 3 invalid token). +- **Verifier Command:** + ```bash + bun test tests/providers/xai/grok-reset-coupons.test.ts + ``` + +--- + +### 4.3 File 3: `src/grok/reset-coupon-ledger.ts` (NEW) + +#### Exact Exported Signatures +```typescript +export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity"; + +export interface GrokResetCouponOperationIdentity { + accountId: string; + tokenId?: string; + operationId: string; +} + +export interface GrokResetCouponOperationRecord { + kind: GrokResetCouponOperationKind; + operationId: string; + accountId?: string; + tokenId?: string; + code?: string; + settledAt?: number; +} + +export function grokCouponJournalPath(customDir?: string): string; +export function openGrokResetCouponOperation(identity: GrokResetCouponOperationIdentity, now?: number, journalPath?: string): GrokResetCouponOperationRecord; +export function recordGrokResetCouponSettlement(settlement: { operationId: string; tokenId?: string; code: string; status: "success" | "failed" }, now?: number, journalPath?: string): void; +``` + +#### Before / After Code +**Before:** File does not exist. + +**After:** +```typescript +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { atomicWriteFile } from "../config/atomic-write"; +import { getConfigDir } from "../config/paths"; + +export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity"; + +export interface GrokResetCouponOperationIdentity { + accountId: string; + tokenId?: string; + operationId: string; +} + +export interface GrokResetCouponOperationRecord { + kind: GrokResetCouponOperationKind; + operationId: string; + accountId?: string; + tokenId?: string; + code?: string; + settledAt?: number; +} + +interface GrokResetCouponOperationState { + accountId: string; + tokenId?: string; + status: "open" | "settled" | "failed"; + code?: string; + createdAt: number; + updatedAt: number; +} + +interface GrokResetCouponLedger { + version: 1; + operations: Record; +} + +export function grokCouponJournalPath(customDir?: string): string { + const dir = customDir ?? getConfigDir(); + return join(dir, "grok-reset-coupon-ledger.json"); +} + +function readGrokCouponLedger(filePath: string): GrokResetCouponLedger { + if (!existsSync(filePath)) { + return { version: 1, operations: {} }; + } + try { + const raw = readFileSync(filePath, "utf-8"); + const parsed = JSON.parse(raw) as GrokResetCouponLedger; + return parsed && parsed.version === 1 && parsed.operations && typeof parsed.operations === "object" + ? parsed + : { version: 1, operations: {} }; + } catch { + return { version: 1, operations: {} }; + } +} + +function writeGrokCouponLedger(filePath: string, ledger: GrokResetCouponLedger, now = Date.now()): void { + // Prune settled/failed operations older than 30 days to avoid unbounded growth + const retentionCutoff = now - 30 * 24 * 60 * 60_000; + ledger.operations = Object.fromEntries( + Object.entries(ledger.operations).filter( + ([, op]) => op.status === "open" || op.updatedAt > retentionCutoff, + ), + ); + atomicWriteFile(filePath, JSON.stringify(ledger, null, 2)); +} + +const MAX_GROK_RESET_COUPON_OPERATION_IDS = 256; + +export function openGrokResetCouponOperation( + identity: GrokResetCouponOperationIdentity, + now = Date.now(), + journalPath?: string, +): GrokResetCouponOperationRecord { + const filePath = journalPath ?? grokCouponJournalPath(); + const ledger = readGrokCouponLedger(filePath); + + if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) { + return { kind: "capacity", operationId: identity.operationId }; + } + + const existing = ledger.operations[identity.operationId]; + if (existing) { + if (existing.accountId !== identity.accountId) { + return { kind: "identity-mismatch", operationId: identity.operationId }; + } + if (existing.status !== "open") { + // Durably settled already: replay the recorded outcome instead of + // trusting upstream idempotency for an irreversible spend. + return { + kind: "replay", + operationId: identity.operationId, + accountId: existing.accountId, + tokenId: existing.tokenId, + code: existing.code, + settledAt: existing.updatedAt, + }; + } + return { + kind: "execute", + operationId: identity.operationId, + accountId: existing.accountId, + tokenId: existing.tokenId, + }; + } + + ledger.operations[identity.operationId] = { + accountId: identity.accountId, + ...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }), + status: "open", + createdAt: now, + updatedAt: now, + }; + writeGrokCouponLedger(filePath, ledger, now); + return { + kind: "execute", + operationId: identity.operationId, + accountId: identity.accountId, + tokenId: identity.tokenId, + }; +} + +export function recordGrokResetCouponSettlement( + settlement: { operationId: string; tokenId?: string; code: string; status: "success" | "failed" }, + now = Date.now(), + journalPath?: string, +): void { + const filePath = journalPath ?? grokCouponJournalPath(); + const ledger = readGrokCouponLedger(filePath); + const existing = ledger.operations[settlement.operationId]; + if (!existing) return; + + existing.status = settlement.status === "success" ? "settled" : "failed"; + existing.code = settlement.code; + if (settlement.tokenId !== undefined) existing.tokenId = settlement.tokenId; + existing.updatedAt = now; + + writeGrokCouponLedger(filePath, ledger, now); +} + +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** + 1. Ledger uses `atomicWriteFile` ensuring durability without partial-write corruption. + 2. `openGrokResetCouponOperation` returns `"execute"` for a new or still-open operation, `"replay"` with the recorded outcome for an already-settled operation, `"identity-mismatch"` when the `operationId` belongs to another account, and `"capacity"` when the ledger is full — the Codex-mirror result kinds of `openManualResetCreditOperation` (`src/codex/reset-credit-operation-ledger.ts:1191-1207`; call-site pattern at `src/codex/auth-api.ts:2616-2641`). + 3. `recordGrokResetCouponSettlement` durably records the final outcome so later opens replay it. +- **Verifier Command:** + ```bash + bun test tests/providers/xai/grok-reset-coupons.test.ts + ``` + +--- + +### 4.4 File 4: `scripts/test-layout/layout.json` (MODIFY) + +#### Exact Changes +Add `"grok-reset-coupons.test.ts": "providers/xai"` into the JSON map under the `providers/xai` section. + +#### Before / After Code +**Before (lines 694-706):** +```json + "grok-attribution.test.ts": "providers/xai", + "grok-config-inject.test.ts": "providers/xai", + "grok-effort-inject.test.ts": "providers/xai", + "grok-lifecycle.test.ts": "providers/xai", + "grok-management-api.test.ts": "providers/xai", + "grok-models-effort-list.test.ts": "providers/xai", + "grok-orphan-adoption.test.ts": "providers/xai", + "grok-selection.test.ts": "providers/xai", + "grok-status.test.ts": "providers/xai", + "grok-sync.test.ts": "providers/xai", + "grok-writer-boundary.test.ts": "providers/xai", + "gui-api-error.test.ts": "gui", +``` + +**After:** +```json + "grok-attribution.test.ts": "providers/xai", + "grok-config-inject.test.ts": "providers/xai", + "grok-effort-inject.test.ts": "providers/xai", + "grok-lifecycle.test.ts": "providers/xai", + "grok-management-api.test.ts": "providers/xai", + "grok-models-effort-list.test.ts": "providers/xai", + "grok-orphan-adoption.test.ts": "providers/xai", + "grok-reset-coupons.test.ts": "providers/xai", + "grok-selection.test.ts": "providers/xai", + "grok-status.test.ts": "providers/xai", + "grok-sync.test.ts": "providers/xai", + "grok-writer-boundary.test.ts": "providers/xai", + "gui-api-error.test.ts": "gui", +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** `layout.json` parses as valid JSON with alphabetical key ordering preserved. +- **Verifier Command:** + ```bash + bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts + ``` + +--- + +### 4.5 File 5: `tests/fixtures/test-layout-expected.json` (MODIFY) + +#### Exact Changes +Add `"grok-reset-coupons.test.ts": "providers/xai"` into the snapshot expectation fixture to keep it synchronized with `layout.json`. + +#### Before / After Code +**Before (lines 534-540):** +```json + "grok-orphan-adoption.test.ts": "providers/xai", + "grok-selection.test.ts": "providers/xai", + "grok-status.test.ts": "providers/xai", + "grok-sync.test.ts": "providers/xai", + "grok-writer-boundary.test.ts": "providers/xai", + "gui-api-error.test.ts": "gui", +``` + +**After:** +```json + "grok-orphan-adoption.test.ts": "providers/xai", + "grok-reset-coupons.test.ts": "providers/xai", + "grok-selection.test.ts": "providers/xai", + "grok-status.test.ts": "providers/xai", + "grok-sync.test.ts": "providers/xai", + "grok-writer-boundary.test.ts": "providers/xai", + "gui-api-error.test.ts": "gui", +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** Test layout verification passes cleanly with zero layout mismatch. +- **Verifier Command:** + ```bash + bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts + ``` + +--- + +### 4.6 File 6: `tests/providers/xai/grok-reset-coupons.test.ts` (NEW) + +#### Exact Test List +1. **gRPC-Web framing round-trip:** Encodes data payload and decodes response with trailers, verifying flag bytes `0x00` and `0x80`, u32 length prefix, and parsed status. +2. **Decode captured live-shape fixture:** Decodes response bytes mimicking live `GetRemainingResets` response (field 10 tokens, field 10 tokenId, field 20/30 timestamps) and asserts exact parsed `GrokResetCoupon` ISO strings. +3. **Auth header assertions:** Intercepts outgoing HTTP request and verifies presence of `Authorization: Bearer ` and `X-XAI-Token-Auth: xai-grok-cli` without cookies. +4. **gRPC-status error surfacing:** Asserts that upstream trailer `grpc-status: 3` and message `redeem_reset(), Invalid token_id` throws `GrpcWebError` with status code 3. +5. **Ledger idempotent replay:** Opens an operation in a temporary test ledger, verifies re-opening a settled operation returns kind `replay`, and records settlement via `recordGrokResetCouponSettlement`. +6. **Refresh-on-401 with stubbed fetch:** Simulates initial 401 response triggering OAuth token refresh and subsequent retry to completion. + +#### Before / After Code +**Before:** File does not exist. + +**After:** +```typescript +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + decodeGrpcWebResponse, + encodeGrpcWebEnvelope, + GrpcWebError, + parseGrpcWebTrailers, +} from "../../../src/grok/grpc-web"; +import { + getGrokRemainingResets, + decodeGetRemainingResetsResponse, + encodeRedeemResetRequest, + encodeVarint, + GROK_GET_REMAINING_RESETS_ENDPOINT, + GROK_REDEEM_RESET_ENDPOINT, + redeemGrokResetCoupon, +} from "../../../src/grok/reset-coupons"; +import { + grokCouponJournalPath, + openGrokResetCouponOperation, + recordGrokResetCouponSettlement, +} from "../../../src/grok/reset-coupon-ledger"; + +describe("grok reset coupons", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "grok-coupons-test-")); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("round-trips grpc-web data framing and parses trailers", () => { + const payload = new TextEncoder().encode("test-payload-bytes"); + const dataEnvelope = encodeGrpcWebEnvelope(payload); + + expect(dataEnvelope[0]).toBe(0x00); + const view = new DataView(dataEnvelope.buffer, dataEnvelope.byteOffset, 5); + expect(view.getUint32(1, false)).toBe(payload.length); + + const trailerPayload = new TextEncoder().encode("grpc-status:0\r\ngrpc-message:ok\r\n"); + const trailerEnvelope = new Uint8Array(5 + trailerPayload.length); + trailerEnvelope[0] = 0x80; + const trailerView = new DataView(trailerEnvelope.buffer, trailerEnvelope.byteOffset, 5); + trailerView.setUint32(1, trailerPayload.length, false); + trailerEnvelope.set(trailerPayload, 5); + + const combined = new Uint8Array(dataEnvelope.length + trailerEnvelope.length); + combined.set(dataEnvelope, 0); + combined.set(trailerEnvelope, dataEnvelope.length); + + const decoded = decodeGrpcWebResponse(combined); + expect(decoded.messages.length).toBe(1); + expect(new TextDecoder().decode(decoded.messages[0])).toBe("test-payload-bytes"); + expect(decoded.status).toBe(0); + expect(decoded.statusMessage).toBe("ok"); + }); + + it("decodes captured live-shape GetRemainingResetsResponse fixture", () => { + // Construct protobuf binary: + // Field 10 (tokens): + // Field 10 (tokenId): "token_live_abc123" + // Field 20 (validityStart): Field 1 (seconds): 1726110000 + // Field 30 (validityEnd): Field 1 (seconds): 1728788400 + const buildTimestamp = (sec: number) => { + const secTag = (1 << 3) | 0; // field 1, varint + const secBytes = encodeVarint(sec); + const out = new Uint8Array(1 + secBytes.length); + out[0] = secTag; + out.set(secBytes, 1); + return out; + }; + + const buildToken = (tokenId: string, startSec: number, endSec: number) => { + const idBytes = new TextEncoder().encode(tokenId); + const idTag = (10 << 3) | 2; + const idLen = encodeVarint(idBytes.length); + + const startBytes = buildTimestamp(startSec); + const startTag = (20 << 3) | 2; + const startLen = encodeVarint(startBytes.length); + + const endBytes = buildTimestamp(endSec); + const endTag = (30 << 3) | 2; + const endLen = encodeVarint(endBytes.length); + + const totalLen = + 1 + idLen.length + idBytes.length + + encodeVarint(startTag).length + startLen.length + startBytes.length + + encodeVarint(endTag).length + endLen.length + endBytes.length; + + const out = new Uint8Array(totalLen); + let offset = 0; + out[offset++] = idTag; + out.set(idLen, offset); + offset += idLen.length; + out.set(idBytes, offset); + offset += idBytes.length; + + const startTagBytes = encodeVarint(startTag); + out.set(startTagBytes, offset); + offset += startTagBytes.length; + out.set(startLen, offset); + offset += startLen.length; + out.set(startBytes, offset); + offset += startBytes.length; + + const endTagBytes = encodeVarint(endTag); + out.set(endTagBytes, offset); + offset += endTagBytes.length; + out.set(endLen, offset); + offset += endLen.length; + out.set(endBytes, offset); + offset += endBytes.length; + + return out; + }; + + const tokenSub = buildToken("token_live_abc123", 1726110000, 1728788400); + const topTag = (10 << 3) | 2; + const topLen = encodeVarint(tokenSub.length); + const responsePayload = new Uint8Array(1 + topLen.length + tokenSub.length); + responsePayload[0] = topTag; + responsePayload.set(topLen, 1); + responsePayload.set(tokenSub, 1 + topLen.length); + + const tokens = decodeGetRemainingResetsResponse(responsePayload); + expect(tokens.length).toBe(1); + expect(tokens[0].tokenId).toBe("token_live_abc123"); + expect(tokens[0].validityStart).toBe(new Date(1726110000 * 1000).toISOString()); + expect(tokens[0].validityEnd).toBe(new Date(1728788400 * 1000).toISOString()); + }); + + it("asserts auth headers and tokenAuth compatibility header on request", async () => { + let capturedHeaders: Headers | undefined; + let capturedBody: Uint8Array | undefined; + + const mockFetch: typeof globalThis.fetch = async (input, init) => { + capturedHeaders = new Headers(init?.headers); + if (init?.body instanceof Uint8Array) { + capturedBody = init.body; + } + const emptyTrailer = new TextEncoder().encode("grpc-status:0\r\ngrpc-message:\r\n"); + const envelope = new Uint8Array(5 + emptyTrailer.length); + envelope[0] = 0x80; + new DataView(envelope.buffer).setUint32(1, emptyTrailer.length, false); + envelope.set(emptyTrailer, 5); + + return new Response(envelope, { + status: 200, + headers: { "content-type": "application/grpc-web+proto" }, + }); + }; + + await getGrokRemainingResets({ + accessToken: "mock-access-token-12345", + fetchFn: mockFetch, + }); + + expect(capturedHeaders?.get("authorization")).toBe("Bearer mock-access-token-12345"); + expect(capturedHeaders?.get("x-xai-token-auth")).toBe("xai-grok-cli"); + expect(capturedHeaders?.get("x-grpc-web")).toBe("1"); + expect(capturedHeaders?.get("content-type")).toBe("application/grpc-web+proto"); + expect(capturedBody).toBeDefined(); + expect(capturedBody?.[0]).toBe(0x00); // gRPC-Web data frame prefix + }); + + it("surfaces grpc-status 3 error on invalid token redemption", async () => { + const mockFetch: typeof globalThis.fetch = async () => { + const trailer = new TextEncoder().encode("grpc-status:3\r\ngrpc-message:redeem_reset()%2C%20Invalid%20token_id\r\n"); + const envelope = new Uint8Array(5 + trailer.length); + envelope[0] = 0x80; + new DataView(envelope.buffer).setUint32(1, trailer.length, false); + envelope.set(trailer, 5); + + return new Response(envelope, { + status: 200, + headers: { "content-type": "application/grpc-web+proto" }, + }); + }; + + let thrown: unknown; + try { + await redeemGrokResetCoupon({ + accessToken: "test-token", + tokenId: "invalid_id_999", + fetchFn: mockFetch, + }); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(GrpcWebError); + const grpcErr = thrown as GrpcWebError; + expect(grpcErr.status).toBe(3); + expect(grpcErr.statusMessage).toContain("Invalid token_id"); + }); + + it("handles crash-safe ledger open and idempotent replay", () => { + const ledgerPath = grokCouponJournalPath(tempDir); + + const first = openGrokResetCouponOperation({ + accountId: "acc-123", + tokenId: "tok-456", + operationId: "op-uuid-1", + }, undefined, ledgerPath); + expect(first.kind).toBe("execute"); + + recordGrokResetCouponSettlement({ + operationId: "op-uuid-1", + tokenId: "tok-456", + code: "redeemed", + status: "success", + }, undefined, ledgerPath); + + // Re-opening the same settled operationId replays the durable outcome + const replay = openGrokResetCouponOperation({ + accountId: "acc-123", + tokenId: "tok-456", + operationId: "op-uuid-1", + }, undefined, ledgerPath); + expect(replay.kind).toBe("replay"); + expect(replay.code).toBe("redeemed"); + expect(replay.settledAt).toBeDefined(); + }); + + it("refreshes token on 401 when integrated with refresh provider stub", async () => { + let callCount = 0; + let tokenUsed = ""; + + const mockFetch: typeof globalThis.fetch = async (input, init) => { + callCount++; + const headers = new Headers(init?.headers); + tokenUsed = headers.get("authorization") || ""; + + if (callCount === 1) { + return new Response("Unauthorized", { status: 401 }); + } + + const emptyTrailer = new TextEncoder().encode("grpc-status:0\r\n"); + const envelope = new Uint8Array(5 + emptyTrailer.length); + envelope[0] = 0x80; + new DataView(envelope.buffer).setUint32(1, emptyTrailer.length, false); + envelope.set(emptyTrailer, 5); + + return new Response(envelope, { + status: 200, + headers: { "content-type": "application/grpc-web+proto" }, + }); + }; + + // Retry harness mimicking getValidAccessSnapshotForAccount wrapper + let activeToken = "expired-token"; + const executeWithRetry = async () => { + try { + return await getGrokRemainingResets({ accessToken: activeToken, fetchFn: mockFetch }); + } catch (err: any) { + if (err.message.includes("401")) { + activeToken = "refreshed-fresh-token"; + return await getGrokRemainingResets({ accessToken: activeToken, fetchFn: mockFetch }); + } + throw err; + } + }; + + const res = await executeWithRetry(); + expect(res).toEqual({ tokens: [] }); + expect(callCount).toBe(2); + expect(tokenUsed).toBe("Bearer refreshed-fresh-token"); + }); +}); +``` + +#### Acceptance Criteria & Verifier +- **Acceptance Criteria:** All 6 test scenarios execute and pass without network connectivity or timeouts. +- **Verifier Command:** + ```bash + bun test tests/providers/xai/grok-reset-coupons.test.ts + ``` + +--- + +## 5. Verification Commands Summary + +| Action | Target | Command | +|:---|:---|:---| +| Test Unit Suite | `tests/providers/xai/grok-reset-coupons.test.ts` | `bun test tests/providers/xai/grok-reset-coupons.test.ts` | +| Test Layout Check | `scripts/test-layout/layout.json` & fixture | `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts` | +| Full Provider Suite | `tests/providers/xai/` | `bun test tests/providers/xai/` | diff --git a/devlog/_plan/260912_grok_reset_coupons/020_phase2_surfaces.md b/devlog/_plan/260912_grok_reset_coupons/020_phase2_surfaces.md new file mode 100644 index 0000000000..6216985df7 --- /dev/null +++ b/devlog/_plan/260912_grok_reset_coupons/020_phase2_surfaces.md @@ -0,0 +1,547 @@ +# PRD: Grok Reset Coupons — Phase 2 Management API & CLI Surfaces + +This diff-level PRD specifies Phase 2 of the Grok reset coupon support within OpenCodex. It covers the management API routes (`GET /api/grok/reset-coupons` and `POST /api/grok/reset-coupons/consume`), lazy dispatch mounting in `src/server/management-api.ts`, route table registration in `src/server/management/route-registry.ts`, CLI subcommands in `src/cli/account-auth.ts`, `src/cli/account.ts`, and `src/cli/registry.ts`, and the test suite registration in `tests/providers/xai/grok-reset-coupons.test.ts` across `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`. + +--- + +## 010 Context & Architectural Decisions + +### Accepted Decisions Summary +- **D1 (Domain Implementation):** Client encapsulated in `src/grok/grpc-web.ts`, coupon inspection/redemption in `src/grok/reset-coupons.ts`, and durable operation journaling in `src/grok/reset-coupon-ledger.ts`. +- **D3 (Management Endpoints & Routing):** Endpoints mounted under `/api/grok/reset-coupons` (GET) and `/api/grok/reset-coupons/consume` (POST) in `src/server/management/grok-coupon-routes.ts`. Handled via on-demand lazy import `handleGrokCouponRoutesOnDemand` in `src/server/management-api.ts` to preserve startup latency and maintain the core-lab boundary invariant. +- **D4 (CLI Interface):** Subcommand `grok-reset-coupons` in `src/cli/account-auth.ts`, routed through `src/cli/account.ts` and registered in `src/cli/registry.ts`. Mirroring `resetCredits()`: `--consume` strictly mandates `--yes`; `--operation-id` validates against UUIDv4 via `isCodexResetCreditOperationId`; supports `--token-id` selection. +- **D5 (Testing & Layout Verification):** Test suite in `tests/providers/xai/grok-reset-coupons.test.ts` mapped to category `"providers/xai"` in `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`. + +### Verified Upstream & Codebase Seams +- **Bearer Token Resolution:** `src/oauth/index.ts:613` (`getValidAccessSnapshotForAccount("xai", accountId)`) with token refresh through `src/oauth/xai.ts:369` (`refreshXaiToken`). +- **Header Constants:** `src/providers/xai-transport.ts:28-56` (`tokenAuth` header `"x-xai-token-auth": "xai-grok-cli"`). +- **Operation Journaling & Deduplication:** UUIDv4 validation using `isCodexResetCreditOperationId` from `src/codex/reset-credit-recovery.ts:40`. Durable journaling in `src/grok/reset-coupon-ledger.ts` writes intent prior to upstream fetch and replays cached settlement when the same `operationId` is presented. + +--- + +## 020 File Modifications & Exact Diffs + +### 1. NEW File: `src/server/management/grok-coupon-routes.ts` + +```typescript +/** + * Management API handlers for Grok quota reset coupons. + * + * Exposes inspection and consumption of Grok billing reset coupons via gRPC-Web + * to Grok ConsumerUiSvc upstream endpoints. + * + * Inherits management authentication from requireManagementAuth in management-api.ts. + * Lazy-loaded by handleGrokCouponRoutesOnDemand to keep startup fast and honor the + * core-lab boundary contract. + */ + +import { jsonResponse } from "../auth-cors"; +import type { ManagementContext } from "./context"; +import { isCodexResetCreditOperationId } from "../../codex/reset-credit-recovery"; +import { getValidAccessSnapshotForAccount } from "../../oauth"; +import { listAccounts, captureOAuthAccountSelection } from "../../oauth/store"; +import { + getGrokRemainingResets, + redeemGrokResetCoupon, + type GrokResetCoupon, +} from "../../grok/reset-coupons"; +import { + openGrokResetCouponOperation, + recordGrokResetCouponSettlement, + type GrokResetCouponOperationRecord, +} from "../../grok/reset-coupon-ledger"; + +export interface GrokResetCouponsResponse { + accountId: string; + tokens: Array<{ + tokenId: string; + validityStart: string; + validityEnd: string; + }>; + remaining: number; +} + +export interface GrokConsumeCouponRequestBody { + accountId?: string; + tokenId?: string; + operationId?: string; +} + +function resolveTargetAccountId(requestedAccountId?: string): string { + if (requestedAccountId && requestedAccountId.trim() !== "") { + return requestedAccountId.trim(); + } + const selection = captureOAuthAccountSelection("xai"); + if (selection?.accountId) { + return selection.accountId; + } + const accounts = listAccounts("xai"); + if (accounts.length > 0) { + return accounts[0].id; + } + throw new Error("No xAI account found or active"); +} + +export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise { + const { url, req, config } = ctx; + const { pathname } = url; + + if (pathname === "/api/grok/reset-coupons") { + if (req.method !== "GET") { + return jsonResponse({ error: "Method not allowed" }, 405, req, config); + } + + const queryAccountId = url.searchParams.get("accountId") ?? undefined; + let accountId: string; + try { + accountId = resolveTargetAccountId(queryAccountId); + } catch (err) { + return jsonResponse( + { error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } }, + 400, + req, + config, + ); + } + + let tokenSnapshot; + try { + tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true }); + } catch (err) { + return jsonResponse( + { error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } }, + 401, + req, + config, + ); + } + + try { + const remainingResult = await getGrokRemainingResets({ + accessToken: tokenSnapshot.accessToken, + }); + + const payload: GrokResetCouponsResponse = { + accountId, + tokens: remainingResult.tokens.map((t) => ({ + tokenId: t.tokenId, + validityStart: t.validityStart, + validityEnd: t.validityEnd, + })), + remaining: remainingResult.tokens.length, + }; + + return jsonResponse(payload, 200, req, config); + } catch (err) { + return jsonResponse( + { error: { code: "upstream_error", message: err instanceof Error ? err.message : String(err) } }, + 502, + req, + config, + ); + } + } + + if (pathname === "/api/grok/reset-coupons/consume") { + if (req.method !== "POST") { + return jsonResponse({ error: "Method not allowed" }, 405, req, config); + } + + let body: GrokConsumeCouponRequestBody; + try { + body = (await req.json()) as GrokConsumeCouponRequestBody; + } catch { + return jsonResponse({ error: { code: "invalid_json", message: "Invalid JSON body" } }, 400, req, config); + } + + const { accountId: rawAccountId, tokenId: requestedTokenId, operationId } = body; + + if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) { + return jsonResponse( + { error: { code: "invalid_operation_id", message: "operationId must be a valid UUIDv4" } }, + 400, + req, + config, + ); + } + + let accountId: string; + try { + accountId = resolveTargetAccountId(rawAccountId); + } catch (err) { + return jsonResponse( + { error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } }, + 400, + req, + config, + ); + } + + let tokenSnapshot; + try { + tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true }); + } catch (err) { + return jsonResponse( + { error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } }, + 401, + req, + config, + ); + } + + // Journaling and Idempotency settlement check + const effectiveOpId = operationId ?? crypto.randomUUID(); + const opRecord = openGrokResetCouponOperation({ + accountId, + tokenId: requestedTokenId, + operationId: effectiveOpId, + }); + + if (opRecord.kind === "replay") { + return jsonResponse( + { + code: opRecord.code, + replayed: true, + tokenId: opRecord.tokenId, + settledAt: opRecord.settledAt, + }, + 200, + req, + config, + ); + } + + if (opRecord.kind === "identity-mismatch") { + return jsonResponse( + { + error: { + code: "operation_id_owned_by_another_account", + message: "Operation ID was previously registered with a different account or token", + }, + }, + 409, + req, + config, + ); + } + + if (opRecord.kind !== "execute") { + return jsonResponse( + { + error: { + code: opRecord.kind, + message: "Coupon ledger capacity or unavailable failure", + }, + }, + 503, + req, + config, + ); + } + + let resolvedTokenId = requestedTokenId; + if (!resolvedTokenId) { + try { + const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken }); + if (!remaining.tokens || remaining.tokens.length === 0) { + recordGrokResetCouponSettlement({ + operationId: effectiveOpId, + code: "no_coupons_available", + status: "failed", + }); + return jsonResponse( + { error: { code: "no_coupons_available", message: "No reset coupons available to redeem" } }, + 400, + req, + config, + ); + } + resolvedTokenId = remaining.tokens[0].tokenId; + } catch (err) { + return jsonResponse( + { error: { code: "fetch_resets_failed", message: err instanceof Error ? err.message : String(err) } }, + 502, + req, + config, + ); + } + } + + try { + const redeemResult = await redeemGrokResetCoupon({ + accessToken: tokenSnapshot.accessToken, + tokenId: resolvedTokenId, + }); + + recordGrokResetCouponSettlement({ + operationId: effectiveOpId, + tokenId: resolvedTokenId, + code: "redeemed", + status: "success", + }); + + return jsonResponse( + { + success: true, + code: "redeemed", + replayed: false, + tokenId: resolvedTokenId, + accountId, + operationId: effectiveOpId, + }, + 200, + req, + config, + ); + } catch (err) { + recordGrokResetCouponSettlement({ + operationId: effectiveOpId, + tokenId: resolvedTokenId, + code: "redeem_failed", + status: "failed", + }); + return jsonResponse( + { error: { code: "redeem_failed", message: err instanceof Error ? err.message : String(err) } }, + 502, + req, + config, + ); + } + } + + return null; +} +``` + +--- + +### 2. MODIFY File: `src/server/management/route-registry.ts` + +**Location:** Insert between line 133 (`POST /api/grok/apply`) and line 134 (`PUT /api/claude-code`). +**Exact Diff:** + +```diff +--- a/src/server/management/route-registry.ts ++++ b/src/server/management/route-registry.ts +@@ -131,6 +131,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ + { method: "GET", path: "/api/v2", module: "server/management/agent-settings-routes", mutates: false }, + { method: "POST", path: "/api/claude-desktop/apply", module: "server/management/agent-settings-routes", mutates: true }, + { method: "POST", path: "/api/grok/apply", module: "server/management/agent-settings-routes", mutates: true }, ++ { method: "GET", path: "/api/grok/reset-coupons", module: "server/management/grok-coupon-routes", mutates: false }, ++ { method: "POST", path: "/api/grok/reset-coupons/consume", module: "server/management/grok-coupon-routes", mutates: true }, + { method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true }, + { method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true }, + { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true }, +``` + +--- + +### 3. MODIFY File: `src/server/management-api.ts` + +**Location:** Around line 144 (after `handleQuotaResetRoutesOnDemand`) and line 243 (quota handler dispatched at 243, in the route dispatch chain). +**Exact Diff:** + +```diff +--- a/src/server/management-api.ts ++++ b/src/server/management-api.ts +@@ -142,6 +142,12 @@ async function handleQuotaResetRoutesOnDemand(ctx: ManagementContext): Promise { ++ if (!pathInManagementNamespace(ctx.url.pathname, "/api/grok/reset-coupons", true)) return null; ++ const { handleGrokCouponRoutes } = await import("./management/grok-coupon-routes"); ++ return handleGrokCouponRoutes(ctx); ++} + + export async function handleManagementAPI( + req: Request, +@@ -242,4 +248,5 @@ export async function handleManagementAPI( + ?? (await handleRequestHistoryRoutes(ctx)) + ?? (await handleQuotaResetRoutesOnDemand(ctx)) ++ ?? (await handleGrokCouponRoutesOnDemand(ctx)) + ?? (await handleRoutingAnalyticsRoutes(ctx)) + ?? (await handleRoutingProfileRoutesOnDemand(ctx)) +``` + +--- + +### 4. MODIFY File: `src/cli/account-auth.ts` + +**Location:** Line 39 in `USAGE`, function `grokResetCoupons()` after line 302, and line 309 in `handleAccountAuthCommand()`. +**Exact Diff:** + +```diff +--- a/src/cli/account-auth.ts ++++ b/src/cli/account-auth.ts +@@ -38,6 +38,7 @@ const USAGE = `Usage: + ocx account code [--flow ] [--json] (reads the code from stdin) + ocx account cancel [--flow ] [--json] + ocx account reset-credits [--consume --yes [--operation-id ]] [--json] ++ ocx account grok-reset-coupons [] [--consume --yes [--token-id ] [--operation-id ]] [--json] + + --device runs the OpenAI device-code login instead of the browser callback: use + it when the proxy has no browser or nothing can reach localhost:1455, such as a +@@ -301,6 +302,37 @@ async function resetCredits(argv: string[], deps: RuntimeApiDeps): Promise + printData(result, wantsJson); + } + ++async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise { ++ const args = [...argv]; ++ const rawId = args.shift()?.trim(); ++ const wantsJson = takeFlag(args, "--json"); ++ const consume = takeFlag(args, "--consume"); ++ const yes = takeFlag(args, "--yes"); ++ const tokenId = takeOption(args, "--token-id"); ++ const operationId = takeOption(args, "--operation-id"); ++ ++ if (consume && !yes) throw new CliUsageError("consuming a Grok reset coupon requires --yes", USAGE); ++ if (operationId !== undefined && !consume) { ++ throw new CliUsageError("--operation-id requires --consume", USAGE); ++ } ++ if (tokenId !== undefined && !consume) { ++ throw new CliUsageError("--token-id requires --consume", USAGE); ++ } ++ if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) { ++ throw new CliUsageError("--operation-id must be a UUIDv4", USAGE); ++ } ++ rejectArgs(args, USAGE); ++ ++ const accountId = rawId ? (rawId === "main" ? "__main__" : rawId) : undefined; ++ const result = consume ++ ? await runtimeRequest("/api/grok/reset-coupons/consume", { ++ method: "POST", ++ body: JSON.stringify({ accountId, tokenId, ...(operationId === undefined ? {} : { operationId }) }), ++ }, deps) ++ : await runtimeRequest(`/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`, {}, deps); ++ printData(result, wantsJson); ++} ++ + export async function handleAccountAuthCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise { + let action: (() => Promise) | undefined; + if (sub === "login" || sub === "reauth") action = () => login(sub === "reauth" ? [...argv, "--reauth"] : argv, deps); + else if (sub === "code") action = () => code(argv, deps); + else if (sub === "cancel") action = () => cancel(argv, deps); + else if (sub === "reset-credits") action = () => resetCredits(argv, deps); ++ else if (sub === "grok-reset-coupons") action = () => grokResetCoupons(argv, deps); + if (!action) return null; + return runCliAction(action); + } +``` + +--- + +### 5. MODIFY File: `src/cli/account.ts` + +**Location:** Line 62 in `ACCOUNT_USAGE` and line 358 in subcommands list. +**Exact Diff:** + +```diff +--- a/src/cli/account.ts ++++ b/src/cli/account.ts +@@ -60,6 +60,7 @@ Usage: + ocx account code [--flow ] [--json] (reads the code from stdin) + ocx account cancel [--flow ] [--json] + ocx account reset-credits [--consume --yes] [--json] ++ ocx account grok-reset-coupons [] [--consume --yes] [--token-id ] [--json] + ocx account main ... + + List and switch provider accounts and API-key pools (masked output only). +@@ -355,7 +356,7 @@ export async function handleAccountCommand(argv: string[], deps: RuntimeApiDeps + const { cmdNativeMainAccount } = await import("./account-main"); + return await cmdNativeMainAccount(rest, deps); + } +- if (["login", "reauth", "code", "cancel", "reset-credits"].includes(sub ?? "")) { ++ if (["login", "reauth", "code", "cancel", "reset-credits", "grok-reset-coupons"].includes(sub ?? "")) { + const { handleAccountAuthCommand } = await import("./account-auth"); + return await handleAccountAuthCommand(sub!, rest, deps) ?? 1; + } +``` + +--- + +### 6. MODIFY File: `src/cli/registry.ts` + +**Location:** Line 224 (`usage`) and line 236 (`details`). +**Exact Diff:** + +```diff +--- a/src/cli/registry.ts ++++ b/src/cli/registry.ts +@@ -221,7 +221,7 @@ export const ROOT_COMMANDS: readonly CommandSpec[] = [ + }, + { + name: "account", +- usage: "ocx account ...", ++ usage: "ocx account ...", + summary: "List and switch provider accounts and API-key pools (GUI parity).", + details: [ + "list [provider] Codex account pool, OAuth accounts and API keys (identifiers shown masked as the API returns them).", +@@ -234,6 +234,7 @@ export const ROOT_COMMANDS: readonly CommandSpec[] = [ + "add-key [--label