feat(rust): consolidate SDK semantics into a Rust core with five language surfaces - #319
Open
jacksmithinsulander wants to merge 33 commits into
Open
feat(rust): consolidate SDK semantics into a Rust core with five language surfaces#319jacksmithinsulander wants to merge 33 commits into
jacksmithinsulander wants to merge 33 commits into
Conversation
Freeze the /v1/sdk/* OpenAPI surface as a checked-in source + canonical snapshot, share the filter/prune/placeholder pipeline with generate-types, and gate offline idempotence in CI for the Rust core migration Step 1. Co-authored-by: Cursor <cursoragent@cursor.com>
Catalog the 36-client public surface with five-language names, error templates, and OpenAPI cross-checks so Phase 0 parity work has a single canonical API source of truth. Co-authored-by: Cursor <cursoragent@cursor.com>
…ures Phase 0 Steps 3–4: Zod fixture schema + replay harness with dual node/edge verifyWebhook bindings, and the full §6.1 webhook axis set for Rust migration. Co-authored-by: Cursor <cursoragent@cursor.com>
Pin Rust 1.96.0, add solvapay-core + empty-suite fixture-runner, and wire CI for fmt/clippy/no-unwrap/wasm32/no-tokio gates. Co-authored-by: Cursor <cursoragent@cursor.com>
…9–24) Land Phase 1–3 progress: pure core logic, dto-gen/overlays, HTTP transport, ClientShell, and all 36 typed SolvaPayClient methods with fixture parity on reqwest and Fetch, plus OPERATION_NAMES coverage gate. Co-authored-by: Cursor <cursoragent@cursor.com>
…write Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # pnpm-lock.yaml
Port auth/customer-sync/activation, payment/checkout, balance-poll, and purchase/renewal decision cores to solvapay-core with golden fixtures, TS extracts, and shadow-mode identity checks. Phase 4 still ships dark. Co-authored-by: Cursor <cursoragent@cursor.com>
Port usage/limits/plans and merchant/product/error decision cores to solvapay-core with TS pure extracts, golden fixtures, and characterization suites; close Phase 4 at executed=367. Co-authored-by: Cursor <cursoragent@cursor.com>
Port paywall decision/payload cores and MCP envelope/descriptor metadata into solvapay-core with golden fixtures; close Phase 5 ahead of napi cutover. Co-authored-by: Cursor <cursoragent@cursor.com>
Add solvapay-node with napiVersion/verifyWebhook smoke, per-target optionalDependency layout + WASI fallback, hard artifact gate, and CI matrix. Co-authored-by: Cursor <cursoragent@cursor.com>
…teps 37–39) Wire SOLVAPAY_IMPL Node napi and edge wasm-bindgen verifyWebhook paths, then prove publish-shaped npm installs across the §7.7 matrix plus WASI. Co-authored-by: Cursor <cursoragent@cursor.com>
Delegate the @solvapay/server edge surface (client Groups A–C, sync decisions/paywall/retry, core + mcp-core installs) to @solvapay/server-wasm with SOLVAPAY_IMPL rollback, initSync sync path, browser public-safe opt-in, and workerd/Deno smoke gates. Co-authored-by: Cursor <cursoragent@cursor.com>
Enrich the manifest bindings descriptor and dto-gen so the eight committed 37R/38R shim files regenerate byte-identically (plus @generated headers), with CI regen-drift and header gates covering the retrofit. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve dist/native.js via a dedicated tsup entry and createRequire fallback so webpack/Turbopack cannot rewrite the Node-only graph. Wire checkout-demo rust smoke, boot assertions, and impl diag routes for SOLVAPAY_IMPL=rust. Co-authored-by: Cursor <cursoragent@cursor.com>
Regenerate the Node/edge TS glue from Ir.binding_symbols with chrome assets, prove header-only retrofit against the committed 37R/38R files, and extend CI regen-drift + @generated gates to cover both paths. Co-authored-by: Cursor <cursoragent@cursor.com>
Ship Phase 7–8 bindings, shared IR doc-model + TSDoc (18T), and Python strict .pyi/py.typed gates with mypy/pyright/ruff (42T). Co-authored-by: Cursor <cursoragent@cursor.com>
Emit richer RBS/YARD from the contract, add Steep/RuboCop gates in CI, and regenerate the Ruby client so type-strictness and hover docs stay in sync with dto-gen. Co-authored-by: Cursor <cursoragent@cursor.com>
Add publish-graph checks, dry-run/shadow workflows, dto-gen Rust client/parity emitters, and live-contract tooling so the crate train can be verified before first crates.io upload. Co-authored-by: Cursor <cursoragent@cursor.com>
Make @solvapay/server Rust-only (Step 53): delete webhook/paywall/retry/client TS bodies, inject optional napi webhook clock, drive fixtures/shadow via WASM FetchTransport, and gate regressions with server-superseded-ts. Also include the Step 52 core facade deletion, Step 54 C ABI scaffold, Go binding artifacts, and examples/typescript relocation already staged on this branch. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the vestigial ts/rust selection flag after Steps 52/53 made core and server Rust-only. Always dispatch via napi/WASM (throw when missing); mcp-core keeps its TS path when the binding is uninstalled. Sweep CI, smokes, shadow, and docs accordingly. Co-authored-by: Cursor <cursoragent@cursor.com>
Steps 52/53 re-exported decision helpers from the TS surface without cataloguing them in the cross-language operations map. Add them to TS_ONLY_ALLOWLIST so pre-push parity:check stays green. Co-authored-by: Cursor <cursoragent@cursor.com>
Teach the extras loop to treat bindings catalog names.ts as catalogued so cross-language decision helpers no longer need TS_ONLY_ALLOWLIST entries. Co-authored-by: Cursor <cursoragent@cursor.com>
Skip file: optionalDeps in deps:check, harden no-unwrap brace tracking, exclude solvapay-wasm from workspace Rust CI, build auth for server unit jobs, clear manylinux RUSTC_WRAPPER, fix Windows pip self-upgrade, set aarch64 ring CFLAGS, soften go wasm bit-identity, and fix Ruby crate name collision with the solvapay facade. Co-authored-by: Cursor <cursoragent@cursor.com>
Break the server↔mcp-core turbo build cycle, export Init_solvapay for the Ruby gem, normalize Windows fixture path separators, retake wasm cold-start baselines from the CI host, and disable oxidize-rb cargo-cache on aarch64-linux where cargo-binstall has no musl binary. Co-authored-by: Cursor <cursoragent@cursor.com>
Swallow eager browser-WASM warm rejection for Node/SSR, fix win32 npm spawn EINVAL, reclaim root-owned rust/target after manylinux, replace retired macos-13, drop broken x64-mingw-ucrt Ruby, and harden WASM cold-start as p20 with a wider wall-clock tolerance. Co-authored-by: Cursor <cursoragent@cursor.com>
Swallowing eager browser-WASM rejection unblocked Lint past package tests; checkout-demo must install napi (and stub browser-wasm) like @solvapay/react. wasm-binding now builds server-native so verify-webhook node cases can run after the cold-start budget gate passes. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the static node:module import from register-virtual-tools-mcp so the edge bundle no longer emits bare `import from "module"` (Deno rejects it). Run solvapay-c tests single-threaded — the handle registry is process-global. Co-authored-by: Cursor <cursoragent@cursor.com>
wasm-binding now reaches validate:fetch-runtime after earlier gates pass; that check needs packages/mcp/dist/fetch built. Co-authored-by: Cursor <cursoragent@cursor.com>
Wire all Next examples through a shared serverExternalPackages + webpack config so @solvapay/server-native builds reliably, update example test mocks for current Balance/Purchase types, expand CI example coverage, and restore pnpm dev to the checkout-jack-local ngrok tunnel after the examples/typescript relocation broke the ngrok.yml path. Co-authored-by: Cursor <cursoragent@cursor.com>
createClient throws when Supabase URL/key are empty, which broke /_not-found prerender in CI. Mirror hosted-checkout-demo by copying env.example before build and guard client creation without credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
TL;DR — All semantic logic in the SDK (validation, retry schedules, webhook verification, paywall/helper decisions, MCP builders, the error model, and all 36 client operations) now lives in a Rust core. The TypeScript packages became thin facades over that core via napi and WASM. Four new language surfaces (Python, Ruby, Go, and a C ABI scaffold) are generated from the same contract, so they cannot drift by construction. Correctness is argued from 535 shared golden fixtures and a set of drift gates rather than from re-reviewing 1,773 files.
@solvapay/*package names and all React imports are unchanged by design — but@solvapay/servergains a hard dependency on@solvapay/server-wasm, which changes install shape.This is large but mechanically constrained: ~90% of the diff is generated or fixture data, and every generated file is regeneration-gated in CI. See "How to review a 1,773-file diff" below for the reading order.
Why
The motivation is semantic consolidation, not performance (redesign v2 §1). Before this change, the same business rule — how a webhook signature is verified, when a retry is attempted, what a paywall gate decides — was implemented once per language surface and kept in sync by review discipline. That does not scale past one surface, and it had already produced divergence.
After this change there is exactly one implementation of each rule, and divergence becomes a build failure rather than a support ticket. Adding a language surface is now a codegen target, not a reimplementation.
Explicit non-goal: performance is not the rationale. The perf budgets below are regression guards, not a claimed win.
Architecture
Four crates under
rust/(toolchain pinned to1.96.0; workspace lints denyunwrap_used,expect_used,panic, andmissing_docs):solvapay-core— pure semantics: validation, retry schedules (it computes delays, it never sleeps), webhook verification, paywall and helper decision cores, MCP builders,SdkError. It never reads env and never performs I/O. It is not dependency-free — it depends onsolvapay-dtofor the frozen error templates — and it is notno_std; browser size is controlled by the Cargo feature graph (server/browser/client-public) plus a WASM symbol audit.solvapay-dto— fully generated (schemas.rs,routes.rs,overlays.rs,error_templates.rs).solvapay-transport— theTransporttrait,ClientShell(auth, base URL, idempotency, retry, HTTP→SdkErrormapping), and the 36 typedSolvaPayClientmethods insrc/client.rs, landed in three groups (A: 10, B: 5, C: 21). Native usesreqwest+ rustls;wasm32-unknown-unknownusesFetchTransport; the Gowasm32-wasip1guest uses neither and receives HTTP through wazero host imports. NoteClientShelldefaults tomax_retries: 0for TS parity —withRetrydeliberately stays facade-side.solvapay— the public crates.io facade. No new logic, plus optionalblockingsync twins.Codegen is dual-input. The OpenAPI snapshot owns wire shapes;
contract/manifest/sdk-contract.yamlowns public names across five languages, overlays, idempotency, frozen error messages, docs, the sync matrix, and thebindings:descriptors. Both lower into one IR inrust/tools/dto-gen/src/ir.rs, which emits the Rust DTOs, the TypeScript.d.tsplusnative.ts/wasm.ts, every binding shim, and the Python/Ruby/Go/Rust facades.Every binding shares one ABI: a JSON envelope,
{"ok":true,"value":…}or{"ok":false,"error":…}, identical across napi, wasm-bindgen, PyO3, Magnus, and wazero. 102 binding symbols are snapshotted incontract/manifest/binding-symbols.snapshot.json— more than the 36 operations, because bindings also cover the sync decision cores and payload builders.Binding surfaces: node (napi-rs 3 →
@solvapay/server-nativeplus 8 per-target packages and awasm32-wasifallback behind a tri-stateNAPI_RS_FORCE_WASIloader), wasm (wasm-bindgen 0.2.126 →@solvapay/server-wasm, with mutually exclusive edge and browser profiles), python (PyO3/maturin), ruby (Magnus/rb-sys, no Windows prebuilds), go (wazero + embedded WASM, released via subtree split togithub.com/solvapay/solvapay-go), and c (cbindgen — scaffold only, hand-maintained dispatch allowlist, no published artifact).The thin-facade rule, and what enforces it
The load-bearing invariant of the whole design: facades do type conversion, env/config resolution, and host concerns (timers, caches, event loops) only. No decision logic. If that invariant erodes, the consolidation is undone silently. Eight gates hold it:
pnpm gen:checkpnpm manifest:checkpnpm snapshot:openapi:checkpnpm parity:checkpnpm delegation:checkcontract/delegation-allowlist.jsonpnpm server-superseded-ts:checkverifyWebhookTs,tsFallback,SOLVAPAY_IMPL, orfetch(left inclient.ts@generatedheader gatedenyplusrust/scripts/check-no-unwrap.shdelegation:checkandserver-superseded-ts:checkare the two that specifically prevent the architecture from rotting back into per-language logic. Treat changes tocontract/delegation-allowlist.jsonas architecture changes.What this means for TypeScript consumers
Package names and React imports are unchanged. The install shape is not, and the fallback behaviour is deliberately asymmetric:
@solvapay/server:@solvapay/server-wasmis a hard dependency (edge and WASM paths);@solvapay/server-nativeis optional (napi, with a WASI fallback for exotic platforms). The per-target@solvapay/server-native-*packages arrive transitively — integrators add nothing to their manifests.@solvapay/serverand@solvapay/coreare Rust-only after steps 52/53. The errors are'core sync API not installed','server sync API not installed', or aSolvaPayErrornaming@solvapay/server-native.@solvapay/mcp-coreis the deliberate exception — it still keeps a TS fallback when the ambient sync API is not installed. Stated explicitly so nobody "fixes" it for consistency.import '@solvapay/core/browser-wasm'(which@solvapay/reactdoes for you) or an explicitwarmBrowserCoreWasm().withSolvaPayNextConfigfromexamples/typescript/shared/solvapay-next-config.mjs— it setsserverExternalPackagesand aliases@solvapay/server-native: falseon the client. Prefer--webpack: Turbopack rebundles workspace packages and breaks napi resolution.SOLVAPAY_IMPLwas introduced mid-migration and removed again before merge. Rollback is a package downgrade, not a flag flip.How to work in this repo now
Day-to-day loop:
Adding an operation — the most common future task — is
pnpm gen:scaffold→pnpm gen:bindings→pnpm gen. Full runbook in docs/contributing/sdk-codegen.md.Hooks are split deliberately (see
scripts/setup-pre-commit-hook.sh): pre-commit re-runspnpm genonly when the manifest or OpenAPI snapshot is staged; pre-push runsgen:check,manifest:check, andparity:check; everything else is CI-only. That keeps the common commit fast without letting drift reach a shared branch.How to review a 1,773-file diff
Reading order:
docs/contributing/rust-core-sdk-redesign-v2.md— spec and rationale (the "why", 55 steps).docs/contributing/architecture.md— as-built reference.contract/manifest/sdk-contract.yaml— the contract everything is generated from. This is the highest-leverage file in the PR.rust/crates/solvapay-core— the semantics themselves. Second-highest leverage.rust/crates/solvapay-transport/src/shell.rsandpackages/server/src/client.ts— the seam between core and facade.rust/bindings/nodeis the most load-bearing)..github/workflows/ci.yml.Skim rather than read: anything marked
@generated(drift-gated — reviewing it reviews the emitter twice),contract/fixtures/(535 JSON goldens), and theexamples/*→examples/typescript/*relocation, which is a move with no logic change.Base is
dev. Note that a squash merge collapses 31 commits whose messages carry the step numbering used throughout the design docs — consider a merge commit if that history is worth keeping.Verification
fixture-runnerreportsparsed=535 executed=431 passed=431 skipped-unbound=104. The 104 skipped are allclient/*— they have no Rust HTTP binding in the runner registry and are exercised through the TS/shadow path instead. (executed=367appears in older commit messages; that is a historical step-31 milestone, not the current number.)shadow-invoker. Both sides are Rust after step 53, so this is now a marshalling and normalization check, not a TS-vs-Rust equivalence check. Worth knowing before you read it as stronger evidence than it is.rust/bindings/wasm/budgets.json), as regression guards only: browser 63,460 B gzip / 23.34 ms p20 cold start; edge 298,593 B / 28.02 ms. Tolerances are +10% bytes and +50% cold start.Known gaps and risks
Read this section before approving.
@solvapay/server-nativeand@solvapay/server-wasmsit at0.1.0underrust/bindings/*and are not in the Changesetsignorelist. They must publish at or before@solvapay/server@2.0.1, or every consumer install fails to resolve@solvapay/server-wasm. This needs a deliberate release order, not luck.pnpm changeset status --verboseresolves patch-only with no majors (@solvapay/core1.2.1,@solvapay/server2.0.1,@solvapay/react1.6.1,@solvapay/next1.3.1,@solvapay/mcp-core0.2.9), which is correct on the versioning rules. The problem is content: the only changeset (.changeset/core-eager-wasm-catch.md) describes an unhandled-rejection fix and says nothing about@solvapay/serverbecoming Rust-only with a new hard dependency. As written, the published CHANGELOG will not tell consumers why their install shape changed. An added changeset is needed before release.docs/contributing/rust-migration-map.md).rust/bindings/wasm/scripts/workerd-edge-smoke/is unreferenced inci.yml; only the Deno edge smoke runs. Cloudflare Workers is a first-class target, so this is a real coverage gap even though the artifact exists.parity:checkwith 24 uncatalogued core extras that797051e0/00a28069fixed —parity:checkpasses locally and runs inside the greenlint-build-testjob.Type of Change
Not marked as a breaking change: no public TS API was removed or renamed, and Changesets resolves no majors. The consumer-visible change is install shape (
@solvapay/server-wasmbecomes a hard dependency, and a missing binding throws instead of falling back to TS), which is called out above and needs to reach the CHANGELOG.Related Issues
Related to the Rust core SDK redesign track (
docs/contributing/rust-core-sdk-redesign-v2.md, steps 1–54 of 55).Changes Made
solvapay-core,solvapay-dto,solvapay-transport,solvapay) holding all SDK semantics, withunwrap/panicdenied workspace-wide.rust/tools/dto-gen: a dual-input (OpenAPI snapshot +sdk-contract.yaml) generator lowering to one IR and emitting Rust DTOs, TS declarations and delegation shims, every binding shim, and the Python/Ruby/Go/Rust facades across 41 generated path groups.@solvapay/server,@solvapay/core, and@solvapay/mcp-coreinto thin facades over napi (@solvapay/server-native) and WASM (@solvapay/server-wasm); deleted the superseded TS implementations and the interimSOLVAPAY_IMPLflag.fixture-runner, 38 shadow scenarios over all 36 operations, and eight drift/architecture gates in CI.docs/contributing/and relocatedexamples/*toexamples/typescript/*.Changeset
pnpm changesetand committed the generated fileCaveat, repeated from "Known gaps" because it matters at release time: the existing changeset's bump levels are right but its body does not describe the install-shape change to
@solvapay/server. An added changeset is required before publishing.Testing
pnpm test— unit tests passpnpm build— full monorepo build passespnpm tsx scripts/validate-fetch-runtime.ts— Web-standards runtime gate passesAlso green:
cargo test --workspace,cargo run -p fixture-runner(431/431 executed),pnpm test:contract,pnpm shadow:selftest,pnpm gen:check,pnpm manifest:check,pnpm snapshot:openapi:check,pnpm parity:check,pnpm delegation:check,pnpm server-superseded-ts:check,pnpm test:clean-install(27 CI jobs). Not covered: the workerd edge smoke (gap 4 above).Checklist
pnpm lint/pnpm format)README.md,docs/, package READMEs) accordinglyAdditional Notes
Design documentation is checked in and is the intended source of truth — this description routes to it rather than restating it: