PK!AppAttest.swift// App Attest: register a hardware-backed key once, then produce a per-request // assertion proving this is a genuine, unmodified instance of the app on a // real device. // // The key is generated and attested once; the attestation goes to the BFF's // registration endpoint, which verifies it and stores the public key under // keyId. Each request then carries an assertion the Worker verifies against // that stored key. import Foundation import CryptoKit import DeviceCheck extension ClaudeBFF { struct Assertion { let keyId: String let assertion: String // base64 } /// Produce an App Attest assertion over the given client data, or nil when /// App Attest is unavailable (Simulator, unsupported device). static func appAttestAssertion(clientData: String) async throws -> Assertion? { let service = DCAppAttestService.shared guard service.isSupported else { return nil } let keyId = try await attestedKeyId(service) let hash = Data(SHA256.hash(data: Data(clientData.utf8))) let assertion = try await service.generateAssertion(keyId, clientDataHash: hash) return Assertion(keyId: keyId, assertion: assertion.base64EncodedString()) } private static let keyIdDefaultsKey = "ClaudeBFF.appAttestKeyId" private static func attestedKeyId(_ service: DCAppAttestService) async throws -> String { if let saved = UserDefaults.standard.string(forKey: keyIdDefaultsKey) { return saved } let keyId = try await service.generateKey() try await register(keyId, service) // Persist only after the BFF accepted the attestation, so a failed // registration retries with a fresh key on the next request. UserDefaults.standard.set(keyId, forKey: keyIdDefaultsKey) return keyId } /// One-time registration: fetch a server-issued challenge, attest the key /// over it, then hand the attestation to the BFF so it can verify the /// chain and store the public key. The BFF consumes each challenge /// exactly once. private static func register(_ keyId: String, _ service: DCAppAttestService) async throws { let challenge = try await fetchChallenge() let hash = Data(SHA256.hash(data: Data(challenge.utf8))) let attestation = try await service.attestKey(keyId, clientDataHash: hash) var request = URLRequest(url: baseURL.appending(path: registerPath)) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode( registrationBody( keyId: keyId, attestation: attestation.base64EncodedString(), challenge: challenge ) ) let (_, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { throw BFFError.registrationFailed } } /// Ask the BFF for a one-time registration challenge. private static func fetchChallenge() async throws -> String { var request = URLRequest(url: baseURL.appending(path: challengePath)) request.httpMethod = "POST" let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, http.statusCode == 200, let body = try? JSONDecoder().decode([String: String].self, from: data), let challenge = body["challenge"] else { throw BFFError.registrationFailed } return challenge } /// Pure body assembly, testable without DeviceCheck or a network. static func registrationBody( keyId: String, attestation: String, challenge: String ) -> [String: String] { ["keyId": keyId, "attestation": attestation, "challenge": challenge] } } PK!J9BFFError.swift// Errors surfaced by the ClaudeBFF helper. // import Foundation enum BFFError: Error { case noActiveEntitlement case registrationFailed } PK!DClaudeBFF.swift// Namespace for talking to Claude through a gated Backend-for-Frontend Worker. // // Point baseURL at your own Worker. // Requires Xcode 27 / OS 27 betas and the ClaudeForFoundationModels package. // // A caseless enum is Swift's idiomatic namespace: it groups the static members // across these files and cannot be instantiated (unlike a struct, which can). // One concern per file: entitlement, App Attest, headers, and errors each live // in their own extension. import Foundation import FoundationModels import ClaudeForFoundationModels enum ClaudeBFF { /// The BFF endpoint host. The Foundation Models client appends the Messages /// path, so the Worker sees "/v1/messages", the one upstream path it allows. static let baseURL = URL(string: "https://bff.example.com")! /// Headers the BFF reads each credential from. static let transactionHeader = "X-IAP-Transaction" static let assertionHeader = "X-App-Attest-Assertion" static let keyIdHeader = "X-App-Attest-Key-Id" /// The BFF's one-time App Attest key registration endpoints. static let challengePath = "app-attest/challenge" static let registerPath = "app-attest/register" /// Convenience: gather credentials, build a session, and respond. static func respond(to prompt: String) async throws -> String { let session = LanguageModelSession(model: try await model()) let response = try await session.respond(to: prompt) return response.content } } PK!_E?ClaudeBFFClientTests.swift// Tests for the client's pure header assembly. // // Requires Xcode 27 / OS 27 betas; not run in this repo's CI. Set the test // module name to your target. import XCTest @testable import ClaudeBFFExample final class ClaudeBFFClientTests: XCTestCase { func testHeadersWithoutAttestationCarryOnlyTheTransaction() { let headers = ClaudeBFF.headers(jws: "signed-jws", attestation: nil) XCTAssertEqual(headers[ClaudeBFF.transactionHeader], "signed-jws") XCTAssertNil(headers[ClaudeBFF.assertionHeader]) XCTAssertNil(headers[ClaudeBFF.keyIdHeader]) } func testHeadersWithAttestationCarryBothCredentials() { let attestation = ClaudeBFF.Assertion(keyId: "key-1", assertion: "base64-assertion") let headers = ClaudeBFF.headers(jws: "signed-jws", attestation: attestation) XCTAssertEqual(headers[ClaudeBFF.transactionHeader], "signed-jws") XCTAssertEqual(headers[ClaudeBFF.assertionHeader], "base64-assertion") XCTAssertEqual(headers[ClaudeBFF.keyIdHeader], "key-1") } func testNoApiKeyHeaderIsEverSent() { let headers = ClaudeBFF.headers(jws: "signed-jws", attestation: nil) XCTAssertNil(headers["x-api-key"]) XCTAssertNil(headers["Authorization"]) } func testRegistrationBodyCarriesExactlyTheThreeFields() { let body = ClaudeBFF.registrationBody( keyId: "key-1", attestation: "base64-attestation", challenge: "challenge-1" ) XCTAssertEqual(body, [ "keyId": "key-1", "attestation": "base64-attestation", "challenge": "challenge-1", ]) } } PK!sEntitlement.swift// StoreKit entitlement: pull the current entitlement's signed representation. // import Foundation import StoreKit extension ClaudeBFF { /// The user's current entitlement as a signed JWS, if one is active. /// A free trial and a paid subscription both surface here while active, so /// the app does not distinguish them. The BFF makes the final decision and /// re-verifies the signature server-side. static func currentEntitlementJWS() async throws -> String { for await result in Transaction.currentEntitlements { // Forward the signed representation as-is. The device's own // verification is not the gate; the server's is. return result.jwsRepresentation } throw BFFError.noActiveEntitlement } } PK!+ProxyHeaders.swift// Header assembly and the proxied model. The app ships no API key; these // headers are how the Worker authorizes the caller before injecting the real // credential. // import Foundation import ClaudeForFoundationModels extension ClaudeBFF { /// Pure header assembly, so it can be tested without StoreKit or a device. /// The App Attest assertion is bound to the same request line the Worker /// reconstructs: "POST\n/v1/messages\n". static func headers(jws: String, attestation: Assertion?) -> [String: String] { var headers = [transactionHeader: jws] if let attestation { headers[assertionHeader] = attestation.assertion headers[keyIdHeader] = attestation.keyId } return headers } /// Gather the entitlement JWS and (when available) an App Attest assertion, /// then assemble the proxy headers. static func proxyHeaders() async throws -> [String: String] { let jws = try await currentEntitlementJWS() let clientData = "POST\n/v1/messages\n\(jws)" let attestation = try await appAttestAssertion(clientData: clientData) return headers(jws: jws, attestation: attestation) } /// A Claude model that routes through the BFF using `.proxied` auth. static func model(_ name: ClaudeModel = .opus4_8) async throws -> ClaudeLanguageModel { ClaudeLanguageModel( name: name, auth: .proxied(headers: try await proxyHeaders()), baseURL: baseURL ) } } PK!6 README.md# Gated Claude BFF, edge proxy, example code Source for the post *"Your API Key Doesn't Belong in the App."* These files are **illustrative examples, not deployed infrastructure**. Everything is a placeholder, no real keys, bundle IDs, team IDs, account IDs, or Worker URLs. ## Files | File | Role | |---|---| | `env.ts` | The Worker's configuration surface (gates, spend controls, bindings). | | `transaction.ts` | The decoded StoreKit 2 transaction fields the gate reads. | | `entitlement.ts` | The pure entitlement decision (no crypto, no network). | | `applejws.ts` | Shared Apple x5c chain verification (`jose`, `@peculiar/x509`). | | `storekit.ts` | StoreKit 2 signed-transaction verification (a typed wrapper). | | `revocation.ts` | Revocation record (Durable Object) plus the pure notification mapping. | | `notifications.ts` | App Store Server Notifications V2 webhook handler. | | `challenge.ts` | One-time App Attest registration challenges (Durable Object). | | `attestation.ts` | App Attest one-time registration (attestation verification). | | `attestkey.ts` | Registered App Attest keys and counters (Durable Object). | | `appattest.ts` | App Attest per-request assertion verification (P-256, counter). | | `limits.ts` | The pure spend decision (model allowlist, max_tokens cap). | | `budget.ts` | Per-caller daily token budget (Durable Object). | | `bytes.ts` | Byte helpers shared by the verifiers. | | `proxy.ts` | Upstream path allowlist, clean header set, forward to Anthropic. | | `worker.ts` | The entry: routes, config-gating, gates, spend controls, then proxies. | | `worker.test.ts` | Offline tests; verifiers and stores are injectable, so no installs. | | `wrangler.toml` | Worker config (vars, Durable Objects, the one secret). | | `ClaudeBFF.swift` and siblings | App-side helper, one type per file, StoreKit + App Attest. | | `ClaudeBFFClientTests.swift` | Tests for the client's pure header and body assembly. | ## Endpoints | Path | Purpose | |---|---| | `/v1/messages` | The gated proxy (every configured gate, then spend controls). | | `/apple/notifications` | App Store Server Notifications V2; writes the revocation record. | | `/app-attest/challenge` | Issues a one-time App Attest registration challenge. | | `/app-attest/register` | One-time App Attest key registration; stores the public key. | ## Run the worker tests (offline, no installs) ```sh node --experimental-strip-types --test worker.test.ts ``` This exercises the real decision-and-proxy path, who gets through, what they may spend, and what gets forwarded, with fakes in place of Apple's certificate chain, a real device, and Durable Objects. The production crypto in `applejws.ts` / `attestation.ts` / `appattest.ts` uses `jose`, `@peculiar/x509`, and `cbor-x` (`npm install`), imported lazily so the tests need none. ## Swift client ```sh swift test # or run in Xcode ``` Not run in this repo's CI; it requires Xcode 27 / OS 27 betas and the `ClaudeForFoundationModels` package. Set the test module name to your target. ## Deploying for real (outside this repo) ```sh wrangler secret put ANTHROPIC_API_KEY # the secret, never in wrangler.toml # set the gate(s) you want in wrangler.toml [vars]: # StoreKit: ALLOWED_BUNDLE_IDS + APPLE_ROOT_CA_SHA256 # App Attest: APP_ATTEST_TEAM_ID + APP_ATTEST_BUNDLE_ID + APP_ATTEST_ROOT_CA # and the spend controls: ALLOWED_MODELS, MAX_TOKENS_LIMIT, DAILY_TOKEN_BUDGET wrangler deploy # the migration deploys all four Durable Object classes ``` Configure at least one gate. With neither set, the Worker returns 500 rather than acting as an open relay for your API bill. Pin `APPLE_ROOT_CA_SHA256` to the SHA-256 of the DER-encoded *Apple Root CA - G3* certificate, and set `APP_ATTEST_ROOT_CA` to the *Apple App Attestation Root CA* certificate, both from . For revocation, point App Store Server Notifications V2 at `https:///apple/notifications` in App Store Connect (App Information, App Store Server Notifications). The route authenticates by verifying Apple's signature on the payload itself. PK!, appattest.ts/** * App Attest assertion verification, the per-request half. * * StoreKit answers "is this user entitled?" App Attest answers "is this even * my app talking?" The app attests a hardware-backed key once (attestation.ts * verifies it and stores the public key), then signs an assertion over each * request. This file verifies that per-request assertion: * * 1. look up the registered public key for keyId, * 2. CBOR-decode the assertion into { signature, authenticatorData }, * 3. check rpIdHash === SHA-256("."), * 4. verify the ES256 signature over (authenticatorData || clientDataHash), * 5. check the signature counter is strictly increasing (replay defense), * and only after a valid signature, so forgeries never burn a counter. * * The verifier is injectable, mirroring storekit.ts, so tests run offline * with fakes in place of a real device assertion and a stored public key. */ import type { Env } from "./env.ts"; import type { AttestKeyStore } from "./attestkey.ts"; import { base64ToBytes, bytesEqual, concatBytes } from "./bytes.ts"; export interface AppAttestConfig { teamId: string; bundleId: string; } export interface AppAttestAssertion { keyId: string; /** Base64 CBOR assertion produced by DCAppAttestService.generateAssertion. */ assertion: string; /** SHA-256 of the exact request payload the client signed over. */ clientDataHash: ArrayBuffer; } /** Resolves if the assertion is authentic; throws otherwise. */ export type AppAttestVerifier = ( assertion: AppAttestAssertion, cfg: AppAttestConfig, keys: AttestKeyStore, ) => Promise; /** App Attest is "configured" only when both identity values are present. */ export function appAttestConfig(env: Env): AppAttestConfig | null { if (!env.APP_ATTEST_TEAM_ID || !env.APP_ATTEST_BUNDLE_ID) return null; return { teamId: env.APP_ATTEST_TEAM_ID, bundleId: env.APP_ATTEST_BUNDLE_ID, }; } export const verifyAppAttestAssertion: AppAttestVerifier = async ( assertion, cfg, keys, ) => { if (!assertion.keyId || !assertion.assertion) { throw new Error("missing App Attest assertion"); } // 1. The registered key. An unregistered keyId is untrusted, full stop. const spkiBase64 = await keys.getPublicKey(assertion.keyId); if (!spkiBase64) { throw new Error("unregistered App Attest key"); } // 2. Decode. The bytes are untrusted, so guard lengths before slicing. const cbor = await import("cbor-x"); const decoded = cbor.decode(base64ToBytes(assertion.assertion)) as { authenticatorData?: Uint8Array; signature?: Uint8Array; }; const authData = decoded.authenticatorData; const signature = decoded.signature; if (!authData || !signature || authData.length < 37) { throw new Error("malformed App Attest assertion"); } // 3. The assertion must bind to this App ID. const rpIdHash = authData.slice(0, 32); const appId = `${cfg.teamId}.${cfg.bundleId}`; const expected = new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appId)), ); if (!bytesEqual(rpIdHash, expected)) { throw new Error("App Attest rpId does not match this app"); } // 4. The signature: ES256 over (authenticatorData || clientDataHash) with // the registered key. Apple emits DER-encoded ECDSA signatures and // WebCrypto verifies the raw r||s form, so convert first; skipping that // step makes every verification fail and is a classic landmine. const key = await crypto.subtle.importKey( "spki", base64ToBytes(spkiBase64), { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"], ); const message = concatBytes(authData, new Uint8Array(assertion.clientDataHash)); const ok = await crypto.subtle.verify( { name: "ECDSA", hash: "SHA-256" }, key, derToRaw(signature), message, ); if (!ok) { throw new Error("App Attest signature verification failed"); } // 5. The counter, last: only a valid signature may advance it. const counter = new DataView( authData.buffer, authData.byteOffset + 33, 4, ).getUint32(0); if (!(await keys.bump(assertion.keyId, counter))) { throw new Error("App Attest assertion replayed"); } }; /** * Convert a DER ECDSA-Sig-Value (SEQUENCE of two INTEGERs) to the raw 64-byte * r||s form WebCrypto expects for P-256. */ function derToRaw(der: Uint8Array): Uint8Array { if (der[0] !== 0x30) throw new Error("not a DER signature"); let offset = der[1] & 0x80 ? 2 + (der[1] & 0x7f) : 2; const readInteger = (): Uint8Array => { if (der[offset] !== 0x02) throw new Error("not a DER signature"); const len = der[offset + 1]; const start = offset + 2; if (!Number.isInteger(len) || start + len > der.length) { throw new Error("not a DER signature"); } offset = start + len; let bytes = der.slice(start, offset); // Strip the sign padding, then left-pad back to 32 bytes. while (bytes.length > 32 && bytes[0] === 0) bytes = bytes.slice(1); if (bytes.length > 32) throw new Error("not a P-256 signature"); const out = new Uint8Array(32); out.set(bytes, 32 - bytes.length); return out; }; const r = readInteger(); const s = readInteger(); const raw = new Uint8Array(64); raw.set(r, 0); raw.set(s, 32); return raw; } PK!bS]CC applejws.ts/** * Shared Apple certificate-chain and JWS verification. * * Three Apple credentials flow through this Worker, and all of them are signed * the same way: a StoreKit 2 transaction, an App Store Server Notifications V2 * payload, and an App Attest attestation each carry an x5c certificate chain. * This file owns the chain walk so every caller gets the same checks: every * certificate inside its validity window, every link signed by the next, and * the trust anchor pinned rather than assumed. * * Two anchoring modes, because Apple ships two chain shapes. StoreKit and the * notifications payload include the root in the chain, so pinning its SHA-256 * is enough. The App Attest attestation omits the root, so the verifier needs * the root certificate itself to check the last link's signature. * * The crypto dependencies are imported lazily so the pure logic elsewhere * stays unit-testable with zero installs. Production dependencies: `jose`, * `@peculiar/x509`. */ import type { X509Certificate } from "@peculiar/x509"; /** Apple chains are leaf, intermediate, root. Refuse anything outlandish. */ const MAX_CHAIN_LENGTH = 5; type X509Module = typeof import("@peculiar/x509"); async function chainCerts( x509: X509Module, x5c: Array, ): Promise { if (x5c.length < 2 || x5c.length > MAX_CHAIN_LENGTH) { throw new Error("unexpected certificate chain length"); } const certs = x5c.map((der) => new x509.X509Certificate(der)); const now = new Date(); for (const cert of certs) { if (now < cert.notBefore || now > cert.notAfter) { throw new Error("certificate outside its validity window"); } } // Verify each link in the chain (leaf first, root last). for (let i = 0; i < certs.length - 1; i++) { const issuerKey = await certs[i + 1].publicKey.export(); if (!(await certs[i].verify({ publicKey: issuerKey }))) { throw new Error("broken certificate chain"); } } return certs; } /** * Verify a chain that carries its own root, and pin that root by SHA-256. * Used for StoreKit 2 transactions and server notifications, whose x5c ends * at Apple Root CA - G3. */ export async function verifyChainAndPin( x5c: Array, pinnedSha256: string, ): Promise { const x509 = await import("@peculiar/x509"); const certs = await chainCerts(x509, x5c); const root = certs[certs.length - 1]; const rootSha256 = toHex(await crypto.subtle.digest("SHA-256", root.rawData)); if (rootSha256 !== normalizeHex(pinnedSha256)) { throw new Error("untrusted root certificate"); } return certs; } /** * Verify a chain that omits its root, anchoring the last certificate to a * configured trust anchor. Used for App Attest attestations, whose x5c stops * at the intermediate; `anchor` is the Apple App Attestation Root CA * certificate (PEM or base64 DER). */ export async function verifyChainToAnchor( x5c: Array, anchor: string, ): Promise { const x509 = await import("@peculiar/x509"); const certs = await chainCerts(x509, x5c); const root = new x509.X509Certificate(anchor); const rootKey = await root.publicKey.export(); const last = certs[certs.length - 1]; if (!(await last.verify({ publicKey: rootKey }))) { throw new Error("chain does not anchor to the pinned root"); } return certs; } /** * Verify an Apple JWS end to end: walk and pin the x5c chain, then verify the * signature with the leaf key, accepting only ES256. Returns the payload. */ export async function verifyAppleJWS( jws: string, pinnedRootSha256: string, ): Promise { const { decodeProtectedHeader, importX509, jwtVerify } = await import("jose"); const header = decodeProtectedHeader(jws); if (!Array.isArray(header.x5c)) { throw new Error("JWS is missing an x5c certificate chain"); } const certs = await verifyChainAndPin(header.x5c, pinnedRootSha256); const leafKey = await importX509(certs[0].toString("pem"), "ES256"); const { payload } = await jwtVerify(jws, leafKey, { algorithms: ["ES256"] }); return payload; } export function toHex(buf: ArrayBuffer): string { return [...new Uint8Array(buf)] .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export function normalizeHex(s: string): string { return s.toLowerCase().replace(/[^0-9a-f]/g, ""); } PK!|Sjr&&attestation.ts/** * App Attest one-time registration: verify the attestation object and store * the hardware-backed public key. * * The app asks the Worker for a challenge, calls * DCAppAttestService.attestKey over its SHA-256, and POSTs the result here. * The handler consumes the challenge exactly once (challenge.ts), then the * verifier follows Apple's documented procedure: check the certificate chain * to the Apple App Attestation Root CA, the nonce Apple embeds in the * credential certificate, the key identifier, and the authenticator data. * Only then does the public key land in the key record (attestkey.ts), where * appattest.ts looks it up on every request. */ import type { AppAttestConfig } from "./appattest.ts"; import type { AttestKeyStore } from "./attestkey.ts"; import type { ChallengeStore } from "./challenge.ts"; import { verifyChainToAnchor } from "./applejws.ts"; import { base64ToBytes, bytesEqual, bytesToBase64, concatBytes, } from "./bytes.ts"; export interface AttestationInput { keyId: string; /** Base64 CBOR attestation object from DCAppAttestService.attestKey. */ attestation: string; /** SHA-256 of the challenge the key was attested over. */ clientDataHash: ArrayBuffer; } export type AttestationConfig = AppAttestConfig & { /** The Apple App Attestation Root CA certificate (PEM or base64 DER). */ attestRootCa: string; }; /** Resolves with the verified public key (SPKI base64); throws otherwise. */ export type AttestationVerifier = ( input: AttestationInput, cfg: AttestationConfig, ) => Promise<{ spkiBase64: string }>; /** Apple's marker for the App Attest authenticator in authData. */ const AAGUIDS = ["appattest\0\0\0\0\0\0\0", "appattestdevelop"]; /** The credential-certificate extension Apple reserves for the nonce. */ const NONCE_EXTENSION_OID = "1.2.840.113635.100.8.2"; export const verifyAttestation: AttestationVerifier = async (input, cfg) => { const cbor = await import("cbor-x"); const decoded = cbor.decode(base64ToBytes(input.attestation)) as { fmt?: string; attStmt?: { x5c?: Uint8Array[] }; authData?: Uint8Array; }; if (decoded.fmt !== "apple-appattest") { throw new Error("not an App Attest attestation"); } const x5c = decoded.attStmt?.x5c; const authData = decoded.authData; if (!Array.isArray(x5c) || !authData || authData.length < 55) { throw new Error("malformed attestation object"); } // 1. The chain: credCert <- intermediate, anchored to the configured Apple // App Attestation Root CA. The x5c omits the root, which is why the // anchor is a certificate rather than a fingerprint. const certs = await verifyChainToAnchor(x5c, cfg.attestRootCa); const credCert = certs[0]; // 2. The nonce: SHA-256(authData || clientDataHash) must appear in the // credential certificate's reserved extension. This binds the // certificate Apple issued to this exact attestation. const nonce = new Uint8Array( await crypto.subtle.digest( "SHA-256", concatBytes(authData, new Uint8Array(input.clientDataHash)), ), ); const ext = credCert.getExtension(NONCE_EXTENSION_OID); if (!ext || !containsOctetString(new Uint8Array(ext.value), nonce)) { throw new Error("attestation nonce mismatch"); } // 3. The key identifier: SHA-256 of the credential public key (the // uncompressed P-256 point) must equal the keyId the app submitted. const spki = new Uint8Array(credCert.publicKey.rawData); const point = spki.slice(spki.length - 65); if (spki.length < 65 || point[0] !== 0x04) { throw new Error("unexpected public key format"); } const keyIdHash = new Uint8Array(await crypto.subtle.digest("SHA-256", point)); if (!bytesEqual(keyIdHash, base64ToBytes(input.keyId))) { throw new Error("key identifier does not match the attested key"); } // 4. The authenticator data: bound to this App ID, counter at zero, the // App Attest aaguid, and a credentialId equal to the key identifier. const appId = `${cfg.teamId}.${cfg.bundleId}`; const expectedRpId = new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appId)), ); if (!bytesEqual(authData.slice(0, 32), expectedRpId)) { throw new Error("attestation rpId does not match this app"); } const counter = new DataView( authData.buffer, authData.byteOffset + 33, 4, ).getUint32(0); if (counter !== 0) { throw new Error("attestation counter must be zero"); } const aaguid = new TextDecoder().decode(authData.slice(37, 53)); if (!AAGUIDS.includes(aaguid)) { throw new Error("unexpected attestation aaguid"); } const credIdLen = (authData[53] << 8) | authData[54]; if (authData.length < 55 + credIdLen) { throw new Error("malformed attestation object"); } const credentialId = authData.slice(55, 55 + credIdLen); if (!bytesEqual(credentialId, base64ToBytes(input.keyId))) { throw new Error("credentialId does not match the key identifier"); } return { spkiBase64: bytesToBase64(spki) }; }; export async function handleRegistration( request: Request, cfg: AttestationConfig, verifier: AttestationVerifier, keys: AttestKeyStore, challenges: ChallengeStore, ): Promise { let body: { keyId?: unknown; attestation?: unknown; challenge?: unknown }; try { body = (await request.json()) as typeof body; } catch { return new Response("malformed registration", { status: 400 }); } const { keyId, attestation, challenge } = body; if ( typeof keyId !== "string" || typeof attestation !== "string" || typeof challenge !== "string" ) { return new Response("malformed registration", { status: 400 }); } // The challenge is consumed before verification, so one issued challenge // buys one attempt; a failed attempt starts over with a fresh challenge. if (!(await challenges.consume(challenge))) { return new Response("unknown or used challenge", { status: 401 }); } const clientDataHash = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(challenge), ); try { const { spkiBase64 } = await verifier( { keyId, attestation, clientDataHash }, cfg, ); await keys.putPublicKey(keyId, spkiBase64); } catch { return new Response("invalid attestation", { status: 401 }); } return new Response(null, { status: 204 }); } /** * Scan DER for an OCTET STRING holding exactly `needle`. Apple wraps the * nonce as a single tagged octet string inside a small sequence; scanning for * the tagged value avoids a full ASN.1 parser for one field. */ function containsOctetString(der: Uint8Array, needle: Uint8Array): boolean { for (let i = 0; i + 2 + needle.length <= der.length; i++) { if ( der[i] === 0x04 && der[i + 1] === needle.length && bytesEqual(der.slice(i + 2, i + 2 + needle.length), needle) ) { return true; } } return false; } PK!|uT T attestkey.ts/** * The App Attest key record: one Durable Object per keyId holding the * registered public key and its assertion counter, plus the injectable store * the verifiers talk to. * * The key and the counter belong together: registration writes the public key * and starts the counter at zero, and every verified assertion advances the * counter through the same single-threaded instance. Durable Objects give * each keyId its own strongly consistent instance with great distributed * performance, so "strictly increasing" holds everywhere at once. */ import type { DurableObjectNamespaceLike, Env } from "./env.ts"; /** Injectable seam: registered keys and their assertion counters. */ export interface AttestKeyStore { getPublicKey(keyId: string): Promise; putPublicKey(keyId: string, spkiBase64: string): Promise; /** True only when `counter` is strictly greater than the stored value. */ bump(keyId: string, counter: number): Promise; } interface StorageLike { get(key: string): Promise; put(key: string, value: unknown): Promise; } interface StateLike { storage: StorageLike; } type AttestKeyRequest = | { action: "get" } | { action: "put"; spkiBase64: string } | { action: "bump"; counter: number }; /** One instance per keyId (idFromName). Classic fetch-based Durable Object. */ export class AttestKey { storage: StorageLike; constructor(state: StateLike) { this.storage = state.storage; } async fetch(request: Request): Promise { const body = (await request.json()) as AttestKeyRequest; if (body.action === "put") { await this.storage.put("spki", body.spkiBase64); await this.storage.put("counter", 0); return Response.json({ ok: true }); } if (body.action === "get") { const spki = (await this.storage.get("spki")) ?? null; return Response.json({ spki }); } const last = ((await this.storage.get("counter")) ?? 0) as number; if (!Number.isInteger(body.counter) || body.counter <= last) { return Response.json({ ok: false }); } await this.storage.put("counter", body.counter); return Response.json({ ok: true }); } } /** Wrap the ATTEST_KEYS binding, or null when it is not configured. */ export function attestKeyStoreFromEnv(env: Env): AttestKeyStore | null { const ns: DurableObjectNamespaceLike | undefined = env.ATTEST_KEYS; if (!ns) return null; const call = async (keyId: string, body: AttestKeyRequest) => { const stub = ns.get(ns.idFromName(keyId)); const res = await stub.fetch("https://attest-key/", { method: "POST", body: JSON.stringify(body), }); return res.json(); }; return { async getPublicKey(keyId) { const { spki } = (await call(keyId, { action: "get" })) as { spki: string | null; }; return spki; }, async putPublicKey(keyId, spkiBase64) { await call(keyId, { action: "put", spkiBase64 }); }, async bump(keyId, counter) { const { ok } = (await call(keyId, { action: "bump", counter })) as { ok: boolean; }; return ok; }, }; } PK!R   budget.ts/** * The per-caller daily token budget: a Durable Object plus the injectable * store the worker talks to. * * A Durable Object gives each caller identity one strongly consistent, * single-threaded instance with great distributed performance, so * debit-then-store is atomic and the budget holds everywhere at once. * * The debit is the request's declared max_tokens, taken before forwarding. * Simple and worst-case-safe: the caller can never be billed more output than * was debited. The honest trade is in the walkthrough's closing section. */ import type { DurableObjectNamespaceLike, Env } from "./env.ts"; /** Injectable seam: the worker depends on this, tests inject an in-memory fake. */ export interface BudgetStore { debit( callerId: string, tokens: number, limit: number, ): Promise<{ ok: boolean; remaining: number }>; } /** The slice of DurableObjectState the class uses; tests fake it with a Map. */ interface StorageLike { get(key: string): Promise; put(key: string, value: unknown): Promise; } interface StateLike { storage: StorageLike; } interface DebitRequest { tokens: number; limit: number; day: string; // UTC date, "YYYY-MM-DD"; a new day resets the window } /** * One instance per caller identity (idFromName). Classic fetch-based Durable * Object: no cloudflare:workers import, so node:test can instantiate it * directly with a fake storage. */ export class TokenBudget { storage: StorageLike; constructor(state: StateLike) { this.storage = state.storage; } async fetch(request: Request): Promise { const { tokens, limit, day } = (await request.json()) as DebitRequest; const window = ((await this.storage.get("window")) ?? { day, used: 0, }) as { day: string; used: number }; const used = window.day === day ? window.used : 0; if (used + tokens > limit) { // Refused requests are not debited; the caller keeps what remains. return Response.json({ ok: false, remaining: Math.max(0, limit - used) }); } await this.storage.put("window", { day, used: used + tokens }); return Response.json({ ok: true, remaining: limit - used - tokens }); } } /** Wrap the TOKEN_BUDGET binding, or null when it is not configured. */ export function budgetStoreFromEnv(env: Env): BudgetStore | null { const ns: DurableObjectNamespaceLike | undefined = env.TOKEN_BUDGET; if (!ns) return null; return { async debit(callerId, tokens, limit) { const stub = ns.get(ns.idFromName(callerId)); const day = new Date().toISOString().slice(0, 10); const res = await stub.fetch("https://budget/debit", { method: "POST", body: JSON.stringify({ tokens, limit, day }), }); return (await res.json()) as { ok: boolean; remaining: number }; }, }; } PK!A}bytes.ts/** * Byte helpers shared by the verifiers: base64, constant-time comparison, and * concatenation. No crypto and no dependencies. */ export function base64ToBytes(b64: string): Uint8Array { const bin = atob(b64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } export function bytesToBase64(bytes: Uint8Array): string { let bin = ""; for (const b of bytes) bin += String.fromCharCode(b); return btoa(bin); } /** Constant-time equality, so a comparison never leaks how far it matched. */ export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; return diff === 0; } export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { const out = new Uint8Array(a.length + b.length); out.set(a, 0); out.set(b, a.length); return out; } PK! challenge.ts/** * The App Attest registration challenge: the Worker issues it, a Durable * Object remembers it, and registration consumes it exactly once. * * The challenge is what binds an attestation to one registration attempt: the * app attests its key over the SHA-256 of a value the server chose, so a * captured registration cannot be replayed. One Durable Object per challenge * value makes "exactly once" atomic: consume is a single-threaded * test-and-set, and a spent or expired challenge never passes again. */ import type { DurableObjectNamespaceLike, Env } from "./env.ts"; /** A challenge is good for one registration attempt within this window. */ export const CHALLENGE_TTL_MS = 10 * 60 * 1000; /** Injectable seam: issue a fresh challenge, consume it exactly once. */ export interface ChallengeStore { issue(): Promise; consume(challenge: string): Promise; } interface StorageLike { get(key: string): Promise; put(key: string, value: unknown): Promise; } interface StateLike { storage: StorageLike; } type ChallengeRequest = | { action: "issue"; now: number } | { action: "consume"; now: number }; /** One instance per challenge value (idFromName). Classic fetch-based DO. */ export class AttestChallenge { storage: StorageLike; constructor(state: StateLike) { this.storage = state.storage; } async fetch(request: Request): Promise { const { action, now } = (await request.json()) as ChallengeRequest; if (action === "issue") { await this.storage.put("issuedAt", now); return Response.json({ ok: true }); } const issuedAt = (await this.storage.get("issuedAt")) as number | undefined; const used = ((await this.storage.get("used")) ?? false) as boolean; if (issuedAt == null || used || now - issuedAt > CHALLENGE_TTL_MS) { return Response.json({ ok: false }); } await this.storage.put("used", true); return Response.json({ ok: true }); } } /** Wrap the ATTEST_CHALLENGES binding, or null when it is not configured. */ export function challengeStoreFromEnv(env: Env): ChallengeStore | null { const ns: DurableObjectNamespaceLike | undefined = env.ATTEST_CHALLENGES; if (!ns) return null; const call = async (challenge: string, action: "issue" | "consume") => { const stub = ns.get(ns.idFromName(challenge)); const res = await stub.fetch("https://challenge/", { method: "POST", body: JSON.stringify({ action, now: Date.now() }), }); const { ok } = (await res.json()) as { ok: boolean }; return ok; }; return { async issue() { const challenge = crypto.randomUUID(); await call(challenge, "issue"); return challenge; }, consume: (challenge) => call(challenge, "consume"), }; } PK!}}entitlement.ts/** * The entitlement decision: a pure function, no crypto and no network. Given a * decoded StoreKit 2 transaction and the gate config, decide whether the caller * is entitled. Keeping it pure makes it testable in isolation. */ import type { JWSTransaction } from "./transaction.ts"; export interface GateConfig { allowedBundleIds: Set; allowedProductIds: Set | null; // null = any product now: number; // ms since epoch } export type GateResult = | { ok: true } | { ok: false; status: 401 | 403; reason: string }; /** Parse a comma-separated env var into a trimmed, non-empty Set. */ export function parseList(value: string | undefined): Set { return new Set( (value ?? "") .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0), ); } export function evaluateEntitlement( txn: JWSTransaction, cfg: GateConfig, ): GateResult { if (!txn.bundleId || !cfg.allowedBundleIds.has(txn.bundleId)) { return { ok: false, status: 403, reason: "bundle_id_not_allowed" }; } if ( cfg.allowedProductIds && (!txn.productId || !cfg.allowedProductIds.has(txn.productId)) ) { return { ok: false, status: 403, reason: "product_id_not_allowed" }; } // A sandbox transaction is signed by the same chain as production and costs // nothing to mint, so it must never clear the gate. if (txn.environment !== "Production") { return { ok: false, status: 403, reason: "environment_not_production" }; } if (txn.revocationDate != null) { return { ok: false, status: 403, reason: "entitlement_revoked" }; } // Trial and paid are the same check: a subscription is valid while its // expiresDate is in the future. Non-subscription products omit expiresDate. if (txn.expiresDate != null && txn.expiresDate <= cfg.now) { return { ok: false, status: 403, reason: "entitlement_expired" }; } return { ok: true }; } PK!Menv.ts/** * Env: the Worker's configuration surface. * * Two independent gates can be turned on by configuration: * * StoreKit (entitlement): set ALLOWED_BUNDLE_IDS + APPLE_ROOT_CA_SHA256. * App Attest (authenticity): set APP_ATTEST_TEAM_ID + APP_ATTEST_BUNDLE_ID. * * If neither is configured the Worker refuses to run (500). If both are * configured a request must clear both. * * On top of the gates, spend controls decide what an entitled caller may * spend: a model allowlist, a per-request max_tokens cap, and a per-caller * daily token budget. Each is optional and configuration-driven too. */ export interface Env { /** Anthropic API key. Set as a secret: `wrangler secret put ANTHROPIC_API_KEY`. */ ANTHROPIC_API_KEY: string; // --- StoreKit entitlement gate (active when both are set) --- /** Comma-separated allowlist of app bundle IDs, e.g. "com.example.app". */ ALLOWED_BUNDLE_IDS?: string; /** Comma-separated allowlist of product IDs. Empty/unset = accept any product. */ ALLOWED_PRODUCT_IDS?: string; /** Hex SHA-256 of the DER-encoded "Apple Root CA - G3" certificate. * Obtain and verify from https://www.apple.com/certificateauthority/ */ APPLE_ROOT_CA_SHA256?: string; // --- App Attest authenticity gate (active when both are set) --- /** Apple Developer Team ID the app ships under, e.g. "ABCDE12345". */ APP_ATTEST_TEAM_ID?: string; /** Bundle ID the attestation must be bound to, e.g. "com.example.app". */ APP_ATTEST_BUNDLE_ID?: string; /** The "Apple App Attestation Root CA" certificate (PEM or base64 DER). * The attestation's x5c omits the root, so a fingerprint cannot anchor the * chain; the verifier needs the certificate itself. Obtain and verify from * https://www.apple.com/certificateauthority/ */ APP_ATTEST_ROOT_CA?: string; // --- Spend controls (each optional) --- /** Comma-separated allowlist of model IDs the proxy will forward. */ ALLOWED_MODELS?: string; /** Upper bound on max_tokens per request, e.g. "4096". */ MAX_TOKENS_LIMIT?: string; /** Tokens each caller may request per UTC day, e.g. "200000". Debited with * the request's max_tokens before forwarding. */ DAILY_TOKEN_BUDGET?: string; // --- Proxy configuration --- /** Comma-separated allowlist of upstream paths. Defaults to "/v1/messages". */ ALLOWED_UPSTREAM_PATHS?: string; /** Anthropic API version header. Defaults to "2023-06-01". */ ANTHROPIC_VERSION?: string; // --- Durable Object bindings (declared in wrangler.toml) --- /** Per-caller daily token budget. */ TOKEN_BUDGET?: DurableObjectNamespaceLike; /** Registered App Attest public keys and assertion counters, per keyId. */ ATTEST_KEYS?: DurableObjectNamespaceLike; /** One-time App Attest registration challenges. */ ATTEST_CHALLENGES?: DurableObjectNamespaceLike; /** Revocation record per originalTransactionId, written by the webhook. */ REVOCATIONS?: DurableObjectNamespaceLike; } /** * Minimal structural type for a Durable Object namespace binding, so the * bundle type-checks and tests run with zero installs instead of depending on * `@cloudflare/workers-types`. The real binding satisfies this shape. */ export interface DurableObjectNamespaceLike { idFromName(name: string): unknown; get(id: unknown): { fetch(input: string, init?: RequestInit): Promise; }; } export const ANTHROPIC_BASE = "https://api.anthropic.com"; export const DEFAULT_ANTHROPIC_VERSION = "2023-06-01"; export const DEFAULT_UPSTREAM_PATHS = "/v1/messages"; /** Header carrying the StoreKit 2 signed transaction (JWS). */ export const TRANSACTION_HEADER = "X-IAP-Transaction"; /** Header carrying the App Attest assertion (base64). */ export const ASSERTION_HEADER = "X-App-Attest-Assertion"; /** Header carrying the App Attest key identifier the assertion was signed with. */ export const KEY_ID_HEADER = "X-App-Attest-Key-Id"; /** Route Apple's App Store Server Notifications V2 POSTs here. */ export const NOTIFICATIONS_PATH = "/apple/notifications"; /** Route the app's one-time App Attest key registration here. */ export const REGISTER_PATH = "/app-attest/register"; /** Route the app's registration-challenge requests here. */ export const CHALLENGE_PATH = "/app-attest/challenge"; PK! - - limits.ts/** * The spend decision: a pure function, no crypto and no network, mirroring * entitlement.ts. The gates decide who may call; this decides what a call may * cost: which model, how many tokens per request, and (via the worker and the * budget store) how many tokens per caller per day. */ import { parseList } from "./entitlement.ts"; import type { Env } from "./env.ts"; export interface SpendConfig { allowedModels: Set | null; // null = any model maxTokensLimit: number | null; // per-request cap dailyTokenBudget: number | null; // per-caller daily budget } /** Spend controls are active when any of the three knobs is set. */ export function spendConfig(env: Env): SpendConfig | null { const models = parseList(env.ALLOWED_MODELS); const allowedModels = models.size > 0 ? models : null; const maxTokensLimit = parsePositiveInt(env.MAX_TOKENS_LIMIT); const dailyTokenBudget = parsePositiveInt(env.DAILY_TOKEN_BUDGET); if (!allowedModels && maxTokensLimit == null && dailyTokenBudget == null) { return null; } return { allowedModels, maxTokensLimit, dailyTokenBudget }; } export type SpendResult = | { ok: true; maxTokens: number } | { ok: false; status: 400 | 403; reason: string }; /** Reject oversized bodies before parsing JSON. */ export const MAX_BODY_BYTES = 1_048_576; /** * Decide whether the parsed request body may be forwarded. A malformed shape * is 400; a policy denial is 403. Budget exhaustion is 429, decided in the * worker, because it needs the store. */ export function evaluateSpend(body: unknown, cfg: SpendConfig): SpendResult { if (typeof body !== "object" || body === null || Array.isArray(body)) { return { ok: false, status: 400, reason: "body_not_json_object" }; } const { model, max_tokens: maxTokens } = body as { model?: unknown; max_tokens?: unknown; }; if (cfg.allowedModels) { if (typeof model !== "string" || !cfg.allowedModels.has(model)) { return { ok: false, status: 403, reason: "model_not_allowed" }; } } // A cap can only be enforced, and a budget can only debit, what the request // declares, so max_tokens is mandatory whenever either is configured. if (cfg.maxTokensLimit != null || cfg.dailyTokenBudget != null) { if ( typeof maxTokens !== "number" || !Number.isInteger(maxTokens) || maxTokens <= 0 ) { return { ok: false, status: 400, reason: "max_tokens_required" }; } if (cfg.maxTokensLimit != null && maxTokens > cfg.maxTokensLimit) { return { ok: false, status: 400, reason: "max_tokens_exceeds_limit" }; } return { ok: true, maxTokens }; } return { ok: true, maxTokens: 0 }; } function parsePositiveInt(value: string | undefined): number | null { if (!value) return null; const n = Number(value); return Number.isInteger(n) && n > 0 ? n : null; } PK!)R| notifications.ts/** * The App Store Server Notifications V2 webhook: Apple pushing the truth * about refunds and revocations to the Worker. * * The route is unauthenticated by design. Apple does not sign requests with a * shared secret; it signs the payload itself. The body is one JWS * (signedPayload) wrapping another (data.signedTransactionInfo), both * verified against the same pinned Apple Root CA - G3 as a StoreKit * transaction. A forged POST fails the signature and gets a 401; the * signature is the authentication. */ import { verifyAppleJWS } from "./applejws.ts"; import { revocationFromNotification, type RevocationStore, } from "./revocation.ts"; export interface DecodedNotification { notificationType?: string; transaction: { originalTransactionId?: string }; } /** Verifier signature, so tests can swap in a fake and skip real certificates. */ export type NotificationVerifier = ( signedPayload: string, appleRootSha256: string, ) => Promise; export const verifyNotification: NotificationVerifier = async ( signedPayload, appleRootSha256, ) => { const payload = (await verifyAppleJWS(signedPayload, appleRootSha256)) as { notificationType?: string; data?: { signedTransactionInfo?: string }; }; const inner = payload.data?.signedTransactionInfo; const transaction = inner ? ((await verifyAppleJWS(inner, appleRootSha256)) as { originalTransactionId?: string; }) : {}; return { notificationType: payload.notificationType, transaction }; }; export async function handleNotification( request: Request, appleRootSha256: string, verifier: NotificationVerifier, store: RevocationStore, ): Promise { let signedPayload: unknown; try { ({ signedPayload } = (await request.json()) as { signedPayload?: unknown; }); } catch { return new Response("malformed notification", { status: 400 }); } if (typeof signedPayload !== "string") { return new Response("malformed notification", { status: 400 }); } let decoded: DecodedNotification; try { decoded = await verifier(signedPayload, appleRootSha256); } catch { // A failed signature is a 401 so App Store Connect retries a transient // problem; a forged payload never reaches the store. return new Response("invalid signature", { status: 401 }); } const action = revocationFromNotification( decoded.notificationType, decoded.transaction, ); if (action?.kind === "revoke") { await store.revoke(action.originalTransactionId, action.reason); } else if (action?.kind === "unrevoke") { await store.unrevoke(action.originalTransactionId); } // 200 for processed and ignored alike: Apple only needs delivery confirmed. return new Response(null, { status: 200 }); } PK!#gg package.json{ "name": "claude-bff-worker-example", "private": true, "type": "module", "description": "Example BFF Cloudflare Worker, a gated Claude proxy. Not deployed.", "scripts": { "test": "node --experimental-strip-types --test worker.test.ts" }, "dependencies": { "jose": "^5.9.6", "@peculiar/x509": "^1.12.3", "cbor-x": "^1.6.0" } } PK!~T  proxy.ts/** * The proxy: decide which upstream path is allowed, build a clean header set, * and forward to Anthropic. * * Two deliberate narrowings live here. First, an entitled caller may reach only * the upstream paths you opt into (default "/v1/messages"), not any Anthropic * endpoint your key can touch. Second, the upstream request carries only the * three headers it needs, not whatever the client sent (cookies, arbitrary * anthropic-beta flags, the app token). */ import { ANTHROPIC_BASE, DEFAULT_ANTHROPIC_VERSION, type Env, } from "./env.ts"; /** Resolve the upstream path, or null when the requested path is not allowed. */ export function resolveUpstreamPath( pathname: string, allowed: Set, ): string | null { return allowed.has(pathname) ? pathname : null; } /** * Build the only headers the upstream call needs. Nothing the client sent is * forwarded by default: we copy content-type, set the Anthropic version, and * inject the key. */ export function buildUpstreamHeaders(request: Request, env: Env): Headers { const headers = new Headers(); const contentType = request.headers.get("content-type"); if (contentType) headers.set("content-type", contentType); headers.set( "anthropic-version", env.ANTHROPIC_VERSION ?? DEFAULT_ANTHROPIC_VERSION, ); headers.set("x-api-key", env.ANTHROPIC_API_KEY); return headers; } /** * Forward the (already authorized) request to Anthropic. When the spend gate * has read the body, the same text is re-sent verbatim via `body`; otherwise * the request body streams through untouched. The query string is dropped on * purpose: resolveUpstreamPath matched only the pathname, so nothing unvetted * rides along. */ export function forwardToAnthropic( request: Request, env: Env, upstreamPath: string, body?: string, ): Promise { const headers = buildUpstreamHeaders(request, env); // The spend gate parsed the body as JSON, so pin the declared type to what // was actually inspected. if (body !== undefined) headers.set("content-type", "application/json"); return fetch(ANTHROPIC_BASE + upstreamPath, { method: request.method, headers, body: body ?? request.body, // @ts-expect-error duplex is required when streaming a request body duplex: "half", }); } PK!dή readme.json{ "schemaVersion": 1, "title": "A Gated Claude BFF at the Edge", "description": "A Cloudflare Worker that gates an Anthropic Claude proxy behind a StoreKit 2 in-app purchase and App Attest, records refunds server-side the moment Apple reports them, meters spend with a per-caller token budget, and injects the API key at the edge.", "url": "https://george.tsiokos.com/code/2026/claude-bff-edge-proxy/", "copyright": "© 2026 George Tsiokos. All rights reserved.", "author": "George Tsiokos", "siteUrl": "https://george.tsiokos.com", "commit": "cbe0089821a5c507cde517d7105b18da6c53ad98", "created": "2026-06-10T00:00:00Z", "lastModified": "2026-07-01T17:00:22Z", "generatedAt": "2026-07-01T17:00:55Z", "fileCount": 26, "totalBytes": 104210, "files": [ { "name": "AppAttest.swift", "bytes": 3981, "sha256": "7e311d562379d897ea4577656f7f0a2a9924907480456b5853d2e341cc74d013" }, { "name": "BFFError.swift", "bytes": 149, "sha256": "02591420ca5712e6c8a0b95eac6d0cb0f4329739a5c406238df9857fe049058f" }, { "name": "ClaudeBFF.swift", "bytes": 1489, "sha256": "4d8ab5f3180d2f71eedad8fe19d800970f66fb3152590a07c9801ee50ec11ee6" }, { "name": "ClaudeBFFClientTests.swift", "bytes": 1669, "sha256": "2526746a875e2f535e706e9c591367eea30c713c20b749249320abd65022e93f" }, { "name": "Entitlement.swift", "bytes": 788, "sha256": "dd3d33947fada08700417e70945bc301b39ec29c4e3a51fd71d867eb87fca4d5" }, { "name": "ProxyHeaders.swift", "bytes": 1536, "sha256": "de28cf46b3be3b57d98d0018cb1dbcfd40555418cea4767c28853a7c35078109" }, { "name": "README.md", "bytes": 4126, "sha256": "0895f017ec6cc1e69d8bc684f392b9ed2fe8763bcd455db9b7f5492259dba50b" }, { "name": "appattest.ts", "bytes": 5341, "sha256": "5dc9b709401c4a3f7d1b00c5a49a13ae0e4a8a1d6ea149bb3d63d5b1d2f1a229" }, { "name": "applejws.ts", "bytes": 4419, "sha256": "9dfae195c5d3ad07d06061b1f886da4acd7a91adf717da8ed8aaa76890cef74f" }, { "name": "attestation.ts", "bytes": 6950, "sha256": "062b6a927ef9963975b81b53073bc953f832e0d4d992885a33223ad47265aa72" }, { "name": "attestkey.ts", "bytes": 3156, "sha256": "d53b6046ae99c95504a511a323f3a78b612a7fd2b8c6de2db1054c3442205bbc" }, { "name": "budget.ts", "bytes": 2843, "sha256": "2a897a6fed97f483d3a06ad57d8616366293fbd88bc7cc1b0ce9e8fbc0db1725" }, { "name": "bytes.ts", "bytes": 967, "sha256": "511baca8ed0e00d3a08119eb4e3a7e7f678e856cf1bdab6b53e4d1fb84571059" }, { "name": "challenge.ts", "bytes": 2794, "sha256": "d5e1a8e2d6acd90e297e674930cb2396982e665f1832d7d887185e3e7325b990" }, { "name": "entitlement.ts", "bytes": 1917, "sha256": "79bbfde96c50c13e69efbc5389e05150d5a4f4811340dea38fa69f3631ddf2e7" }, { "name": "env.ts", "bytes": 4286, "sha256": "82f1c57216c182854da09c19971db4616820d412121160ad4eebb618e593e85f" }, { "name": "limits.ts", "bytes": 2861, "sha256": "ee0d40c4fa88bd08e1d27315f5a9bc2ecfb0c2983b8e4514e87d516ec6332c81" }, { "name": "notifications.ts", "bytes": 2797, "sha256": "eb349fa2de6319c3a49f712e21b2ecc673116d6efeaf10c58877c4e332719421" }, { "name": "package.json", "bytes": 359, "sha256": "deedcc8d54ab66370b99f4bac33965bb29138f1e8de93887b93097a074622a49" }, { "name": "proxy.ts", "bytes": 2309, "sha256": "e582028dd8d73b420531795385e508e3deb832cb7570f694dbcda656438c7dbd" }, { "name": "revocation.ts", "bytes": 3806, "sha256": "c5c75105ef4b8e0b5f43d3f404fc46be558e8e3a9158651bdfe74c53ff22e22a" }, { "name": "storekit.ts", "bytes": 847, "sha256": "95742ffd1587e48327f710880834c5996a234701ecc5976977b252a71357c978" }, { "name": "transaction.ts", "bytes": 1023, "sha256": "890adcbe7b1ed6615080379cbb98a641eff69a110db59a438a1c15bf3e3941a4" }, { "name": "worker.test.ts", "bytes": 30891, "sha256": "4b2b3009ca577bd3f8f230f79ad838d7c61cbc30833e79731ce68871498a08e0" }, { "name": "worker.ts", "bytes": 10444, "sha256": "8671c89895f69b8c77971e4565fba724f9abf01570397ba8eae1adc353dedb5e" }, { "name": "wrangler.toml", "bytes": 2462, "sha256": "d0c7d7e68805fcdde9def1f1f35e4be1485d65989b07c0f982a612570f9433b7" } ] } PK!ݣ z revocation.ts/** * The server-side revocation record: what Apple has told us since the JWS in * the caller's hand was minted. * * The entitlement gate reads revocationDate from the token the client sends, * so on its own it only catches a refund the client honestly reports. This * record closes that: the notifications webhook writes refunds and * revocations here as Apple reports them, and the worker checks it on every * request. One Durable Object per originalTransactionId keeps the record * strongly consistent everywhere with great distributed performance. * * The notification-to-action mapping is a pure function, kept apart from the * store so it tests in isolation. */ import type { DurableObjectNamespaceLike, Env } from "./env.ts"; /** Injectable seam over the REVOCATIONS binding. */ export interface RevocationStore { isRevoked(originalTransactionId: string): Promise; revoke(originalTransactionId: string, reason: string): Promise; unrevoke(originalTransactionId: string): Promise; } interface StorageLike { get(key: string): Promise; put(key: string, value: unknown): Promise; } interface StateLike { storage: StorageLike; } type RevocationRequest = | { action: "get" } | { action: "revoke"; reason: string } | { action: "unrevoke" }; /** * One instance per originalTransactionId (idFromName). Classic fetch-based * Durable Object. */ export class RevocationRecord { storage: StorageLike; constructor(state: StateLike) { this.storage = state.storage; } async fetch(request: Request): Promise { const body = (await request.json()) as RevocationRequest; if (body.action === "revoke") { await this.storage.put("revoked", { reason: body.reason }); return Response.json({ revoked: true }); } if (body.action === "unrevoke") { await this.storage.put("revoked", null); return Response.json({ revoked: false }); } const revoked = ((await this.storage.get("revoked")) ?? null) != null; return Response.json({ revoked }); } } /** Wrap the REVOCATIONS binding, or null when it is not configured. */ export function revocationStoreFromEnv(env: Env): RevocationStore | null { const ns: DurableObjectNamespaceLike | undefined = env.REVOCATIONS; if (!ns) return null; const call = async (id: string, body: RevocationRequest) => { const stub = ns.get(ns.idFromName(id)); const res = await stub.fetch("https://revocation/", { method: "POST", body: JSON.stringify(body), }); const { revoked } = (await res.json()) as { revoked: boolean }; return revoked; }; return { isRevoked: (id) => call(id, { action: "get" }), async revoke(id, reason) { await call(id, { action: "revoke", reason }); }, async unrevoke(id) { await call(id, { action: "unrevoke" }); }, }; } export type RevocationAction = | { kind: "revoke"; originalTransactionId: string; reason: string } | { kind: "unrevoke"; originalTransactionId: string } | null; /** * Map a verified notification to a revocation action. REFUND and REVOKE * (family sharing loss) revoke; REFUND_REVERSED restores; every other type * (renewals, subscribes, test notifications) is none of this record's * business and maps to null. */ export function revocationFromNotification( notificationType: string | undefined, txn: { originalTransactionId?: string }, ): RevocationAction { const id = txn.originalTransactionId; if (!id) return null; if (notificationType === "REFUND" || notificationType === "REVOKE") { return { kind: "revoke", originalTransactionId: id, reason: notificationType }; } if (notificationType === "REFUND_REVERSED") { return { kind: "unrevoke", originalTransactionId: id }; } return null; } PK!6yOO storekit.ts/** * StoreKit 2 signed-transaction verification. * * A StoreKit 2 transaction is an Apple JWS whose x5c chain ends at Apple Root * CA - G3, so the whole job lives in applejws.ts: walk the chain, check every * validity window, pin the root, verify the ES256 signature with the leaf key. * This file keeps the typed seam, so tests can swap in a fake and skip real * certificates. */ import type { JWSTransaction } from "./transaction.ts"; import { verifyAppleJWS } from "./applejws.ts"; /** Verifier signature, so tests can swap in a fake and skip real certificates. */ export type TransactionVerifier = ( jws: string, appleRootSha256: string, ) => Promise; export const verifyTransactionJWS: TransactionVerifier = async ( jws, appleRootSha256, ) => (await verifyAppleJWS(jws, appleRootSha256)) as JWSTransaction; PK!04transaction.ts/** * JWSTransaction: the decoded StoreKit 2 transaction fields this gate cares * about. The full payload carries far more; we read only what the entitlement * decision needs. */ export interface JWSTransaction { bundleId?: string; productId?: string; /** * Stable across renewals and restores, so it is the identity everything * server-side keys on: the revocation record and the per-caller budget. */ originalTransactionId?: string; /** * "Sandbox" or "Production". StoreKit 2 signs sandbox transactions with the * same certificate chain as production, and a sandbox Apple ID is free, so a * Production check is the only thing that stops anyone minting a valid * entitlement for nothing. The gate requires "Production". */ environment?: "Sandbox" | "Production"; /** Subscription expiry, ms since epoch. Absent for non-subscription products. */ expiresDate?: number; /** Set (ms since epoch) when Apple revokes the purchase (refund, dispute). */ revocationDate?: number; } PK!30-~xxworker.test.ts/** * Tests for the IAP- and App-Attest-gated Claude BFF Worker. * * Run offline, zero installs: * node --experimental-strip-types --test worker.test.ts * * The crypto verifiers and the stores are injectable, so these tests exercise * the real decision-and-proxy logic, the part that decides who gets through, * what they may spend, and what gets forwarded, without Apple's certificate * chain, a real device, or Durable Objects. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { parseList, evaluateEntitlement } from "./entitlement.ts"; import { evaluateSpend, spendConfig } from "./limits.ts"; import { TokenBudget, type BudgetStore } from "./budget.ts"; import { AttestKey, type AttestKeyStore } from "./attestkey.ts"; import { AttestChallenge, CHALLENGE_TTL_MS, type ChallengeStore, } from "./challenge.ts"; import { RevocationRecord, revocationFromNotification, type RevocationStore, } from "./revocation.ts"; import type { JWSTransaction } from "./transaction.ts"; import type { TransactionVerifier } from "./storekit.ts"; import type { AppAttestVerifier } from "./appattest.ts"; import { handleRequest, type Handlers } from "./worker.ts"; import type { Env } from "./env.ts"; const baseCfg = () => ({ allowedBundleIds: new Set(["com.example.app"]), allowedProductIds: null as Set | null, now: 1_000_000, }); const prod = (extra: Partial = {}): JWSTransaction => ({ bundleId: "com.example.app", environment: "Production", ...extra, }); test("parseList trims and drops empties", () => { assert.deepEqual([...parseList(" a , b ,, c ")], ["a", "b", "c"]); assert.equal(parseList(undefined).size, 0); }); test("entitlement: allowed bundle + Production + future expiry passes", () => { assert.deepEqual( evaluateEntitlement(prod({ expiresDate: 2_000_000 }), baseCfg()), { ok: true }, ); }); test("entitlement: non-subscription (no expiresDate) passes", () => { assert.deepEqual(evaluateEntitlement(prod(), baseCfg()), { ok: true }); }); test("entitlement: sandbox environment is rejected (403)", () => { const txn = prod({ environment: "Sandbox", expiresDate: 2_000_000 }); assert.deepEqual(evaluateEntitlement(txn, baseCfg()), { ok: false, status: 403, reason: "environment_not_production", }); }); test("entitlement: missing environment is rejected (403)", () => { const txn: JWSTransaction = { bundleId: "com.example.app" }; assert.deepEqual(evaluateEntitlement(txn, baseCfg()), { ok: false, status: 403, reason: "environment_not_production", }); }); test("entitlement: wrong bundle is rejected (403)", () => { const txn = prod({ bundleId: "com.evil.app", expiresDate: 2_000_000 }); assert.deepEqual(evaluateEntitlement(txn, baseCfg()), { ok: false, status: 403, reason: "bundle_id_not_allowed", }); }); test("entitlement: expired subscription is rejected (403)", () => { const txn = prod({ expiresDate: 500_000 }); assert.deepEqual(evaluateEntitlement(txn, baseCfg()), { ok: false, status: 403, reason: "entitlement_expired", }); }); test("entitlement: revoked purchase is rejected (403)", () => { const txn = prod({ expiresDate: 2_000_000, revocationDate: 600_000 }); assert.deepEqual(evaluateEntitlement(txn, baseCfg()), { ok: false, status: 403, reason: "entitlement_revoked", }); }); test("entitlement: product allowlist is enforced when set", () => { const cfg = { ...baseCfg(), allowedProductIds: new Set(["pro.monthly"]) }; assert.equal( evaluateEntitlement(prod({ productId: "pro.monthly" }), cfg).ok, true, ); assert.deepEqual(evaluateEntitlement(prod({ productId: "free.tier" }), cfg), { ok: false, status: 403, reason: "product_id_not_allowed", }); }); // --- spend decision (pure) --- const spendCfg = (over = {}) => ({ allowedModels: new Set(["claude-opus-4-8"]) as Set | null, maxTokensLimit: 1024 as number | null, dailyTokenBudget: null as number | null, ...over, }); test("spendConfig: null when no knob is set, active when any is", () => { assert.equal(spendConfig({ ANTHROPIC_API_KEY: "x" }), null); const cfg = spendConfig({ ANTHROPIC_API_KEY: "x", ALLOWED_MODELS: "m" }); assert.deepEqual(cfg, { allowedModels: new Set(["m"]), maxTokensLimit: null, dailyTokenBudget: null, }); }); test("spend: allowed model under the cap passes", () => { assert.deepEqual( evaluateSpend({ model: "claude-opus-4-8", max_tokens: 256 }, spendCfg()), { ok: true, maxTokens: 256 }, ); }); test("spend: model outside the allowlist is rejected (403)", () => { assert.deepEqual( evaluateSpend({ model: "claude-haiku-4-5", max_tokens: 256 }, spendCfg()), { ok: false, status: 403, reason: "model_not_allowed" }, ); assert.equal(evaluateSpend({ max_tokens: 256 }, spendCfg()).ok, false); }); test("spend: max_tokens is required when a cap or budget is configured", () => { for (const max_tokens of [undefined, 0, -5, 1.5, "16"]) { assert.deepEqual( evaluateSpend({ model: "claude-opus-4-8", max_tokens }, spendCfg()), { ok: false, status: 400, reason: "max_tokens_required" }, ); } const budgetOnly = spendCfg({ allowedModels: null, maxTokensLimit: null, dailyTokenBudget: 1000, }); assert.equal(evaluateSpend({}, budgetOnly).ok, false); }); test("spend: max_tokens over the cap is rejected (400)", () => { assert.deepEqual( evaluateSpend({ model: "claude-opus-4-8", max_tokens: 2048 }, spendCfg()), { ok: false, status: 400, reason: "max_tokens_exceeds_limit" }, ); }); test("spend: non-object body is rejected (400)", () => { for (const body of [null, [], "x", 7]) { assert.deepEqual(evaluateSpend(body, spendCfg()), { ok: false, status: 400, reason: "body_not_json_object", }); } }); // --- Durable Objects, tested directly with a fake storage --- function fakeState() { const map = new Map(); return { storage: { get: async (key: string) => map.get(key), put: async (key: string, value: unknown) => { map.set(key, value); }, }, }; } test("TokenBudget: debits, refuses over-limit without debiting, resets daily", async () => { const dobj = new TokenBudget(fakeState()); const debit = async (tokens: number, day: string) => { const res = await dobj.fetch( new Request("https://budget/debit", { method: "POST", body: JSON.stringify({ tokens, limit: 1000, day }), }), ); return res.json(); }; assert.deepEqual(await debit(600, "2026-06-12"), { ok: true, remaining: 400 }); assert.deepEqual(await debit(600, "2026-06-12"), { ok: false, remaining: 400 }); // The refused request was not debited; a smaller one still fits. assert.deepEqual(await debit(400, "2026-06-12"), { ok: true, remaining: 0 }); // A new day resets the window. assert.deepEqual(await debit(600, "2026-06-13"), { ok: true, remaining: 400 }); }); test("AttestKey: stores the key, accepts only strictly increasing counters", async () => { const dobj = new AttestKey(fakeState()); const call = async (body: unknown) => { const res = await dobj.fetch( new Request("https://attest-key/", { method: "POST", body: JSON.stringify(body), }), ); return res.json(); }; assert.deepEqual(await call({ action: "get" }), { spki: null }); assert.deepEqual(await call({ action: "put", spkiBase64: "spki" }), { ok: true, }); assert.deepEqual(await call({ action: "get" }), { spki: "spki" }); assert.deepEqual(await call({ action: "bump", counter: 1 }), { ok: true }); assert.deepEqual(await call({ action: "bump", counter: 2 }), { ok: true }); assert.deepEqual(await call({ action: "bump", counter: 2 }), { ok: false }); // replay assert.deepEqual(await call({ action: "bump", counter: 1 }), { ok: false }); // rollback }); test("AttestChallenge: consumed exactly once, within its window", async () => { const dobj = new AttestChallenge(fakeState()); const call = async (action: "issue" | "consume", now: number) => { const res = await dobj.fetch( new Request("https://challenge/", { method: "POST", body: JSON.stringify({ action, now }), }), ); return res.json(); }; // Consuming before issuing fails. assert.deepEqual(await call("consume", 1_000), { ok: false }); await call("issue", 1_000); assert.deepEqual(await call("consume", 2_000), { ok: true }); assert.deepEqual(await call("consume", 3_000), { ok: false }); // spent // A fresh challenge past its window fails. const stale = new AttestChallenge(fakeState()); const staleCall = async (action: "issue" | "consume", now: number) => { const res = await stale.fetch( new Request("https://challenge/", { method: "POST", body: JSON.stringify({ action, now }), }), ); return res.json(); }; await staleCall("issue", 1_000); assert.deepEqual(await staleCall("consume", 1_000 + CHALLENGE_TTL_MS + 1), { ok: false, }); }); test("RevocationRecord: revoke sets, unrevoke clears", async () => { const dobj = new RevocationRecord(fakeState()); const call = async (body: unknown) => { const res = await dobj.fetch( new Request("https://revocation/", { method: "POST", body: JSON.stringify(body), }), ); return res.json(); }; assert.deepEqual(await call({ action: "get" }), { revoked: false }); assert.deepEqual(await call({ action: "revoke", reason: "REFUND" }), { revoked: true, }); assert.deepEqual(await call({ action: "get" }), { revoked: true }); assert.deepEqual(await call({ action: "unrevoke" }), { revoked: false }); assert.deepEqual(await call({ action: "get" }), { revoked: false }); }); // --- revocation mapping (pure) --- test("revocation: REFUND and REVOKE revoke, REFUND_REVERSED restores", () => { const txn = { originalTransactionId: "txn-1" }; assert.deepEqual(revocationFromNotification("REFUND", txn), { kind: "revoke", originalTransactionId: "txn-1", reason: "REFUND", }); assert.deepEqual(revocationFromNotification("REVOKE", txn), { kind: "revoke", originalTransactionId: "txn-1", reason: "REVOKE", }); assert.deepEqual(revocationFromNotification("REFUND_REVERSED", txn), { kind: "unrevoke", originalTransactionId: "txn-1", }); assert.equal(revocationFromNotification("SUBSCRIBED", txn), null); assert.equal(revocationFromNotification("TEST", txn), null); assert.equal(revocationFromNotification("REFUND", {}), null); }); // --- handleRequest integration --- const storeKitEnv: Env = { ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER", ALLOWED_BUNDLE_IDS: "com.example.app", APPLE_ROOT_CA_SHA256: "00", }; function messagesRequest( headers: Record, path = "/v1/messages", body: unknown = { model: "claude-opus-4-8", max_tokens: 16, messages: [] }, ): Request { return new Request(`https://bff.example.com${path}`, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body), }); } const passTxn: TransactionVerifier = async () => prod({ expiresDate: Date.now() + 60_000 }); function memoryKeyStore( initial: Record = {}, ): AttestKeyStore & { map: Map } { const map = new Map(Object.entries(initial)); const counters = new Map(); return { map, getPublicKey: async (keyId) => map.get(keyId) ?? null, putPublicKey: async (keyId, spkiBase64) => { map.set(keyId, spkiBase64); counters.set(keyId, 0); }, bump: async (keyId, counter) => { const last = counters.get(keyId) ?? 0; if (counter <= last) return false; counters.set(keyId, counter); return true; }, }; } function memoryChallengeStore(): ChallengeStore & { issued: Set } { const issued = new Set(); let n = 0; return { issued, issue: async () => { const challenge = `challenge-${++n}`; issued.add(challenge); return challenge; }, consume: async (challenge) => issued.delete(challenge), }; } function memoryRevocationStore( revoked: string[] = [], ): RevocationStore & { map: Map } { const map = new Map(revoked.map((id) => [id, "REFUND"])); return { map, isRevoked: async (id) => map.has(id), revoke: async (id, reason) => { map.set(id, reason); }, unrevoke: async (id) => { map.delete(id); }, }; } function memoryBudgetStore(): BudgetStore & { seen: string[] } { const used = new Map(); const seen: string[] = []; return { seen, debit: async (callerId, tokens, limit) => { seen.push(callerId); const u = used.get(callerId) ?? 0; if (u + tokens > limit) { return { ok: false, remaining: Math.max(0, limit - u) }; } used.set(callerId, u + tokens); return { ok: true, remaining: limit - u - tokens }; }, }; } function handlers(over: Partial = {}): Handlers { const keys = memoryKeyStore({ "key-1": "spki" }); const challenges = memoryChallengeStore(); return { verifyTransaction: passTxn, verifyAppAttest: async () => {}, verifyAttestation: async () => ({ spkiBase64: "spki" }), verifyNotification: async () => ({ notificationType: "TEST", transaction: {}, }), budgetStore: () => null, attestKeyStore: () => keys, challengeStore: () => challenges, revocationStore: () => null, ...over, }; } /** Mock global fetch for one call, capturing the upstream request. */ async function withMockedFetch( run: () => Promise, ): Promise<{ result: T; calls: Array<{ url: string; init: RequestInit }> }> { const calls: Array<{ url: string; init: RequestInit }> = []; const orig = globalThis.fetch; globalThis.fetch = (async (url: unknown, init: RequestInit) => { calls.push({ url: String(url), init }); return new Response(JSON.stringify({ ok: true }), { status: 200 }); }) as typeof fetch; try { return { result: await run(), calls }; } finally { globalThis.fetch = orig; } } test("handleRequest: no gate configured → 500, no upstream call", async () => { const res = await handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), { ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER" }, handlers(), ); assert.equal(res.status, 500); }); test("handleRequest: disallowed upstream path → 403", async () => { const res = await handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/models"), storeKitEnv, handlers(), ); assert.equal(res.status, 403); }); test("handleRequest: missing transaction header → 401, verifier never called", async () => { let called = false; const res = await handleRequest( messagesRequest({}), storeKitEnv, handlers({ verifyTransaction: async () => { called = true; return prod(); }, }), ); assert.equal(res.status, 401); assert.equal(called, false); }); test("handleRequest: invalid JWS → 401, no upstream call", async () => { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "bad" }), storeKitEnv, handlers({ verifyTransaction: async () => { throw new Error("bad signature"); }, }), ), ); assert.equal(result.status, 401); assert.equal(calls.length, 0); }); test("handleRequest: unentitled caller → 403, no upstream call", async () => { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), storeKitEnv, handlers({ verifyTransaction: async () => prod({ bundleId: "com.evil.app" }), }), ), ); assert.equal(result.status, 403); assert.equal(calls.length, 0); }); test("handleRequest: entitled caller → forwards with a clean, injected header set", async () => { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok", cookie: "session=abc", "anthropic-beta": "evil-flag", }), storeKitEnv, handlers(), ), ); assert.equal(result.status, 200); assert.equal(calls[0].url, "https://api.anthropic.com/v1/messages"); const h = new Headers(calls[0].init.headers); assert.equal(h.get("x-api-key"), "sk-ant-PLACEHOLDER"); assert.equal(h.get("anthropic-version"), "2023-06-01"); assert.equal(h.get("content-type"), "application/json"); // Nothing the client sent leaks upstream. assert.equal(h.get("X-IAP-Transaction"), null); assert.equal(h.get("cookie"), null); assert.equal(h.get("anthropic-beta"), null); }); test("handleRequest: revoked originalTransactionId → 403, no upstream call", async () => { const revocations = memoryRevocationStore(["txn-9"]); const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), storeKitEnv, handlers({ verifyTransaction: async () => prod({ expiresDate: Date.now() + 60_000, originalTransactionId: "txn-9", }), revocationStore: () => revocations, }), ), ); assert.equal(result.status, 403); assert.equal(calls.length, 0); }); // --- App Attest gate --- const appAttestEnv: Env = { ANTHROPIC_API_KEY: "sk-ant-PLACEHOLDER", APP_ATTEST_TEAM_ID: "ABCDE12345", APP_ATTEST_BUNDLE_ID: "com.example.app", }; test("handleRequest: App Attest only, missing assertion → 401", async () => { const res = await handleRequest(messagesRequest({}), appAttestEnv, handlers()); assert.equal(res.status, 401); }); test("handleRequest: App Attest only, valid assertion → forwards", async () => { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-App-Attest-Assertion": "ok", "X-App-Attest-Key-Id": "key-1", }), appAttestEnv, handlers(), ), ); assert.equal(result.status, 200); assert.equal(calls[0].url, "https://api.anthropic.com/v1/messages"); }); test("handleRequest: App Attest failure → 401, no upstream call", async () => { const failAttest: AppAttestVerifier = async () => { throw new Error("bad assertion"); }; const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-App-Attest-Assertion": "bad", "X-App-Attest-Key-Id": "key-1", }), appAttestEnv, handlers({ verifyAppAttest: failAttest }), ), ); assert.equal(result.status, 401); assert.equal(calls.length, 0); }); test("handleRequest: App Attest configured without stores → 500", async () => { const res = await handleRequest( messagesRequest({ "X-App-Attest-Assertion": "ok", "X-App-Attest-Key-Id": "key-1", }), appAttestEnv, handlers({ attestKeyStore: () => null }), ); assert.equal(res.status, 500); }); test("handleRequest: replayed counter (via the injected key store) → 401", async () => { // The fake verifier exercises the store wiring: it consumes the counter the // way the real verifier does, after every other check. const verifyAppAttest: AppAttestVerifier = async (a, _cfg, keys) => { if (!(await keys.bump(a.keyId, 7))) { throw new Error("replayed"); } }; const h = handlers({ verifyAppAttest }); const send = () => handleRequest( messagesRequest({ "X-App-Attest-Assertion": "ok", "X-App-Attest-Key-Id": "key-1", }), appAttestEnv, h, ); const { result: first } = await withMockedFetch(send); assert.equal(first.status, 200); const { result: replay, calls } = await withMockedFetch(send); assert.equal(replay.status, 401); assert.equal(calls.length, 0); }); test("handleRequest: both gates configured, request must clear both", async () => { const bothEnv: Env = { ...storeKitEnv, ...appAttestEnv }; const failAttest: AppAttestVerifier = async () => { throw new Error("bad assertion"); }; const { result: rejected } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok", "X-App-Attest-Assertion": "bad", "X-App-Attest-Key-Id": "key-1", }), bothEnv, handlers({ verifyAppAttest: failAttest }), ), ); assert.equal(rejected.status, 401); const { result: ok } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok", "X-App-Attest-Assertion": "ok", "X-App-Attest-Key-Id": "key-1", }), bothEnv, handlers(), ), ); assert.equal(ok.status, 200); }); // --- spend controls through the worker --- const spendEnv: Env = { ...storeKitEnv, ALLOWED_MODELS: "claude-opus-4-8", MAX_TOKENS_LIMIT: "1024", }; test("handleRequest: disallowed model → 403, no upstream call", async () => { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", { model: "claude-haiku-4-5", max_tokens: 16, messages: [], }), spendEnv, handlers(), ), ); assert.equal(result.status, 403); assert.equal(calls.length, 0); }); test("handleRequest: max_tokens over the cap or missing → 400", async () => { for (const body of [ { model: "claude-opus-4-8", max_tokens: 4096, messages: [] }, { model: "claude-opus-4-8", messages: [] }, ]) { const { result, calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", body), spendEnv, handlers(), ), ); assert.equal(result.status, 400); assert.equal(calls.length, 0); } }); test("handleRequest: spend gate forwards the body it inspected, verbatim", async () => { const body = { model: "claude-opus-4-8", max_tokens: 16, messages: [] }; const { calls } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", body), spendEnv, handlers(), ), ); assert.equal(calls[0].init.body, JSON.stringify(body)); }); test("handleRequest: budget configured without a store → 500", async () => { const res = await handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" }, handlers(), ); assert.equal(res.status, 500); }); test("handleRequest: daily budget exhausts → 200 then 429, one upstream call", async () => { const budget = memoryBudgetStore(); const env: Env = { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" }; const h = handlers({ verifyTransaction: async () => prod({ expiresDate: Date.now() + 60_000, originalTransactionId: "txn-1", }), budgetStore: () => budget, }); const send = () => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }, "/v1/messages", { model: "claude-opus-4-8", max_tokens: 80, messages: [], }), env, h, ); const { result: first, calls: firstCalls } = await withMockedFetch(send); assert.equal(first.status, 200); assert.equal(firstCalls.length, 1); const { result: second, calls: secondCalls } = await withMockedFetch(send); assert.equal(second.status, 429); assert.equal(secondCalls.length, 0); assert.deepEqual(await second.json(), { error: "daily_token_budget_exhausted", remaining: 20, }); }); test("handleRequest: budget keys on originalTransactionId, or verified keyId", async () => { // StoreKit: the purchase identity wins. const viaStoreKit = memoryBudgetStore(); await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" }, handlers({ verifyTransaction: async () => prod({ expiresDate: Date.now() + 60_000, originalTransactionId: "txn-1", }), budgetStore: () => viaStoreKit, }), ), ); assert.deepEqual(viaStoreKit.seen, ["txn-1"]); // App Attest only: the device key the assertion just proved. const viaKeyId = memoryBudgetStore(); await withMockedFetch(() => handleRequest( messagesRequest({ "X-App-Attest-Assertion": "ok", "X-App-Attest-Key-Id": "key-1", }), { ...appAttestEnv, DAILY_TOKEN_BUDGET: "100" }, handlers({ budgetStore: () => viaKeyId }), ), ); assert.deepEqual(viaKeyId.seen, ["key-1"]); // StoreKit without an originalTransactionId yields no verified identity. const noIdentity = memoryBudgetStore(); const { result } = await withMockedFetch(() => handleRequest( messagesRequest({ "X-IAP-Transaction": "ok" }), { ...storeKitEnv, DAILY_TOKEN_BUDGET: "100" }, handlers({ budgetStore: () => noIdentity }), ), ); assert.equal(result.status, 403); assert.deepEqual(noIdentity.seen, []); }); // --- service routes --- test("notifications route: verified REFUND lands in the revocation store", async () => { const revocations = memoryRevocationStore(); const res = await handleRequest( messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }), storeKitEnv, handlers({ verifyNotification: async () => ({ notificationType: "REFUND", transaction: { originalTransactionId: "txn-7" }, }), revocationStore: () => revocations, }), ); assert.equal(res.status, 200); assert.equal(await revocations.isRevoked("txn-7"), true); }); test("notifications route: bad signature → 401, store untouched", async () => { const revocations = memoryRevocationStore(); const res = await handleRequest( messagesRequest({}, "/apple/notifications", { signedPayload: "forged" }), storeKitEnv, handlers({ verifyNotification: async () => { throw new Error("bad signature"); }, revocationStore: () => revocations, }), ); assert.equal(res.status, 401); assert.equal(revocations.map.size, 0); }); test("notifications route: ignored type and malformed body", async () => { const revocations = memoryRevocationStore(); const h = handlers({ revocationStore: () => revocations }); const ignored = await handleRequest( messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }), storeKitEnv, h, // default fake verifier returns TEST ); assert.equal(ignored.status, 200); assert.equal(revocations.map.size, 0); const malformed = await handleRequest( new Request("https://bff.example.com/apple/notifications", { method: "POST", body: "not json", }), storeKitEnv, h, ); assert.equal(malformed.status, 400); }); test("notifications route: 404 without a revocation store", async () => { const res = await handleRequest( messagesRequest({}, "/apple/notifications", { signedPayload: "jws" }), storeKitEnv, handlers(), // revocationStore: () => null ); assert.equal(res.status, 404); }); test("challenge route: issues a one-time challenge, 404 when unconfigured", async () => { const challenges = memoryChallengeStore(); const issued = await handleRequest( messagesRequest({}, "/app-attest/challenge", {}), appAttestEnv, handlers({ challengeStore: () => challenges }), ); assert.equal(issued.status, 200); const { challenge } = (await issued.json()) as { challenge: string }; assert.equal(challenges.issued.has(challenge), true); const unconfigured = await handleRequest( messagesRequest({}, "/app-attest/challenge", {}), storeKitEnv, // App Attest not configured handlers(), ); assert.equal(unconfigured.status, 404); }); test("registration route: issued challenge + verified attestation → 204, once", async () => { const keys = memoryKeyStore(); const challenges = memoryChallengeStore(); const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" }; const h = handlers({ attestKeyStore: () => keys, challengeStore: () => challenges, }); const challenge = await challenges.issue(); const register = () => handleRequest( messagesRequest({}, "/app-attest/register", { keyId: "key-2", attestation: "cbor", challenge, }), env, h, ); const res = await register(); assert.equal(res.status, 204); assert.equal(keys.map.get("key-2"), "spki"); // The same challenge never registers twice. const replay = await register(); assert.equal(replay.status, 401); }); test("registration route: unissued challenge → 401, verifier never called", async () => { const keys = memoryKeyStore(); let called = false; const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" }; const res = await handleRequest( messagesRequest({}, "/app-attest/register", { keyId: "key-2", attestation: "cbor", challenge: "never-issued", }), env, handlers({ attestKeyStore: () => keys, verifyAttestation: async () => { called = true; return { spkiBase64: "spki" }; }, }), ); assert.equal(res.status, 401); assert.equal(called, false); assert.equal(keys.map.size, 0); }); test("registration route: invalid attestation → 401, key not stored", async () => { const keys = memoryKeyStore(); const challenges = memoryChallengeStore(); const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" }; const challenge = await challenges.issue(); const res = await handleRequest( messagesRequest({}, "/app-attest/register", { keyId: "key-2", attestation: "forged", challenge, }), env, handlers({ attestKeyStore: () => keys, challengeStore: () => challenges, verifyAttestation: async () => { throw new Error("bad attestation"); }, }), ); assert.equal(res.status, 401); assert.equal(keys.map.size, 0); }); test("registration route: missing fields → 400, unconfigured → 404", async () => { const env: Env = { ...appAttestEnv, APP_ATTEST_ROOT_CA: "PEM_PLACEHOLDER" }; const missing = await handleRequest( messagesRequest({}, "/app-attest/register", { keyId: "key-2" }), env, handlers(), ); assert.equal(missing.status, 400); const unconfigured = await handleRequest( messagesRequest({}, "/app-attest/register", { keyId: "key-2", attestation: "cbor", challenge: "uuid", }), appAttestEnv, // no APP_ATTEST_ROOT_CA handlers(), ); assert.equal(unconfigured.status, 404); }); PK!B#(( worker.ts/** * Backend-for-Frontend (BFF) Cloudflare Worker, an in-app-purchase- and/or * App-Attest-gated Claude proxy with server-side revocation and spend * controls. * * Flow: * 1. Service routes first: Apple's server notifications land on * /apple/notifications (writing the revocation record), the app fetches * a one-time registration challenge from /app-attest/challenge, and * registers its App Attest key on /app-attest/register. Each exists only * when configured. * 2. For everything else, every configured gate must pass: StoreKit proves * the caller is entitled (and the revocation record proves Apple has not * taken it back), App Attest proves the caller is a genuine instance of * your app. If neither gate is configured the Worker refuses to run * (500). * 3. Spend controls run after the gates: model allowlist, max_tokens cap, * and a per-caller daily token budget debited in a Durable Object. * 4. On success it injects the Anthropic key server-side and forwards the * request to an allowlisted upstream path. The app ships no key. */ import { ASSERTION_HEADER, CHALLENGE_PATH, DEFAULT_UPSTREAM_PATHS, KEY_ID_HEADER, NOTIFICATIONS_PATH, REGISTER_PATH, TRANSACTION_HEADER, type Env, } from "./env.ts"; import { evaluateEntitlement, parseList, type GateResult, } from "./entitlement.ts"; import { verifyTransactionJWS, type TransactionVerifier, } from "./storekit.ts"; import { appAttestConfig, verifyAppAttestAssertion, type AppAttestVerifier, } from "./appattest.ts"; import { handleRegistration, verifyAttestation, type AttestationVerifier, } from "./attestation.ts"; import { attestKeyStoreFromEnv, type AttestKeyStore, } from "./attestkey.ts"; import { challengeStoreFromEnv, type ChallengeStore, } from "./challenge.ts"; import { handleNotification, verifyNotification, type NotificationVerifier, } from "./notifications.ts"; import { revocationStoreFromEnv, type RevocationStore, } from "./revocation.ts"; import { budgetStoreFromEnv, type BudgetStore } from "./budget.ts"; import { evaluateSpend, MAX_BODY_BYTES, spendConfig } from "./limits.ts"; import { forwardToAnthropic, resolveUpstreamPath } from "./proxy.ts"; import type { JWSTransaction } from "./transaction.ts"; // wrangler resolves the Durable Object classes from `main`. export { TokenBudget } from "./budget.ts"; export { AttestKey } from "./attestkey.ts"; export { AttestChallenge } from "./challenge.ts"; export { RevocationRecord } from "./revocation.ts"; /** * Verifiers and stores are injectable so tests exercise the flow without real * crypto or Durable Objects. */ export interface Handlers { verifyTransaction: TransactionVerifier; verifyAppAttest: AppAttestVerifier; verifyAttestation: AttestationVerifier; verifyNotification: NotificationVerifier; budgetStore: (env: Env) => BudgetStore | null; attestKeyStore: (env: Env) => AttestKeyStore | null; challengeStore: (env: Env) => ChallengeStore | null; revocationStore: (env: Env) => RevocationStore | null; } const defaultHandlers: Handlers = { verifyTransaction: verifyTransactionJWS, verifyAppAttest: verifyAppAttestAssertion, verifyAttestation, verifyNotification, budgetStore: budgetStoreFromEnv, attestKeyStore: attestKeyStoreFromEnv, challengeStore: challengeStoreFromEnv, revocationStore: revocationStoreFromEnv, }; function storeKitConfig(env: Env) { if (!env.ALLOWED_BUNDLE_IDS || !env.APPLE_ROOT_CA_SHA256) return null; return { allowedBundleIds: parseList(env.ALLOWED_BUNDLE_IDS), allowedProductIds: env.ALLOWED_PRODUCT_IDS ? parseList(env.ALLOWED_PRODUCT_IDS) : null, rootSha256: env.APPLE_ROOT_CA_SHA256, }; } export async function handleRequest( request: Request, env: Env, handlers: Handlers = defaultHandlers, ): Promise { if (request.method !== "POST") { return new Response("method not allowed", { status: 405 }); } const incoming = new URL(request.url); // Service routes come before the gates: Apple authenticates by signature, // and a registering app has no assertion yet. Each route exists only when // its configuration and stores do. if (incoming.pathname === NOTIFICATIONS_PATH) { const revocations = handlers.revocationStore(env); if (!env.APPLE_ROOT_CA_SHA256 || !revocations) { return json(404, { error: "not found" }); } return handleNotification( request, env.APPLE_ROOT_CA_SHA256, handlers.verifyNotification, revocations, ); } if (incoming.pathname === CHALLENGE_PATH) { const cfg = appAttestConfig(env); const challenges = handlers.challengeStore(env); if (!cfg || !challenges) { return json(404, { error: "not found" }); } return json(200, { challenge: await challenges.issue() }); } if (incoming.pathname === REGISTER_PATH) { const cfg = appAttestConfig(env); const keys = handlers.attestKeyStore(env); const challenges = handlers.challengeStore(env); if (!cfg || !env.APP_ATTEST_ROOT_CA || !keys || !challenges) { return json(404, { error: "not found" }); } return handleRegistration( request, { ...cfg, attestRootCa: env.APP_ATTEST_ROOT_CA }, handlers.verifyAttestation, keys, challenges, ); } const storeKit = storeKitConfig(env); const appAttest = appAttestConfig(env); // A Worker with no gate configured is an open relay for your API bill. Refuse // to run rather than forward anything. if (!storeKit && !appAttest) { return json(500, { error: "no entitlement gate configured" }); } // Partial configuration fails closed too: a gate or a budget that cannot // reach its store must refuse, not wave callers through. const keys = handlers.attestKeyStore(env); if (appAttest && !keys) { return json(500, { error: "App Attest key store not configured" }); } const spend = spendConfig(env); const budget = handlers.budgetStore(env); if (spend?.dailyTokenBudget != null && !budget) { return json(500, { error: "token budget store not configured" }); } const upstreamPath = resolveUpstreamPath( incoming.pathname, parseList(env.ALLOWED_UPSTREAM_PATHS ?? DEFAULT_UPSTREAM_PATHS), ); if (!upstreamPath) { return json(403, { error: "upstream path not allowed" }); } const jws = request.headers.get(TRANSACTION_HEADER); let txn: JWSTransaction | null = null; // StoreKit gate (if configured): verify the signed transaction, then decide. if (storeKit) { if (!jws) { return json(401, { error: "missing in-app purchase transaction" }); } try { txn = await handlers.verifyTransaction(jws, storeKit.rootSha256); } catch { return json(401, { error: "invalid in-app purchase transaction" }); } const gate: GateResult = evaluateEntitlement(txn, { allowedBundleIds: storeKit.allowedBundleIds, allowedProductIds: storeKit.allowedProductIds, now: Date.now(), }); if (!gate.ok) { return json(gate.status, { error: gate.reason }); } // Server-side revocation: the JWS in hand can predate a refund. The // record holds what Apple has told us since, via the notifications webhook. const revocations = handlers.revocationStore(env); if ( revocations && txn.originalTransactionId && (await revocations.isRevoked(txn.originalTransactionId)) ) { return json(403, { error: "entitlement_revoked" }); } } // App Attest gate (if configured): prove the caller is a genuine app instance. const keyId = request.headers.get(KEY_ID_HEADER); if (appAttest) { const assertion = request.headers.get(ASSERTION_HEADER); if (!assertion || !keyId) { return json(401, { error: "missing App Attest assertion" }); } try { // Bind the assertion to the request line and the IAP token, so it cannot // be lifted onto a different call. The body is left untouched for forwarding. const clientData = `${request.method}\n${upstreamPath}\n${jws ?? ""}`; const clientDataHash = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(clientData), ); await handlers.verifyAppAttest( { keyId, assertion, clientDataHash }, appAttest, keys!, ); } catch { return json(401, { error: "invalid App Attest assertion" }); } } // Spend controls, after every auth gate: an unauthorized caller never gets // its body parsed. The body is read once, inspected, then forwarded verbatim. if (spend) { const bodyText = await request.text(); // A cheap guard before JSON.parse; code units approximate bytes well // enough to stop a deliberately huge body. if (bodyText.length > MAX_BODY_BYTES) { return json(413, { error: "body_too_large" }); } let body: unknown; try { body = JSON.parse(bodyText); } catch { return json(400, { error: "body_not_json" }); } const verdict = evaluateSpend(body, spend); if (!verdict.ok) { return json(verdict.status, { error: verdict.reason }); } if (spend.dailyTokenBudget != null) { // The budget keys on a verified identity only: the purchase's // originalTransactionId, or the keyId an assertion just proved. A // client-chosen header is not an identity. const verifiedKeyId = appAttest ? keyId : null; const callerId = txn?.originalTransactionId ?? verifiedKeyId; if (!callerId) { return json(403, { error: "caller_identity_required" }); } const debit = await budget!.debit( callerId, verdict.maxTokens, spend.dailyTokenBudget, ); if (!debit.ok) { return json(429, { error: "daily_token_budget_exhausted", remaining: debit.remaining, }); } } return forwardToAnthropic(request, env, upstreamPath, bodyText); } // Every configured gate passed. Inject the secret and forward upstream. return forwardToAnthropic(request, env, upstreamPath); } function json(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }); } export default { fetch(request: Request, env: Env): Promise { return handleRequest(request, env); }, }; PK!8 wrangler.toml# Example Wrangler config for the gated Claude BFF Worker. name = "claude-bff-example" main = "worker.ts" compatibility_date = "2026-06-01" # Public, non-secret configuration: [vars] # --- StoreKit entitlement gate (active when both are set) --- # Comma-separated bundle IDs your app(s) ship under. ALLOWED_BUNDLE_IDS = "com.example.app" # Optional: restrict to specific product IDs. Empty = accept any product. ALLOWED_PRODUCT_IDS = "" # Hex SHA-256 of the DER-encoded "Apple Root CA - G3" certificate. # Obtain and verify from https://www.apple.com/certificateauthority/ APPLE_ROOT_CA_SHA256 = "PUT_THE_PINNED_FINGERPRINT_HERE" # --- App Attest authenticity gate (active when both are set) --- # Leave blank to run StoreKit only. Set both to require a genuine app instance. APP_ATTEST_TEAM_ID = "" APP_ATTEST_BUNDLE_ID = "" # The "Apple App Attestation Root CA" certificate (PEM or base64 DER). The # attestation chain omits its root, so the verifier needs the certificate # itself. Obtain and verify from https://www.apple.com/certificateauthority/ APP_ATTEST_ROOT_CA = "" # --- Spend controls (each optional; set to bound what entitled callers spend) --- # Comma-separated allowlist of model IDs the proxy will forward. ALLOWED_MODELS = "claude-opus-4-8" # Upper bound on max_tokens per request. MAX_TOKENS_LIMIT = "4096" # Tokens each caller may request per UTC day (max_tokens, debited upfront). DAILY_TOKEN_BUDGET = "200000" # --- Proxy configuration --- # Opt in to each upstream path the app may reach. Default is the Messages API. ALLOWED_UPSTREAM_PATHS = "/v1/messages" # Anthropic API version header. ANTHROPIC_VERSION = "2023-06-01" # --- Durable Object bindings --- # One TokenBudget instance per caller identity. [[durable_objects.bindings]] name = "TOKEN_BUDGET" class_name = "TokenBudget" # One AttestKey instance per App Attest keyId (public key + counter). [[durable_objects.bindings]] name = "ATTEST_KEYS" class_name = "AttestKey" # One AttestChallenge instance per issued registration challenge. [[durable_objects.bindings]] name = "ATTEST_CHALLENGES" class_name = "AttestChallenge" # One RevocationRecord instance per originalTransactionId. [[durable_objects.bindings]] name = "REVOCATIONS" class_name = "RevocationRecord" [[migrations]] tag = "v1" new_sqlite_classes = ["TokenBudget", "AttestKey", "AttestChallenge", "RevocationRecord"] # Secret, never commit this; set it out of band: # wrangler secret put ANTHROPIC_API_KEY PK!AppAttest.swiftPK!J9BFFError.swiftPK!D{ClaudeBFF.swiftPK!_E?yClaudeBFFClientTests.swiftPK!s6Entitlement.swiftPK!+y ProxyHeaders.swiftPK!6 &README.mdPK!, 6appattest.tsPK!bS]CC Kapplejws.tsPK!|Sjr&&a]attestation.tsPK!|uT T xattestkey.tsPK!R   1budget.tsPK!A}sbytes.tsPK! `challenge.tsPK!}}tentitlement.tsPK!Menv.tsPK! - - limits.tsPK!)R| Snotifications.tsPK!#gg npackage.jsonPK!~T  proxy.tsPK!dή *readme.jsonPK!ݣ z arevocation.tsPK!6yOO jstorekit.tsPK!04transaction.tsPK!30-~xx worker.test.tsPK!B#(( zworker.tsPK!8 ףwrangler.tomlPK0