Add a code review step which uses a short-lived provider token (#7932)

This commit is contained in:
Jack Amadeo
2026-03-24 08:57:40 -04:00
committed by GitHub
parent ce160b183d
commit 928f4ac46a
11 changed files with 1165 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.wrangler/
.dev.vars
+111
View File
@@ -0,0 +1,111 @@
# OIDC Proxy
A Cloudflare Worker that authenticates GitHub Actions OIDC tokens and proxies requests to an upstream API with an injected API key. This lets CI workflows call APIs without storing long-lived secrets in GitHub.
## How it works
```
GitHub Actions (OIDC token) → Worker (validate JWT, inject API key) → Upstream API
```
1. A GitHub Actions workflow mints an OIDC token with a configured audience
2. The workflow sends requests to this proxy, passing the OIDC token as the API key
3. The worker validates the JWT against GitHub's JWKS, checks issuer/audience/age/repo
4. If valid, the request is forwarded to the upstream API with the real API key injected
## Setup
```bash
cd oidc-proxy
npm install
```
## Configuration
Edit `wrangler.toml` for your upstream:
| Variable | Description |
|---|---|
| `OIDC_ISSUER` | `https://token.actions.githubusercontent.com` |
| `OIDC_AUDIENCE` | The audience your workflow requests (e.g. `goose-oidc-proxy`) |
| `MAX_TOKEN_AGE_SECONDS` | Max age of OIDC token in seconds (default: `1200` = 20 min) |
| `MAX_REQUESTS_PER_TOKEN` | Max requests per OIDC token (default: `200`) |
| `RATE_LIMIT_PER_SECOND` | Max requests per second per token (default: `2`) |
| `ALLOWED_REPOS` | *(optional)* Comma-separated `owner/repo` list |
| `ALLOWED_REFS` | *(optional)* Comma-separated allowed refs |
| `UPSTREAM_URL` | The upstream API base URL |
| `UPSTREAM_AUTH_HEADER` | Header name for the API key (e.g. `x-api-key`, `Authorization`) |
| `UPSTREAM_AUTH_PREFIX` | *(optional)* Prefix before the key (e.g. `Bearer `) — omit for raw value |
| `CORS_ORIGIN` | *(optional)* Allowed CORS origin |
| `CORS_EXTRA_HEADERS` | *(optional)* Additional CORS allowed headers |
Set your upstream API key as a secret:
```bash
npx wrangler secret put UPSTREAM_API_KEY
```
### Example: Anthropic
```toml
UPSTREAM_URL = "https://api.anthropic.com"
UPSTREAM_AUTH_HEADER = "x-api-key"
CORS_EXTRA_HEADERS = "anthropic-version"
```
### Example: OpenAI-compatible
```toml
UPSTREAM_URL = "https://api.openai.com"
UPSTREAM_AUTH_HEADER = "Authorization"
UPSTREAM_AUTH_PREFIX = "Bearer "
```
## Usage in GitHub Actions
```yaml
permissions:
id-token: write
steps:
- name: Get OIDC token
id: oidc
uses: actions/github-script@v7
with:
script: |
const token = await core.getIDToken('goose-oidc-proxy');
core.setOutput('token', token);
core.setSecret(token);
- name: Call API through proxy
env:
ANTHROPIC_BASE_URL: https://oidc-proxy.your-subdomain.workers.dev
ANTHROPIC_API_KEY: ${{ steps.oidc.outputs.token }}
run: goose run --recipe my-recipe.yaml
```
## Testing
```bash
npm test
```
## Deploy
```bash
npx wrangler secret put UPSTREAM_API_KEY
npm run deploy
```
## Token budget and rate limiting
Each OIDC token is tracked by its `jti` (JWT ID) claim using a Durable Object. This provides:
- **Budget**: Each token is limited to `MAX_REQUESTS_PER_TOKEN` total requests (default: 200). Once exhausted, the proxy returns `429` with `{"error": "Token budget exhausted"}`.
- **Rate limit**: Each token is limited to `RATE_LIMIT_PER_SECOND` requests per second (default: 2). When exceeded, the proxy returns `429` with `{"error": "Rate limit exceeded"}` and a `Retry-After: 1` header.
Both limits are enforced atomically — the Durable Object processes one request at a time per token, so there are no race conditions.
## Token age vs expiry
GitHub OIDC tokens expire after ~5 minutes. For longer-running jobs, set `MAX_TOKEN_AGE_SECONDS` to allow recently-expired tokens. When set, the proxy checks the token's `iat` (issued-at) claim instead of `exp`.
+27
View File
@@ -0,0 +1,27 @@
{
"name": "anthropic-oidc-proxy",
"engine": "anthropic",
"display_name": "Anthropic (OIDC Proxy)",
"description": "Anthropic API via OIDC proxy — uses a short-lived OIDC token instead of a long-lived API key",
"api_key_env": "ANTHROPIC_OIDC_PROXY_API_KEY",
"base_url": "${OIDC_PROXY_URL}",
"models": [
{
"name": "claude-opus-4-6",
"context_limit": 200000
},
{
"name": "claude-sonnet-4-6",
"context_limit": 200000
}
],
"supports_streaming": true,
"env_vars": [
{
"name": "OIDC_PROXY_URL",
"required": true,
"secret": false,
"description": "URL of the OIDC proxy worker (e.g. https://oidc-proxy.your-subdomain.workers.dev)"
}
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "oidc-proxy",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run"
},
"devDependencies": {
"wrangler": "^4.0.0",
"vitest": "^3.0.0",
"@cloudflare/vitest-pool-workers": "^0.8.0"
}
}
+323
View File
@@ -0,0 +1,323 @@
export class TokenBucket {
constructor(state) {
this.state = state;
this.count = 0;
this.timestamps = [];
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
const stored = await this.state.storage.get("count");
if (stored !== undefined) this.count = stored;
this.initialized = true;
}
async fetch(request) {
await this.initialize();
const url = new URL(request.url);
if (url.pathname === "/check") {
const { maxRequests, ratePerSecond } = await request.json();
if (this.count >= maxRequests) {
return Response.json({ allowed: false, error: "budget_exhausted" });
}
const now = Date.now();
this.timestamps = this.timestamps.filter((t) => now - t < 1000);
if (this.timestamps.length >= ratePerSecond) {
return Response.json(
{ allowed: false, error: "rate_limited" },
{ headers: { "Retry-After": "1" } },
);
}
this.count++;
this.timestamps.push(now);
await this.state.storage.put("count", this.count);
return Response.json({
allowed: true,
remaining: maxRequests - this.count,
});
}
return Response.json({ error: "not found" }, { status: 404 });
}
}
export default {
async fetch(request, env) {
if (request.method === "OPTIONS") {
return handleCors(env);
}
const token =
request.headers.get("x-api-key") ||
request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return jsonResponse(401, { error: "Missing authentication" });
}
const result = await verifyOidcToken(token, env);
if (!result.valid) {
return jsonResponse(401, { error: result.reason });
}
// Check rate limit and budget via Durable Object
const bucketCheck = await checkTokenBucket(result.jti, env);
if (!bucketCheck.allowed) {
if (bucketCheck.error === "rate_limited") {
return jsonResponse(
429,
{ error: "Rate limit exceeded" },
{ "Retry-After": "1" },
);
}
return jsonResponse(429, { error: "Token budget exhausted" });
}
const url = new URL(request.url);
const upstreamUrl = `${env.UPSTREAM_URL}${url.pathname}${url.search}`;
const headers = new Headers(request.headers);
headers.delete("Authorization");
headers.delete("x-api-key");
const authHeader = env.UPSTREAM_AUTH_HEADER || "Authorization";
const authPrefix = env.UPSTREAM_AUTH_PREFIX;
headers.set(
authHeader,
authPrefix
? `${authPrefix}${env.UPSTREAM_API_KEY}`
: env.UPSTREAM_API_KEY,
);
headers.set("Host", new URL(env.UPSTREAM_URL).host);
const response = await fetch(upstreamUrl, {
method: request.method,
headers,
body: request.body,
});
const respHeaders = new Headers(response.headers);
// Workers' fetch auto-decompresses but keeps the Content-Encoding header,
// which would cause clients to try decompressing already-decompressed data.
respHeaders.delete("Content-Encoding");
respHeaders.delete("Content-Length");
respHeaders.set("Access-Control-Allow-Origin", env.CORS_ORIGIN || "*");
return new Response(response.body, {
status: response.status,
headers: respHeaders,
});
},
};
// --- Token bucket (Durable Object) ---
async function checkTokenBucket(jti, env) {
const maxRequests = parseInt(env.MAX_REQUESTS_PER_TOKEN || "200", 10);
const ratePerSecond = parseInt(env.RATE_LIMIT_PER_SECOND || "2", 10);
const id = env.TOKEN_BUCKET.idFromName(jti);
const stub = env.TOKEN_BUCKET.get(id);
const resp = await stub.fetch("https://bucket/check", {
method: "POST",
body: JSON.stringify({ maxRequests, ratePerSecond }),
});
return resp.json();
}
// --- OIDC JWT verification using Web Crypto API ---
let jwksCache = null;
let jwksCacheTime = 0;
const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
async function fetchJwks(issuer) {
const now = Date.now();
if (jwksCache && now - jwksCacheTime < JWKS_CACHE_TTL_MS) {
return jwksCache;
}
const wellKnownUrl = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
const configResp = await fetch(wellKnownUrl);
if (!configResp.ok) {
throw new Error(`Failed to fetch OIDC config: ${configResp.status}`);
}
const config = await configResp.json();
const jwksResp = await fetch(config.jwks_uri);
if (!jwksResp.ok) {
throw new Error(`Failed to fetch JWKS: ${jwksResp.status}`);
}
jwksCache = await jwksResp.json();
jwksCacheTime = now;
return jwksCache;
}
function base64UrlDecode(str) {
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(padded);
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}
function decodeJwtPart(b64url) {
return JSON.parse(new TextDecoder().decode(base64UrlDecode(b64url)));
}
const ALG_MAP = {
RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
RS512: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
ES256: { name: "ECDSA", namedCurve: "P-256", hash: "SHA-256" },
ES384: { name: "ECDSA", namedCurve: "P-384", hash: "SHA-384" },
};
async function verifyOidcToken(token, env) {
try {
const parts = token.split(".");
if (parts.length !== 3) {
return { valid: false, reason: "Malformed JWT" };
}
const [headerB64, payloadB64, sigB64] = parts;
const header = decodeJwtPart(headerB64);
const payload = decodeJwtPart(payloadB64);
if (env.MAX_TOKEN_AGE_SECONDS && payload.iat) {
const age = Date.now() / 1000 - payload.iat;
if (age > parseInt(env.MAX_TOKEN_AGE_SECONDS, 10)) {
return { valid: false, reason: "Token too old" };
}
} else if (!payload.exp || payload.exp < Date.now() / 1000) {
return { valid: false, reason: "Token expired" };
}
const expectedIssuer = env.OIDC_ISSUER.replace(/\/$/, "");
const actualIssuer = (payload.iss || "").replace(/\/$/, "");
if (actualIssuer !== expectedIssuer) {
return { valid: false, reason: "Invalid issuer" };
}
if (env.OIDC_AUDIENCE) {
const audiences = Array.isArray(payload.aud)
? payload.aud
: [payload.aud];
if (!audiences.includes(env.OIDC_AUDIENCE)) {
return { valid: false, reason: "Invalid audience" };
}
}
if (env.ALLOWED_REPOS) {
const allowed = env.ALLOWED_REPOS.split(",").map((r) => r.trim());
if (!allowed.includes(payload.repository)) {
return {
valid: false,
reason: `Repository '${payload.repository}' not allowed`,
};
}
}
if (env.ALLOWED_REFS) {
const allowed = env.ALLOWED_REFS.split(",").map((r) => r.trim());
if (!allowed.includes(payload.ref)) {
return { valid: false, reason: `Ref '${payload.ref}' not allowed` };
}
}
const jwks = await fetchJwks(env.OIDC_ISSUER);
let jwk = jwks.keys.find((k) => k.kid === header.kid);
if (!jwk) {
jwksCache = null;
const refreshed = await fetchJwks(env.OIDC_ISSUER);
jwk = refreshed.keys.find((k) => k.kid === header.kid);
if (!jwk) {
return { valid: false, reason: "No matching key in JWKS" };
}
}
const sigResult = await verifySignature(
header,
jwk,
headerB64,
payloadB64,
sigB64,
);
if (!sigResult.valid) return sigResult;
const jti = payload.jti || `${payload.iss}:${payload.iat}:${payload.sub}`;
return { valid: true, jti };
} catch (err) {
return { valid: false, reason: `Verification error: ${err.message}` };
}
}
async function verifySignature(header, jwk, headerB64, payloadB64, sigB64) {
const alg = ALG_MAP[header.alg];
if (!alg) {
return { valid: false, reason: `Unsupported algorithm: ${header.alg}` };
}
const keyAlgorithm = alg.namedCurve
? { name: alg.name, namedCurve: alg.namedCurve }
: { name: alg.name, hash: alg.hash };
const cryptoKey = await crypto.subtle.importKey(
"jwk",
jwk,
keyAlgorithm,
false,
["verify"],
);
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
const signature = base64UrlDecode(sigB64);
const verifyAlgorithm =
alg.name === "ECDSA" ? { name: alg.name, hash: alg.hash } : alg.name;
const valid = await crypto.subtle.verify(
verifyAlgorithm,
cryptoKey,
signature,
data,
);
if (!valid) {
return { valid: false, reason: "Invalid signature" };
}
return { valid: true };
}
// --- Helpers ---
function jsonResponse(status, body, extraHeaders = {}) {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json", ...extraHeaders },
});
}
function handleCors(env) {
const extraHeaders = env.CORS_EXTRA_HEADERS || "";
const baseHeaders = "Authorization, Content-Type, x-api-key";
const allowHeaders = extraHeaders
? `${baseHeaders}, ${extraHeaders}`
: baseHeaders;
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": env.CORS_ORIGIN || "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": allowHeaders,
"Access-Control-Max-Age": "86400",
},
});
}
+306
View File
@@ -0,0 +1,306 @@
import {
env,
createExecutionContext,
waitOnExecutionContext,
fetchMock,
} from "cloudflare:test";
import { describe, it, expect, beforeAll, afterEach } from "vitest";
import worker from "../src/index.js";
let testKeyPair;
let testJwk;
const TEST_KID = "test-kid-001";
beforeAll(async () => {
testKeyPair = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["sign", "verify"],
);
const exported = await crypto.subtle.exportKey("jwk", testKeyPair.publicKey);
testJwk = { ...exported, kid: TEST_KID, alg: "RS256", use: "sig" };
});
afterEach(() => {
fetchMock.deactivate();
});
function base64UrlEncode(data) {
const str = typeof data === "string" ? data : JSON.stringify(data);
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
async function createSignedJwt(payload, kid = TEST_KID) {
const header = { alg: "RS256", typ: "JWT", kid };
const headerB64 = base64UrlEncode(header);
const payloadB64 = base64UrlEncode(payload);
const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
const signature = await crypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
testKeyPair.privateKey,
data,
);
const sigB64 = btoa(String.fromCharCode(...new Uint8Array(signature)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
return `${headerB64}.${payloadB64}.${sigB64}`;
}
let jtiCounter = 0;
function validPayload(overrides = {}) {
const now = Math.floor(Date.now() / 1000);
return {
iss: "https://token.actions.githubusercontent.com",
aud: "goose-oidc-proxy",
iat: now - 10,
exp: now + 300,
jti: `test-jti-${++jtiCounter}`,
repository: "block/goose",
ref: "refs/heads/main",
sub: "repo:block/goose:ref:refs/heads/main",
...overrides,
};
}
function mockAll(upstreamStatus = 200, upstreamBody = { ok: true }) {
fetchMock.activate();
fetchMock.disableNetConnect();
const oidc = fetchMock.get("https://token.actions.githubusercontent.com");
oidc
.intercept({ path: "/.well-known/openid-configuration", method: "GET" })
.reply(
200,
JSON.stringify({
jwks_uri:
"https://token.actions.githubusercontent.com/.well-known/jwks",
}),
)
.persist();
oidc
.intercept({ path: "/.well-known/jwks", method: "GET" })
.reply(200, JSON.stringify({ keys: [testJwk] }))
.persist();
const upstream = fetchMock.get("https://api.anthropic.com");
upstream
.intercept({ path: /.*/, method: "POST" })
.reply(upstreamStatus, JSON.stringify(upstreamBody));
}
function mockAllPersistent(upstreamStatus = 200, upstreamBody = { ok: true }) {
fetchMock.activate();
fetchMock.disableNetConnect();
const oidc = fetchMock.get("https://token.actions.githubusercontent.com");
oidc
.intercept({ path: "/.well-known/openid-configuration", method: "GET" })
.reply(
200,
JSON.stringify({
jwks_uri:
"https://token.actions.githubusercontent.com/.well-known/jwks",
}),
)
.persist();
oidc
.intercept({ path: "/.well-known/jwks", method: "GET" })
.reply(200, JSON.stringify({ keys: [testJwk] }))
.persist();
const upstream = fetchMock.get("https://api.anthropic.com");
upstream
.intercept({ path: /.*/, method: "POST" })
.reply(upstreamStatus, JSON.stringify(upstreamBody))
.persist();
}
// Mock TokenBucket Durable Object for unit tests
function mockTokenBucket(overrides = {}) {
const defaults = { allowed: true, remaining: 199 };
const response = { ...defaults, ...overrides };
return {
idFromName: () => "mock-id",
get: () => ({
fetch: async () => Response.json(response),
}),
};
}
function testEnv(overrides = {}) {
return {
OIDC_ISSUER: "https://token.actions.githubusercontent.com",
OIDC_AUDIENCE: "goose-oidc-proxy",
UPSTREAM_URL: "https://api.anthropic.com",
UPSTREAM_AUTH_HEADER: "x-api-key",
UPSTREAM_API_KEY: "sk-ant-real-key",
ALLOWED_REPOS: "block/goose",
MAX_TOKEN_AGE_SECONDS: "1200",
MAX_REQUESTS_PER_TOKEN: "200",
RATE_LIMIT_PER_SECOND: "2",
TOKEN_BUCKET: mockTokenBucket(),
...overrides,
};
}
describe("rejects invalid requests", () => {
it("missing auth", async () => {
const request = new Request("https://proxy.example.com/v1/messages");
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Missing authentication");
});
it("malformed token", async () => {
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": "not-a-jwt" },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Malformed JWT");
});
it("wrong claims (repo, audience, issuer)", async () => {
for (const [override, expectedError] of [
[{ repository: "evil/repo" }, "not allowed"],
[{ aud: "wrong" }, "Invalid audience"],
[{ iss: "https://evil.example.com" }, "Invalid issuer"],
]) {
const token = await createSignedJwt(validPayload(override));
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": token },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toContain(expectedError);
}
});
it("token too old", async () => {
const token = await createSignedJwt(
validPayload({ iat: Math.floor(Date.now() / 1000) - 1500 }),
);
const request = new Request("https://proxy.example.com/v1/messages", {
headers: { "x-api-key": token },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
expect((await response.json()).error).toBe("Token too old");
});
});
describe("proxies valid requests", () => {
it("forwards to upstream with injected API key", async () => {
const token = await createSignedJwt(validPayload());
mockAll(200, { id: "msg_123", type: "message" });
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": token,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "claude-sonnet-4-20250514", messages: [] }),
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
expect((await response.json()).id).toBe("msg_123");
});
it("accepts recently-expired token within MAX_TOKEN_AGE_SECONDS", async () => {
const now = Math.floor(Date.now() / 1000);
const token = await createSignedJwt(
validPayload({ iat: now - 600, exp: now - 300 }),
);
mockAll(200, { ok: true });
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, testEnv(), ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
});
});
describe("token budget and rate limiting", () => {
it("rejects when budget exhausted", async () => {
const token = await createSignedJwt(validPayload());
mockAll();
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(
request,
testEnv({
TOKEN_BUCKET: mockTokenBucket({
allowed: false,
error: "budget_exhausted",
}),
}),
ctx,
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(429);
expect((await response.json()).error).toBe("Token budget exhausted");
expect(response.headers.get("Retry-After")).toBeNull();
});
it("rejects with Retry-After when rate limited", async () => {
const token = await createSignedJwt(validPayload());
mockAll();
const request = new Request("https://proxy.example.com/v1/messages", {
method: "POST",
headers: { "x-api-key": token, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const ctx = createExecutionContext();
const response = await worker.fetch(
request,
testEnv({
TOKEN_BUCKET: mockTokenBucket({
allowed: false,
error: "rate_limited",
}),
}),
ctx,
);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(429);
expect((await response.json()).error).toBe("Rate limit exceeded");
expect(response.headers.get("Retry-After")).toBe("1");
});
});
+11
View File
@@ -0,0 +1,11 @@
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.toml" },
},
},
},
});
+30
View File
@@ -0,0 +1,30 @@
name = "oidc-proxy"
main = "src/index.js"
compatibility_date = "2026-03-01"
[durable_objects]
bindings = [{ name = "TOKEN_BUCKET", class_name = "TokenBucket" }]
[[migrations]]
tag = "v1"
new_classes = ["TokenBucket"]
[vars]
OIDC_ISSUER = "https://token.actions.githubusercontent.com"
OIDC_AUDIENCE = "goose-oidc-proxy"
MAX_TOKEN_AGE_SECONDS = "1200" # 20 minutes
MAX_REQUESTS_PER_TOKEN = "200"
RATE_LIMIT_PER_SECOND = "2"
ALLOWED_REPOS = "block/goose,aaif/goose"
# Upstream configuration
UPSTREAM_URL = "https://api.anthropic.com"
UPSTREAM_AUTH_HEADER = "x-api-key"
# UPSTREAM_AUTH_PREFIX is unset — Anthropic expects a raw key, not "Bearer <key>"
# Additional CORS headers for Anthropic SDK
CORS_EXTRA_HEADERS = "anthropic-version"
# Set the upstream API key as a secret:
# npx wrangler secret put UPSTREAM_API_KEY