@codefox-inc/oauth-provider 0.2.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 (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +572 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/auth-config.d.ts +85 -0
  8. package/dist/client/auth-config.d.ts.map +1 -0
  9. package/dist/client/auth-config.js +81 -0
  10. package/dist/client/auth-config.js.map +1 -0
  11. package/dist/client/auth-helper.d.ts +81 -0
  12. package/dist/client/auth-helper.d.ts.map +1 -0
  13. package/dist/client/auth-helper.js +97 -0
  14. package/dist/client/auth-helper.js.map +1 -0
  15. package/dist/client/index.d.ts +189 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +230 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/routes.d.ts +94 -0
  20. package/dist/client/routes.d.ts.map +1 -0
  21. package/dist/client/routes.js +113 -0
  22. package/dist/client/routes.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +44 -0
  24. package/dist/component/_generated/api.d.ts.map +1 -0
  25. package/dist/component/_generated/api.js +31 -0
  26. package/dist/component/_generated/api.js.map +1 -0
  27. package/dist/component/_generated/component.d.ts +123 -0
  28. package/dist/component/_generated/component.d.ts.map +1 -0
  29. package/dist/component/_generated/component.js +11 -0
  30. package/dist/component/_generated/component.js.map +1 -0
  31. package/dist/component/_generated/dataModel.d.ts +46 -0
  32. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  33. package/dist/component/_generated/dataModel.js +11 -0
  34. package/dist/component/_generated/dataModel.js.map +1 -0
  35. package/dist/component/_generated/server.d.ts +121 -0
  36. package/dist/component/_generated/server.d.ts.map +1 -0
  37. package/dist/component/_generated/server.js +78 -0
  38. package/dist/component/_generated/server.js.map +1 -0
  39. package/dist/component/clientManagement.d.ts +39 -0
  40. package/dist/component/clientManagement.d.ts.map +1 -0
  41. package/dist/component/clientManagement.js +169 -0
  42. package/dist/component/clientManagement.js.map +1 -0
  43. package/dist/component/constants.d.ts +31 -0
  44. package/dist/component/constants.d.ts.map +1 -0
  45. package/dist/component/constants.js +36 -0
  46. package/dist/component/constants.js.map +1 -0
  47. package/dist/component/convex.config.d.ts +3 -0
  48. package/dist/component/convex.config.d.ts.map +1 -0
  49. package/dist/component/convex.config.js +3 -0
  50. package/dist/component/convex.config.js.map +1 -0
  51. package/dist/component/handlers.d.ts +143 -0
  52. package/dist/component/handlers.d.ts.map +1 -0
  53. package/dist/component/handlers.js +624 -0
  54. package/dist/component/handlers.js.map +1 -0
  55. package/dist/component/mutations.d.ts +111 -0
  56. package/dist/component/mutations.d.ts.map +1 -0
  57. package/dist/component/mutations.js +459 -0
  58. package/dist/component/mutations.js.map +1 -0
  59. package/dist/component/queries.d.ts +127 -0
  60. package/dist/component/queries.d.ts.map +1 -0
  61. package/dist/component/queries.js +145 -0
  62. package/dist/component/queries.js.map +1 -0
  63. package/dist/component/schema.d.ts +116 -0
  64. package/dist/component/schema.d.ts.map +1 -0
  65. package/dist/component/schema.js +77 -0
  66. package/dist/component/schema.js.map +1 -0
  67. package/dist/component/token_security.d.ts +53 -0
  68. package/dist/component/token_security.d.ts.map +1 -0
  69. package/dist/component/token_security.js +91 -0
  70. package/dist/component/token_security.js.map +1 -0
  71. package/dist/lib/convex-types.d.ts +21 -0
  72. package/dist/lib/convex-types.d.ts.map +1 -0
  73. package/dist/lib/convex-types.js +2 -0
  74. package/dist/lib/convex-types.js.map +1 -0
  75. package/dist/lib/oauth.d.ts +123 -0
  76. package/dist/lib/oauth.d.ts.map +1 -0
  77. package/dist/lib/oauth.js +295 -0
  78. package/dist/lib/oauth.js.map +1 -0
  79. package/dist/react/index.d.ts +2 -0
  80. package/dist/react/index.d.ts.map +1 -0
  81. package/dist/react/index.js +6 -0
  82. package/dist/react/index.js.map +1 -0
  83. package/package.json +121 -0
  84. package/src/client/__tests__/auth-config.test.ts +244 -0
  85. package/src/client/__tests__/auth-helper.test.ts +273 -0
  86. package/src/client/__tests__/oauth-provider.test.ts +418 -0
  87. package/src/client/__tests__/routes.test.ts +428 -0
  88. package/src/client/_generated/_ignore.ts +1 -0
  89. package/src/client/auth-config.ts +157 -0
  90. package/src/client/auth-helper.ts +201 -0
  91. package/src/client/index.ts +326 -0
  92. package/src/client/routes.ts +251 -0
  93. package/src/component/__tests__/oauth.test.ts +3310 -0
  94. package/src/component/__tests__/rfc-compliance.test.ts +788 -0
  95. package/src/component/__tests__/token-security.test.ts +133 -0
  96. package/src/component/_generated/api.ts +60 -0
  97. package/src/component/_generated/component.ts +201 -0
  98. package/src/component/_generated/dataModel.ts +60 -0
  99. package/src/component/_generated/server.ts +156 -0
  100. package/src/component/clientManagement.ts +189 -0
  101. package/src/component/constants.ts +40 -0
  102. package/src/component/convex.config.ts +3 -0
  103. package/src/component/handlers.ts +964 -0
  104. package/src/component/mutations.ts +531 -0
  105. package/src/component/queries.ts +165 -0
  106. package/src/component/schema.ts +92 -0
  107. package/src/component/token_security.ts +102 -0
  108. package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
  109. package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
  110. package/src/lib/convex-types.ts +37 -0
  111. package/src/lib/oauth.ts +412 -0
  112. package/src/react/index.ts +7 -0
  113. package/src/test.ts +21 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Auth Helper for handling both Convex Auth and OAuth tokens
3
+ *
4
+ * This helper provides a unified way to get the current user
5
+ * regardless of whether they authenticated via Convex Auth (session)
6
+ * or OAuth token (MCP clients).
7
+ */
8
+
9
+ import {
10
+ isOAuthToken as checkIsOAuthToken,
11
+ getOAuthClientId,
12
+ DEFAULT_OAUTH_ISSUER_PATTERN,
13
+ } from "../lib/oauth.js";
14
+
15
+ /**
16
+ * Context types (simplified for compatibility)
17
+ */
18
+
19
+ type QueryCtx = any;
20
+
21
+ type MutationCtx = any;
22
+
23
+ /**
24
+ * Configuration for the auth helper
25
+ */
26
+ export interface AuthHelperConfig {
27
+ /**
28
+ * Convex Auth provider names to check for subject ID lookup
29
+ * @example ["anonymous", "password", "google"]
30
+ */
31
+ providers?: string[];
32
+
33
+ /**
34
+ * Custom function to get auth user ID from Convex Auth
35
+ * If not provided, you must pass it when calling methods
36
+ */
37
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>;
38
+
39
+ /**
40
+ * Function to check if OAuth authorization is still valid
41
+ * Called for OAuth token requests to verify the authorization wasn't revoked
42
+ * @param ctx - Query/Mutation context
43
+ * @param userId - User ID from JWT
44
+ * @param clientId - Client ID from JWT (may be undefined if not in JWT)
45
+ * @returns true if authorization is valid, false if revoked
46
+ */
47
+ checkAuthorization?: (ctx: QueryCtx | MutationCtx, userId: string, clientId?: string) => Promise<boolean>;
48
+
49
+ /**
50
+ * OAuth issuer URL pattern to identify OAuth tokens
51
+ * If the token's issuer contains this string, authorization check is enforced
52
+ * @example "/oauth"
53
+ */
54
+ oauthIssuerPattern?: string;
55
+ }
56
+
57
+ /**
58
+ * Auth Helper instance
59
+ */
60
+ export interface AuthHelper {
61
+ /**
62
+ * Get the current user ID from either Convex Auth or OAuth token
63
+ * Returns null if not authenticated
64
+ */
65
+ getCurrentUserId: (
66
+ ctx: QueryCtx | MutationCtx,
67
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
68
+ ) => Promise<string | null>;
69
+
70
+ /**
71
+ * Get the current user document from the database
72
+ * Returns null if not authenticated or user not found
73
+ */
74
+ getCurrentUser: <T>(
75
+ ctx: QueryCtx | MutationCtx,
76
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
77
+ ) => Promise<T | null>;
78
+
79
+ /**
80
+ * Require authentication - throws if not authenticated
81
+ */
82
+ requireAuth: (
83
+ ctx: QueryCtx | MutationCtx,
84
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
85
+ ) => Promise<string>;
86
+ }
87
+
88
+ /**
89
+ * Create an auth helper for handling both Convex Auth and OAuth tokens
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { createAuthHelper } from "@codefox-inc/oauth-provider";
94
+ * import { getAuthUserId } from "./auth";
95
+ *
96
+ * const authHelper = createAuthHelper({
97
+ * providers: ["anonymous"],
98
+ * });
99
+ *
100
+ * // In a query/mutation:
101
+ * const userId = await authHelper.getCurrentUserId(ctx, getAuthUserId);
102
+ * const user = await authHelper.getCurrentUser(ctx, getAuthUserId);
103
+ * ```
104
+ */
105
+ export function createAuthHelper(config: AuthHelperConfig = {}): AuthHelper {
106
+ const {
107
+ providers = ["anonymous"],
108
+ getAuthUserId: defaultGetAuthUserId,
109
+ checkAuthorization,
110
+ oauthIssuerPattern = DEFAULT_OAUTH_ISSUER_PATTERN,
111
+ } = config;
112
+
113
+ async function getCurrentUserId(
114
+ ctx: QueryCtx | MutationCtx,
115
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
116
+ ): Promise<string | null> {
117
+ const authFn = getAuthUserId ?? defaultGetAuthUserId;
118
+
119
+ // First, check if this is an OAuth token by looking at identity issuer
120
+ const identity = await ctx.auth.getUserIdentity();
121
+ const isOAuth = checkIsOAuthToken(identity, oauthIssuerPattern);
122
+
123
+ // If this is an OAuth token, skip Convex Auth and enforce authorization check
124
+ if (isOAuth && identity?.subject) {
125
+ const validId = ctx.db.normalizeId("users", identity.subject);
126
+ if (validId) {
127
+ // OAuth tokens MUST pass authorization check
128
+ if (checkAuthorization) {
129
+ const clientId = getOAuthClientId(identity);
130
+ const isValid = await checkAuthorization(ctx, validId, clientId);
131
+ if (!isValid) {
132
+ // Authorization was revoked - reject access
133
+ return null;
134
+ }
135
+ }
136
+ return validId;
137
+ }
138
+ // OAuth token but invalid user ID
139
+ return null;
140
+ }
141
+
142
+ // 1. Try Convex Auth (session-based, getAuthUserId)
143
+ if (authFn) {
144
+ const userIdOrSubject = await authFn(ctx);
145
+ if (userIdOrSubject) {
146
+ // Handle "userId|sessionId" format from some Convex Auth versions
147
+ const idToLookup = userIdOrSubject.includes("|")
148
+ ? userIdOrSubject.split("|")[0]
149
+ : userIdOrSubject;
150
+
151
+ // Try as Convex ID
152
+ const validId = ctx.db.normalizeId("users", idToLookup);
153
+ if (validId) {
154
+ return validId;
155
+ }
156
+
157
+ // Try as Subject ID via authAccounts
158
+ for (const provider of providers) {
159
+
160
+ const account = await (ctx.db as any)
161
+ .query("authAccounts")
162
+ .withIndex("providerAndAccountId", (q: { eq: (field: string, value: string) => { eq: (field: string, value: string) => unknown } }) =>
163
+ q.eq("provider", provider).eq("providerAccountId", idToLookup)
164
+ )
165
+ .unique();
166
+ if (account) {
167
+ return account.userId;
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ async function getCurrentUser<T>(
177
+ ctx: QueryCtx | MutationCtx,
178
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
179
+ ): Promise<T | null> {
180
+ const userId = await getCurrentUserId(ctx, getAuthUserId);
181
+ if (!userId) return null;
182
+ return ctx.db.get(userId) as Promise<T | null>;
183
+ }
184
+
185
+ async function requireAuth(
186
+ ctx: QueryCtx | MutationCtx,
187
+ getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
188
+ ): Promise<string> {
189
+ const userId = await getCurrentUserId(ctx, getAuthUserId);
190
+ if (!userId) {
191
+ throw new Error("Not authenticated");
192
+ }
193
+ return userId;
194
+ }
195
+
196
+ return {
197
+ getCurrentUserId,
198
+ getCurrentUser,
199
+ requireAuth,
200
+ };
201
+ }
@@ -0,0 +1,326 @@
1
+ import {
2
+ openIdConfigurationHandler,
3
+ jwksHandler,
4
+ tokenHandler,
5
+ userInfoHandler,
6
+ registerHandler,
7
+ authorizeHandler,
8
+ oauthProtectedResourceHandler,
9
+ } from "../component/handlers.js";
10
+ import type { OAuthComponentAPI } from "../component/handlers.js";
11
+ import type { OAuthConfig, UserProfile } from "../lib/oauth.js";
12
+ import type { RunQueryCtx, RunMutationCtx, RunActionCtx } from "../lib/convex-types.js";
13
+
14
+ // Re-export types and utilities
15
+ export type { OAuthConfig, UserProfile } from "../lib/oauth.js";
16
+ export {
17
+ OAuthError,
18
+ verifyAccessToken,
19
+ isOAuthToken,
20
+ getOAuthClientId,
21
+ DEFAULT_OAUTH_ISSUER_PATTERN,
22
+ } from "../lib/oauth.js";
23
+ export { OAUTH_CONSTANTS, OAUTH_ERROR_CODES } from "../component/constants.js";
24
+
25
+ // Auth helper for getCurrentUser pattern
26
+ export { createAuthHelper } from "./auth-helper.js";
27
+ export type { AuthHelper, AuthHelperConfig } from "./auth-helper.js";
28
+
29
+ // Route registration helper
30
+ export { registerOAuthRoutes } from "./routes.js";
31
+ export type { RegisterOAuthRoutesOptions } from "./routes.js";
32
+
33
+ // Auth config generator
34
+ export { generateAuthConfig, createAuthConfig } from "./auth-config.js";
35
+ export type { AuthConfig, AuthProvider, GenerateAuthConfigOptions } from "./auth-config.js";
36
+
37
+ /**
38
+ * OAuth Provider Client Configuration
39
+ */
40
+ export type OAuthProviderConfig = OAuthConfig;
41
+
42
+ /**
43
+ * OAuth Provider SDK
44
+ *
45
+ * Usage:
46
+ * ```typescript
47
+ * import { OAuthProvider } from "@codefox-inc/oauth-provider";
48
+ * import { components } from "./_generated/api";
49
+ *
50
+ * const oauthProvider = new OAuthProvider(components.oauthProvider, {
51
+ * privateKey: process.env.OAUTH_PRIVATE_KEY!,
52
+ * publicKey: process.env.OAUTH_PUBLIC_KEY!,
53
+ * siteUrl: process.env.SITE_URL!,
54
+ * });
55
+ *
56
+ * // In http.ts
57
+ * http.route({
58
+ * path: "/oauth/.well-known/openid-configuration",
59
+ * method: "GET",
60
+ * handler: httpAction((ctx, req) => oauthProvider.handlers.openIdConfiguration(ctx, req)),
61
+ * });
62
+ * ```
63
+ */
64
+ export class OAuthProvider {
65
+ private config: OAuthProviderConfig;
66
+ private api: OAuthComponentAPI;
67
+
68
+ private component: any;
69
+
70
+ constructor(
71
+
72
+ component: any,
73
+ config: OAuthProviderConfig
74
+ ) {
75
+ this.config = config;
76
+ this.component = component;
77
+ this.api = this.createAPI(component);
78
+ }
79
+
80
+ getConfig(): OAuthProviderConfig {
81
+ return this.config;
82
+ }
83
+
84
+
85
+ private createAPI(component: any): OAuthComponentAPI {
86
+ return {
87
+ queries: {
88
+ getClient: (ctx, args) => ctx.runQuery(component.queries.getClient, args),
89
+ getRefreshToken: (ctx, args) => ctx.runQuery(component.queries.getRefreshToken, args),
90
+ getTokensByUser: (ctx, args) => ctx.runQuery(component.queries.getTokensByUser, args),
91
+ },
92
+ mutations: {
93
+ issueAuthorizationCode: (ctx, args) =>
94
+ ctx.runMutation(component.mutations.issueAuthorizationCode, args),
95
+ consumeAuthCode: (ctx, args) =>
96
+ ctx.runMutation(component.mutations.consumeAuthCode, args),
97
+ saveTokens: (ctx, args) =>
98
+ ctx.runMutation(component.mutations.saveTokens, args),
99
+ rotateRefreshToken: (ctx, args) =>
100
+ ctx.runMutation(component.mutations.rotateRefreshToken, args),
101
+ upsertAuthorization: (ctx, args) =>
102
+ ctx.runMutation(component.mutations.upsertAuthorization, args),
103
+ updateAuthorizationLastUsed: (ctx, args) =>
104
+ ctx.runMutation(component.mutations.updateAuthorizationLastUsed, args),
105
+ },
106
+ clientManagement: {
107
+ registerClient: (ctx, args) =>
108
+ ctx.runMutation(component.clientManagement.registerClient, args),
109
+ verifyClientSecret: (ctx, args) =>
110
+ ctx.runMutation(component.clientManagement.verifyClientSecret, args),
111
+ },
112
+ };
113
+ }
114
+
115
+ /**
116
+ * HTTP Handlers for mounting in http.ts
117
+ *
118
+ * Note: ctx expects Convex ActionCtx (HTTP Action context).
119
+ * RunActionCtx is used as the base type for compatibility.
120
+ */
121
+ get handlers() {
122
+ return {
123
+ /**
124
+ * OpenID Connect Discovery
125
+ * Mount at: /oauth/.well-known/openid-configuration
126
+ */
127
+ openIdConfiguration: (ctx: RunActionCtx, request: Request) =>
128
+ openIdConfigurationHandler(ctx as Parameters<typeof openIdConfigurationHandler>[0], request, this.config),
129
+
130
+ /**
131
+ * Authorization Endpoint
132
+ * Mount at: /oauth/authorize
133
+ */
134
+ authorize: (ctx: RunActionCtx, request: Request) =>
135
+ authorizeHandler(ctx as Parameters<typeof authorizeHandler>[0], request, this.config, this.api),
136
+
137
+ /**
138
+ * JWKS Endpoint
139
+ * Mount at: /oauth/.well-known/jwks.json
140
+ */
141
+ jwks: (ctx: RunActionCtx, request: Request) =>
142
+ jwksHandler(ctx as Parameters<typeof jwksHandler>[0], request, this.config),
143
+
144
+ /**
145
+ * Token Endpoint
146
+ * Mount at: /oauth/token
147
+ */
148
+ token: (ctx: RunActionCtx, request: Request) =>
149
+ tokenHandler(ctx as Parameters<typeof tokenHandler>[0], request, this.config, this.api),
150
+
151
+ /**
152
+ * UserInfo Endpoint
153
+ * Mount at: /oauth/userinfo
154
+ * Requires getUserProfile callback
155
+ */
156
+ userInfo: (ctx: RunActionCtx, request: Request, getUserProfile: (userId: string) => Promise<UserProfile | null>) =>
157
+ userInfoHandler(ctx as Parameters<typeof userInfoHandler>[0], request, this.config, getUserProfile),
158
+
159
+ /**
160
+ * Dynamic Client Registration
161
+ * Mount at: /oauth/register
162
+ */
163
+ register: (ctx: RunActionCtx, request: Request) =>
164
+ registerHandler(ctx as Parameters<typeof registerHandler>[0], request, this.config, this.api),
165
+
166
+ /**
167
+ * Protected Resource Metadata
168
+ * Mount at: /.well-known/oauth-protected-resource
169
+ */
170
+ protectedResource: (ctx: RunActionCtx, request: Request) =>
171
+ oauthProtectedResourceHandler(ctx as Parameters<typeof oauthProtectedResourceHandler>[0], request, this.config),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Issue Authorization Code
177
+ * Called from consent approval mutation
178
+ * Also creates/updates authorization record automatically
179
+ */
180
+ async issueAuthorizationCode(ctx: RunMutationCtx, args: {
181
+ userId: string;
182
+ clientId: string;
183
+ scopes: string[];
184
+ redirectUri: string;
185
+ codeChallenge?: string;
186
+ codeChallengeMethod?: string;
187
+ nonce?: string;
188
+ }): Promise<string> {
189
+ if (!args.codeChallenge) {
190
+ throw new Error("codeChallenge required");
191
+ }
192
+ const codeChallengeMethod = args.codeChallengeMethod ?? "S256";
193
+ if (codeChallengeMethod !== "S256") {
194
+ throw new Error("codeChallengeMethod must be S256");
195
+ }
196
+
197
+ // 1. Create/update authorization record (user consented)
198
+ await this.api.mutations.upsertAuthorization(ctx, {
199
+ userId: args.userId,
200
+ clientId: args.clientId,
201
+ scopes: args.scopes,
202
+ });
203
+
204
+ // 2. Issue the authorization code
205
+ return this.api.mutations.issueAuthorizationCode(ctx, {
206
+ ...args,
207
+ codeChallenge: args.codeChallenge,
208
+ codeChallengeMethod,
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Get OAuth Client
214
+ */
215
+ async getClient(ctx: RunQueryCtx, clientId: string) {
216
+ return this.api.queries.getClient(ctx, { clientId });
217
+ }
218
+
219
+ /**
220
+ * Register OAuth Client (for admin use)
221
+ */
222
+ async registerClient(ctx: RunMutationCtx, args: {
223
+ name: string;
224
+ redirectUris: string[];
225
+ scopes: string[];
226
+ type: "confidential" | "public";
227
+ website?: string;
228
+ logoUrl?: string;
229
+ tosUrl?: string;
230
+ policyUrl?: string;
231
+ }) {
232
+ return this.api.clientManagement.registerClient(ctx, args);
233
+ }
234
+
235
+ /**
236
+ * Get user's active tokens
237
+ */
238
+ async getTokensByUser(ctx: RunQueryCtx, userId: string) {
239
+ return this.api.queries.getTokensByUser(ctx, { userId });
240
+ }
241
+
242
+ // -------------------------------------------------------------------------
243
+ // Authorization Management
244
+ // -------------------------------------------------------------------------
245
+
246
+ /**
247
+ * Get authorization for a specific user-client pair
248
+ * Returns null if user has not authorized this client
249
+ */
250
+ async getAuthorization(ctx: RunQueryCtx, userId: string, clientId: string) {
251
+ return ctx.runQuery(this.component.queries.getAuthorization, { userId, clientId });
252
+ }
253
+
254
+ /**
255
+ * List all authorized apps for a user
256
+ * Returns client info along with authorization details
257
+ */
258
+ async listUserAuthorizations(ctx: RunQueryCtx, userId: string) {
259
+ return ctx.runQuery(this.component.queries.listUserAuthorizations, { userId });
260
+ }
261
+
262
+ /**
263
+ * Create or update authorization when user grants consent
264
+ * Call this when user approves OAuth consent
265
+ */
266
+ async upsertAuthorization(ctx: RunMutationCtx, args: {
267
+ userId: string;
268
+ clientId: string;
269
+ scopes: string[];
270
+ }) {
271
+ return ctx.runMutation(this.component.mutations.upsertAuthorization, args);
272
+ }
273
+
274
+ /**
275
+ * Revoke authorization and delete all associated tokens
276
+ * Call this when user wants to disconnect an app
277
+ */
278
+ async revokeAuthorization(ctx: RunMutationCtx, userId: string, clientId: string) {
279
+ return ctx.runMutation(this.component.mutations.revokeAuthorization, { userId, clientId });
280
+ }
281
+
282
+ /**
283
+ * Check if user has already authorized this client with sufficient scopes
284
+ * Useful for "skip consent" flow
285
+ */
286
+ async hasAuthorization(ctx: RunQueryCtx, userId: string, clientId: string, requiredScopes: string[]): Promise<boolean> {
287
+ const auth = await this.getAuthorization(ctx, userId, clientId);
288
+ if (!auth) return false;
289
+
290
+ // Check if all required scopes are authorized
291
+ return requiredScopes.every(scope => auth.scopes.includes(scope));
292
+ }
293
+
294
+ /**
295
+ * Check if authorization exists (for revocation check)
296
+ * Use this with createAuthHelper's checkAuthorization option
297
+ */
298
+ async checkAuthorizationValid(ctx: RunQueryCtx, userId: string, clientId?: string): Promise<boolean> {
299
+ if (clientId) {
300
+ // Check specific client authorization
301
+ return ctx.runQuery(this.component.queries.hasAuthorization, { userId, clientId });
302
+ } else {
303
+ // Check if user has any authorization
304
+ return ctx.runQuery(this.component.queries.hasAnyAuthorization, { userId });
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Create a checkAuthorization function for use with createAuthHelper
310
+ * This ensures revoked authorizations are rejected
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const oauthProvider = new OAuthProvider(components.oauthProvider, config);
315
+ * const authHelper = createAuthHelper({
316
+ * providers: ["anonymous"],
317
+ * checkAuthorization: oauthProvider.createAuthorizationChecker(),
318
+ * });
319
+ * ```
320
+ */
321
+ createAuthorizationChecker() {
322
+ return async (ctx: RunQueryCtx, userId: string, clientId?: string): Promise<boolean> => {
323
+ return this.checkAuthorizationValid(ctx, userId, clientId);
324
+ };
325
+ }
326
+ }