feat: add auth/access foundation - deps, DB schema, shared types, and config

Add Better Auth, drizzle-orm, @dnd-kit, and remark-gfm dependencies.
Introduce DB schema for auth tables (user, session, account, verification),
company memberships, instance user roles, permission grants, invites, and
join requests. Add assigneeUserId to issues. Extend shared config schema
with deployment mode/exposure/auth settings, add access types and validators,
and wire up new API path constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-23 14:40:16 -06:00
parent 08faeb53b5
commit 60d6122271
23 changed files with 6143 additions and 30 deletions

View File

@@ -0,0 +1,47 @@
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const authUsers = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
});
export const authSessions = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
token: text("token").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
});
export const authAccounts = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true }),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { withTimezone: true }),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
});
export const authVerifications = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }),
updatedAt: timestamp("updated_at", { withTimezone: true }),
});

View File

@@ -0,0 +1,29 @@
import { pgTable, uuid, text, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
export const companyMemberships = pgTable(
"company_memberships",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
principalType: text("principal_type").notNull(),
principalId: text("principal_id").notNull(),
status: text("status").notNull().default("active"),
membershipRole: text("membership_role"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyPrincipalUniqueIdx: uniqueIndex("company_memberships_company_principal_unique_idx").on(
table.companyId,
table.principalType,
table.principalId,
),
principalStatusIdx: index("company_memberships_principal_status_idx").on(
table.principalType,
table.principalId,
table.status,
),
companyStatusIdx: index("company_memberships_company_status_idx").on(table.companyId, table.status),
}),
);

View File

@@ -1,5 +1,11 @@
export { companies } from "./companies.js";
export { authUsers, authSessions, authAccounts, authVerifications } from "./auth.js";
export { instanceUserRoles } from "./instance_user_roles.js";
export { agents } from "./agents.js";
export { companyMemberships } from "./company_memberships.js";
export { principalPermissionGrants } from "./principal_permission_grants.js";
export { invites } from "./invites.js";
export { joinRequests } from "./join_requests.js";
export { agentConfigRevisions } from "./agent_config_revisions.js";
export { agentApiKeys } from "./agent_api_keys.js";
export { agentRuntimeState } from "./agent_runtime_state.js";

View File

@@ -0,0 +1,16 @@
import { pgTable, uuid, text, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";
export const instanceUserRoles = pgTable(
"instance_user_roles",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: text("user_id").notNull(),
role: text("role").notNull().default("instance_admin"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
userRoleUniqueIdx: uniqueIndex("instance_user_roles_user_role_unique_idx").on(table.userId, table.role),
roleIdx: index("instance_user_roles_role_idx").on(table.role),
}),
);

View File

@@ -0,0 +1,29 @@
import { pgTable, uuid, text, timestamp, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
export const invites = pgTable(
"invites",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").references(() => companies.id),
inviteType: text("invite_type").notNull().default("company_join"),
tokenHash: text("token_hash").notNull(),
allowedJoinTypes: text("allowed_join_types").notNull().default("both"),
defaultsPayload: jsonb("defaults_payload").$type<Record<string, unknown> | null>(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
invitedByUserId: text("invited_by_user_id"),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
acceptedAt: timestamp("accepted_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
tokenHashUniqueIdx: uniqueIndex("invites_token_hash_unique_idx").on(table.tokenHash),
companyInviteStateIdx: index("invites_company_invite_state_idx").on(
table.companyId,
table.inviteType,
table.revokedAt,
table.expiresAt,
),
}),
);

View File

@@ -27,6 +27,7 @@ export const issues = pgTable(
status: text("status").notNull().default("backlog"),
priority: text("priority").notNull().default("medium"),
assigneeAgentId: uuid("assignee_agent_id").references(() => agents.id),
assigneeUserId: text("assignee_user_id"),
checkoutRunId: uuid("checkout_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
executionRunId: uuid("execution_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
executionAgentNameKey: text("execution_agent_name_key"),
@@ -51,6 +52,11 @@ export const issues = pgTable(
table.assigneeAgentId,
table.status,
),
assigneeUserStatusIdx: index("issues_company_assignee_user_status_idx").on(
table.companyId,
table.assigneeUserId,
table.status,
),
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
identifierIdx: uniqueIndex("issues_company_identifier_idx").on(table.companyId, table.identifier),

View File

@@ -0,0 +1,38 @@
import { pgTable, uuid, text, timestamp, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { invites } from "./invites.js";
import { agents } from "./agents.js";
export const joinRequests = pgTable(
"join_requests",
{
id: uuid("id").primaryKey().defaultRandom(),
inviteId: uuid("invite_id").notNull().references(() => invites.id),
companyId: uuid("company_id").notNull().references(() => companies.id),
requestType: text("request_type").notNull(),
status: text("status").notNull().default("pending_approval"),
requestIp: text("request_ip").notNull(),
requestingUserId: text("requesting_user_id"),
requestEmailSnapshot: text("request_email_snapshot"),
agentName: text("agent_name"),
adapterType: text("adapter_type"),
capabilities: text("capabilities"),
agentDefaultsPayload: jsonb("agent_defaults_payload").$type<Record<string, unknown> | null>(),
createdAgentId: uuid("created_agent_id").references(() => agents.id),
approvedByUserId: text("approved_by_user_id"),
approvedAt: timestamp("approved_at", { withTimezone: true }),
rejectedByUserId: text("rejected_by_user_id"),
rejectedAt: timestamp("rejected_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
inviteUniqueIdx: uniqueIndex("join_requests_invite_unique_idx").on(table.inviteId),
companyStatusTypeCreatedIdx: index("join_requests_company_status_type_created_idx").on(
table.companyId,
table.status,
table.requestType,
table.createdAt,
),
}),
);

View File

@@ -0,0 +1,29 @@
import { pgTable, uuid, text, timestamp, jsonb, uniqueIndex, index } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
export const principalPermissionGrants = pgTable(
"principal_permission_grants",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
principalType: text("principal_type").notNull(),
principalId: text("principal_id").notNull(),
permissionKey: text("permission_key").notNull(),
scope: jsonb("scope").$type<Record<string, unknown> | null>(),
grantedByUserId: text("granted_by_user_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
uniqueGrantIdx: uniqueIndex("principal_permission_grants_unique_idx").on(
table.companyId,
table.principalType,
table.principalId,
table.permissionKey,
),
companyPermissionIdx: index("principal_permission_grants_company_permission_idx").on(
table.companyId,
table.permissionKey,
),
}),
);