Spaces:
Paused
Paused
File size: 4,259 Bytes
b152fd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 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,
};
}
|