import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch'; const MAX_CATALOG_BYTES = 1_310_720; const MAX_CATALOG_VERSION = 64; const MAX_PLUGIN_ID = 48; const MAX_CAPABILITY_ID = 64; const MAX_OPERATION_ID = 64; const MAX_NOTICE = 160; const DEFAULT_POLICY_REQUEST_TIMEOUT_MS = 10_000; const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9.-]{0,47}$/u; const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u; const OPERATION_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u; const TOKEN_POINT_AMOUNT_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/u; type UnknownRecord = Record; type FetchImplementation = (input: string | URL, init?: RequestInit) => Promise; export interface PluginPricingVersion { version: number; version_id: string; effective_at: string; } export type PluginBillingPolicy = | { mode: 'included'; entitlement_scope: null; notice: string; } | { mode: 'external_account'; notice: string; } | { mode: 'platform_metered'; entitlement_scope: string; notice: string; unit_name: string; unit_size: number; rate_points: string; minimum_charge_points: string; rounding_mode: 'ceil'; } | { mode: 'platform_metered'; status: 'billing_unavailable'; entitlement_scope: string; notice: string; }; export interface PluginCatalogOperation { operation: string; billing: PluginBillingPolicy; } export interface PluginCatalogCapability { capability_id: string; operations: PluginCatalogOperation[]; } export interface PluginCatalogPlugin { plugin_id: string; supported_contract_versions: number[]; status: 'active'; capabilities: PluginCatalogCapability[]; } export interface PluginCatalog { schema_version: 1; catalog_version: string; pricing_version: PluginPricingVersion | null; plugins: PluginCatalogPlugin[]; } export type PluginPolicyAvailability = 'unavailable' | 'current' | 'stale'; export interface PluginPolicyClientState { status: PluginPolicyAvailability; catalog: PluginCatalog | null; revision: number; lastVerifiedAt: number | null; errorCode?: 'plugin_backend_unavailable'; } export interface PluginPolicyClientOptions { fetchImpl?: FetchImplementation; apiBaseUrl?: string; now?: () => number; requestTimeoutMs?: number; } export class PluginPolicyCatalogError extends Error { readonly code = 'plugin_policy_invalid' as const; constructor(message = 'Plugin policy catalog is invalid') { super(message); this.name = 'PluginPolicyCatalogError'; } } function isRecord(value: unknown): value is UnknownRecord { return value !== null && typeof value === 'object' && !Array.isArray(value); } function exactKeys( value: UnknownRecord, required: readonly string[], optional: readonly string[] = [], ): void { const allowed = new Set([...required, ...optional]); const keys = Object.keys(value); if (keys.some((key) => !allowed.has(key)) || required.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) { throw new PluginPolicyCatalogError('Plugin policy catalog contains unexpected fields'); } } function text(value: unknown, maximum: number, field: string): string { if (typeof value !== 'string' || value.length === 0 || value.length > maximum) { throw new PluginPolicyCatalogError(`${field} is invalid`); } return value; } function identifier( value: unknown, maximum: number, pattern: RegExp, field: string, ): string { const result = text(value, maximum, field); if (!pattern.test(result)) throw new PluginPolicyCatalogError(`${field} has invalid shape`); return result; } function positiveInteger(value: unknown, field: string): number { if (!Number.isSafeInteger(value) || (value as number) < 1) { throw new PluginPolicyCatalogError(`${field} must be a positive integer`); } return value as number; } function unique(values: readonly string[], field: string): void { if (new Set(values).size !== values.length) { throw new PluginPolicyCatalogError(`${field} contains duplicates`); } } function timestamp(value: unknown, field: string): string { const result = text(value, 64, field); if (!result.endsWith('Z') || Number.isNaN(Date.parse(result))) { throw new PluginPolicyCatalogError(`${field} is invalid`); } return result; } function publicAmount(value: unknown, field: string): string { const result = text(value, 32, field); if (!TOKEN_POINT_AMOUNT_PATTERN.test(result)) { throw new PluginPolicyCatalogError(`${field} is invalid`); } return result; } function parsePricingVersion(value: unknown): PluginPricingVersion | null { if (value === null) return null; if (!isRecord(value)) throw new PluginPolicyCatalogError('pricing_version is invalid'); exactKeys(value, ['version', 'version_id', 'effective_at']); return { version: positiveInteger(value.version, 'pricing_version.version'), version_id: text(value.version_id, 36, 'pricing_version.version_id'), effective_at: timestamp(value.effective_at, 'pricing_version.effective_at'), }; } function parseBilling(value: unknown): PluginBillingPolicy { if (!isRecord(value)) throw new PluginPolicyCatalogError('billing is invalid'); if (value.mode === 'included') { exactKeys(value, ['mode', 'entitlement_scope', 'notice']); if (value.entitlement_scope !== null) { throw new PluginPolicyCatalogError('included entitlement_scope must be null'); } return { mode: 'included', entitlement_scope: null, notice: text(value.notice, MAX_NOTICE, 'billing.notice'), }; } if (value.mode === 'external_account') { exactKeys(value, ['mode', 'notice']); return { mode: 'external_account', notice: text(value.notice, MAX_NOTICE, 'billing.notice'), }; } if (value.mode !== 'platform_metered') { throw new PluginPolicyCatalogError('billing.mode is invalid'); } if (value.status === 'billing_unavailable') { exactKeys(value, ['mode', 'status', 'entitlement_scope', 'notice']); return { mode: 'platform_metered', status: 'billing_unavailable', entitlement_scope: identifier( value.entitlement_scope, 64, OPERATION_ID_PATTERN, 'billing.entitlement_scope', ), notice: text(value.notice, MAX_NOTICE, 'billing.notice'), }; } exactKeys(value, [ 'mode', 'entitlement_scope', 'notice', 'unit_name', 'unit_size', 'rate_points', 'minimum_charge_points', 'rounding_mode', ]); if (value.rounding_mode !== 'ceil') { throw new PluginPolicyCatalogError('billing.rounding_mode is invalid'); } return { mode: 'platform_metered', entitlement_scope: identifier( value.entitlement_scope, 64, OPERATION_ID_PATTERN, 'billing.entitlement_scope', ), notice: text(value.notice, MAX_NOTICE, 'billing.notice'), unit_name: text(value.unit_name, 80, 'billing.unit_name'), unit_size: positiveInteger(value.unit_size, 'billing.unit_size'), rate_points: publicAmount(value.rate_points, 'billing.rate_points'), minimum_charge_points: publicAmount( value.minimum_charge_points, 'billing.minimum_charge_points', ), rounding_mode: 'ceil', }; } function parseOperation(value: unknown): PluginCatalogOperation { if (!isRecord(value)) throw new PluginPolicyCatalogError('operation is invalid'); exactKeys(value, ['operation', 'billing']); return { operation: identifier(value.operation, MAX_OPERATION_ID, OPERATION_ID_PATTERN, 'operation'), billing: parseBilling(value.billing), }; } function parseCapability(value: unknown): PluginCatalogCapability { if (!isRecord(value)) throw new PluginPolicyCatalogError('capability is invalid'); exactKeys(value, ['capability_id', 'operations']); if (!Array.isArray(value.operations) || value.operations.length < 1 || value.operations.length > 256) { throw new PluginPolicyCatalogError('capability.operations is invalid'); } const operations = value.operations.map(parseOperation); unique(operations.map(({ operation }) => operation), 'capability.operations'); return { capability_id: identifier( value.capability_id, MAX_CAPABILITY_ID, CAPABILITY_ID_PATTERN, 'capability_id', ), operations, }; } function parsePlugin(value: unknown): PluginCatalogPlugin { if (!isRecord(value)) throw new PluginPolicyCatalogError('plugin is invalid'); exactKeys(value, ['plugin_id', 'supported_contract_versions', 'status', 'capabilities']); if (value.status !== 'active') throw new PluginPolicyCatalogError('plugin.status is invalid'); if (!Array.isArray(value.supported_contract_versions) || value.supported_contract_versions.length < 1 || value.supported_contract_versions.length > 32) { throw new PluginPolicyCatalogError('supported_contract_versions is invalid'); } const supportedContractVersions = value.supported_contract_versions.map((item) => ( positiveInteger(item, 'supported_contract_versions') )); if (new Set(supportedContractVersions).size !== supportedContractVersions.length) { throw new PluginPolicyCatalogError('supported_contract_versions contains duplicates'); } if (!Array.isArray(value.capabilities) || value.capabilities.length < 1 || value.capabilities.length > 128) { throw new PluginPolicyCatalogError('plugin.capabilities is invalid'); } const capabilities = value.capabilities.map(parseCapability); unique(capabilities.map(({ capability_id }) => capability_id), 'plugin.capabilities'); return { plugin_id: identifier(value.plugin_id, MAX_PLUGIN_ID, PLUGIN_ID_PATTERN, 'plugin_id'), supported_contract_versions: supportedContractVersions, status: 'active', capabilities, }; } /** Parse the exact public server catalog; unknown fields are rejected. */ export function parsePluginCatalog(value: unknown): PluginCatalog { if (!isRecord(value)) throw new PluginPolicyCatalogError(); exactKeys(value, ['schema_version', 'catalog_version', 'pricing_version', 'plugins']); if (value.schema_version !== 1) throw new PluginPolicyCatalogError('schema_version must equal 1'); if (!Array.isArray(value.plugins) || value.plugins.length > 128) { throw new PluginPolicyCatalogError('plugins is invalid'); } const plugins = value.plugins.map(parsePlugin); unique(plugins.map(({ plugin_id }) => plugin_id), 'plugins'); return Object.freeze({ schema_version: 1 as const, catalog_version: text(value.catalog_version, MAX_CATALOG_VERSION, 'catalog_version'), pricing_version: parsePricingVersion(value.pricing_version), plugins, }); } async function readBoundedText(response: Response): Promise { const declaredLength = response.headers.get('content-length'); if (declaredLength && /^\d+$/u.test(declaredLength) && Number(declaredLength) > MAX_CATALOG_BYTES) { await response.body?.cancel().catch(() => undefined); throw new PluginPolicyCatalogError('catalog response is too large'); } if (!response.body) { const textValue = await response.text(); if (Buffer.byteLength(textValue, 'utf8') > MAX_CATALOG_BYTES) { throw new PluginPolicyCatalogError('catalog response is too large'); } return textValue; } const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; if (!value) continue; size += value.byteLength; if (size > MAX_CATALOG_BYTES) { await reader.cancel().catch(() => undefined); throw new PluginPolicyCatalogError('catalog response is too large'); } chunks.push(value); } } finally { reader.releaseLock(); } return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8'); } async function readCatalog(response: Response): Promise { let source: string; try { source = await readBoundedText(response); } catch (error) { if (error instanceof PluginPolicyCatalogError) throw error; throw new PluginPolicyCatalogError(); } let payload: unknown; try { payload = JSON.parse(source) as unknown; } catch { throw new PluginPolicyCatalogError(); } return parsePluginCatalog(payload); } function cloneState(state: PluginPolicyClientState): PluginPolicyClientState { return { ...state, catalog: state.catalog ? structuredClone(state.catalog) : null, }; } /** * Narrow HTTP adapter for the unauthenticated, user-independent policy catalog. * Only the last successfully parsed catalog is retained after a refresh error; * it is visibly stale and never becomes a source of billing authority. */ export class PluginPolicyClient { private readonly fetchImpl: FetchImplementation; private readonly apiBaseUrl: string; private readonly now: () => number; private readonly requestTimeoutMs: number; private state: PluginPolicyClientState = { status: 'unavailable', catalog: null, revision: 0, lastVerifiedAt: null, }; private refreshFlight: Promise | null = null; constructor(options: PluginPolicyClientOptions = {}) { this.fetchImpl = options.fetchImpl ?? proxyAwareFetch; this.apiBaseUrl = (options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/u, ''); this.now = options.now ?? (() => Date.now()); this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_POLICY_REQUEST_TIMEOUT_MS; } getState(): PluginPolicyClientState { return cloneState(this.state); } getStatus(): PluginPolicyAvailability { return this.state.status; } get catalog(): PluginCatalog | null { return this.state.catalog ? structuredClone(this.state.catalog) : null; } refresh(): Promise { if (this.refreshFlight) return this.refreshFlight; const flight = this.fetchCatalog() .then((catalog) => { this.state = { status: 'current', catalog, revision: this.state.revision + 1, lastVerifiedAt: this.now(), }; return this.getState(); }) .catch(() => { this.state = { ...this.state, status: this.state.catalog ? 'stale' : 'unavailable', errorCode: 'plugin_backend_unavailable', }; return this.getState(); }) .finally(() => { if (this.refreshFlight === flight) this.refreshFlight = null; }); this.refreshFlight = flight; return flight; } refreshCatalog(): Promise { return this.refresh(); } private async fetchCatalog(): Promise { try { return await runWithDeadline(async (signal) => { const response = await this.fetchImpl( `${this.apiBaseUrl}/api/plugins/v1/catalog`, { method: 'GET', headers: { Accept: 'application/json' }, redirect: 'manual', signal, }, ); if (!response.ok) { await response.body?.cancel().catch(() => undefined); throw new PluginPolicyCatalogError('catalog request failed'); } return await readCatalog(response); }, this.requestTimeoutMs); } catch { throw new PluginPolicyCatalogError('catalog request failed'); } } } export const parsePluginPolicyCatalog = parsePluginCatalog;