feat(den): add org API key auth and management (#1368)

* feat(den): add org API key auth and management

Let org owners and admins manage named API keys while keeping keys scoped to the issuing member and org. Reuse existing org middleware so API-key-backed requests behave like normal user actions across Den routes.

* fix(den): polish API key creation flow

Avoid duplicating the visible key prefix in the table preview and make key creation an explicit one-time reveal flow. Show the create form only after the user asks for a new key, then replace it with the copyable secret after issuance.

* fix(den): simplify API key table UI

Remove rate-limit details from the Den web API keys screen and keep the page focused on naming, reveal, and deletion. Leave the generated next-env types file out of the commit.

---------

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-06 11:35:33 -07:00
committed by GitHub
parent 0589897b2f
commit 4f1905882b
26 changed files with 1194 additions and 78 deletions

View File

@@ -10,13 +10,14 @@
"start": "node dist/server.js"
},
"dependencies": {
"@better-auth/api-key": "^1.5.6",
"@openwork-ee/den-db": "workspace:*",
"@openwork-ee/utils": "workspace:*",
"@daytonaio/sdk": "^0.150.0",
"@hono/node-server": "^1.13.8",
"@hono/zod-validator": "^0.7.6",
"better-call": "^1.1.8",
"better-auth": "^1.4.18",
"better-call": "^1.3.2",
"better-auth": "^1.5.6",
"dotenv": "^16.4.5",
"hono": "^4.7.2",
"zod": "^4.3.6"

View File

@@ -0,0 +1,222 @@
import { and, asc, desc, eq, inArray } from "@openwork-ee/den-db/drizzle"
import { AuthApiKeyTable, AuthUserTable, MemberTable } from "@openwork-ee/den-db/schema"
import type { DenTypeId } from "@openwork-ee/utils/typeid"
import { db } from "./db.js"
export const DEN_API_KEY_HEADER = "x-api-key"
export const DEN_API_KEY_DEFAULT_PREFIX = "den_"
export const DEN_API_KEY_RATE_LIMIT_MAX = 600
export const DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS = 60_000
type UserId = typeof AuthUserTable.$inferSelect.id
type OrganizationId = typeof MemberTable.$inferSelect.organizationId
type OrganizationMemberId = typeof MemberTable.$inferSelect.id
type ApiKeyId = typeof AuthApiKeyTable.$inferSelect.id
export type DenApiKeyMetadata = {
organizationId: OrganizationId
orgMembershipId: OrganizationMemberId
issuedByUserId: UserId
issuedByOrgMembershipId: OrganizationMemberId
}
export type DenApiKeySession = {
id: ApiKeyId
configId: string
referenceId: string
metadata: DenApiKeyMetadata | null
}
export type OrganizationApiKeySummary = {
id: ApiKeyId
configId: string
name: string | null
start: string | null
prefix: string | null
enabled: boolean
rateLimitEnabled: boolean
rateLimitMax: number | null
rateLimitTimeWindow: number | null
lastRequest: Date | null
expiresAt: Date | null
createdAt: Date
updatedAt: Date
owner: {
userId: UserId
memberId: OrganizationMemberId
name: string
email: string
image: string | null
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function parseApiKeyMetadata(value: unknown): DenApiKeyMetadata | null {
const parsed = typeof value === "string"
? (() => {
try {
return JSON.parse(value) as unknown
} catch {
return null
}
})()
: value
if (!isRecord(parsed)) {
return null
}
const organizationId = typeof parsed.organizationId === "string" ? parsed.organizationId : null
const orgMembershipId = typeof parsed.orgMembershipId === "string" ? parsed.orgMembershipId : null
const issuedByUserId = typeof parsed.issuedByUserId === "string" ? parsed.issuedByUserId : null
const issuedByOrgMembershipId = typeof parsed.issuedByOrgMembershipId === "string" ? parsed.issuedByOrgMembershipId : null
if (!organizationId || !orgMembershipId || !issuedByUserId || !issuedByOrgMembershipId) {
return null
}
return {
organizationId: organizationId as OrganizationId,
orgMembershipId: orgMembershipId as OrganizationMemberId,
issuedByUserId: issuedByUserId as UserId,
issuedByOrgMembershipId: issuedByOrgMembershipId as OrganizationMemberId,
}
}
export function buildOrganizationApiKeyMetadata(input: {
organizationId: OrganizationId
orgMembershipId: OrganizationMemberId
issuedByUserId: UserId
issuedByOrgMembershipId: OrganizationMemberId
}): DenApiKeyMetadata {
return {
organizationId: input.organizationId,
orgMembershipId: input.orgMembershipId,
issuedByUserId: input.issuedByUserId,
issuedByOrgMembershipId: input.issuedByOrgMembershipId,
}
}
export async function getApiKeySessionById(apiKeyId: string): Promise<DenApiKeySession | null> {
const rows = await db
.select({
id: AuthApiKeyTable.id,
configId: AuthApiKeyTable.configId,
referenceId: AuthApiKeyTable.referenceId,
metadata: AuthApiKeyTable.metadata,
})
.from(AuthApiKeyTable)
.where(eq(AuthApiKeyTable.id, apiKeyId))
.limit(1)
const row = rows[0]
if (!row) {
return null
}
return {
id: row.id,
configId: row.configId,
referenceId: row.referenceId,
metadata: parseApiKeyMetadata(row.metadata),
}
}
export async function listOrganizationApiKeys(organizationId: OrganizationId): Promise<OrganizationApiKeySummary[]> {
const members = await db
.select({
memberId: MemberTable.id,
userId: MemberTable.userId,
userName: AuthUserTable.name,
userEmail: AuthUserTable.email,
userImage: AuthUserTable.image,
})
.from(MemberTable)
.innerJoin(AuthUserTable, eq(MemberTable.userId, AuthUserTable.id))
.where(eq(MemberTable.organizationId, organizationId))
.orderBy(asc(MemberTable.createdAt))
if (members.length === 0) {
return []
}
const memberByUserId = new Map(members.map((member) => [member.userId, member]))
const apiKeys = await db
.select()
.from(AuthApiKeyTable)
.where(inArray(AuthApiKeyTable.referenceId, members.map((member) => member.userId)))
.orderBy(desc(AuthApiKeyTable.createdAt))
return apiKeys
.map((apiKey) => {
const owner = memberByUserId.get(apiKey.referenceId as UserId)
const metadata = parseApiKeyMetadata(apiKey.metadata)
if (!owner || !metadata || metadata.organizationId !== organizationId || metadata.orgMembershipId !== owner.memberId) {
return null
}
return {
id: apiKey.id,
configId: apiKey.configId,
name: apiKey.name,
start: apiKey.start,
prefix: apiKey.prefix,
enabled: apiKey.enabled,
rateLimitEnabled: apiKey.rateLimitEnabled,
rateLimitMax: apiKey.rateLimitMax,
rateLimitTimeWindow: apiKey.rateLimitTimeWindow,
lastRequest: apiKey.lastRequest,
expiresAt: apiKey.expiresAt,
createdAt: apiKey.createdAt,
updatedAt: apiKey.updatedAt,
owner: {
userId: owner.userId,
memberId: owner.memberId,
name: owner.userName,
email: owner.userEmail,
image: owner.userImage,
},
} satisfies OrganizationApiKeySummary
})
.filter((apiKey): apiKey is OrganizationApiKeySummary => apiKey !== null)
}
export async function getOrganizationApiKeyById(input: {
organizationId: OrganizationId
apiKeyId: ApiKeyId
}) {
const keys = await listOrganizationApiKeys(input.organizationId)
return keys.find((apiKey) => apiKey.id === input.apiKeyId) ?? null
}
export async function deleteOrganizationApiKey(input: {
organizationId: OrganizationId
apiKeyId: ApiKeyId
}) {
const apiKey = await getOrganizationApiKeyById(input)
if (!apiKey) {
return null
}
await db
.delete(AuthApiKeyTable)
.where(and(eq(AuthApiKeyTable.id, input.apiKeyId), eq(AuthApiKeyTable.referenceId, apiKey.owner.userId)))
return apiKey
}
export function isScopedApiKeyForOrganization(input: {
apiKey: DenApiKeySession | null
organizationId: string
}) {
return input.apiKey?.metadata?.organizationId === input.organizationId
}
export function getApiKeyScopedOrganizationId(apiKey: DenApiKeySession | null): DenTypeId<"organization"> | null {
return apiKey?.metadata?.organizationId ?? null
}

View File

@@ -32,13 +32,13 @@ app.use("*", async (c, next) => {
if (env.corsOrigins.length > 0) {
app.use(
"*",
cors({
origin: env.corsOrigins,
credentials: true,
allowHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Request-Id"],
maxAge: 600,
cors({
origin: env.corsOrigins,
credentials: true,
allowHeaders: ["Content-Type", "Authorization", "X-Api-Key", "X-Request-Id"],
allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
exposeHeaders: ["Content-Length", "X-Request-Id"],
maxAge: 600,
}),
)
}

View File

@@ -2,10 +2,16 @@ import { db } from "./db.js"
import { env } from "./env.js"
import { sendDenOrganizationInvitationEmail, sendDenVerificationEmail } from "./email.js"
import { syncDenSignupContact } from "./loops.js"
import {
DEN_API_KEY_DEFAULT_PREFIX,
DEN_API_KEY_RATE_LIMIT_MAX,
DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
} from "./api-keys.js"
import { denOrganizationAccess, denOrganizationStaticRoles } from "./organization-access.js"
import { seedDefaultOrganizationRoles } from "./orgs.js"
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import * as schema from "@openwork-ee/den-db/schema"
import { apiKey } from "@better-auth/api-key"
import { APIError } from "better-call"
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
@@ -71,6 +77,9 @@ export const auth = betterAuth({
return createDenTypeId("account")
case "verification":
return createDenTypeId("verification")
case "apikey":
case "apiKey":
return createDenTypeId("apiKey")
case "rateLimit":
return createDenTypeId("rateLimit")
case "organization":
@@ -201,5 +210,18 @@ export const auth = betterAuth({
},
},
}),
apiKey({
defaultPrefix: DEN_API_KEY_DEFAULT_PREFIX,
enableMetadata: true,
enableSessionForAPIKeys: true,
maximumNameLength: 64,
requireName: true,
storage: "database",
rateLimit: {
enabled: true,
maxRequests: DEN_API_KEY_RATE_LIMIT_MAX,
timeWindow: DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
},
}),
],
})

View File

@@ -1,5 +1,6 @@
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { MiddlewareHandler } from "hono"
import { isScopedApiKeyForOrganization } from "../api-keys.js"
import { getOrganizationContextForUser, type OrganizationContext } from "../orgs.js"
import type { AuthContextVariables } from "../session.js"
@@ -37,6 +38,21 @@ export const resolveOrganizationContextMiddleware: MiddlewareHandler<{
return c.json({ error: "organization_not_found" }, 404) as never
}
const apiKey = c.get("apiKey")
if (apiKey && !isScopedApiKeyForOrganization({ apiKey, organizationId })) {
return c.json({
error: "forbidden",
message: "This API key is scoped to a different organization.",
}, 403) as never
}
if (apiKey?.metadata?.orgMembershipId && apiKey.metadata.orgMembershipId !== context.currentMember.id) {
return c.json({
error: "forbidden",
message: "This API key is no longer valid for the current organization member.",
}, 403) as never
}
c.set("organizationContext", context)
await next()
}

View File

@@ -1,5 +1,6 @@
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { MiddlewareHandler } from "hono"
import { getApiKeyScopedOrganizationId } from "../api-keys.js"
import { resolveUserOrganizations, type UserOrgSummary } from "../orgs.js"
import type { AuthContextVariables } from "../session.js"
@@ -18,13 +19,24 @@ export const resolveUserOrganizationsMiddleware: MiddlewareHandler<{
}
const session = c.get("session")
const apiKey = c.get("apiKey")
const scopedOrganizationId = getApiKeyScopedOrganizationId(apiKey)
const resolved = await resolveUserOrganizations({
activeOrganizationId: session?.activeOrganizationId ?? null,
activeOrganizationId: scopedOrganizationId ?? session?.activeOrganizationId ?? null,
userId: normalizeDenTypeId("user", user.id),
})
c.set("userOrganizations", resolved.orgs)
c.set("activeOrganizationId", resolved.activeOrgId)
c.set("activeOrganizationSlug", resolved.activeOrgSlug)
const scopedOrgs = scopedOrganizationId
? resolved.orgs.filter((org) => org.id === scopedOrganizationId)
: resolved.orgs
c.set("userOrganizations", scopedOrgs)
c.set("activeOrganizationId", scopedOrganizationId ? scopedOrgs[0]?.id ?? null : resolved.activeOrgId)
c.set(
"activeOrganizationSlug",
scopedOrganizationId
? scopedOrgs[0]?.slug ?? null
: resolved.activeOrgSlug,
)
await next()
}

View File

@@ -178,6 +178,12 @@ export function serializePermissionRecord(value: Record<string, string[]>) {
return JSON.stringify(value)
}
function clonePermissionRecord(value: Record<string, readonly string[]>) {
return Object.fromEntries(
Object.entries(value).map(([resource, actions]) => [resource, [...actions]]),
) as Record<string, string[]>
}
async function listMembershipRows(userId: UserId) {
return db
.select()
@@ -213,17 +219,18 @@ async function getInvitationById(invitationIdRaw: string) {
async function ensureDefaultDynamicRoles(orgId: OrgId) {
for (const [role, permission] of Object.entries(denDefaultDynamicOrganizationRoles)) {
const serializedPermission = serializePermissionRecord(clonePermissionRecord(permission))
await db
.insert(OrganizationRoleTable)
.values({
id: createDenTypeId("organizationRole"),
organizationId: orgId,
role,
permission: serializePermissionRecord(permission),
permission: serializedPermission,
})
.onDuplicateKeyUpdate({
set: {
permission: serializePermissionRecord(permission),
permission: serializedPermission,
},
})
}
@@ -656,7 +663,7 @@ export async function getOrganizationContextForUser(input: {
{
id: "builtin-owner",
role: "owner",
permission: denOrganizationStaticRoles.owner.statements,
permission: clonePermissionRecord(denOrganizationStaticRoles.owner.statements),
builtIn: true,
protected: true,
createdAt: null,

View File

@@ -0,0 +1,112 @@
import type { Hono } from "hono"
import { z } from "zod"
import {
buildOrganizationApiKeyMetadata,
deleteOrganizationApiKey,
DEN_API_KEY_RATE_LIMIT_MAX,
DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
listOrganizationApiKeys,
} from "../../api-keys.js"
import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { auth } from "../../auth.js"
import type { OrgRouteVariables } from "./shared.js"
import { ensureApiKeyManager, idParamSchema, orgIdParamSchema } from "./shared.js"
const createOrganizationApiKeySchema = z.object({
name: z.string().trim().min(2).max(64),
})
const apiKeyIdParamSchema = orgIdParamSchema.extend(idParamSchema("apiKeyId").shape)
export function registerOrgApiKeyRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.get(
"/v1/orgs/:orgId/api-keys",
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const access = ensureApiKeyManager(c)
if (!access.ok) {
return c.json(access.response, access.response.error === "forbidden" ? 403 : 404)
}
const payload = c.get("organizationContext")
const apiKeys = await listOrganizationApiKeys(payload.organization.id)
return c.json({ apiKeys })
},
)
app.post(
"/v1/orgs/:orgId/api-keys",
requireUserMiddleware,
paramValidator(orgIdParamSchema),
resolveOrganizationContextMiddleware,
jsonValidator(createOrganizationApiKeySchema),
async (c) => {
const access = ensureApiKeyManager(c)
if (!access.ok) {
return c.json(access.response, access.response.error === "forbidden" ? 403 : 404)
}
const payload = c.get("organizationContext")
const input = c.req.valid("json")
const created = await auth.api.createApiKey({
body: {
userId: payload.currentMember.userId,
name: input.name,
metadata: buildOrganizationApiKeyMetadata({
organizationId: payload.organization.id,
orgMembershipId: payload.currentMember.id,
issuedByUserId: payload.currentMember.userId,
issuedByOrgMembershipId: payload.currentMember.id,
}),
rateLimitEnabled: true,
rateLimitMax: DEN_API_KEY_RATE_LIMIT_MAX,
rateLimitTimeWindow: DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
},
})
return c.json({
apiKey: {
id: created.id,
name: created.name,
start: created.start,
prefix: created.prefix,
enabled: created.enabled,
rateLimitEnabled: created.rateLimitEnabled,
rateLimitMax: created.rateLimitMax,
rateLimitTimeWindow: created.rateLimitTimeWindow,
createdAt: created.createdAt,
updatedAt: created.updatedAt,
},
key: created.key,
}, 201)
},
)
app.delete(
"/v1/orgs/:orgId/api-keys/:apiKeyId",
requireUserMiddleware,
paramValidator(apiKeyIdParamSchema),
resolveOrganizationContextMiddleware,
async (c) => {
const access = ensureApiKeyManager(c)
if (!access.ok) {
return c.json(access.response, access.response.error === "forbidden" ? 403 : 404)
}
const payload = c.get("organizationContext")
const params = c.req.valid("param")
const deleted = await deleteOrganizationApiKey({
organizationId: payload.organization.id,
apiKeyId: params.apiKeyId,
})
if (!deleted) {
return c.json({ error: "api_key_not_found" }, 404)
}
return c.body(null, 204)
},
)
}

View File

@@ -24,8 +24,27 @@ const acceptInvitationSchema = z.object({
id: z.string().trim().min(1),
})
function getStoredSessionId(session: { id?: string | null } | null) {
if (!session?.id) {
return null
}
try {
return normalizeDenTypeId("session", session.id)
} catch {
return null
}
}
export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
app.post("/v1/orgs", requireUserMiddleware, jsonValidator(createOrganizationSchema), async (c) => {
if (c.get("apiKey")) {
return c.json({
error: "forbidden",
message: "API keys cannot create organizations.",
}, 403)
}
const user = c.get("user")
const session = c.get("session")
const input = c.req.valid("json")
@@ -58,8 +77,9 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
name: input.name,
})
if (session?.id) {
await setSessionActiveOrganization(normalizeDenTypeId("session", session.id), organizationId)
const sessionId = getStoredSessionId(session)
if (sessionId) {
await setSessionActiveOrganization(sessionId, organizationId)
}
const organization = await db
@@ -83,6 +103,13 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
})
app.post("/v1/orgs/invitations/accept", requireUserMiddleware, jsonValidator(acceptInvitationSchema), async (c) => {
if (c.get("apiKey")) {
return c.json({
error: "forbidden",
message: "API keys cannot accept organization invitations.",
}, 403)
}
const user = c.get("user")
const session = c.get("session")
const input = c.req.valid("json")
@@ -102,8 +129,9 @@ export function registerOrgCoreRoutes<T extends { Variables: OrgRouteVariables }
return c.json({ error: "invitation_not_found" }, 404)
}
if (session?.id) {
await setSessionActiveOrganization(normalizeDenTypeId("session", session.id), accepted.member.organizationId)
const sessionId = getStoredSessionId(session)
if (sessionId) {
await setSessionActiveOrganization(sessionId, accepted.member.organizationId)
}
const orgRows = await db

View File

@@ -1,4 +1,5 @@
import type { Hono } from "hono"
import { registerOrgApiKeyRoutes } from "./api-keys.js"
import type { OrgRouteVariables } from "./shared.js"
import { registerOrgCoreRoutes } from "./core.js"
import { registerOrgInvitationRoutes } from "./invitations.js"
@@ -11,6 +12,7 @@ import { registerOrgTemplateRoutes } from "./templates.js"
export function registerOrgRoutes<T extends { Variables: OrgRouteVariables }>(app: Hono<T>) {
registerOrgCoreRoutes(app)
registerOrgApiKeyRoutes(app)
registerOrgInvitationRoutes(app)
registerOrgLlmProviderRoutes(app)
registerOrgMemberRoutes(app)

View File

@@ -128,6 +128,30 @@ export function ensureTeamManager(c: { get: (key: "organizationContext") => OrgR
}
}
export function ensureApiKeyManager(c: { get: (key: "organizationContext") => OrgRouteVariables["organizationContext"] }) {
const payload = c.get("organizationContext")
if (!payload) {
return {
ok: false as const,
response: {
error: "organization_not_found",
},
}
}
if (payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin")) {
return { ok: true as const }
}
return {
ok: false as const,
response: {
error: "forbidden",
message: "Only organization owners and admins can manage API keys.",
},
}
}
export function createInvitationId() {
return createDenTypeId("invitation")
}

View File

@@ -2,6 +2,7 @@ import { and, eq, gt } from "@openwork-ee/den-db/drizzle"
import { AuthSessionTable, AuthUserTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { MiddlewareHandler } from "hono"
import { DEN_API_KEY_HEADER, getApiKeySessionById, type DenApiKeySession } from "./api-keys.js"
import { auth } from "./auth.js"
import { db } from "./db.js"
@@ -11,6 +12,7 @@ type AuthSessionValue = NonNullable<AuthSessionLike>
export type AuthContextVariables = {
user: AuthSessionValue["user"] | null
session: AuthSessionValue["session"] | null
apiKey: DenApiKeySession | null
}
function readBearerToken(headers: Headers): string | null {
@@ -73,7 +75,13 @@ async function getSessionFromBearerToken(token: string): Promise<AuthSessionLike
}
export async function getRequestSession(headers: Headers): Promise<AuthSessionLike> {
const cookieSession = await auth.api.getSession({ headers })
let cookieSession: AuthSessionLike
try {
cookieSession = await auth.api.getSession({ headers })
} catch {
return null
}
if (cookieSession?.user?.id) {
return {
...cookieSession,
@@ -92,9 +100,19 @@ export async function getRequestSession(headers: Headers): Promise<AuthSessionLi
return getSessionFromBearerToken(bearerToken)
}
async function getRequestApiKeySession(headers: Headers, session: AuthSessionLike): Promise<DenApiKeySession | null> {
if (!headers.has(DEN_API_KEY_HEADER) || !session?.session?.id) {
return null
}
return getApiKeySessionById(session.session.id)
}
export const sessionMiddleware: MiddlewareHandler<{ Variables: AuthContextVariables }> = async (c, next) => {
const resolved = await getRequestSession(c.req.raw.headers)
const apiKey = await getRequestApiKeySession(c.req.raw.headers, resolved)
c.set("user", resolved?.user ?? null)
c.set("session", resolved?.session ?? null)
c.set("apiKey", apiKey)
await next()
}

View File

@@ -77,6 +77,29 @@ export type DenOrgRole = {
updatedAt: string | null;
};
export type DenOrgApiKey = {
id: string;
configId: string;
name: string | null;
start: string | null;
prefix: string | null;
enabled: boolean;
rateLimitEnabled: boolean;
rateLimitMax: number | null;
rateLimitTimeWindow: number | null;
lastRequest: string | null;
expiresAt: string | null;
createdAt: string | null;
updatedAt: string | null;
owner: {
userId: string;
memberId: string;
name: string;
email: string;
image: string | null;
};
};
export type DenOrgContext = {
organization: {
id: string;
@@ -161,6 +184,7 @@ export function getOrgAccessFlags(roleValue: string, isOwner: boolean) {
canManageMembers: isOwner,
canManageRoles: isOwner,
canManageTeams: isAdmin,
canManageApiKeys: isAdmin,
};
}
@@ -220,6 +244,10 @@ export function getBillingRoute(orgSlug: string): string {
return `${getOrgDashboardRoute(orgSlug)}/billing`;
}
export function getApiKeysRoute(orgSlug: string): string {
return `${getOrgDashboardRoute(orgSlug)}/api-keys`;
}
export function getSkillHubsRoute(orgSlug: string): string {
return `${getOrgDashboardRoute(orgSlug)}/skill-hubs`;
}
@@ -509,3 +537,52 @@ export function parseInvitationPreviewPayload(payload: unknown): DenInvitationPr
},
};
}
export function parseOrgApiKeysPayload(payload: unknown): DenOrgApiKey[] {
if (!isRecord(payload) || !Array.isArray(payload.apiKeys)) {
return [];
}
return payload.apiKeys
.map((entry) => {
if (!isRecord(entry) || !isRecord(entry.owner)) {
return null;
}
const id = asString(entry.id);
const configId = asString(entry.configId);
const owner = entry.owner;
const ownerUserId = asString(owner.userId);
const ownerMemberId = asString(owner.memberId);
const ownerName = asString(owner.name);
const ownerEmail = asString(owner.email);
if (!id || !configId || !ownerUserId || !ownerMemberId || !ownerName || !ownerEmail) {
return null;
}
return {
id,
configId,
name: asString(entry.name),
start: asString(entry.start),
prefix: asString(entry.prefix),
enabled: asBoolean(entry.enabled),
rateLimitEnabled: asBoolean(entry.rateLimitEnabled),
rateLimitMax: typeof entry.rateLimitMax === "number" ? entry.rateLimitMax : null,
rateLimitTimeWindow: typeof entry.rateLimitTimeWindow === "number" ? entry.rateLimitTimeWindow : null,
lastRequest: asIsoString(entry.lastRequest),
expiresAt: asIsoString(entry.expiresAt),
createdAt: asIsoString(entry.createdAt),
updatedAt: asIsoString(entry.updatedAt),
owner: {
userId: ownerUserId,
memberId: ownerMemberId,
name: ownerName,
email: ownerEmail,
image: asString(owner.image),
},
} satisfies DenOrgApiKey;
})
.filter((entry): entry is DenOrgApiKey => entry !== null);
}

View File

@@ -0,0 +1,356 @@
"use client";
import { type FormEvent, useEffect, useMemo, useState } from "react";
import { Copy, KeyRound, Trash2 } from "lucide-react";
import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page-template";
import { DenButton } from "../../../../_components/ui/button";
import { DenInput } from "../../../../_components/ui/input";
import { getErrorMessage, requestJson } from "../../../../_lib/den-flow";
import { getOrgAccessFlags, parseOrgApiKeysPayload, type DenOrgApiKey } from "../../../../_lib/den-org";
import { useOrgDashboard } from "../_providers/org-dashboard-provider";
function formatDateTime(value: string | null) {
if (!value) {
return "Never";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "Never";
}
return date.toLocaleString();
}
function formatKeyPreview(apiKey: DenOrgApiKey) {
if (apiKey.start) {
return `${apiKey.start}...`;
}
if (apiKey.prefix) {
return `${apiKey.prefix}${apiKey.id.slice(0, 6)}...`;
}
return `${apiKey.id.slice(0, 6)}...`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function getCreatedKey(payload: unknown) {
if (!isRecord(payload) || typeof payload.key !== "string") {
return null;
}
return payload.key;
}
export function ApiKeysScreen() {
const { orgId, orgContext } = useOrgDashboard();
const [apiKeys, setApiKeys] = useState<DenOrgApiKey[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState("");
const [creating, setCreating] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [createdKey, setCreatedKey] = useState<string | null>(null);
const [createdKeyName, setCreatedKeyName] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const access = useMemo(
() => getOrgAccessFlags(orgContext?.currentMember.role ?? "member", orgContext?.currentMember.isOwner ?? false),
[orgContext?.currentMember.isOwner, orgContext?.currentMember.role],
);
async function loadApiKeys() {
if (!orgId || !access.canManageApiKeys) {
setApiKeys([]);
return;
}
setBusy(true);
setError(null);
try {
const { response, payload } = await requestJson(`/v1/orgs/${encodeURIComponent(orgId)}/api-keys`, { method: "GET" }, 12000);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to load API keys (${response.status}).`));
}
setApiKeys(parseOrgApiKeysPayload(payload));
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : "Failed to load API keys.");
} finally {
setBusy(false);
}
}
useEffect(() => {
void loadApiKeys();
}, [orgId, access.canManageApiKeys]);
useEffect(() => {
if (!copied) {
return;
}
const timeout = window.setTimeout(() => setCopied(false), 1500);
return () => window.clearTimeout(timeout);
}, [copied]);
async function handleCreate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!orgId) {
setError("Organization not found.");
return;
}
setCreating(true);
setError(null);
setCreatedKey(null);
setCreatedKeyName(null);
setCopied(false);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/api-keys`,
{
method: "POST",
body: JSON.stringify({ name }),
},
12000,
);
if (!response.ok) {
throw new Error(getErrorMessage(payload, `Failed to create API key (${response.status}).`));
}
const nextKey = getCreatedKey(payload);
if (!nextKey) {
throw new Error("API key was created, but the secret was not returned.");
}
setCreatedKey(nextKey);
setCreatedKeyName(name);
setName("");
setShowCreateForm(false);
await loadApiKeys();
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : "Failed to create API key.");
} finally {
setCreating(false);
}
}
function openCreateForm() {
setError(null);
setCopied(false);
setCreatedKey(null);
setCreatedKeyName(null);
setName("");
setShowCreateForm(true);
}
function closeCreateForm() {
setName("");
setShowCreateForm(false);
}
async function handleDelete(apiKey: DenOrgApiKey) {
if (!orgId || !window.confirm(`Delete ${apiKey.name ?? apiKey.start ?? "this API key"}? This cannot be undone.`)) {
return;
}
setDeletingId(apiKey.id);
setError(null);
try {
const { response, payload } = await requestJson(
`/v1/orgs/${encodeURIComponent(orgId)}/api-keys/${encodeURIComponent(apiKey.id)}`,
{ method: "DELETE" },
12000,
);
if (response.status !== 204 && !response.ok) {
throw new Error(getErrorMessage(payload, `Failed to delete API key (${response.status}).`));
}
await loadApiKeys();
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : "Failed to delete API key.");
} finally {
setDeletingId(null);
}
}
async function copyCreatedKey() {
if (!createdKey) {
return;
}
try {
await navigator.clipboard.writeText(createdKey);
setCopied(true);
} catch {
setError("Could not copy the API key. Copy it manually before leaving this page.");
}
}
if (!orgContext) {
return (
<DashboardPageTemplate
icon={KeyRound}
badgeLabel="Admin"
title="API Keys"
description="Create named, rate-limited API keys for your own org membership and revoke any key in the workspace when needed."
colors={["#E6FFFA", "#0F766E", "#14B8A6", "#99F6E4"]}
>
<div className="rounded-[28px] border border-gray-200 bg-white px-6 py-10 text-[15px] text-gray-500">
Loading organization details...
</div>
</DashboardPageTemplate>
);
}
return (
<DashboardPageTemplate
icon={KeyRound}
badgeLabel="Admin"
title="API Keys"
description="Create named, rate-limited API keys for your own org membership and revoke any key in the workspace when needed."
colors={["#E6FFFA", "#0F766E", "#14B8A6", "#99F6E4"]}
>
{!access.canManageApiKeys ? (
<div className="rounded-[28px] border border-amber-200 bg-amber-50 px-6 py-5 text-[14px] text-amber-900">
Only organization owners and admins can view or manage API keys.
</div>
) : (
<>
{error ? (
<div className="mb-6 rounded-[28px] border border-red-200 bg-red-50 px-6 py-4 text-[14px] text-red-700">
{error}
</div>
) : null}
<div className="mb-6 rounded-[30px] border border-gray-200 bg-white p-6 shadow-[0_18px_48px_-34px_rgba(15,23,42,0.22)]">
{createdKey ? (
<div className="rounded-[24px] bg-[#0f172a] p-6 text-white">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-[16px] font-semibold tracking-[-0.03em]">
{createdKeyName ? `${createdKeyName} is ready` : "Your new API key is ready"}
</p>
<p className="mt-1 text-[14px] leading-6 text-slate-300">
Copy it now. After this state closes, only the name and leading characters remain visible in the table.
</p>
</div>
</div>
<div className="mt-5 rounded-[20px] border border-white/10 bg-white/5 p-4">
<code className="block break-all text-[13px] leading-6 text-emerald-200">{createdKey}</code>
</div>
<div className="mt-5 flex flex-wrap justify-end gap-3">
<DenButton variant="secondary" icon={Copy} onClick={() => void copyCreatedKey()}>
{copied ? "Copied" : "Copy key"}
</DenButton>
<DenButton onClick={openCreateForm}>Create another key</DenButton>
</div>
</div>
) : showCreateForm ? (
<form onSubmit={handleCreate}>
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<p className="text-[16px] font-semibold tracking-[-0.03em] text-gray-900">Issue a new key</p>
<p className="mt-1 text-[14px] leading-6 text-gray-500">
Keys are always issued for your own membership in this workspace and inherit the built-in request limit.
</p>
</div>
</div>
<label className="grid gap-3">
<span className="text-[14px] font-medium text-gray-700">Key name</span>
<DenInput
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="CI worker"
required
/>
</label>
<div className="mt-5 flex flex-wrap justify-end gap-3">
<DenButton type="button" variant="secondary" onClick={closeCreateForm}>
Cancel
</DenButton>
<DenButton type="submit" loading={creating}>
Create API key
</DenButton>
</div>
</form>
) : (
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-[16px] font-semibold tracking-[-0.03em] text-gray-900">Create a new API key</p>
<p className="mt-1 text-[14px] leading-6 text-gray-500">
Issue a named, rate-limited key for your own org membership when you need one.
</p>
</div>
<DenButton onClick={openCreateForm}>New key</DenButton>
</div>
)}
</div>
<div className="overflow-hidden rounded-[28px] border border-gray-100 bg-white">
<div className="grid grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)_180px_120px] gap-4 border-b border-gray-100 px-6 py-3 text-[11px] font-medium uppercase tracking-wide text-gray-400">
<span>Key</span>
<span>Owner</span>
<span>Last used</span>
<span />
</div>
{busy ? (
<div className="px-6 py-8 text-center text-[13px] text-gray-400">Loading API keys...</div>
) : apiKeys.length === 0 ? (
<div className="px-6 py-8 text-center text-[13px] text-gray-400">No API keys for this workspace yet.</div>
) : (
apiKeys.map((apiKey) => (
<div
key={apiKey.id}
className="grid grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)_180px_120px] items-center gap-4 border-b border-gray-100 px-6 py-4 transition hover:bg-gray-50/70 last:border-b-0"
>
<div className="min-w-0">
<p className="truncate text-[14px] font-medium text-gray-900">
{apiKey.name ?? apiKey.start ?? "Untitled key"}
</p>
<p className="mt-1 truncate text-[12px] text-gray-400">
{formatKeyPreview(apiKey)} {formatDateTime(apiKey.createdAt)}
</p>
</div>
<div className="min-w-0">
<p className="truncate text-[13px] font-medium text-gray-900">{apiKey.owner.name}</p>
<p className="truncate text-[12px] text-gray-400">{apiKey.owner.email}</p>
</div>
<span className="text-[13px] text-gray-500">{formatDateTime(apiKey.lastRequest)}</span>
<div className="flex justify-end">
<DenButton
variant="destructive"
size="sm"
icon={Trash2}
onClick={() => void handleDelete(apiKey)}
disabled={deletingId === apiKey.id}
>
{deletingId === apiKey.id ? "Deleting..." : "Delete"}
</DenButton>
</div>
</div>
))
)}
</div>
</>
)}
</DashboardPageTemplate>
);
}

View File

@@ -5,14 +5,17 @@ import {
Bot,
CreditCard,
Cpu,
KeyRound,
Monitor,
Share2,
Users,
} from "lucide-react";
import {
getBackgroundAgentsRoute,
getApiKeysRoute,
getBillingRoute,
getCustomLlmProvidersRoute,
getOrgAccessFlags,
getMembersRoute,
getSharedSetupsRoute,
} from "../../../../_lib/den-org";
@@ -48,6 +51,10 @@ export function DashboardOverviewScreen() {
const { orgSlug, activeOrg, orgContext } = useOrgDashboard();
const { user } = useDenFlow();
const { templates } = useOrgTemplates(orgSlug);
const access = getOrgAccessFlags(
orgContext?.currentMember.role ?? "member",
orgContext?.currentMember.isOwner ?? false,
);
const quickActions = [
{
@@ -62,6 +69,14 @@ export function DashboardOverviewScreen() {
href: getMembersRoute(orgSlug),
tint: "bg-cyan-50 text-cyan-600 group-hover:bg-cyan-100",
},
...(access.canManageApiKeys
? [{
label: "API Keys",
icon: KeyRound,
href: getApiKeysRoute(orgSlug),
tint: "bg-emerald-50 text-emerald-600 group-hover:bg-emerald-100",
}]
: []),
{
label: "Shared Workspace",
icon: Bot,

View File

@@ -6,11 +6,11 @@ import { useMemo, useState } from "react";
import {
BookOpen,
Bot,
ChevronDown,
CreditCard,
Cpu,
FileText,
Home,
KeyRound,
LogOut,
MessageSquare,
Share2,
@@ -20,8 +20,10 @@ import { useDenFlow } from "../../../../_providers/den-flow-provider";
import {
formatRoleLabel,
getBackgroundAgentsRoute,
getApiKeysRoute,
getBillingRoute,
getCustomLlmProvidersRoute,
getOrgAccessFlags,
getMembersRoute,
getOrgDashboardRoute,
getSharedSetupsRoute,
@@ -96,6 +98,9 @@ function getDashboardPageTitle(pathname: string, orgSlug: string | null) {
if (pathname.startsWith(getMembersRoute(orgSlug))) {
return "Members";
}
if (pathname.startsWith(getApiKeysRoute(orgSlug))) {
return "API Keys";
}
if (pathname.startsWith(getBackgroundAgentsRoute(orgSlug))) {
return "Shared Workspaces";
}
@@ -118,14 +123,18 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
const {
activeOrg,
orgDirectory,
orgContext,
orgBusy,
orgError,
mutationBusy,
createOrganization,
switchOrganization,
} = useOrgDashboard();
const [switcherOpen, setSwitcherOpen] = useState(false);
const access = getOrgAccessFlags(
orgContext?.currentMember.role ?? "member",
orgContext?.currentMember.isOwner ?? false,
);
const pageTitle = getDashboardPageTitle(pathname, activeOrg?.slug ?? null);
const feedbackHref = buildDenFeedbackUrl({
pathname,
@@ -166,6 +175,13 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
label: "Members",
icon: Users,
},
...(access.canManageApiKeys
? [{
href: activeOrg ? getApiKeysRoute(activeOrg.slug) : "#",
label: "API Keys",
icon: KeyRound,
}]
: []),
{
href: activeOrg ? getBillingRoute(activeOrg.slug) : "/checkout",
label: "Billing",

View File

@@ -0,0 +1,5 @@
import { ApiKeysScreen } from "../_components/api-keys-screen";
export default function ApiKeysPage() {
return <ApiKeysScreen />;
}

View File

@@ -133,6 +133,7 @@ function buildHeaders(request: NextRequest, contentType: string | null): Headers
"cookie",
"user-agent",
"x-requested-with",
"x-api-key",
"origin",
"x-forwarded-for",
];

View File

@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS `apikey` (
`id` varchar(64) NOT NULL,
`config_id` varchar(255) NOT NULL DEFAULT 'default',
`name` varchar(255) DEFAULT NULL,
`start` varchar(32) DEFAULT NULL,
`prefix` varchar(255) DEFAULT NULL,
`key` varchar(255) NOT NULL,
`reference_id` varchar(64) NOT NULL,
`refill_interval` bigint DEFAULT NULL,
`refill_amount` int DEFAULT NULL,
`last_refill_at` timestamp(3) NULL,
`enabled` boolean NOT NULL DEFAULT true,
`rate_limit_enabled` boolean NOT NULL DEFAULT true,
`rate_limit_time_window` bigint DEFAULT NULL,
`rate_limit_max` int DEFAULT NULL,
`request_count` int DEFAULT 0,
`remaining` int DEFAULT NULL,
`last_request` timestamp(3) NULL,
`expires_at` timestamp(3) NULL,
`created_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`permissions` text,
`metadata` text,
CONSTRAINT `apikey_id` PRIMARY KEY (`id`),
KEY `apikey_config_id` (`config_id`),
KEY `apikey_reference_id` (`reference_id`),
KEY `apikey_key` (`key`)
);

View File

@@ -57,6 +57,13 @@
"when": 1775261156284,
"tag": "0008_cynical_boomerang",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1775350000000,
"tag": "0009_api_keys",
"breakpoints": true
}
]
}
}

View File

@@ -1,5 +1,5 @@
import { sql } from "drizzle-orm"
import { boolean, index, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { bigint, boolean, index, int, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { denTypeIdColumn } from "../columns"
export const AuthUserTable = mysqlTable(
@@ -74,7 +74,43 @@ export const AuthVerificationTable = mysqlTable(
(table) => [index("verification_identifier").on(table.identifier)],
)
export const AuthApiKeyTable = mysqlTable(
"apikey",
{
id: varchar("id", { length: 64 }).notNull().primaryKey(),
configId: varchar("config_id", { length: 255 }).notNull().default("default"),
name: varchar("name", { length: 255 }),
start: varchar("start", { length: 32 }),
prefix: varchar("prefix", { length: 255 }),
key: varchar("key", { length: 255 }).notNull(),
referenceId: varchar("reference_id", { length: 64 }).notNull(),
refillInterval: bigint("refill_interval", { mode: "number" }),
refillAmount: int("refill_amount"),
lastRefillAt: timestamp("last_refill_at", { fsp: 3 }),
enabled: boolean("enabled").notNull().default(true),
rateLimitEnabled: boolean("rate_limit_enabled").notNull().default(true),
rateLimitTimeWindow: bigint("rate_limit_time_window", { mode: "number" }),
rateLimitMax: int("rate_limit_max"),
requestCount: int("request_count").default(0),
remaining: int("remaining"),
lastRequest: timestamp("last_request", { fsp: 3 }),
expiresAt: timestamp("expires_at", { fsp: 3 }),
createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.notNull()
.default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`),
permissions: text("permissions"),
metadata: text("metadata"),
},
(table) => [
index("apikey_config_id").on(table.configId),
index("apikey_reference_id").on(table.referenceId),
index("apikey_key").on(table.key),
],
)
export const user = AuthUserTable
export const session = AuthSessionTable
export const account = AuthAccountTable
export const verification = AuthVerificationTable
export const apikey = AuthApiKeyTable

View File

@@ -6,6 +6,7 @@ export const denTypeIdPrefixes = {
session: "ses",
account: "acc",
verification: "ver",
apiKey: "apk",
rateLimit: "rli",
org: "org",
organization: "org",

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

214
pnpm-lock.yaml generated
View File

@@ -425,6 +425,9 @@ importers:
ee/apps/den-api:
dependencies:
'@better-auth/api-key':
specifier: ^1.5.6
version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10))
'@daytonaio/sdk':
specifier: ^0.150.0
version: 0.150.0(ws@8.19.0)
@@ -441,11 +444,11 @@ importers:
specifier: workspace:*
version: link:../../packages/utils
better-auth:
specifier: ^1.4.18
version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)
specifier: ^1.5.6
version: 1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)
better-call:
specifier: ^1.1.8
version: 1.1.8(zod@4.3.6)
specifier: ^1.3.2
version: 1.3.2(zod@4.3.6)
dotenv:
specifier: ^16.4.5
version: 16.6.1
@@ -602,7 +605,7 @@ importers:
version: 1.19.0
drizzle-orm:
specifier: ^0.45.1
version: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4)
version: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4)
mysql2:
specifier: ^3.11.3
version: 3.17.4
@@ -1038,23 +1041,84 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@better-auth/core@1.4.18':
resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==}
'@better-auth/api-key@1.5.6':
resolution: {integrity: sha512-jr3m4/caFxn9BuY9pGDJ4B1HP1Qoqmyd7heBHm4KUFel+a9Whe/euROgZ/L+o7mbmUdZtreneaU15dpn0tJZ5g==}
peerDependencies:
'@better-auth/utils': 0.3.0
'@better-auth/core': 1.5.6
'@better-auth/utils': 0.3.1
better-auth: 1.5.6
'@better-auth/core@1.5.6':
resolution: {integrity: sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==}
peerDependencies:
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
better-call: 1.1.8
'@cloudflare/workers-types': '>=4'
'@opentelemetry/api': ^1.9.0
better-call: 1.3.2
jose: ^6.1.0
kysely: ^0.28.5
nanostores: ^1.0.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
'@better-auth/telemetry@1.4.18':
resolution: {integrity: sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ==}
'@better-auth/drizzle-adapter@1.5.6':
resolution: {integrity: sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==}
peerDependencies:
'@better-auth/core': 1.4.18
'@better-auth/core': 1.5.6
'@better-auth/utils': ^0.3.0
drizzle-orm: '>=0.41.0'
peerDependenciesMeta:
drizzle-orm:
optional: true
'@better-auth/utils@0.3.0':
resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==}
'@better-auth/kysely-adapter@1.5.6':
resolution: {integrity: sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==}
peerDependencies:
'@better-auth/core': 1.5.6
'@better-auth/utils': ^0.3.0
kysely: ^0.27.0 || ^0.28.0
peerDependenciesMeta:
kysely:
optional: true
'@better-auth/memory-adapter@1.5.6':
resolution: {integrity: sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==}
peerDependencies:
'@better-auth/core': 1.5.6
'@better-auth/utils': ^0.3.0
'@better-auth/mongo-adapter@1.5.6':
resolution: {integrity: sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==}
peerDependencies:
'@better-auth/core': 1.5.6
'@better-auth/utils': ^0.3.0
mongodb: ^6.0.0 || ^7.0.0
peerDependenciesMeta:
mongodb:
optional: true
'@better-auth/prisma-adapter@1.5.6':
resolution: {integrity: sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==}
peerDependencies:
'@better-auth/core': 1.5.6
'@better-auth/utils': ^0.3.0
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
prisma: ^5.0.0 || ^6.0.0 || ^7.0.0
peerDependenciesMeta:
'@prisma/client':
optional: true
prisma:
optional: true
'@better-auth/telemetry@1.5.6':
resolution: {integrity: sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==}
peerDependencies:
'@better-auth/core': 1.5.6
'@better-auth/utils@0.3.1':
resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==}
'@better-fetch/fetch@1.1.21':
resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==}
@@ -3444,8 +3508,8 @@ packages:
resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
hasBin: true
better-auth@1.4.18:
resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==}
better-auth@1.5.6:
resolution: {integrity: sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==}
peerDependencies:
'@lynx-js/react': '*'
'@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -3506,8 +3570,8 @@ packages:
vue:
optional: true
better-call@1.1.8:
resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==}
better-call@1.3.2:
resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==}
peerDependencies:
zod: ^4.0.0
peerDependenciesMeta:
@@ -4466,8 +4530,8 @@ packages:
khroma@2.1.0:
resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
kysely@0.28.11:
resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==}
kysely@0.28.15:
resolution: {integrity: sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==}
engines: {node: '>=20.0.0'}
langium@4.2.1:
@@ -4836,8 +4900,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanostores@1.1.0:
resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==}
nanostores@1.2.0:
resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==}
engines: {node: ^20.0.0 || >=22.0.0}
next@14.2.35:
@@ -5345,8 +5409,8 @@ packages:
resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==}
engines: {node: '>=10'}
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
set-cookie-parser@3.1.0:
resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
@@ -6694,24 +6758,62 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)':
'@better-auth/api-key@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10))':
dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.21
'@standard-schema/spec': 1.1.0
better-call: 1.1.8(zod@4.3.6)
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.0
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
better-auth: 1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10)
zod: 4.3.6
'@better-auth/telemetry@1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))':
'@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)':
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/utils': 0.3.0
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.40.0
'@standard-schema/spec': 1.1.0
better-call: 1.3.2(zod@4.3.6)
jose: 6.1.3
kysely: 0.28.15
nanostores: 1.2.0
zod: 4.3.6
'@better-auth/drizzle-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
optionalDependencies:
drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4)
'@better-auth/kysely-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.15)':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
optionalDependencies:
kysely: 0.28.15
'@better-auth/memory-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
'@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
'@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
'@better-auth/telemetry@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))':
dependencies:
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@better-auth/utils@0.3.0': {}
'@better-auth/utils@0.3.1': {}
'@better-fetch/fetch@1.1.21': {}
@@ -9145,35 +9247,43 @@ snapshots:
baseline-browser-mapping@2.9.14: {}
better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10):
better-auth@1.5.6(@opentelemetry/api@1.9.0)(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))(mysql2@3.17.4)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.10):
dependencies:
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
'@better-auth/utils': 0.3.0
'@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0)
'@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4))
'@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.15)
'@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
'@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
'@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)
'@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.1.8(zod@4.3.6)
better-call: 1.3.2(zod@4.3.6)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.0
kysely: 0.28.15
nanostores: 1.2.0
zod: 4.3.6
optionalDependencies:
drizzle-kit: 0.31.9
drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4)
drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4)
mysql2: 3.17.4
next: 16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
solid-js: 1.9.10
transitivePeerDependencies:
- '@cloudflare/workers-types'
- '@opentelemetry/api'
better-call@1.1.8(zod@4.3.6):
better-call@1.3.2(zod@4.3.6):
dependencies:
'@better-auth/utils': 0.3.0
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
rou3: 0.7.12
set-cookie-parser: 2.7.2
set-cookie-parser: 3.1.0
optionalDependencies:
zod: 4.3.6
@@ -9600,12 +9710,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.11)(mysql2@3.17.4):
drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.6)(kysely@0.28.15)(mysql2@3.17.4):
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@planetscale/database': 1.19.0
bun-types: 1.3.6
kysely: 0.28.11
kysely: 0.28.15
mysql2: 3.17.4
dunder-proto@1.0.1:
@@ -10137,7 +10247,7 @@ snapshots:
khroma@2.1.0: {}
kysely@0.28.11: {}
kysely@0.28.15: {}
langium@4.2.1:
dependencies:
@@ -10702,7 +10812,7 @@ snapshots:
nanoid@3.3.11: {}
nanostores@1.1.0: {}
nanostores@1.2.0: {}
next@14.2.35(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
@@ -11243,7 +11353,7 @@ snapshots:
seroval@1.3.2: {}
set-cookie-parser@2.7.2: {}
set-cookie-parser@3.1.0: {}
sharp@0.34.5:
dependencies: