63 lines
2.4 KiB
JavaScript
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 })
|