From abaa63cb59c42f55062be22baa316384e950638b Mon Sep 17 00:00:00 2001 From: i Date: Sun, 6 Sep 2026 11:28:36 -0400 Subject: [PATCH] Add Stripe API checkpoint Add a read-only Stripe balance checkpoint with pinned API version, synthetic fixture, manual receipt, API surface map, and top-level source link. Keep secrets in the environment and preserve ICU/Idric-Net as the transport boundary. --- README.md | 3 +- Stripe.idric | 1 + checkpoints/stripe/README.md | 131 +++++++++++++++++++++++ checkpoints/stripe/SURFACE.md | 116 +++++++++++++++++++++ checkpoints/stripe/check | 57 ++++++++++ checkpoints/stripe/fixture/balance.json | 30 ++++++ checkpoints/stripe/fixture/expected.tsv | 4 + checkpoints/stripe/idric/Stripe.idric | 133 ++++++++++++++++++++++++ 8 files changed, 474 insertions(+), 1 deletion(-) create mode 120000 Stripe.idric create mode 100644 checkpoints/stripe/README.md create mode 100644 checkpoints/stripe/SURFACE.md create mode 100755 checkpoints/stripe/check create mode 100644 checkpoints/stripe/fixture/balance.json create mode 100644 checkpoints/stripe/fixture/expected.tsv create mode 100644 checkpoints/stripe/idric/Stripe.idric diff --git a/README.md b/README.md index abe7022..8dbb14f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This repository is the consolidation point for the CLI/API-access programs that - `Guardian.idric` — Guardian API checkpoint. - `Nyt.idric` — New York Times API checkpoint. - `Reddit.idric` — Reddit Data API checkpoint, with a synthetic fixture and manual receipt. +- `Stripe.idric` — Stripe API checkpoint; first slice is pinned, read-only Balance access with a synthetic fixture and manual receipt. - `Reuters.idric` — Reuters GraphQL checkpoint. - `Wayback.idric` — Internet Archive Wayback/CDX checkpoint. @@ -27,6 +28,6 @@ Where these clients need networking, ICU/Idric-Net remains the intended transpor ## Tests -`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit has a separate manual compiler checkpoint at `checkpoints/reddit/check`; it is not part of `make test` while named Idriç holes remain. +`make test` runs the existing Amazon and AbeBooks smoke tests. Reddit and Stripe have separate manual compiler checkpoints under `checkpoints/reddit/check` and `checkpoints/stripe/check`; they are not part of `make test` while named Idriç holes remain. See `PROVENANCE.md` for the source branches copied into this repository. diff --git a/Stripe.idric b/Stripe.idric new file mode 120000 index 0000000..88259fc --- /dev/null +++ b/Stripe.idric @@ -0,0 +1 @@ +checkpoints/stripe/idric/Stripe.idric \ No newline at end of file diff --git a/checkpoints/stripe/README.md b/checkpoints/stripe/README.md new file mode 100644 index 0000000..a0ec7eb --- /dev/null +++ b/checkpoints/stripe/README.md @@ -0,0 +1,131 @@ +# Stripe CLI compiler checkpoint + +This directory is the Idriç Stripe CLI checkpoint. Idriç is the implementation target; ICU/Idric-Net owns HTTP transport. + +The first slice is deliberately read-only. Stripe has many operations that create, capture, refund, invoice, or pay out money. Those do not belong in the first transport/compiler checkpoint. + +## Command contract + +```text +stripe version +stripe url balance +stripe fixture FILE +stripe balance +``` + +### `version` + +Print the API version pinned by this checkpoint: + +```text +2026-08-26.dahlia +``` + +Stripe's API is account-versioned unless a request supplies `Stripe-Version`. Pinning the version makes the response contract reproducible. + +### `url balance` + +No network. Print: + +```text +https://api.stripe.com/v1/balance +``` + +### `fixture` + +No network. Read a Stripe Balance-shaped JSON file and emit TSV. + +The committed synthetic fixture is: + +```text +fixture/balance.json +``` + +Expected output is: + +```text +fixture/expected.tsv +``` + +Columns are: + +```text +state currency amount +``` + +`amount` is emitted as Stripe's integer value without converting it to a decimal. Keeping the currency code and integer intact avoids silently applying the wrong decimal convention to zero-decimal or other currencies. + +### `balance` + +Perform one authenticated `GET /v1/balance`, decode the same shape as the fixture, and emit the same TSV. + +The process environment supplies: + +```text +STRIPE_API_KEY=... +``` + +Do not accept a secret key as a positional command-line argument and do not commit a key. Restricted keys should be preferred when the eventual command needs only a subset of the account. + +The request needs: + +```text +Authorization: Bearer KEY +Stripe-Version: 2026-08-26.dahlia +``` + +Stripe documents both Basic and Bearer authentication. Bearer avoids introducing base64/basic-auth machinery solely for this client. + +## Idriç and ICU + +`idric/Stripe.idric` is the implementation target. + +ICU/Idric-Net remains the networking boundary. The Stripe client currently exposes one narrow request seam: a GET carrying caller-supplied `Authorization` and `Stripe-Version` headers. Do not introduce a Stripe-specific socket/TLS implementation or a curl fallback. + +The remaining named holes are intentionally visible: + +- file input; +- Stripe Balance JSON decoding; +- environment access; +- authenticated ICU GET with caller-supplied headers. + +The Reddit checkpoint already needs the same general caller-supplied-header capability. That should be solved once in Idric-Net/ICU rather than independently in each service client. + +## Manual receipt + +Run: + +```text +ysh check +``` + +or: + +```text +IDRIC=/opt/Idric/build/exec/idris2 ysh check +``` + +The runner emits only: + +```text +PASS checkpoint +FAIL checkpoint +SKIP checkpoint +``` + +A focused failure at a named hole is useful evidence. Do not make it green with an unrelated implementation. + +## Checkpoint ladder + +1. source parses/checks; +2. `version` prints the pinned version; +3. `url balance` prints the exact endpoint; +4. `fixture` reads the synthetic file; +5. `fixture` decodes Balance and matches `expected.tsv` byte-for-byte; +6. `balance` reads `STRIPE_API_KEY` from the environment; +7. ICU sends `Authorization` and `Stripe-Version`; +8. live balance output uses the same TSV contract as the fixture; +9. add read-only list/retrieve resources with common cursor pagination; +10. add mutating operations only with explicit verbs and idempotency-key support. + +See `SURFACE.md` for the API pass and sequencing. diff --git a/checkpoints/stripe/SURFACE.md b/checkpoints/stripe/SURFACE.md new file mode 100644 index 0000000..deb7216 --- /dev/null +++ b/checkpoints/stripe/SURFACE.md @@ -0,0 +1,116 @@ +# Stripe API surface pass + +Audit baseline: 2026-09-06. + +Pinned API version for this checkpoint: `2026-08-26.dahlia`. + +Canonical references: + +- API reference: https://docs.stripe.com/api +- authentication: https://docs.stripe.com/api/authentication +- versioning: https://docs.stripe.com/api/versioning +- pagination: https://docs.stripe.com/api/pagination +- idempotent requests: https://docs.stripe.com/api/idempotent_requests +- key handling: https://docs.stripe.com/keys-best-practices +- public GA OpenAPI: https://github.com/stripe/openapi/tree/master/latest + +The GA OpenAPI `latest/` directory contains a unified public specification for both v1 and v2. The repository master observed for this pass was commit `58e06a3214ae1574600fba64d40b770e5da6d505`; the public YAML blob was `0eeb1665acf426ce068aac8cac6092db5739d363`. + +## Wire contract + +Base URL: + +```text +https://api.stripe.com +``` + +v1 requests use resource-oriented paths. Request bodies for the v1 API are form-encoded and responses are JSON. Authentication is by API key. This client uses bearer auth so the key remains a direct header value rather than introducing Basic-auth encoding. + +Always send: + +```text +Authorization: Bearer $STRIPE_API_KEY +Stripe-Version: 2026-08-26.dahlia +``` + +The secret belongs in the environment or a secrets store, never in source. Prefer a restricted key with the smallest permissions that cover the command. + +## First read-side inventory + +These are the useful command-line surfaces to implement before money-moving writes. + +| Resource | Read endpoints to cover | CLI priority | +| --- | --- | --- | +| Balance | `GET /v1/balance` | 1 | +| Balance transactions | `GET /v1/balance_transactions`, `GET /v1/balance_transactions/:id` | 2 | +| PaymentIntents | `GET /v1/payment_intents`, `GET /v1/payment_intents/:id`, search | 3 | +| Charges | `GET /v1/charges`, `GET /v1/charges/:id`, search | 3 | +| Refunds | `GET /v1/refunds`, `GET /v1/refunds/:id` | 4 | +| Payouts | `GET /v1/payouts`, `GET /v1/payouts/:id` | 4 | +| Customers | `GET /v1/customers`, `GET /v1/customers/:id`, search | 5 | +| Products | `GET /v1/products`, `GET /v1/products/:id`, search | 6 | +| Prices | `GET /v1/prices`, `GET /v1/prices/:id`, search | 6 | +| Checkout Sessions | `GET /v1/checkout/sessions`, retrieve, line items | 7 | +| Subscriptions | `GET /v1/subscriptions`, retrieve, search | 8 | +| Invoices | `GET /v1/invoices`, retrieve, search | 8 | +| Events | `GET /v1/events`, `GET /v1/events/:id` | 9 | + +The first committed checkpoint implements only Balance. It is the smallest authenticated read with no pagination or user-supplied query parameters, so it isolates the transport/header and JSON-decoding boundaries cleanly. + +## Pagination + +v1 list endpoints share cursor pagination: + +```text +limit +starting_after +ending_before +``` + +`starting_after` and `ending_before` are mutually exclusive. List results contain `data`, `has_more`, and `url`. + +Do not hide pagination behind an unbounded fetch-all default. A CLI should expose a bounded page size and explicit continuation. v2 pagination is different and should be modeled separately rather than pretending it is v1. + +## Mutation boundary + +Important write surfaces include: + +- PaymentIntents: create, update, confirm, capture, cancel; +- Customers: create, update, delete; +- Refunds: create, update, cancel where supported; +- Payouts: create, update, cancel, reverse; +- Products and Prices: create/update, plus product delete; +- Checkout Sessions: create/update/expire; +- Subscriptions: create/update/cancel/migrate/resume; +- Invoices: create/update/finalize/pay/send/void/delete draft invoices. + +Do not make a generic `post PATH ...` command the primary interface. These operations have materially different consequences and should get named commands with typed inputs. + +All Stripe `POST` requests accept idempotency keys. When writes are added, every mutating command should either require or generate an idempotency key and make the chosen key observable without leaking other secrets. GET and DELETE do not need idempotency keys. + +## Payment model + +PaymentIntents are the primary modern payment workflow. Charges still have list/retrieve/search value, but direct Charge creation is legacy for most new integrations. Sequence the client accordingly: PaymentIntents first, Charges mainly as observable payment-attempt records. + +## Event model + +Events are read-only through the API and are useful for diagnosis and replay-oriented tooling. Webhook/event-destination configuration is a separate surface. Keep event retrieval independent from any future local webhook listener so the CLI remains useful without a daemon. + +## Expansion order + +1. Balance fixture + live balance. +2. Shared v1 list envelope and cursor types. +3. Balance transactions. +4. PaymentIntent list/retrieve/search. +5. Charge list/retrieve/search. +6. Refund and payout read side. +7. Customer list/retrieve/search. +8. Product/Price read side. +9. Checkout Session read side. +10. Subscription/Invoice read side. +11. Event list/retrieve. +12. Write request body encoding + idempotency keys. +13. Named write commands, starting in sandbox/test usage. +14. v2 resources as a distinct namespace with its own pagination/response contracts. + +Each new resource gets the same receipt pattern: exact URL construction, synthetic fixture, byte-stable output contract, then live transport through ICU/Idric-Net. diff --git a/checkpoints/stripe/check b/checkpoints/stripe/check new file mode 100755 index 0000000..2d217f5 --- /dev/null +++ b/checkpoints/stripe/check @@ -0,0 +1,57 @@ +#!/usr/bin/env ysh + +# Manual Idriç Stripe checkpoint runner. +# Deliberately not part of the root make test while named holes remain. + +set -u + +HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +IDRIC=${IDRIC:-idris2} + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT HUP INT TERM + +status=0 + +available() { + case "$1" in + */*) test -x "$1" ;; + *) command -v "$1" >/dev/null 2>&1 ;; + esac +} + +pass() { + printf 'PASS\t%s\n' "$1" +} + +fail() { + printf 'FAIL\t%s\n' "$1" + status=1 +} + +skip() { + printf 'SKIP\t%s\n' "$1" +} + +show_diagnostics() { + for path in "$@"; do + if test -s "$path"; then + sed 's/^/ /' "$path" + fi + done +} + +if ! available "$IDRIC"; then + skip "idric (not found: $IDRIC)" + exit 0 +fi + +if "$IDRIC" --check "$HERE/idric/Stripe.idric" \ + >"$TMP/idric" 2>"$TMP/idric.err"; then + pass 'idric/check' +else + fail 'idric/check' + show_diagnostics "$TMP/idric" "$TMP/idric.err" +fi + +exit "$status" diff --git a/checkpoints/stripe/fixture/balance.json b/checkpoints/stripe/fixture/balance.json new file mode 100644 index 0000000..b219981 --- /dev/null +++ b/checkpoints/stripe/fixture/balance.json @@ -0,0 +1,30 @@ +{ + "object": "balance", + "available": [ + { + "amount": 1250, + "currency": "usd", + "source_types": { + "card": 1250 + } + }, + { + "amount": 900, + "currency": "eur", + "source_types": { + "card": 900 + } + } + ], + "connect_reserved": [], + "livemode": false, + "pending": [ + { + "amount": 640, + "currency": "usd", + "source_types": { + "card": 640 + } + } + ] +} diff --git a/checkpoints/stripe/fixture/expected.tsv b/checkpoints/stripe/fixture/expected.tsv new file mode 100644 index 0000000..1048a9c --- /dev/null +++ b/checkpoints/stripe/fixture/expected.tsv @@ -0,0 +1,4 @@ +state currency amount +available usd 1250 +available eur 900 +pending usd 640 diff --git a/checkpoints/stripe/idric/Stripe.idric b/checkpoints/stripe/idric/Stripe.idric new file mode 100644 index 0000000..35d655b --- /dev/null +++ b/checkpoints/stripe/idric/Stripe.idric @@ -0,0 +1,133 @@ +module Stripe + +import System + + +api_base : String +api_base = "https://api.stripe.com" + +api_version : String +api_version = "2026-08-26.dahlia" + +balance_path : String +balance_path = "/v1/balance" + +header : String +header = "state\tcurrency\tamount" + +usage : String +usage = "usage: stripe {version | url balance | fixture FILE | balance}" + + +choice command one_of + show_version + show_balance_url + fixture String + balance + + +record Money where + constructor make_money + amount : Integer + currency : String + + +record BalanceSnapshot where + constructor make_balance_snapshot + available : List Money + pending : List Money + + +-- Named holes are intentional compiler/library watchpoints. Do not replace +-- them with curl, Python, or another untyped transport merely to make the +-- checkpoint appear green. + +read_fixture : String → IO (Either String String) +read_fixture path = ?read_fixture_text + +decode_balance : String → Either String BalanceSnapshot +decode_balance body = ?decode_stripe_balance + +environment_value : String → IO (Maybe String) +environment_value name = ?read_environment_value + +-- Stripe accepts bearer authentication. This seam needs caller-supplied +-- Authorization and Stripe-Version headers through ICU/Idric-Net. +authenticated_icu_get : String → String → String → IO (Either String String) +authenticated_icu_get token version url = ?icu_get_with_request_headers + + +balance_url : String +balance_url = api_base ++ balance_path + + +parse_command : List String → Either String command +parse_command ["version"] = Right show_version +parse_command ["url", "balance"] = Right show_balance_url +parse_command ["fixture", path] = Right (fixture path) +parse_command ["balance"] = Right balance +parse_command _ = Left usage + + +print_rows : String → List Money → IO () +print_rows state [] = pure () +print_rows state (value :: rest) = do + putStrLn (state ++ "\t" ++ value.currency ++ "\t" ++ show value.amount) + print_rows state rest + + +print_balance : String → IO () +print_balance body = + case decode_balance body of + Left problem => do + putStrLn ("stripe: " ++ problem) + exitFailure + Right snapshot => do + putStrLn header + print_rows "available" snapshot.available + print_rows "pending" snapshot.pending + + +run : command → IO () +run show_version = + putStrLn api_version + +run show_balance_url = + putStrLn balance_url + +run (fixture path) = do + outcome ← read_fixture path + case outcome of + Left problem => do + putStrLn ("stripe: " ++ problem) + exitFailure + Right body => print_balance body + +run balance = do + maybe_key ← environment_value "STRIPE_API_KEY" + case maybe_key of + Just key => do + outcome ← authenticated_icu_get key api_version balance_url + case outcome of + Left problem => do + putStrLn ("stripe: " ++ problem) + exitFailure + Right body => print_balance body + Nothing => do + putStrLn "stripe: missing STRIPE_API_KEY" + exitFailure + + +main : IO () +main = do + arguments ← getArgs + case arguments of + _ :: rest => + case parse_command rest of + Left problem => do + putStrLn problem + exitFailure + Right value => run value + [] => do + putStrLn usage + exitFailure