@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/dist/ai.d.ts +32 -0
  4. package/dist/ai.js +65 -0
  5. package/dist/browser/client.d.ts +39 -0
  6. package/dist/browser/client.js +88 -0
  7. package/dist/browser/index.d.ts +10 -0
  8. package/dist/browser/index.js +5 -0
  9. package/dist/browser/store.d.ts +43 -0
  10. package/dist/browser/store.js +99 -0
  11. package/dist/claims.d.ts +60 -0
  12. package/dist/claims.js +68 -0
  13. package/dist/client.d.ts +54 -0
  14. package/dist/client.js +204 -0
  15. package/dist/config.d.ts +51 -0
  16. package/dist/config.js +80 -0
  17. package/dist/errors.d.ts +82 -0
  18. package/dist/errors.js +87 -0
  19. package/dist/fastify/index.d.ts +21 -0
  20. package/dist/fastify/index.js +88 -0
  21. package/dist/gateway-errors.d.ts +14 -0
  22. package/dist/gateway-errors.js +48 -0
  23. package/dist/gateway.d.ts +72 -0
  24. package/dist/gateway.js +115 -0
  25. package/dist/grants.d.ts +72 -0
  26. package/dist/grants.js +157 -0
  27. package/dist/http.d.ts +30 -0
  28. package/dist/http.js +63 -0
  29. package/dist/index.d.ts +20 -0
  30. package/dist/index.js +10 -0
  31. package/dist/introspection.d.ts +13 -0
  32. package/dist/introspection.js +12 -0
  33. package/dist/jwt.d.ts +16 -0
  34. package/dist/jwt.js +35 -0
  35. package/dist/oauth-errors.d.ts +9 -0
  36. package/dist/oauth-errors.js +50 -0
  37. package/dist/provider.d.ts +8 -0
  38. package/dist/provider.js +86 -0
  39. package/dist/react/index.d.ts +44 -0
  40. package/dist/react/index.js +88 -0
  41. package/dist/server/backchannel-logout.d.ts +47 -0
  42. package/dist/server/backchannel-logout.js +105 -0
  43. package/dist/server/bearer.d.ts +30 -0
  44. package/dist/server/bearer.js +58 -0
  45. package/dist/server/billing.d.ts +78 -0
  46. package/dist/server/billing.js +83 -0
  47. package/dist/server/consumption.d.ts +40 -0
  48. package/dist/server/consumption.js +79 -0
  49. package/dist/server/index.d.ts +11 -0
  50. package/dist/server/index.js +6 -0
  51. package/dist/server/machine-api.d.ts +17 -0
  52. package/dist/server/machine-api.js +31 -0
  53. package/dist/server/machine-tokens.d.ts +13 -0
  54. package/dist/server/machine-tokens.js +49 -0
  55. package/dist/server/protected-resource.d.ts +27 -0
  56. package/dist/server/protected-resource.js +37 -0
  57. package/dist/session-user.d.ts +21 -0
  58. package/dist/session-user.js +35 -0
  59. package/dist/types.d.ts +50 -0
  60. package/dist/types.js +1 -0
  61. package/dist/vue/index.d.ts +42 -0
  62. package/dist/vue/index.js +82 -0
  63. package/dist/wallet.d.ts +182 -0
  64. package/dist/wallet.js +184 -0
  65. package/dist/web/ai-routes.d.ts +8 -0
  66. package/dist/web/ai-routes.js +38 -0
  67. package/dist/web/cookies.d.ts +11 -0
  68. package/dist/web/cookies.js +46 -0
  69. package/dist/web/handlers.d.ts +47 -0
  70. package/dist/web/handlers.js +290 -0
  71. package/dist/web/http.d.ts +24 -0
  72. package/dist/web/http.js +85 -0
  73. package/dist/web/index.d.ts +7 -0
  74. package/dist/web/index.js +4 -0
  75. package/dist/web/seal.d.ts +11 -0
  76. package/dist/web/seal.js +39 -0
  77. package/dist/web/sessions.d.ts +43 -0
  78. package/dist/web/sessions.js +39 -0
  79. package/package.json +127 -4
@@ -0,0 +1,27 @@
1
+ import type { AuthClient } from "../client.js";
2
+ export interface ProtectedResourceMetadataOptions {
3
+ /** The audience this API accepts: its RFC 8707 resource identifier. */
4
+ readonly resource: string;
5
+ readonly scopesSupported?: readonly string[];
6
+ }
7
+ export interface ProtectedResourceMetadata {
8
+ readonly resource: string;
9
+ readonly authorization_servers: readonly string[];
10
+ readonly bearer_methods_supported: readonly ["header"];
11
+ readonly scopes_supported?: readonly string[];
12
+ }
13
+ /** RFC 9728 metadata, which lets MCP and OAuth clients find the authorization server. */
14
+ export declare function protectedResourceMetadata(auth: Pick<AuthClient, "config">, options: ProtectedResourceMetadataOptions): ProtectedResourceMetadata;
15
+ /**
16
+ * Where RFC 9728 §3 serves the metadata of `resource`: the well-known segment
17
+ * sits between the host and the resource path.
18
+ */
19
+ export declare function protectedResourceMetadataPath(resource: string): string;
20
+ export interface BearerChallengeOptions {
21
+ /** Absolute URL of the metadata document, advertised to unauthenticated clients. */
22
+ readonly resourceMetadataUrl: string;
23
+ readonly error?: "invalid_token" | "insufficient_scope";
24
+ readonly scope?: string;
25
+ }
26
+ /** Value of the `WWW-Authenticate` header of a 401 or 403 response. */
27
+ export declare function bearerChallenge(options: BearerChallengeOptions): string;
@@ -0,0 +1,37 @@
1
+ import { ERR } from "../errors.js";
2
+ /** RFC 9728 metadata, which lets MCP and OAuth clients find the authorization server. */
3
+ export function protectedResourceMetadata(auth, options) {
4
+ return {
5
+ resource: options.resource,
6
+ authorization_servers: [auth.config.issuer],
7
+ bearer_methods_supported: ["header"],
8
+ ...(options.scopesSupported
9
+ ? { scopes_supported: options.scopesSupported }
10
+ : {}),
11
+ };
12
+ }
13
+ /**
14
+ * Where RFC 9728 §3 serves the metadata of `resource`: the well-known segment
15
+ * sits between the host and the resource path.
16
+ */
17
+ export function protectedResourceMetadataPath(resource) {
18
+ const { pathname } = new URL(resource);
19
+ return `/.well-known/oauth-protected-resource${pathname === "/" ? "" : pathname}`;
20
+ }
21
+ const UNSAFE_PARAMETER = /["\\\r\n]/;
22
+ /** Value of the `WWW-Authenticate` header of a 401 or 403 response. */
23
+ export function bearerChallenge(options) {
24
+ const parameters = [
25
+ ["resource_metadata", options.resourceMetadataUrl],
26
+ ];
27
+ if (options.error)
28
+ parameters.push(["error", options.error]);
29
+ if (options.scope)
30
+ parameters.push(["scope", options.scope]);
31
+ for (const [name, value] of parameters) {
32
+ if (UNSAFE_PARAMETER.test(value)) {
33
+ throw ERR.CFG_001(`Invalid ${name} in a Bearer challenge`);
34
+ }
35
+ }
36
+ return `Bearer ${parameters.map(([name, value]) => `${name}="${value}"`).join(", ")}`;
37
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import type { IdTokenClaims } from "./claims.js";
3
+ /**
4
+ * What a signed-in user looks like to the application's frontend: profile and
5
+ * entitlements, with the absent claims normalised to empty values. Tokens,
6
+ * `sid` and timestamps never reach the browser.
7
+ */
8
+ export declare const sessionUserSchema: z.ZodObject<{
9
+ sub: z.ZodString;
10
+ name: z.ZodOptional<z.ZodString>;
11
+ email: z.ZodOptional<z.ZodString>;
12
+ emailVerified: z.ZodOptional<z.ZodBoolean>;
13
+ picture: z.ZodOptional<z.ZodString>;
14
+ roles: z.ZodArray<z.ZodString>;
15
+ permissions: z.ZodArray<z.ZodString>;
16
+ features: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
17
+ plan: z.ZodOptional<z.ZodString>;
18
+ orgId: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strip>;
20
+ export type SessionUser = z.infer<typeof sessionUserSchema>;
21
+ export declare function toSessionUser(claims: IdTokenClaims): SessionUser;
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ const featureValueSchema = z.union([z.string(), z.number(), z.boolean()]);
3
+ /**
4
+ * What a signed-in user looks like to the application's frontend: profile and
5
+ * entitlements, with the absent claims normalised to empty values. Tokens,
6
+ * `sid` and timestamps never reach the browser.
7
+ */
8
+ export const sessionUserSchema = z.object({
9
+ sub: z.string(),
10
+ name: z.string().optional(),
11
+ email: z.string().optional(),
12
+ emailVerified: z.boolean().optional(),
13
+ picture: z.string().optional(),
14
+ roles: z.array(z.string()),
15
+ permissions: z.array(z.string()),
16
+ features: z.record(z.string(), featureValueSchema),
17
+ plan: z.string().optional(),
18
+ orgId: z.string().optional(),
19
+ });
20
+ export function toSessionUser(claims) {
21
+ return {
22
+ sub: claims.sub,
23
+ roles: claims.roles ?? [],
24
+ permissions: claims.permissions ?? [],
25
+ features: claims.features ?? {},
26
+ ...(claims.name === undefined ? {} : { name: claims.name }),
27
+ ...(claims.email === undefined ? {} : { email: claims.email }),
28
+ ...(claims.email_verified === undefined
29
+ ? {}
30
+ : { emailVerified: claims.email_verified }),
31
+ ...(claims.picture === undefined ? {} : { picture: claims.picture }),
32
+ ...(claims.plan === undefined ? {} : { plan: claims.plan }),
33
+ ...(claims.org_id === undefined ? {} : { orgId: claims.org_id }),
34
+ };
35
+ }
@@ -0,0 +1,50 @@
1
+ import type { IdTokenClaims } from "./claims.js";
2
+ export interface TokenSet {
3
+ readonly accessToken: string;
4
+ readonly tokenType: string;
5
+ /** Unix seconds. */
6
+ readonly expiresAt: number;
7
+ readonly scope?: string;
8
+ /** Absent when the provider issued none (no `offline_access`, or no rotation). */
9
+ readonly refreshToken?: string;
10
+ readonly idToken?: string;
11
+ }
12
+ /**
13
+ * Secrets of one sign-in attempt. The caller persists them (typically in a
14
+ * short-lived sealed cookie) between the redirect and the callback.
15
+ */
16
+ export interface LoginHandshake {
17
+ readonly state: string;
18
+ readonly nonce: string;
19
+ readonly codeVerifier: string;
20
+ }
21
+ export interface LoginRequest {
22
+ readonly url: string;
23
+ readonly handshake: LoginHandshake;
24
+ }
25
+ export interface StartLoginOptions {
26
+ readonly prompt?: "login" | "consent" | "select_account" | "create" | "none";
27
+ readonly loginHint?: string;
28
+ }
29
+ export interface LoginResult {
30
+ readonly tokens: TokenSet & {
31
+ readonly idToken: string;
32
+ };
33
+ readonly claims: IdTokenClaims;
34
+ /** The provider session, only present when end-session or back-channel logout is enabled for the application. */
35
+ readonly sessionId: string | undefined;
36
+ }
37
+ export interface RefreshOptions {
38
+ /** Narrows the new access token to one RFC 8707 audience. */
39
+ readonly resource?: string;
40
+ }
41
+ export interface ClientCredentialsOptions {
42
+ /** Space-separated machine scopes, e.g. `billing:read`. */
43
+ readonly scope?: string;
44
+ readonly resource?: string;
45
+ }
46
+ export interface LogoutUrlOptions {
47
+ readonly idTokenHint?: string;
48
+ readonly state?: string;
49
+ }
50
+ export type TokenTypeHint = "access_token" | "refresh_token";
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { App, ComputedRef, ShallowRef } from "vue";
2
+ import type { BrowserAuth, WalletView } from "../browser/client.js";
3
+ import type { AuthState, ResourceState } from "../browser/store.js";
4
+ import type { FeatureValue, PermissionAction } from "../claims.js";
5
+ import type { GatewayModel } from "../gateway.js";
6
+ import type { SessionUser } from "../session-user.js";
7
+ import type { WalletStats } from "../wallet.js";
8
+ export interface AuthPluginOptions {
9
+ /** Defaults to a client on `basePath`. */
10
+ readonly auth?: BrowserAuth;
11
+ readonly basePath?: string;
12
+ }
13
+ /** `app.use(createAuthPlugin())` reads the session once the app is mounted in a browser. */
14
+ export declare function createAuthPlugin(options?: AuthPluginOptions): {
15
+ install(app: App): void;
16
+ };
17
+ export interface UseAuthResult {
18
+ readonly state: Readonly<ShallowRef<AuthState>>;
19
+ readonly user: ComputedRef<SessionUser | null>;
20
+ readonly login: (next?: string) => void;
21
+ readonly logout: () => Promise<void>;
22
+ readonly refresh: () => Promise<void>;
23
+ }
24
+ export declare function useAuth(): UseAuthResult;
25
+ export interface Entitlements {
26
+ hasRole(role: string): boolean;
27
+ hasPermission(resource: string, action?: PermissionAction): boolean;
28
+ hasFeature(key: string): boolean;
29
+ getFeature(key: string): FeatureValue | undefined;
30
+ }
31
+ /** Entitlement checks reading the current user at call time, so templates stay reactive. */
32
+ export declare function useEntitlements(): Entitlements;
33
+ export interface UseResourceResult<T> {
34
+ readonly state: Readonly<ShallowRef<ResourceState<T>>>;
35
+ readonly refresh: () => Promise<void>;
36
+ }
37
+ export interface UseWalletOptions {
38
+ readonly pollIntervalMs?: number;
39
+ }
40
+ export declare function useWallet(options?: UseWalletOptions): UseResourceResult<WalletView>;
41
+ export declare function useWalletStats(days?: number): UseResourceResult<WalletStats>;
42
+ export declare function useModels(): UseResourceResult<GatewayModel[]>;
@@ -0,0 +1,82 @@
1
+ import { computed, getCurrentScope, inject, onScopeDispose, shallowRef, } from "vue";
2
+ import { createBrowserAuth } from "../browser/client.js";
3
+ import { createAuthStore, createResourceStore } from "../browser/store.js";
4
+ import { getFeature, hasFeature, hasPermission, hasRole } from "../claims.js";
5
+ const AUTH_KEY = Symbol("auth-ai");
6
+ /** `app.use(createAuthPlugin())` reads the session once the app is mounted in a browser. */
7
+ export function createAuthPlugin(options = {}) {
8
+ return {
9
+ install(app) {
10
+ const auth = options.auth ??
11
+ createBrowserAuth(options.basePath ? { basePath: options.basePath } : {});
12
+ const store = createAuthStore(auth);
13
+ app.provide(AUTH_KEY, { auth, store });
14
+ if (typeof window !== "undefined")
15
+ void store.refresh();
16
+ },
17
+ };
18
+ }
19
+ function useContext() {
20
+ const context = inject(AUTH_KEY, null);
21
+ if (!context)
22
+ throw new Error("Install createAuthPlugin() on the app");
23
+ return context;
24
+ }
25
+ /** A ref that follows an observable until the calling scope is disposed. */
26
+ function follow(source) {
27
+ const state = shallowRef(source.get());
28
+ const stop = source.subscribe(() => {
29
+ state.value = source.get();
30
+ });
31
+ if (getCurrentScope())
32
+ onScopeDispose(stop);
33
+ return state;
34
+ }
35
+ export function useAuth() {
36
+ const { auth, store } = useContext();
37
+ const state = follow(store.state);
38
+ return {
39
+ state,
40
+ user: computed(() => state.value.user),
41
+ login: (next) => auth.login(next),
42
+ logout: () => auth.logout(),
43
+ refresh: () => store.refresh(),
44
+ };
45
+ }
46
+ /** Entitlement checks reading the current user at call time, so templates stay reactive. */
47
+ export function useEntitlements() {
48
+ const { user } = useAuth();
49
+ return {
50
+ hasRole: (role) => hasRole(user.value ?? {}, role),
51
+ hasPermission: (resource, action) => hasPermission(user.value ?? {}, resource, action),
52
+ hasFeature: (key) => hasFeature(user.value ?? {}, key),
53
+ getFeature: (key) => getFeature(user.value ?? {}, key),
54
+ };
55
+ }
56
+ function useResource(load, options = {}) {
57
+ const { auth, store } = useContext();
58
+ const resource = createResourceStore(() => load(auth), {
59
+ ...options,
60
+ onError: (error) => {
61
+ if (error.code === "SES_001")
62
+ void store.refresh();
63
+ },
64
+ });
65
+ const state = follow(resource.state);
66
+ if (getCurrentScope())
67
+ onScopeDispose(resource.start());
68
+ else
69
+ void resource.refresh();
70
+ return { state, refresh: resource.refresh };
71
+ }
72
+ export function useWallet(options = {}) {
73
+ return useResource((auth) => auth.getWallet(), options.pollIntervalMs === undefined
74
+ ? {}
75
+ : { pollIntervalMs: options.pollIntervalMs });
76
+ }
77
+ export function useWalletStats(days) {
78
+ return useResource((auth) => auth.getWalletStats(days));
79
+ }
80
+ export function useModels() {
81
+ return useResource((auth) => auth.listModels());
82
+ }
@@ -0,0 +1,182 @@
1
+ import { z } from "zod";
2
+ import type { JsonHttp } from "./http.js";
3
+ /** 1 EUR in the ledger unit. */
4
+ export declare const MICRO_PER_EUR = 1000000n;
5
+ export declare const walletTransactionTypeSchema: z.ZodEnum<{
6
+ topup: "topup";
7
+ grant: "grant";
8
+ refund: "refund";
9
+ usage: "usage";
10
+ adjust: "adjust";
11
+ reversal: "reversal";
12
+ }>;
13
+ export declare const walletAccountSchema: z.ZodObject<{
14
+ balance: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
15
+ isUnlimited: z.ZodBoolean;
16
+ canSpend: z.ZodBoolean;
17
+ currency: z.ZodLiteral<"eur">;
18
+ }, z.core.$strip>;
19
+ export declare const walletTransactionSchema: z.ZodObject<{
20
+ id: z.ZodString;
21
+ type: z.ZodEnum<{
22
+ topup: "topup";
23
+ grant: "grant";
24
+ refund: "refund";
25
+ usage: "usage";
26
+ adjust: "adjust";
27
+ reversal: "reversal";
28
+ }>;
29
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
30
+ delta: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
31
+ balanceAfter: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
32
+ applicationId: z.ZodNullable<z.ZodString>;
33
+ metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
34
+ createdAt: z.ZodISODateTime;
35
+ }, z.core.$strip>;
36
+ export declare const walletTransactionPageSchema: z.ZodObject<{
37
+ items: z.ZodArray<z.ZodObject<{
38
+ id: z.ZodString;
39
+ type: z.ZodEnum<{
40
+ topup: "topup";
41
+ grant: "grant";
42
+ refund: "refund";
43
+ usage: "usage";
44
+ adjust: "adjust";
45
+ reversal: "reversal";
46
+ }>;
47
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
48
+ delta: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
49
+ balanceAfter: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
50
+ applicationId: z.ZodNullable<z.ZodString>;
51
+ metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
52
+ createdAt: z.ZodISODateTime;
53
+ }, z.core.$strip>>;
54
+ nextCursor: z.ZodNullable<z.ZodString>;
55
+ }, z.core.$strip>;
56
+ export declare const walletStatsSchema: z.ZodObject<{
57
+ days: z.ZodNumber;
58
+ total: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
59
+ notBilled: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
60
+ calls: z.ZodNumber;
61
+ series: z.ZodArray<z.ZodObject<{
62
+ date: z.ZodString;
63
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
64
+ count: z.ZodNumber;
65
+ }, z.core.$strip>>;
66
+ byApplication: z.ZodArray<z.ZodObject<{
67
+ applicationId: z.ZodNullable<z.ZodString>;
68
+ name: z.ZodNullable<z.ZodString>;
69
+ slug: z.ZodNullable<z.ZodString>;
70
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
71
+ count: z.ZodNumber;
72
+ }, z.core.$strip>>;
73
+ byKind: z.ZodArray<z.ZodObject<{
74
+ kind: z.ZodString;
75
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
76
+ count: z.ZodNumber;
77
+ }, z.core.$strip>>;
78
+ byModel: z.ZodArray<z.ZodObject<{
79
+ model: z.ZodString;
80
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
81
+ count: z.ZodNumber;
82
+ promptTokens: z.ZodNumber;
83
+ completionTokens: z.ZodNumber;
84
+ totalTokens: z.ZodNumber;
85
+ }, z.core.$strip>>;
86
+ daily: z.ZodArray<z.ZodObject<{
87
+ date: z.ZodString;
88
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
89
+ count: z.ZodNumber;
90
+ byApplication: z.ZodArray<z.ZodObject<{
91
+ applicationId: z.ZodNullable<z.ZodString>;
92
+ name: z.ZodNullable<z.ZodString>;
93
+ slug: z.ZodNullable<z.ZodString>;
94
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
95
+ count: z.ZodNumber;
96
+ }, z.core.$strip>>;
97
+ byModel: z.ZodArray<z.ZodObject<{
98
+ model: z.ZodString;
99
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
100
+ count: z.ZodNumber;
101
+ promptTokens: z.ZodNumber;
102
+ completionTokens: z.ZodNumber;
103
+ totalTokens: z.ZodNumber;
104
+ }, z.core.$strip>>;
105
+ }, z.core.$strip>>;
106
+ }, z.core.$strip>;
107
+ export declare const walletSessionUsageSchema: z.ZodObject<{
108
+ sessionId: z.ZodString;
109
+ applicationSlug: z.ZodNullable<z.ZodString>;
110
+ days: z.ZodNumber;
111
+ total: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
112
+ notBilled: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
113
+ calls: z.ZodNumber;
114
+ byModel: z.ZodArray<z.ZodObject<{
115
+ model: z.ZodString;
116
+ amount: z.ZodPipe<z.ZodString, z.ZodTransform<bigint, string>>;
117
+ count: z.ZodNumber;
118
+ promptTokens: z.ZodNumber;
119
+ completionTokens: z.ZodNumber;
120
+ totalTokens: z.ZodNumber;
121
+ }, z.core.$strip>>;
122
+ }, z.core.$strip>;
123
+ export declare const topupOptionsSchema: z.ZodObject<{
124
+ enabled: z.ZodBoolean;
125
+ currency: z.ZodLiteral<"eur">;
126
+ minCents: z.ZodNumber;
127
+ maxCents: z.ZodNumber;
128
+ }, z.core.$strip>;
129
+ export type WalletAccount = z.infer<typeof walletAccountSchema>;
130
+ export type WalletTransaction = z.infer<typeof walletTransactionSchema>;
131
+ export type WalletTransactionPage = z.infer<typeof walletTransactionPageSchema>;
132
+ export type WalletTransactionType = z.infer<typeof walletTransactionTypeSchema>;
133
+ export type WalletStats = z.infer<typeof walletStatsSchema>;
134
+ export type WalletSessionUsage = z.infer<typeof walletSessionUsageSchema>;
135
+ export type TopupOptions = z.infer<typeof topupOptionsSchema>;
136
+ export interface WalletStatsOptions {
137
+ /** 1 to 90, 30 by default. */
138
+ readonly days?: number;
139
+ }
140
+ export interface WalletSessionUsageOptions {
141
+ /** 1 to 90, 30 by default. */
142
+ readonly days?: number;
143
+ }
144
+ export interface ListTransactionsOptions {
145
+ readonly limit?: number;
146
+ readonly cursor?: string;
147
+ readonly type?: WalletTransactionType;
148
+ }
149
+ /**
150
+ * Exact decimal euros of a micro-euro amount, e.g. `5000000n` gives `"5.00"`.
151
+ * The amount is truncated toward zero at `fractionDigits` (0 to 6, 2 by
152
+ * default): rounding up could show a user more credit than they hold.
153
+ */
154
+ export declare function formatEuros(amount: bigint, fractionDigits?: number): string;
155
+ /**
156
+ * The signed-in user's own wallet, read with their access token for the
157
+ * auth-service audience and the `wallet:self:read` scope. Debits are made by
158
+ * the gateway after each call: an application only reads, and refills by
159
+ * sending the user to `topupUrl()`.
160
+ */
161
+ export declare class WalletClient {
162
+ #private;
163
+ constructor(http: JsonHttp, issuer: string);
164
+ /** Decide with `canSpend` rather than recomputing it from the balance. */
165
+ getAccount(): Promise<WalletAccount>;
166
+ getStats(options?: WalletStatsOptions): Promise<WalletStats>;
167
+ /**
168
+ * What one chat session spent, for the calling application only. The id is
169
+ * the one passed to the gateway as `metadata.session_id`.
170
+ */
171
+ getSessionUsage(sessionId: string, options?: WalletSessionUsageOptions): Promise<WalletSessionUsage>;
172
+ /** Newest first, keyset-paginated: pass `nextCursor` back until it is null. */
173
+ listTransactions(options?: ListTransactionsOptions): Promise<WalletTransactionPage>;
174
+ getTopupOptions(): Promise<TopupOptions>;
175
+ /**
176
+ * The Credit tab of the user's profile. Checkout needs the user's session,
177
+ * not a bearer token, so an application links there instead of creating it.
178
+ */
179
+ topupUrl(): string;
180
+ }
181
+ /** Key handed to the token supplier: the wallet audience of auth-service. */
182
+ export declare const WALLET_SCOPE_KEY = "wallet";
package/dist/wallet.js ADDED
@@ -0,0 +1,184 @@
1
+ import { z } from "zod";
2
+ /** 1 EUR in the ledger unit. */
3
+ export const MICRO_PER_EUR = 1000000n;
4
+ // Amounts travel as integer strings of micro-euros: a JSON number cannot carry them exactly.
5
+ const amountSchema = z
6
+ .string()
7
+ .regex(/^-?[0-9]{1,19}$/, "must be an integer string of micro-euros")
8
+ .transform((value) => BigInt(value));
9
+ const isoDateSchema = z.iso.datetime();
10
+ export const walletTransactionTypeSchema = z.enum([
11
+ "topup",
12
+ "grant",
13
+ "refund",
14
+ "usage",
15
+ "adjust",
16
+ "reversal",
17
+ ]);
18
+ export const walletAccountSchema = z.object({
19
+ /** Negative when usage or a refund exceeded the credit. */
20
+ balance: amountSchema,
21
+ isUnlimited: z.boolean(),
22
+ /** Whether new spending is allowed: unlimited, or a strictly positive balance. */
23
+ canSpend: z.boolean(),
24
+ currency: z.literal("eur"),
25
+ });
26
+ export const walletTransactionSchema = z.object({
27
+ id: z.string(),
28
+ type: walletTransactionTypeSchema,
29
+ amount: amountSchema,
30
+ delta: amountSchema,
31
+ balanceAfter: amountSchema,
32
+ applicationId: z.string().nullable(),
33
+ metadata: z.record(z.string(), z.unknown()),
34
+ createdAt: isoDateSchema,
35
+ });
36
+ export const walletTransactionPageSchema = z.object({
37
+ items: z.array(walletTransactionSchema),
38
+ nextCursor: z.string().nullable(),
39
+ });
40
+ const modelEntrySchema = z.object({
41
+ /** `mcp` for tool calls, `other` when the gateway reported no model. */
42
+ model: z.string(),
43
+ amount: amountSchema,
44
+ count: z.number(),
45
+ promptTokens: z.number(),
46
+ completionTokens: z.number(),
47
+ totalTokens: z.number(),
48
+ });
49
+ const applicationEntrySchema = z.object({
50
+ applicationId: z.string().nullable(),
51
+ name: z.string().nullable(),
52
+ slug: z.string().nullable(),
53
+ amount: amountSchema,
54
+ count: z.number(),
55
+ });
56
+ export const walletStatsSchema = z.object({
57
+ days: z.number(),
58
+ total: amountSchema,
59
+ /** Part of `total` never deducted: usage on an unlimited account. */
60
+ notBilled: amountSchema,
61
+ calls: z.number(),
62
+ series: z.array(z.object({ date: z.string(), amount: amountSchema, count: z.number() })),
63
+ byApplication: z.array(applicationEntrySchema),
64
+ byKind: z.array(z.object({ kind: z.string(), amount: amountSchema, count: z.number() })),
65
+ byModel: z.array(modelEntrySchema),
66
+ daily: z.array(z.object({
67
+ date: z.string(),
68
+ amount: amountSchema,
69
+ count: z.number(),
70
+ byApplication: z.array(applicationEntrySchema),
71
+ byModel: z.array(modelEntrySchema),
72
+ })),
73
+ });
74
+ export const walletSessionUsageSchema = z.object({
75
+ sessionId: z.string(),
76
+ applicationSlug: z.string().nullable(),
77
+ days: z.number(),
78
+ total: amountSchema,
79
+ notBilled: amountSchema,
80
+ calls: z.number(),
81
+ byModel: z.array(modelEntrySchema),
82
+ });
83
+ export const topupOptionsSchema = z.object({
84
+ enabled: z.boolean(),
85
+ currency: z.literal("eur"),
86
+ minCents: z.number(),
87
+ maxCents: z.number(),
88
+ });
89
+ /**
90
+ * Exact decimal euros of a micro-euro amount, e.g. `5000000n` gives `"5.00"`.
91
+ * The amount is truncated toward zero at `fractionDigits` (0 to 6, 2 by
92
+ * default): rounding up could show a user more credit than they hold.
93
+ */
94
+ export function formatEuros(amount, fractionDigits = 2) {
95
+ if (!Number.isInteger(fractionDigits) ||
96
+ fractionDigits < 0 ||
97
+ fractionDigits > 6) {
98
+ throw new RangeError("fractionDigits must be an integer from 0 to 6");
99
+ }
100
+ const negative = amount < 0n;
101
+ const magnitude = negative ? -amount : amount;
102
+ const whole = magnitude / MICRO_PER_EUR;
103
+ const micro = (magnitude % MICRO_PER_EUR).toString().padStart(6, "0");
104
+ const fraction = micro.slice(0, fractionDigits);
105
+ const text = fractionDigits === 0 ? `${whole}` : `${whole}.${fraction}`;
106
+ // A negative amount truncated to zero is not a debt worth a minus sign.
107
+ return negative && /[1-9]/.test(text) ? `-${text}` : text;
108
+ }
109
+ /**
110
+ * The signed-in user's own wallet, read with their access token for the
111
+ * auth-service audience and the `wallet:self:read` scope. Debits are made by
112
+ * the gateway after each call: an application only reads, and refills by
113
+ * sending the user to `topupUrl()`.
114
+ */
115
+ export class WalletClient {
116
+ #http;
117
+ #issuer;
118
+ constructor(http, issuer) {
119
+ this.#http = http;
120
+ this.#issuer = issuer;
121
+ }
122
+ /** Decide with `canSpend` rather than recomputing it from the balance. */
123
+ getAccount() {
124
+ return this.#http.request({
125
+ method: "GET",
126
+ path: "/api/user/wallet",
127
+ scope: WALLET_SCOPE_KEY,
128
+ schema: walletAccountSchema,
129
+ });
130
+ }
131
+ getStats(options = {}) {
132
+ return this.#http.request({
133
+ method: "GET",
134
+ path: "/api/user/wallet/stats",
135
+ scope: WALLET_SCOPE_KEY,
136
+ query: { days: options.days },
137
+ schema: walletStatsSchema,
138
+ });
139
+ }
140
+ /**
141
+ * What one chat session spent, for the calling application only. The id is
142
+ * the one passed to the gateway as `metadata.session_id`.
143
+ */
144
+ getSessionUsage(sessionId, options = {}) {
145
+ return this.#http.request({
146
+ method: "GET",
147
+ path: "/api/user/wallet/usage",
148
+ scope: WALLET_SCOPE_KEY,
149
+ query: { sessionId, days: options.days },
150
+ schema: walletSessionUsageSchema,
151
+ });
152
+ }
153
+ /** Newest first, keyset-paginated: pass `nextCursor` back until it is null. */
154
+ listTransactions(options = {}) {
155
+ return this.#http.request({
156
+ method: "GET",
157
+ path: "/api/user/wallet/transactions",
158
+ scope: WALLET_SCOPE_KEY,
159
+ query: {
160
+ limit: options.limit,
161
+ cursor: options.cursor,
162
+ type: options.type,
163
+ },
164
+ schema: walletTransactionPageSchema,
165
+ });
166
+ }
167
+ getTopupOptions() {
168
+ return this.#http.request({
169
+ method: "GET",
170
+ path: "/api/user/wallet/topup-options",
171
+ scope: WALLET_SCOPE_KEY,
172
+ schema: topupOptionsSchema,
173
+ });
174
+ }
175
+ /**
176
+ * The Credit tab of the user's profile. Checkout needs the user's session,
177
+ * not a bearer token, so an application links there instead of creating it.
178
+ */
179
+ topupUrl() {
180
+ return `${this.#issuer}/profile`;
181
+ }
182
+ }
183
+ /** Key handed to the token supplier: the wallet audience of auth-service. */
184
+ export const WALLET_SCOPE_KEY = "wallet";
@@ -0,0 +1,8 @@
1
+ import type { AiClient } from "../ai.js";
2
+ import type { Session } from "./sessions.js";
3
+ export type RouteHandler = (request: Request) => Promise<Response>;
4
+ /**
5
+ * Wallet and model reads for the application's own frontend, which holds no
6
+ * token: the backend calls auth-service and the gateway on the user's behalf.
7
+ */
8
+ export declare function createAiRoutes(ai: AiClient, requireSession: (request: Request) => Promise<Session>): Record<string, RouteHandler>;