@circle-rd/auth-ai 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/ai.d.ts +32 -0
- package/dist/ai.js +65 -0
- package/dist/browser/client.d.ts +39 -0
- package/dist/browser/client.js +88 -0
- package/dist/browser/index.d.ts +10 -0
- package/dist/browser/index.js +5 -0
- package/dist/browser/store.d.ts +43 -0
- package/dist/browser/store.js +99 -0
- package/dist/claims.d.ts +60 -0
- package/dist/claims.js +68 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.js +204 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +80 -0
- package/dist/errors.d.ts +82 -0
- package/dist/errors.js +87 -0
- package/dist/fastify/index.d.ts +21 -0
- package/dist/fastify/index.js +88 -0
- package/dist/gateway-errors.d.ts +14 -0
- package/dist/gateway-errors.js +48 -0
- package/dist/gateway.d.ts +72 -0
- package/dist/gateway.js +115 -0
- package/dist/grants.d.ts +72 -0
- package/dist/grants.js +157 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.js +63 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +10 -0
- package/dist/introspection.d.ts +13 -0
- package/dist/introspection.js +12 -0
- package/dist/jwt.d.ts +16 -0
- package/dist/jwt.js +35 -0
- package/dist/oauth-errors.d.ts +9 -0
- package/dist/oauth-errors.js +50 -0
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +86 -0
- package/dist/react/index.d.ts +44 -0
- package/dist/react/index.js +88 -0
- package/dist/server/backchannel-logout.d.ts +47 -0
- package/dist/server/backchannel-logout.js +105 -0
- package/dist/server/bearer.d.ts +30 -0
- package/dist/server/bearer.js +58 -0
- package/dist/server/billing.d.ts +78 -0
- package/dist/server/billing.js +83 -0
- package/dist/server/consumption.d.ts +40 -0
- package/dist/server/consumption.js +79 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +6 -0
- package/dist/server/machine-api.d.ts +17 -0
- package/dist/server/machine-api.js +31 -0
- package/dist/server/machine-tokens.d.ts +13 -0
- package/dist/server/machine-tokens.js +49 -0
- package/dist/server/protected-resource.d.ts +27 -0
- package/dist/server/protected-resource.js +37 -0
- package/dist/session-user.d.ts +21 -0
- package/dist/session-user.js +35 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +1 -0
- package/dist/vue/index.d.ts +42 -0
- package/dist/vue/index.js +82 -0
- package/dist/wallet.d.ts +182 -0
- package/dist/wallet.js +184 -0
- package/dist/web/ai-routes.d.ts +8 -0
- package/dist/web/ai-routes.js +38 -0
- package/dist/web/cookies.d.ts +11 -0
- package/dist/web/cookies.js +46 -0
- package/dist/web/handlers.d.ts +47 -0
- package/dist/web/handlers.js +290 -0
- package/dist/web/http.d.ts +24 -0
- package/dist/web/http.js +85 -0
- package/dist/web/index.d.ts +7 -0
- package/dist/web/index.js +4 -0
- package/dist/web/seal.d.ts +11 -0
- package/dist/web/seal.js +39 -0
- package/dist/web/sessions.d.ts +43 -0
- package/dist/web/sessions.js +39 -0
- package/package.json +127 -4
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type AuthAiErrorDetails = Readonly<Record<string, string | number | boolean | null>>;
|
|
2
|
+
export interface AuthAiErrorOptions {
|
|
3
|
+
readonly details?: AuthAiErrorDetails;
|
|
4
|
+
readonly cause?: unknown;
|
|
5
|
+
}
|
|
6
|
+
declare const DEFAULT_MESSAGES: {
|
|
7
|
+
readonly CFG_001: "Invalid SDK configuration";
|
|
8
|
+
readonly OIDC_001: "Identity provider discovery failed";
|
|
9
|
+
readonly OIDC_002: "Invalid authorization response";
|
|
10
|
+
readonly OIDC_003: "Token request failed";
|
|
11
|
+
readonly OIDC_004: "The identity provider does not support this operation";
|
|
12
|
+
readonly TOK_001: "Token verification failed";
|
|
13
|
+
readonly TOK_002: "No usable grant for this user: sign in again";
|
|
14
|
+
readonly TOK_003: "Missing or malformed bearer credentials";
|
|
15
|
+
readonly TOK_004: "Insufficient scope";
|
|
16
|
+
readonly API_001: "auth-service rejected the request";
|
|
17
|
+
readonly API_002: "Unexpected response from auth-service";
|
|
18
|
+
readonly GW_001: "Insufficient credit: the wallet cannot pay for this call";
|
|
19
|
+
readonly GW_002: "The gateway rate limit was reached";
|
|
20
|
+
readonly GW_003: "The gateway refused the credentials or the resource";
|
|
21
|
+
readonly GW_004: "The gateway is temporarily unavailable";
|
|
22
|
+
readonly GW_005: "Unexpected response from the gateway";
|
|
23
|
+
readonly SES_001: "Not signed in";
|
|
24
|
+
readonly SES_002: "The sign-in attempt is missing, expired or was tampered with";
|
|
25
|
+
readonly SES_003: "Sign-in could not be completed";
|
|
26
|
+
readonly SES_004: "Cross-origin request refused";
|
|
27
|
+
readonly SES_005: "Sign-in failed";
|
|
28
|
+
readonly REQ_001: "Invalid request";
|
|
29
|
+
readonly SRV_001: "Internal error";
|
|
30
|
+
};
|
|
31
|
+
export type AuthAiErrorCode = keyof typeof DEFAULT_MESSAGES;
|
|
32
|
+
export declare function isAuthAiErrorCode(value: unknown): value is AuthAiErrorCode;
|
|
33
|
+
/**
|
|
34
|
+
* Every failure raised by the SDK. `details` must never carry tokens, secrets
|
|
35
|
+
* or other credentials: it is meant to be logged and serialised.
|
|
36
|
+
*/
|
|
37
|
+
export declare class AuthAiError extends Error {
|
|
38
|
+
readonly code: AuthAiErrorCode;
|
|
39
|
+
readonly details: AuthAiErrorDetails | undefined;
|
|
40
|
+
constructor(code: AuthAiErrorCode, message: string, options?: AuthAiErrorOptions);
|
|
41
|
+
/**
|
|
42
|
+
* What may reach an end user: the code and the registry's generic message.
|
|
43
|
+
* `message` and `details` carry what a developer needs (a provider's
|
|
44
|
+
* description, a scope name) and belong in logs, not in responses.
|
|
45
|
+
*/
|
|
46
|
+
toPublicJSON(): {
|
|
47
|
+
code: AuthAiErrorCode;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
toJSON(): {
|
|
51
|
+
code: AuthAiErrorCode;
|
|
52
|
+
message: string;
|
|
53
|
+
details?: AuthAiErrorDetails;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
type ErrorFactory = (message?: string, options?: AuthAiErrorOptions) => AuthAiError;
|
|
57
|
+
export declare const ERR: {
|
|
58
|
+
readonly CFG_001: ErrorFactory;
|
|
59
|
+
readonly OIDC_001: ErrorFactory;
|
|
60
|
+
readonly OIDC_002: ErrorFactory;
|
|
61
|
+
readonly OIDC_003: ErrorFactory;
|
|
62
|
+
readonly OIDC_004: ErrorFactory;
|
|
63
|
+
readonly TOK_001: ErrorFactory;
|
|
64
|
+
readonly TOK_002: ErrorFactory;
|
|
65
|
+
readonly TOK_003: ErrorFactory;
|
|
66
|
+
readonly TOK_004: ErrorFactory;
|
|
67
|
+
readonly API_001: ErrorFactory;
|
|
68
|
+
readonly API_002: ErrorFactory;
|
|
69
|
+
readonly GW_001: ErrorFactory;
|
|
70
|
+
readonly GW_002: ErrorFactory;
|
|
71
|
+
readonly GW_003: ErrorFactory;
|
|
72
|
+
readonly GW_004: ErrorFactory;
|
|
73
|
+
readonly GW_005: ErrorFactory;
|
|
74
|
+
readonly SES_001: ErrorFactory;
|
|
75
|
+
readonly SES_002: ErrorFactory;
|
|
76
|
+
readonly SES_003: ErrorFactory;
|
|
77
|
+
readonly SES_004: ErrorFactory;
|
|
78
|
+
readonly SES_005: ErrorFactory;
|
|
79
|
+
readonly REQ_001: ErrorFactory;
|
|
80
|
+
readonly SRV_001: ErrorFactory;
|
|
81
|
+
};
|
|
82
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const DEFAULT_MESSAGES = {
|
|
2
|
+
CFG_001: "Invalid SDK configuration",
|
|
3
|
+
OIDC_001: "Identity provider discovery failed",
|
|
4
|
+
OIDC_002: "Invalid authorization response",
|
|
5
|
+
OIDC_003: "Token request failed",
|
|
6
|
+
OIDC_004: "The identity provider does not support this operation",
|
|
7
|
+
TOK_001: "Token verification failed",
|
|
8
|
+
TOK_002: "No usable grant for this user: sign in again",
|
|
9
|
+
TOK_003: "Missing or malformed bearer credentials",
|
|
10
|
+
TOK_004: "Insufficient scope",
|
|
11
|
+
API_001: "auth-service rejected the request",
|
|
12
|
+
API_002: "Unexpected response from auth-service",
|
|
13
|
+
GW_001: "Insufficient credit: the wallet cannot pay for this call",
|
|
14
|
+
GW_002: "The gateway rate limit was reached",
|
|
15
|
+
GW_003: "The gateway refused the credentials or the resource",
|
|
16
|
+
GW_004: "The gateway is temporarily unavailable",
|
|
17
|
+
GW_005: "Unexpected response from the gateway",
|
|
18
|
+
SES_001: "Not signed in",
|
|
19
|
+
SES_002: "The sign-in attempt is missing, expired or was tampered with",
|
|
20
|
+
SES_003: "Sign-in could not be completed",
|
|
21
|
+
SES_004: "Cross-origin request refused",
|
|
22
|
+
SES_005: "Sign-in failed",
|
|
23
|
+
REQ_001: "Invalid request",
|
|
24
|
+
SRV_001: "Internal error",
|
|
25
|
+
};
|
|
26
|
+
export function isAuthAiErrorCode(value) {
|
|
27
|
+
return typeof value === "string" && Object.hasOwn(DEFAULT_MESSAGES, value);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Every failure raised by the SDK. `details` must never carry tokens, secrets
|
|
31
|
+
* or other credentials: it is meant to be logged and serialised.
|
|
32
|
+
*/
|
|
33
|
+
export class AuthAiError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
details;
|
|
36
|
+
constructor(code, message, options = {}) {
|
|
37
|
+
super(message, { cause: options.cause });
|
|
38
|
+
this.name = "AuthAiError";
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.details = options.details;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What may reach an end user: the code and the registry's generic message.
|
|
44
|
+
* `message` and `details` carry what a developer needs (a provider's
|
|
45
|
+
* description, a scope name) and belong in logs, not in responses.
|
|
46
|
+
*/
|
|
47
|
+
toPublicJSON() {
|
|
48
|
+
return { code: this.code, message: DEFAULT_MESSAGES[this.code] };
|
|
49
|
+
}
|
|
50
|
+
toJSON() {
|
|
51
|
+
return this.details
|
|
52
|
+
? { code: this.code, message: this.message, details: this.details }
|
|
53
|
+
: { code: this.code, message: this.message };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function factory(code) {
|
|
57
|
+
return (message = DEFAULT_MESSAGES[code], options) => new AuthAiError(code, message, options);
|
|
58
|
+
}
|
|
59
|
+
// Domain prefixes mirror auth-service: CFG_ configuration, OIDC_ protocol
|
|
60
|
+
// exchanges with the identity provider, TOK_ token and grant handling, API_ calls to the auth-service REST API,
|
|
61
|
+
// GW_ calls to the LiteLLM gateway, SES_ browser sessions, REQ_ invalid requests to the SDK's own routes, SRV_ unexpected
|
|
62
|
+
// failures of the SDK's own HTTP handlers.
|
|
63
|
+
export const ERR = {
|
|
64
|
+
CFG_001: factory("CFG_001"),
|
|
65
|
+
OIDC_001: factory("OIDC_001"),
|
|
66
|
+
OIDC_002: factory("OIDC_002"),
|
|
67
|
+
OIDC_003: factory("OIDC_003"),
|
|
68
|
+
OIDC_004: factory("OIDC_004"),
|
|
69
|
+
TOK_001: factory("TOK_001"),
|
|
70
|
+
TOK_002: factory("TOK_002"),
|
|
71
|
+
TOK_003: factory("TOK_003"),
|
|
72
|
+
TOK_004: factory("TOK_004"),
|
|
73
|
+
API_001: factory("API_001"),
|
|
74
|
+
API_002: factory("API_002"),
|
|
75
|
+
GW_001: factory("GW_001"),
|
|
76
|
+
GW_002: factory("GW_002"),
|
|
77
|
+
GW_003: factory("GW_003"),
|
|
78
|
+
GW_004: factory("GW_004"),
|
|
79
|
+
GW_005: factory("GW_005"),
|
|
80
|
+
SES_001: factory("SES_001"),
|
|
81
|
+
SES_002: factory("SES_002"),
|
|
82
|
+
SES_003: factory("SES_003"),
|
|
83
|
+
SES_004: factory("SES_004"),
|
|
84
|
+
SES_005: factory("SES_005"),
|
|
85
|
+
REQ_001: factory("REQ_001"),
|
|
86
|
+
SRV_001: factory("SRV_001"),
|
|
87
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FastifyPluginAsync, preHandlerAsyncHookHandler } from "fastify";
|
|
2
|
+
import type { AuthHandlers } from "../web/handlers.js";
|
|
3
|
+
import type { Session } from "../web/sessions.js";
|
|
4
|
+
declare module "fastify" {
|
|
5
|
+
interface FastifyRequest {
|
|
6
|
+
/** Set by `requireAuth` and `loadSession`; null when signed out. */
|
|
7
|
+
authSession: Session | null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export interface AuthPluginOptions {
|
|
11
|
+
readonly handlers: AuthHandlers;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serves the sign-in routes below `handlers.basePath` and decorates requests
|
|
15
|
+
* with `authSession`. Register it once; protect routes with `requireAuth`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const authPlugin: FastifyPluginAsync<AuthPluginOptions>;
|
|
18
|
+
/** Answers 401 `SES_001` to signed-out requests, and sets `request.authSession` for the others. */
|
|
19
|
+
export declare function requireAuth(handlers: AuthHandlers): preHandlerAsyncHookHandler;
|
|
20
|
+
/** Sets `request.authSession` when signed in and leaves it null otherwise. */
|
|
21
|
+
export declare function loadSession(handlers: AuthHandlers): preHandlerAsyncHookHandler;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ERR } from "../errors.js";
|
|
2
|
+
import { errorResponse } from "../web/http.js";
|
|
3
|
+
// A Web Request is built on the registered origin: the Host header is not trusted.
|
|
4
|
+
function toWebRequest(request, origin) {
|
|
5
|
+
const path = request.raw.url ?? "/";
|
|
6
|
+
if (!path.startsWith("/"))
|
|
7
|
+
return null;
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
10
|
+
// HTTP/2 pseudo-headers are not valid Headers names.
|
|
11
|
+
if (name.startsWith(":") || value === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
14
|
+
}
|
|
15
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
16
|
+
return new Request(`${origin}${path}`, {
|
|
17
|
+
method: request.method,
|
|
18
|
+
headers,
|
|
19
|
+
...(hasBody && typeof request.body === "string"
|
|
20
|
+
? { body: request.body }
|
|
21
|
+
: {}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function send(reply, response) {
|
|
25
|
+
reply.status(response.status);
|
|
26
|
+
for (const [name, value] of response.headers) {
|
|
27
|
+
if (name !== "set-cookie")
|
|
28
|
+
void reply.header(name, value);
|
|
29
|
+
}
|
|
30
|
+
const cookies = response.headers.getSetCookie();
|
|
31
|
+
if (cookies.length > 0)
|
|
32
|
+
void reply.header("set-cookie", cookies);
|
|
33
|
+
const body = await response.text();
|
|
34
|
+
await (body === "" ? reply.send() : reply.send(body));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Serves the sign-in routes below `handlers.basePath` and decorates requests
|
|
38
|
+
* with `authSession`. Register it once; protect routes with `requireAuth`.
|
|
39
|
+
*/
|
|
40
|
+
export const authPlugin = async (fastify, { handlers }) => {
|
|
41
|
+
fastify.decorateRequest("authSession", null);
|
|
42
|
+
// Scoped: the back-channel endpoint receives a form, which Fastify does not parse by default,
|
|
43
|
+
// and an application's own parser must not clash with this one.
|
|
44
|
+
await fastify.register(async (scope) => {
|
|
45
|
+
scope.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "string", bodyLimit: 16_384 }, (_request, body, done) => done(null, body));
|
|
46
|
+
scope.all(`${handlers.basePath}/*`, async (request, reply) => {
|
|
47
|
+
const webRequest = toWebRequest(request, handlers.origin);
|
|
48
|
+
const response = webRequest ? await handlers.handle(webRequest) : null;
|
|
49
|
+
if (!response)
|
|
50
|
+
return reply.callNotFound();
|
|
51
|
+
return send(reply, response);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
// Without it the decorator is scoped to the plugin and invisible to the application's routes.
|
|
56
|
+
authPlugin[Symbol.for("skip-override")] = true;
|
|
57
|
+
async function fail(reply, err) {
|
|
58
|
+
await send(reply, errorResponse(err, undefined));
|
|
59
|
+
return reply;
|
|
60
|
+
}
|
|
61
|
+
/** Answers 401 `SES_001` to signed-out requests, and sets `request.authSession` for the others. */
|
|
62
|
+
export function requireAuth(handlers) {
|
|
63
|
+
return async (request, reply) => {
|
|
64
|
+
try {
|
|
65
|
+
const webRequest = toWebRequest(request, handlers.origin);
|
|
66
|
+
if (!webRequest)
|
|
67
|
+
throw ERR.SES_001();
|
|
68
|
+
request.authSession = await handlers.requireSession(webRequest);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return fail(reply, err);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Sets `request.authSession` when signed in and leaves it null otherwise. */
|
|
76
|
+
export function loadSession(handlers) {
|
|
77
|
+
return async (request, reply) => {
|
|
78
|
+
try {
|
|
79
|
+
const webRequest = toWebRequest(request, handlers.origin);
|
|
80
|
+
request.authSession = webRequest
|
|
81
|
+
? await handlers.getSession(webRequest)
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return fail(reply, err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthAiError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps a failed gateway answer to the SDK registry. Branch on the status first:
|
|
4
|
+
* LiteLLM serialises the final body itself, so only the status is a contract.
|
|
5
|
+
* The message is the gateway's own and carries no credential.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toGatewayError(status: number, body: unknown, headers?: Headers): AuthAiError;
|
|
8
|
+
/** True for the error of a call refused because the wallet is empty. */
|
|
9
|
+
export declare function isInsufficientCredit(err: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Same test for the error of an OpenAI-compatible SDK, which exposes the HTTP
|
|
12
|
+
* status as `status`: the gateway answers 402 when the balance is zero or negative.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isInsufficientCreditStatus(err: unknown): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AuthAiError, ERR } from "./errors.js";
|
|
3
|
+
const gatewayErrorBodySchema = z.object({
|
|
4
|
+
error: z.object({
|
|
5
|
+
message: z.string().optional(),
|
|
6
|
+
type: z.string().optional(),
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Maps a failed gateway answer to the SDK registry. Branch on the status first:
|
|
11
|
+
* LiteLLM serialises the final body itself, so only the status is a contract.
|
|
12
|
+
* The message is the gateway's own and carries no credential.
|
|
13
|
+
*/
|
|
14
|
+
export function toGatewayError(status, body, headers) {
|
|
15
|
+
const parsed = gatewayErrorBodySchema.safeParse(body);
|
|
16
|
+
const type = parsed.success ? parsed.data.error.type : undefined;
|
|
17
|
+
const message = parsed.success ? parsed.data.error.message : undefined;
|
|
18
|
+
const details = { status };
|
|
19
|
+
if (type)
|
|
20
|
+
details["type"] = type;
|
|
21
|
+
const retryAfter = Number(headers?.get("retry-after"));
|
|
22
|
+
if (status === 429 && Number.isFinite(retryAfter) && retryAfter >= 0) {
|
|
23
|
+
details["retryAfterSeconds"] = retryAfter;
|
|
24
|
+
}
|
|
25
|
+
if (status === 402)
|
|
26
|
+
return ERR.GW_001(message, { details });
|
|
27
|
+
if (status === 429)
|
|
28
|
+
return ERR.GW_002(message, { details });
|
|
29
|
+
if (status === 401 || status === 403)
|
|
30
|
+
return ERR.GW_003(message, { details });
|
|
31
|
+
if (status === 503)
|
|
32
|
+
return ERR.GW_004(message, { details });
|
|
33
|
+
return ERR.GW_005(message ?? `The gateway answered ${status}`, { details });
|
|
34
|
+
}
|
|
35
|
+
/** True for the error of a call refused because the wallet is empty. */
|
|
36
|
+
export function isInsufficientCredit(err) {
|
|
37
|
+
return err instanceof AuthAiError && err.code === "GW_001";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Same test for the error of an OpenAI-compatible SDK, which exposes the HTTP
|
|
41
|
+
* status as `status`: the gateway answers 402 when the balance is zero or negative.
|
|
42
|
+
*/
|
|
43
|
+
export function isInsufficientCreditStatus(err) {
|
|
44
|
+
return (typeof err === "object" &&
|
|
45
|
+
err !== null &&
|
|
46
|
+
"status" in err &&
|
|
47
|
+
err.status === 402);
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const gatewayModelSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
object: z.ZodOptional<z.ZodString>;
|
|
5
|
+
created: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
owned_by: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, z.core.$loose>;
|
|
8
|
+
export declare const gatewayAgentSchema: z.ZodObject<{
|
|
9
|
+
agent_id: z.ZodString;
|
|
10
|
+
agent_name: z.ZodString;
|
|
11
|
+
}, z.core.$loose>;
|
|
12
|
+
export declare const gatewayMcpServerSchema: z.ZodObject<{
|
|
13
|
+
server_id: z.ZodString;
|
|
14
|
+
server_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
15
|
+
alias: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
16
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
17
|
+
transport: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
|
+
}, z.core.$loose>;
|
|
19
|
+
export type GatewayModel = z.infer<typeof gatewayModelSchema>;
|
|
20
|
+
export type GatewayAgent = z.infer<typeof gatewayAgentSchema>;
|
|
21
|
+
export type GatewayMcpServer = z.infer<typeof gatewayMcpServerSchema>;
|
|
22
|
+
/** Supplies the access token minted for the gateway audience. */
|
|
23
|
+
export interface GatewayTokens {
|
|
24
|
+
getToken(): Promise<string>;
|
|
25
|
+
/** Drops the token the gateway refused, so the next call mints a fresh one. */
|
|
26
|
+
invalidate(): void;
|
|
27
|
+
}
|
|
28
|
+
export interface GatewayClientOptions {
|
|
29
|
+
/** Public URL of the gateway: the token audience, with no trailing slash. */
|
|
30
|
+
readonly gatewayUrl: string;
|
|
31
|
+
readonly tokens: GatewayTokens;
|
|
32
|
+
readonly fetch: typeof fetch;
|
|
33
|
+
readonly timeoutMs: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The LiteLLM gateway on behalf of a signed-in user. The user's access token
|
|
37
|
+
* is the API key: it lives one hour, so it is minted per call (and cached by
|
|
38
|
+
* the grant manager) and never stored. The gateway bills the user's wallet.
|
|
39
|
+
*/
|
|
40
|
+
export declare class GatewayClient {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(options: GatewayClientOptions);
|
|
43
|
+
/** `baseURL` of an OpenAI-compatible SDK. */
|
|
44
|
+
get openAiBaseUrl(): string;
|
|
45
|
+
/**
|
|
46
|
+
* `fetch` that signs requests to the gateway with a fresh user token. Pass it
|
|
47
|
+
* to an OpenAI-compatible SDK together with `baseURL: openAiBaseUrl` and any
|
|
48
|
+
* placeholder `apiKey`; it refuses any other origin so the token cannot leak.
|
|
49
|
+
* A 401 drops the cached token: retrying the call mints a new one.
|
|
50
|
+
*/
|
|
51
|
+
readonly fetch: typeof fetch;
|
|
52
|
+
/** A fresh access token, for clients that cannot take a custom `fetch`. */
|
|
53
|
+
getAccessToken(): Promise<string>;
|
|
54
|
+
/** The models the user's plan may call. */
|
|
55
|
+
listModels(): Promise<GatewayModel[]>;
|
|
56
|
+
/** The agents the user's plan may reach. */
|
|
57
|
+
listAgents(): Promise<GatewayAgent[]>;
|
|
58
|
+
/** The MCP servers the user's plan may reach. */
|
|
59
|
+
listMcpServers(): Promise<GatewayMcpServer[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Endpoint of the gateway's MCP server, or of one registered server: connect
|
|
62
|
+
* an MCP client to it with the `Authorization: Bearer` header of
|
|
63
|
+
* `getAccessToken()`.
|
|
64
|
+
*/
|
|
65
|
+
mcpUrl(serverName?: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* POSTs JSON to a gateway route (`/v1/chat/completions`, `/v1/embeddings`…)
|
|
68
|
+
* and returns the parsed answer, which the caller validates. A refusal
|
|
69
|
+
* becomes a gateway error: GW_001 when the wallet cannot pay (402).
|
|
70
|
+
*/
|
|
71
|
+
postJson(path: string, body: unknown): Promise<unknown>;
|
|
72
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ERR } from "./errors.js";
|
|
3
|
+
import { toGatewayError } from "./gateway-errors.js";
|
|
4
|
+
const SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
5
|
+
export const gatewayModelSchema = z.looseObject({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
object: z.string().optional(),
|
|
8
|
+
created: z.number().optional(),
|
|
9
|
+
owned_by: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
const modelListSchema = z.object({ data: z.array(gatewayModelSchema) });
|
|
12
|
+
export const gatewayAgentSchema = z.looseObject({
|
|
13
|
+
agent_id: z.string(),
|
|
14
|
+
agent_name: z.string(),
|
|
15
|
+
});
|
|
16
|
+
export const gatewayMcpServerSchema = z.looseObject({
|
|
17
|
+
server_id: z.string(),
|
|
18
|
+
server_name: z.string().nullish(),
|
|
19
|
+
alias: z.string().nullish(),
|
|
20
|
+
description: z.string().nullish(),
|
|
21
|
+
transport: z.string().nullish(),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* The LiteLLM gateway on behalf of a signed-in user. The user's access token
|
|
25
|
+
* is the API key: it lives one hour, so it is minted per call (and cached by
|
|
26
|
+
* the grant manager) and never stored. The gateway bills the user's wallet.
|
|
27
|
+
*/
|
|
28
|
+
export class GatewayClient {
|
|
29
|
+
#options;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.#options = options;
|
|
32
|
+
}
|
|
33
|
+
/** `baseURL` of an OpenAI-compatible SDK. */
|
|
34
|
+
get openAiBaseUrl() {
|
|
35
|
+
return `${this.#options.gatewayUrl}/v1`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `fetch` that signs requests to the gateway with a fresh user token. Pass it
|
|
39
|
+
* to an OpenAI-compatible SDK together with `baseURL: openAiBaseUrl` and any
|
|
40
|
+
* placeholder `apiKey`; it refuses any other origin so the token cannot leak.
|
|
41
|
+
* A 401 drops the cached token: retrying the call mints a new one.
|
|
42
|
+
*/
|
|
43
|
+
fetch = async (input, init) => {
|
|
44
|
+
const request = new Request(input, init);
|
|
45
|
+
if (new URL(request.url).origin !== new URL(this.#options.gatewayUrl).origin) {
|
|
46
|
+
throw ERR.CFG_001("This fetch only serves requests to the gateway");
|
|
47
|
+
}
|
|
48
|
+
request.headers.set("authorization", `Bearer ${await this.#options.tokens.getToken()}`);
|
|
49
|
+
const response = await this.#options.fetch(request);
|
|
50
|
+
if (response.status === 401)
|
|
51
|
+
this.#options.tokens.invalidate();
|
|
52
|
+
return response;
|
|
53
|
+
};
|
|
54
|
+
/** A fresh access token, for clients that cannot take a custom `fetch`. */
|
|
55
|
+
getAccessToken() {
|
|
56
|
+
return this.#options.tokens.getToken();
|
|
57
|
+
}
|
|
58
|
+
/** The models the user's plan may call. */
|
|
59
|
+
async listModels() {
|
|
60
|
+
return (await this.#getJson("/v1/models", modelListSchema)).data;
|
|
61
|
+
}
|
|
62
|
+
/** The agents the user's plan may reach. */
|
|
63
|
+
listAgents() {
|
|
64
|
+
return this.#getJson("/v1/agents", z.array(gatewayAgentSchema));
|
|
65
|
+
}
|
|
66
|
+
/** The MCP servers the user's plan may reach. */
|
|
67
|
+
listMcpServers() {
|
|
68
|
+
return this.#getJson("/v1/mcp/server", z.array(gatewayMcpServerSchema));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Endpoint of the gateway's MCP server, or of one registered server: connect
|
|
72
|
+
* an MCP client to it with the `Authorization: Bearer` header of
|
|
73
|
+
* `getAccessToken()`.
|
|
74
|
+
*/
|
|
75
|
+
mcpUrl(serverName) {
|
|
76
|
+
if (serverName === undefined)
|
|
77
|
+
return `${this.#options.gatewayUrl}/mcp`;
|
|
78
|
+
if (!SERVER_NAME_PATTERN.test(serverName)) {
|
|
79
|
+
throw ERR.CFG_001("Invalid MCP server name");
|
|
80
|
+
}
|
|
81
|
+
return `${this.#options.gatewayUrl}/${serverName}/mcp`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* POSTs JSON to a gateway route (`/v1/chat/completions`, `/v1/embeddings`…)
|
|
85
|
+
* and returns the parsed answer, which the caller validates. A refusal
|
|
86
|
+
* becomes a gateway error: GW_001 when the wallet cannot pay (402).
|
|
87
|
+
*/
|
|
88
|
+
async postJson(path, body) {
|
|
89
|
+
return this.#send(path, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async #getJson(path, schema) {
|
|
96
|
+
const parsed = schema.safeParse(await this.#send(path, {}));
|
|
97
|
+
if (!parsed.success) {
|
|
98
|
+
throw ERR.GW_005(undefined, { cause: parsed.error });
|
|
99
|
+
}
|
|
100
|
+
return parsed.data;
|
|
101
|
+
}
|
|
102
|
+
async #send(path, init) {
|
|
103
|
+
const response = await this.fetch(`${this.#options.gatewayUrl}${path}`, {
|
|
104
|
+
...init,
|
|
105
|
+
headers: { accept: "application/json", ...init.headers },
|
|
106
|
+
redirect: "error",
|
|
107
|
+
signal: AbortSignal.timeout(this.#options.timeoutMs),
|
|
108
|
+
});
|
|
109
|
+
const body = await response.json().catch(() => undefined);
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw toGatewayError(response.status, body, response.headers);
|
|
112
|
+
}
|
|
113
|
+
return body;
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/grants.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AuthClient } from "./client.js";
|
|
2
|
+
import type { TokenSet } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Persists the single refresh-token chain of a user. The provider rotates it
|
|
5
|
+
* on every use, so a store must hold exactly one value per grant. Encrypt it
|
|
6
|
+
* at rest: it is a long-lived credential.
|
|
7
|
+
*/
|
|
8
|
+
export interface GrantStore {
|
|
9
|
+
get(grantId: string): Promise<string | undefined>;
|
|
10
|
+
set(grantId: string, refreshToken: string): Promise<void>;
|
|
11
|
+
delete(grantId: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Serialises the refreshes of one grant. The default lock only covers a single
|
|
15
|
+
* process: replicas need a distributed lock (or a row lock in the store).
|
|
16
|
+
*/
|
|
17
|
+
export interface GrantLock {
|
|
18
|
+
run<T>(grantId: string, task: () => Promise<T>): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
export interface AccessToken {
|
|
21
|
+
readonly accessToken: string;
|
|
22
|
+
/** Unix seconds. */
|
|
23
|
+
readonly expiresAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface GrantManagerOptions {
|
|
26
|
+
readonly auth: Pick<AuthClient, "refresh" | "revoke">;
|
|
27
|
+
readonly store: GrantStore;
|
|
28
|
+
readonly lock?: GrantLock;
|
|
29
|
+
/** A cached access token is renewed this long before it expires. */
|
|
30
|
+
readonly expirySkewSeconds?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class MemoryGrantStore implements GrantStore {
|
|
33
|
+
#private;
|
|
34
|
+
get(grantId: string): Promise<string | undefined>;
|
|
35
|
+
set(grantId: string, refreshToken: string): Promise<void>;
|
|
36
|
+
delete(grantId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare class InProcessGrantLock implements GrantLock {
|
|
39
|
+
#private;
|
|
40
|
+
run<T>(grantId: string, task: () => Promise<T>): Promise<T>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The only place that refreshes tokens. Access tokens are minted per resource
|
|
44
|
+
* (RFC 8707) from the stored refresh token and cached in memory until shortly
|
|
45
|
+
* before they expire; they are never persisted.
|
|
46
|
+
*/
|
|
47
|
+
export declare class GrantManager {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(options: GrantManagerOptions);
|
|
50
|
+
/** Stores the refresh token obtained at sign-in, replacing any older chain. */
|
|
51
|
+
save(grantId: string, refreshToken: string): Promise<void>;
|
|
52
|
+
has(grantId: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Returns an access token for `resource` (or for the whole sign-in set when
|
|
55
|
+
* omitted). Throws TOK_002 when the user has no grant or the provider
|
|
56
|
+
* rejected it: the grant is then dropped and only a new sign-in restores it.
|
|
57
|
+
*/
|
|
58
|
+
getAccessToken(grantId: string, options?: {
|
|
59
|
+
readonly resource?: string;
|
|
60
|
+
}): Promise<AccessToken>;
|
|
61
|
+
/**
|
|
62
|
+
* Refreshes without narrowing to a resource and returns the whole token set,
|
|
63
|
+
* ID token included: the way to renew the claims of a session. The rotated
|
|
64
|
+
* refresh token is stored. Failures are those of `getAccessToken`.
|
|
65
|
+
*/
|
|
66
|
+
refreshTokens(grantId: string): Promise<TokenSet>;
|
|
67
|
+
/** Drops a cached access token the API refused, so the next call mints a fresh one. */
|
|
68
|
+
invalidate(grantId: string, resource?: string): void;
|
|
69
|
+
/** Revokes the refresh token at the provider, then forgets the grant. */
|
|
70
|
+
revoke(grantId: string): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
export declare function createGrantManager(options: GrantManagerOptions): GrantManager;
|