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,135 @@
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp with time zone,
"refresh_token_expires_at" timestamp with time zone,
"scope" text,
"password" text,
"created_at" timestamp with time zone NOT NULL,
"updated_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"token" text NOT NULL,
"created_at" timestamp with time zone NOT NULL,
"updated_at" timestamp with time zone NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp with time zone NOT NULL,
"updated_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone,
"updated_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "company_memberships" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"principal_type" text NOT NULL,
"principal_id" text NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"membership_role" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "instance_user_roles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text NOT NULL,
"role" text DEFAULT 'instance_admin' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "invites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid,
"invite_type" text DEFAULT 'company_join' NOT NULL,
"token_hash" text NOT NULL,
"allowed_join_types" text DEFAULT 'both' NOT NULL,
"defaults_payload" jsonb,
"expires_at" timestamp with time zone NOT NULL,
"invited_by_user_id" text,
"revoked_at" timestamp with time zone,
"accepted_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "join_requests" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"invite_id" uuid NOT NULL,
"company_id" uuid NOT NULL,
"request_type" text NOT NULL,
"status" text DEFAULT 'pending_approval' NOT NULL,
"request_ip" text NOT NULL,
"requesting_user_id" text,
"request_email_snapshot" text,
"agent_name" text,
"adapter_type" text,
"capabilities" text,
"agent_defaults_payload" jsonb,
"created_agent_id" uuid,
"approved_by_user_id" text,
"approved_at" timestamp with time zone,
"rejected_by_user_id" text,
"rejected_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "principal_permission_grants" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"principal_type" text NOT NULL,
"principal_id" text NOT NULL,
"permission_key" text NOT NULL,
"scope" jsonb,
"granted_by_user_id" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN "assignee_user_id" text;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "company_memberships" ADD CONSTRAINT "company_memberships_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invites" ADD CONSTRAINT "invites_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "join_requests" ADD CONSTRAINT "join_requests_invite_id_invites_id_fk" FOREIGN KEY ("invite_id") REFERENCES "public"."invites"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "join_requests" ADD CONSTRAINT "join_requests_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "join_requests" ADD CONSTRAINT "join_requests_created_agent_id_agents_id_fk" FOREIGN KEY ("created_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "principal_permission_grants" ADD CONSTRAINT "principal_permission_grants_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "company_memberships_company_principal_unique_idx" ON "company_memberships" USING btree ("company_id","principal_type","principal_id");--> statement-breakpoint
CREATE INDEX "company_memberships_principal_status_idx" ON "company_memberships" USING btree ("principal_type","principal_id","status");--> statement-breakpoint
CREATE INDEX "company_memberships_company_status_idx" ON "company_memberships" USING btree ("company_id","status");--> statement-breakpoint
CREATE UNIQUE INDEX "instance_user_roles_user_role_unique_idx" ON "instance_user_roles" USING btree ("user_id","role");--> statement-breakpoint
CREATE INDEX "instance_user_roles_role_idx" ON "instance_user_roles" USING btree ("role");--> statement-breakpoint
CREATE UNIQUE INDEX "invites_token_hash_unique_idx" ON "invites" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "invites_company_invite_state_idx" ON "invites" USING btree ("company_id","invite_type","revoked_at","expires_at");--> statement-breakpoint
CREATE UNIQUE INDEX "join_requests_invite_unique_idx" ON "join_requests" USING btree ("invite_id");--> statement-breakpoint
CREATE INDEX "join_requests_company_status_type_created_idx" ON "join_requests" USING btree ("company_id","status","request_type","created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "principal_permission_grants_unique_idx" ON "principal_permission_grants" USING btree ("company_id","principal_type","principal_id","permission_key");--> statement-breakpoint
CREATE INDEX "principal_permission_grants_company_permission_idx" ON "principal_permission_grants" USING btree ("company_id","permission_key");--> statement-breakpoint
CREATE INDEX "issues_company_assignee_user_status_idx" ON "issues" USING btree ("company_id","assignee_user_id","status");

File diff suppressed because it is too large Load Diff

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,
),
}),
);