diff --git a/.gitignore b/.gitignore index 37ea1adb..4f62ff58 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ next-env.d.ts old/ out/ + +apps/api/auth-db-temp.json diff --git a/apps/api/package.json b/apps/api/package.json index d334640b..cd898376 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -33,14 +33,15 @@ "@hono/node-server": "^1.14.1", "@hono/sentry": "^1.2.1", "@hono/zod-openapi": "^0.19.6", + "@openauthjs/openauth": "^0.4.3", "@paralleldrive/cuid2": "^2.2.2", "@redocly/openapi-core": "^1.34.3", "@sentry/node": "^9.19.0", - "better-auth": "^1.2.8", "eventid": "^2.0.1", "exit-hook": "catalog:", "hono": "^4.7.9", "jsonwebtoken": "^9.0.2", + "octokit": "^5.0.2", "p-all": "^5.0.0", "parse-json": "^8.3.0", "postgres": "^3.4.5", diff --git a/apps/api/src/api-v1/index.ts b/apps/api/src/api-v1/index.ts index 15632149..2a00cb6e 100644 --- a/apps/api/src/api-v1/index.ts +++ b/apps/api/src/api-v1/index.ts @@ -1,8 +1,7 @@ import { OpenAPIHono } from '@hono/zod-openapi' import { fromError } from 'zod-validation-error' -import type { AuthenticatedEnv, DefaultContext } from '@/lib/types' -import { auth } from '@/lib/auth' +import type { AuthenticatedEnv } from '@/lib/types' import * as middleware from '@/lib/middleware' import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils' @@ -105,16 +104,6 @@ registerV1AdminConsumersGetConsumerByToken(privateRouter) // Webhook event handlers registerV1StripeWebhook(publicRouter) -// Better-Auth Handler for all auth-related routes -apiV1.on(['POST', 'GET'], 'auth/**', async (c: DefaultContext) => { - const logger = c.get('logger') - logger.info(c.req.method, c.req.url, c.req.header()) - - const res = await auth.handler(c.req.raw) - logger.info('auth result', res) - return res -}) - // Setup routes and middleware apiV1.route('/', publicRouter) apiV1.use(middleware.authenticate) diff --git a/apps/api/src/api-v1/teams/create-team.ts b/apps/api/src/api-v1/teams/create-team.ts index 0e14c5d4..a17b2e48 100644 --- a/apps/api/src/api-v1/teams/create-team.ts +++ b/apps/api/src/api-v1/teams/create-team.ts @@ -4,7 +4,7 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi' import type { AuthenticatedEnv } from '@/lib/types' import { db, schema } from '@/db' import { ensureAuthUser } from '@/lib/ensure-auth-user' -import { ensureUniqueTeamSlug } from '@/lib/ensure-unique-team-slug' +import { ensureUniqueNamespace } from '@/lib/ensure-unique-namespace' import { openapiAuthenticatedSecuritySchemas, openapiErrorResponses @@ -45,7 +45,7 @@ export function registerV1TeamsCreateTeam(app: OpenAPIHono) { const user = await ensureAuthUser(c) const body = c.req.valid('json') - await ensureUniqueTeamSlug(body.slug) + await ensureUniqueNamespace(body.slug, { label: 'Team slug' }) return db.transaction(async (tx) => { const [team] = await tx diff --git a/apps/api/src/api-v1/webhooks/stripe-webhook.ts b/apps/api/src/api-v1/webhooks/stripe-webhook.ts index ca57afcb..e1771245 100644 --- a/apps/api/src/api-v1/webhooks/stripe-webhook.ts +++ b/apps/api/src/api-v1/webhooks/stripe-webhook.ts @@ -5,7 +5,7 @@ import { assert, HttpError } from '@agentic/platform-core' import { and, db, eq, schema } from '@/db' import { setConsumerStripeSubscriptionStatus } from '@/lib/consumers/utils' import { env, isStripeLive } from '@/lib/env' -import { stripe } from '@/lib/stripe' +import { stripe } from '@/lib/external/stripe' const relevantStripeEvents = new Set([ 'customer.subscription.updated' diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts new file mode 100644 index 00000000..4e729da9 --- /dev/null +++ b/apps/api/src/auth.ts @@ -0,0 +1,118 @@ +import { assert, pick } from '@agentic/platform-core' +import { issuer } from '@openauthjs/openauth' +import { GithubProvider } from '@openauthjs/openauth/provider/github' +import { PasswordProvider } from '@openauthjs/openauth/provider/password' +import { MemoryStorage } from '@openauthjs/openauth/storage/memory' +import { PasswordUI } from '@openauthjs/openauth/ui/password' + +import { type RawUser } from '@/db' +import { subjects } from '@/lib/auth/subjects' +import { upsertOrLinkUserAccount } from '@/lib/auth/upsert-or-link-user-account' +import { env } from '@/lib/env' +import { getGitHubClient } from '@/lib/external/github' + +export const authRouter = issuer({ + subjects, + storage: MemoryStorage({ + persist: './auth-db-temp.json' + }), + providers: { + github: GithubProvider({ + clientID: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + scopes: ['user:email'] + }), + password: PasswordProvider( + PasswordUI({ + sendCode: async (email, code) => { + // eslint-disable-next-line no-console + console.log({ email, code }) + } + }) + ) + }, + success: async (ctx, value) => { + const { provider } = value + let user: RawUser | undefined + + // eslint-disable-next-line no-console + console.log('Auth success', provider, ctx, JSON.stringify(value, null, 2)) + + function getPartialOAuthAccount() { + assert(provider === 'github', `Unsupported provider "${provider}"`) + + return { + provider, + accessToken: value.tokenset.access, + refreshToken: value.tokenset.refresh, + // `expires_in` and `refresh_token_expires_in` are given in seconds + accessTokenExpiresAt: new Date( + Date.now() + value.tokenset.raw.expires_in * 1000 + ), + refreshTokenExpiresAt: new Date( + Date.now() + value.tokenset.raw.refresh_token_expires_in * 1000 + ), + scope: (value.tokenset.raw.scope as string) || undefined + } + } + + if (provider === 'github') { + const client = getGitHubClient({ + accessToken: value.tokenset.access + }) + const { data: ghUser } = await client.rest.users.getAuthenticated() + + if (!ghUser.email) { + const { data: emails } = await client.request('GET /user/emails') + const primary = emails.find((e) => e.primary) + const verified = emails.find((e) => e.verified) + const fallback = emails.find((e) => e.email) + const email = primary?.email || verified?.email || fallback?.email + ghUser.email = email! + } + + assert( + ghUser.email, + 'Error authenticating with GitHub: user email is required.' + ) + + user = await upsertOrLinkUserAccount({ + partialAccount: { + accountId: `${ghUser.id}`, + accountUsername: ghUser.login.toLowerCase(), + ...getPartialOAuthAccount() + }, + partialUser: { + email: ghUser.email, + emailVerified: true, + name: ghUser.name || undefined, + username: ghUser.login.toLowerCase(), + image: ghUser.avatar_url + } + }) + } else if (provider === 'password') { + user = await upsertOrLinkUserAccount({ + partialAccount: { + provider, + accountId: value.email + }, + partialUser: { + email: value.email + } + }) + } else { + assert( + user, + 404, + `Authentication error: unsupported provider "${provider}"` + ) + } + + assert( + user, + 404, + `Authentication error for provider "${provider}": User not found` + ) + return ctx.subject('user', pick(user, 'id')) + } +}) diff --git a/apps/api/src/db/schema/account.ts b/apps/api/src/db/schema/account.ts new file mode 100644 index 00000000..1c28ab3c --- /dev/null +++ b/apps/api/src/db/schema/account.ts @@ -0,0 +1,72 @@ +import { relations } from '@fisch0920/drizzle-orm' +import { index, pgTable, text, timestamp } from '@fisch0920/drizzle-orm/pg-core' + +import { userIdSchema } from '../schemas' +import { + accountPrimaryId, + authProviderTypeEnum, + createSelectSchema, + timestamps, + userId +} from './common' +import { users } from './user' + +export const accounts = pgTable( + 'accounts', + { + ...accountPrimaryId, + ...timestamps, + + userId: userId() + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + provider: authProviderTypeEnum().notNull(), + + /** Provider-specific account ID (or email in the case of `password` provider) */ + accountId: text().notNull(), + + /** Provider-specific username */ + accountUsername: text(), + + /** Standard OAuth2 access token */ + accessToken: text(), + + /** Standard OAuth2 refresh token */ + refreshToken: text(), + + /** Standard OAuth2 access token expires at */ + accessTokenExpiresAt: timestamp(), + + /** Standard OAuth2 refresh token expires at */ + refreshTokenExpiresAt: timestamp(), + + /** OAuth scope(s) */ + scope: text() + }, + (table) => [ + index('account_provider_idx').on(table.provider), + index('account_userId_idx').on(table.userId), + index('account_createdAt_idx').on(table.createdAt), + index('account_updatedAt_idx').on(table.updatedAt), + index('account_deletedAt_idx').on(table.deletedAt) + ] +) + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { + fields: [accounts.userId], + references: [users.id] + }) +})) + +export const accountSelectSchema = createSelectSchema(accounts, { + userId: userIdSchema +}) + .omit({ + accessToken: true, + refreshToken: true, + accessTokenExpiresAt: true, + refreshTokenExpiresAt: true + }) + .strip() + .openapi('Account') diff --git a/apps/api/src/db/schema/auth.ts b/apps/api/src/db/schema/auth.ts deleted file mode 100644 index f16ad562..00000000 --- a/apps/api/src/db/schema/auth.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { pgTable, text, timestamp } from '@fisch0920/drizzle-orm/pg-core' - -import { - accountPrimaryId, - authTimestamps, - sessionPrimaryId, - userId, - verificationPrimaryId -} from './common' -import { users } from './user' - -// These tables are all managed by better-auth. - -export const sessions = pgTable('sessions', { - ...sessionPrimaryId, - ...authTimestamps, - - token: text().notNull().unique(), - expiresAt: timestamp({ mode: 'date' }).notNull(), - ipAddress: text(), - userAgent: text(), - userId: userId() - .notNull() - .references(() => users.id, { onDelete: 'cascade' }) -}) - -export const accounts = pgTable('accounts', { - ...accountPrimaryId, - ...authTimestamps, - - accountId: text().notNull(), - providerId: text().notNull(), - userId: userId() - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - accessToken: text(), - refreshToken: text(), - accessTokenExpiresAt: timestamp({ mode: 'date' }), - refreshTokenExpiresAt: timestamp({ mode: 'date' }), - scope: text(), - idToken: text(), - password: text() -}) - -export const verifications = pgTable('verifications', { - ...verificationPrimaryId, - ...authTimestamps, - - identifier: text().notNull(), - value: text().notNull(), - - expiresAt: timestamp({ mode: 'date' }).notNull() -}) diff --git a/apps/api/src/db/schema/common.ts b/apps/api/src/db/schema/common.ts index 37b0f041..31a4f639 100644 --- a/apps/api/src/db/schema/common.ts +++ b/apps/api/src/db/schema/common.ts @@ -14,7 +14,7 @@ import { createSchemaFactory } from '@fisch0920/drizzle-zod' import { z } from '@hono/zod-openapi' import { createId as createCuid2 } from '@paralleldrive/cuid2' -const usernameAndTeamSlugLength = 64 as const +export const namespaceMaxLength = 64 as const // prefix is max 5 characters // separator is 1 character @@ -29,18 +29,10 @@ export const idPrefixMap = { consumer: 'csmr', logEntry: 'log', - // better-auth + // auth user: 'user', account: 'acct', - session: 'sess', - verification: 'veri', - 'rate-limit': 'ratel', - organization: 'org', - member: 'mem', - invitation: 'inv', - jwks: 'jwks', - passkey: 'passk', - 'two-factor': '2fa' + session: 'sess' } as const export type ModelType = keyof typeof idPrefixMap @@ -69,9 +61,7 @@ export const consumerPrimaryId = getPrimaryId('consumer') export const logEntryPrimaryId = getPrimaryId('logEntry') export const teamPrimaryId = getPrimaryId('team') export const userPrimaryId = getPrimaryId('user') -export const sessionPrimaryId = getPrimaryId('session') export const accountPrimaryId = getPrimaryId('account') -export const verificationPrimaryId = getPrimaryId('verification') /** * All of our model primary ids have the following format: @@ -123,14 +113,14 @@ export function deploymentIdentifier< export function username>( config?: PgVarcharConfig, never> -): PgVarcharBuilderInitial<'', Writable, typeof usernameAndTeamSlugLength> { - return varchar({ length: usernameAndTeamSlugLength, ...config }) +): PgVarcharBuilderInitial<'', Writable, typeof namespaceMaxLength> { + return varchar({ length: namespaceMaxLength, ...config }) } export function teamSlug>( config?: PgVarcharConfig, never> -): PgVarcharBuilderInitial<'', Writable, typeof usernameAndTeamSlugLength> { - return varchar({ length: usernameAndTeamSlugLength, ...config }) +): PgVarcharBuilderInitial<'', Writable, typeof namespaceMaxLength> { + return varchar({ length: namespaceMaxLength, ...config }) } /** @@ -157,15 +147,6 @@ export const timestamps = { deletedAt: timestamp() } -export const authTimestamps = { - createdAt: timestamp({ mode: 'date' }) - .notNull() - .$defaultFn(() => /* @__PURE__ */ new Date()), - updatedAt: timestamp({ mode: 'date' }) - .notNull() - .$defaultFn(() => /* @__PURE__ */ new Date()) -} - export const userRoleEnum = pgEnum('UserRole', ['user', 'admin']) export const teamMemberRoleEnum = pgEnum('TeamMemberRole', ['user', 'admin']) export const logEntryTypeEnum = pgEnum('LogEntryType', ['log']) @@ -183,6 +164,10 @@ export const pricingIntervalEnum = pgEnum('PricingInterval', [ 'year' ]) export const pricingCurrencyEnum = pgEnum('PricingCurrency', ['usd']) +export const authProviderTypeEnum = pgEnum('AuthProviderType', [ + 'github', + 'password' +]) export const { createInsertSchema, createSelectSchema, createUpdateSchema } = createSchemaFactory({ diff --git a/apps/api/src/db/schema/index.ts b/apps/api/src/db/schema/index.ts index 2136901b..27c8bd4d 100644 --- a/apps/api/src/db/schema/index.ts +++ b/apps/api/src/db/schema/index.ts @@ -1,4 +1,4 @@ -export * from './auth' +export * from './account' export * from './common' export * from './consumer' export * from './deployment' diff --git a/apps/api/src/db/schema/temp b/apps/api/src/db/schema/temp deleted file mode 100644 index 2ad6fb02..00000000 --- a/apps/api/src/db/schema/temp +++ /dev/null @@ -1,226 +0,0 @@ - -// TODO: Currently unused after forking @fisch0920/drizzle-zod. -export function makeNullablePropsOptional( - schema: Schema -): z.ZodObject<{ - [key in keyof Schema['shape']]: Schema['shape'][key] extends z.ZodNullable< - infer T - > - ? z.ZodOptional - : Schema['shape'][key] -}> { - const entries = Object.entries(schema.shape) - const newProps: any = {} - - for (const [key, value] of entries) { - newProps[key] = - value instanceof z.ZodNullable ? value.unwrap().optional() : value - return newProps - } - - return z.object(newProps) as any -} - -export type ColumnType = - // string - | 'text' - | 'varchar' - | 'timestamp' - | 'stripeId' - | 'projectId' - | 'deploymentId' - | 'cuid' - // boolean - | 'boolean' - // number - | 'integer' - | 'smallint' - | 'bigint' - // json - | 'json' - | 'jsonb' - -export type ColumnTypeToTSType = T extends - | 'text' - | 'varchar' - | 'timestamp' - | 'cuid' - | 'stripeId' - | 'projectId' - | 'deploymentId' - ? string - : T extends 'boolean' - ? boolean - : T extends 'integer' | 'smallint' | 'bigint' - ? number - : never - -/** - * @see https://github.com/drizzle-team/@fisch0920/drizzle-orm/issues/2745 - */ -function optional< - T extends ColumnType, - InferredType extends - | string - | boolean - | number - | object = ColumnTypeToTSType ->(dataType: T) { - return customType<{ - data: InferredType | undefined - driverData: InferredType | null - config: T extends 'stripeId' - ? { - length: number - } - : never - }>({ - dataType() { - if (dataType === 'stripeId') { - return 'varchar({ length: 255 })' - } - - if (dataType === 'cuid') { - return 'varchar({ length: 24 })' - } - - if (dataType === 'projectId') { - return 'varchar({ length: 130 })' - } - - if (dataType === 'deploymentId') { - return 'varchar({ length: 160 })' - } - - if (dataType === 'timestamp') { - return 'timestamp({ mode: "string" })' - } - - return dataType - }, - fromDriver: (v) => v ?? undefined, - toDriver: (v) => v ?? null - }) -} - -export const optionalText = optional('text') -export const optionalTimestamp = optional('timestamp') -export const optionalBoolean = optional('boolean') -export const optionalVarchar = optional('varchar') -export const optionalCuid = optional('cuid') -export const optionalStripeId = optional('stripeId') -export const optionalProjectId = optional('projectId') -export const optionalDeploymentId = optional('deploymentId') - -// --- - - -type MaybePromise = Promise | T; -type RequestTypes = { - body?: ZodRequestBody; - params?: ZodType; - query?: ZodType; - cookies?: ZodType; - headers?: ZodType | ZodType[]; -}; -type InputTypeBase = R['request'] extends RequestTypes ? RequestPart extends ZodType ? { - in: { - [K in Type]: HasUndefined extends true ? { - [K2 in keyof z.input>]?: z.input>[K2]; - } : { - [K2 in keyof z.input>]: z.input>[K2]; - }; - }; - out: { - [K in Type]: z.output>; - }; -} : {} : {}; - -type InputTypeParam = InputTypeBase; -type InputTypeQuery = InputTypeBase; -type InputTypeHeader = InputTypeBase; -type InputTypeCookie = InputTypeBase; - -type o: & InputTypeQuery & InputTypeHeader & InputTypeCookie & InputTypeForm & InputTypeJson, P extends string = ConvertPathType>({ middleware: routeMiddleware, hide, ...route }: R, handler: Handler["env"] & E : E, P, I, R extends { - responses: { - [statusCode: number]: { - content: { - [mediaType: string]: ZodMediaTypeObject; - }; - }; - }; -} ? MaybePromise> : MaybePromise> | MaybePromise>) => OpenAPIHono, I, RouteConfigToTypedResponse>, BasePath>; - - -// --- - -import type { SetOptional, SetRequired, Simplify } from 'type-fest' -import type { AnyZodObject } from 'zod' -import { createRoute, type RouteConfig } from '@hono/zod-openapi' - -export type CreateOpenAPIHonoRouteOpts< - TAuthenticated extends boolean = true, - TMethod extends RouteConfig['method'] = RouteConfig['method'] -> = Simplify< - { - authenticated: TAuthenticated - paramsSchema?: AnyZodObject - bodySchema?: AnyZodObject - responseSchema: AnyZodObject - method: TMethod - } & SetRequired< - Omit< - SetOptional[0], 'responses'>, - 'request' | 'responses' | 'security' - >, - 'path' | 'operationId' | 'tags' | 'description' - > -> - -export function createOpenAPIHonoRoute( - opts: CreateOpenAPIHonoRouteOpts -) { - const { - authenticated, - paramsSchema, - bodySchema, - responseSchema, - ...createRouteOpts - } = opts - - return createRoute({ - ...createRouteOpts, - security: authenticated - ? [ - { - bearerAuth: [] - } - ] - : [], - request: { - params: paramsSchema!, - body: bodySchema - ? { - required: true, - content: { - 'application/json': { - schema: bodySchema - } - } - } - : undefined - }, - responses: { - 200: { - description: responseSchema.shape.description, - content: { - 'application/json': { - schema: responseSchema - } - } - } - // TODO - // ...openppiErrorResponses - } - }) -} diff --git a/apps/api/src/db/schema/user.ts b/apps/api/src/db/schema/user.ts index 5f748880..e8c3dc6c 100644 --- a/apps/api/src/db/schema/user.ts +++ b/apps/api/src/db/schema/user.ts @@ -1,3 +1,4 @@ +import { relations } from '@fisch0920/drizzle-orm' import { boolean, index, @@ -6,11 +7,12 @@ import { uniqueIndex } from '@fisch0920/drizzle-orm/pg-core' +import { accounts } from './account' import { - authTimestamps, createSelectSchema, createUpdateSchema, stripeId, + timestamps, username, // username, userPrimaryId, @@ -23,17 +25,16 @@ export const users = pgTable( 'users', { ...userPrimaryId, - ...authTimestamps, + ...timestamps, - name: text().notNull(), + username: username().unique().notNull(), + role: userRoleEnum().default('user').notNull(), + + name: text(), email: text().notNull().unique(), emailVerified: boolean().default(false).notNull(), image: text(), - // TODO: re-add username - username: username().unique(), - role: userRoleEnum().default('user').notNull(), - isStripeConnectEnabledByDefault: boolean().default(true).notNull(), stripeCustomerId: stripeId() @@ -47,6 +48,10 @@ export const users = pgTable( ] ) +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts) +})) + export const userSelectSchema = createSelectSchema(users) .strip() .openapi('User') diff --git a/apps/api/src/db/types.ts b/apps/api/src/db/types.ts index 4a40fcce..2efc7945 100644 --- a/apps/api/src/db/types.ts +++ b/apps/api/src/db/types.ts @@ -78,4 +78,5 @@ export type RawConsumerUpdate = Partial< export type LogEntry = z.infer export type RawLogEntry = InferSelectModel -export type RawSession = InferSelectModel +export type Account = z.infer +export type RawAccount = InferSelectModel diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts deleted file mode 100644 index 983d14fb..00000000 --- a/apps/api/src/lib/auth.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { validators } from '@agentic/platform-validators' -import { betterAuth } from 'better-auth' -import { drizzleAdapter } from 'better-auth/adapters/drizzle' -import { username } from 'better-auth/plugins' - -import { createIdForModel, db, type ModelType } from '@/db' - -import { env } from './env' - -export const auth = betterAuth({ - appName: 'Agentic', - basePath: '/v1/auth', - database: drizzleAdapter(db, { - provider: 'pg' - }), - trustedOrigins: [ - // Used for an oauth redirect when authenticating via the CLI - 'http://localhost:6013' - ], - emailAndPassword: { - enabled: true - }, - socialProviders: { - github: { - clientId: env.GITHUB_CLIENT_ID, - clientSecret: env.GITHUB_CLIENT_SECRET - } - }, - user: { - modelName: 'users', - additionalFields: { - role: { - type: 'string', - required: false, - defaultValue: 'user', - input: false // don't allow user to set role - }, - // username: { - // type: 'string', - // required: false - // }, - stripeCustomerId: { - type: 'string', - required: false - } - } - }, - session: { - modelName: 'sessions', - cookieCache: { - enabled: true, - maxAge: 10 * 60 // 10 minutes in seconds - } - }, - account: { - modelName: 'accounts', - accountLinking: { - enabled: true, - trustedProviders: ['github'] - } - }, - verification: { - modelName: 'verifications' - }, - advanced: { - database: { - generateId: ({ model }) => createIdForModel(model as ModelType) - } - }, - plugins: [ - username({ - usernameValidator: validators.username - }) - ] -}) diff --git a/apps/api/src/lib/auth/client.ts b/apps/api/src/lib/auth/client.ts new file mode 100644 index 00000000..d8f23839 --- /dev/null +++ b/apps/api/src/lib/auth/client.ts @@ -0,0 +1,6 @@ +import { createClient as createAuthClient } from '@openauthjs/openauth/client' + +export const authClient = createAuthClient({ + issuer: 'http://localhost:3000', + clientID: 'agentic-internal-api-client' +}) diff --git a/apps/api/src/lib/auth/subjects.ts b/apps/api/src/lib/auth/subjects.ts new file mode 100644 index 00000000..e8ea983a --- /dev/null +++ b/apps/api/src/lib/auth/subjects.ts @@ -0,0 +1,10 @@ +import { createSubjects } from '@openauthjs/openauth/subject' +import { z } from 'zod' + +import { userIdSchema } from '@/db' + +export const subjects = createSubjects({ + user: z.object({ + id: userIdSchema + }) +}) diff --git a/apps/api/src/lib/auth/upsert-or-link-user-account.ts b/apps/api/src/lib/auth/upsert-or-link-user-account.ts new file mode 100644 index 00000000..0cfdaae7 --- /dev/null +++ b/apps/api/src/lib/auth/upsert-or-link-user-account.ts @@ -0,0 +1,144 @@ +import type { SetRequired, Simplify } from 'type-fest' +import { assert } from '@agentic/platform-core' + +import { and, db, eq, type RawAccount, type RawUser, schema } from '@/db' + +import { getUniqueNamespace } from '../ensure-unique-namespace' + +/** + * After a user completes an authentication flow, we'll have partial account info + * and partial suer info. This function takes these partial values and maps them + * to a valid database Account and User. + * + * This will result in the Account being upserted, and may result in a new User + * being created. + */ +export async function upsertOrLinkUserAccount({ + partialAccount, + partialUser +}: { + partialAccount: Simplify< + SetRequired< + Partial< + Pick< + RawAccount, + | 'provider' + | 'accountId' + | 'accountUsername' + | 'accessToken' + | 'refreshToken' + | 'accessTokenExpiresAt' + | 'refreshTokenExpiresAt' + | 'scope' + > + >, + 'provider' | 'accountId' + > + > + partialUser: Simplify< + SetRequired< + Partial< + Pick + >, + 'email' + > + > +}): Promise { + const { provider, accountId } = partialAccount + + const [existingAccount, existingUser] = await Promise.all([ + db.query.accounts.findFirst({ + where: and( + eq(schema.accounts.provider, provider), + eq(schema.accounts.accountId, accountId) + ), + with: { + user: true + } + }), + + db.query.users.findFirst({ + where: eq(schema.users.email, partialUser.email) + }) + ]) + + if (existingAccount && existingUser) { + // Happy path case: the user is just logging in with an existing account + // that's already linked to a user. + assert( + existingAccount.userId === existingUser.id, + `Error authenticating with ${provider}: Account id "${existingAccount.id}" user id "${existingAccount.userId}" does not match expected user id "${existingUser.id}"` + ) + + // Udate the account with the up-to-date provider data, including any OAuth + // tokens. + await db + .update(schema.accounts) + .set(partialAccount) + .where(eq(schema.accounts.id, existingAccount.id)) + + return existingUser + } else if (existingUser && !existingAccount) { + // Linking a new account to an existing user + await db.insert(schema.accounts).values({ + ...partialAccount, + userId: existingUser.id + }) + + // TODO: Same caveat as below: if the existing user has a different email than + // the one in the account we're linking, we should throw an error unless it's + // a "trusted" provider. + if (provider === 'password' && existingUser.email !== partialUser.email) { + await db + .update(schema.users) + .set(partialUser) + .where(eq(schema.users.id, existingUser.id)) + } + + return existingUser + } else if (existingAccount && !existingUser) { + assert( + existingAccount.user, + 404, + `Error authenticating with ${provider}: Account id "${existingAccount.id}" is linked to a user with a different email address than their ${provider} account, but the linked account user id "${existingAccount.userId}" is not found.` + ) + + // Existing account is linked to a user with a different email address than + // this provider account. This should be fine since it's pretty common for + // users to have multiple email addresses, but we may want to limit the + // ability to automatically link accounts like this in the future to only + // certain, trusted providers like `better-auth` does. + return existingAccount.user + } else { + const username = await getUniqueNamespace( + partialUser.username || partialUser.email.split('@')[0]!.toLowerCase(), + { label: 'Username' } + ) + + // This is a user's first time signing up with the platform, so create both + // a new user and linked account. + return db.transaction(async (tx) => { + // Create a new user + const [user] = await tx + .insert(schema.users) + .values({ + ...partialUser, + username + }) + .returning() + assert( + user, + 500, + `Error creating new user during ${provider} authentication` + ) + + // Create a new account linked to the new user + await tx.insert(schema.accounts).values({ + ...partialAccount, + userId: user.id + }) + + return user + }) + } +} diff --git a/apps/api/src/lib/billing/upsert-stripe-connect-customer.ts b/apps/api/src/lib/billing/upsert-stripe-connect-customer.ts index 9b90e01d..187578ab 100644 --- a/apps/api/src/lib/billing/upsert-stripe-connect-customer.ts +++ b/apps/api/src/lib/billing/upsert-stripe-connect-customer.ts @@ -2,7 +2,7 @@ import type Stripe from 'stripe' import { assert } from '@agentic/platform-core' import { db, eq, type RawConsumer, type RawProject, schema } from '@/db' -import { stripe } from '@/lib/stripe' +import { stripe } from '@/lib/external/stripe' // TODO: Update this for the new / updated Stripe Connect API diff --git a/apps/api/src/lib/billing/upsert-stripe-customer.ts b/apps/api/src/lib/billing/upsert-stripe-customer.ts index 6e752dfc..e075328c 100644 --- a/apps/api/src/lib/billing/upsert-stripe-customer.ts +++ b/apps/api/src/lib/billing/upsert-stripe-customer.ts @@ -4,7 +4,7 @@ import { assert } from '@agentic/platform-core' import type { AuthenticatedContext } from '@/lib/types' import { db, eq, type RawUser, schema } from '@/db' import { ensureAuthUser } from '@/lib/ensure-auth-user' -import { stripe } from '@/lib/stripe' +import { stripe } from '@/lib/external/stripe' export async function upsertStripeCustomer(ctx: AuthenticatedContext): Promise<{ user: RawUser diff --git a/apps/api/src/lib/billing/upsert-stripe-pricing.ts b/apps/api/src/lib/billing/upsert-stripe-pricing.ts index 82880500..d3ba76ff 100644 --- a/apps/api/src/lib/billing/upsert-stripe-pricing.ts +++ b/apps/api/src/lib/billing/upsert-stripe-pricing.ts @@ -14,7 +14,7 @@ import { type RawProject, schema } from '@/db' -import { stripe } from '@/lib/stripe' +import { stripe } from '@/lib/external/stripe' /** * Upserts all the Stripe resources corresponding to a Deployment's pricing diff --git a/apps/api/src/lib/billing/upsert-stripe-subscription.ts b/apps/api/src/lib/billing/upsert-stripe-subscription.ts index 79355c77..9f1b9185 100644 --- a/apps/api/src/lib/billing/upsert-stripe-subscription.ts +++ b/apps/api/src/lib/billing/upsert-stripe-subscription.ts @@ -13,7 +13,7 @@ import { type RawUser, schema } from '@/db' -import { stripe } from '@/lib/stripe' +import { stripe } from '@/lib/external/stripe' import { setConsumerStripeSubscriptionStatus } from '../consumers/utils' diff --git a/apps/api/src/lib/ensure-unique-namespace.ts b/apps/api/src/lib/ensure-unique-namespace.ts new file mode 100644 index 00000000..a8640847 --- /dev/null +++ b/apps/api/src/lib/ensure-unique-namespace.ts @@ -0,0 +1,55 @@ +import { assert, sha256 } from '@agentic/platform-core' + +import { db, eq, schema } from '@/db' + +export async function ensureUniqueNamespace( + namespace: string, + { label = 'Namespace' }: { label?: string } = {} +) { + namespace = namespace.toLocaleLowerCase() + + const [existingTeam, existingUser] = await Promise.all([ + db.query.teams.findFirst({ + where: eq(schema.teams.slug, namespace) + }), + + db.query.users.findFirst({ + where: eq(schema.users.username, namespace) + }) + ]) + + assert( + !existingUser && !existingTeam, + 409, + `${label} "${namespace}" is not available` + ) +} + +export async function getUniqueNamespace( + namespace?: string, + { label = 'Namespace' }: { label?: string } = {} +) { + namespace ??= `${label}_${sha256().slice(0, 24)}` + namespace = namespace + .replaceAll(/[^a-zA-Z0-9_-]/g, '') + .toLowerCase() + .slice(0, schema.namespaceMaxLength - 1) + + let currentNamespace = namespace + let attempts = 0 + + do { + try { + await ensureUniqueNamespace(namespace, { label }) + + return currentNamespace + } catch (err) { + if (++attempts > 10) { + throw err + } + + const suffix = sha256().slice(0, 8) + currentNamespace = `${namespace.slice(0, schema.namespaceMaxLength - 1 - suffix.length)}${suffix}` + } + } while (true) +} diff --git a/apps/api/src/lib/ensure-unique-team-slug.ts b/apps/api/src/lib/ensure-unique-team-slug.ts deleted file mode 100644 index de680b1a..00000000 --- a/apps/api/src/lib/ensure-unique-team-slug.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { assert } from '@agentic/platform-core' - -import { db, eq, schema } from '@/db' - -export async function ensureUniqueTeamSlug(slug: string) { - slug = slug.toLocaleLowerCase() - - const [existingTeam, existingUser] = await Promise.all([ - db.query.teams.findFirst({ - where: eq(schema.teams.slug, slug) - }), - - db.query.users.findFirst({ - where: eq(schema.users.username, slug) - }) - ]) - - assert( - !existingUser && !existingTeam, - 409, - `Team slug "${slug}" is not available` - ) -} diff --git a/apps/api/src/lib/external/github.ts b/apps/api/src/lib/external/github.ts new file mode 100644 index 00000000..26177655 --- /dev/null +++ b/apps/api/src/lib/external/github.ts @@ -0,0 +1,9 @@ +import { Octokit } from 'octokit' + +export function getGitHubClient({ + accessToken +}: { + accessToken: string +}): Octokit { + return new Octokit({ auth: accessToken }) +} diff --git a/apps/api/src/lib/sentry.ts b/apps/api/src/lib/external/sentry.ts similarity index 100% rename from apps/api/src/lib/sentry.ts rename to apps/api/src/lib/external/sentry.ts diff --git a/apps/api/src/lib/stripe.ts b/apps/api/src/lib/external/stripe.ts similarity index 79% rename from apps/api/src/lib/stripe.ts rename to apps/api/src/lib/external/stripe.ts index 52fc4feb..9e0687c3 100644 --- a/apps/api/src/lib/stripe.ts +++ b/apps/api/src/lib/external/stripe.ts @@ -1,6 +1,6 @@ import Stripe from 'stripe' -import { env } from './env' +import { env } from '@/lib/env' export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { apiVersion: '2025-04-30.basil' diff --git a/apps/api/src/lib/middleware/authenticate.ts b/apps/api/src/lib/middleware/authenticate.ts index 913b873b..f8fc6729 100644 --- a/apps/api/src/lib/middleware/authenticate.ts +++ b/apps/api/src/lib/middleware/authenticate.ts @@ -1,38 +1,48 @@ import { assert } from '@agentic/platform-core' +// import { auth } from '@/lib/auth' import { createMiddleware } from 'hono/factory' // import * as jwt from 'hono/jwt' import type { AuthenticatedEnv } from '@/lib/types' -import { auth } from '@/lib/auth' +import { authClient } from '@/lib/auth/client' +import { subjects } from '@/lib/auth/subjects' export const authenticate = createMiddleware( async function authenticateMiddleware(ctx, next) { - const session = await auth.api.getSession({ - // TODO: investigate this type issue - headers: ctx.req.raw.headers as any - }) - assert(session, 401, 'Unauthorized') - assert(session.user?.id, 401, 'Unauthorized') - assert(session.session, 401, 'Unauthorized') + // TODO + // const session = await auth.api.getSession({ + // // TODO: investigate this type issue + // headers: ctx.req.raw.headers as any + // }) + // assert(session, 401, 'Unauthorized') + // assert(session.user?.id, 401, 'Unauthorized') + // assert(session.session, 401, 'Unauthorized') - ctx.set('userId', session.user.id) - ctx.set('user', session.user as any) // TODO: resolve AuthUser and RawUser types - ctx.set('session', session.session) + // ctx.set('userId', session.user.id) + // ctx.set('user', session.user as any) // TODO: resolve AuthUser and RawUser types + // ctx.set('session', session.session) - await next() + // await next() - // const credentials = ctx.req.raw.headers.get('Authorization') - // assert(credentials, 401, 'Unauthorized') + const credentials = ctx.req.raw.headers.get('Authorization') + assert(credentials, 401, 'Unauthorized') - // const parts = credentials.split(/\s+/) - // assert( - // parts.length === 1 || - // (parts.length === 2 && parts[0]?.toLowerCase() === 'bearer'), - // 401, - // 'Unauthorized' - // ) - // const token = parts.at(-1) - // assert(token, 401, 'Unauthorized') + const parts = credentials.split(/\s+/) + assert( + parts.length === 1 || + (parts.length === 2 && parts[0]?.toLowerCase() === 'bearer'), + 401, + 'Unauthorized' + ) + const token = parts.at(-1) + assert(token, 401, 'Unauthorized') + + const verified = await authClient.verify(subjects, token) + assert(!verified.err, 401, 'Unauthorized') + + const userId = verified.subject.properties.id + assert(userId, 401, 'Unauthorized') + ctx.set('userId', userId) // const payload = await jwt.verify(token, env.JWT_SECRET) // assert(payload, 401, 'Unauthorized') @@ -50,6 +60,6 @@ export const authenticate = createMiddleware( // assert(user, 401, 'Unauthorized') // ctx.set('user', user as any) - // await next() + await next() } ) diff --git a/apps/api/src/lib/types.ts b/apps/api/src/lib/types.ts index 412ee543..9594156c 100644 --- a/apps/api/src/lib/types.ts +++ b/apps/api/src/lib/types.ts @@ -2,7 +2,6 @@ import type { Context } from 'hono' import type { RawTeamMember, RawUser } from '@/db' -import type { auth } from './auth' import type { Env } from './env' import type { Logger } from './logger' @@ -11,9 +10,6 @@ export type { OpenAPI3 as LooseOpenAPI3Spec } from 'openapi-typescript' export type Environment = Env['NODE_ENV'] export type Service = 'api' -// export type AuthUser = typeof auth.$Infer.Session.user -export type AuthSession = typeof auth.$Infer.Session.session - export type DefaultEnvVariables = { requestId: string logger: Logger @@ -22,7 +18,6 @@ export type DefaultEnvVariables = { export type AuthenticatedEnvVariables = DefaultEnvVariables & { userId: string user?: RawUser - session?: AuthSession teamMember?: RawTeamMember } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index c0dcb8a0..47fd2539 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,4 +1,4 @@ -import '@/lib/sentry' +import '@/lib/external/sentry' import { serve } from '@hono/node-server' import { sentry } from '@hono/sentry' @@ -10,6 +10,7 @@ import { apiV1 } from '@/api-v1' import { env } from '@/lib/env' import * as middleware from '@/lib/middleware' +import { authRouter } from './auth' import { initExitHooks } from './lib/exit-hooks' export const app = new OpenAPIHono() @@ -31,6 +32,8 @@ app.use(middleware.accessLogger) app.use(middleware.responseTime) app.use(middleware.errorHandler) +app.route('', authRouter) + app.route('/v1', apiV1) app.doc31('/docs', { diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 01eea6ca..e3313686 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -24,9 +24,10 @@ }, "dependencies": { "@agentic/platform-core": "workspace:*", - "better-auth": "^1.2.8", + "@openauthjs/openauth": "^0.4.3", "ky": "catalog:", - "type-fest": "catalog:" + "type-fest": "catalog:", + "zod": "catalog:" }, "devDependencies": { "openapi-typescript": "^7.8.0" diff --git a/packages/api-client/src/agentic-api-client.ts b/packages/api-client/src/agentic-api-client.ts index f6657251..da38d497 100644 --- a/packages/api-client/src/agentic-api-client.ts +++ b/packages/api-client/src/agentic-api-client.ts @@ -1,61 +1,153 @@ import type { Simplify } from 'type-fest' -import { assert, getEnv, sanitizeSearchParams } from '@agentic/platform-core' -import { createAuthClient } from 'better-auth/client' -import { username } from 'better-auth/plugins' +import { assert, sanitizeSearchParams } from '@agentic/platform-core' +import { + type Client as AuthClient, + createClient as createAuthClient +} from '@openauthjs/openauth/client' import defaultKy, { type KyInstance } from 'ky' import type { operations } from './openapi' -import type { AuthSession } from './types' +import type { + AuthorizeResult, + AuthTokens, + AuthUser, + OnUpdateAuthSessionFunction +} from './types' +import { subjects } from './subjects' export class AgenticApiClient { static readonly DEFAULT_API_BASE_URL = 'https://api.agentic.so' public readonly apiBaseUrl: string - public readonly authClient: ReturnType - public ky: KyInstance + public readonly ky: KyInstance + public readonly onUpdateAuth?: OnUpdateAuthSessionFunction + + protected _authTokens?: Readonly + protected _authClient: AuthClient constructor({ - apiCookie = getEnv('AGENTIC_API_COOKIE'), apiBaseUrl = AgenticApiClient.DEFAULT_API_BASE_URL, - ky = defaultKy + ky = defaultKy, + onUpdateAuth }: { - apiCookie?: string apiBaseUrl?: string ky?: KyInstance + onUpdateAuth?: OnUpdateAuthSessionFunction }) { assert(apiBaseUrl, 'AgenticApiClient missing required "apiBaseUrl"') + this.apiBaseUrl = apiBaseUrl + this.onUpdateAuth = onUpdateAuth + + this._authClient = createAuthClient({ + issuer: apiBaseUrl, + clientID: 'agentic-api-client' + }) this.ky = ky.extend({ prefixUrl: apiBaseUrl, - // headers: { Authorization: `Bearer ${apiKey}` } - headers: { cookie: apiCookie } - }) - - this.authClient = createAuthClient({ - baseURL: `${apiBaseUrl}/v1/auth`, - plugins: [username()] + hooks: { + beforeRequest: [ + async (request) => { + // Always verify freshness of auth tokens before making a request + await this.verifyAuthAndRefreshIfNecessary() + assert(this._authTokens, 'Not authenticated') + request.headers.set( + 'Authorization', + `Bearer ${this._authTokens.access}` + ) + } + ] + } }) } - async getAuthSession(cookie?: string): Promise { - return this.ky - .get('v1/auth/get-session', cookie ? { headers: { cookie } } : {}) - .json() + get isAuthenticated(): boolean { + return !!this._authTokens } - async setAuthSession(cookie: string): Promise { - this.ky = this.ky.extend({ - headers: { cookie } - }) - - return this.getAuthSession() + get authTokens(): Readonly | undefined { + return this._authTokens } - async clearAuthSession(): Promise { - this.ky = this.ky.extend({ - headers: {} + async setRefreshAuthToken(refreshToken: string): Promise { + const result = await this._authClient.refresh(refreshToken) + if (result.err) { + throw result.err + } + + this._authTokens = result.tokens + } + + async verifyAuthAndRefreshIfNecessary(): Promise { + if (!this._authTokens) { + throw new Error('This method requires authentication.') + } + + const verified = await this._authClient.verify( + subjects, + 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 { + 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' | 'password' + }): Promise { + return this._authClient.authorize(redirectUri, 'code', { + provider + }) + } + + async logout(): Promise { + this._authTokens = undefined + this.onUpdateAuth?.() + } + + async getMe(): Promise> { + const user = await this.verifyAuthAndRefreshIfNecessary() + + return this.ky.get(`v1/users/${user.id}`).json() } async getUser({ diff --git a/packages/api-client/src/subjects.ts b/packages/api-client/src/subjects.ts new file mode 100644 index 00000000..9bb6f338 --- /dev/null +++ b/packages/api-client/src/subjects.ts @@ -0,0 +1,10 @@ +import { createSubjects } from '@openauthjs/openauth/subject' +import { z } from 'zod' + +// TODO: share this with the API server +export const subjects = createSubjects({ + user: z.object({ + id: z.string() + }) +}) +export type AuthUser = z.infer diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts index a0344f56..dee35353 100644 --- a/packages/api-client/src/types.ts +++ b/packages/api-client/src/types.ts @@ -1,4 +1,7 @@ +import type { Tokens as AuthTokens } from '@openauthjs/openauth/client' + import type { components } from './openapi' +import type { AuthUser } from './subjects' export type Consumer = components['schemas']['Consumer'] export type Project = components['schemas']['Project'] @@ -23,30 +26,13 @@ export type PricingPlanName = components['schemas']['name'] export type PricingPlanSlug = components['schemas']['slug'] export type PricingPlanLabel = components['schemas']['label'] -export type AuthSession = { - session: Session +export type { AuthUser } from './subjects' +export type { + AuthorizeResult, + Tokens as AuthTokens +} from '@openauthjs/openauth/client' + +export type OnUpdateAuthSessionFunction = (update?: { + session: AuthTokens user: AuthUser -} - -export interface Session { - id: string - token: string - userId: string - ipAddress?: string | null - userAgent?: string | null - expiresAt: string - createdAt: string - updatedAt: string -} - -export interface AuthUser { - id: string - name: string - role: string - username?: string - email: string - emailVerified: boolean - image?: string - createdAt: string - updatedAt: string -} +}) => unknown diff --git a/packages/cli/src/auth-with-email-password.ts b/packages/cli/src/auth-with-email-password.ts deleted file mode 100644 index 2a800aca..00000000 --- a/packages/cli/src/auth-with-email-password.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { - AgenticApiClient, - AuthSession -} from '@agentic/platform-api-client' -import { assert } from '@agentic/platform-core' - -import { AuthStore } from './store' - -export async function authWithEmailPassword({ - client, - email, - password -}: { - client: AgenticApiClient - email: string - password: string -}): Promise { - let cookie: string | undefined - await client.authClient.signIn.email({ - email, - password, - fetchOptions: { - onSuccess: ({ response }) => { - cookie = response.headers.get('set-cookie')! - } - } - }) - assert(cookie, 'Failed to get auth cookie') - - const session = await client.setAuthSession(cookie) - assert(session, 'Failed to get auth session') - AuthStore.setAuth({ cookie, session }) - - return session -} diff --git a/packages/cli/src/auth-with-github.ts b/packages/cli/src/auth-with-github.ts deleted file mode 100644 index c90c188e..00000000 --- a/packages/cli/src/auth-with-github.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { - AgenticApiClient, - AuthSession -} from '@agentic/platform-api-client' -import { assert } from '@agentic/platform-core' -import { serve } from '@hono/node-server' -import getPort from 'get-port' -import { Hono } from 'hono' -import open from 'open' -import { oraPromise } from 'ora' - -import { client } from './client' -import { AuthStore } from './store' - -export async function authWithGitHub({ - preferredPort = 6013 -}: { - client: AgenticApiClient - preferredPort?: number -}): Promise { - const port = await getPort({ port: preferredPort }) - const app = new Hono() - - if (port !== preferredPort) { - throw new Error( - `Port ${preferredPort} is required to authenticate with GitHub, but it is already in use.` - ) - } - - let _resolveAuth: any - let _rejectAuth: any - - const authP = new Promise((resolve, reject) => { - _resolveAuth = resolve - _rejectAuth = reject - }) - - app.get('/callback/github/success', async (c) => { - const cookie = c.req.header().cookie - assert(cookie, 'Missing required auth cookie header') - - const session = await client.setAuthSession(cookie) - assert(session, 'Failed to get auth session') - - AuthStore.setAuth({ cookie, session }) - _resolveAuth(session) - - return c.text( - 'Huzzah! You are now signed in to the Agentic CLI with GitHub.\n\nYou may close this browser tab. 😄' - ) - }) - - const server = serve({ - fetch: app.fetch, - port - }) - - // TODO: clean these up - process.on('SIGINT', () => { - server.close() - process.exit(0) - }) - process.on('SIGTERM', () => { - server.close((err) => { - if (err) { - console.error(err) - process.exit(1) - } - process.exit(0) - }) - }) - - const res = await client.authClient.signIn.social({ - provider: 'github', - // TODO: add error url as well - callbackURL: `http://localhost:${port}/callback/github/success` - }) - assert( - !res.error, - ['Error signing in with GitHub', res.error?.code, res.error?.message] - .filter(Boolean) - .join(', ') - ) - assert(res.data?.url, 'No URL returned from authClient.signIn.social') - await open(res.data.url) - - const session = await oraPromise(authP, { - text: 'Authenticating with GitHub', - successText: 'You are now signed in with GitHub.', - failText: 'Failed to authenticate with GitHub.' - }) - server.close() - - return session -} diff --git a/packages/cli/src/auth.ts b/packages/cli/src/auth.ts new file mode 100644 index 00000000..4a24e856 --- /dev/null +++ b/packages/cli/src/auth.ts @@ -0,0 +1,108 @@ +import type { AgenticApiClient } from '@agentic/platform-api-client' +import { assert } from '@agentic/platform-core' +import { serve } from '@hono/node-server' +import getPort from 'get-port' +import { Hono } from 'hono' +import open from 'open' +import { oraPromise } from 'ora' + +import type { AuthSession } from './types' +import { AuthStore } from './store' + +const providerToLabel = { + github: 'GitHub', + password: 'email and password' +} + +export async function auth({ + client, + provider, + preferredPort = 6013 +}: { + client: AgenticApiClient + provider: 'github' | 'password' + preferredPort?: number +}): Promise { + const providerLabel = providerToLabel[provider]! + const port = await getPort({ port: preferredPort }) + const app = new Hono() + + if (port !== preferredPort) { + throw new Error( + `Port ${preferredPort} is required to sign in with ${providerLabel}, but it is already in use.` + ) + } + + const redirectUri = `http://localhost:${port}/callback/${provider}/success` + let _resolveAuth: any + let _rejectAuth: any + + const authP = new Promise((resolve, reject) => { + _resolveAuth = resolve + _rejectAuth = reject + }) + + app.get(`/callback/${provider}/success`, async (c) => { + // console.log(`/callback/${provider}/success`, c.req.method, c.req.url, { + // headers: c.req.header(), + // query: c.req.query() + // }) + + const code = c.req.query('code') + assert(code, 'Missing required code query parameter') + await client.exchangeAuthCode({ + code, + redirectUri, + verifier: authorizeResult.challenge?.verifier + }) + assert( + client.authTokens, + `Error ${providerLabel} auth: failed to exchange auth code for token` + ) + + // AuthStore should be updated via the onUpdateAuth callback + const session = AuthStore.tryGetAuth() + assert(session && session.refreshToken === client.authTokens.refresh) + _resolveAuth(session) + + return c.text( + `Huzzah! You are now signed in to the Agentic CLI.\n\nYou may close this browser tab. 😄` + ) + }) + + const server = serve({ + fetch: app.fetch, + port + }) + + // TODO: clean these up + process.on('SIGINT', () => { + server.close() + process.exit(0) + }) + process.on('SIGTERM', () => { + server.close((err) => { + if (err) { + console.error(err) + process.exit(1) + } + process.exit(0) + }) + }) + + const authorizeResult = await client.initAuthFlow({ + provider, + redirectUri + }) + assert(authorizeResult.url, `Error signing in with ${providerLabel}`) + await open(authorizeResult.url) + + const authSession = await oraPromise(authP, { + text: `Signing in with ${providerLabel}`, + successText: 'You are now signed in.', + failText: 'Failed to sign in.' + }) + server.close() + + return authSession +} diff --git a/packages/cli/src/client.ts b/packages/cli/src/client.ts deleted file mode 100644 index 6897cbbd..00000000 --- a/packages/cli/src/client.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AgenticApiClient } from '@agentic/platform-api-client' - -import { AuthStore } from './store' - -// Create a singleton instance of the API client -export const client = new AgenticApiClient({ - apiCookie: AuthStore.tryGetAuth()?.cookie, - apiBaseUrl: 'http://localhost:3000' -}) diff --git a/packages/cli/src/commands/signin.ts b/packages/cli/src/commands/signin.ts index 0dc7009b..a2259fcb 100644 --- a/packages/cli/src/commands/signin.ts +++ b/packages/cli/src/commands/signin.ts @@ -1,9 +1,7 @@ -import type { AuthSession } from '@agentic/platform-api-client' -import { Command, InvalidArgumentError } from 'commander' +import { Command } from 'commander' import type { Context } from '../types' -import { authWithEmailPassword } from '../auth-with-email-password' -import { authWithGitHub } from '../auth-with-github' +import { auth } from '../auth' export function registerSigninCommand({ client, program, logger }: Context) { const command = new Command('login') @@ -11,29 +9,21 @@ export function registerSigninCommand({ client, program, logger }: Context) { .description( 'Signs in to Agentic. If no credentials are provided, uses GitHub auth.' ) - // TODO - // .option('-u, --username ', 'account username') - .option('-e, --email ', 'account email') - .option('-p, --password ', 'account password') + .option('-e, --email', 'Log in using email and password') .action(async (opts) => { - let session: AuthSession | undefined if (opts.email) { - if (!opts.password) { - throw new InvalidArgumentError( - 'Password is required when using email' - ) - } - - session = await authWithEmailPassword({ + await auth({ client, - email: opts.email, - password: opts.password + provider: 'password' + // email: opts.email, + // password: opts.password }) } else { - session = await authWithGitHub({ client }) + await auth({ client, provider: 'github' }) } - logger.log(session) + const user = await client.getMe() + logger.log(user) }) program.addCommand(command) diff --git a/packages/cli/src/commands/signout.ts b/packages/cli/src/commands/signout.ts index f4d41694..c6d3672a 100644 --- a/packages/cli/src/commands/signout.ts +++ b/packages/cli/src/commands/signout.ts @@ -8,11 +8,11 @@ export function registerSignoutCommand({ client, program, logger }: Context) { .alias('signout') .description('Signs the current user out') .action(async () => { - if (!AuthStore.isAuthenticated()) { + if (!client.isAuthenticated) { return } - await client.clearAuthSession() + await client.logout() AuthStore.clearAuth() logger.log('Signed out') diff --git a/packages/cli/src/commands/whoami.ts b/packages/cli/src/commands/whoami.ts index 4ca0e883..5db085af 100644 --- a/packages/cli/src/commands/whoami.ts +++ b/packages/cli/src/commands/whoami.ts @@ -1,18 +1,17 @@ import { Command } from 'commander' import type { Context } from '../types' -import { AuthStore } from '../store' export function registerWhoAmICommand({ client, program, logger }: Context) { const command = new Command('whoami') .description('Displays info about the current user') .action(async () => { - if (!AuthStore.isAuthenticated()) { + if (!client.isAuthenticated) { logger.log('Not signed in') return } - const res = await client.getAuthSession() + const res = await client.getMe() logger.log(res) }) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f30cce17..c50bcca2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,10 +13,33 @@ async function main() { restoreCursor() const client = new AgenticApiClient({ - apiCookie: AuthStore.tryGetAuth()?.cookie, - apiBaseUrl: process.env.AGENTIC_API_BASE_URL + apiBaseUrl: process.env.AGENTIC_API_BASE_URL, + onUpdateAuth: (update) => { + if (update) { + AuthStore.setAuth({ + refreshToken: update.session.refresh, + user: update.user + }) + } else { + AuthStore.clearAuth() + } + } }) + // Try to initialize the existing auth session if one exists + const authSession = AuthStore.tryGetAuth() + if (authSession) { + try { + await client.setRefreshAuthToken(authSession.refreshToken) + } catch (err: any) { + console.warn( + 'Existing auth session is invalid; logging out...', + err.message + ) + AuthStore.clearAuth() + } + } + const program = new Command('agentic') .option('-j, --json', 'Print output in JSON format') .showHelpAfterError() diff --git a/packages/cli/src/store.ts b/packages/cli/src/store.ts index 57261ba7..478d2b26 100644 --- a/packages/cli/src/store.ts +++ b/packages/cli/src/store.ts @@ -1,24 +1,17 @@ -import type { AuthSession } from '@agentic/platform-api-client' import { assert } from '@agentic/platform-core' import Conf from 'conf' -export type AuthState = { - cookie: string - session: AuthSession - teamId?: string - teamSlug?: string -} +import type { AuthSession } from './types' -const keyTeamId = 'teamId' -const keyTeamSlug = 'teamSlug' -const keyCookie = 'cookie' -const keySession = 'session' +const keyAuthSession = 'authSession' export const AuthStore = { - store: new Conf({ projectName: 'agentic' }), + store: new Conf<{ + authSession: AuthSession + }>({ projectName: 'agentic' }), isAuthenticated() { - return this.store.has(keyCookie) && this.store.has(keySession) + return this.store.has(keyAuthSession) }, requireAuth() { @@ -28,43 +21,34 @@ export const AuthStore = { ) }, - tryGetAuth(): AuthState | undefined { + tryGetAuth(): AuthSession | undefined { if (!this.isAuthenticated()) { return undefined } - return { - cookie: this.store.get(keyCookie), - session: this.store.get(keySession), - teamId: this.store.get(keyTeamId), - teamSlug: this.store.get(keyTeamSlug) - } as AuthState + return this.store.get(keyAuthSession) }, - getAuth(): AuthState { + getAuth(): AuthSession { this.requireAuth() return this.tryGetAuth()! }, - setAuth({ cookie, session }: { cookie: string; session: AuthSession }) { - this.store.set(keyCookie, cookie) - this.store.set(keySession, session) + setAuth(authSession: AuthSession) { + this.store.set(keyAuthSession, authSession) }, clearAuth() { - this.store.delete(keyCookie) - this.store.delete(keySession) - this.store.delete(keyTeamId) - this.store.delete(keyTeamSlug) - }, - - switchTeam(team?: { id: string; slug: string }) { - if (team?.id) { - this.store.set(keyTeamId, team.id) - this.store.set(keyTeamSlug, team.slug) - } else { - this.store.delete(keyTeamId) - this.store.delete(keyTeamSlug) - } + this.store.delete(keyAuthSession) } + + // switchTeam(team?: { id: string; slug: string }) { + // if (team?.id) { + // this.store.set(keyTeamId, team.id) + // this.store.set(keyTeamSlug, team.slug) + // } else { + // this.store.delete(keyTeamId) + // this.store.delete(keyTeamSlug) + // } + // } } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index a4385b66..0147f17a 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,4 +1,4 @@ -import type { AgenticApiClient } from '@agentic/platform-api-client' +import type { AgenticApiClient, AuthUser } from '@agentic/platform-api-client' import type { Command } from 'commander' export type Context = { @@ -8,3 +8,8 @@ export type Context = { log: (...args: any[]) => void } } + +export type AuthSession = { + refreshToken: string + user: AuthUser +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd80159e..d6b34d80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,6 +149,9 @@ importers: '@hono/zod-openapi': specifier: ^0.19.6 version: 0.19.6(hono@4.7.9)(zod@3.24.4) + '@openauthjs/openauth': + specifier: ^0.4.3 + version: 0.4.3(arctic@2.3.4)(hono@4.7.9) '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 @@ -158,9 +161,6 @@ importers: '@sentry/node': specifier: ^9.19.0 version: 9.19.0 - better-auth: - specifier: ^1.2.8 - version: 1.2.8 eventid: specifier: ^2.0.1 version: 2.0.1 @@ -173,6 +173,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + octokit: + specifier: ^5.0.2 + version: 5.0.2 p-all: specifier: ^5.0.0 version: 5.0.0 @@ -222,15 +225,18 @@ importers: '@agentic/platform-core': specifier: workspace:* version: link:../core - better-auth: - specifier: ^1.2.8 - version: 1.2.8 + '@openauthjs/openauth': + specifier: ^0.4.3 + version: 0.4.3(arctic@2.3.4)(hono@4.7.9) ky: specifier: 'catalog:' version: 1.8.1 type-fest: specifier: 'catalog:' version: 4.41.0 + zod: + specifier: 'catalog:' + version: 3.24.4 devDependencies: openapi-typescript: specifier: ^7.8.0 @@ -332,12 +338,6 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@better-auth/utils@0.2.5': - resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==} - - '@better-fetch/fetch@1.1.18': - resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} - '@commander-js/extra-typings@14.0.0': resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==} peerDependencies: @@ -783,9 +783,6 @@ packages: '@fisch0920/drizzle-orm': '>=0.36.0' zod: '>=3.0.0' - '@hexagon/base64@1.1.28': - resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} - '@hono/node-server@1.14.1': resolution: {integrity: sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==} engines: {node: '>=18.14.1'} @@ -856,16 +853,10 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@levischuck/tiny-cbor@0.2.11': - resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} - '@modelcontextprotocol/sdk@1.11.2': resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==} engines: {node: '>=18'} - '@noble/ciphers@0.6.0': - resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} - '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -882,6 +873,119 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@octokit/app@16.0.1': + resolution: {integrity: sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==} + engines: {node: '>= 20'} + + '@octokit/auth-app@8.0.1': + resolution: {integrity: sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-app@9.0.1': + resolution: {integrity: sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-device@8.0.1': + resolution: {integrity: sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==} + engines: {node: '>= 20'} + + '@octokit/auth-oauth-user@6.0.0': + resolution: {integrity: sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==} + engines: {node: '>= 20'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/auth-unauthenticated@7.0.1': + resolution: {integrity: sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.2': + resolution: {integrity: sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.0': + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.1': + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} + engines: {node: '>= 20'} + + '@octokit/oauth-app@8.0.1': + resolution: {integrity: sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==} + engines: {node: '>= 20'} + + '@octokit/oauth-authorization-url@8.0.0': + resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} + engines: {node: '>= 20'} + + '@octokit/oauth-methods@6.0.0': + resolution: {integrity: sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@25.0.0': + resolution: {integrity: sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==} + + '@octokit/openapi-webhooks-types@11.0.0': + resolution: {integrity: sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==} + + '@octokit/plugin-paginate-graphql@6.0.0': + resolution: {integrity: sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-paginate-rest@13.0.0': + resolution: {integrity: sha512-nPXM3wgil9ONnAINcm8cN+nwso4QhNB13PtnlRFkYFHCUIogcH9DHak/StQYcwkkjuc7pUluLG1AWZNscgvH7Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@15.0.0': + resolution: {integrity: sha512-db6UdWvpX7O6tNsdkPk1BttVwTeVdA4n8RDFeXOyjBCPjE2YPufIAlzWh8CyeH8hl/3dSuQXDa+qLmsBlkTY+Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-retry@8.0.1': + resolution: {integrity: sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=7' + + '@octokit/plugin-throttling@11.0.1': + resolution: {integrity: sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': ^7.0.0 + + '@octokit/request-error@7.0.0': + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.2': + resolution: {integrity: sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==} + engines: {node: '>= 20'} + + '@octokit/types@14.0.0': + resolution: {integrity: sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==} + + '@octokit/webhooks-methods@6.0.0': + resolution: {integrity: sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==} + engines: {node: '>= 20'} + + '@octokit/webhooks@14.0.0': + resolution: {integrity: sha512-IZV4vg/s1pqIpCs86a0tp5FQ/O94DUaqksMdNrXFSaE037TXsB+fIhr8OVig09oEx3WazVgE6B2U+u7/Fvdlsw==} + engines: {node: '>= 20'} + + '@openauthjs/openauth@0.4.3': + resolution: {integrity: sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw==} + peerDependencies: + arctic: ^2.2.2 + hono: ^4.0.0 + '@opentelemetry/api-logs@0.57.2': resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} engines: {node: '>=14'} @@ -1070,24 +1174,27 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + + '@oslojs/crypto@1.0.1': + resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + + '@oslojs/encoding@0.4.1': + resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@oslojs/jwt@0.2.0': + resolution: {integrity: sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==} + '@paralleldrive/cuid2@2.2.2': resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} - '@peculiar/asn1-android@2.3.16': - resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==} - - '@peculiar/asn1-ecc@2.3.15': - resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} - - '@peculiar/asn1-rsa@2.3.15': - resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} - - '@peculiar/asn1-schema@2.3.15': - resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} - - '@peculiar/asn1-x509@2.3.15': - resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1241,20 +1348,19 @@ packages: resolution: {integrity: sha512-A4srR9mEBFdVXwSEKjQ94msUbVkMr8JeFiEj9ouOFORw/Y/ux/WV2bWVD/ZI9wq0TcTNK8L1wBgU8UMS5lIq3A==} engines: {node: '>=14.18'} - '@simplewebauthn/browser@13.1.0': - resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} - - '@simplewebauthn/server@13.1.1': - resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} - engines: {node: '>=20.0.0'} - '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@standard-schema/spec@1.0.0-beta.3': + resolution: {integrity: sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw==} + '@total-typescript/ts-reset@0.6.1': resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==} + '@types/aws-lambda@8.10.149': + resolution: {integrity: sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -1460,6 +1566,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + arctic@2.3.4: + resolution: {integrity: sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1499,10 +1608,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - asn1js@3.0.6: - resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} - engines: {node: '>=12.0.0'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1521,6 +1626,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -1535,11 +1643,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-auth@1.2.8: - resolution: {integrity: sha512-y8ry7ZW3/3ZIr82Eo1zUDtMzdoQlFnwNuZ0+b0RxoNZgqmvgTIc/0tCDC7NDJerqSu4UCzer0dvYxBsv3WMIGg==} - - better-call@1.0.9: - resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1548,6 +1653,9 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1816,9 +1924,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - del-cli@6.0.0: resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} engines: {node: '>=18'} @@ -2223,6 +2328,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2662,8 +2770,8 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -2917,10 +3025,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanostores@0.11.4: - resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==} - engines: {node: ^18.0.0 || >=20.0.0} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -2976,6 +3080,10 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + octokit@5.0.2: + resolution: {integrity: sha512-WCO9Oip2F+qsrIcNMfLwm1+dL2g70oO++pkmiluisJDMXXwdO4susVaVg1iQZgZNiDtA1qcLXs5662Mdj/vqdw==} + engines: {node: '>= 20'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3198,13 +3306,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pvtsutils@1.3.6: - resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} - - pvutils@1.1.3: - resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} - engines: {node: '>=6.0.0'} - qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -3314,9 +3415,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3373,9 +3471,6 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3621,6 +3716,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -3770,9 +3869,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - uncrypto@0.1.3: - resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3784,6 +3880,12 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + universal-github-app-jwt@2.2.2: + resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -4022,13 +4124,6 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@better-auth/utils@0.2.5': - dependencies: - typescript: 5.8.3 - uncrypto: 0.1.3 - - '@better-fetch/fetch@1.1.18': {} - '@commander-js/extra-typings@14.0.0(commander@14.0.0)': dependencies: commander: 14.0.0 @@ -4281,8 +4376,6 @@ snapshots: '@fisch0920/drizzle-orm': 0.43.7(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.5) zod: 3.24.4 - '@hexagon/base64@1.1.28': {} - '@hono/node-server@1.14.1(hono@4.7.9)': dependencies: hono: 4.7.9 @@ -4345,8 +4438,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@levischuck/tiny-cbor@0.2.11': {} - '@modelcontextprotocol/sdk@1.11.2': dependencies: content-type: 1.0.5 @@ -4362,8 +4453,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@noble/ciphers@0.6.0': {} - '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -4378,6 +4467,161 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@octokit/app@16.0.1': + dependencies: + '@octokit/auth-app': 8.0.1 + '@octokit/auth-unauthenticated': 7.0.1 + '@octokit/core': 7.0.2 + '@octokit/oauth-app': 8.0.1 + '@octokit/plugin-paginate-rest': 13.0.0(@octokit/core@7.0.2) + '@octokit/types': 14.0.0 + '@octokit/webhooks': 14.0.0 + + '@octokit/auth-app@8.0.1': + dependencies: + '@octokit/auth-oauth-app': 9.0.1 + '@octokit/auth-oauth-user': 6.0.0 + '@octokit/request': 10.0.2 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + toad-cache: 3.7.0 + universal-github-app-jwt: 2.2.2 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-app@9.0.1': + dependencies: + '@octokit/auth-oauth-device': 8.0.1 + '@octokit/auth-oauth-user': 6.0.0 + '@octokit/request': 10.0.2 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-device@8.0.1': + dependencies: + '@octokit/oauth-methods': 6.0.0 + '@octokit/request': 10.0.2 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-oauth-user@6.0.0': + dependencies: + '@octokit/auth-oauth-device': 8.0.1 + '@octokit/oauth-methods': 6.0.0 + '@octokit/request': 10.0.2 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/auth-token@6.0.0': {} + + '@octokit/auth-unauthenticated@7.0.1': + dependencies: + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + + '@octokit/core@7.0.2': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.1 + '@octokit/request': 10.0.2 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.0': + dependencies: + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.1': + dependencies: + '@octokit/request': 10.0.2 + '@octokit/types': 14.0.0 + universal-user-agent: 7.0.3 + + '@octokit/oauth-app@8.0.1': + dependencies: + '@octokit/auth-oauth-app': 9.0.1 + '@octokit/auth-oauth-user': 6.0.0 + '@octokit/auth-unauthenticated': 7.0.1 + '@octokit/core': 7.0.2 + '@octokit/oauth-authorization-url': 8.0.0 + '@octokit/oauth-methods': 6.0.0 + '@types/aws-lambda': 8.10.149 + universal-user-agent: 7.0.3 + + '@octokit/oauth-authorization-url@8.0.0': {} + + '@octokit/oauth-methods@6.0.0': + dependencies: + '@octokit/oauth-authorization-url': 8.0.0 + '@octokit/request': 10.0.2 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + + '@octokit/openapi-types@25.0.0': {} + + '@octokit/openapi-webhooks-types@11.0.0': {} + + '@octokit/plugin-paginate-graphql@6.0.0(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + + '@octokit/plugin-paginate-rest@13.0.0(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/types': 14.0.0 + + '@octokit/plugin-rest-endpoint-methods@15.0.0(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/types': 14.0.0 + + '@octokit/plugin-retry@8.0.1(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + bottleneck: 2.19.5 + + '@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.2)': + dependencies: + '@octokit/core': 7.0.2 + '@octokit/types': 14.0.0 + bottleneck: 2.19.5 + + '@octokit/request-error@7.0.0': + dependencies: + '@octokit/types': 14.0.0 + + '@octokit/request@10.0.2': + dependencies: + '@octokit/endpoint': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + fast-content-type-parse: 3.0.0 + universal-user-agent: 7.0.3 + + '@octokit/types@14.0.0': + dependencies: + '@octokit/openapi-types': 25.0.0 + + '@octokit/webhooks-methods@6.0.0': {} + + '@octokit/webhooks@14.0.0': + dependencies: + '@octokit/openapi-webhooks-types': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/webhooks-methods': 6.0.0 + + '@openauthjs/openauth@0.4.3(arctic@2.3.4)(hono@4.7.9)': + dependencies: + '@standard-schema/spec': 1.0.0-beta.3 + arctic: 2.3.4 + aws4fetch: 1.0.20 + hono: 4.7.9 + jose: 5.9.6 + '@opentelemetry/api-logs@0.57.2': dependencies: '@opentelemetry/api': 1.9.0 @@ -4620,43 +4864,29 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + + '@oslojs/binary@1.0.0': {} + + '@oslojs/crypto@1.0.1': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + + '@oslojs/encoding@0.4.1': {} + + '@oslojs/encoding@1.1.0': {} + + '@oslojs/jwt@0.2.0': + dependencies: + '@oslojs/encoding': 0.4.1 + '@paralleldrive/cuid2@2.2.2': dependencies: '@noble/hashes': 1.8.0 - '@peculiar/asn1-android@2.3.16': - dependencies: - '@peculiar/asn1-schema': 2.3.15 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-ecc@2.3.15': - dependencies: - '@peculiar/asn1-schema': 2.3.15 - '@peculiar/asn1-x509': 2.3.15 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-rsa@2.3.15': - dependencies: - '@peculiar/asn1-schema': 2.3.15 - '@peculiar/asn1-x509': 2.3.15 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-schema@2.3.15': - dependencies: - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - - '@peculiar/asn1-x509@2.3.15': - dependencies: - '@peculiar/asn1-schema': 2.3.15 - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - '@pkgjs/parseargs@0.11.0': optional: true @@ -4814,22 +5044,14 @@ snapshots: dependencies: '@sentry/types': 8.9.2 - '@simplewebauthn/browser@13.1.0': {} - - '@simplewebauthn/server@13.1.1': - dependencies: - '@hexagon/base64': 1.1.28 - '@levischuck/tiny-cbor': 0.2.11 - '@peculiar/asn1-android': 2.3.16 - '@peculiar/asn1-ecc': 2.3.15 - '@peculiar/asn1-rsa': 2.3.15 - '@peculiar/asn1-schema': 2.3.15 - '@peculiar/asn1-x509': 2.3.15 - '@sindresorhus/merge-streams@2.3.0': {} + '@standard-schema/spec@1.0.0-beta.3': {} + '@total-typescript/ts-reset@0.6.1': {} + '@types/aws-lambda@8.10.149': {} + '@types/connect@3.4.38': dependencies: '@types/node': 22.15.18 @@ -5066,6 +5288,12 @@ snapshots: any-promise@1.3.0: {} + arctic@2.3.4: + dependencies: + '@oslojs/crypto': 1.0.1 + '@oslojs/encoding': 1.1.0 + '@oslojs/jwt': 0.2.0 + argparse@2.0.1: {} aria-query@5.3.2: {} @@ -5135,12 +5363,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - asn1js@3.0.6: - dependencies: - pvtsutils: 1.3.6 - pvutils: 1.1.3 - tslib: 2.8.1 - assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} @@ -5156,6 +5378,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws4fetch@1.0.20: {} + axe-core@4.10.3: {} axobject-query@4.1.0: {} @@ -5164,27 +5388,7 @@ snapshots: base64-js@1.5.1: {} - better-auth@1.2.8: - dependencies: - '@better-auth/utils': 0.2.5 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 0.6.0 - '@noble/hashes': 1.8.0 - '@simplewebauthn/browser': 13.1.0 - '@simplewebauthn/server': 13.1.1 - better-call: 1.0.9 - defu: 6.1.4 - jose: 5.10.0 - kysely: 0.28.2 - nanostores: 0.11.4 - zod: 3.24.4 - - better-call@1.0.9: - dependencies: - '@better-fetch/fetch': 1.1.18 - rou3: 0.5.1 - set-cookie-parser: 2.7.1 - uncrypto: 0.1.3 + before-after-hook@4.0.0: {} bl@4.1.0: dependencies: @@ -5206,6 +5410,8 @@ snapshots: transitivePeerDependencies: - supports-color + bottleneck@2.19.5: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -5449,8 +5655,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} - del-cli@6.0.0: dependencies: del: 8.0.0 @@ -5968,6 +6172,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-content-type-parse@3.0.0: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6410,7 +6616,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jose@5.10.0: {} + jose@5.9.6: {} joycon@3.1.1: {} @@ -6479,7 +6685,8 @@ snapshots: ky@1.8.1: {} - kysely@0.28.2: {} + kysely@0.28.2: + optional: true language-subtag-registry@0.3.23: {} @@ -6639,8 +6846,6 @@ snapshots: nanoid@3.3.11: {} - nanostores@0.11.4: {} - natural-compare@1.4.0: {} negotiator@1.0.0: {} @@ -6708,6 +6913,20 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + octokit@5.0.2: + dependencies: + '@octokit/app': 16.0.1 + '@octokit/core': 7.0.2 + '@octokit/oauth-app': 8.0.1 + '@octokit/plugin-paginate-graphql': 6.0.0(@octokit/core@7.0.2) + '@octokit/plugin-paginate-rest': 13.0.0(@octokit/core@7.0.2) + '@octokit/plugin-rest-endpoint-methods': 15.0.0(@octokit/core@7.0.2) + '@octokit/plugin-retry': 8.0.1(@octokit/core@7.0.2) + '@octokit/plugin-throttling': 11.0.1(@octokit/core@7.0.2) + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.0.0 + '@octokit/webhooks': 14.0.0 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -6910,12 +7129,6 @@ snapshots: punycode@2.3.1: {} - pvtsutils@1.3.6: - dependencies: - tslib: 2.8.1 - - pvutils@1.1.3: {} - qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -7058,8 +7271,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 - rou3@0.5.1: {} - router@2.2.0: dependencies: debug: 4.4.1(supports-color@10.0.0) @@ -7138,8 +7349,6 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@2.7.1: {} - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -7408,6 +7617,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} toucan-js@4.1.1: @@ -7570,14 +7781,16 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - uncrypto@0.1.3: {} - undici-types@6.21.0: {} unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} + universal-github-app-jwt@2.2.2: {} + + universal-user-agent@7.0.3: {} + unpipe@1.0.0: {} update-browserslist-db@1.1.3(browserslist@4.24.4): diff --git a/readme.md b/readme.md index 86e336b5..0ae4e390 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,7 @@ - https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search - clerk / workos / auth0 - consider switching to [consola](https://github.com/unjs/consola) for logging? +- consider switching to `bun` (for `--hot` reloading!!) ## License