diff --git a/src/backend/controllers/drivers/DriverController.test.ts b/src/backend/controllers/drivers/DriverController.test.ts index a1168ef3e..1a4d90dee 100644 --- a/src/backend/controllers/drivers/DriverController.test.ts +++ b/src/backend/controllers/drivers/DriverController.test.ts @@ -319,10 +319,11 @@ describe('DriverController.#handleCall (via captured router)', () => { ).rejects.toMatchObject({ statusCode: 404 }); }); - it('rejects a bare session ("root" token) actor on a noUserSession driver with a helpful 403', async () => { - // The AI drivers all set `noUserSession` — a session token must - // not double as an AI credential. The message has to point the - // caller at the credentials that DO work. + it('admits a bare session ("root" token) actor on the AI drivers', async () => { + // Privileged ("godmode") apps call AI with the user's own session + // token, so the AI drivers no longer set `noUserSession`: the + // session actor reaches the permission scan (403 forbidden — no + // perms granted) instead of the credential-shape rejection. const actor = await makeUserActor(); const req = makeReq( { @@ -342,12 +343,11 @@ describe('DriverController.#handleCall (via captured router)', () => { ), ).rejects.toMatchObject({ statusCode: 403, - legacyCode: 'app_or_api_token_required', - message: expect.stringMatching(/app or worker token|API token/i), + legacyCode: 'forbidden', }); }); - it('admits app and access-token actors past the noUserSession gate (they fail later on permission, not credential shape)', async () => { + it('admits app and access-token actors on the AI drivers (they fail later on permission, not credential shape)', async () => { const base = await makeUserActor(); const delegatedActors: Actor[] = [ { ...base, app: { uid: `app-${uuidv4()}` } }, @@ -387,7 +387,7 @@ describe('DriverController.#handleCall (via captured router)', () => { } }); - it('admits a user-scoped worker session past the noUserSession gate', async () => { + it('admits a user-scoped worker session on the AI drivers', async () => { // Workers deployed without an app binding authenticate as a user // actor whose session row is kind='worker' — they must keep their // AI access (they fail later on permission here, not on diff --git a/src/backend/controllers/drivers/DriverController.ts b/src/backend/controllers/drivers/DriverController.ts index caccdeb2b..56a8b637c 100644 --- a/src/backend/controllers/drivers/DriverController.ts +++ b/src/backend/controllers/drivers/DriverController.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import type { Request, Response } from 'express'; @@ -116,17 +117,17 @@ const translateProviderError = (err: unknown): unknown => { @Controller('/drivers') export class DriverController extends PuterController { - /** iface → Map */ + /** Iface → Map */ #drivers = new Map>(); - /** iface → default driver name */ + /** Iface → default driver name */ #defaults = new Map(); /** - * driver instance → resolved meta. Cached so the per-call rate-limit - * lookup doesn't have to walk prototype chains on every request. + * Driver instance → resolved meta. Cached so the per-call rate-limit lookup + * doesn't have to walk prototype chains on every request. */ #meta = new WeakMap(); /** - * driver instance → the set of method names callable via `/drivers/call`. + * Driver instance → the set of method names callable via `/drivers/call`. * Resolved once at registration (server startup) via * `resolveCallableMethods`; the request path only does a `Set.has` lookup. * This is what stops framework/lifecycle methods (`onServerStart`, etc.) @@ -236,7 +237,7 @@ export class DriverController extends PuterController { const driverMeta = this.#meta.get(driver); - // Drivers flagged `noUserSession` (the AI drivers) refuse the bare + // Drivers flagged `noUserSession` refuse the bare // account-session ("root") token: callers must present an app or // worker token, or an API token minted from the dashboard. This is // the per-driver counterpart of the `noUserSession` route option — diff --git a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts index 4cdc41801..90ebc3f8b 100644 --- a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts +++ b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import crypto from 'node:crypto'; @@ -72,13 +73,13 @@ type ProviderAttempt = { }; /** - * Capture what an upstream provider gave us so the classifier downstream - * can decide a user-facing status code instead of always returning 500. + * Capture what an upstream provider gave us so the classifier downstream can + * decide a user-facing status code instead of always returning 500. * - * OpenAI-SDK-based providers throw `APIError` with `.status` and a - * structured `.error` body — pull both. For arbitrary errors we fall - * back to the message and a status sniff so providers that throw plain - * `Error("... 503 ...")` strings still classify correctly. + * OpenAI-SDK-based providers throw `APIError` with `.status` and a structured + * `.error` body — pull both. For arbitrary errors we fall back to the message + * and a status sniff so providers that throw plain `Error("... 503 ...")` + * strings still classify correctly. */ const toAttempt = ( modelId: string, @@ -126,11 +127,12 @@ const isUpstream5xx = (a: ProviderAttempt) => * Map an exhausted fallback chain to a single user-facing HttpError. * * Per-class rules (see also alarm gate in server.ts): - * - all rate-limited → 429 `upstream_rate_limited` (paged: forced alert) - * - all auth failures → 500 `upstream_auth_failed` (paged: our config) - * - all upstream 5xx → 400 `upstream_provider_unavailable` (no page) - * - all upstream 4xx (other) → 400 `upstream_bad_request` (no page) - * - mixed → 400 `upstream_failed` (no page) + * + * - All rate-limited → 429 `upstream_rate_limited` (paged: forced alert) + * - All auth failures → 500 `upstream_auth_failed` (paged: our config) + * - All upstream 5xx → 400 `upstream_provider_unavailable` (no page) + * - All upstream 4xx (other) → 400 `upstream_bad_request` (no page) + * - Mixed → 400 `upstream_failed` (no page) */ const classifyAttempts = (attempts: ProviderAttempt[]): HttpError => { const fields = { attempts }; @@ -196,16 +198,15 @@ const classifyAttempts = (attempts: ProviderAttempt[]): HttpError => { /** * Driver implementing the `puter-chat-completion` interface. * - * Manages multiple upstream providers (Claude, OpenAI, …) and handles - * model resolution, provider routing, fallback on failure, and message - * normalisation. Each provider is a plain `IChatProvider` — the driver - * instantiates them from config on boot. + * Manages multiple upstream providers (Claude, OpenAI, …) and handles model + * resolution, provider routing, fallback on failure, and message normalisation. + * Each provider is a plain `IChatProvider` — the driver instantiates them from + * config on boot. * * Providers handle their own metering internally. */ export class ChatCompletionDriver extends PuterDriver { readonly driverInterface = 'puter-chat-completion'; - readonly noUserSession = true; readonly driverName = 'ai-chat'; readonly isDefault = true; diff --git a/src/backend/drivers/ai-image/ImageGenerationDriver.ts b/src/backend/drivers/ai-image/ImageGenerationDriver.ts index 4f75093b5..e682bb750 100644 --- a/src/backend/drivers/ai-image/ImageGenerationDriver.ts +++ b/src/backend/drivers/ai-image/ImageGenerationDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import crypto from 'node:crypto'; @@ -37,17 +38,16 @@ import type { IGenerateParams, IImageModel, IImageProvider } from './types.js'; /** * Driver implementing the `puter-image-generation` interface. * - * Manages multiple upstream providers and routes `generate()` calls - * based on the requested model. Mirrors ChatCompletionDriver's pattern: - * providers are instantiated from config on boot, a model map is built - * from each provider's declared models, and calls are dispatched. + * Manages multiple upstream providers and routes `generate()` calls based on + * the requested model. Mirrors ChatCompletionDriver's pattern: providers are + * instantiated from config on boot, a model map is built from each provider's + * declared models, and calls are dispatched. * - * Output is a URL string (web URL or data URI) — no streaming, no - * TypedValue wrapper. + * Output is a URL string (web URL or data URI) — no streaming, no TypedValue + * wrapper. */ export class ImageGenerationDriver extends PuterDriver { readonly driverInterface = 'puter-image-generation'; - readonly noUserSession = true; readonly driverName = 'ai-image'; // puter-js's `txt2img` falls through `options.driver` into the // driver-name slot (e.g. `xai-image-generation`), so alias all provider diff --git a/src/backend/drivers/ai-ocr/OCRDriver.ts b/src/backend/drivers/ai-ocr/OCRDriver.ts index c27ec1350..2c4f4b15a 100644 --- a/src/backend/drivers/ai-ocr/OCRDriver.ts +++ b/src/backend/drivers/ai-ocr/OCRDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import { @@ -33,9 +34,9 @@ import { loadFileInput, type LoadedFile } from '../util/fileInput.js'; import { OCR_COSTS } from './costs.js'; /** - * Driver implementing `puter-ocr` — document OCR. Two providers: - * • `aws-textract` — AWS Textract (region-aware clients; direct S3 source when available) - * • `mistral` — Mistral OCR (URL/data-URL based) + * Driver implementing `puter-ocr` — document OCR. Two providers: • + * `aws-textract` — AWS Textract (region-aware clients; direct S3 source when + * available) • `mistral` — Mistral OCR (URL/data-URL based) */ interface RecognizeArgs { source?: unknown; @@ -79,7 +80,6 @@ interface MistralOcrClient { export class OCRDriver extends PuterDriver { readonly driverInterface = 'puter-ocr'; - readonly noUserSession = true; readonly driverName = 'ai-ocr'; // Shared AI policy — see `drivers/util/aiLimits.ts` for the tier table. @@ -117,9 +117,11 @@ export class OCRDriver extends PuterDriver { const providers = this.config.providers ?? {}; const textract = providers['aws-textract'] as - Record | undefined; + | Record + | undefined; const textractAws = (textract?.aws ?? textract) as - Record | undefined; + | Record + | undefined; const textractAccessKey = textractAws?.access_key as string | undefined; const textractSecretKey = textractAws?.secret_key as string | undefined; const textractRegion = diff --git a/src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts b/src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts index b35f23b4d..ddb21e943 100644 --- a/src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts +++ b/src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import { Readable } from 'node:stream'; @@ -27,8 +28,8 @@ import { loadFileInput } from '../util/fileInput.js'; import { VOICE_CHANGER_COSTS } from './costs.js'; /** - * Driver implementing `puter-speech2speech` — voice changer. Currently a - * single provider (ElevenLabs). + * Driver implementing `puter-speech2speech` — voice changer. Currently a single + * provider (ElevenLabs). */ const DEFAULT_MODEL = 'eleven_multilingual_sts_v2'; @@ -57,7 +58,6 @@ interface ConvertArgs { export class VoiceChangerDriver extends PuterDriver { readonly driverInterface = 'puter-speech2speech'; - readonly noUserSession = true; readonly driverName = 'elevenlabs-voice-changer'; readonly isDefault = true; diff --git a/src/backend/drivers/ai-speech2txt/SpeechToTextDriver.ts b/src/backend/drivers/ai-speech2txt/SpeechToTextDriver.ts index 88932377b..2a6f6b664 100644 --- a/src/backend/drivers/ai-speech2txt/SpeechToTextDriver.ts +++ b/src/backend/drivers/ai-speech2txt/SpeechToTextDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import OpenAI, { toFile } from 'openai'; @@ -26,8 +27,8 @@ import { loadFileInput } from '../util/fileInput.js'; import { SPEECH_TO_TEXT_COSTS } from './costs.js'; /** - * Driver implementing `puter-speech2txt`. Wraps OpenAI's audio API - * (Whisper + GPT-4o transcribe models) for transcription and translation. + * Driver implementing `puter-speech2txt`. Wraps OpenAI's audio API (Whisper + + * GPT-4o transcribe models) for transcription and translation. * * `file` may be a path, uid/uuid ref, or data URL. */ @@ -100,7 +101,6 @@ interface TranscribeArgs { export class SpeechToTextDriver extends PuterDriver { readonly driverInterface = 'puter-speech2txt'; - readonly noUserSession = true; readonly driverName = 'openai-speech2txt'; readonly isDefault = true; diff --git a/src/backend/drivers/ai-speech2txt/XAISpeechToTextDriver.ts b/src/backend/drivers/ai-speech2txt/XAISpeechToTextDriver.ts index 4ab09d05a..1e0e6640e 100644 --- a/src/backend/drivers/ai-speech2txt/XAISpeechToTextDriver.ts +++ b/src/backend/drivers/ai-speech2txt/XAISpeechToTextDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import { Context } from '../../core/context.js'; @@ -26,10 +27,11 @@ import { loadFileInput } from '../util/fileInput.js'; /** * Driver implementing `puter-speech2txt` for the xAI (Grok) STT API. * - * Uses the xAI /v1/stt REST endpoint which accepts multipart/form-data - * with an audio file and returns a JSON transcript with word-level timestamps. + * Uses the xAI /v1/stt REST endpoint which accepts multipart/form-data with an + * audio file and returns a JSON transcript with word-level timestamps. * - * Pricing: $0.10/hr REST = 10 cents/hr = 10 * 1_000_000 / 3600 ≈ 2778 microcents/second + * Pricing: $0.10/hr REST = 10 cents/hr = 10 * 1_000_000 / 3600 ≈ 2778 + * microcents/second */ const API_BASE = 'https://api.x.ai/v1'; @@ -66,7 +68,6 @@ interface TranscribeArgs { export class XAISpeechToTextDriver extends PuterDriver { readonly driverInterface = 'puter-speech2txt'; - readonly noUserSession = true; readonly driverName = 'xai-speech2txt'; // Shared AI policy — see `drivers/util/aiLimits.ts` for the tier table. diff --git a/src/backend/drivers/ai-tts/TTSDriver.ts b/src/backend/drivers/ai-tts/TTSDriver.ts index 591045c48..0cb085985 100644 --- a/src/backend/drivers/ai-tts/TTSDriver.ts +++ b/src/backend/drivers/ai-tts/TTSDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import { Context } from '../../core/context.js'; @@ -37,10 +38,9 @@ import type { /** * Driver implementing the `puter-tts` interface. * - * Manages multiple upstream TTS providers (OpenAI, ElevenLabs, AWS Polly) - * and handles provider routing, voice/engine aggregation, and speech - * synthesis. Each provider is an `ITTSProvider` instantiated from config - * on boot. + * Manages multiple upstream TTS providers (OpenAI, ElevenLabs, AWS Polly) and + * handles provider routing, voice/engine aggregation, and speech synthesis. + * Each provider is an `ITTSProvider` instantiated from config on boot. */ // puter-js still routes TTS via legacy per-provider driver names rather // than passing `{ provider }` in args, so alias the unified driver under @@ -64,7 +64,6 @@ const ALIAS_TO_PROVIDER: Record = { export class TTSDriver extends PuterDriver { readonly driverInterface = 'puter-tts'; - readonly noUserSession = true; readonly driverName = 'ai-tts'; readonly driverAliases = [...TTS_ALIASES]; readonly isDefault = true; @@ -88,9 +87,7 @@ export class TTSDriver extends PuterDriver { // -- Interface methods ------------------------------------------- - /** - * List all available voices across all configured providers. - */ + /** List all available voices across all configured providers. */ async list_voices(args?: Record): Promise { const provider = (args?.provider as string | undefined) ?? this.#providerFromAlias(); @@ -109,9 +106,7 @@ export class TTSDriver extends PuterDriver { return allVoices; } - /** - * List all available engines/models across all configured providers. - */ + /** List all available engines/models across all configured providers. */ async list_engines(args?: Record): Promise { const provider = (args?.provider as string | undefined) ?? this.#providerFromAlias(); @@ -130,9 +125,7 @@ export class TTSDriver extends PuterDriver { return allEngines; } - /** - * List provider names that are currently configured. - */ + /** List provider names that are currently configured. */ async list(): Promise { return Object.keys(this.#providers); } @@ -158,9 +151,8 @@ export class TTSDriver extends PuterDriver { } /** - * Synthesize speech from text. Routes to the appropriate provider - * based on the `provider` argument, or falls back to the first - * available provider. + * Synthesize speech from text. Routes to the appropriate provider based on + * the `provider` argument, or falls back to the first available provider. */ async synthesize( args: ISynthesizeArgs, diff --git a/src/backend/drivers/ai-video/VideoGenerationDriver.ts b/src/backend/drivers/ai-video/VideoGenerationDriver.ts index 1e1b38800..a4497dc09 100644 --- a/src/backend/drivers/ai-video/VideoGenerationDriver.ts +++ b/src/backend/drivers/ai-video/VideoGenerationDriver.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import { posix as pathPosix } from 'node:path'; @@ -48,7 +49,6 @@ const DEFAULT_PROVIDER = 'openai-video-generation'; */ export class VideoGenerationDriver extends PuterDriver { readonly driverInterface = 'puter-video-generation'; - readonly noUserSession = true; readonly driverName = 'ai-video'; // puter-js's `txt2vid` can pass a provider id via `options.driver`, so // alias all provider ids here. `generate` falls back to diff --git a/src/backend/drivers/driverPolicies.test.ts b/src/backend/drivers/driverPolicies.test.ts index d9cd237c0..66a97db66 100644 --- a/src/backend/drivers/driverPolicies.test.ts +++ b/src/backend/drivers/driverPolicies.test.ts @@ -151,11 +151,11 @@ describe.each([ expect(m.concurrent).toBe(AI_CONCURRENT); }); - it('refuses bare account-session ("root") tokens', () => { - // AI calls need a delegated credential — an app/worker token or a - // dashboard-minted API token. `DriverController` enforces this off - // the meta flag; dropping it silently reopens session-token AI use. - expect(m.noUserSession).toBe(true); + it('accepts bare account-session ("root") tokens', () => { + // Privileged ("godmode") apps run on the user's own session token + // rather than an app token, so the AI drivers can't distinguish + // them from a browser session and have to admit both. + expect(m.noUserSession).toBe(false); }); }); diff --git a/src/backend/drivers/meta.ts b/src/backend/drivers/meta.ts index 8ed42c97e..0dae932b0 100644 --- a/src/backend/drivers/meta.ts +++ b/src/backend/drivers/meta.ts @@ -3,18 +3,19 @@ * * 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. + * 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. + * 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 . + * along with this program. If not, see + * [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/). */ import type { Readable } from 'node:stream'; @@ -78,24 +79,24 @@ export interface DriverRateLimitSpec { /** Window length, in milliseconds. */ window: number; /** - * Per-subscription overrides for `limit`. Keyed by - * `SubscriptionPolicy.id` (`user_free`, `temp_free`, `unlimited`, - * etc.). Falls back to `limit` when the actor's subscription isn't - * in the map. Same mechanic as `DriverConcurrentSpec.bySubscription`. + * Per-subscription overrides for `limit`. Keyed by `SubscriptionPolicy.id` + * (`user_free`, `temp_free`, `unlimited`, etc.). Falls back to `limit` when + * the actor's subscription isn't in the map. Same mechanic as + * `DriverConcurrentSpec.bySubscription`. */ bySubscription?: Record; /** - * Storage backend to count against. Omit to use the server-wide - * default configured by `config.rate_limit.backend`. + * Storage backend to count against. Omit to use the server-wide default + * configured by `config.rate_limit.backend`. */ backend?: RateLimitBackend; } export interface DriverRateLimitConfig { /** - * Applied to any method not listed in `methods`. Lets a driver opt - * the whole interface into tighter limits than the global driver - * default without enumerating every method. + * Applied to any method not listed in `methods`. Lets a driver opt the + * whole interface into tighter limits than the global driver default + * without enumerating every method. */ default?: DriverRateLimitSpec; /** Per-method overrides. Keys are driver method names. */ @@ -103,10 +104,9 @@ export interface DriverRateLimitConfig { } /** - * Validate a `rateLimit` block declared by a driver. Throws on bad - * shape so registration fails loudly at boot rather than silently - * misconfiguring production traffic. Returns the value unchanged on - * success for chaining. + * Validate a `rateLimit` block declared by a driver. Throws on bad shape so + * registration fails loudly at boot rather than silently misconfiguring + * production traffic. Returns the value unchanged on success for chaining. */ export function validateDriverRateLimit( value: unknown, @@ -183,9 +183,9 @@ 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. + * 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, @@ -205,15 +205,15 @@ export interface DriverConcurrentSpec { /** Maximum simultaneous in-flight requests. */ limit: number; /** - * Per-subscription overrides keyed by `SubscriptionPolicy.id` - * (`user_free`, `temp_free`, `unlimited`, etc.). Falls back to - * `limit` when the actor's subscription isn't in the map. + * Per-subscription overrides keyed by `SubscriptionPolicy.id` (`user_free`, + * `temp_free`, `unlimited`, etc.). Falls back to `limit` when the actor's + * subscription isn't in the map. */ bySubscription?: Record; /** * Storage backend. Memory is per-process (use only on single-node - * deployments); redis coordinates across nodes; kv is rarely the - * right choice for concurrency but supported for parity. + * deployments); redis coordinates across nodes; kv is rarely the right + * choice for concurrency but supported for parity. */ backend?: RateLimitBackend; } @@ -226,8 +226,8 @@ export interface DriverConcurrentConfig { } /** - * Validate a `concurrent` block. Mirrors `validateDriverRateLimit` — - * throws with a labelled path so a malformed entry surfaces at boot. + * Validate a `concurrent` block. Mirrors `validateDriverRateLimit` — throws + * with a labelled path so a malformed entry surfaces at boot. */ export function validateDriverConcurrent( value: unknown, @@ -284,10 +284,10 @@ function validateConcurrentSpec(value: unknown, label: string): void { } /** - * 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. + * 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. */ export function resolveDriverMethodConcurrent( cfg: DriverConcurrentConfig | undefined, @@ -320,31 +320,30 @@ export interface DriverMeta { /** * Rate-limit policy for this driver. Per-method specs override the * `default` spec; both are optional. `DriverController` consults this - * before invoking the method and falls back to the global driver - * default (600/min) if nothing is declared. + * before invoking the method and falls back to the global driver default + * (600/min) if nothing is declared. */ rateLimit?: DriverRateLimitConfig; /** - * Concurrent in-flight policy for this driver. When set, the - * controller acquires a slot before invoking the method and releases - * in `finally`. Absent → no concurrency cap (current behaviour). + * Concurrent in-flight policy for this driver. When set, the controller + * acquires a slot before invoking the method and releases in `finally`. + * Absent → no concurrency cap (current behaviour). */ concurrent?: DriverConcurrentConfig; /** - * When true, `/drivers/call` rejects bare account-session ("root") - * tokens for this driver: callers must present an app/worker token or - * a dashboard-minted API token. Per-driver counterpart of the - * `noUserSession` route option — the dispatch route is shared, so the - * flag lives on the driver. Set on the AI drivers so a copied session - * token can't double as an AI credential. + * When true, `/drivers/call` rejects bare account-session ("root") tokens + * for this driver: callers must present an app/worker token or a + * dashboard-minted API token. Per-driver counterpart of the `noUserSession` + * route option — the dispatch route is shared, so the flag lives on the + * driver. */ noUserSession?: boolean; } /** * 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. + * prototype metadata first, then falls back to instance properties. Returns + * `null` if the driver doesn't declare an interface. */ export function resolveDriverMeta( driver: WithLifecycle & Record, @@ -414,15 +413,15 @@ export function resolveDriverMeta( /** * Framework/lifecycle method names that must never be reachable via - * `/drivers/call`. These live on `PuterDriver` (see `drivers/types.ts`) and - * are the machinery the dispatch surface must exclude. For class-based - * drivers a concrete `override` of one still carries the same name and is - * caught here; for plain-object drivers (registered by extensions — see - * `server.ts`, `typeof DriverClass === 'object'`) there is no base prototype - * to distinguish them, so this denylist is the *only* thing keeping a - * hook off the RPC surface. Any lifecycle hook added to `PuterDriver` must - * be added here too — the per-driver guard test (`callableMethods.test.ts`) - * fails loudly if a base method starts leaking into every driver's surface. + * `/drivers/call`. These live on `PuterDriver` (see `drivers/types.ts`) and are + * the machinery the dispatch surface must exclude. For class-based drivers a + * concrete `override` of one still carries the same name and is caught here; + * for plain-object drivers (registered by extensions — see `server.ts`, `typeof + * DriverClass === 'object'`) there is no base prototype to distinguish them, so + * this denylist is the _only_ thing keeping a hook off the RPC surface. Any + * lifecycle hook added to `PuterDriver` must be added here too — the per-driver + * guard test (`callableMethods.test.ts`) fails loudly if a base method starts + * leaking into every driver's surface. */ export const RESERVED_DRIVER_METHODS: ReadonlySet = new Set([ 'onServerStart', @@ -437,16 +436,16 @@ export const RESERVED_DRIVER_METHODS: ReadonlySet = new Set([ * The RPC surface is defined structurally rather than by a hand-maintained * per-method allow-list. Walking from the instance up to (but not including) * `Object.prototype`, a name is callable iff it resolves to a function and is - * neither `constructor` nor a `RESERVED_DRIVER_METHODS` entry. This covers - * both driver shapes the server accepts (`server.ts`): class instances (RPC - * methods on the concrete prototype, config on the instance) and plain - * objects (everything own, used verbatim by extensions). It excludes all + * neither `constructor` nor a `RESERVED_DRIVER_METHODS` entry. This covers both + * driver shapes the server accepts (`server.ts`): class instances (RPC methods + * on the concrete prototype, config on the instance) and plain objects + * (everything own, used verbatim by extensions). It excludes all * `Object.prototype` members (`toString`, `valueOf`, `__proto__`, …), the * `constructor`, and the lifecycle hooks. * * `#`-private helpers need no handling: they are not real property keys, so * `getOwnPropertyNames` never lists them and `driver['#x']` is `undefined`. - * Only *plain* public methods can appear here. + * Only _plain_ public methods can appear here. * * Getters are excluded (we read the descriptor's `.value`, never access the * property), so evaluating this set never runs driver code. Intended to be diff --git a/src/backend/testUtil.ts b/src/backend/testUtil.ts index 8f760648d..a6cf752f2 100644 --- a/src/backend/testUtil.ts +++ b/src/backend/testUtil.ts @@ -76,13 +76,13 @@ export const createPgMockPostgresDatabaseClient = async ( /** * When `PUTER_TEST_DB_ENGINE=postgres` is set, `setupTestServer` swaps its - * default sqlite test database for an in-memory Postgres backed by pgmock - * (with the bundled Postgres migrations applied on boot). Tests that - * explicitly override `database` still win — the env var only affects the - * implicit default used by callers that don't pass any DB overrides. + * default sqlite test database for an in-memory Postgres backed by pgmock (with + * the bundled Postgres migrations applied on boot). Tests that explicitly + * override `database` still win — the env var only affects the implicit default + * used by callers that don't pass any DB overrides. * - * Recognized values: `postgres` → pgmock. Anything else (including unset) → - * the original sqlite-in-memory default. + * Recognized values: `postgres` → pgmock. Anything else (including unset) → the + * original sqlite-in-memory default. */ const testDatabaseDefault = (): IConfig['database'] => { const engine = (process.env.PUTER_TEST_DB_ENGINE ?? '').toLowerCase(); @@ -105,10 +105,10 @@ export type SetupTestServerOptions = { }; /** - * Grab a free port by binding to 0 and releasing it. Done up-front (rather - * than letting the server listen on 0) so the port is known while building - * config — `origin` / `api_base_url` consumers like LocalWorkerService read - * it at construction time. + * Grab a free port by binding to 0 and releasing it. Done up-front (rather than + * letting the server listen on 0) so the port is known while building config — + * `origin` / `api_base_url` consumers like LocalWorkerService read it at + * construction time. */ export const allocateEphemeralPort = (): Promise => new Promise((resolve, reject) => { @@ -210,25 +210,25 @@ export type TestUserCredentials = { password: string; token: string; /** - * Full-access access token (what the dashboard's "API Token" flow - * mints). AI surfaces reject bare session tokens (`noUserSession`), - * so suites exercising them authenticate with this instead. + * Full-access access token (what the dashboard's "API Token" flow mints). + * The `/puterai/*` wire routes reject bare session tokens + * (`noUserSession`), so suites exercising them authenticate with this. */ apiToken: string; /** - * User-scoped worker session token (what deploying a worker with no - * app binding mints — `kind='worker'` session row). Never treated as - * a root token: suites use it to prove worker credentials pass the - * `noUserSession` gates. + * User-scoped worker session token (what deploying a worker with no app + * binding mints — `kind='worker'` session row). Never treated as a root + * token: suites use it to prove worker credentials pass the `noUserSession` + * gates. */ workerToken: string; }; /** - * Seed a user with a known password directly through the stores (same steps - * as DefaultUserService's admin bootstrap: bcrypt-hashed password, home - * directory tree, optional admin-group membership) and mint a session token - * the same way `POST /login` does. + * Seed a user with a known password directly through the stores (same steps as + * DefaultUserService's admin bootstrap: bcrypt-hashed password, home directory + * tree, optional admin-group membership) and mint a session token the same way + * `POST /login` does. */ export const createTestUser = async ( server: PuterServer, @@ -292,20 +292,20 @@ export const createTestUser = async ( export type PuterTestEnv = { /** - * Root origin (`http://puter.localhost:`) — GUI and root-only - * routes like `POST /login` live here. + * Root origin (`http://puter.localhost:`) — GUI and root-only routes + * like `POST /login` live here. */ origin: string; /** - * API origin (`http://api.puter.localhost:`) — what puter.js - * clients use as their APIOrigin. Routes gated on the `api` subdomain - * (e.g. `/whoami`) only match this host. + * API origin (`http://api.puter.localhost:`) — what puter.js clients + * use as their APIOrigin. Routes gated on the `api` subdomain (e.g. + * `/whoami`) only match this host. */ apiOrigin: string; /** - * Seeded accounts: an admin and two regular (non-privileged) users. - * `other` exists so suites can exercise cross-user flows (permission - * grants, access denials) without creating users on the fly. + * Seeded accounts: an admin and two regular (non-privileged) users. `other` + * exists so suites can exercise cross-user flows (permission grants, access + * denials) without creating users on the fly. */ users: { admin: TestUserCredentials; @@ -332,8 +332,8 @@ export const TEST_OTHER_USER_CREDENTIALS = { /** * Boot an in-memory Puter server on a real ephemeral port with deterministic * credentials, for client test runners (puter.js on node, browsers, workerd). - * Clients can authenticate with the pre-minted tokens or via a real - * `POST /login` using the fixed passwords — no stdout scraping. + * Clients can authenticate with the pre-minted tokens or via a real `POST + * /login` using the fixed passwords — no stdout scraping. */ export const setupPuterTestEnv = async ( configOverrides?: IConfig, diff --git a/src/puter-js/tests/api/suites/ai.suite.ts b/src/puter-js/tests/api/suites/ai.suite.ts index ae15795fc..6347b2bc6 100644 --- a/src/puter-js/tests/api/suites/ai.suite.ts +++ b/src/puter-js/tests/api/suites/ai.suite.ts @@ -8,10 +8,10 @@ import { suite, type TestContext } from '../harness/types.ts'; */ /** - * The AI drivers reject bare session tokens (`noUserSession` driver meta): - * programmatic AI callers must hold an app/worker token or a - * dashboard-minted API token. Authenticate each AI test the way a real - * caller would — with the full-access API token. The harness re-issues the + * The `/puterai/*` wire routes still require a delegated credential, so + * these tests authenticate the way a programmatic caller would — with the + * full-access API token. (The drivers themselves also take a plain session + * token; that path has its own test below.) The harness re-issues the * session token to shared SDK instances between tests, so no restore is * needed here. */ @@ -190,9 +190,10 @@ export default suite('ai', { } }, - 'a bare session token cannot call the AI driver': async (t) => { - // No useApiToken here — the point is that the account session - // ("root") token is rejected with guidance toward app/API tokens. + 'a bare session token can call the AI driver': async (t) => { + // No useApiToken here — privileged ("godmode") apps run on the + // user's own account session token, so the driver has to accept + // it. The `/puterai/*` wire routes still don't (see below). const res = await fetch(`${t.env.apiOrigin}/drivers/call`, { method: 'POST', headers: { @@ -209,22 +210,25 @@ export default suite('ai', { }, }), }); - t.assert.equal(res.status, 403, 'session token should be rejected'); const body = JSON.stringify(await res.json()); + t.assert.equal( + res.status, + 200, + `session token should reach the driver, got ${res.status}: ${body}`, + ); t.assert.ok( - body.includes('app_or_api_token_required'), - `rejection should carry app_or_api_token_required, got ${body}`, + !body.includes('app_or_api_token_required'), + `session token must not be rejected by credential shape, got ${body}`, ); }, - 'a worker token passes the AI credential gate': async (t) => { + 'a worker token can call the AI driver': async (t) => { // Workers are never treated as root tokens. This uses a REAL // user-scoped worker session token (minted the same way an // app-less worker deployment mints one), so the whole middleware // path is exercised: JWT → session row (kind='worker') → actor → - // noUserSession gate. Calling the driver's `models` method keeps - // this free of any AI inference — the gate rejects by credential - // shape before the handler, so a 200 here proves admission. + // driver. Calling the driver's `models` method keeps this free of + // any AI inference. const res = await fetch(`${t.env.apiOrigin}/drivers/call`, { method: 'POST', headers: {