* feat: add personal access tokens * feat: replace MCP tokens with OAuth foundation * fix: keep OAuth constants private in auth foundation * fix: clean up mcp oauth branch scope * fix: expose oauth metadata endpoints * fix: trim mcp oauth config to non-default options Drop OIDC scopes, the org-id JWT claim, and the openid-configuration metadata endpoint since the MCP integration is OAuth-only and the org gets resolved server-side. Also remove options that just duplicated better-auth defaults. * fix: drop redundant oauth metadata helpers Remove `session.storeSessionInDatabase: true` since better-auth only enforces it when secondaryStorage is configured. Inline the `getHostedBaseUrlForOAuthMetadata` alias and skip the async `getOAuthServerConfig()` call in the protected-resource metadata handler — the issuer is just `baseURL` without a custom jwt.issuer override. * docs: explain cache headers on mcp metadata response * Use escaped file routes for OAuth metadata * save
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
import { relations, sql } from "drizzle-orm";
|
|
import {
|
|
sqliteTable,
|
|
text,
|
|
integer,
|
|
index,
|
|
uniqueIndex,
|
|
} from "drizzle-orm/sqlite-core";
|
|
|
|
export const user = sqliteTable("user", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
email: text("email").notNull().unique(),
|
|
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
.default(false)
|
|
.notNull(),
|
|
image: text("image"),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
analyticsOptedOut: integer("analytics_opted_out", { mode: "boolean" }),
|
|
});
|
|
|
|
export const session = sqliteTable(
|
|
"session",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
token: text("token").notNull().unique(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
ipAddress: text("ip_address"),
|
|
userAgent: text("user_agent"),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
activeOrganizationId: text("active_organization_id"),
|
|
},
|
|
(table) => [index("session_userId_idx").on(table.userId)],
|
|
);
|
|
|
|
export const account = sqliteTable(
|
|
"account",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
accountId: text("account_id").notNull(),
|
|
providerId: text("provider_id").notNull(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
accessToken: text("access_token"),
|
|
refreshToken: text("refresh_token"),
|
|
idToken: text("id_token"),
|
|
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
mode: "timestamp_ms",
|
|
}),
|
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
mode: "timestamp_ms",
|
|
}),
|
|
scope: text("scope"),
|
|
password: text("password"),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index("account_userId_idx").on(table.userId)],
|
|
);
|
|
|
|
export const verification = sqliteTable(
|
|
"verification",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
identifier: text("identifier").notNull(),
|
|
value: text("value").notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
.notNull(),
|
|
},
|
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
|
);
|
|
|
|
export const organization = sqliteTable(
|
|
"organization",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
slug: text("slug").notNull().unique(),
|
|
logo: text("logo"),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
metadata: text("metadata"),
|
|
},
|
|
(table) => [uniqueIndex("organization_slug_uidx").on(table.slug)],
|
|
);
|
|
|
|
export const member = sqliteTable(
|
|
"member",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
organizationId: text("organization_id")
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: "cascade" }),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
role: text("role").default("member").notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
},
|
|
(table) => [
|
|
index("member_organizationId_idx").on(table.organizationId),
|
|
index("member_userId_idx").on(table.userId),
|
|
],
|
|
);
|
|
|
|
export const invitation = sqliteTable(
|
|
"invitation",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
organizationId: text("organization_id")
|
|
.notNull()
|
|
.references(() => organization.id, { onDelete: "cascade" }),
|
|
email: text("email").notNull(),
|
|
role: text("role"),
|
|
status: text("status").default("pending").notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
.notNull(),
|
|
inviterId: text("inviter_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
},
|
|
(table) => [
|
|
index("invitation_organizationId_idx").on(table.organizationId),
|
|
index("invitation_email_idx").on(table.email),
|
|
],
|
|
);
|
|
|
|
export const jwks = sqliteTable("jwks", {
|
|
id: text("id").primaryKey(),
|
|
publicKey: text("public_key").notNull(),
|
|
privateKey: text("private_key").notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
|
|
});
|
|
|
|
export const oauthClient = sqliteTable("oauth_client", {
|
|
id: text("id").primaryKey(),
|
|
clientId: text("client_id").notNull().unique(),
|
|
clientSecret: text("client_secret"),
|
|
disabled: integer("disabled", { mode: "boolean" }).default(false),
|
|
skipConsent: integer("skip_consent", { mode: "boolean" }),
|
|
enableEndSession: integer("enable_end_session", { mode: "boolean" }),
|
|
subjectType: text("subject_type"),
|
|
scopes: text("scopes", { mode: "json" }),
|
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" }),
|
|
name: text("name"),
|
|
uri: text("uri"),
|
|
icon: text("icon"),
|
|
contacts: text("contacts", { mode: "json" }),
|
|
tos: text("tos"),
|
|
policy: text("policy"),
|
|
softwareId: text("software_id"),
|
|
softwareVersion: text("software_version"),
|
|
softwareStatement: text("software_statement"),
|
|
redirectUris: text("redirect_uris", { mode: "json" }).notNull(),
|
|
postLogoutRedirectUris: text("post_logout_redirect_uris", { mode: "json" }),
|
|
tokenEndpointAuthMethod: text("token_endpoint_auth_method"),
|
|
grantTypes: text("grant_types", { mode: "json" }),
|
|
responseTypes: text("response_types", { mode: "json" }),
|
|
public: integer("public", { mode: "boolean" }),
|
|
type: text("type"),
|
|
requirePKCE: integer("require_pkce", { mode: "boolean" }),
|
|
referenceId: text("reference_id"),
|
|
metadata: text("metadata", { mode: "json" }),
|
|
});
|
|
|
|
export const oauthRefreshToken = sqliteTable("oauth_refresh_token", {
|
|
id: text("id").primaryKey(),
|
|
token: text("token").notNull(),
|
|
clientId: text("client_id")
|
|
.notNull()
|
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
sessionId: text("session_id").references(() => session.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
referenceId: text("reference_id"),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
revoked: integer("revoked", { mode: "timestamp_ms" }),
|
|
authTime: integer("auth_time", { mode: "timestamp_ms" }),
|
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
});
|
|
|
|
export const oauthAccessToken = sqliteTable("oauth_access_token", {
|
|
id: text("id").primaryKey(),
|
|
token: text("token").notNull().unique(),
|
|
clientId: text("client_id")
|
|
.notNull()
|
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
sessionId: text("session_id").references(() => session.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
referenceId: text("reference_id"),
|
|
refreshId: text("refresh_id").references(() => oauthRefreshToken.id, {
|
|
onDelete: "cascade",
|
|
}),
|
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
});
|
|
|
|
export const oauthConsent = sqliteTable("oauth_consent", {
|
|
id: text("id").primaryKey(),
|
|
clientId: text("client_id")
|
|
.notNull()
|
|
.references(() => oauthClient.clientId, { onDelete: "cascade" }),
|
|
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
|
referenceId: text("reference_id"),
|
|
scopes: text("scopes", { mode: "json" }).notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
});
|
|
|
|
export const userRelations = relations(user, ({ many }) => ({
|
|
sessions: many(session),
|
|
accounts: many(account),
|
|
members: many(member),
|
|
invitations: many(invitation),
|
|
oauthClients: many(oauthClient),
|
|
oauthRefreshTokens: many(oauthRefreshToken),
|
|
oauthAccessTokens: many(oauthAccessToken),
|
|
oauthConsents: many(oauthConsent),
|
|
}));
|
|
|
|
export const sessionRelations = relations(session, ({ one, many }) => ({
|
|
user: one(user, {
|
|
fields: [session.userId],
|
|
references: [user.id],
|
|
}),
|
|
oauthRefreshTokens: many(oauthRefreshToken),
|
|
oauthAccessTokens: many(oauthAccessToken),
|
|
}));
|
|
|
|
export const accountRelations = relations(account, ({ one }) => ({
|
|
user: one(user, {
|
|
fields: [account.userId],
|
|
references: [user.id],
|
|
}),
|
|
}));
|
|
|
|
export const organizationRelations = relations(organization, ({ many }) => ({
|
|
members: many(member),
|
|
invitations: many(invitation),
|
|
}));
|
|
|
|
export const memberRelations = relations(member, ({ one }) => ({
|
|
organization: one(organization, {
|
|
fields: [member.organizationId],
|
|
references: [organization.id],
|
|
}),
|
|
user: one(user, {
|
|
fields: [member.userId],
|
|
references: [user.id],
|
|
}),
|
|
}));
|
|
|
|
export const invitationRelations = relations(invitation, ({ one }) => ({
|
|
organization: one(organization, {
|
|
fields: [invitation.organizationId],
|
|
references: [organization.id],
|
|
}),
|
|
user: one(user, {
|
|
fields: [invitation.inviterId],
|
|
references: [user.id],
|
|
}),
|
|
}));
|
|
|
|
export const oauthClientRelations = relations(oauthClient, ({ one, many }) => ({
|
|
user: one(user, {
|
|
fields: [oauthClient.userId],
|
|
references: [user.id],
|
|
}),
|
|
oauthRefreshTokens: many(oauthRefreshToken),
|
|
oauthAccessTokens: many(oauthAccessToken),
|
|
oauthConsents: many(oauthConsent),
|
|
}));
|
|
|
|
export const oauthRefreshTokenRelations = relations(
|
|
oauthRefreshToken,
|
|
({ one, many }) => ({
|
|
oauthClient: one(oauthClient, {
|
|
fields: [oauthRefreshToken.clientId],
|
|
references: [oauthClient.clientId],
|
|
}),
|
|
session: one(session, {
|
|
fields: [oauthRefreshToken.sessionId],
|
|
references: [session.id],
|
|
}),
|
|
user: one(user, {
|
|
fields: [oauthRefreshToken.userId],
|
|
references: [user.id],
|
|
}),
|
|
oauthAccessTokens: many(oauthAccessToken),
|
|
}),
|
|
);
|
|
|
|
export const oauthAccessTokenRelations = relations(
|
|
oauthAccessToken,
|
|
({ one }) => ({
|
|
oauthClient: one(oauthClient, {
|
|
fields: [oauthAccessToken.clientId],
|
|
references: [oauthClient.clientId],
|
|
}),
|
|
session: one(session, {
|
|
fields: [oauthAccessToken.sessionId],
|
|
references: [session.id],
|
|
}),
|
|
user: one(user, {
|
|
fields: [oauthAccessToken.userId],
|
|
references: [user.id],
|
|
}),
|
|
oauthRefreshToken: one(oauthRefreshToken, {
|
|
fields: [oauthAccessToken.refreshId],
|
|
references: [oauthRefreshToken.id],
|
|
}),
|
|
}),
|
|
);
|
|
|
|
export const oauthConsentRelations = relations(oauthConsent, ({ one }) => ({
|
|
oauthClient: one(oauthClient, {
|
|
fields: [oauthConsent.clientId],
|
|
references: [oauthClient.clientId],
|
|
}),
|
|
user: one(user, {
|
|
fields: [oauthConsent.userId],
|
|
references: [user.id],
|
|
}),
|
|
}));
|