@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,99 @@
1
+ import { AuthAiError, ERR } from "../errors.js";
2
+ export function createObservable(initial) {
3
+ let value = initial;
4
+ const listeners = new Set();
5
+ return {
6
+ get: () => value,
7
+ set(next) {
8
+ if (Object.is(next, value))
9
+ return;
10
+ value = next;
11
+ for (const listener of [...listeners])
12
+ listener();
13
+ },
14
+ subscribe(listener) {
15
+ listeners.add(listener);
16
+ return () => listeners.delete(listener);
17
+ },
18
+ };
19
+ }
20
+ function asError(err) {
21
+ return err instanceof AuthAiError
22
+ ? err
23
+ : ERR.API_001("The request failed", { cause: err });
24
+ }
25
+ export const INITIAL_AUTH_STATE = {
26
+ status: "loading",
27
+ user: null,
28
+ error: null,
29
+ };
30
+ export function createAuthStore(auth) {
31
+ const state = createObservable(INITIAL_AUTH_STATE);
32
+ // A slow answer must not overwrite a newer one.
33
+ let latest = 0;
34
+ return {
35
+ state,
36
+ async refresh() {
37
+ const ticket = (latest += 1);
38
+ try {
39
+ const user = await auth.getUser();
40
+ if (ticket !== latest)
41
+ return;
42
+ state.set(user
43
+ ? { status: "authenticated", user, error: null }
44
+ : { status: "unauthenticated", user: null, error: null });
45
+ }
46
+ catch (err) {
47
+ if (ticket !== latest)
48
+ return;
49
+ // Keep the last known user: a failed refresh does not sign anyone out.
50
+ state.set({
51
+ status: "error",
52
+ user: state.get().user,
53
+ error: asError(err),
54
+ });
55
+ }
56
+ },
57
+ };
58
+ }
59
+ export function createResourceStore(load, options = {}) {
60
+ const state = createObservable({
61
+ status: "idle",
62
+ data: null,
63
+ error: null,
64
+ });
65
+ let latest = 0;
66
+ async function refresh() {
67
+ const ticket = (latest += 1);
68
+ const previous = state.get().data;
69
+ state.set({ status: "loading", data: previous, error: null });
70
+ try {
71
+ const data = await load();
72
+ if (ticket === latest)
73
+ state.set({ status: "ready", data, error: null });
74
+ }
75
+ catch (err) {
76
+ if (ticket !== latest)
77
+ return;
78
+ const error = asError(err);
79
+ state.set({ status: "error", data: previous, error });
80
+ options.onError?.(error);
81
+ }
82
+ }
83
+ return {
84
+ state,
85
+ refresh,
86
+ start() {
87
+ void refresh();
88
+ const timer = options.pollIntervalMs
89
+ ? setInterval(() => void refresh(), options.pollIntervalMs)
90
+ : undefined;
91
+ return () => {
92
+ // A load in flight when the consumer leaves must not write afterwards.
93
+ latest += 1;
94
+ if (timer !== undefined)
95
+ clearInterval(timer);
96
+ };
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ declare const featureValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
3
+ /** Claims of an ID token issued by auth-service. Unknown claims are kept as is. */
4
+ export declare const idTokenClaimsSchema: z.ZodObject<{
5
+ roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ permissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ features: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
8
+ plan: z.ZodOptional<z.ZodString>;
9
+ org_id: z.ZodOptional<z.ZodString>;
10
+ sid: z.ZodOptional<z.ZodString>;
11
+ azp: z.ZodOptional<z.ZodString>;
12
+ name: z.ZodOptional<z.ZodString>;
13
+ picture: z.ZodOptional<z.ZodString>;
14
+ given_name: z.ZodOptional<z.ZodString>;
15
+ family_name: z.ZodOptional<z.ZodString>;
16
+ company: z.ZodOptional<z.ZodString>;
17
+ updated_at: z.ZodOptional<z.ZodNumber>;
18
+ email: z.ZodOptional<z.ZodString>;
19
+ email_verified: z.ZodOptional<z.ZodBoolean>;
20
+ phone_number: z.ZodOptional<z.ZodString>;
21
+ iss: z.ZodString;
22
+ sub: z.ZodString;
23
+ aud: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
24
+ exp: z.ZodNumber;
25
+ iat: z.ZodNumber;
26
+ }, z.core.$loose>;
27
+ /**
28
+ * Claims of a JWT access token. `azp` is the OAuth client that obtained the
29
+ * token and `scope` is a space-separated list.
30
+ */
31
+ export declare const accessTokenClaimsSchema: z.ZodObject<{
32
+ roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
+ permissions: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ features: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
35
+ plan: z.ZodOptional<z.ZodString>;
36
+ org_id: z.ZodOptional<z.ZodString>;
37
+ azp: z.ZodString;
38
+ scope: z.ZodOptional<z.ZodString>;
39
+ sid: z.ZodOptional<z.ZodString>;
40
+ iss: z.ZodString;
41
+ sub: z.ZodString;
42
+ aud: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
43
+ exp: z.ZodNumber;
44
+ iat: z.ZodNumber;
45
+ }, z.core.$loose>;
46
+ export type FeatureValue = z.infer<typeof featureValueSchema>;
47
+ export type IdTokenClaims = z.infer<typeof idTokenClaimsSchema>;
48
+ export type AccessTokenClaims = z.infer<typeof accessTokenClaimsSchema>;
49
+ export type PermissionAction = "read" | "write";
50
+ type EntitlementClaims = Pick<IdTokenClaims, "roles" | "permissions" | "features">;
51
+ export declare function hasRole(claims: EntitlementClaims, role: string): boolean;
52
+ /**
53
+ * `resource` grants read access and `resource.write` grants write access,
54
+ * which also implies read: auth-service keeps only the highest action.
55
+ */
56
+ export declare function hasPermission(claims: EntitlementClaims, resource: string, action?: PermissionAction): boolean;
57
+ /** A feature is enabled when present and neither `false` nor `0` nor `""`. */
58
+ export declare function hasFeature(claims: EntitlementClaims, key: string): boolean;
59
+ export declare function getFeature(claims: EntitlementClaims, key: string): FeatureValue | undefined;
60
+ export {};
package/dist/claims.js ADDED
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ const featureValueSchema = z.union([z.string(), z.number(), z.boolean()]);
3
+ const audienceSchema = z.union([z.string(), z.array(z.string())]);
4
+ // Present only when the matching scope was granted.
5
+ const entitlementShape = {
6
+ roles: z.array(z.string()).optional(),
7
+ permissions: z.array(z.string()).optional(),
8
+ features: z.record(z.string(), featureValueSchema).optional(),
9
+ plan: z.string().optional(),
10
+ org_id: z.string().optional(),
11
+ };
12
+ const registeredShape = {
13
+ iss: z.string(),
14
+ sub: z.string(),
15
+ aud: audienceSchema,
16
+ exp: z.number(),
17
+ iat: z.number(),
18
+ };
19
+ /** Claims of an ID token issued by auth-service. Unknown claims are kept as is. */
20
+ export const idTokenClaimsSchema = z.looseObject({
21
+ ...registeredShape,
22
+ sid: z.string().optional(),
23
+ azp: z.string().optional(),
24
+ name: z.string().optional(),
25
+ picture: z.string().optional(),
26
+ given_name: z.string().optional(),
27
+ family_name: z.string().optional(),
28
+ company: z.string().optional(),
29
+ updated_at: z.number().optional(),
30
+ email: z.string().optional(),
31
+ email_verified: z.boolean().optional(),
32
+ phone_number: z.string().optional(),
33
+ ...entitlementShape,
34
+ });
35
+ /**
36
+ * Claims of a JWT access token. `azp` is the OAuth client that obtained the
37
+ * token and `scope` is a space-separated list.
38
+ */
39
+ export const accessTokenClaimsSchema = z.looseObject({
40
+ ...registeredShape,
41
+ azp: z.string(),
42
+ scope: z.string().optional(),
43
+ sid: z.string().optional(),
44
+ ...entitlementShape,
45
+ });
46
+ export function hasRole(claims, role) {
47
+ return claims.roles?.includes(role) ?? false;
48
+ }
49
+ /**
50
+ * `resource` grants read access and `resource.write` grants write access,
51
+ * which also implies read: auth-service keeps only the highest action.
52
+ */
53
+ export function hasPermission(claims, resource, action = "read") {
54
+ const permissions = claims.permissions;
55
+ if (!permissions)
56
+ return false;
57
+ if (permissions.includes(`${resource}.write`))
58
+ return true;
59
+ return action === "read" && permissions.includes(resource);
60
+ }
61
+ /** A feature is enabled when present and neither `false` nor `0` nor `""`. */
62
+ export function hasFeature(claims, key) {
63
+ const value = claims.features?.[key];
64
+ return value !== undefined && value !== false && value !== 0 && value !== "";
65
+ }
66
+ export function getFeature(claims, key) {
67
+ return claims.features?.[key];
68
+ }
@@ -0,0 +1,54 @@
1
+ import type { IdTokenClaims } from "./claims.js";
2
+ import type { AuthClientConfig, ResolvedAuthClientConfig } from "./config.js";
3
+ import type { IntrospectionResult } from "./introspection.js";
4
+ import type { JWTPayload } from "jose";
5
+ import type { VerifyJwtOptions } from "./jwt.js";
6
+ import type { LoginHandshake, LoginRequest, LoginResult, ClientCredentialsOptions, LogoutUrlOptions, RefreshOptions, StartLoginOptions, TokenSet, TokenTypeHint } from "./types.js";
7
+ export interface AuthClientOptions {
8
+ /** Replaces the global `fetch`, e.g. to add a proxy or an observability layer. */
9
+ readonly fetch?: typeof fetch;
10
+ }
11
+ /**
12
+ * OIDC relying party for auth-service: authorization-code grant with PKCE,
13
+ * refresh with RFC 8707 resource narrowing, logout, revocation and
14
+ * introspection. It keeps no session: callers persist the handshake, the
15
+ * tokens and the claims where it suits them.
16
+ */
17
+ export declare class AuthClient {
18
+ #private;
19
+ readonly config: ResolvedAuthClientConfig;
20
+ /** The fetch used for every request, shared with the REST API clients built on this one. */
21
+ readonly fetch: typeof fetch;
22
+ constructor(config: AuthClientConfig, options?: AuthClientOptions);
23
+ startLogin(options?: StartLoginOptions): Promise<LoginRequest>;
24
+ /**
25
+ * Completes the sign-in from the callback URL. `resource` is deliberately not
26
+ * sent on the code exchange: the token is then issued for every audience
27
+ * requested at sign-in and each refresh narrows it to one.
28
+ */
29
+ handleCallback(callbackUrl: URL | string, handshake: LoginHandshake): Promise<LoginResult>;
30
+ verifyIdToken(idToken: string): Promise<IdTokenClaims>;
31
+ /**
32
+ * Exchanges a refresh token for fresh tokens. The provider rotates the
33
+ * refresh token on every use: the returned one replaces the presented one,
34
+ * and a replayed stale token revokes the whole family. Concurrent callers
35
+ * should go through a grant manager. `scope` is never sent: the rotated
36
+ * token would be left with the scopes of that request only.
37
+ */
38
+ refresh(refreshToken: string, options?: RefreshOptions): Promise<TokenSet>;
39
+ /** URL ending the provider session (RP-initiated logout). */
40
+ logoutUrl(options?: LogoutUrlOptions): Promise<string>;
41
+ revoke(token: string, hint?: TokenTypeHint): Promise<void>;
42
+ introspect(token: string, hint?: TokenTypeHint): Promise<IntrospectionResult>;
43
+ /**
44
+ * Verifies the signature, issuer, audience and expiry of a JWT signed by the
45
+ * provider. Low-level: prefer the bearer and logout token verifiers.
46
+ */
47
+ verifyJwt(token: string, options: VerifyJwtOptions): Promise<JWTPayload>;
48
+ /**
49
+ * `client_credentials` grant for the application itself: no user is bound to
50
+ * the token. Needs a confidential client.
51
+ */
52
+ clientCredentials(options?: ClientCredentialsOptions): Promise<TokenSet>;
53
+ }
54
+ export declare function createAuthClient(config: AuthClientConfig, options?: AuthClientOptions): AuthClient;
package/dist/client.js ADDED
@@ -0,0 +1,204 @@
1
+ import * as oidc from "openid-client";
2
+ import { idTokenClaimsSchema } from "./claims.js";
3
+ import { parseAuthClientConfig } from "./config.js";
4
+ import { ERR } from "./errors.js";
5
+ import { introspectionSchema } from "./introspection.js";
6
+ import { toAuthAiError } from "./oauth-errors.js";
7
+ import { createProviderLoader } from "./provider.js";
8
+ function toTokenSet(response) {
9
+ const expiresIn = response.expiresIn();
10
+ if (expiresIn === undefined) {
11
+ throw ERR.OIDC_003("The token response carries no expires_in");
12
+ }
13
+ return {
14
+ accessToken: response.access_token,
15
+ tokenType: response.token_type,
16
+ expiresAt: Math.floor(Date.now() / 1000) + expiresIn,
17
+ ...(response.scope ? { scope: response.scope } : {}),
18
+ ...(response.refresh_token ? { refreshToken: response.refresh_token } : {}),
19
+ ...(response.id_token ? { idToken: response.id_token } : {}),
20
+ };
21
+ }
22
+ /**
23
+ * OIDC relying party for auth-service: authorization-code grant with PKCE,
24
+ * refresh with RFC 8707 resource narrowing, logout, revocation and
25
+ * introspection. It keeps no session: callers persist the handshake, the
26
+ * tokens and the claims where it suits them.
27
+ */
28
+ export class AuthClient {
29
+ config;
30
+ /** The fetch used for every request, shared with the REST API clients built on this one. */
31
+ fetch;
32
+ #loadProvider;
33
+ constructor(config, options = {}) {
34
+ this.config = parseAuthClientConfig(config);
35
+ this.fetch =
36
+ options.fetch ?? ((input, init) => globalThis.fetch(input, init));
37
+ this.#loadProvider = createProviderLoader(this.config, options.fetch);
38
+ }
39
+ async startLogin(options = {}) {
40
+ const { configuration } = await this.#loadProvider();
41
+ const handshake = {
42
+ state: oidc.randomState(),
43
+ nonce: oidc.randomNonce(),
44
+ codeVerifier: oidc.randomPKCECodeVerifier(),
45
+ };
46
+ const parameters = new URLSearchParams({
47
+ redirect_uri: this.config.redirectUri,
48
+ scope: this.config.scopes.join(" "),
49
+ state: handshake.state,
50
+ nonce: handshake.nonce,
51
+ code_challenge: await oidc.calculatePKCECodeChallenge(handshake.codeVerifier),
52
+ code_challenge_method: "S256",
53
+ });
54
+ if (options.prompt)
55
+ parameters.set("prompt", options.prompt);
56
+ if (options.loginHint)
57
+ parameters.set("login_hint", options.loginHint);
58
+ for (const resource of this.config.resources) {
59
+ parameters.append("resource", resource);
60
+ }
61
+ return {
62
+ url: oidc.buildAuthorizationUrl(configuration, parameters).href,
63
+ handshake,
64
+ };
65
+ }
66
+ /**
67
+ * Completes the sign-in from the callback URL. `resource` is deliberately not
68
+ * sent on the code exchange: the token is then issued for every audience
69
+ * requested at sign-in and each refresh narrows it to one.
70
+ */
71
+ async handleCallback(callbackUrl, handshake) {
72
+ const provider = await this.#loadProvider();
73
+ let response;
74
+ try {
75
+ response = await oidc.authorizationCodeGrant(provider.configuration, new URL(callbackUrl), {
76
+ pkceCodeVerifier: handshake.codeVerifier,
77
+ expectedState: handshake.state,
78
+ expectedNonce: handshake.nonce,
79
+ idTokenExpected: true,
80
+ });
81
+ }
82
+ catch (err) {
83
+ throw toAuthAiError(err, "authorization-response");
84
+ }
85
+ const tokens = toTokenSet(response);
86
+ if (!tokens.idToken) {
87
+ throw ERR.OIDC_002("The token response carries no ID token");
88
+ }
89
+ const claims = await this.#verifyIdToken(provider, tokens.idToken);
90
+ if (claims["nonce"] !== handshake.nonce) {
91
+ throw ERR.TOK_001("ID token nonce mismatch");
92
+ }
93
+ return {
94
+ tokens: { ...tokens, idToken: tokens.idToken },
95
+ claims,
96
+ sessionId: claims.sid,
97
+ };
98
+ }
99
+ async verifyIdToken(idToken) {
100
+ return this.#verifyIdToken(await this.#loadProvider(), idToken);
101
+ }
102
+ /**
103
+ * Exchanges a refresh token for fresh tokens. The provider rotates the
104
+ * refresh token on every use: the returned one replaces the presented one,
105
+ * and a replayed stale token revokes the whole family. Concurrent callers
106
+ * should go through a grant manager. `scope` is never sent: the rotated
107
+ * token would be left with the scopes of that request only.
108
+ */
109
+ async refresh(refreshToken, options = {}) {
110
+ const { configuration } = await this.#loadProvider();
111
+ try {
112
+ const response = await oidc.refreshTokenGrant(configuration, refreshToken, options.resource ? { resource: options.resource } : undefined);
113
+ return toTokenSet(response);
114
+ }
115
+ catch (err) {
116
+ throw toAuthAiError(err, "token-request");
117
+ }
118
+ }
119
+ /** URL ending the provider session (RP-initiated logout). */
120
+ async logoutUrl(options = {}) {
121
+ const { configuration } = await this.#loadProvider();
122
+ if (!configuration.serverMetadata().end_session_endpoint) {
123
+ throw ERR.OIDC_004("The provider advertises no end_session_endpoint");
124
+ }
125
+ const parameters = new URLSearchParams();
126
+ if (this.config.postLogoutRedirectUri) {
127
+ parameters.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
128
+ }
129
+ if (options.idTokenHint) {
130
+ parameters.set("id_token_hint", options.idTokenHint);
131
+ }
132
+ if (options.state)
133
+ parameters.set("state", options.state);
134
+ return oidc.buildEndSessionUrl(configuration, parameters).href;
135
+ }
136
+ async revoke(token, hint) {
137
+ const { configuration } = await this.#loadProvider();
138
+ if (!configuration.serverMetadata().revocation_endpoint) {
139
+ throw ERR.OIDC_004("The provider advertises no revocation_endpoint");
140
+ }
141
+ try {
142
+ await oidc.tokenRevocation(configuration, token, hint ? { token_type_hint: hint } : undefined);
143
+ }
144
+ catch (err) {
145
+ throw toAuthAiError(err, "token-request");
146
+ }
147
+ }
148
+ async introspect(token, hint) {
149
+ const { configuration } = await this.#loadProvider();
150
+ if (!configuration.serverMetadata().introspection_endpoint) {
151
+ throw ERR.OIDC_004("The provider advertises no introspection_endpoint");
152
+ }
153
+ try {
154
+ const response = await oidc.tokenIntrospection(configuration, token, hint ? { token_type_hint: hint } : undefined);
155
+ return introspectionSchema.parse(response);
156
+ }
157
+ catch (err) {
158
+ throw toAuthAiError(err, "token-request");
159
+ }
160
+ }
161
+ /**
162
+ * Verifies the signature, issuer, audience and expiry of a JWT signed by the
163
+ * provider. Low-level: prefer the bearer and logout token verifiers.
164
+ */
165
+ async verifyJwt(token, options) {
166
+ return (await this.#loadProvider()).verifyJwt(token, options);
167
+ }
168
+ /**
169
+ * `client_credentials` grant for the application itself: no user is bound to
170
+ * the token. Needs a confidential client.
171
+ */
172
+ async clientCredentials(options = {}) {
173
+ if (!this.config.clientSecret) {
174
+ throw ERR.CFG_001("clientCredentials requires a clientSecret");
175
+ }
176
+ const { configuration } = await this.#loadProvider();
177
+ const parameters = {};
178
+ if (options.scope)
179
+ parameters["scope"] = options.scope;
180
+ if (options.resource)
181
+ parameters["resource"] = options.resource;
182
+ try {
183
+ return toTokenSet(await oidc.clientCredentialsGrant(configuration, parameters));
184
+ }
185
+ catch (err) {
186
+ throw toAuthAiError(err, "token-request");
187
+ }
188
+ }
189
+ async #verifyIdToken(provider, idToken) {
190
+ const payload = await provider.verifyJwt(idToken, {
191
+ audience: this.config.clientId,
192
+ });
193
+ const claims = idTokenClaimsSchema.safeParse(payload);
194
+ if (!claims.success) {
195
+ throw ERR.TOK_001("The ID token claims have an unexpected shape", {
196
+ cause: claims.error,
197
+ });
198
+ }
199
+ return claims.data;
200
+ }
201
+ }
202
+ export function createAuthClient(config, options = {}) {
203
+ return new AuthClient(config, options);
204
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ export declare const DEFAULT_SCOPES: readonly ["openid", "profile", "email", "roles", "permissions", "features", "offline_access"];
3
+ export declare const WALLET_SELF_SCOPE = "wallet:self:read";
4
+ /**
5
+ * With a gateway, the sign-in also asks for the audiences of the gateway and
6
+ * of auth-service (which serves the wallet) and for the wallet scope: this is
7
+ * what the application's "LiteLLM support" switch authorises.
8
+ */
9
+ export declare const authClientConfigSchema: z.ZodPipe<z.ZodObject<{
10
+ issuer: z.ZodURL;
11
+ clientId: z.ZodString;
12
+ clientSecret: z.ZodOptional<z.ZodString>;
13
+ tokenEndpointAuthMethod: z.ZodDefault<z.ZodEnum<{
14
+ client_secret_basic: "client_secret_basic";
15
+ client_secret_post: "client_secret_post";
16
+ }>>;
17
+ redirectUri: z.ZodURL;
18
+ postLogoutRedirectUri: z.ZodOptional<z.ZodURL>;
19
+ scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
20
+ resources: z.ZodDefault<z.ZodArray<z.ZodURL>>;
21
+ gatewayUrl: z.ZodOptional<z.ZodURL>;
22
+ clockToleranceSeconds: z.ZodDefault<z.ZodNumber>;
23
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
24
+ }, z.core.$strip>, z.ZodTransform<{
25
+ issuer: string;
26
+ clientId: string;
27
+ tokenEndpointAuthMethod: "client_secret_basic" | "client_secret_post";
28
+ redirectUri: string;
29
+ scopes: string[];
30
+ resources: string[];
31
+ clockToleranceSeconds: number;
32
+ timeoutMs: number;
33
+ clientSecret?: string | undefined;
34
+ postLogoutRedirectUri?: string | undefined;
35
+ gatewayUrl?: string | undefined;
36
+ }, {
37
+ issuer: string;
38
+ clientId: string;
39
+ tokenEndpointAuthMethod: "client_secret_basic" | "client_secret_post";
40
+ redirectUri: string;
41
+ scopes: string[];
42
+ resources: string[];
43
+ clockToleranceSeconds: number;
44
+ timeoutMs: number;
45
+ clientSecret?: string | undefined;
46
+ postLogoutRedirectUri?: string | undefined;
47
+ gatewayUrl?: string | undefined;
48
+ }>>;
49
+ export type AuthClientConfig = z.input<typeof authClientConfigSchema>;
50
+ export type ResolvedAuthClientConfig = z.output<typeof authClientConfigSchema>;
51
+ export declare function parseAuthClientConfig(input: unknown): ResolvedAuthClientConfig;
package/dist/config.js ADDED
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { ERR } from "./errors.js";
3
+ export const DEFAULT_SCOPES = [
4
+ "openid",
5
+ "profile",
6
+ "email",
7
+ "roles",
8
+ "permissions",
9
+ "features",
10
+ "offline_access",
11
+ ];
12
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
13
+ // Scope that lets a user token read the user's own wallet.
14
+ export const WALLET_SELF_SCOPE = "wallet:self:read";
15
+ const issuerSchema = z
16
+ .url({ protocol: /^https$/ })
17
+ .refine((value) => !value.endsWith("/"), {
18
+ message: "must not end with a slash: it is compared verbatim to `iss`",
19
+ });
20
+ // http is allowed for loopback only, so local development needs no tunnel.
21
+ const redirectUriSchema = z.url().refine((value) => {
22
+ // Zod runs refinements even when `z.url()` failed.
23
+ if (!URL.canParse(value))
24
+ return false;
25
+ const url = new URL(value);
26
+ return (url.protocol === "https:" ||
27
+ (url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname)));
28
+ }, { message: "must be https (http is only accepted for localhost)" });
29
+ // An audience is compared verbatim to `aud`: a trailing slash would never match.
30
+ const gatewayUrlSchema = redirectUriSchema.refine((value) => !value.endsWith("/"), { message: "must not end with a slash: it is compared verbatim to `aud`" });
31
+ const authClientConfigShape = z.object({
32
+ issuer: issuerSchema,
33
+ clientId: z.string().min(1),
34
+ // Absent for a public client, which then relies on PKCE alone.
35
+ clientSecret: z.string().min(1).optional(),
36
+ tokenEndpointAuthMethod: z
37
+ .enum(["client_secret_basic", "client_secret_post"])
38
+ .default("client_secret_basic"),
39
+ redirectUri: redirectUriSchema,
40
+ postLogoutRedirectUri: redirectUriSchema.optional(),
41
+ scopes: z
42
+ .array(z.string().min(1))
43
+ .refine((scopes) => scopes.includes("openid"), {
44
+ message: "must include `openid`",
45
+ })
46
+ .default([...DEFAULT_SCOPES]),
47
+ // RFC 8707 audiences requested at sign-in; each refresh narrows to one.
48
+ resources: z.array(z.url()).default([]),
49
+ // Public URL of the LiteLLM gateway: turns on the gateway and wallet audiences.
50
+ gatewayUrl: gatewayUrlSchema.optional(),
51
+ clockToleranceSeconds: z.number().int().min(0).max(300).default(5),
52
+ timeoutMs: z.number().int().positive().default(10_000),
53
+ });
54
+ /**
55
+ * With a gateway, the sign-in also asks for the audiences of the gateway and
56
+ * of auth-service (which serves the wallet) and for the wallet scope: this is
57
+ * what the application's "LiteLLM support" switch authorises.
58
+ */
59
+ export const authClientConfigSchema = authClientConfigShape.transform((config) => {
60
+ if (!config.gatewayUrl)
61
+ return config;
62
+ return {
63
+ ...config,
64
+ scopes: [...new Set([...config.scopes, WALLET_SELF_SCOPE])],
65
+ resources: [
66
+ ...new Set([...config.resources, config.gatewayUrl, config.issuer]),
67
+ ],
68
+ };
69
+ });
70
+ export function parseAuthClientConfig(input) {
71
+ const result = authClientConfigSchema.safeParse(input);
72
+ if (result.success)
73
+ return result.data;
74
+ const issues = result.error.issues
75
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
76
+ .join("; ");
77
+ throw ERR.CFG_001(`Invalid SDK configuration: ${issues}`, {
78
+ cause: result.error,
79
+ });
80
+ }