diff --git a/src/backend/controllers/drivers/DriverController.ts b/src/backend/controllers/drivers/DriverController.ts index c3e28cf43..a84b2c9bb 100644 --- a/src/backend/controllers/drivers/DriverController.ts +++ b/src/backend/controllers/drivers/DriverController.ts @@ -41,10 +41,7 @@ import { isDriverStreamResult, resolveCallableMethods, resolveDriverMeta, - resolveDriverMethodConcurrent, - resolveDriverMethodRateLimit, - resolveDriverMethodRequireReputation, - resolveDriverMethodRequireSubscription, + resolvePerMethod, } from '../../drivers/meta.js'; import { assertActorHasSubscription } from '../../services/metering/enforcement.js'; import type { PermissionService } from '../../services/permission/PermissionService.js'; @@ -301,16 +298,10 @@ export class DriverController extends PuterController { } const fn = driver[method]; - // Resolve the concrete driver name for permission keys, falling - // back through prototype metadata → instance field → requested name. - const resolvedDriverName = - (driver as Record).driverName ?? - (Object.getPrototypeOf(driver) as Record) - .__driverName ?? - requestedDriver ?? - 'unknown'; - const driverMeta = this.#meta.get(driver); + // The concrete driver name, not an alias, keys the permission check. + const resolvedDriverName = + driverMeta?.driverName ?? requestedDriver ?? 'unknown'; // Drivers flagged `noUserSession` refuse the bare // account-session ("root") token: callers must present an app or @@ -355,13 +346,13 @@ export class DriverController extends PuterController { } // Methods that ask for a trusted-enough account. Declared per-driver - // (`@Driver({ requireReputation })`) for the same reason the + // (`requireReputation`) for the same reason the // subscription block is. Checked ahead of the plan and rate-limit // gates, matching the route chain: whether this account should be // reaching the method at all is settled before what it pays for or how // often it may ask. Inert unless the running config gives the named // tier a score. - const reputationRequirement = resolveDriverMethodRequireReputation( + const reputationRequirement = resolvePerMethod( driverMeta?.requireReputation, method, ); @@ -373,13 +364,13 @@ export class DriverController extends PuterController { ); } - // Subscriber-only methods. Declared per-driver - // (`@Driver({ requireSubscription })`) because `/drivers/call` is a - // single route and a route option would apply to every driver at once. + // Subscriber-only methods. Declared per-driver (`requireSubscription`) + // because `/drivers/call` is a single route and a route option would + // apply to every driver at once. // Checked before the rate limit — the same order the route chain uses // — so a caller whose plan never included the method is told that // rather than spending a bucket on it. - const subscriptionRequirement = resolveDriverMethodRequireSubscription( + const subscriptionRequirement = resolvePerMethod( driverMeta?.requireSubscription, method, ); @@ -398,16 +389,12 @@ export class DriverController extends PuterController { } // Per-method rate-limit and concurrent specs both live on the - // driver's resolved meta (set by `@Driver({ rateLimit, concurrent })` - // or imperative fields). Rate-limit is single-shot; concurrent + // driver's resolved meta. Rate-limit is single-shot; concurrent // acquires a slot that must be released when the response is done // — we hook `res.finish` / `res.close` for that so streamed // responses hold their slot until the stream drains, and aborted // requests still give the slot back. - const rateLimitSpec = resolveDriverMethodRateLimit( - driverMeta?.rateLimit, - method, - ); + const rateLimitSpec = resolvePerMethod(driverMeta?.rateLimit, method); if ( !(await checkDriverRateLimit(req, ifaceName, method, rateLimitSpec)) ) { @@ -418,10 +405,7 @@ export class DriverController extends PuterController { }); } - const concurrentSpec = resolveDriverMethodConcurrent( - driverMeta?.concurrent, - method, - ); + const concurrentSpec = resolvePerMethod(driverMeta?.concurrent, method); // Only acquire (and attach release listeners) when the driver // actually declared a concurrency cap. Skipping in the unbounded // case keeps the hot path free of needless event-listener churn diff --git a/src/backend/drivers/decorators.ts b/src/backend/drivers/decorators.ts deleted file mode 100644 index 51b243e74..000000000 --- a/src/backend/drivers/decorators.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { - DRIVER_CONCURRENT_KEY, - DRIVER_DEFAULT_KEY, - DRIVER_INTERFACE_KEY, - DRIVER_NAME_KEY, - DRIVER_NO_USER_SESSION_KEY, - DRIVER_RATE_LIMIT_KEY, - DRIVER_REQUIRE_REPUTATION_KEY, - DRIVER_REQUIRE_SUBSCRIPTION_KEY, - validateDriverConcurrent, - validateDriverRateLimit, - validateDriverRequireReputation, - validateDriverRequireSubscription, - type DriverConcurrentConfig, - type DriverRateLimitConfig, - type DriverRequireReputationConfig, - type DriverRequireSubscriptionConfig, -} from './meta'; - -/** - * Options for the `@Driver` class decorator. Each policy takes `{ default?, - * methods? }`; a method covered by neither is ungated (rate limits fall through - * to the global driver default). See `DriverMeta` for semantics. - */ -export interface DriverOptions { - /** Unique within the interface. Defaults to the class name. */ - name?: string; - /** The default driver for its interface. */ - default?: boolean; - rateLimit?: DriverRateLimitConfig; - concurrent?: DriverConcurrentConfig; - /** Reject bare account-session tokens on `/drivers/call`. */ - noUserSession?: boolean; - /** - * `true` accepts any non-free plan; an array of `SubscriptionPolicy.id`s - * only those. - */ - requireSubscription?: DriverRequireSubscriptionConfig; - /** Reputation tier names; thresholds come from `reputationGate.tiers`. */ - requireReputation?: DriverRequireReputationConfig; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyCtor = new (...args: any[]) => any; - -/** - * Class decorator that marks a driver implementation and records its interface - * - * - Name on the prototype. - * - * Equivalent imperative approach (no decorator needed): - * - * ```ts - * class MyDriver extends PuterDriver { - * readonly driverInterface = 'puter-chat-completion'; - * readonly driverName = 'my-impl'; - * readonly isDefault = true; - * } - * ``` - * - * Usage: - * - * ```ts - * @Driver('puter-chat-completion', { name: 'openai-completion', default: true }) - * class OpenAIChatDriver extends PuterDriver { - * async complete(args) { ... } - * } - * ``` - */ -export function Driver(interfaceName: string, opts: DriverOptions = {}) { - // Validate eagerly at decoration time so a malformed rateLimit / - // concurrent block surfaces during module load — not when the first - // request hits the route and the controller resolves driver meta. - const label = `@Driver('${interfaceName}'${opts.name ? `, name='${opts.name}'` : ''})`; - const rateLimit = - opts.rateLimit !== undefined - ? validateDriverRateLimit(opts.rateLimit, label) - : undefined; - const concurrent = - opts.concurrent !== undefined - ? validateDriverConcurrent(opts.concurrent, label) - : undefined; - const requireSubscription = - opts.requireSubscription !== undefined - ? validateDriverRequireSubscription(opts.requireSubscription, label) - : undefined; - const requireReputation = - opts.requireReputation !== undefined - ? validateDriverRequireReputation(opts.requireReputation, label) - : undefined; - - return ( - value: T, - _context: ClassDecoratorContext, - ): void => { - const proto = value.prototype as Record; - proto[DRIVER_INTERFACE_KEY] = interfaceName; - proto[DRIVER_NAME_KEY] = opts.name ?? value.name; - proto[DRIVER_DEFAULT_KEY] = opts.default ?? false; - if (rateLimit) proto[DRIVER_RATE_LIMIT_KEY] = rateLimit; - if (concurrent) proto[DRIVER_CONCURRENT_KEY] = concurrent; - if (requireSubscription) - proto[DRIVER_REQUIRE_SUBSCRIPTION_KEY] = requireSubscription; - if (requireReputation) - proto[DRIVER_REQUIRE_REPUTATION_KEY] = requireReputation; - if (opts.noUserSession) proto[DRIVER_NO_USER_SESSION_KEY] = true; - }; -} diff --git a/src/backend/drivers/index.ts b/src/backend/drivers/index.ts index 0142762c8..8fb7c1e6f 100644 --- a/src/backend/drivers/index.ts +++ b/src/backend/drivers/index.ts @@ -31,7 +31,6 @@ import { SubdomainDriver } from './subdomain/SubdomainDriver'; import type { IPuterDriverRegistry } from './types'; import { WorkerDriver } from './workers/WorkerDriver'; -export { Driver } from './decorators'; export { resolveDriverMeta } from './meta'; export const puterDrivers = { diff --git a/src/backend/drivers/meta.test.ts b/src/backend/drivers/meta.test.ts index 25567584f..88ef5b3e7 100644 --- a/src/backend/drivers/meta.test.ts +++ b/src/backend/drivers/meta.test.ts @@ -18,29 +18,24 @@ */ import { describe, expect, it } from 'vitest'; -import { Driver } from './decorators.js'; import { resolveDriverMeta, resolveDriverMethodConcurrent, resolveDriverMethodRateLimit, - resolveDriverMethodRequireReputation, - resolveDriverMethodRequireSubscription, - validateDriverConcurrent, - validateDriverRateLimit, - validateDriverRequireReputation, - validateDriverRequireSubscription, + resolvePerMethod, + validatePerMethod, type DriverConcurrentConfig, type DriverRateLimitConfig, type DriverRequireReputationConfig, type DriverRequireSubscriptionConfig, } from './meta.js'; -// ── validateDriverRateLimit ───────────────────────────────────────── +// ── validatePerMethod: rateLimit ───────────────────────────────────────── -describe('validateDriverRateLimit', () => { +describe('validatePerMethod: rateLimit', () => { it('returns an empty config for null/undefined input', () => { - expect(validateDriverRateLimit(undefined, 't')).toEqual({}); - expect(validateDriverRateLimit(null, 't')).toEqual({}); + expect(validatePerMethod(undefined, 't', 'rateLimit')).toEqual({}); + expect(validatePerMethod(null, 't', 'rateLimit')).toEqual({}); }); it('accepts a well-formed default + methods block', () => { @@ -51,39 +46,45 @@ describe('validateDriverRateLimit', () => { set: { limit: 100, window: 60_000, backend: 'redis' }, }, }; - expect(validateDriverRateLimit(cfg, 't')).toBe(cfg); + expect(validatePerMethod(cfg, 't', 'rateLimit')).toBe(cfg); }); it('rejects a non-object config', () => { - expect(() => validateDriverRateLimit(42, 't')).toThrow( + expect(() => validatePerMethod(42, 't', 'rateLimit')).toThrow( /rateLimit must be an object/, ); - expect(() => validateDriverRateLimit([], 't')).toThrow( + expect(() => validatePerMethod([], 't', 'rateLimit')).toThrow( /rateLimit must be an object/, ); }); it('rejects non-positive / non-numeric limit and window', () => { expect(() => - validateDriverRateLimit({ default: { limit: 0, window: 1 } }, 't'), + validatePerMethod( + { default: { limit: 0, window: 1 } }, + 't', + 'rateLimit', + ), ).toThrow(/limit: expected a positive number/); expect(() => - validateDriverRateLimit( + validatePerMethod( { default: { limit: 1, window: -10 } }, 't', + 'rateLimit', ), ).toThrow(/window: expected a positive number/); expect(() => - validateDriverRateLimit( + validatePerMethod( { default: { limit: 'x', window: 60_000 } }, 't', + 'rateLimit', ), ).toThrow(/limit: expected a positive number/); }); it('rejects unknown backend names', () => { expect(() => - validateDriverRateLimit( + validatePerMethod( { default: { limit: 1, @@ -92,13 +93,14 @@ describe('validateDriverRateLimit', () => { }, }, 't', + 'rateLimit', ), ).toThrow(/backend: expected one of/); }); it('walks the methods map and labels the failing entry', () => { expect(() => - validateDriverRateLimit( + validatePerMethod( { methods: { goodOne: { limit: 5, window: 60_000 }, @@ -106,13 +108,14 @@ describe('validateDriverRateLimit', () => { }, }, 'drv', + 'rateLimit', ), ).toThrow(/drv\.rateLimit\.methods\.badOne\.window/); }); it('rejects a non-object methods bag', () => { expect(() => - validateDriverRateLimit({ methods: [] as unknown }, 't'), + validatePerMethod({ methods: [] as unknown }, 't', 'rateLimit'), ).toThrow(/methods must be an object/); }); @@ -126,13 +129,13 @@ describe('validateDriverRateLimit', () => { }, }, }; - expect(validateDriverRateLimit(cfg, 't')).toBe(cfg); + expect(validatePerMethod(cfg, 't', 'rateLimit')).toBe(cfg); }); it('rejects malformed bySubscription entries on a rate-limit spec', () => { // Symmetry with the concurrent validator — bad numbers fail loud. expect(() => - validateDriverRateLimit( + validatePerMethod( { default: { limit: 1, @@ -141,14 +144,15 @@ describe('validateDriverRateLimit', () => { }, }, 'drv', + 'rateLimit', ), ).toThrow(/drv\.rateLimit\.default\.bySubscription\.user_free/); }); }); -// ── resolveDriverMethodRateLimit ──────────────────────────────────── +// ── resolvePerMethod: rateLimit ──────────────────────────────────── -describe('resolveDriverMethodRateLimit', () => { +describe('resolvePerMethod: rateLimit', () => { const cfg: DriverRateLimitConfig = { default: { limit: 100, window: 60_000 }, methods: { @@ -157,7 +161,7 @@ describe('resolveDriverMethodRateLimit', () => { }; it('returns the per-method spec when one is declared', () => { - expect(resolveDriverMethodRateLimit(cfg, 'get')).toEqual({ + expect(resolvePerMethod(cfg, 'get')).toEqual({ limit: 1000, window: 60_000, backend: 'memory', @@ -165,19 +169,19 @@ describe('resolveDriverMethodRateLimit', () => { }); it('falls back to the default spec when no method override exists', () => { - expect(resolveDriverMethodRateLimit(cfg, 'set')).toEqual({ + expect(resolvePerMethod(cfg, 'set')).toEqual({ limit: 100, window: 60_000, }); }); it('returns undefined when the driver declared no rate-limit at all', () => { - expect(resolveDriverMethodRateLimit(undefined, 'get')).toBeUndefined(); + expect(resolvePerMethod(undefined, 'get')).toBeUndefined(); }); it('returns undefined when neither default nor a matching method is declared', () => { expect( - resolveDriverMethodRateLimit( + resolvePerMethod( { methods: { other: { limit: 1, window: 1 } } }, 'get', ), @@ -185,53 +189,10 @@ describe('resolveDriverMethodRateLimit', () => { }); }); -// ── @Driver decorator: rateLimit propagation ──────────────────────── +// ── resolveDriverMeta: rateLimit ──────────────────────────────────── -describe('@Driver — rateLimit option', () => { - it('stamps a validated rateLimit block onto the prototype, surfacing via resolveDriverMeta', () => { - @Driver('test-iface', { - name: 'test-impl', - rateLimit: { - default: { limit: 50, window: 60_000 }, - methods: { - chat: { limit: 10, window: 60_000, backend: 'redis' }, - }, - }, - }) - class FakeDriver {} - - const inst = new FakeDriver(); - const meta = resolveDriverMeta( - inst as unknown as Record & { - onServerStart?: () => void; - onServerPrepareShutdown?: () => void; - onServerShutdown?: () => void; - }, - ); - expect(meta).not.toBeNull(); - expect(meta?.rateLimit).toEqual({ - default: { limit: 50, window: 60_000 }, - methods: { - chat: { limit: 10, window: 60_000, backend: 'redis' }, - }, - }); - }); - - it('throws at decoration time on a malformed rateLimit block', () => { - // The whole point of eager validation: bad config takes the - // module down at boot, not at the first request. - expect(() => { - @Driver('test-iface', { - name: 'broken', - rateLimit: { default: { limit: -1, window: 1_000 } }, - }) - class BrokenDriver {} - void BrokenDriver; - }).toThrow(/limit: expected a positive number/); - }); - - it('falls back to imperative `rateLimit` field when no decorator metadata is set', () => { - // Imperative drivers (no decorator) declare the field directly. +describe('resolveDriverMeta — rateLimit', () => { + it('reads the `rateLimit` field', () => { class Imperative { readonly driverInterface = 'imp-iface'; readonly driverName = 'imp'; @@ -253,7 +214,7 @@ describe('@Driver — rateLimit option', () => { }); }); - it('validates the imperative `rateLimit` field on first read (loud failure)', () => { + it('validates the `rateLimit` field on first read (loud failure)', () => { class BadImperative { readonly driverInterface = 'imp-iface'; readonly driverName = 'imp-bad'; @@ -274,12 +235,12 @@ describe('@Driver — rateLimit option', () => { }); }); -// ── validateDriverConcurrent ──────────────────────────────────────── +// ── validatePerMethod: concurrent ──────────────────────────────────────── -describe('validateDriverConcurrent', () => { +describe('validatePerMethod: concurrent', () => { it('returns an empty config for null/undefined input', () => { - expect(validateDriverConcurrent(undefined, 't')).toEqual({}); - expect(validateDriverConcurrent(null, 't')).toEqual({}); + expect(validatePerMethod(undefined, 't', 'concurrent')).toEqual({}); + expect(validatePerMethod(null, 't', 'concurrent')).toEqual({}); }); it('accepts a well-formed default + methods block with bySubscription', () => { @@ -293,30 +254,31 @@ describe('validateDriverConcurrent', () => { }, }, }; - expect(validateDriverConcurrent(cfg, 't')).toBe(cfg); + expect(validatePerMethod(cfg, 't', 'concurrent')).toBe(cfg); }); it('rejects non-positive / non-numeric limit', () => { expect(() => - validateDriverConcurrent({ default: { limit: 0 } }, 't'), + validatePerMethod({ default: { limit: 0 } }, 't', 'concurrent'), ).toThrow(/limit: expected a positive number/); expect(() => - validateDriverConcurrent({ default: { limit: 'x' } }, 't'), + validatePerMethod({ default: { limit: 'x' } }, 't', 'concurrent'), ).toThrow(/limit: expected a positive number/); }); it('rejects unknown backend names', () => { expect(() => - validateDriverConcurrent( + validatePerMethod( { default: { limit: 1, backend: 'sqlite' } }, 't', + 'concurrent', ), ).toThrow(/backend: expected one of/); }); it('rejects malformed bySubscription entries with a labelled path', () => { expect(() => - validateDriverConcurrent( + validatePerMethod( { default: { limit: 5, @@ -324,13 +286,14 @@ describe('validateDriverConcurrent', () => { }, }, 'drv', + 'concurrent', ), ).toThrow(/drv\.concurrent\.default\.bySubscription\.user_free/); }); it('walks the methods map and labels the failing entry', () => { expect(() => - validateDriverConcurrent( + validatePerMethod( { methods: { goodOne: { limit: 5 }, @@ -338,14 +301,15 @@ describe('validateDriverConcurrent', () => { }, }, 'drv', + 'concurrent', ), ).toThrow(/drv\.concurrent\.methods\.badOne\.backend/); }); }); -// ── resolveDriverMethodConcurrent ─────────────────────────────────── +// ── resolvePerMethod: concurrent ─────────────────────────────────── -describe('resolveDriverMethodConcurrent', () => { +describe('resolvePerMethod: concurrent', () => { const cfg: DriverConcurrentConfig = { default: { limit: 3 }, methods: { @@ -354,77 +318,28 @@ describe('resolveDriverMethodConcurrent', () => { }; it('returns the per-method spec when one is declared', () => { - expect(resolveDriverMethodConcurrent(cfg, 'heavy')).toEqual({ + expect(resolvePerMethod(cfg, 'heavy')).toEqual({ limit: 1, backend: 'redis', }); }); it('falls back to default for methods not in the map', () => { - expect(resolveDriverMethodConcurrent(cfg, 'light')).toEqual({ + expect(resolvePerMethod(cfg, 'light')).toEqual({ limit: 3, }); }); it('returns undefined when nothing is declared', () => { - expect( - resolveDriverMethodConcurrent(undefined, 'anything'), - ).toBeUndefined(); - expect(resolveDriverMethodConcurrent({}, 'anything')).toBeUndefined(); + expect(resolvePerMethod(undefined, 'anything')).toBeUndefined(); + expect(resolvePerMethod({}, 'anything')).toBeUndefined(); }); }); -// ── @Driver — concurrent option ───────────────────────────────────── +// ── resolveDriverMeta: concurrent ─────────────────────────────────── -describe('@Driver — concurrent option', () => { - it('stamps a validated concurrent block onto the prototype', () => { - @Driver('test-iface', { - name: 'cdec', - concurrent: { - default: { limit: 4 }, - methods: { - chat: { - limit: 5, - bySubscription: { user_free: 1 }, - backend: 'redis', - }, - }, - }, - }) - class FakeDriver {} - - const inst = new FakeDriver(); - const meta = resolveDriverMeta( - inst as unknown as Record & { - onServerStart?: () => void; - onServerPrepareShutdown?: () => void; - onServerShutdown?: () => void; - }, - ); - expect(meta?.concurrent).toEqual({ - default: { limit: 4 }, - methods: { - chat: { - limit: 5, - bySubscription: { user_free: 1 }, - backend: 'redis', - }, - }, - }); - }); - - it('throws at decoration time on a malformed concurrent block', () => { - expect(() => { - @Driver('test-iface', { - name: 'bad-concurrent', - concurrent: { default: { limit: -1 } }, - }) - class BrokenDriver {} - void BrokenDriver; - }).toThrow(/limit: expected a positive number/); - }); - - it('falls back to imperative `concurrent` field when decorator metadata is absent', () => { +describe('resolveDriverMeta — concurrent', () => { + it('reads the `concurrent` field', () => { class Imperative { readonly driverInterface = 'imp-iface'; readonly driverName = 'imp-c'; @@ -445,10 +360,12 @@ describe('@Driver — concurrent option', () => { // ── requireSubscription ───────────────────────────────────────────── -describe('validateDriverRequireSubscription', () => { +describe('validatePerMethod: requireSubscription', () => { it('returns an empty config for null/undefined input', () => { - expect(validateDriverRequireSubscription(undefined, 't')).toEqual({}); - expect(validateDriverRequireSubscription(null, 't')).toEqual({}); + expect( + validatePerMethod(undefined, 't', 'requireSubscription'), + ).toEqual({}); + expect(validatePerMethod(null, 't', 'requireSubscription')).toEqual({}); }); it('accepts booleans and id allowlists', () => { @@ -456,70 +373,58 @@ describe('validateDriverRequireSubscription', () => { default: false, methods: { generate: true, generateLong: ['business', 'pro'] }, }; - expect(validateDriverRequireSubscription(cfg, 't')).toBe(cfg); + expect(validatePerMethod(cfg, 't', 'requireSubscription')).toBe(cfg); }); it('rejects a non-object config', () => { - expect(() => validateDriverRequireSubscription(42, 't')).toThrow( + expect(() => validatePerMethod(42, 't', 'requireSubscription')).toThrow( /requireSubscription must be an object/, ); - expect(() => validateDriverRequireSubscription([], 't')).toThrow( + expect(() => validatePerMethod([], 't', 'requireSubscription')).toThrow( /requireSubscription must be an object/, ); }); it('rejects requirements that name nothing', () => { expect(() => - validateDriverRequireSubscription({ default: [] }, 't'), + validatePerMethod({ default: [] }, 't', 'requireSubscription'), ).toThrow(/at least one subscription id/); expect(() => - validateDriverRequireSubscription({ methods: { a: [1] } }, 't'), + validatePerMethod( + { methods: { a: [1] } }, + 't', + 'requireSubscription', + ), ).toThrow(/must be strings/); expect(() => - validateDriverRequireSubscription({ methods: { a: 'pro' } }, 't'), + validatePerMethod( + { methods: { a: 'pro' } }, + 't', + 'requireSubscription', + ), ).toThrow(/expected true\/false or an array of ids/); }); }); -describe('resolveDriverMethodRequireSubscription', () => { +describe('resolvePerMethod: requireSubscription', () => { const cfg: DriverRequireSubscriptionConfig = { default: true, methods: { list: false, generateLong: ['pro'] }, }; it('prefers a per-method entry over the default', () => { - expect(resolveDriverMethodRequireSubscription(cfg, 'list')).toBe(false); - expect( - resolveDriverMethodRequireSubscription(cfg, 'generateLong'), - ).toEqual(['pro']); + expect(resolvePerMethod(cfg, 'list')).toBe(false); + expect(resolvePerMethod(cfg, 'generateLong')).toEqual(['pro']); }); it('falls back to the default, and to undefined with no config', () => { - expect(resolveDriverMethodRequireSubscription(cfg, 'generate')).toBe( - true, - ); - expect( - resolveDriverMethodRequireSubscription(undefined, 'generate'), - ).toBeUndefined(); - expect(resolveDriverMethodRequireSubscription({}, 'generate')).toBe( - undefined, - ); + expect(resolvePerMethod(cfg, 'generate')).toBe(true); + expect(resolvePerMethod(undefined, 'generate')).toBeUndefined(); + expect(resolvePerMethod({}, 'generate')).toBe(undefined); }); }); describe('resolveDriverMeta — requireSubscription', () => { - it('reads the block off the decorator', () => { - @Driver('test-iface', { - name: 'decorated', - requireSubscription: { methods: { generate: true } }, - }) - class Decorated {} - - expect(resolveDriverMeta(new Decorated() as never)).toMatchObject({ - requireSubscription: { methods: { generate: true } }, - }); - }); - it('validates an imperatively declared block', () => { const driver = { driverInterface: 'test-iface', @@ -549,10 +454,12 @@ describe('resolveDriverMeta — requireSubscription', () => { // ── requireReputation ─────────────────────────────────────────────── -describe('validateDriverRequireReputation', () => { +describe('validatePerMethod: requireReputation', () => { it('returns an empty config for null/undefined input', () => { - expect(validateDriverRequireReputation(undefined, 't')).toEqual({}); - expect(validateDriverRequireReputation(null, 't')).toEqual({}); + expect(validatePerMethod(undefined, 't', 'requireReputation')).toEqual( + {}, + ); + expect(validatePerMethod(null, 't', 'requireReputation')).toEqual({}); }); it('accepts tier names and explicit opt-outs', () => { @@ -560,70 +467,54 @@ describe('validateDriverRequireReputation', () => { default: false, methods: { generate: 'standard' }, }; - expect(validateDriverRequireReputation(cfg, 't')).toBe(cfg); + expect(validatePerMethod(cfg, 't', 'requireReputation')).toBe(cfg); }); it('rejects a non-object config', () => { - expect(() => validateDriverRequireReputation(42, 't')).toThrow( + expect(() => validatePerMethod(42, 't', 'requireReputation')).toThrow( /requireReputation must be an object/, ); - expect(() => validateDriverRequireReputation([], 't')).toThrow( + expect(() => validatePerMethod([], 't', 'requireReputation')).toThrow( /requireReputation must be an object/, ); }); it('rejects requirements that name no tier', () => { expect(() => - validateDriverRequireReputation({ default: '' }, 't'), + validatePerMethod({ default: '' }, 't', 'requireReputation'), ).toThrow(/non-empty tier name/); expect(() => - validateDriverRequireReputation({ methods: { a: true } }, 't'), + validatePerMethod( + { methods: { a: true } }, + 't', + 'requireReputation', + ), ).toThrow(/expected a tier name, or false/); expect(() => - validateDriverRequireReputation({ methods: { a: 60 } }, 't'), + validatePerMethod({ methods: { a: 60 } }, 't', 'requireReputation'), ).toThrow(/expected a tier name, or false/); }); }); -describe('resolveDriverMethodRequireReputation', () => { +describe('resolvePerMethod: requireReputation', () => { const cfg: DriverRequireReputationConfig = { default: 'standard', methods: { list: false, generateLong: 'trusted' }, }; it('prefers a per-method entry over the default', () => { - expect(resolveDriverMethodRequireReputation(cfg, 'list')).toBe(false); - expect(resolveDriverMethodRequireReputation(cfg, 'generateLong')).toBe( - 'trusted', - ); + expect(resolvePerMethod(cfg, 'list')).toBe(false); + expect(resolvePerMethod(cfg, 'generateLong')).toBe('trusted'); }); it('falls back to the default, and to undefined with no config', () => { - expect(resolveDriverMethodRequireReputation(cfg, 'generate')).toBe( - 'standard', - ); - expect( - resolveDriverMethodRequireReputation(undefined, 'generate'), - ).toBeUndefined(); - expect( - resolveDriverMethodRequireReputation({}, 'generate'), - ).toBeUndefined(); + expect(resolvePerMethod(cfg, 'generate')).toBe('standard'); + expect(resolvePerMethod(undefined, 'generate')).toBeUndefined(); + expect(resolvePerMethod({}, 'generate')).toBeUndefined(); }); }); describe('resolveDriverMeta — requireReputation', () => { - it('reads the block off the decorator', () => { - @Driver('test-iface', { - name: 'decorated-reputation', - requireReputation: { methods: { generate: 'standard' } }, - }) - class Decorated {} - - expect(resolveDriverMeta(new Decorated() as never)).toMatchObject({ - requireReputation: { methods: { generate: 'standard' } }, - }); - }); - it('validates an imperatively declared block', () => { const driver = { driverInterface: 'test-iface', @@ -650,3 +541,18 @@ describe('resolveDriverMeta — requireReputation', () => { ).toBeUndefined(); }); }); + +describe('deprecated per-method resolver names', () => { + it('still resolve, for extensions that import them', () => { + const cfg = { + default: { limit: 1, window: 1_000 }, + methods: { send: { limit: 2, window: 1_000 } }, + }; + expect(resolveDriverMethodRateLimit(cfg, 'send')).toBe( + cfg.methods.send, + ); + expect( + resolveDriverMethodConcurrent({ default: { limit: 3 } }, 'x'), + ).toEqual({ limit: 3 }); + }); +}); diff --git a/src/backend/drivers/meta.ts b/src/backend/drivers/meta.ts index 5a159582d..41aeaa418 100644 --- a/src/backend/drivers/meta.ts +++ b/src/backend/drivers/meta.ts @@ -54,27 +54,18 @@ export function isDriverStreamResult(v: unknown): v is DriverStreamResult { ); } -// -- Driver metadata keys -------------------------------------------- +// -- Per-method driver policies ---------------------------------------- // -// Metadata keys stored on driver prototypes by the `@Driver` decorator. -// Imperative drivers set these as instance properties instead. +// Every policy a driver declares has the same shape: a `default` entry and +// per-method overrides. `DriverController` resolves the entry for the called +// method, so methods can differ in limits, plan and reputation floor. -export const DRIVER_INTERFACE_KEY = '__driverInterface' as const; -export const DRIVER_NAME_KEY = '__driverName' as const; -export const DRIVER_DEFAULT_KEY = '__driverDefault' as const; -export const DRIVER_ALIASES_KEY = '__driverAliases' as const; -export const DRIVER_RATE_LIMIT_KEY = '__driverRateLimit' as const; -export const DRIVER_CONCURRENT_KEY = '__driverConcurrent' as const; -export const DRIVER_NO_USER_SESSION_KEY = '__driverNoUserSession' as const; -export const DRIVER_REQUIRE_SUBSCRIPTION_KEY = - '__driverRequireSubscription' as const; -export const DRIVER_REQUIRE_REPUTATION_KEY = - '__driverRequireReputation' as const; - -// -- Driver rate-limit config ---------------------------------------- -// -// Declared per driver; `DriverController` resolves the spec for the requested -// method, so methods can differ in limits and storage backend. +/** A driver policy: `default` for any method not listed in `methods`. */ +export interface PerMethodConfig { + default?: T; + /** Keys are driver method names. */ + methods?: Record; +} export const RATE_LIMIT_BACKEND_NAMES = ['memory', 'redis', 'kv'] as const; export type RateLimitBackend = (typeof RATE_LIMIT_BACKEND_NAMES)[number]; @@ -90,42 +81,36 @@ export interface DriverRateLimitSpec { backend?: RateLimitBackend; } -export interface DriverRateLimitConfig { - /** Applied to any method not listed in `methods`. */ - default?: DriverRateLimitSpec; - /** Per-method overrides. Keys are driver method names. */ - methods?: Record; +export interface DriverConcurrentSpec { + /** Maximum simultaneous in-flight requests. */ + limit: number; + /** Per-`SubscriptionPolicy.id` overrides for `limit`. */ + bySubscription?: Record; + /** `memory` is per-process; use `redis` on multi-node deployments. */ + backend?: RateLimitBackend; } -/** Validate a driver's `rateLimit` block; throws at boot on a bad shape. */ -export function validateDriverRateLimit( +export type DriverRateLimitConfig = PerMethodConfig; +export type DriverConcurrentConfig = PerMethodConfig; +/** + * Per-method counterpart of `RouteOptions.requireSubscription`: `/drivers/call` + * is one shared route, so a route option would apply to every driver at once. + * `undefined` for a method means it is open to every plan. + */ +export type DriverRequireSubscriptionConfig = + PerMethodConfig; +/** + * Per-method counterpart of `RouteOptions.requireReputation`. A tier is only a + * name; the score it takes is deployment config. + */ +export type DriverRequireReputationConfig = + PerMethodConfig; + +function validateLimitSpec( value: unknown, label: string, -): DriverRateLimitConfig { - if (value == null) return {}; - if (typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`${label}: rateLimit must be an object`); - } - const cfg = value as Record; - if (cfg.default !== undefined) { - validateSpec(cfg.default, `${label}.rateLimit.default`); - } - if (cfg.methods !== undefined) { - if ( - typeof cfg.methods !== 'object' || - cfg.methods === null || - Array.isArray(cfg.methods) - ) { - throw new Error(`${label}.rateLimit.methods must be an object`); - } - for (const [name, spec] of Object.entries(cfg.methods)) { - validateSpec(spec, `${label}.rateLimit.methods.${name}`); - } - } - return cfg as DriverRateLimitConfig; -} - -function validateSpec(value: unknown, label: string): void { + { windowed }: { windowed: boolean }, +): void { if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new Error(`${label}: expected an object`); } @@ -138,9 +123,10 @@ function validateSpec(value: unknown, label: string): void { throw new Error(`${label}.limit: expected a positive number`); } if ( - typeof spec.window !== 'number' || - !Number.isFinite(spec.window) || - spec.window <= 0 + windowed && + (typeof spec.window !== 'number' || + !Number.isFinite(spec.window) || + spec.window <= 0) ) { throw new Error(`${label}.window: expected a positive number (ms)`); } @@ -172,52 +158,43 @@ function validateBySubscription(value: unknown, label: string): void { } } -/** - * Resolve the spec that applies to a given method on a driver. Per-method entry - * wins over `default`; returns `undefined` if neither is set so the caller can - * apply its own fallback. - */ -export function resolveDriverMethodRateLimit( - cfg: DriverRateLimitConfig | undefined, - method: string, -): DriverRateLimitSpec | undefined { - if (!cfg) return undefined; - return cfg.methods?.[method] ?? cfg.default; -} +/** The entry validator for each policy a driver may declare. */ +const POLICY_ENTRY_VALIDATORS = { + rateLimit: (value: unknown, label: string) => + validateLimitSpec(value, label, { windowed: true }), + concurrent: (value: unknown, label: string) => + validateLimitSpec(value, label, { windowed: false }), + requireSubscription: validateSubscriptionRequirement, + requireReputation: validateReputationRequirement, +}; -// -- Driver concurrent-limit config ---------------------------------- +export type DriverPolicyName = keyof typeof POLICY_ENTRY_VALIDATORS; -export interface DriverConcurrentSpec { - /** Maximum simultaneous in-flight requests. */ - limit: number; - /** Per-`SubscriptionPolicy.id` overrides for `limit`. */ - bySubscription?: Record; - /** `memory` is per-process; use `redis` on multi-node deployments. */ - backend?: RateLimitBackend; -} - -export interface DriverConcurrentConfig { - /** Applied to any method not listed in `methods`. */ - default?: DriverConcurrentSpec; - /** Per-method overrides. Keys are driver method names. */ - methods?: Record; +interface DriverPolicyConfigs { + rateLimit: DriverRateLimitConfig; + concurrent: DriverConcurrentConfig; + requireSubscription: DriverRequireSubscriptionConfig; + requireReputation: DriverRequireReputationConfig; } /** - * Validate a `concurrent` block. Mirrors `validateDriverRateLimit` — throws - * with a labelled path so a malformed entry surfaces at boot. + * Validate a driver's `policy` block. Throws with a labelled path at + * registration, so a malformed entry surfaces at boot rather than on the first + * call. */ -export function validateDriverConcurrent( +export function validatePerMethod( value: unknown, label: string, -): DriverConcurrentConfig { + policy: K, +): DriverPolicyConfigs[K] { if (value == null) return {}; if (typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`${label}: concurrent must be an object`); + throw new Error(`${label}: ${policy} must be an object`); } + const validateEntry = POLICY_ENTRY_VALIDATORS[policy]; const cfg = value as Record; if (cfg.default !== undefined) { - validateConcurrentSpec(cfg.default, `${label}.concurrent.default`); + validateEntry(cfg.default, `${label}.${policy}.default`); } if (cfg.methods !== undefined) { if ( @@ -225,193 +202,38 @@ export function validateDriverConcurrent( cfg.methods === null || Array.isArray(cfg.methods) ) { - throw new Error(`${label}.concurrent.methods must be an object`); + throw new Error(`${label}.${policy}.methods must be an object`); } - for (const [name, spec] of Object.entries(cfg.methods)) { - validateConcurrentSpec(spec, `${label}.concurrent.methods.${name}`); + for (const [name, entry] of Object.entries(cfg.methods)) { + validateEntry(entry, `${label}.${policy}.methods.${name}`); } } - return cfg as DriverConcurrentConfig; -} - -function validateConcurrentSpec(value: unknown, label: string): void { - if (typeof value !== 'object' || value === null || Array.isArray(value)) { - throw new Error(`${label}: expected an object`); - } - const spec = value as Record; - if ( - typeof spec.limit !== 'number' || - !Number.isFinite(spec.limit) || - spec.limit <= 0 - ) { - throw new Error(`${label}.limit: expected a positive number`); - } - if (spec.backend !== undefined) { - if ( - typeof spec.backend !== 'string' || - !RATE_LIMIT_BACKEND_NAMES.includes(spec.backend as RateLimitBackend) - ) { - throw new Error( - `${label}.backend: expected one of ${RATE_LIMIT_BACKEND_NAMES.join(', ')}`, - ); - } - } - if (spec.bySubscription !== undefined) { - validateBySubscription(spec.bySubscription, label); - } + return cfg as DriverPolicyConfigs[K]; } /** - * Resolve the concurrent spec for a given method on a driver. Same precedence - * as `resolveDriverMethodRateLimit`: per-method wins over `default`; - * `undefined` means no concurrency cap is declared, and the caller should leave - * the method unbounded. + * The entry that applies to `method`: its own, else `default`, else `undefined` + * (the caller decides what an undeclared policy means). */ -export function resolveDriverMethodConcurrent( - cfg: DriverConcurrentConfig | undefined, +export function resolvePerMethod( + cfg: PerMethodConfig | undefined, method: string, -): DriverConcurrentSpec | undefined { +): T | undefined { if (!cfg) return undefined; return cfg.methods?.[method] ?? cfg.default; } -// -- Driver subscription requirement --------------------------------- -// -// Per-method counterpart of `RouteOptions.requireSubscription`. It lives on the -// driver for the same reason `noUserSession` does: `/drivers/call` is one -// shared route, so a requirement declared in route options would apply to every -// driver at once. - -export interface DriverRequireSubscriptionConfig { - /** Applied to any method not listed in `methods`. */ - default?: SubscriptionRequirement; - /** Per-method overrides. Keys are driver method names. */ - methods?: Record; -} +/** @deprecated Use `resolvePerMethod`; kept for extensions still on it. */ +export const resolveDriverMethodRateLimit = + resolvePerMethod; +/** @deprecated Use `resolvePerMethod`; kept for extensions still on it. */ +export const resolveDriverMethodConcurrent = + resolvePerMethod; /** - * Validate a `requireSubscription` block. Same loud-at-boot contract as the - * rate-limit and concurrent validators. - */ -export function validateDriverRequireSubscription( - value: unknown, - label: string, -): DriverRequireSubscriptionConfig { - if (value == null) return {}; - if (typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`${label}: requireSubscription must be an object`); - } - const cfg = value as Record; - if (cfg.default !== undefined) { - validateSubscriptionRequirement( - cfg.default, - `${label}.requireSubscription.default`, - ); - } - if (cfg.methods !== undefined) { - if ( - typeof cfg.methods !== 'object' || - cfg.methods === null || - Array.isArray(cfg.methods) - ) { - throw new Error( - `${label}.requireSubscription.methods must be an object`, - ); - } - for (const [name, requirement] of Object.entries(cfg.methods)) { - validateSubscriptionRequirement( - requirement, - `${label}.requireSubscription.methods.${name}`, - ); - } - } - return cfg as DriverRequireSubscriptionConfig; -} - -/** - * Resolve the subscription requirement for a method. Same precedence as the - * other per-method resolvers: an entry in `methods` wins over `default`, and - * `undefined` means the method is open to every plan. - */ -export function resolveDriverMethodRequireSubscription( - cfg: DriverRequireSubscriptionConfig | undefined, - method: string, -): SubscriptionRequirement | undefined { - if (!cfg) return undefined; - return cfg.methods?.[method] ?? cfg.default; -} - -// -- Driver reputation requirement ----------------------------------- -// -// Per-method counterpart of `RouteOptions.requireReputation`, on the driver -// for the same reason the subscription block is: `/drivers/call` is one shared -// route, so a requirement in route options would apply to every driver at -// once. The tier named here is only a name — the score it takes is deployment -// config, so a driver never carries the number it is worth. - -export interface DriverRequireReputationConfig { - /** Applied to any method not listed in `methods`. */ - default?: ReputationRequirement; - /** Per-method overrides. Keys are driver method names. */ - methods?: Record; -} - -/** - * Validate a `requireReputation` block. Same loud-at-boot contract as the - * rate-limit, concurrent, and subscription validators. - */ -export function validateDriverRequireReputation( - value: unknown, - label: string, -): DriverRequireReputationConfig { - if (value == null) return {}; - if (typeof value !== 'object' || Array.isArray(value)) { - throw new Error(`${label}: requireReputation must be an object`); - } - const cfg = value as Record; - if (cfg.default !== undefined) { - validateReputationRequirement( - cfg.default, - `${label}.requireReputation.default`, - ); - } - if (cfg.methods !== undefined) { - if ( - typeof cfg.methods !== 'object' || - cfg.methods === null || - Array.isArray(cfg.methods) - ) { - throw new Error( - `${label}.requireReputation.methods must be an object`, - ); - } - for (const [name, requirement] of Object.entries(cfg.methods)) { - validateReputationRequirement( - requirement, - `${label}.requireReputation.methods.${name}`, - ); - } - } - return cfg as DriverRequireReputationConfig; -} - -/** - * Resolve the reputation requirement for a method. Same precedence as the other - * per-method resolvers: an entry in `methods` wins over `default`, and - * `undefined` means the method asks for no floor. - */ -export function resolveDriverMethodRequireReputation( - cfg: DriverRequireReputationConfig | undefined, - method: string, -): ReputationRequirement | undefined { - if (!cfg) return undefined; - return cfg.methods?.[method] ?? cfg.default; -} - -/** - * Resolved metadata for a registered driver, from decorator metadata or - * imperative instance properties. The gates live here rather than on a route - * because every driver shares the `/drivers/call` dispatch route. + * Resolved metadata for a registered driver, read from its instance properties. + * The gates live here rather than on a route because every driver shares the + * `/drivers/call` dispatch route. */ export interface DriverMeta { /** E.g. 'puter-chat-completion'. */ @@ -441,97 +263,33 @@ export interface DriverMeta { } /** - * Extract driver metadata from a driver instance. Checks decorator-set - * prototype metadata first, then falls back to instance properties. Returns - * `null` if the driver doesn't declare an interface. + * Extract a driver's metadata, validating its policy blocks so a malformed one + * fails loud at registration. Returns `null` if the driver doesn't declare an + * interface and name. */ export function resolveDriverMeta( driver: WithLifecycle & Record, ): DriverMeta | null { - const proto = Object.getPrototypeOf(driver) as Record; - - const interfaceName = - (proto[DRIVER_INTERFACE_KEY] as string | undefined) ?? - (driver.driverInterface as string | undefined); - const driverName = - (proto[DRIVER_NAME_KEY] as string | undefined) ?? - (driver.driverName as string | undefined); - const isDefault = - (proto[DRIVER_DEFAULT_KEY] as boolean | undefined) ?? - (driver.isDefault as boolean | undefined) ?? - false; - const aliases = - (proto[DRIVER_ALIASES_KEY] as string[] | undefined) ?? - (driver.driverAliases as string[] | undefined) ?? - []; - // Decorator stashes a validated config on the prototype; imperative - // drivers declare a raw object on the instance, which we validate here - // so a malformed `rateLimit` field still fails loud at registration. - const protoRateLimit = proto[DRIVER_RATE_LIMIT_KEY] as - DriverRateLimitConfig | undefined; - let rateLimit: DriverRateLimitConfig | undefined; - if (protoRateLimit) { - rateLimit = protoRateLimit; - } else if (driver.rateLimit !== undefined) { - rateLimit = validateDriverRateLimit( - driver.rateLimit, - `driver '${driverName ?? '(unnamed)'}'`, - ); - } - - const protoConcurrent = proto[DRIVER_CONCURRENT_KEY] as - DriverConcurrentConfig | undefined; - let concurrent: DriverConcurrentConfig | undefined; - if (protoConcurrent) { - concurrent = protoConcurrent; - } else if (driver.concurrent !== undefined) { - concurrent = validateDriverConcurrent( - driver.concurrent, - `driver '${driverName ?? '(unnamed)'}'`, - ); - } - - const protoRequireSubscription = proto[DRIVER_REQUIRE_SUBSCRIPTION_KEY] as - DriverRequireSubscriptionConfig | undefined; - let requireSubscription: DriverRequireSubscriptionConfig | undefined; - if (protoRequireSubscription) { - requireSubscription = protoRequireSubscription; - } else if (driver.requireSubscription !== undefined) { - requireSubscription = validateDriverRequireSubscription( - driver.requireSubscription, - `driver '${driverName ?? '(unnamed)'}'`, - ); - } - - const protoRequireReputation = proto[DRIVER_REQUIRE_REPUTATION_KEY] as - DriverRequireReputationConfig | undefined; - let requireReputation: DriverRequireReputationConfig | undefined; - if (protoRequireReputation) { - requireReputation = protoRequireReputation; - } else if (driver.requireReputation !== undefined) { - requireReputation = validateDriverRequireReputation( - driver.requireReputation, - `driver '${driverName ?? '(unnamed)'}'`, - ); - } - - const noUserSession = - (proto[DRIVER_NO_USER_SESSION_KEY] as boolean | undefined) ?? - (driver.noUserSession as boolean | undefined) ?? - false; - + const interfaceName = driver.driverInterface as string | undefined; + const driverName = driver.driverName as string | undefined; if (!interfaceName || !driverName) return null; + const label = `driver '${driverName}'`; + const policy = (name: K) => + driver[name] === undefined + ? undefined + : validatePerMethod(driver[name], label, name); + return { interfaceName, driverName, - isDefault, - aliases, - rateLimit, - concurrent, - noUserSession, - requireSubscription, - requireReputation, + isDefault: (driver.isDefault as boolean | undefined) ?? false, + aliases: (driver.driverAliases as string[] | undefined) ?? [], + rateLimit: policy('rateLimit'), + concurrent: policy('concurrent'), + noUserSession: (driver.noUserSession as boolean | undefined) ?? false, + requireSubscription: policy('requireSubscription'), + requireReputation: policy('requireReputation'), }; } diff --git a/src/backend/drivers/types.ts b/src/backend/drivers/types.ts index de0c10a01..4c281a4fe 100644 --- a/src/backend/drivers/types.ts +++ b/src/backend/drivers/types.ts @@ -47,41 +47,27 @@ export type IPuterDriver = /** * Base class for drivers. A driver implements a named interface (e.g. - * `puter-chat-completion`); several drivers may implement the same one. Declare - * it with `@Driver(interface, options)` or by setting the readonly fields below - * imperatively. + * `puter-chat-completion`); several drivers may implement the same one, and + * declares itself through the readonly fields below. */ export const PuterDriver = class PuterDriver implements WithCostsReporting { - /** The interface this driver implements. Set by `@Driver` or override. */ + /** The interface this driver implements. */ declare readonly driverInterface?: string; - /** Unique name within its interface. Set by `@Driver` or override. */ + /** Unique name within its interface. */ declare readonly driverName?: string; /** When true, this is the default driver for its interface. */ declare readonly isDefault?: boolean; - /** - * Rate-limit policy applied to RPC calls into this driver. Set by - * `@Driver({ rateLimit: ... })` or declared imperatively. See - * `DriverRateLimitConfig` in `./meta` for the shape. - */ + /** Rate-limit policy for RPC calls into this driver; see `./meta`. */ declare readonly rateLimit?: DriverRateLimitConfig; - /** - * Concurrent in-flight policy applied to RPC calls into this driver. Set by - * `@Driver({ concurrent: ... })` or declared imperatively. See - * `DriverConcurrentConfig` in `./meta` for the shape. - */ + /** In-flight cap for RPC calls into this driver; see `./meta`. */ declare readonly concurrent?: DriverConcurrentConfig; /** * When true, `/drivers/call` rejects bare account-session ("root") tokens * for this driver — callers need an app/worker token or a dashboard-minted - * API token. Set by `@Driver({ noUserSession: true })` or declared - * imperatively. See `DriverMeta.noUserSession` in `./meta`. + * API token. See `DriverMeta.noUserSession` in `./meta`. */ declare readonly noUserSession?: boolean; - /** - * Subscriber-only methods on this driver. Set by `@Driver({ - * requireSubscription: ... })` or declared imperatively. See - * `DriverRequireSubscriptionConfig` in `./meta` for the shape. - */ + /** Subscriber-only methods on this driver; see `./meta`. */ declare readonly requireSubscription?: DriverRequireSubscriptionConfig; constructor( diff --git a/src/backend/drivers/util/aiLimits.test.ts b/src/backend/drivers/util/aiLimits.test.ts index 9452f4fe7..ec0c12cb6 100644 --- a/src/backend/drivers/util/aiLimits.test.ts +++ b/src/backend/drivers/util/aiLimits.test.ts @@ -22,12 +22,7 @@ import { DEFAULT_FREE_SUBSCRIPTION, DEFAULT_TEMP_SUBSCRIPTION, } from '../../services/metering/consts.js'; -import { - resolveDriverMethodConcurrent, - resolveDriverMethodRateLimit, - validateDriverConcurrent, - validateDriverRateLimit, -} from '../meta.js'; +import { resolvePerMethod, validatePerMethod } from '../meta.js'; import { AI_CONCURRENT, AI_RATE_LIMIT } from './aiLimits.js'; // The shared AI policy is consumed verbatim by 8 drivers — these tests @@ -46,16 +41,16 @@ describe('AI_RATE_LIMIT', () => { }); }); - it('passes the same validator the @Driver decorator runs at boot', () => { + it('passes the validator drivers run at boot', () => { // If validation ever tightens, the AI policy must keep up. expect(() => - validateDriverRateLimit(AI_RATE_LIMIT, 'AI_RATE_LIMIT'), + validatePerMethod(AI_RATE_LIMIT, 'AI_RATE_LIMIT', 'rateLimit'), ).not.toThrow(); }); it('resolves the same spec for any method since only `default` is set', () => { - const a = resolveDriverMethodRateLimit(AI_RATE_LIMIT, 'complete'); - const b = resolveDriverMethodRateLimit(AI_RATE_LIMIT, 'generate'); + const a = resolvePerMethod(AI_RATE_LIMIT, 'complete'); + const b = resolvePerMethod(AI_RATE_LIMIT, 'generate'); expect(a).toEqual(b); expect(a).toBe(AI_RATE_LIMIT.default); }); @@ -83,17 +78,17 @@ describe('AI_CONCURRENT', () => { }); }); - it('passes the same validator the @Driver decorator runs at boot', () => { + it('passes the validator drivers run at boot', () => { expect(() => - validateDriverConcurrent(AI_CONCURRENT, 'AI_CONCURRENT'), + validatePerMethod(AI_CONCURRENT, 'AI_CONCURRENT', 'concurrent'), ).not.toThrow(); }); it('resolves the same spec for any method since only `default` is set', () => { - expect(resolveDriverMethodConcurrent(AI_CONCURRENT, 'complete')).toBe( + expect(resolvePerMethod(AI_CONCURRENT, 'complete')).toBe( AI_CONCURRENT.default, ); - expect(resolveDriverMethodConcurrent(AI_CONCURRENT, 'generate')).toBe( + expect(resolvePerMethod(AI_CONCURRENT, 'generate')).toBe( AI_CONCURRENT.default, ); });