mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-07 12:21:56 +00:00
fix: allow root token to also call ai drivers (#3442)
This commit is contained in:
1 parent
e6531d4357
commit
6c4fa629a9
14 files changed
+265
-265
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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<driverName, driverInstance> */
|
||||
/** Iface → Map<driverName, driverInstance> */
|
||||
#drivers = new Map<string, Map<string, DriverInstance>>();
|
||||
/** iface → default driver name */
|
||||
/** Iface → default driver name */
|
||||
#defaults = new Map<string, string>();
|
||||
/**
|
||||
* 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<DriverInstance, DriverMeta>();
|
||||
/**
|
||||
* 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 —
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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<string, unknown> | undefined;
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const textractAws = (textract?.aws ?? textract) as
|
||||
Record<string, unknown> | undefined;
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const textractAccessKey = textractAws?.access_key as string | undefined;
|
||||
const textractSecretKey = textractAws?.secret_key as string | undefined;
|
||||
const textractRegion =
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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.
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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<TTSAlias, string> = {
|
||||
|
||||
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<string, unknown>): Promise<ITTSVoice[]> {
|
||||
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<string, unknown>): Promise<ITTSEngine[]> {
|
||||
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<string[]> {
|
||||
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,
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+62
-63
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
* 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<string, number>;
|
||||
/**
|
||||
* 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<string, number>;
|
||||
/**
|
||||
* 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<string, unknown>,
|
||||
@@ -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<string> = new Set([
|
||||
'onServerStart',
|
||||
@@ -437,16 +436,16 @@ export const RESERVED_DRIVER_METHODS: ReadonlySet<string> = 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
|
||||
|
||||
+31
-31
@@ -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<number> =>
|
||||
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:<port>`) — GUI and root-only
|
||||
* routes like `POST /login` live here.
|
||||
* Root origin (`http://puter.localhost:<port>`) — GUI and root-only routes
|
||||
* like `POST /login` live here.
|
||||
*/
|
||||
origin: string;
|
||||
/**
|
||||
* API origin (`http://api.puter.localhost:<port>`) — 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:<port>`) — 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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user