Files
dsh/upstream/container/control-plane/services/outbound-proxy/lib/provider-handles.mjs
T

63 lines
2.4 KiB
JavaScript

import { randomBytes } from 'node:crypto'
import { ProxyConfigurationError } from './errors.mjs'
import { validProviderId } from './contracts.mjs'
const HANDLE_BYTES = 32
const HANDLE_PATTERN = /^[A-Za-z0-9_-]{43}$/
export const PROVIDER_HANDLE_TTL_MS = 15 * 60 * 1000
function invalidHandle() {
return new ProxyConfigurationError('provider policy handle is invalid', {
code: 'PROVIDER_HANDLE_INVALID', statusCode: 407, stage: 'authorize', retryable: true,
})
}
export class ProviderHandleStore {
constructor({ ttlMs = PROVIDER_HANDLE_TTL_MS, now = () => Date.now(), random = randomBytes } = {}) {
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1) throw new TypeError('provider handle TTL is invalid')
this.ttlMs = ttlMs
this.now = now
this.random = random
this.handles = new Map()
}
issue({ providerId, policyRevision }, snapshot) {
if (!validProviderId(providerId)) throw new ProxyConfigurationError('model API provider ID is invalid')
if (typeof policyRevision !== 'string' || policyRevision !== snapshot.revision) {
throw new ProxyConfigurationError('provider policy revision changed', {
code: 'REVISION_CONFLICT', statusCode: 409, stage: 'authorize', retryable: true,
})
}
this.prune()
const handle = this.random(HANDLE_BYTES).toString('base64url')
if (!HANDLE_PATTERN.test(handle) || this.handles.has(handle)) throw new Error('provider handle generation failed')
const expiresAtMs = this.now() + this.ttlMs
this.handles.set(handle, Object.freeze({ providerId, policyRevision, expiresAtMs }))
return Object.freeze({ handle, policyRevision, expiresAt: new Date(expiresAtMs).toISOString() })
}
resolve(authorization, snapshot) {
const match = typeof authorization === 'string' ? /^DSH-Provider ([A-Za-z0-9_-]{43})$/.exec(authorization) : null
if (match === null) throw invalidHandle()
const entry = this.handles.get(match[1])
if (entry === undefined || entry.expiresAtMs <= this.now() || entry.policyRevision !== snapshot.revision) {
if (entry !== undefined) this.handles.delete(match[1])
throw invalidHandle()
}
return entry.providerId
}
prune() {
const now = this.now()
for (const [handle, entry] of this.handles) {
if (entry.expiresAtMs <= now) this.handles.delete(handle)
}
}
clear() {
this.handles.clear()
}
}
export const providerHandleInternals = Object.freeze({ HANDLE_BYTES, HANDLE_PATTERN })