🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@ai-sdk/gateway

Package Overview
Dependencies
Maintainers
3
Versions
516
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ai-sdk/gateway - npm Package Compare versions

Comparing version
4.0.0-beta.108
to
4.0.0-beta.109
+20
-0
CHANGELOG.md
# @ai-sdk/gateway
## 4.0.0-beta.109
### Patch Changes
- 15eb253: feat(gateway): mint short-lived client secrets for experimental realtime
`gateway.experimental_realtime.getToken()` now mints a single-use, short-lived
client secret (`vcst_`) via the Gateway's `POST /v1/realtime/client-secrets`
endpoint instead of returning the long-lived Gateway credential. The customer's
server calls `getToken()` and hands the returned token to the browser, which
opens the realtime WebSocket with it through the existing
`ai-gateway-auth.<token>` subprotocol — the API key / OIDC token never reaches
the client. `expiresAfterSeconds` is forwarded to the mint endpoint and the
returned `expiresAt` is surfaced on the result.
The server-environment guard moves from realtime model construction to minting:
the browser can now build the realtime event codec it needs to drive the
transport, while minting (which requires the Gateway credential) stays
server-side.
## 4.0.0-beta.108

@@ -4,0 +24,0 @@

+1
-1
{
"name": "@ai-sdk/gateway",
"private": false,
"version": "4.0.0-beta.108",
"version": "4.0.0-beta.109",
"type": "module",

@@ -6,0 +6,0 @@ "license": "Apache-2.0",

import {
createJsonErrorResponseHandler,
createJsonResponseHandler,
loadOptionalSetting,
postJsonToApi,
withoutTrailingSlash,

@@ -7,2 +10,3 @@ withUserAgentSuffix,

} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
import { asGatewayError, GatewayAuthenticationError } from './errors';

@@ -223,2 +227,8 @@ import {

/** Response shape of `POST /v1/realtime/client-secrets`. `expiresAt` is epoch seconds. */
const gatewayClientSecretResponseSchema = z.object({
token: z.string(),
expiresAt: z.number().nullish(),
});
/**

@@ -283,2 +293,45 @@ * Create a remote provider instance.

// Mints a short-lived realtime client secret (`vcst_`) via the Gateway's
// `/v1/realtime/client-secrets` route, authenticated with the long-lived
// Gateway credential. Server-side only (asserted) — the credential never
// belongs in a browser; the browser receives only the minted token. The
// mint route lives at the gateway origin's `/v1/realtime/client-secrets`,
// not under the realtime `baseURL` path (which targets `/v4/ai`), so the
// URL is resolved against the origin.
const mintRealtimeClientSecret = async (params: {
modelId: string;
expiresAfterSeconds?: number;
}): Promise<{ token: string; expiresAt?: number }> => {
assertGatewayRealtimeServerEnvironment();
const auth = await getRealtimeAuthToken();
const headers = createAuthHeaders(auth);
const url = new URL('/v1/realtime/client-secrets', baseURL).toString();
try {
const { value } = await postJsonToApi({
url,
headers,
body: {
model: params.modelId,
...(params.expiresAfterSeconds != null && {
expiresIn: params.expiresAfterSeconds,
}),
},
successfulResponseHandler: createJsonResponseHandler(
gatewayClientSecretResponseSchema,
),
failedResponseHandler: createJsonErrorResponseHandler({
errorSchema: z.any(),
errorToMessage: data => data,
}),
fetch: options.fetch,
});
return {
token: value.token,
...(value.expiresAt != null && { expiresAt: value.expiresAt }),
};
} catch (error) {
throw await asGatewayError(error, await parseAuthMethod(headers));
}
};
const createO11yHeaders = () => {

@@ -474,11 +527,14 @@ const deploymentId = loadOptionalSetting({

const createRealtimeModel = (modelId: GatewayRealtimeModelId) => {
assertGatewayRealtimeServerEnvironment();
return new GatewayRealtimeModel(modelId, {
// No server-environment guard here: building the realtime model is just the
// event codec + WebSocket-config helper, which the browser legitimately
// needs to drive the transport with a server-minted client secret. The
// server-only boundary is enforced on minting itself
// (`mintRealtimeClientSecret`), which requires the Gateway credential.
const createRealtimeModel = (modelId: GatewayRealtimeModelId) =>
new GatewayRealtimeModel(modelId, {
provider: 'gateway.realtime',
baseURL,
teamIdOrSlug: options.teamIdOrSlug,
getAuthToken: getRealtimeAuthToken,
createClientSecret: mintRealtimeClientSecret,
});
};
provider.experimental_realtime = Object.assign(

@@ -488,4 +544,5 @@ (modelId: GatewayRealtimeModelId) => createRealtimeModel(modelId),

getToken: async (tokenOptions: RealtimeFactoryV4GetTokenOptions) => {
const model = createRealtimeModel(tokenOptions.model);
const secret = await model.doCreateClientSecret();
const { model: modelId, ...secretOptions } = tokenOptions;
const model = createRealtimeModel(modelId);
const secret = await model.doCreateClientSecret(secretOptions);
return {

@@ -535,5 +592,5 @@ token: secret.token,

throw new Error(
'AI Gateway realtime models cannot be used in browsers yet. Use gateway.experimental_realtime from server-side code only.',
'AI Gateway realtime client secrets must be minted server-side: minting needs your Gateway credential, which must never reach the browser. Call gateway.experimental_realtime.getToken() from your server and pass the returned token to the client.',
);
}
}
import type {
Experimental_RealtimeModelV4 as RealtimeModelV4,
Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent,
Experimental_RealtimeModelV4ClientSecretOptions as RealtimeModelV4ClientSecretOptions,
Experimental_RealtimeModelV4ClientSecretResult as RealtimeModelV4ClientSecretResult,

@@ -15,9 +16,11 @@ Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent,

/**
* Resolves the Gateway auth token used to authenticate the WebSocket upgrade
* (API key or Vercel OIDC token).
* Mints a short-lived client secret (`vcst_`) for this model via the
* Gateway's `/v1/realtime/client-secrets` endpoint. Implemented by the
* provider because minting requires the long-lived Gateway credential
* (API key / OIDC) and must run server-side.
*/
getAuthToken: () => PromiseLike<{
token: string;
authMethod: 'api-key' | 'oidc';
}>;
createClientSecret: (params: {
modelId: string;
expiresAfterSeconds?: number;
}) => PromiseLike<{ token: string; expiresAt?: number }>;
};

@@ -48,16 +51,24 @@

/**
* Unlike providers with a dedicated ephemeral-secret endpoint (e.g. OpenAI),
* the Gateway v0 realtime path does not mint a new client secret. The returned
* token is the Gateway credential resolved by the provider (`apiKey`,
* `AI_GATEWAY_API_KEY`, or Vercel OIDC token) and the WebSocket upgrade is
* authenticated directly with that credential. The
* `RealtimeModelV4ClientSecretOptions` are therefore intentionally unused:
* `sessionConfig` is applied later via the normalized `session-update` event,
* and `expiresAfterSeconds` has no Gateway-side equivalent.
* Mints a single-use, short-lived client secret (`vcst_`) the browser uses to
* open the realtime WebSocket without ever holding the long-lived Gateway
* credential. The customer's server calls this (via
* `gateway.experimental_realtime.getToken`) and hands the returned token to
* the browser, which connects with it through the `ai-gateway-auth.<token>`
* subprotocol. `expiresAfterSeconds` is forwarded to the mint endpoint;
* `sessionConfig` is intentionally unused here — it is applied later via the
* normalized `session-update` event.
*/
async doCreateClientSecret(): Promise<RealtimeModelV4ClientSecretResult> {
const { token } = await this.config.getAuthToken();
async doCreateClientSecret(
options?: RealtimeModelV4ClientSecretOptions,
): Promise<RealtimeModelV4ClientSecretResult> {
const secret = await this.config.createClientSecret({
modelId: this.modelId,
...(options?.expiresAfterSeconds != null && {
expiresAfterSeconds: options.expiresAfterSeconds,
}),
});
return {
token,
token: secret.token,
url: toGatewayRealtimeUrl(this.config.baseURL, this.modelId),
...(secret.expiresAt != null && { expiresAt: secret.expiresAt }),
};

@@ -64,0 +75,0 @@ }

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display