{"openapi":"3.0.0","info":{"version":"2.0.0","title":"Fine-Grained Proxy (FGP) API","description":"Stateless HTTP proxy that adds fine-grained token scoping on top of any API. Zero storage: the token and permission config are encrypted in the URL itself."},"components":{"schemas":{"SaltResponse":{"type":"object","properties":{"salt":{"type":"string"}},"required":["salt"]},"DecodeResponse":{"type":"object","properties":{"target":{"type":"string"},"auth":{"type":"string"},"scopes":{"type":"array","items":{"nullable":true}},"ttl":{"type":"number"},"createdAt":{"type":"number"},"version":{"type":"number"},"tokenRedacted":{"type":"string","example":"tk-us-****xxxx","description":"Token with only last 4 chars visible"}},"required":["target","auth","scopes","ttl","createdAt","version","tokenRedacted"]},"DecodeError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"message":{"type":"string"}},"required":["error","message"]},"DecodeError401":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_credentials"]},"message":{"type":"string"}},"required":["error","message"]},"DecodeError500":{"type":"object","properties":{"error":{"type":"string","enum":["server_error"]},"message":{"type":"string"}},"required":["error","message"]},"DecodeBody":{"type":"object","properties":{"blob":{"type":"string","minLength":1,"example":"eyJhbGci..."},"key":{"type":"string","minLength":1,"example":"a7f2c9d4-1234-5678-abcd-ef0123456789"}},"required":["blob","key"]},"ShareEncodeResponse":{"type":"object","properties":{"encoded":{"type":"string"},"url":{"type":"string"}},"required":["encoded","url"]},"ShareEncodeError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"message":{"type":"string"}},"required":["error","message"]},"ShareEncodeBody":{"type":"object","properties":{"target":{"type":"string","minLength":1,"example":"https://api.osc-fr1.scalingo.com"},"auth":{"type":"string","minLength":1,"example":"scalingo-exchange"},"scopes":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"methods":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"pattern":{"type":"string"},"bodyFilters":{"type":"array","items":{"type":"object","properties":{"objectPath":{"type":"string","minLength":1},"objectValue":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["any"]},"value":{"nullable":true}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["wildcard"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["stringwildcard"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["regex"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["and"]},"value":{"type":"array","items":{"nullable":true}}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["not"]},"value":{"nullable":true}},"required":["type"]}]},"minItems":1}},"required":["objectPath","objectValue"]}}},"required":["methods","pattern"]}]},"minItems":1,"example":["GET:/v1/apps/*"]},"ttl":{"type":"number","example":3600},"test":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"string"}},"required":["method","path"]}},"required":["target","auth","scopes","ttl"]},"ShareDecodeResponse":{"type":"object","properties":{"target":{"type":"string"},"auth":{"type":"string"},"scopes":{"type":"array","items":{"nullable":true}},"ttl":{"type":"number"},"test":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"string"}},"required":["method","path"]}},"required":["target","auth","scopes","ttl"]},"ShareDecodeError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body","invalid_encoded"]},"message":{"type":"string"}},"required":["error","message"]},"ShareDecodeBody":{"type":"object","properties":{"encoded":{"type":"string","minLength":1}},"required":["encoded"]},"GenerateResponse":{"type":"object","properties":{"url":{"type":"string","example":"https://fgp.example.com/eyJhbGci.../"},"key":{"type":"string","example":"a7f2c9d4-1234-5678-abcd-ef0123456789"},"blob":{"type":"string","example":"eyJhbGci...","description":"Raw encrypted blob, for use with X-FGP-Blob header mode"}},"required":["url","key","blob"]},"GenerateError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body","blob_too_large","scope_limit_exceeded"]},"message":{"type":"string"}},"required":["error","message"]},"GenerateError500":{"type":"object","properties":{"error":{"type":"string","enum":["server_error"]},"message":{"type":"string"}},"required":["error","message"]},"GenerateBody":{"type":"object","properties":{"token":{"type":"string","minLength":1,"example":"tk-us-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"target":{"type":"string","minLength":1,"example":"https://api.osc-fr1.scalingo.com"},"auth":{"type":"string","minLength":1,"example":"scalingo-exchange","description":"Auth mode: bearer, basic, scalingo-exchange, or header:{name}"},"scopes":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"methods":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"pattern":{"type":"string"},"bodyFilters":{"type":"array","items":{"type":"object","properties":{"objectPath":{"type":"string","minLength":1},"objectValue":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["any"]},"value":{"nullable":true}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["wildcard"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["stringwildcard"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["regex"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["and"]},"value":{"type":"array","items":{"nullable":true}}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["not"]},"value":{"nullable":true}},"required":["type"]}]},"minItems":1}},"required":["objectPath","objectValue"]}}},"required":["methods","pattern"]}]},"example":["GET:/v1/apps/*","POST:/v1/apps/my-app/scale"],"description":"List of scopes: string patterns or structured ScopeEntry objects"},"ttl":{"type":"number","example":3600,"description":"Validity duration in seconds. 0 = no expiration"},"name":{"type":"string","example":"Production Scalingo","description":"Human-readable configuration name stored in the blob (optional)"},"logs":{"$ref":"#/components/schemas/LogsConfig"}},"required":["token","target","auth","scopes","ttl"]},"LogsConfig":{"type":"object","properties":{"enabled":{"type":"boolean"},"detailed":{"type":"boolean"}},"required":["enabled","detailed"],"description":"Enable in-memory logs capture for this blob (optional)"},"ListAppsResponse":{"type":"object","properties":{"apps":{"type":"array","items":{"type":"string"},"example":["my-app","other-app","staging-app"]}},"required":["apps"]},"ListAppsError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"message":{"type":"string"}},"required":["error","message"]},"ListAppsError401":{"type":"object","properties":{"error":{"type":"string","enum":["token_exchange_failed"]},"message":{"type":"string"}},"required":["error","message"]},"ListAppsError502":{"type":"object","properties":{"error":{"type":"string","enum":["upstream_unreachable","upstream_list_apps_failed"]},"message":{"type":"string"}},"required":["error","message"]},"ListAppsBody":{"type":"object","properties":{"token":{"type":"string","minLength":1,"example":"tk-us-xxxxxxxxxxxxxxxxxxxxxxxxxxxx","description":"Scalingo API token (tk-us-...)"},"target":{"type":"string","example":"https://api.osc-fr1.scalingo.com","description":"Scalingo API URL (defaults to osc-fr1 if omitted)"}},"required":["token"]},"TestScopeResponse":{"type":"object","properties":{"allowed":{"type":"boolean"},"results":{"type":"array","items":{"$ref":"#/components/schemas/TestScopeResult"}}},"required":["allowed","results"]},"TestScopeResult":{"type":"object","properties":{"index":{"type":"number"},"matched":{"type":"boolean"},"methodMatch":{"type":"boolean"},"pathMatch":{"type":"boolean"},"bodyMatch":{"type":"boolean","nullable":true}},"required":["index","matched","methodMatch","pathMatch","bodyMatch"]},"TestScopeError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"message":{"type":"string"}},"required":["error","message"]},"TestScopeBody":{"type":"object","properties":{"method":{"type":"string","minLength":1,"example":"GET"},"path":{"type":"string","minLength":1,"example":"/v1/apps/my-app"},"scopes":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"methods":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"pattern":{"type":"string"},"bodyFilters":{"type":"array","items":{"type":"object","properties":{"objectPath":{"type":"string","minLength":1},"objectValue":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["any"]},"value":{"nullable":true}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["wildcard"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["stringwildcard"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["regex"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["and"]},"value":{"type":"array","items":{"nullable":true}}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["not"]},"value":{"nullable":true}},"required":["type"]}]},"minItems":1}},"required":["objectPath","objectValue"]}}},"required":["methods","pattern"]}]},"minItems":1,"example":["GET:/v1/apps/*","POST:/v1/apps/my-app/scale"]},"body":{"nullable":true,"description":"Optional JSON body for body filter testing"}},"required":["method","path","scopes"]},"TestProxyResponse":{"type":"object","properties":{"allowed":{"type":"boolean"},"reason":{"type":"string"},"upstream":{"$ref":"#/components/schemas/UpstreamResponse"}},"required":["allowed"]},"UpstreamResponse":{"type":"object","properties":{"status":{"type":"number"},"body":{"nullable":true}},"required":["status"]},"TestProxyError400":{"type":"object","properties":{"error":{"type":"string","enum":["invalid_body"]},"message":{"type":"string"}},"required":["error","message"]},"TestProxyBody":{"type":"object","properties":{"method":{"type":"string","minLength":1,"example":"GET"},"path":{"type":"string","minLength":1,"example":"/v1/apps/my-app"},"token":{"type":"string","minLength":1,"example":"tk-us-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"target":{"type":"string","minLength":1,"example":"https://api.osc-fr1.scalingo.com"},"auth":{"type":"string","minLength":1,"example":"scalingo-exchange"},"scopes":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"methods":{"type":"array","items":{"type":"string","minLength":1},"minItems":1},"pattern":{"type":"string"},"bodyFilters":{"type":"array","items":{"type":"object","properties":{"objectPath":{"type":"string","minLength":1},"objectValue":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["any"]},"value":{"nullable":true}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["wildcard"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["stringwildcard"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["regex"]},"value":{"type":"string"}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["and"]},"value":{"type":"array","items":{"nullable":true}}},"required":["type","value"]},{"type":"object","properties":{"type":{"type":"string","enum":["not"]},"value":{"nullable":true}},"required":["type"]}]},"minItems":1}},"required":["objectPath","objectValue"]}}},"required":["methods","pattern"]}]},"minItems":1},"body":{"nullable":true}},"required":["method","path","token","target","auth","scopes"]}},"parameters":{}},"paths":{"/api/salt":{"get":{"tags":["Configuration"],"summary":"Get server salt","description":"Returns the server salt used for PBKDF2 key derivation.","responses":{"200":{"description":"Server salt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaltResponse"}}}}}}},"/api/decode":{"post":{"tags":["Configuration"],"summary":"Decode an FGP blob","description":"Decrypts a blob with the provided client key and returns the config with redacted token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecodeBody"}}}},"responses":{"200":{"description":"Decoded config with redacted token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecodeResponse"}}}},"400":{"description":"Invalid body (missing or malformed fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecodeError400"}}}},"401":{"description":"Unable to decrypt blob (wrong key or corrupted blob)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecodeError401"}}}},"500":{"description":"Server misconfigured (FGP_SALT missing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecodeError500"}}}}}}},"/api/share/encode":{"post":{"tags":["Configuration"],"summary":"Encode a public config URL","description":"Compresses a config (without token) into a gzip+base64url string for sharing via ?c= parameter.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEncodeBody"}}}},"responses":{"200":{"description":"Encoded config and full URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEncodeResponse"}}}},"400":{"description":"Invalid body (missing or malformed fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEncodeError400"}}}}}}},"/api/share/decode":{"post":{"tags":["Configuration"],"summary":"Decode a public config URL","description":"Decompresses a gzip+base64url encoded config string back to its components.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDecodeBody"}}}},"responses":{"200":{"description":"Decoded public config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDecodeResponse"}}}},"400":{"description":"Invalid body or unable to decode the shared config string","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareDecodeError400"}}}}}}},"/api/generate":{"post":{"tags":["Configuration"],"summary":"Generate an FGP URL","description":"Server-side encrypted URL generation. Creates a client key, encrypts the blob, returns URL + key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateBody"}}}},"responses":{"200":{"description":"Generated URL and client key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateResponse"}}}},"400":{"description":"Invalid body, generated blob exceeds 4KB, or scope limits violated (body filters, depth, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateError400"}}}},"500":{"description":"Server misconfigured (FGP_SALT missing)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateError500"}}}}}}},"/api/list-apps":{"post":{"tags":["Scalingo"],"summary":"List Scalingo apps","description":"Scalingo helper: lists apps accessible with the provided token via token exchange.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAppsBody"}}}},"responses":{"200":{"description":"Sorted list of app names","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAppsResponse"}}}},"400":{"description":"Invalid body (missing or malformed fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAppsError400"}}}},"401":{"description":"Scalingo token exchange failed (token invalid or unauthorized)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAppsError401"}}}},"502":{"description":"Scalingo API unreachable (fetch throw) or returned a non-ok status when listing apps","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAppsError502"}}}}}}},"/api/test-scope":{"post":{"tags":["Configuration"],"summary":"Test scope matching","description":"Tests whether a method + path + optional body would be allowed by the given scopes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestScopeBody"}}}},"responses":{"200":{"description":"Test results per scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestScopeResponse"}}}},"400":{"description":"Invalid body (missing or malformed fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestScopeError400"}}}}}}},"/api/test-proxy":{"post":{"tags":["Configuration"],"summary":"Test proxy end-to-end","description":"Checks scopes, authenticates, and forwards a real request to the target API. Returns the upstream response.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestProxyBody"}}}},"responses":{"200":{"description":"Proxy test result with upstream response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestProxyResponse"}}}},"400":{"description":"Invalid body (missing or malformed fields)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestProxyError400"}}}}}}}}}