API Contract Map — DevPay Product Surface
Reading guide
Use this as a route inventory at the stated commit, not as a substitute for live endpoint discovery. Worker and Axum expose different subsets.
For ordinary requests start with Endpoints; check the inventory and current source when exact authorization or parity matters.
Status: inventory of what exists at HEAD ad360cf (base cb0270be lineage). Route source of truth: be-cloudflare/src/lib.rs dispatch table. Axum be/ mirrors the legacy subset (be/src/lib.rs routes) — the license-security endpoints are Worker-only (no Axum parity). Proto DTOs for the legacy surface: contracts/proto/nougat/v1/nougat.proto.
1. be-cloudflare Worker routes (/api/v1)
Auth & account
| Method | Path | Auth | Notes |
|---|
| POST | /auth/signup | public | usr_{12hex} + role='buyer' (auth_impl.rs:366) |
| POST | /auth/login | public | JWT iss/aud/sub/role/seller_id/tv/iat/exp |
| POST | /auth/logout | user | bumps token_version |
| GET | /me | user | role + seller_id used by FE guards |
| PATCH | /me/password | user | PBKDF2, token invalidation |
| POST | /me/deactivate | user | status→inactive |
Checkout & payments (legacy commerce spine)
| Method | Path | Auth | Notes |
|---|
| POST | /checkout | public | Idempotent (idempotency_key); mock provider → atomic order/payment/subscription/license/ledger/audit/outbox batch; toss → pending order + widget payload (toss_impl::checkout_toss); live env + mock → 402 NOT_LIVE_PAYMENT |
| POST | /checkout/confirm | public | Toss confirm → finalize_paid (same side-effects as mock paid) |
| POST | /webhooks/toss | public | Signature-verified (TOSS webhook secret), ref-validated, refetches PG state — body never trusted |
| GET | /orders | seller/admin | Scoped list |
| GET | /orders/:id | seller/admin | |
| POST | /orders/:id/refund | seller(owner) | refund_attempts pre-record → PG cancel → `succeeded |
| GET | /orders/:id/refund-attempts | seller(owner) | Attempt audit view |
| GET | /payments/:id | seller/admin | |
| POST | /purchase/game-item | user | Game-item purchase + fulfillment (purchase_impl.rs auth) |
| POST | /fulfillments/:id/retry | seller | |
Products
| Method | Path | Auth | Notes |
|---|
| GET | /products | seller/admin | Scoped list — effective_role_sid + list_scope; seller sees own rows, admin unscoped (lib.rs:319) |
| POST | /products | seller | product_create — prod_{12hex}, unique (seller_id, slug) → 409 CONFLICT (lib.rs:454, verify_impl.rs:172) |
| PATCH/PUT | /products/:id | seller(owner) | product_update — ownership check → 404 NOT_FOUND for others' rows (lib.rs:458) |
| DELETE | /products/:id | seller(owner)/admin | ep::delete_product — scope(): None(admin)=any row, Some(sid)=own only; non-owned → 404 (lib.rs:645, endpoints_impl.rs:919) |
License — legacy plane
| Method | Path | Auth | Notes |
|---|
| POST | /licenses | seller | Issue (lic_ + NG-… key) |
| GET | /licenses | seller/admin | Scoped list |
| PATCH | /licenses/:id | seller(owner) | status/expires_at |
| POST | /licenses/:id/activations | seller(owner) | Legacy device_id record (display only) |
| DELETE | /licenses/:id/activations/:device | seller(owner) | |
| POST | /licenses/verify | public | Lookup-only verdict {valid,code,status,product_id,product_name,expires_at,max_devices,activated_devices,device_registered,checked_at}; codes `OK |
License — security plane (PoP; gated LICENSE_SECURITY_ENABLED=true, exact match; else 404 incl. preflight)
| Method | Path | Proof required | Notes |
|---|
| POST | /licenses/activate | none (license key) | {key, product_id, public_jwk} → validates JWK on-curve → gate check → atomic license_bindings slot (UNIQUE(lic_id,jkt), count<max_devices) → {entitlement, expires_in:120, features} |
| POST | /licenses/capability | entitlement + DPoP | {feature} → {cap_id, feature, exp} (60 s) |
| POST | /licenses/execute | entitlement + DPoP + capability | {feature,input,cap_id} → attribute + atomic consume → built-in premium_convert only → {result} + license_usage write (best-effort); reuse → 409 CAPABILITY_CONSUMED |
| GET | /licenses/status | entitlement + DPoP | {license:{product_id,product_name,status,expires_at,max_devices,bound_devices,activated_devices,features,entitlement_exp}} |
Guard order (guard()): entitlement sig/iss/exp → license row re-query (revoke/expire/product-mismatch) → DPoP sig/htm/htu/iat → jkt match → jti atomic consume (license_nonces) → body hash h. Denials write security_events (no keys/tokens/PII in detail_json).
Cron (5 min, wrangler.toml crons): dispatch_outbox always; prune_expired (nonces+capabilities) only when the flag is enabled.
Subscriptions / settlements
| Method | Path | Auth | Notes |
|---|
| GET | /subscriptions | seller/admin | |
| POST | /subscriptions/:id/event | seller(owner) | `{event: renew |
| GET | /settlements | seller/admin | |
| POST | /settlements | seller | Accrual run: gross/pg_fee/platform_fee/net from seller_settings rates; set_,ST-YYYY-NNNN, status pending |
Seller admin / settings / verification
| Method | Path | Auth | Notes |
|---|
| POST | /seller/onboarding | user | Creates seller_{…} + mem_ + sset_ + ver_ |
| GET/PATCH | /seller | seller | Profile, payout fields |
| GET/PATCH | /settings | seller | api_key, webhook, pg_fee_rate, platform_fee_rate, default_max_devices, store_published |
| GET | /verification | seller | |
| POST | /verification/apply | seller | ver_{…} row |
| POST | /admin/verification/review | admin | |
| Method | Path | Auth | Notes |
|---|
| GET | /admin/users · PATCH /admin/users/:id/status | admin | |
| GET | /admin/overview · /admin/sellers · /admin/verifications · /admin/audit | admin | |
| GET | /metrics | admin | |
| GET | /ledger/verify · /audit/verify | admin | Integrity-chain recompute |
Projects / game-item commerce (legacy)
| Method | Path | Auth |
|---|
| GET/POST | /projects · GET/PATCH /projects/:id | seller/admin |
| GET | /projects/:id/items · /players · /fulfillments | seller/admin |
| POST/PATCH | /items · /items/:id | seller |
| GET/POST · PATCH/PUT · DELETE | /products · /products/:id | seller(owner)/admin — see "Products" section above |
| GET/POST/PATCH | /feedback-widgets + /:id | seller |
| GET/POST/PATCH | /feedbacks + /:id | mixed (public widget post, seller patch) |
| POST | /feedback-payments | public (paid feedback) |
| GET | /widget-config/:id | public |
| GET | /seller/events | seller |
| GET | /dashboard/stats | seller/admin |
Public store
| Method | Path | Notes |
|---|
| GET | /public/stores/:slug · /public/stores/:slug/products · /public/stores/:slug/products/:id | store_published gate |
| GET | /public/orders/:order_no?email= | Buyer receipt lookup by order_no + email (public_impl.rs) |
Health & metrics
GET /api/v1/health → {ok, request_id} (ok = D1 binding present, public). GET /api/v1/metrics → {ok, request_id, metrics} (admin).
2. sdk/license wire contract (BYOS plane)
Three packages — @yetdapay/license-web (browser), @yetdapay/license-server (host), @yetdapay/license-nuxt (adapter). Source: sdk/license/README.md contract table; server entry sdk/license/server/src/server.mjs.
| Element | Value |
|---|
| Activation | POST {activatePath} (SDK default /api/license/activate — differs from Worker /api/v1/licenses/activate; pass activatePath) body {key, product_id, public_jwk} → {entitlement, expires_in, features} |
| Auth header | Authorization: Entitlement <entitlement-jws> |
| Proof header | DPoP: <proof-jws> payload {htu, htm, iat, jti, h, jwk} |
htu | same-origin pathname + query (not full URI) |
h | base64url(sha256(raw request body)) |
jkt | sha256(RFC 7638 JWK thumbprint) — DevPay-specific double-hash; entitlement carries jkt_bound + cnf.jkt |
| Entitlement claims | iss, aud?, lic_id, product_id, tenant_id?, features, jkt_bound, cnf, iat, exp — Worker emits iss=devpay-lic, no aud/tenant_id; SDK default iss=devpay |
| TTLs / windows | entitlement 120 s · proof iat ±60 s · capability 60 s · nonce TTL 600 s |
| Errors | {code, message} — internal reason never exposed; store failure → INTERNAL (fail closed) |
| Store contract | LicenseStore: getProduct/findLicenseByKey/getLicense/bindDevice/countBindings/setLicenseStatus/consumeNonce/createCapability/getCapability/consumeCapability (+optional pruneNonces) — sqlite + memory adapters shipped |
| Server API | createLicenseServer({store, keys, issuer, audience, entitlementTtlSec, expectedTenantId, resourceOwnership}) → activate() / authorize() / issueCapability(); createNuxtLicense → defineLicenseEventHandler({feature}, handler) |
| Client API | createLicenseClient({productId, activatePath?, …}) → activate({licenseKey}), signedFetch(url, init) (auto-renews entitlement once on 401) |
Interop delta (documented blocker, p5-sdk-contract-audit.md): SDK defaults (/api/license/activate, iss=devpay, optional aud) do not match Worker values (/api/v1/licenses/activate, iss=devpay-lic, no aud). Mutual interop is proven only with explicit path/issuer config. The wire format is a DevPay-specific versioned contract, not RFC 9449 DPoP.
| Entry | Invocation | Contract |
|---|
| Analyzer | node analyzer/run.mjs <target.{mjs,js,wasm}> [--driver x.mjs] [--out dir] [--no-dynamic] [--stdout] / --all-samples | → *.analysis.json (8-class verdict, L0–L4, confidence) |
| Spec validate | node spec/validate.mjs <spec.json> | exit 0/≠0; enforces closed invariant registry + REQUIRES_APPROVAL markers |
| Spec from requirements | node spec/from-requirements.mjs <requirements.json> [--overrides o.json] [--out spec.json] | Track-B draft |
| Spec from analysis | spec/from-analysis.mjs | Track-A draft |
| Planner | planner/plan.mjs (via track-a/b) | → ProtectionPlan (plan_version:1, spec_sha256, per-asset `AUTO |
| Track A | node track-a/run.mjs --target m.mjs [--driver d.mjs] --spec s.json [--out dir] · --draft [--overrides] · --apply-auto [--vectors v.json] [--partition-auth dev-open|yp-license] [--port N] [--license-seed f] | exit 0 ok / 2 input·invalid·refused / 3 apply failure; sha256-recorded artifacts + track-a-report.md |
| Track B | node track-b/run.mjs --requirements reqs.json --out dir [--approved-spec s.json] [--analysis a.json] [--overrides o.json] [--now ISO] | requirements→draft→plan handoff bundle |
| Partitioner | node partitioner/run.mjs <target.mjs> [--driver d.mjs] [--out dir] [--functions a,b] [--port N] [--auth dev-open|yp-license] [--allow-dev-open] [--feature NAME] [--vectors v.vectors.json] [--no-serve] | Refusal-first; emits generated server importing runtime/ + partitioned client + manifest |
| Evidence | evidence/record.mjs · evidence/validate.mjs · evidence/cross-track.mjs | record schema v1; claim-grade gates; cross-track report |
| Red team | red-team/unified-attacks.mjs (+ per-exp batteries) | → out/unified-attack-*, per-leg transcripts |
Generated partition servers speak HTTP on loopback with auth modes dev-open (no auth; opt-in + loopback only) or yp-license (the SDK entitlement gate above) — endpoints registered per partitioned function, plus nonce-gated attestation counts and dev-open-only /api/usage.
4. What is NOT part of the contract (yet)
- No API for protection spec/plan/evidence — CLI/file artifacts only.
- No usage-read or metering-report endpoints for sellers or BYOS hosts.
- No seller-billing / platform-fee collection endpoints.
- No Axum routes for
activate/capability/execute/status — Worker-only. - FE client (
fe/lib/api/client.ts) does not call the security-plane routes — contract exists server-side only.