kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: re-add github auth
rodzic
b98701cb39
commit
f9254b5965
|
@ -45,6 +45,7 @@
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"exit-hook": "catalog:",
|
"exit-hook": "catalog:",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
|
"ky": "catalog:",
|
||||||
"octokit": "catalog:",
|
"octokit": "catalog:",
|
||||||
"p-all": "catalog:",
|
"p-all": "catalog:",
|
||||||
"postgres": "catalog:",
|
"postgres": "catalog:",
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
|
import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
||||||
import { assert, parseZodSchema } from '@agentic/platform-core'
|
import { assert, parseZodSchema } from '@agentic/platform-core'
|
||||||
import { isValidPassword } from '@agentic/platform-validators'
|
|
||||||
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import type { AuthenticatedHonoEnv } from '@/lib/types'
|
|
||||||
import { type RawAccount, usernameSchema } from '@/db'
|
|
||||||
import { createAuthToken } from '@/lib/auth/create-auth-token'
|
import { createAuthToken } from '@/lib/auth/create-auth-token'
|
||||||
import { upsertOrLinkUserAccount } from '@/lib/auth/upsert-or-link-user-account'
|
import { upsertOrLinkUserAccount } from '@/lib/auth/upsert-or-link-user-account'
|
||||||
import { getGitHubClient } from '@/lib/external/github'
|
import {
|
||||||
|
exchangeGitHubOAuthCodeForAccessToken,
|
||||||
|
getGitHubClient
|
||||||
|
} from '@/lib/external/github'
|
||||||
import {
|
import {
|
||||||
openapiAuthenticatedSecuritySchemas,
|
openapiAuthenticatedSecuritySchemas,
|
||||||
openapiErrorResponse404,
|
openapiErrorResponse404,
|
||||||
openapiErrorResponses
|
openapiErrorResponses
|
||||||
} from '@/lib/openapi-utils'
|
} from '@/lib/openapi-utils'
|
||||||
|
|
||||||
import { userAuthResponseSchema } from './schemas'
|
import { authSessionResponseSchema } from './schemas'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
description: 'Authenticates with GitHub.',
|
description: 'Exchanges a GitHub OAuth code for an Agentic auth session.',
|
||||||
tags: ['auth'],
|
tags: ['auth'],
|
||||||
operationId: 'authWithGitHub',
|
operationId: 'exchangeOAuthCodeWithGitHub',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: 'auth/github',
|
path: 'auth/github',
|
||||||
security: openapiAuthenticatedSecuritySchemas,
|
security: openapiAuthenticatedSecuritySchemas,
|
||||||
|
@ -27,21 +28,21 @@ const route = createRoute({
|
||||||
required: true,
|
required: true,
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: z.object({
|
schema: z
|
||||||
username: usernameSchema,
|
.object({
|
||||||
email: z.string().email(),
|
code: z.string()
|
||||||
password: z.string().refine((password) => isValidPassword(password))
|
|
||||||
})
|
})
|
||||||
|
.passthrough()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: 'A user object',
|
description: 'An auth session',
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: userAuthResponseSchema
|
schema: authSessionResponseSchema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -50,15 +51,21 @@ const route = createRoute({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export function registerV1AuthWithGitHub(
|
export function registerV1AuthExchangeOAuthCodeWithGitHub(
|
||||||
app: OpenAPIHono<AuthenticatedHonoEnv>
|
app: OpenAPIHono<DefaultHonoEnv>
|
||||||
) {
|
) {
|
||||||
return app.openapi(route, async (c) => {
|
return app.openapi(route, async (c) => {
|
||||||
|
const logger = c.get('logger')
|
||||||
const body = c.req.valid('json')
|
const body = c.req.valid('json')
|
||||||
|
|
||||||
const client = getGitHubClient({ accessToken: value.tokenset.access })
|
const result = await exchangeGitHubOAuthCodeForAccessToken(body)
|
||||||
|
logger.info('github oauth', result)
|
||||||
|
|
||||||
|
const client = getGitHubClient({ accessToken: result.access_token! })
|
||||||
const { data: ghUser } = await client.rest.users.getAuthenticated()
|
const { data: ghUser } = await client.rest.users.getAuthenticated()
|
||||||
|
|
||||||
|
logger.info('github user', ghUser)
|
||||||
|
|
||||||
if (!ghUser.email) {
|
if (!ghUser.email) {
|
||||||
const { data: emails } = await client.request('GET /user/emails')
|
const { data: emails } = await client.request('GET /user/emails')
|
||||||
const primary = emails.find((e) => e.primary)
|
const primary = emails.find((e) => e.primary)
|
||||||
|
@ -73,29 +80,22 @@ export function registerV1AuthWithGitHub(
|
||||||
'Error authenticating with GitHub: user email is required.'
|
'Error authenticating with GitHub: user email is required.'
|
||||||
)
|
)
|
||||||
|
|
||||||
function getPartialOAuthAccount(): Partial<RawAccount> {
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
return {
|
|
||||||
provider: 'github',
|
|
||||||
accessToken: value.tokenset.access,
|
|
||||||
refreshToken: value.tokenset.refresh,
|
|
||||||
// `expires_in` and `refresh_token_expires_in` are given in seconds
|
|
||||||
accessTokenExpiresAt: new Date(
|
|
||||||
now + value.tokenset.raw.expires_in * 1000
|
|
||||||
),
|
|
||||||
refreshTokenExpiresAt: new Date(
|
|
||||||
now + value.tokenset.raw.refresh_token_expires_in * 1000
|
|
||||||
),
|
|
||||||
scope: (value.tokenset.raw.scope as string) || undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await upsertOrLinkUserAccount({
|
const user = await upsertOrLinkUserAccount({
|
||||||
partialAccount: {
|
partialAccount: {
|
||||||
|
provider: 'github',
|
||||||
accountId: `${ghUser.id}`,
|
accountId: `${ghUser.id}`,
|
||||||
accountUsername: ghUser.login.toLowerCase(),
|
accountUsername: ghUser.login.toLowerCase(),
|
||||||
...getPartialOAuthAccount()
|
accessToken: result.access_token,
|
||||||
|
refreshToken: result.refresh_token,
|
||||||
|
// `expires_in` and `refresh_token_expires_in` are given in seconds
|
||||||
|
accessTokenExpiresAt: result.expires_in
|
||||||
|
? new Date(now + result.expires_in * 1000)
|
||||||
|
: undefined,
|
||||||
|
refreshTokenExpiresAt: result.refresh_token_expires_in
|
||||||
|
? new Date(now + result.refresh_token_expires_in * 1000)
|
||||||
|
: undefined,
|
||||||
|
scope: result.scope || undefined
|
||||||
},
|
},
|
||||||
partialUser: {
|
partialUser: {
|
||||||
email: ghUser.email,
|
email: ghUser.email,
|
||||||
|
@ -106,7 +106,9 @@ export function registerV1AuthWithGitHub(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.info('github user result', user)
|
||||||
|
|
||||||
const token = await createAuthToken(user)
|
const token = await createAuthToken(user)
|
||||||
return c.json(parseZodSchema(userAuthResponseSchema, { token, user }))
|
return c.json(parseZodSchema(authSessionResponseSchema, { token, user }))
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ import type { AuthenticatedHonoEnv } from '@/lib/types'
|
||||||
import * as middleware from '@/lib/middleware'
|
import * as middleware from '@/lib/middleware'
|
||||||
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
|
||||||
|
|
||||||
|
import { registerV1AuthExchangeOAuthCodeWithGitHub } from './auth/github'
|
||||||
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
|
import { registerV1AuthSignInWithPassword } from './auth/sign-in-with-password'
|
||||||
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
|
import { registerV1AuthSignUpWithPassword } from './auth/sign-up-with-password'
|
||||||
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
|
import { registerV1AdminConsumersActivateConsumer } from './consumers/admin-activate-consumer'
|
||||||
|
@ -77,6 +78,7 @@ registerHealthCheck(publicRouter)
|
||||||
// Auth
|
// Auth
|
||||||
registerV1AuthSignInWithPassword(publicRouter)
|
registerV1AuthSignInWithPassword(publicRouter)
|
||||||
registerV1AuthSignUpWithPassword(publicRouter)
|
registerV1AuthSignUpWithPassword(publicRouter)
|
||||||
|
registerV1AuthExchangeOAuthCodeWithGitHub(publicRouter)
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
registerV1UsersGetUser(privateRouter)
|
registerV1UsersGetUser(privateRouter)
|
||||||
|
|
|
@ -58,7 +58,7 @@ export function getGitHubClient({
|
||||||
return new Octokit({ auth: accessToken })
|
return new Octokit({ auth: accessToken })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exchangeOAuthCodeForAccessToken({
|
export async function exchangeGitHubOAuthCodeForAccessToken({
|
||||||
code,
|
code,
|
||||||
clientId = env.GITHUB_CLIENT_ID,
|
clientId = env.GITHUB_CLIENT_ID,
|
||||||
clientSecret = env.GITHUB_CLIENT_SECRET,
|
clientSecret = env.GITHUB_CLIENT_SECRET,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
||||||
import type { OpenAPIHono } from '@hono/zod-openapi'
|
import type { OpenAPIHono } from '@hono/zod-openapi'
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
export function registerV1OAuthRedirect(app: OpenAPIHono) {
|
export function registerOAuthRedirect(app: OpenAPIHono<DefaultHonoEnv>) {
|
||||||
return app.all('oauth', async (ctx) => {
|
return app.all('/oauth/callback', async (ctx) => {
|
||||||
|
const logger = ctx.get('logger')
|
||||||
|
|
||||||
if (ctx.req.query('state')) {
|
if (ctx.req.query('state')) {
|
||||||
const { state: state64, ...query } = ctx.req.query()
|
const { state: state64, ...query } = ctx.req.query()
|
||||||
|
|
||||||
|
@ -21,8 +24,17 @@ export function registerV1OAuthRedirect(app: OpenAPIHono) {
|
||||||
...state,
|
...state,
|
||||||
...query
|
...query
|
||||||
})
|
})
|
||||||
|
const redirectUri = `${uri}?${searchParams.toString()}`
|
||||||
|
|
||||||
ctx.redirect(`${uri}?${searchParams.toString()}`)
|
logger.info(
|
||||||
|
'OAUTH CALLBACK',
|
||||||
|
ctx.req.method,
|
||||||
|
ctx.req.url,
|
||||||
|
ctx.req.query(),
|
||||||
|
'=>',
|
||||||
|
redirectUri
|
||||||
|
)
|
||||||
|
return ctx.redirect(redirectUri)
|
||||||
} else {
|
} else {
|
||||||
// github oauth
|
// github oauth
|
||||||
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls
|
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls
|
||||||
|
@ -35,7 +47,17 @@ export function registerV1OAuthRedirect(app: OpenAPIHono) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(params)
|
const searchParams = new URLSearchParams(params)
|
||||||
ctx.redirect(`${uri}?${searchParams.toString()}`)
|
const redirectUri = `${uri}?${searchParams.toString()}`
|
||||||
|
logger.info(
|
||||||
|
'OAUTH CALLBACK',
|
||||||
|
ctx.req.method,
|
||||||
|
ctx.req.url,
|
||||||
|
ctx.req.query(),
|
||||||
|
'=>',
|
||||||
|
redirectUri
|
||||||
|
)
|
||||||
|
|
||||||
|
return ctx.redirect(redirectUri)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { env } from '@/lib/env'
|
||||||
import * as middleware from '@/lib/middleware'
|
import * as middleware from '@/lib/middleware'
|
||||||
|
|
||||||
import { initExitHooks } from './lib/exit-hooks'
|
import { initExitHooks } from './lib/exit-hooks'
|
||||||
|
import { registerOAuthRedirect } from './oauth-redirect'
|
||||||
|
|
||||||
export const app = new OpenAPIHono<DefaultHonoEnv>()
|
export const app = new OpenAPIHono<DefaultHonoEnv>()
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ app.use(middleware.accessLogger)
|
||||||
app.use(middleware.responseTime)
|
app.use(middleware.responseTime)
|
||||||
|
|
||||||
// TODO: top-level auth routes
|
// TODO: top-level auth routes
|
||||||
|
registerOAuthRedirect(app)
|
||||||
|
|
||||||
// Mount all v1 API routes
|
// Mount all v1 API routes
|
||||||
app.route('/v1', apiV1)
|
app.route('/v1', apiV1)
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentic/platform-core": "workspace:*",
|
"@agentic/platform-core": "workspace:*",
|
||||||
"@agentic/platform-types": "workspace:*",
|
"@agentic/platform-types": "workspace:*",
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"jose": "^6.0.11",
|
||||||
"ky": "catalog:",
|
"ky": "catalog:",
|
||||||
"type-fest": "catalog:"
|
"type-fest": "catalog:"
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,11 @@ import { assert, sanitizeSearchParams } from '@agentic/platform-core'
|
||||||
import defaultKy, { type KyInstance } from 'ky'
|
import defaultKy, { type KyInstance } from 'ky'
|
||||||
|
|
||||||
import type { OnUpdateAuthSessionFunction } from './types'
|
import type { OnUpdateAuthSessionFunction } from './types'
|
||||||
|
// import {
|
||||||
|
// type AuthClient,
|
||||||
|
// type AuthorizeResult,
|
||||||
|
// createAuthClient
|
||||||
|
// } from './auth-client'
|
||||||
|
|
||||||
export class AgenticApiClient {
|
export class AgenticApiClient {
|
||||||
static readonly DEFAULT_API_BASE_URL = 'https://api.agentic.so'
|
static readonly DEFAULT_API_BASE_URL = 'https://api.agentic.so'
|
||||||
|
@ -24,6 +29,7 @@ export class AgenticApiClient {
|
||||||
public readonly ky: KyInstance
|
public readonly ky: KyInstance
|
||||||
public readonly onUpdateAuth?: OnUpdateAuthSessionFunction
|
public readonly onUpdateAuth?: OnUpdateAuthSessionFunction
|
||||||
|
|
||||||
|
// protected _authClient: AuthClient
|
||||||
protected _authSession?: AuthSession
|
protected _authSession?: AuthSession
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
|
@ -43,6 +49,11 @@ export class AgenticApiClient {
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey
|
||||||
this.onUpdateAuth = onUpdateAuth
|
this.onUpdateAuth = onUpdateAuth
|
||||||
|
|
||||||
|
// this._authClient = createAuthClient({
|
||||||
|
// issuer: apiBaseUrl,
|
||||||
|
// clientId: 'agentic-api-client'
|
||||||
|
// })
|
||||||
|
|
||||||
this.ky = ky.extend({
|
this.ky = ky.extend({
|
||||||
prefixUrl: apiBaseUrl,
|
prefixUrl: apiBaseUrl,
|
||||||
|
|
||||||
|
@ -88,6 +99,71 @@ export class AgenticApiClient {
|
||||||
this._authSession = structuredClone(authSession)
|
this._authSession = structuredClone(authSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// async verifyAuthAndRefreshIfNecessary(): Promise<AuthSession> {
|
||||||
|
// this._ensureNoApiKey()
|
||||||
|
|
||||||
|
// if (!this._authTokens) {
|
||||||
|
// throw new Error('This method requires authentication.')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const verified = await this._authClient.verify(
|
||||||
|
// authSubjects,
|
||||||
|
// this._authTokens.access,
|
||||||
|
// {
|
||||||
|
// refresh: this._authTokens.refresh
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if (verified.err) {
|
||||||
|
// throw verified.err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (verified.tokens) {
|
||||||
|
// this._authTokens = verified.tokens
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.onUpdateAuth?.({
|
||||||
|
// session: this._authTokens,
|
||||||
|
// user: verified.subject.properties
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return verified.subject.properties
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async exchangeAuthCode({
|
||||||
|
// code,
|
||||||
|
// redirectUri,
|
||||||
|
// verifier
|
||||||
|
// }: {
|
||||||
|
// code: string
|
||||||
|
// redirectUri: string
|
||||||
|
// verifier?: string
|
||||||
|
// }): Promise<AuthSession> {
|
||||||
|
// this._ensureNoApiKey()
|
||||||
|
// const result = await this._authClient.exchange(code, redirectUri, verifier)
|
||||||
|
|
||||||
|
// if (result.err) {
|
||||||
|
// throw result.err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this._authTokens = result.tokens
|
||||||
|
// return this.verifyAuthAndRefreshIfNecessary()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async initAuthFlow({
|
||||||
|
// redirectUri,
|
||||||
|
// provider
|
||||||
|
// }: {
|
||||||
|
// redirectUri: string
|
||||||
|
// provider: 'github'
|
||||||
|
// }): Promise<AuthorizeResult> {
|
||||||
|
// this._ensureNoApiKey()
|
||||||
|
|
||||||
|
// return this._authClient.authorize(redirectUri, 'code', {
|
||||||
|
// provider
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
this._authSession = undefined
|
this._authSession = undefined
|
||||||
this.onUpdateAuth?.()
|
this.onUpdateAuth?.()
|
||||||
|
@ -106,7 +182,7 @@ export class AgenticApiClient {
|
||||||
// searchParams?: OperationParameters<'signInWithPassword'>
|
// searchParams?: OperationParameters<'signInWithPassword'>
|
||||||
): Promise<AuthSession> {
|
): Promise<AuthSession> {
|
||||||
this._authSession = await this.ky
|
this._authSession = await this.ky
|
||||||
.post(`v1/auth/password/signin`, { json })
|
.post('v1/auth/password/signin', { json })
|
||||||
.json<AuthSession>()
|
.json<AuthSession>()
|
||||||
|
|
||||||
this.onUpdateAuth?.(this._authSession)
|
this.onUpdateAuth?.(this._authSession)
|
||||||
|
@ -119,13 +195,43 @@ export class AgenticApiClient {
|
||||||
// searchParams?: OperationParameters<'signUpWithPassword'>
|
// searchParams?: OperationParameters<'signUpWithPassword'>
|
||||||
): Promise<AuthSession> {
|
): Promise<AuthSession> {
|
||||||
this._authSession = await this.ky
|
this._authSession = await this.ky
|
||||||
.post(`v1/auth/password/signup`, { json })
|
.post('v1/auth/password/signup', { json })
|
||||||
.json()
|
.json()
|
||||||
|
|
||||||
this.onUpdateAuth?.(this._authSession)
|
this.onUpdateAuth?.(this._authSession)
|
||||||
return this._authSession
|
return this._authSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
async initAuthFlowWithGitHub({
|
||||||
|
redirectUri,
|
||||||
|
scope = 'user:email',
|
||||||
|
clientId = 'Iv23lizZv3CnggDT7JED'
|
||||||
|
}: {
|
||||||
|
redirectUri: string
|
||||||
|
scope?: string
|
||||||
|
clientId?: string
|
||||||
|
}): Promise<string> {
|
||||||
|
const publicRedirectUri = `${this.apiBaseUrl}/oauth/callback?${new URLSearchParams({ uri: redirectUri }).toString()}`
|
||||||
|
|
||||||
|
const url = new URL('https://github.com/login/oauth/authorize')
|
||||||
|
url.searchParams.append('client_id', clientId)
|
||||||
|
url.searchParams.append('scope', scope)
|
||||||
|
url.searchParams.append('redirect_uri', publicRedirectUri)
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
async exchangeOAuthCodeWithGitHub(
|
||||||
|
json: OperationBody<'exchangeOAuthCodeWithGitHub'>
|
||||||
|
): Promise<AuthSession> {
|
||||||
|
this._authSession = await this.ky.post('v1/auth/github', { json }).json()
|
||||||
|
|
||||||
|
this.onUpdateAuth?.(this._authSession)
|
||||||
|
return this._authSession
|
||||||
|
}
|
||||||
|
|
||||||
/** Gets the currently authenticated user. */
|
/** Gets the currently authenticated user. */
|
||||||
async getMe(): Promise<User> {
|
async getMe(): Promise<User> {
|
||||||
// const user = await this.verifyAuthAndRefreshIfNecessary()
|
// const user = await this.verifyAuthAndRefreshIfNecessary()
|
||||||
|
|
|
@ -0,0 +1,738 @@
|
||||||
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||||
|
import {
|
||||||
|
createLocalJWKSet,
|
||||||
|
decodeJwt,
|
||||||
|
errors,
|
||||||
|
type JSONWebKeySet,
|
||||||
|
jwtVerify
|
||||||
|
} from 'jose'
|
||||||
|
|
||||||
|
import type { SubjectSchema } from './subject'
|
||||||
|
import {
|
||||||
|
InvalidAccessTokenError,
|
||||||
|
InvalidAuthorizationCodeError,
|
||||||
|
InvalidRefreshTokenError,
|
||||||
|
InvalidSubjectError
|
||||||
|
} from './errors'
|
||||||
|
import { generatePKCE } from './pkce'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The well-known information for an OAuth 2.0 authorization server.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface WellKnown {
|
||||||
|
/**
|
||||||
|
* The URI to the JWKS endpoint.
|
||||||
|
*/
|
||||||
|
jwks_uri: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI to the token endpoint.
|
||||||
|
*/
|
||||||
|
token_endpoint: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI to the authorization endpoint.
|
||||||
|
*/
|
||||||
|
authorization_endpoint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tokens returned by the auth server.
|
||||||
|
*/
|
||||||
|
export interface Tokens {
|
||||||
|
/**
|
||||||
|
* The access token.
|
||||||
|
*/
|
||||||
|
access: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The refresh token.
|
||||||
|
*/
|
||||||
|
refresh: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds until the access token expires.
|
||||||
|
*/
|
||||||
|
expiresIn: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseLike {
|
||||||
|
json(): Promise<unknown>
|
||||||
|
ok: Response['ok']
|
||||||
|
}
|
||||||
|
type FetchLike = (...args: any[]) => Promise<ResponseLike>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The challenge that you can use to verify the code.
|
||||||
|
*/
|
||||||
|
export type Challenge = {
|
||||||
|
/**
|
||||||
|
* The state that was sent to the redirect URI.
|
||||||
|
*/
|
||||||
|
state: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verifier that was sent to the redirect URI.
|
||||||
|
*/
|
||||||
|
verifier?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the client.
|
||||||
|
*/
|
||||||
|
export interface AuthClientInput {
|
||||||
|
/**
|
||||||
|
* The client ID. This is just a string to identify your app.
|
||||||
|
*
|
||||||
|
* If you have a web app and a mobile app, you want to use different client IDs both.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* clientId: "my-client"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
clientId: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of your OpenAuth server.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* issuer: "https://auth.myserver.com"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
issuer: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally, override the internally used fetch function.
|
||||||
|
*
|
||||||
|
* This is useful if you are using a polyfilled fetch function in your application and you
|
||||||
|
* want the client to use it too.
|
||||||
|
*/
|
||||||
|
fetch?: FetchLike
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeOptions {
|
||||||
|
/**
|
||||||
|
* Enable the PKCE flow. This is for SPA apps.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* pkce: true
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
pkce?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The provider you want to use for the OAuth flow.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* provider: "google"
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If no provider is specified, the user is directed to a page where they can select from the
|
||||||
|
* list of configured providers.
|
||||||
|
*
|
||||||
|
* If there's only one provider configured, the user will be redirected to that.
|
||||||
|
*/
|
||||||
|
provider?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeResult {
|
||||||
|
/**
|
||||||
|
* The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps.
|
||||||
|
*
|
||||||
|
* This is an object that you _stringify_ and store it in session storage.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* sessionStorage.setItem("challenge", JSON.stringify(challenge))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
challenge: Challenge
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to redirect the user to. This starts the OAuth flow.
|
||||||
|
*
|
||||||
|
* For example, for SPA apps.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* location.href = url
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when the exchange is successful.
|
||||||
|
*/
|
||||||
|
export interface ExchangeSuccess {
|
||||||
|
/**
|
||||||
|
* This is always `false` when the exchange is successful.
|
||||||
|
*/
|
||||||
|
err: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The access and refresh tokens.
|
||||||
|
*/
|
||||||
|
tokens: Tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when the exchange fails.
|
||||||
|
*/
|
||||||
|
export interface ExchangeError {
|
||||||
|
/**
|
||||||
|
* The type of error that occurred. You can handle this by checking the type.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidAuthorizationCodeError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* console.log(err instanceof InvalidAuthorizationCodeError)
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
err: InvalidAuthorizationCodeError
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshOptions {
|
||||||
|
/**
|
||||||
|
* Optionally, pass in the access token.
|
||||||
|
*/
|
||||||
|
access?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when the refresh is successful.
|
||||||
|
*/
|
||||||
|
export interface RefreshSuccess {
|
||||||
|
/**
|
||||||
|
* This is always `false` when the refresh is successful.
|
||||||
|
*/
|
||||||
|
err: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the refreshed tokens only if they've been refreshed.
|
||||||
|
*
|
||||||
|
* If they are still valid, this will be `undefined`.
|
||||||
|
*/
|
||||||
|
tokens?: Tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when the refresh fails.
|
||||||
|
*/
|
||||||
|
export interface RefreshError {
|
||||||
|
/**
|
||||||
|
* The type of error that occurred. You can handle this by checking the type.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidRefreshTokenError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* console.log(err instanceof InvalidRefreshTokenError)
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
err: InvalidRefreshTokenError | InvalidAccessTokenError
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyOptions {
|
||||||
|
/**
|
||||||
|
* Optionally, pass in the refresh token.
|
||||||
|
*
|
||||||
|
* If passed in, this will automatically refresh the access token if it has expired.
|
||||||
|
*/
|
||||||
|
refresh?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
issuer?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
audience?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally, override the internally used fetch function.
|
||||||
|
*
|
||||||
|
* This is useful if you are using a polyfilled fetch function in your application and you
|
||||||
|
* want the client to use it too.
|
||||||
|
*/
|
||||||
|
fetch?: FetchLike
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyResult<T extends SubjectSchema> {
|
||||||
|
/**
|
||||||
|
* This is always `undefined` when the verify is successful.
|
||||||
|
*/
|
||||||
|
err?: undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the refreshed tokens only if they’ve been refreshed.
|
||||||
|
*
|
||||||
|
* If they are still valid, this will be undefined.
|
||||||
|
*/
|
||||||
|
tokens?: Tokens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
aud: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decoded subjects from the access token.
|
||||||
|
*
|
||||||
|
* Has the same shape as the subjects you defined when creating the issuer.
|
||||||
|
*/
|
||||||
|
subject: {
|
||||||
|
[type in keyof T]: {
|
||||||
|
type: type
|
||||||
|
properties: StandardSchemaV1.InferOutput<T[type]>
|
||||||
|
}
|
||||||
|
}[keyof T]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned when the verify call fails.
|
||||||
|
*/
|
||||||
|
export interface VerifyError {
|
||||||
|
/**
|
||||||
|
* The type of error that occurred. You can handle this by checking the type.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidRefreshTokenError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* console.log(err instanceof InvalidRefreshTokenError)
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
err: InvalidRefreshTokenError | InvalidAccessTokenError
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An instance of the OpenAuth client contains the following methods.
|
||||||
|
*/
|
||||||
|
export interface AuthClient {
|
||||||
|
/**
|
||||||
|
* Start the autorization flow. For example, in SSR sites.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const { url } = await client.authorize(<redirect_uri>, "code")
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This takes a redirect URI and the type of flow you want to use. The redirect URI is the
|
||||||
|
* location where the user will be redirected to after the flow is complete.
|
||||||
|
*
|
||||||
|
* Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more
|
||||||
|
* secure.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* This returns a URL to redirect the user to. This starts the OAuth flow.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* This returns a URL to the auth server. You can redirect the user to the URL to start the
|
||||||
|
* OAuth flow.
|
||||||
|
*
|
||||||
|
* For SPA apps, we recommend using the PKCE flow.
|
||||||
|
*
|
||||||
|
* ```ts {4}
|
||||||
|
* const { challenge, url } = await client.authorize(
|
||||||
|
* <redirect_uri>,
|
||||||
|
* "code",
|
||||||
|
* { pkce: true }
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This returns a redirect URL and a challenge that you need to use later to verify the code.
|
||||||
|
*/
|
||||||
|
authorize(
|
||||||
|
redirectUri: string,
|
||||||
|
response: 'code' | 'token',
|
||||||
|
opts?: AuthorizeOptions
|
||||||
|
): Promise<AuthorizeResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange the code for access and refresh tokens.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const exchanged = await client.exchange(<code>, <redirect_uri>)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You call this after the user has been redirected back to your app after the OAuth flow.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* For SSR sites, the code is returned in the query parameter.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* So the code comes from the query parameter in the redirect URI. The redirect URI here is
|
||||||
|
* the one that you passed in to the `authorize` call when starting the flow.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* For SPA sites, the code is returned through the URL hash.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL
|
||||||
|
* hash.
|
||||||
|
*
|
||||||
|
* ```ts {4}
|
||||||
|
* const exchanged = await client.exchange(
|
||||||
|
* <code>,
|
||||||
|
* <redirect_uri>,
|
||||||
|
* <challenge.verifier>
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You also need to pass in the previously stored challenge verifier.
|
||||||
|
*
|
||||||
|
* This method returns the access and refresh tokens. Or if it fails, it returns an error that
|
||||||
|
* you can handle depending on the error.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidAuthorizationCodeError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* if (exchanged.err) {
|
||||||
|
* if (exchanged.err instanceof InvalidAuthorizationCodeError) {
|
||||||
|
* // handle invalid code error
|
||||||
|
* }
|
||||||
|
* else {
|
||||||
|
* // handle other errors
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const { access, refresh } = exchanged.tokens
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
exchange(
|
||||||
|
code: string,
|
||||||
|
redirectUri: string,
|
||||||
|
verifier?: string
|
||||||
|
): Promise<ExchangeSuccess | ExchangeError>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the tokens if they have expired. This is used in an SPA app to maintain the
|
||||||
|
* session, without logging the user out.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const next = await client.refresh(<refresh_token>)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Can optionally take the access token as well. If passed in, this will skip the refresh
|
||||||
|
* if the access token is still valid.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const next = await client.refresh(<refresh_token>, { access: <access_token> })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This returns the refreshed tokens only if they've been refreshed.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* if (!next.err) {
|
||||||
|
* // tokens are still valid
|
||||||
|
* }
|
||||||
|
* if (next.tokens) {
|
||||||
|
* const { access, refresh } = next.tokens
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Or if it fails, it returns an error that you can handle depending on the error.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidRefreshTokenError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* if (next.err) {
|
||||||
|
* if (next.err instanceof InvalidRefreshTokenError) {
|
||||||
|
* // handle invalid refresh token error
|
||||||
|
* }
|
||||||
|
* else {
|
||||||
|
* // handle other errors
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
refresh(
|
||||||
|
refresh: string,
|
||||||
|
opts?: RefreshOptions
|
||||||
|
): Promise<RefreshSuccess | RefreshError>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the token in the incoming request.
|
||||||
|
*
|
||||||
|
* This is typically used for SSR sites where the token is stored in an HTTP only cookie. And
|
||||||
|
* is passed to the server on every request.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const verified = await client.verify(<subjects>, <token>)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This takes the subjects that you had previously defined when creating the issuer.
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* If the refresh token is passed in, it'll automatically refresh the access token.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* This can optionally take the refresh token as well. If passed in, it'll automatically
|
||||||
|
* refresh the access token if it has expired.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const verified = await client.verify(<subjects>, <token>, { refresh: <refresh_token> })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This returns the decoded subjects from the access token. And the tokens if they've been
|
||||||
|
* refreshed.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* // based on the subjects you defined earlier
|
||||||
|
* console.log(verified.subject.properties.userID)
|
||||||
|
*
|
||||||
|
* if (verified.tokens) {
|
||||||
|
* const { access, refresh } = verified.tokens
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Or if it fails, it returns an error that you can handle depending on the error.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidRefreshTokenError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* if (verified.err) {
|
||||||
|
* if (verified.err instanceof InvalidRefreshTokenError) {
|
||||||
|
* // handle invalid refresh token error
|
||||||
|
* }
|
||||||
|
* else {
|
||||||
|
* // handle other errors
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
verify<T extends SubjectSchema>(
|
||||||
|
subjects: T,
|
||||||
|
token: string,
|
||||||
|
options?: VerifyOptions
|
||||||
|
): Promise<VerifyResult<T> | VerifyError>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an OpenAuth client.
|
||||||
|
*
|
||||||
|
* @param input - Configure the client.
|
||||||
|
*/
|
||||||
|
export function createAuthClient(input: AuthClientInput): AuthClient {
|
||||||
|
const issuer = input.issuer
|
||||||
|
if (!issuer) {
|
||||||
|
throw new Error('No issuer')
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwksCache = new Map<string, ReturnType<typeof createLocalJWKSet>>()
|
||||||
|
const issuerCache = new Map<string, WellKnown>()
|
||||||
|
const f = input.fetch ?? fetch
|
||||||
|
|
||||||
|
async function getIssuer() {
|
||||||
|
const cached = issuerCache.get(issuer!)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const wellKnown = (await f(
|
||||||
|
`${issuer}/.well-known/oauth-authorization-server`
|
||||||
|
).then((r) => r.json())) as WellKnown
|
||||||
|
issuerCache.set(issuer!, wellKnown)
|
||||||
|
|
||||||
|
return wellKnown
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJWKS() {
|
||||||
|
const wk = await getIssuer()
|
||||||
|
|
||||||
|
const cached = jwksCache.get(issuer!)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const keyset = (await f(wk.jwks_uri).then((r) => r.json())) as JSONWebKeySet
|
||||||
|
const result = createLocalJWKSet(keyset)
|
||||||
|
jwksCache.set(issuer!, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const authClient = {
|
||||||
|
async authorize(
|
||||||
|
redirectUri: string,
|
||||||
|
response: 'code' | 'token',
|
||||||
|
opts?: AuthorizeOptions
|
||||||
|
) {
|
||||||
|
const result = new URL(issuer + '/authorize')
|
||||||
|
const challenge: Challenge = { state: crypto.randomUUID() }
|
||||||
|
|
||||||
|
result.searchParams.set('client_id', input.clientId)
|
||||||
|
result.searchParams.set('redirect_uri', redirectUri)
|
||||||
|
result.searchParams.set('response_type', response)
|
||||||
|
result.searchParams.set('state', challenge.state)
|
||||||
|
|
||||||
|
if (opts?.provider) result.searchParams.set('provider', opts.provider)
|
||||||
|
if (opts?.pkce && response === 'code') {
|
||||||
|
const pkce = await generatePKCE()
|
||||||
|
result.searchParams.set('code_challenge_method', 'S256')
|
||||||
|
result.searchParams.set('code_challenge', pkce.challenge)
|
||||||
|
challenge.verifier = pkce.verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
challenge,
|
||||||
|
url: result.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async exchange(
|
||||||
|
code: string,
|
||||||
|
redirectUri: string,
|
||||||
|
verifier?: string
|
||||||
|
): Promise<ExchangeSuccess | ExchangeError> {
|
||||||
|
const tokens = await f(issuer + '/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
code,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: input.clientId,
|
||||||
|
code_verifier: verifier || ''
|
||||||
|
}).toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!tokens.ok) {
|
||||||
|
return {
|
||||||
|
err: new InvalidAuthorizationCodeError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (await tokens.json()) as any
|
||||||
|
return {
|
||||||
|
err: false,
|
||||||
|
tokens: {
|
||||||
|
access: json.access_token as string,
|
||||||
|
refresh: json.refresh_token as string,
|
||||||
|
expiresIn: json.expires_in as number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh(
|
||||||
|
refresh: string,
|
||||||
|
opts?: RefreshOptions
|
||||||
|
): Promise<RefreshSuccess | RefreshError> {
|
||||||
|
if (opts && opts.access) {
|
||||||
|
const decoded = decodeJwt(opts.access)
|
||||||
|
if (!decoded) {
|
||||||
|
return {
|
||||||
|
err: new InvalidAccessTokenError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow 30s window for expiration
|
||||||
|
if ((decoded.exp || 0) > Date.now() / 1000 + 30) {
|
||||||
|
return {
|
||||||
|
err: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await f(issuer + '/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refresh
|
||||||
|
}).toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!tokens.ok) {
|
||||||
|
return {
|
||||||
|
err: new InvalidRefreshTokenError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (await tokens.json()) as any
|
||||||
|
return {
|
||||||
|
err: false,
|
||||||
|
tokens: {
|
||||||
|
access: json.access_token as string,
|
||||||
|
refresh: json.refresh_token as string,
|
||||||
|
expiresIn: json.expires_in as number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async verify<T extends SubjectSchema>(
|
||||||
|
subjects: T,
|
||||||
|
token: string,
|
||||||
|
options?: VerifyOptions
|
||||||
|
): Promise<VerifyResult<T> | VerifyError> {
|
||||||
|
const jwks = await getJWKS()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await jwtVerify<{
|
||||||
|
mode: 'access'
|
||||||
|
type: keyof T
|
||||||
|
properties: StandardSchemaV1.InferInput<T[keyof T]>
|
||||||
|
}>(token, jwks, {
|
||||||
|
issuer
|
||||||
|
})
|
||||||
|
|
||||||
|
const validated = await subjects[result.payload.type]![
|
||||||
|
'~standard'
|
||||||
|
].validate(result.payload.properties)
|
||||||
|
|
||||||
|
if (!validated.issues && result.payload.mode === 'access') {
|
||||||
|
return {
|
||||||
|
aud: result.payload.aud as string,
|
||||||
|
subject: {
|
||||||
|
type: result.payload.type,
|
||||||
|
properties: validated.value
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
err: new InvalidSubjectError()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof errors.JWTExpired && options?.refresh) {
|
||||||
|
const refreshed = await this.refresh(options.refresh)
|
||||||
|
if (refreshed.err) return refreshed
|
||||||
|
|
||||||
|
const verified = await authClient.verify(
|
||||||
|
subjects,
|
||||||
|
refreshed.tokens!.access,
|
||||||
|
{
|
||||||
|
refresh: refreshed.tokens!.refresh,
|
||||||
|
issuer,
|
||||||
|
fetch: options?.fetch
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (verified.err) return verified
|
||||||
|
|
||||||
|
verified.tokens = refreshed.tokens
|
||||||
|
return verified
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
err: new InvalidAccessTokenError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authClient
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// import { authSubjectSchemas } from '@agentic/platform-types'
|
||||||
|
|
||||||
|
// import { createSubjects } from './subject'
|
||||||
|
|
||||||
|
// export const authSubjects = createSubjects(authSubjectSchemas)
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* A list of errors that can be thrown by OpenAuth.
|
||||||
|
*
|
||||||
|
* You can use these errors to check the type of error and handle it. For example.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { InvalidAuthorizationCodeError } from "@agentic/openauth/error"
|
||||||
|
*
|
||||||
|
* if (err instanceof InvalidAuthorizationCodeError) {
|
||||||
|
* // handle invalid code error
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The OAuth server returned an error.
|
||||||
|
*/
|
||||||
|
export class OauthError extends Error {
|
||||||
|
constructor(
|
||||||
|
public error:
|
||||||
|
| 'invalid_request'
|
||||||
|
| 'invalid_grant'
|
||||||
|
| 'unauthorized_client'
|
||||||
|
| 'access_denied'
|
||||||
|
| 'unsupported_grant_type'
|
||||||
|
| 'server_error'
|
||||||
|
| 'temporarily_unavailable',
|
||||||
|
public description: string
|
||||||
|
) {
|
||||||
|
super(error + ' - ' + description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `provider` needs to be passed in.
|
||||||
|
*/
|
||||||
|
export class MissingProviderError extends OauthError {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'invalid_request',
|
||||||
|
'Must specify `provider` query parameter if `select` callback on issuer is not specified'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given parameter is missing.
|
||||||
|
*/
|
||||||
|
export class MissingParameterError extends OauthError {
|
||||||
|
constructor(public parameter: string) {
|
||||||
|
super('invalid_request', 'Missing parameter: ' + parameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given client is not authorized to use the redirect URI that was passed in.
|
||||||
|
*/
|
||||||
|
export class UnauthorizedClientError extends OauthError {
|
||||||
|
constructor(
|
||||||
|
public clientID: string,
|
||||||
|
redirectURI: string
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
'unauthorized_client',
|
||||||
|
`Client ${clientID} is not authorized to use this redirect_uri: ${redirectURI}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser was in an unknown state.
|
||||||
|
*
|
||||||
|
* This can happen when certain cookies have expired. Or the browser was switched in the middle
|
||||||
|
* of the authentication flow.
|
||||||
|
*/
|
||||||
|
export class UnknownStateError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'The browser was in an unknown state. This could be because certain cookies expired or the browser was switched in the middle of an authentication flow.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given subject is invalid.
|
||||||
|
*/
|
||||||
|
export class InvalidSubjectError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid subject')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given refresh token is invalid.
|
||||||
|
*/
|
||||||
|
export class InvalidRefreshTokenError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid refresh token')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given access token is invalid.
|
||||||
|
*/
|
||||||
|
export class InvalidAccessTokenError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid access token')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given authorization code is invalid.
|
||||||
|
*/
|
||||||
|
export class InvalidAuthorizationCodeError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Invalid authorization code')
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { base64url } from 'jose'
|
||||||
|
|
||||||
|
function generateVerifier(length: number): string {
|
||||||
|
const buffer = new Uint8Array(length)
|
||||||
|
crypto.getRandomValues(buffer)
|
||||||
|
return base64url.encode(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateChallenge(verifier: string, method: 'S256' | 'plain') {
|
||||||
|
if (method === 'plain') return verifier
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const data = encoder.encode(verifier)
|
||||||
|
const hash = await crypto.subtle.digest('SHA-256', data)
|
||||||
|
return base64url.encode(new Uint8Array(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generatePKCE(length = 64) {
|
||||||
|
if (length < 43 || length > 128) {
|
||||||
|
throw new Error(
|
||||||
|
'Code verifier length must be between 43 and 128 characters'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifier = generateVerifier(length)
|
||||||
|
const challenge = await generateChallenge(verifier, 'S256')
|
||||||
|
return {
|
||||||
|
verifier,
|
||||||
|
challenge,
|
||||||
|
method: 'S256'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validatePKCE(
|
||||||
|
verifier: string,
|
||||||
|
challenge: string,
|
||||||
|
method: 'S256' | 'plain' = 'S256'
|
||||||
|
) {
|
||||||
|
const generatedChallenge = await generateChallenge(verifier, method)
|
||||||
|
// timing safe equals?
|
||||||
|
return generatedChallenge === challenge
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* Subjects are what the access token generated at the end of the auth flow will map to. Under
|
||||||
|
* the hood, the access token is a JWT that contains this data.
|
||||||
|
*
|
||||||
|
* #### Define subjects
|
||||||
|
*
|
||||||
|
* ```ts title="subjects.ts"
|
||||||
|
* import { object, string } from "valibot"
|
||||||
|
*
|
||||||
|
* const subjects = createSubjects({
|
||||||
|
* user: object({
|
||||||
|
* userID: string()
|
||||||
|
* })
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* We are using [valibot](https://github.com/fabian-hiller/valibot) here. You can use any
|
||||||
|
* validation library that's following the
|
||||||
|
* [standard-schema specification](https://github.com/standard-schema/standard-schema).
|
||||||
|
*
|
||||||
|
* :::tip
|
||||||
|
* You typically want to place subjects in its own file so it can be imported by all of your apps.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* You can start with one subject. Later you can add more for different types of users.
|
||||||
|
*
|
||||||
|
* #### Set the subjects
|
||||||
|
*
|
||||||
|
* Then you can pass it to the `issuer`.
|
||||||
|
*
|
||||||
|
* ```ts title="issuer.ts"
|
||||||
|
* import { subjects } from "./subjects"
|
||||||
|
*
|
||||||
|
* const app = issuer({
|
||||||
|
* providers: { ... },
|
||||||
|
* subjects,
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### Add the subject payload
|
||||||
|
*
|
||||||
|
* When your user completes the flow, you can add the subject payload in the `success` callback.
|
||||||
|
*
|
||||||
|
* ```ts title="issuer.ts"
|
||||||
|
* const app = issuer({
|
||||||
|
* providers: { ... },
|
||||||
|
* subjects,
|
||||||
|
* async success(ctx, value) {
|
||||||
|
* let userID
|
||||||
|
* if (value.provider === "password") {
|
||||||
|
* console.log(value.email)
|
||||||
|
* userID = ... // lookup user or create them
|
||||||
|
* }
|
||||||
|
* return ctx.subject("user", {
|
||||||
|
* userID
|
||||||
|
* })
|
||||||
|
* },
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Here we are looking up the userID from our database and adding it to the subject payload.
|
||||||
|
*
|
||||||
|
* :::caution
|
||||||
|
* You should only store properties that won't change for the lifetime of the user.
|
||||||
|
* :::
|
||||||
|
*
|
||||||
|
* Since these will be stored in the access token, you should avoid storing information
|
||||||
|
* that'll change often. For example, if you store the user's username, you'll need to
|
||||||
|
* revoke the access token when the user changes their username.
|
||||||
|
*
|
||||||
|
* #### Decode the subject
|
||||||
|
*
|
||||||
|
* Now when your user logs in, you can use the OpenAuth client to decode the subject. For
|
||||||
|
* example, in our SSR app we can do the following.
|
||||||
|
*
|
||||||
|
* ```ts title="app/page.tsx"
|
||||||
|
* import { subjects } from "../subjects"
|
||||||
|
*
|
||||||
|
* const verified = await client.verify(subjects, cookies.get("access_token")!)
|
||||||
|
* console.log(verified.subject.properties.userID)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* All this is typesafe based on the shape of the subjects you defined.
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||||
|
import type { Simplify } from 'type-fest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject schema is a map of types that are used to define the subjects.
|
||||||
|
*/
|
||||||
|
export type SubjectSchema = Record<string, StandardSchemaV1>
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export type SubjectPayload<T extends SubjectSchema> = Simplify<
|
||||||
|
{
|
||||||
|
[type in keyof T & string]: {
|
||||||
|
type: type
|
||||||
|
properties: StandardSchemaV1.InferOutput<T[type]>
|
||||||
|
}
|
||||||
|
}[keyof T & string]
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subject schema.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const subjects = createSubjects({
|
||||||
|
* user: object({
|
||||||
|
* userID: string()
|
||||||
|
* }),
|
||||||
|
* admin: object({
|
||||||
|
* workspaceID: string()
|
||||||
|
* })
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This is using [valibot](https://github.com/fabian-hiller/valibot) to define the shape of the
|
||||||
|
* subjects. You can use any validation library that's following the
|
||||||
|
* [standard-schema specification](https://github.com/standard-schema/standard-schema).
|
||||||
|
*/
|
||||||
|
export function createSubjects<Schema extends SubjectSchema>(
|
||||||
|
types: Schema
|
||||||
|
): Schema {
|
||||||
|
return { ...types }
|
||||||
|
}
|
|
@ -12,8 +12,8 @@ import { oraPromise } from 'ora'
|
||||||
import { AuthStore } from './auth-store'
|
import { AuthStore } from './auth-store'
|
||||||
|
|
||||||
const providerToLabel = {
|
const providerToLabel = {
|
||||||
github: 'GitHub',
|
github: 'GitHub'
|
||||||
password: 'email and password'
|
// password: 'email and password'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function auth({
|
export async function auth({
|
||||||
|
@ -22,7 +22,7 @@ export async function auth({
|
||||||
preferredPort = 6013
|
preferredPort = 6013
|
||||||
}: {
|
}: {
|
||||||
client: AgenticApiClient
|
client: AgenticApiClient
|
||||||
provider: 'github' | 'password'
|
provider: 'github' // | 'password'
|
||||||
preferredPort?: number
|
preferredPort?: number
|
||||||
}): Promise<AuthSession> {
|
}): Promise<AuthSession> {
|
||||||
const providerLabel = providerToLabel[provider]
|
const providerLabel = providerToLabel[provider]
|
||||||
|
@ -49,16 +49,23 @@ export async function auth({
|
||||||
|
|
||||||
const code = c.req.query('code')
|
const code = c.req.query('code')
|
||||||
assert(code, 'Missing required code query parameter')
|
assert(code, 'Missing required code query parameter')
|
||||||
await client.exchangeAuthCode({
|
|
||||||
code,
|
await client.exchangeOAuthCodeWithGitHub({ code })
|
||||||
redirectUri,
|
|
||||||
verifier: authorizeResult.challenge?.verifier
|
|
||||||
})
|
|
||||||
assert(
|
assert(
|
||||||
client.authTokens,
|
client.authSession,
|
||||||
`Error ${providerLabel} auth: failed to exchange auth code for token`
|
`Error ${providerLabel} auth: failed to exchange auth code for token`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// await client.exchangeAuthCode({
|
||||||
|
// code,
|
||||||
|
// redirectUri,
|
||||||
|
// verifier: authorizeResult.challenge?.verifier
|
||||||
|
// })
|
||||||
|
// assert(
|
||||||
|
// client.authSession,
|
||||||
|
// `Error ${providerLabel} auth: failed to exchange auth code for token`
|
||||||
|
// )
|
||||||
|
|
||||||
// AuthStore should be updated via the onUpdateAuth callback
|
// AuthStore should be updated via the onUpdateAuth callback
|
||||||
const session = AuthStore.tryGetAuth()
|
const session = AuthStore.tryGetAuth()
|
||||||
assert(session && session?.token === client.authSession?.token)
|
assert(session && session?.token === client.authSession?.token)
|
||||||
|
@ -89,13 +96,18 @@ export async function auth({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO
|
const url = await client.initAuthFlowWithGitHub({
|
||||||
const authorizeResult = await client.initAuthFlow({
|
|
||||||
provider,
|
|
||||||
redirectUri
|
redirectUri
|
||||||
})
|
})
|
||||||
assert(authorizeResult.url, `Error signing in with ${providerLabel}`)
|
await open(url.toString())
|
||||||
await open(authorizeResult.url)
|
|
||||||
|
// TODO
|
||||||
|
// const authorizeResult = await client.initAuthFlow({
|
||||||
|
// provider,
|
||||||
|
// redirectUri
|
||||||
|
// })
|
||||||
|
// assert(authorizeResult.url, `Error signing in with ${providerLabel}`)
|
||||||
|
// await open(authorizeResult.url)
|
||||||
|
|
||||||
const authSession = await oraPromise(authP, {
|
const authSession = await oraPromise(authP, {
|
||||||
text: `Signing in with ${providerLabel}`,
|
text: `Signing in with ${providerLabel}`,
|
||||||
|
|
|
@ -55,6 +55,23 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/v1/auth/github": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** @description Exchanges GitHub code for auth session. */
|
||||||
|
post: operations["exchangeOAuthCodeWithGitHub"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/v1/users/{userId}": {
|
"/v1/users/{userId}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
@ -974,6 +991,36 @@ export interface operations {
|
||||||
404: components["responses"]["404"];
|
404: components["responses"]["404"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
exchangeOAuthCodeWithGitHub: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description An auth session */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["AuthSession"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
400: components["responses"]["400"];
|
||||||
|
401: components["responses"]["401"];
|
||||||
|
403: components["responses"]["403"];
|
||||||
|
404: components["responses"]["404"];
|
||||||
|
};
|
||||||
|
};
|
||||||
getUser: {
|
getUser: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
265
pnpm-lock.yaml
265
pnpm-lock.yaml
|
@ -6,39 +6,256 @@ settings:
|
||||||
|
|
||||||
catalogs:
|
catalogs:
|
||||||
default:
|
default:
|
||||||
|
'@apideck/better-ajv-errors':
|
||||||
|
specifier: ^0.3.6
|
||||||
|
version: 0.3.6
|
||||||
|
'@clack/prompts':
|
||||||
|
specifier: ^0.11.0
|
||||||
|
version: 0.11.0
|
||||||
|
'@cloudflare/workers-types':
|
||||||
|
specifier: ^4.20250614.0
|
||||||
|
version: 4.20250614.0
|
||||||
|
'@commander-js/extra-typings':
|
||||||
|
specifier: ^14.0.0
|
||||||
|
version: 14.0.0
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
|
'@fisch0920/config':
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2
|
||||||
|
'@fisch0920/drizzle-orm':
|
||||||
|
specifier: ^0.43.7
|
||||||
|
version: 0.43.7
|
||||||
|
'@fisch0920/drizzle-zod':
|
||||||
|
specifier: ^0.7.9
|
||||||
|
version: 0.7.9
|
||||||
|
'@hono/node-server':
|
||||||
|
specifier: ^1.14.4
|
||||||
|
version: 1.14.4
|
||||||
|
'@hono/sentry':
|
||||||
|
specifier: ^1.2.2
|
||||||
|
version: 1.2.2
|
||||||
|
'@hono/zod-openapi':
|
||||||
|
specifier: ^0.19.8
|
||||||
|
version: 0.19.8
|
||||||
|
'@hono/zod-validator':
|
||||||
|
specifier: ^0.7.0
|
||||||
|
version: 0.7.0
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.12.3
|
||||||
|
version: 1.12.3
|
||||||
|
'@paralleldrive/cuid2':
|
||||||
|
specifier: ^2.2.2
|
||||||
|
version: 2.2.2
|
||||||
|
'@react-email/components':
|
||||||
|
specifier: ^0.0.42
|
||||||
|
version: 0.0.42
|
||||||
|
'@redocly/openapi-core':
|
||||||
|
specifier: ^1.34.3
|
||||||
|
version: 1.34.3
|
||||||
|
'@sentry/cli':
|
||||||
|
specifier: ^2.46.0
|
||||||
|
version: 2.46.0
|
||||||
|
'@sentry/cloudflare':
|
||||||
|
specifier: ^9.29.0
|
||||||
|
version: 9.29.0
|
||||||
|
'@sentry/core':
|
||||||
|
specifier: ^9.29.0
|
||||||
|
version: 9.29.0
|
||||||
|
'@sentry/node':
|
||||||
|
specifier: ^9.29.0
|
||||||
|
version: 9.29.0
|
||||||
|
'@types/ms':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^24.0.1
|
||||||
|
version: 24.0.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.8
|
specifier: ^19.1.8
|
||||||
version: 19.1.8
|
version: 19.1.8
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.1.6
|
specifier: ^19.1.6
|
||||||
version: 19.1.6
|
version: 19.1.6
|
||||||
|
'@types/semver':
|
||||||
|
specifier: ^7.7.0
|
||||||
|
version: 7.7.0
|
||||||
|
agents:
|
||||||
|
specifier: ^0.0.95
|
||||||
|
version: 0.0.95
|
||||||
|
ajv:
|
||||||
|
specifier: ^8.17.1
|
||||||
|
version: 8.17.1
|
||||||
|
ajv-formats:
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
|
camelcase:
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
|
commander:
|
||||||
|
specifier: ^14.0.0
|
||||||
|
version: 14.0.0
|
||||||
|
conf:
|
||||||
|
specifier: ^14.0.0
|
||||||
|
version: 14.0.0
|
||||||
|
decamelize:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
|
del-cli:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
|
drizzle-kit:
|
||||||
|
specifier: ^0.31.1
|
||||||
|
version: 0.31.1
|
||||||
|
drizzle-orm:
|
||||||
|
specifier: ^0.44.2
|
||||||
|
version: 0.44.2
|
||||||
|
email-validator:
|
||||||
|
specifier: ^2.0.4
|
||||||
|
version: 2.0.4
|
||||||
|
eslint:
|
||||||
|
specifier: ^9.29.0
|
||||||
|
version: 9.29.0
|
||||||
|
eslint-plugin-drizzle:
|
||||||
|
specifier: ^0.2.3
|
||||||
|
version: 0.2.3
|
||||||
|
eventid:
|
||||||
|
specifier: ^2.0.1
|
||||||
|
version: 2.0.1
|
||||||
|
exit-hook:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.0.0
|
||||||
|
fast-content-type-parse:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
|
fast-uri:
|
||||||
|
specifier: ^3.0.6
|
||||||
|
version: 3.0.6
|
||||||
|
fastmcp:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.3.0
|
||||||
|
get-port:
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
|
hono:
|
||||||
|
specifier: ^4.7.11
|
||||||
|
version: 4.7.11
|
||||||
|
knip:
|
||||||
|
specifier: ^5.61.0
|
||||||
|
version: 5.61.0
|
||||||
|
ky:
|
||||||
|
specifier: 1.8.1
|
||||||
|
version: 1.8.1
|
||||||
|
lint-staged:
|
||||||
|
specifier: ^16.1.1
|
||||||
|
version: 16.1.1
|
||||||
|
ms:
|
||||||
|
specifier: ^2.1.3
|
||||||
|
version: 2.1.3
|
||||||
|
npm-run-all2:
|
||||||
|
specifier: ^8.0.4
|
||||||
|
version: 8.0.4
|
||||||
|
octokit:
|
||||||
|
specifier: ^5.0.3
|
||||||
|
version: 5.0.3
|
||||||
|
only-allow:
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
|
open:
|
||||||
|
specifier: ^10.1.2
|
||||||
|
version: 10.1.2
|
||||||
|
openapi-typescript:
|
||||||
|
specifier: ^7.8.0
|
||||||
|
version: 7.8.0
|
||||||
|
ora:
|
||||||
|
specifier: ^8.2.0
|
||||||
|
version: 8.2.0
|
||||||
|
p-all:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
|
p-map:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3
|
||||||
|
parse-json:
|
||||||
|
specifier: ^8.3.0
|
||||||
|
version: 8.3.0
|
||||||
|
plur:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
|
postgres:
|
||||||
|
specifier: ^3.4.7
|
||||||
|
version: 3.4.7
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.5.3
|
||||||
|
version: 3.5.3
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
react-email:
|
||||||
|
specifier: ^4.0.16
|
||||||
|
version: 4.0.16
|
||||||
|
resend:
|
||||||
|
specifier: ^4.6.0
|
||||||
|
version: 4.6.0
|
||||||
|
restore-cursor:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
|
semver:
|
||||||
|
specifier: ^7.7.2
|
||||||
|
version: 7.7.2
|
||||||
|
simple-git-hooks:
|
||||||
|
specifier: ^2.13.0
|
||||||
|
version: 2.13.0
|
||||||
|
sort-keys:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
stripe:
|
stripe:
|
||||||
specifier: ^18.2.1
|
specifier: ^18.2.1
|
||||||
version: 18.2.1
|
version: 18.2.1
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.5.0
|
||||||
|
version: 8.5.0
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.20.3
|
||||||
|
version: 4.20.3
|
||||||
|
turbo:
|
||||||
|
specifier: ^2.5.4
|
||||||
|
version: 2.5.4
|
||||||
type-fest:
|
type-fest:
|
||||||
specifier: ^4.41.0
|
specifier: ^4.41.0
|
||||||
version: 4.41.0
|
version: 4.41.0
|
||||||
|
typescript:
|
||||||
overrides:
|
specifier: ^5.8.3
|
||||||
openauthjs: link:../../temp/openauth
|
version: 5.8.3
|
||||||
'@agentic/openauth': link:../../temp/openauth/packages/openauth
|
unconfig:
|
||||||
|
specifier: ^7.3.2
|
||||||
|
version: 7.3.2
|
||||||
|
vite-tsconfig-paths:
|
||||||
|
specifier: ^5.1.4
|
||||||
|
version: 5.1.4
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.2.3
|
||||||
|
version: 3.2.3
|
||||||
|
wrangler:
|
||||||
|
specifier: ^4.20.0
|
||||||
|
version: 4.20.0
|
||||||
|
zod:
|
||||||
|
specifier: ^3.25.64
|
||||||
|
version: 3.25.64
|
||||||
|
zod-to-json-schema:
|
||||||
|
specifier: ^3.24.5
|
||||||
|
version: 3.24.5
|
||||||
|
zod-validation-error:
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
|
||||||
'@agentic/openauth':
|
|
||||||
specifier: link:../../temp/openauth/packages/openauth
|
|
||||||
version: link:../../temp/openauth/packages/openauth
|
|
||||||
openauthjs:
|
|
||||||
specifier: link:../../temp/openauth
|
|
||||||
version: link:../../temp/openauth
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@dotenvx/dotenvx':
|
'@dotenvx/dotenvx':
|
||||||
specifier: ^1.44.2
|
specifier: ^1.44.2
|
||||||
|
@ -100,9 +317,6 @@ importers:
|
||||||
|
|
||||||
apps/api:
|
apps/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@agentic/openauth':
|
|
||||||
specifier: link:../../../../temp/openauth/packages/openauth
|
|
||||||
version: link:../../../../temp/openauth/packages/openauth
|
|
||||||
'@agentic/platform':
|
'@agentic/platform':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/platform
|
version: link:../../packages/platform
|
||||||
|
@ -148,6 +362,9 @@ importers:
|
||||||
hono:
|
hono:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.7.11
|
version: 4.7.11
|
||||||
|
ky:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.8.1
|
||||||
octokit:
|
octokit:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
|
@ -362,9 +579,6 @@ importers:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.41.0
|
version: 4.41.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@agentic/openauth':
|
|
||||||
specifier: link:../../../../temp/openauth/packages/openauth
|
|
||||||
version: link:../../../../temp/openauth/packages/openauth
|
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.10
|
specifier: ^4.1.10
|
||||||
version: 4.1.10
|
version: 4.1.10
|
||||||
|
@ -392,15 +606,18 @@ importers:
|
||||||
|
|
||||||
packages/api-client:
|
packages/api-client:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@agentic/openauth':
|
|
||||||
specifier: link:../../../../temp/openauth/packages/openauth
|
|
||||||
version: link:../../../../temp/openauth/packages/openauth
|
|
||||||
'@agentic/platform-core':
|
'@agentic/platform-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
'@agentic/platform-types':
|
'@agentic/platform-types':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../types
|
version: link:../types
|
||||||
|
'@standard-schema/spec':
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
|
jose:
|
||||||
|
specifier: ^6.0.11
|
||||||
|
version: 6.0.11
|
||||||
ky:
|
ky:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.8.1
|
version: 1.8.1
|
||||||
|
@ -4812,6 +5029,9 @@ packages:
|
||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@6.0.11:
|
||||||
|
resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==}
|
||||||
|
|
||||||
joycon@3.1.1:
|
joycon@3.1.1:
|
||||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -8846,14 +9066,13 @@ snapshots:
|
||||||
|
|
||||||
'@types/pg-pool@2.0.6':
|
'@types/pg-pool@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/pg': 8.6.1
|
'@types/pg': 8.15.4
|
||||||
|
|
||||||
'@types/pg@8.15.4':
|
'@types/pg@8.15.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.0.1
|
'@types/node': 24.0.1
|
||||||
pg-protocol: 1.10.0
|
pg-protocol: 1.10.0
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@types/pg@8.6.1':
|
'@types/pg@8.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10752,6 +10971,8 @@ snapshots:
|
||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
|
jose@6.0.11: {}
|
||||||
|
|
||||||
joycon@3.1.1: {}
|
joycon@3.1.1: {}
|
||||||
|
|
||||||
js-cookie@2.2.1: {}
|
js-cookie@2.2.1: {}
|
||||||
|
|
|
@ -55,7 +55,7 @@ catalog:
|
||||||
hono: ^4.7.11
|
hono: ^4.7.11
|
||||||
is-relative-url: ^4.0.0
|
is-relative-url: ^4.0.0
|
||||||
knip: ^5.61.0
|
knip: ^5.61.0
|
||||||
ky: ^1.8.1
|
ky: 1.8.1
|
||||||
lint-staged: ^16.1.1
|
lint-staged: ^16.1.1
|
||||||
ms: ^2.1.3
|
ms: ^2.1.3
|
||||||
npm-run-all2: ^8.0.4
|
npm-run-all2: ^8.0.4
|
||||||
|
|
Ładowanie…
Reference in New Issue