import { inject, injectable } from "@codemation/core"; import { PairedFetch } from "../../pairing/PairedFetch"; import { PairingConfigToken } from "../../pairing/PairingConfigToken"; import type { PairingConfig } from "../../pairing/pairing.types"; import { BlobSignClientClockToken } from "./blob.types"; import type { BlobSignOp, BlobSignedUrl } from "./blob.types"; const REFRESH_BEFORE_EXPIRY_MS = 30_000; interface CacheEntry { readonly url: string; readonly expiresAt: number; } @injectable() export class BlobSignClient { private readonly cache = new Map(); constructor( @inject(PairedFetch) private readonly pairedFetch: PairedFetch, @inject(PairingConfigToken) private readonly pairingConfig: PairingConfig, @inject(BlobSignClientClockToken, { isOptional: true }) private readonly clock: (() => number) | null = null, ) {} async getPresignedUrls(op: BlobSignOp, keys: readonly string[]): Promise> { const now = (this.clock ?? (() => Date.now()))(); const hits: BlobSignedUrl[] = []; const missKeys: string[] = []; for (const key of keys) { const cacheKey = `${op}:${key}`; const entry = this.cache.get(cacheKey); if (entry && entry.expiresAt - now > REFRESH_BEFORE_EXPIRY_MS) { hits.push({ key, url: entry.url, expiresAt: new Date(entry.expiresAt).toISOString() }); } else { missKeys.push(key); } } if (missKeys.length === 0) { return this.reorderByInput(hits, keys); } const minted = await this.mintUrls(op, missKeys); for (const signed of minted) { const cacheKey = `${op}:${signed.key}`; this.cache.set(cacheKey, { url: signed.url, expiresAt: Date.parse(signed.expiresAt) }); hits.push(signed); } return this.reorderByInput(hits, keys); } async listKeys(prefix: string): Promise { const url = `${this.pairingConfig.controlPlaneUrl}/internal/blob-sign`; const response = await this.pairedFetch.post(url, { op: "LIST", prefix }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`BlobSignClient LIST failed: ${response.status} ${body.slice(0, 200)}`); } const json = (await response.json()) as { keys?: unknown }; if (!Array.isArray(json.keys)) { throw new Error("BlobSignClient LIST response missing keys array"); } return json.keys.filter((k): k is string => typeof k === "string"); } private async mintUrls(op: BlobSignOp, keys: string[]): Promise> { const url = `${this.pairingConfig.controlPlaneUrl}/internal/blob-sign`; const response = await this.pairedFetch.post(url, { op, keys }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`BlobSignClient sign failed (${op}): ${response.status} ${body.slice(0, 200)}`); } const json = (await response.json()) as { urls?: unknown }; if (!Array.isArray(json.urls)) { throw new Error(`BlobSignClient sign response missing urls array (op=${op})`); } return json.urls.filter( (u): u is BlobSignedUrl => typeof u === "object" && u !== null && typeof u.key === "string" && typeof u.url === "string" && typeof u.expiresAt === "string", ); } private reorderByInput(results: BlobSignedUrl[], keys: readonly string[]): ReadonlyArray { const byKey = new Map(results.map((r) => [r.key, r])); return keys.map((k) => { const r = byKey.get(k); if (!r) throw new Error(`BlobSignClient: no signed URL returned for key ${k}`); return r; }); } }