Spaces:
Paused
Paused
| import { createHmac, timingSafeEqual } from "node:crypto"; | |
| interface JwtHeader { | |
| alg: string; | |
| typ?: string; | |
| } | |
| export interface LocalAgentJwtClaims { | |
| sub: string; | |
| company_id: string; | |
| adapter_type: string; | |
| run_id: string; | |
| iat: number; | |
| exp: number; | |
| iss?: string; | |
| aud?: string; | |
| jti?: string; | |
| } | |
| const JWT_ALGORITHM = "HS256"; | |
| function parseNumber(value: string | undefined, fallback: number) { | |
| const parsed = Number(value); | |
| if (!Number.isFinite(parsed) || parsed <= 0) return fallback; | |
| return Math.floor(parsed); | |
| } | |
| function jwtConfig() { | |
| const secret = process.env.PAPERCLIP_AGENT_JWT_SECRET; | |
| if (!secret) return null; | |
| return { | |
| secret, | |
| ttlSeconds: parseNumber(process.env.PAPERCLIP_AGENT_JWT_TTL_SECONDS, 60 * 60 * 48), | |
| issuer: process.env.PAPERCLIP_AGENT_JWT_ISSUER ?? "paperclip", | |
| audience: process.env.PAPERCLIP_AGENT_JWT_AUDIENCE ?? "paperclip-api", | |
| }; | |
| } | |
| function base64UrlEncode(value: string) { | |
| return Buffer.from(value, "utf8").toString("base64url"); | |
| } | |
| function base64UrlDecode(value: string) { | |
| return Buffer.from(value, "base64url").toString("utf8"); | |
| } | |
| function signPayload(secret: string, signingInput: string) { | |
| return createHmac("sha256", secret).update(signingInput).digest("base64url"); | |
| } | |
| function parseJson(value: string): Record<string, unknown> | null { | |
| try { | |
| const parsed = JSON.parse(value); | |
| return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function safeCompare(a: string, b: string) { | |
| const left = Buffer.from(a); | |
| const right = Buffer.from(b); | |
| if (left.length !== right.length) return false; | |
| return timingSafeEqual(left, right); | |
| } | |
| export function createLocalAgentJwt(agentId: string, companyId: string, adapterType: string, runId: string) { | |
| const config = jwtConfig(); | |
| if (!config) return null; | |
| const now = Math.floor(Date.now() / 1000); | |
| const claims: LocalAgentJwtClaims = { | |
| sub: agentId, | |
| company_id: companyId, | |
| adapter_type: adapterType, | |
| run_id: runId, | |
| iat: now, | |
| exp: now + config.ttlSeconds, | |
| iss: config.issuer, | |
| aud: config.audience, | |
| }; | |
| const header = { | |
| alg: JWT_ALGORITHM, | |
| typ: "JWT", | |
| }; | |
| const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(claims))}`; | |
| const signature = signPayload(config.secret, signingInput); | |
| return `${signingInput}.${signature}`; | |
| } | |
| export function verifyLocalAgentJwt(token: string): LocalAgentJwtClaims | null { | |
| if (!token) return null; | |
| const config = jwtConfig(); | |
| if (!config) return null; | |
| const parts = token.split("."); | |
| if (parts.length !== 3) return null; | |
| const [headerB64, claimsB64, signature] = parts; | |
| const header = parseJson(base64UrlDecode(headerB64)); | |
| if (!header || header.alg !== JWT_ALGORITHM) return null; | |
| const signingInput = `${headerB64}.${claimsB64}`; | |
| const expectedSig = signPayload(config.secret, signingInput); | |
| if (!safeCompare(signature, expectedSig)) return null; | |
| const claims = parseJson(base64UrlDecode(claimsB64)); | |
| if (!claims) return null; | |
| const sub = typeof claims.sub === "string" ? claims.sub : null; | |
| const companyId = typeof claims.company_id === "string" ? claims.company_id : null; | |
| const adapterType = typeof claims.adapter_type === "string" ? claims.adapter_type : null; | |
| const runId = typeof claims.run_id === "string" ? claims.run_id : null; | |
| const iat = typeof claims.iat === "number" ? claims.iat : null; | |
| const exp = typeof claims.exp === "number" ? claims.exp : null; | |
| if (!sub || !companyId || !adapterType || !runId || !iat || !exp) return null; | |
| const now = Math.floor(Date.now() / 1000); | |
| if (exp < now) return null; | |
| const issuer = typeof claims.iss === "string" ? claims.iss : undefined; | |
| const audience = typeof claims.aud === "string" ? claims.aud : undefined; | |
| if (issuer && issuer !== config.issuer) return null; | |
| if (audience && audience !== config.audience) return null; | |
| return { | |
| sub, | |
| company_id: companyId, | |
| adapter_type: adapterType, | |
| run_id: runId, | |
| iat, | |
| exp, | |
| ...(issuer ? { iss: issuer } : {}), | |
| ...(audience ? { aud: audience } : {}), | |
| jti: typeof claims.jti === "string" ? claims.jti : undefined, | |
| }; | |
| } | |