@tangle-network/agent-app 0.60.180 → 0.60.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant/index.js +12 -12
- package/dist/chat-routes/index.js +23 -23
- package/dist/chat-store/index.js +4 -4
- package/dist/{chunk-6ZUVYAKB.js → chunk-6O2NQ7WD.js} +9 -9
- package/dist/{chunk-IIE3K2Y5.js → chunk-EOPFHXEA.js} +1 -1
- package/dist/{chunk-PYPWEXTD.js → chunk-J26IXWFK.js} +12 -12
- package/dist/chunk-LTWJYNEC.js +100 -0
- package/dist/chunk-LTWJYNEC.js.map +1 -0
- package/dist/{chunk-YR6TZ777.js → chunk-TNWDYAFX.js} +6 -6
- package/dist/{chunk-T5PPQPOD.js → chunk-TXUAGSFR.js} +1 -1
- package/dist/{chunk-T5PPQPOD.js.map → chunk-TXUAGSFR.js.map} +1 -1
- package/dist/{chunk-N3CYCSKK.js → chunk-ZBB5KQ6B.js} +1 -1
- package/dist/chunk-ZBB5KQ6B.js.map +1 -0
- package/dist/{chunk-XNKLQG26.js → chunk-ZGUXI5SI.js} +5 -5
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/model-resolution/allow-list.d.ts +64 -0
- package/dist/model-resolution/index.d.ts +1 -0
- package/dist/model-resolution/index.js +28 -0
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/profile/index.js +2 -1
- package/dist/profile/index.js.map +1 -1
- package/dist/public-consultation/index.d.ts +1 -1
- package/dist/public-consultation/index.js +8 -8
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +26 -2
- package/dist/skills/marketplace-drizzle.d.ts +121 -0
- package/dist/skills/marketplace-drizzle.js +34 -0
- package/dist/skills/marketplace-drizzle.js.map +1 -0
- package/dist/skills/marketplace.d.ts +104 -0
- package/dist/skills-placement/index.js +2 -1
- package/dist/skills-placement/index.js.map +1 -1
- package/dist/spend/delegate-budget.d.ts +59 -0
- package/dist/spend/index.d.ts +1 -0
- package/dist/spend/index.js +43 -0
- package/dist/spend/index.js.map +1 -1
- package/dist/stream/index.js +2 -2
- package/dist/teams/drizzle/access.d.ts +4 -0
- package/dist/teams/drizzle.js.map +1 -1
- package/dist/teams/invitations-api.d.ts +6 -3
- package/dist/teams/invitations-api.js +72 -39
- package/dist/teams/invitations-api.js.map +1 -1
- package/dist/teams/members-api.d.ts +2 -2
- package/dist/teams/members-api.js +1 -1
- package/dist/toolbelt/index.d.ts +80 -0
- package/dist/toolbelt/index.js +178 -0
- package/dist/toolbelt/index.js.map +1 -0
- package/dist/web-react/index.js +25 -25
- package/package.json +11 -1
- package/dist/chunk-N3CYCSKK.js.map +0 -1
- /package/dist/{chunk-6ZUVYAKB.js.map → chunk-6O2NQ7WD.js.map} +0 -0
- /package/dist/{chunk-IIE3K2Y5.js.map → chunk-EOPFHXEA.js.map} +0 -0
- /package/dist/{chunk-PYPWEXTD.js.map → chunk-J26IXWFK.js.map} +0 -0
- /package/dist/{chunk-YR6TZ777.js.map → chunk-TNWDYAFX.js.map} +0 -0
- /package/dist/{chunk-XNKLQG26.js.map → chunk-ZGUXI5SI.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/teams/drizzle/schema.ts","../../src/teams/drizzle/invitations-schema.ts","../../src/teams/drizzle/access.ts","../../src/teams/drizzle/personal-organization.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the teams tables. The product owns the user and\n * workspace tables; this factory creates the tenancy/membership tables and\n * wires their foreign keys into the passed-in tables so the whole graph lives\n * in one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror gtm's hand-rolled tables so a product\n * with those tables can adopt the factory without rewriting rows.\n *\n * The three tables and their roles:\n * - `organization` — the TENANT/ownership primitive. `kind` is 'personal'\n * (one auto-created per user; see ensurePersonalOrganization) or 'team'.\n * This is what owns workspaces and what billing/seats attach to.\n * - `organizationMember` — who belongs to an org and at what org role.\n * - `workspaceMember` — the additive invite/member surface: per-workspace\n * access grants, including pending email-only invites carrying a token.\n *\n * Imports `drizzle-orm` at module top — that is WHY this lives behind the\n * `/teams/drizzle` sub-subpath. The pure `./teams` leaf imports none of this,\n * so a consumer that never touches the DB never pulls the optional peer.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable } from 'drizzle-orm/sqlite-core'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type TeamParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options specifying user and workspace tables for creating team-related tables */\nexport interface CreateTeamTablesOptions {\n /** The product's user table — org/member rows reference `userTable.id`. */\n userTable: TeamParentTable\n /** The product's workspace table — workspace members reference `workspaceTable.id`. */\n workspaceTable: TeamParentTable\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\n/** Build SQLite tables for organizations and related team structures using provided options */\nexport function createTeamTables(opts: CreateTeamTablesOptions) {\n const { userTable, workspaceTable } = opts\n\n const organizations = sqliteTable('organization', {\n id: hexId(),\n name: text('name').notNull(),\n slug: text('slug').notNull().unique(),\n kind: text('kind', { enum: ['personal', 'team'] }).notNull().default('personal'),\n createdBy: text('created_by').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n index('idx_organization_created_by').on(table.createdBy),\n ])\n\n const organizationMembers = sqliteTable('organization_member', {\n id: hexId(),\n organizationId: text('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),\n userId: text('user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['owner', 'admin', 'member', 'billing'] }).notNull().default('member'),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_org_member_user').on(table.organizationId, table.userId),\n index('idx_org_member_user').on(table.userId),\n ])\n\n const workspaceMembers = sqliteTable('workspace_member', {\n id: hexId(),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n organizationMemberId: text('organization_member_id').references(() => organizationMembers.id, { onDelete: 'cascade' }),\n userId: text('user_id').references(() => userTable.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['owner', 'admin', 'editor', 'viewer'] }).notNull().default('editor'),\n invitedBy: text('invited_by'),\n inviteEmail: text('invite_email'),\n inviteToken: text('invite_token'),\n invitedAt: integer('invited_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),\n acceptedAt: integer('accepted_at', { mode: 'timestamp' }),\n }, (table) => [\n uniqueIndex('uniq_workspace_member_org_member').on(table.workspaceId, table.organizationMemberId),\n index('idx_workspace_member_user').on(table.workspaceId, table.userId),\n index('idx_member_user').on(table.userId),\n uniqueIndex('idx_member_invite_token').on(table.inviteToken),\n ])\n\n return { organizations, organizationMembers, workspaceMembers }\n}\n\n/** Resolve team tables by deriving the return type of createTeamTables */\nexport type TeamTables = ReturnType<typeof createTeamTables>\n\n/** Resolve the structure of an organization row from the organizations table in TeamTables */\nexport type OrganizationRow = TeamTables['organizations']['$inferSelect']\n/** Resolve the structure of an organization member row from the team tables selection */\nexport type OrganizationMemberRow = TeamTables['organizationMembers']['$inferSelect']\n/** Resolve a workspace member row with selected fields from the workspaceMembers table */\nexport type WorkspaceMemberRow = TeamTables['workspaceMembers']['$inferSelect']\n","/**\n * Drizzle schema factory for the dedicated workspace-invitation table. Separate\n * from `createTeamTables` on purpose: an app opts into the rich email-invitation\n * lifecycle (status / expiry / resend / revoke / preview) by calling this, and an\n * app that doesn't want it never grows the table — `createTeamTables`'s output and\n * migrations stay untouched.\n *\n * The product owns the user and workspace tables; `organizations` comes from\n * `createTeamTables`. Pass all three so the invitation's FKs wire into the same\n * drizzle schema with real cascade semantics. Call this AFTER `createTeamTables`\n * (so `organizations` exists) — the lazy `.references(() => ...)` closures make\n * the ordering within one module safe.\n *\n * Columns / enums / indexes mirror creative-agent's validated hand-rolled table,\n * so an app already running that table adopts this factory with no row rewrite.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { TeamParentTable } from './schema'\n\n/** Define options for creating a workspace invitation table with user, workspace, and organization references */\nexport interface CreateWorkspaceInvitationTableOptions {\n /** The product's user table — `invitedByUserId` references `userTable.id`. */\n userTable: TeamParentTable\n /** The product's workspace table — `workspaceId` references `workspaceTable.id`. */\n workspaceTable: TeamParentTable\n /** The `organizations` table from `createTeamTables` — `organizationId` references it. */\n organizationTable: TeamParentTable\n}\n\n/** Build a workspace invitation table with defined columns and foreign key constraints */\nexport function createWorkspaceInvitationTable(opts: CreateWorkspaceInvitationTableOptions) {\n const { userTable, workspaceTable, organizationTable } = opts\n\n const workspaceInvitations = sqliteTable('workspace_invitation', {\n id: text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n organizationId: text('organization_id').notNull().references(() => organizationTable.id, { onDelete: 'cascade' }),\n email: text('email').notNull(),\n invitedByUserId: text('invited_by_user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n permissions: text('permissions', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'),\n token: text('token').notNull().unique(),\n status: text('status', { enum: ['pending', 'accepted', 'expired', 'revoked'] }).notNull().default('pending'),\n emailStatus: text('email_status', { enum: ['not_sent', 'sent', 'failed'] }).notNull().default('not_sent'),\n expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),\n acceptedAt: integer('accepted_at', { mode: 'timestamp' }),\n revokedAt: integer('revoked_at', { mode: 'timestamp' }),\n lastSentAt: integer('last_sent_at', { mode: 'timestamp' }),\n }, (table) => [\n uniqueIndex('uniq_workspace_invitation_token').on(table.token),\n index('idx_workspace_invitation_workspace').on(table.workspaceId, table.createdAt),\n index('idx_workspace_invitation_email').on(table.email),\n index('idx_workspace_invitation_workspace_email_status').on(table.workspaceId, table.email, table.status),\n ])\n\n return { workspaceInvitations }\n}\n\n/** Resolve the structure of workspace invitation tables from the creation function */\nexport type WorkspaceInvitationTables = ReturnType<typeof createWorkspaceInvitationTable>\n/** Resolve the structure of a workspace invitation row from the workspaceInvitations table */\nexport type WorkspaceInvitationRow = WorkspaceInvitationTables['workspaceInvitations']['$inferSelect']\n","/**\n * RBAC access builders over the teams tables. `createWorkspaceAccess` and\n * `createOrganizationAccess` close over the product's `db`, the tables from\n * `createTeamTables`, and the product's `workspace` table, returning the exact\n * `getWorkspaceAccess` / `requireWorkspaceAccess` / `listUserWorkspaces` /\n * `getOrganizationAccess` / `requireOrganizationAccess` functions a consumer\n * already calls — so adoption is a one-line import swap, every call site stays\n * identical.\n *\n * Defense in depth: org owners/admins are workspace owners across the org;\n * everyone else gets their explicit per-workspace role. Effective role is the\n * fold in `resolveWorkspaceRole` (pure, from `../roles`). Every query pins\n * `userId`, so a leaked id can never read across the tenancy boundary — it\n * surfaces as \"not found\".\n *\n * Driver-agnostic: builders are awaited, never `.run()`/`.all()`, so\n * better-sqlite3, D1, and libsql handles all behave identically.\n */\n\nimport { and, desc, eq, isNotNull } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport {\n type OrganizationRole,\n type WorkspaceRole,\n hasOrganizationRole,\n hasWorkspaceRole,\n resolveWorkspaceRole,\n} from '../roles'\nimport type { TeamParentTable } from './schema'\nimport type { OrganizationMemberRow, OrganizationRow, TeamTables } from './schema'\n\n/** Any SQLite drizzle database — `any` erases driver-specific generics so\n * better-sqlite3, D1, and libsql handles all fit. */\nexport type TeamDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any>\n\n/**\n * The product's workspace table, narrowed to the columns the access joins read.\n * Adopters pass their real drizzle workspace table — it carries these columns\n * (gtm's does); the type only asserts the minimum the joins touch.\n */\nexport interface WorkspaceAccessTable {\n id: any\n organizationId: any\n name: any\n updatedAt: any\n}\n\n/** Define options required to create access with database, tables, and workspace table references */\nexport interface CreateAccessOptions {\n db: TeamDatabase\n tables: TeamTables\n /** The product's workspace table (the FK target passed to createTeamTables). */\n workspaceTable: TeamParentTable & WorkspaceAccessTable\n}\n\n/** Define access details including workspace data, organization info, member info, and role within workspace */\nexport interface WorkspaceAccess {\n workspace: Record<string, unknown>\n organization: OrganizationRow\n organizationMember: OrganizationMemberRow\n role: WorkspaceRole\n}\n\n/** Describe a user's workspace details including organization and role information */\nexport interface UserWorkspaceSummary extends Record<string, unknown> {\n organizationId: string\n organizationName: string\n role: WorkspaceRole\n}\n\n/** Define methods to retrieve and enforce user access permissions within workspaces */\nexport interface WorkspaceAccessApi {\n getWorkspaceAccess(workspaceId: string, userId: string, minRole?: WorkspaceRole): Promise<WorkspaceAccess | null>\n requireWorkspaceAccess(workspaceId: string, userId: string, minRole?: WorkspaceRole): Promise<WorkspaceAccess>\n listUserWorkspaces(userId: string): Promise<UserWorkspaceSummary[]>\n}\n\n/** Create workspace access API to manage user roles and permissions within a workspace */\nexport function createWorkspaceAccess(opts: CreateAccessOptions): WorkspaceAccessApi {\n const { db, tables, workspaceTable } = opts\n const { organizations, organizationMembers, workspaceMembers } = tables\n const workspaces = workspaceTable\n\n async function getWorkspaceAccess(\n workspaceId: string,\n userId: string,\n minRole: WorkspaceRole = 'viewer',\n ): Promise<WorkspaceAccess | null> {\n const [row] = await db\n .select({\n workspace: workspaces,\n organization: organizations,\n organizationMember: organizationMembers,\n projectRole: workspaceMembers.role,\n acceptedAt: workspaceMembers.acceptedAt,\n })\n .from(workspaces)\n .innerJoin(organizations, eq(organizations.id, workspaces.organizationId))\n .innerJoin(organizationMembers, and(\n eq(organizationMembers.organizationId, workspaces.organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .leftJoin(workspaceMembers, and(\n eq(workspaceMembers.workspaceId, workspaces.id),\n eq(workspaceMembers.organizationMemberId, organizationMembers.id),\n isNotNull(workspaceMembers.acceptedAt),\n ))\n .where(eq(workspaces.id, workspaceId))\n .limit(1)\n\n if (!row) return null\n\n const role = resolveWorkspaceRole(row.organizationMember.role, row.projectRole as WorkspaceRole | null)\n if (!role) return null\n if (!hasWorkspaceRole(role, minRole)) return null\n\n return {\n workspace: row.workspace as Record<string, unknown>,\n organization: row.organization,\n organizationMember: row.organizationMember,\n role,\n }\n }\n\n async function requireWorkspaceAccess(\n workspaceId: string,\n userId: string,\n minRole: WorkspaceRole = 'viewer',\n ): Promise<WorkspaceAccess> {\n const access = await getWorkspaceAccess(workspaceId, userId, minRole)\n if (!access) throw new Response('Workspace not found', { status: 404 })\n return access\n }\n\n async function listUserWorkspaces(userId: string): Promise<UserWorkspaceSummary[]> {\n const rows = await db\n .select({\n workspace: workspaces,\n organization: organizations,\n organizationMember: organizationMembers,\n projectRole: workspaceMembers.role,\n })\n .from(workspaces)\n .innerJoin(organizations, eq(organizations.id, workspaces.organizationId))\n .innerJoin(organizationMembers, and(\n eq(organizationMembers.organizationId, workspaces.organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .leftJoin(workspaceMembers, and(\n eq(workspaceMembers.workspaceId, workspaces.id),\n eq(workspaceMembers.organizationMemberId, organizationMembers.id),\n isNotNull(workspaceMembers.acceptedAt),\n ))\n // Most recently active workspace first, so callers taking rows[0] as the\n // default pick the freshest one.\n .orderBy(desc(workspaces.updatedAt))\n\n return rows.flatMap((row: any) => {\n const role = resolveWorkspaceRole(row.organizationMember.role, row.projectRole as WorkspaceRole | null)\n if (!role) return []\n return [{\n ...(row.workspace as Record<string, unknown>),\n organizationId: row.organization.id,\n organizationName: row.organization.name,\n role,\n } as UserWorkspaceSummary]\n })\n }\n\n return { getWorkspaceAccess, requireWorkspaceAccess, listUserWorkspaces }\n}\n\n/** Define access details linking an organization, its member, and the member's role */\nexport interface OrganizationAccess {\n organization: OrganizationRow\n member: OrganizationMemberRow\n role: OrganizationRole\n}\n\n/** Define methods to retrieve and enforce user access levels within an organization */\nexport interface OrganizationAccessApi {\n getOrganizationAccess(organizationId: string, userId: string, minRole?: OrganizationRole): Promise<OrganizationAccess | null>\n requireOrganizationAccess(organizationId: string, userId: string, minRole?: OrganizationRole): Promise<OrganizationAccess>\n}\n\n/** Define options required to create access for an organization including database and tables */\nexport interface CreateOrganizationAccessOptions {\n db: TeamDatabase\n tables: TeamTables\n}\n\n/** Resolve organization access API with specified database and table options */\nexport function createOrganizationAccess(opts: CreateOrganizationAccessOptions): OrganizationAccessApi {\n const { db, tables } = opts\n const { organizations, organizationMembers } = tables\n\n async function getOrganizationAccess(\n organizationId: string,\n userId: string,\n minRole: OrganizationRole = 'member',\n ): Promise<OrganizationAccess | null> {\n const [row] = await db\n .select({\n organization: organizations,\n member: organizationMembers,\n })\n .from(organizationMembers)\n .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))\n .where(and(\n eq(organizationMembers.organizationId, organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .limit(1)\n\n if (!row) return null\n const role = row.member.role as OrganizationRole\n if (!hasOrganizationRole(role, minRole)) return null\n return { organization: row.organization, member: row.member, role }\n }\n\n async function requireOrganizationAccess(\n organizationId: string,\n userId: string,\n minRole: OrganizationRole = 'member',\n ): Promise<OrganizationAccess> {\n const access = await getOrganizationAccess(organizationId, userId, minRole)\n if (!access) throw new Response('Organization not found', { status: 404 })\n return access\n }\n\n return { getOrganizationAccess, requireOrganizationAccess }\n}\n","/**\n * `ensurePersonalOrganization` — the substrate that makes solo-user adoption\n * work. Every user gets exactly one auto-created `kind: 'personal'` org with\n * the user as `owner` member. That org is the tenant that owns their\n * workspaces, so role resolution (`getWorkspaceAccess`) has an org row to read\n * even before any team/invite exists.\n *\n * Idempotent by construction: the org `slug` is derived from the user id\n * (`personal-<id>`) and upserted; the membership upserts on the\n * (organizationId, userId) unique index. Concurrent calls converge — no\n * duplicate personal orgs, no duplicate owner rows.\n *\n * ADOPTION CONTRACT: call this once per user at first authenticated entry\n * (e.g. right after sign-in / session bootstrap) BEFORE the first\n * `getWorkspaceAccess`. Role resolution requires the org membership to exist;\n * an adopter that skips this will see `null` access for a brand-new user with\n * no org row. There is no implicit creation inside the access builders — that\n * keeps reads side-effect-free; provisioning is this explicit call.\n */\n\nimport { and, eq } from 'drizzle-orm'\nimport type { OrganizationRole } from '../roles'\nimport type { TeamDatabase } from './access'\nimport type { OrganizationMemberRow, OrganizationRow, TeamTables } from './schema'\n\n/** Define the structure for a user within a personal organization context */\nexport interface EnsurePersonalOrganizationUser {\n id: string\n name?: string | null\n email?: string | null\n}\n\n/** Describe a personal organization result including organization, member, and role details */\nexport interface PersonalOrganizationResult {\n organization: OrganizationRow\n member: OrganizationMemberRow\n role: OrganizationRole\n}\n\n/** Define options required to create a personal organization including database and tables references */\nexport interface CreatePersonalOrganizationOptions {\n db: TeamDatabase\n tables: TeamTables\n}\n\n/** Ensure a user has a personal organization by creating or retrieving it as needed */\nexport function createEnsurePersonalOrganization(opts: CreatePersonalOrganizationOptions) {\n const { db, tables } = opts\n const { organizations, organizationMembers } = tables\n\n return async function ensurePersonalOrganization(\n user: EnsurePersonalOrganizationUser,\n ): Promise<PersonalOrganizationResult> {\n const [existing] = await db\n .select({\n organization: organizations,\n member: organizationMembers,\n })\n .from(organizationMembers)\n .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))\n .where(and(\n eq(organizationMembers.userId, user.id),\n eq(organizations.kind, 'personal'),\n ))\n .limit(1)\n\n if (existing) {\n return {\n organization: existing.organization,\n member: existing.member,\n role: existing.member.role as OrganizationRole,\n }\n }\n\n const orgName = user.name?.trim() || user.email?.split('@')[0] || 'Personal'\n const slug = `personal-${user.id}`\n const [organization] = await db\n .insert(organizations)\n .values({\n name: `${orgName}'s Organization`,\n slug,\n kind: 'personal',\n createdBy: user.id,\n })\n .onConflictDoUpdate({\n target: organizations.slug,\n set: { updatedAt: new Date() },\n })\n .returning()\n\n // A single-row upsert with .returning() always yields exactly one row.\n const org = organization as OrganizationRow\n\n const [member] = await db\n .insert(organizationMembers)\n .values({\n organizationId: org.id,\n userId: user.id,\n role: 'owner',\n })\n .onConflictDoUpdate({\n target: [organizationMembers.organizationId, organizationMembers.userId],\n set: { role: 'owner', updatedAt: new Date() },\n })\n .returning()\n\n return { organization: org, member: member as OrganizationMemberRow, role: 'owner' }\n }\n}\n"],"mappings":";;;;;;;AAqBA,SAAS,WAAW;AACpB,SAAS,OAAO,SAAS,aAAa,MAAM,mBAAmB;AAc/D,IAAM,QAAQ,MAAM,KAAK,IAAI,EAAE,WAAW,EAAE,QAAQ,iCAAiC;AAErF,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAEzG,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAGlG,SAAS,iBAAiB,MAA+B;AAC9D,QAAM,EAAE,WAAW,eAAe,IAAI;AAEtC,QAAM,gBAAgB,YAAY,gBAAgB;AAAA,IAChD,IAAI,MAAM;AAAA,IACV,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA,IAC3B,MAAM,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO;AAAA,IACpC,MAAM,KAAK,QAAQ,EAAE,MAAM,CAAC,YAAY,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,UAAU;AAAA,IAC/E,WAAW,KAAK,YAAY,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IAC9F,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,MAAM,6BAA6B,EAAE,GAAG,MAAM,SAAS;AAAA,EACzD,CAAC;AAED,QAAM,sBAAsB,YAAY,uBAAuB;AAAA,IAC7D,IAAI,MAAM;AAAA,IACV,gBAAgB,KAAK,iBAAiB,EAAE,QAAQ,EAAE,WAAW,MAAM,cAAc,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IAC5G,QAAQ,KAAK,SAAS,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACxF,MAAM,KAAK,QAAQ,EAAE,MAAM,CAAC,SAAS,SAAS,UAAU,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,QAAQ;AAAA,IAChG,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,sBAAsB,EAAE,GAAG,MAAM,gBAAgB,MAAM,MAAM;AAAA,IACzE,MAAM,qBAAqB,EAAE,GAAG,MAAM,MAAM;AAAA,EAC9C,CAAC;AAED,QAAM,mBAAmB,YAAY,oBAAoB;AAAA,IACvD,IAAI,MAAM;AAAA,IACV,aAAa,KAAK,cAAc,EAAE,QAAQ,EAAE,WAAW,MAAM,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACvG,sBAAsB,KAAK,wBAAwB,EAAE,WAAW,MAAM,oBAAoB,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACrH,QAAQ,KAAK,SAAS,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IAC9E,MAAM,KAAK,QAAQ,EAAE,MAAM,CAAC,SAAS,SAAS,UAAU,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,QAAQ;AAAA,IAC/F,WAAW,KAAK,YAAY;AAAA,IAC5B,aAAa,KAAK,cAAc;AAAA,IAChC,aAAa,KAAK,cAAc;AAAA,IAChC,WAAW,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAAA,IAC5F,YAAY,QAAQ,eAAe,EAAE,MAAM,YAAY,CAAC;AAAA,EAC1D,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,kCAAkC,EAAE,GAAG,MAAM,aAAa,MAAM,oBAAoB;AAAA,IAChG,MAAM,2BAA2B,EAAE,GAAG,MAAM,aAAa,MAAM,MAAM;AAAA,IACrE,MAAM,iBAAiB,EAAE,GAAG,MAAM,MAAM;AAAA,IACxC,YAAY,yBAAyB,EAAE,GAAG,MAAM,WAAW;AAAA,EAC7D,CAAC;AAED,SAAO,EAAE,eAAe,qBAAqB,iBAAiB;AAChE;;;ACxEA,SAAS,OAAAA,YAAW;AACpB,SAAS,SAAAC,QAAO,WAAAC,UAAS,eAAAC,cAAa,QAAAC,OAAM,eAAAC,oBAAmB;AAcxD,SAAS,+BAA+B,MAA6C;AAC1F,QAAM,EAAE,WAAW,gBAAgB,kBAAkB,IAAI;AAEzD,QAAM,uBAAuBF,aAAY,wBAAwB;AAAA,IAC/D,IAAIC,MAAK,IAAI,EAAE,WAAW,EAAE,QAAQJ,kCAAiC;AAAA,IACrE,aAAaI,MAAK,cAAc,EAAE,QAAQ,EAAE,WAAW,MAAM,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACvG,gBAAgBA,MAAK,iBAAiB,EAAE,QAAQ,EAAE,WAAW,MAAM,kBAAkB,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IAChH,OAAOA,MAAK,OAAO,EAAE,QAAQ;AAAA,IAC7B,iBAAiBA,MAAK,oBAAoB,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IAC5G,aAAaA,MAAK,eAAe,EAAE,MAAM,CAAC,SAAS,UAAU,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,QAAQ;AAAA,IACpG,OAAOA,MAAK,OAAO,EAAE,QAAQ,EAAE,OAAO;AAAA,IACtC,QAAQA,MAAK,UAAU,EAAE,MAAM,CAAC,WAAW,YAAY,WAAW,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,SAAS;AAAA,IAC3G,aAAaA,MAAK,gBAAgB,EAAE,MAAM,CAAC,YAAY,QAAQ,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,UAAU;AAAA,IACxG,WAAWF,SAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ;AAAA,IAChE,WAAWA,SAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQF,mBAAkB;AAAA,IAC5F,YAAYE,SAAQ,eAAe,EAAE,MAAM,YAAY,CAAC;AAAA,IACxD,WAAWA,SAAQ,cAAc,EAAE,MAAM,YAAY,CAAC;AAAA,IACtD,YAAYA,SAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAAA,EAC3D,GAAG,CAAC,UAAU;AAAA,IACZG,aAAY,iCAAiC,EAAE,GAAG,MAAM,KAAK;AAAA,IAC7DJ,OAAM,oCAAoC,EAAE,GAAG,MAAM,aAAa,MAAM,SAAS;AAAA,IACjFA,OAAM,gCAAgC,EAAE,GAAG,MAAM,KAAK;AAAA,IACtDA,OAAM,iDAAiD,EAAE,GAAG,MAAM,aAAa,MAAM,OAAO,MAAM,MAAM;AAAA,EAC1G,CAAC;AAED,SAAO,EAAE,qBAAqB;AAChC;;;ACvCA,SAAS,KAAK,MAAM,IAAI,iBAAiB;AA2DlC,SAAS,sBAAsB,MAA+C;AACnF,QAAM,EAAE,IAAI,QAAQ,eAAe,IAAI;AACvC,QAAM,EAAE,eAAe,qBAAqB,iBAAiB,IAAI;AACjE,QAAM,aAAa;AAEnB,iBAAe,mBACb,aACA,QACA,UAAyB,UACQ;AACjC,UAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO;AAAA,MACN,WAAW;AAAA,MACX,cAAc;AAAA,MACd,oBAAoB;AAAA,MACpB,aAAa,iBAAiB;AAAA,MAC9B,YAAY,iBAAiB;AAAA,IAC/B,CAAC,EACA,KAAK,UAAU,EACf,UAAU,eAAe,GAAG,cAAc,IAAI,WAAW,cAAc,CAAC,EACxE,UAAU,qBAAqB;AAAA,MAC9B,GAAG,oBAAoB,gBAAgB,WAAW,cAAc;AAAA,MAChE,GAAG,oBAAoB,QAAQ,MAAM;AAAA,IACvC,CAAC,EACA,SAAS,kBAAkB;AAAA,MAC1B,GAAG,iBAAiB,aAAa,WAAW,EAAE;AAAA,MAC9C,GAAG,iBAAiB,sBAAsB,oBAAoB,EAAE;AAAA,MAChE,UAAU,iBAAiB,UAAU;AAAA,IACvC,CAAC,EACA,MAAM,GAAG,WAAW,IAAI,WAAW,CAAC,EACpC,MAAM,CAAC;AAEV,QAAI,CAAC,IAAK,QAAO;AAEjB,UAAM,OAAO,qBAAqB,IAAI,mBAAmB,MAAM,IAAI,WAAmC;AACtG,QAAI,CAAC,KAAM,QAAO;AAClB,QAAI,CAAC,iBAAiB,MAAM,OAAO,EAAG,QAAO;AAE7C,WAAO;AAAA,MACL,WAAW,IAAI;AAAA,MACf,cAAc,IAAI;AAAA,MAClB,oBAAoB,IAAI;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,uBACb,aACA,QACA,UAAyB,UACC;AAC1B,UAAM,SAAS,MAAM,mBAAmB,aAAa,QAAQ,OAAO;AACpE,QAAI,CAAC,OAAQ,OAAM,IAAI,SAAS,uBAAuB,EAAE,QAAQ,IAAI,CAAC;AACtE,WAAO;AAAA,EACT;AAEA,iBAAe,mBAAmB,QAAiD;AACjF,UAAM,OAAO,MAAM,GAChB,OAAO;AAAA,MACN,WAAW;AAAA,MACX,cAAc;AAAA,MACd,oBAAoB;AAAA,MACpB,aAAa,iBAAiB;AAAA,IAChC,CAAC,EACA,KAAK,UAAU,EACf,UAAU,eAAe,GAAG,cAAc,IAAI,WAAW,cAAc,CAAC,EACxE,UAAU,qBAAqB;AAAA,MAC9B,GAAG,oBAAoB,gBAAgB,WAAW,cAAc;AAAA,MAChE,GAAG,oBAAoB,QAAQ,MAAM;AAAA,IACvC,CAAC,EACA,SAAS,kBAAkB;AAAA,MAC1B,GAAG,iBAAiB,aAAa,WAAW,EAAE;AAAA,MAC9C,GAAG,iBAAiB,sBAAsB,oBAAoB,EAAE;AAAA,MAChE,UAAU,iBAAiB,UAAU;AAAA,IACvC,CAAC,EAGA,QAAQ,KAAK,WAAW,SAAS,CAAC;AAErC,WAAO,KAAK,QAAQ,CAAC,QAAa;AAChC,YAAM,OAAO,qBAAqB,IAAI,mBAAmB,MAAM,IAAI,WAAmC;AACtG,UAAI,CAAC,KAAM,QAAO,CAAC;AACnB,aAAO,CAAC;AAAA,QACN,GAAI,IAAI;AAAA,QACR,gBAAgB,IAAI,aAAa;AAAA,QACjC,kBAAkB,IAAI,aAAa;AAAA,QACnC;AAAA,MACF,CAAyB;AAAA,IAC3B,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,oBAAoB,wBAAwB,mBAAmB;AAC1E;AAsBO,SAAS,yBAAyB,MAA8D;AACrG,QAAM,EAAE,IAAI,OAAO,IAAI;AACvB,QAAM,EAAE,eAAe,oBAAoB,IAAI;AAE/C,iBAAe,sBACb,gBACA,QACA,UAA4B,UACQ;AACpC,UAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO;AAAA,MACN,cAAc;AAAA,MACd,QAAQ;AAAA,IACV,CAAC,EACA,KAAK,mBAAmB,EACxB,UAAU,eAAe,GAAG,cAAc,IAAI,oBAAoB,cAAc,CAAC,EACjF,MAAM;AAAA,MACL,GAAG,oBAAoB,gBAAgB,cAAc;AAAA,MACrD,GAAG,oBAAoB,QAAQ,MAAM;AAAA,IACvC,CAAC,EACA,MAAM,CAAC;AAEV,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,OAAO,IAAI,OAAO;AACxB,QAAI,CAAC,oBAAoB,MAAM,OAAO,EAAG,QAAO;AAChD,WAAO,EAAE,cAAc,IAAI,cAAc,QAAQ,IAAI,QAAQ,KAAK;AAAA,EACpE;AAEA,iBAAe,0BACb,gBACA,QACA,UAA4B,UACC;AAC7B,UAAM,SAAS,MAAM,sBAAsB,gBAAgB,QAAQ,OAAO;AAC1E,QAAI,CAAC,OAAQ,OAAM,IAAI,SAAS,0BAA0B,EAAE,QAAQ,IAAI,CAAC;AACzE,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,uBAAuB,0BAA0B;AAC5D;;;ACnNA,SAAS,OAAAK,MAAK,MAAAC,WAAU;AA0BjB,SAAS,iCAAiC,MAAyC;AACxF,QAAM,EAAE,IAAI,OAAO,IAAI;AACvB,QAAM,EAAE,eAAe,oBAAoB,IAAI;AAE/C,SAAO,eAAe,2BACpB,MACqC;AACrC,UAAM,CAAC,QAAQ,IAAI,MAAM,GACtB,OAAO;AAAA,MACN,cAAc;AAAA,MACd,QAAQ;AAAA,IACV,CAAC,EACA,KAAK,mBAAmB,EACxB,UAAU,eAAeA,IAAG,cAAc,IAAI,oBAAoB,cAAc,CAAC,EACjF,MAAMD;AAAA,MACLC,IAAG,oBAAoB,QAAQ,KAAK,EAAE;AAAA,MACtCA,IAAG,cAAc,MAAM,UAAU;AAAA,IACnC,CAAC,EACA,MAAM,CAAC;AAEV,QAAI,UAAU;AACZ,aAAO;AAAA,QACL,cAAc,SAAS;AAAA,QACvB,QAAQ,SAAS;AAAA,QACjB,MAAM,SAAS,OAAO;AAAA,MACxB;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,MAAM,KAAK,KAAK,KAAK,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK;AAClE,UAAM,OAAO,YAAY,KAAK,EAAE;AAChC,UAAM,CAAC,YAAY,IAAI,MAAM,GAC1B,OAAO,aAAa,EACpB,OAAO;AAAA,MACN,MAAM,GAAG,OAAO;AAAA,MAChB;AAAA,MACA,MAAM;AAAA,MACN,WAAW,KAAK;AAAA,IAClB,CAAC,EACA,mBAAmB;AAAA,MAClB,QAAQ,cAAc;AAAA,MACtB,KAAK,EAAE,WAAW,oBAAI,KAAK,EAAE;AAAA,IAC/B,CAAC,EACA,UAAU;AAGb,UAAM,MAAM;AAEZ,UAAM,CAAC,MAAM,IAAI,MAAM,GACpB,OAAO,mBAAmB,EAC1B,OAAO;AAAA,MACN,gBAAgB,IAAI;AAAA,MACpB,QAAQ,KAAK;AAAA,MACb,MAAM;AAAA,IACR,CAAC,EACA,mBAAmB;AAAA,MAClB,QAAQ,CAAC,oBAAoB,gBAAgB,oBAAoB,MAAM;AAAA,MACvE,KAAK,EAAE,MAAM,SAAS,WAAW,oBAAI,KAAK,EAAE;AAAA,IAC9C,CAAC,EACA,UAAU;AAEb,WAAO,EAAE,cAAc,KAAK,QAAyC,MAAM,QAAQ;AAAA,EACrF;AACF;","names":["sql","index","integer","sqliteTable","text","uniqueIndex","and","eq"]}
|
|
1
|
+
{"version":3,"sources":["../../src/teams/drizzle/schema.ts","../../src/teams/drizzle/invitations-schema.ts","../../src/teams/drizzle/access.ts","../../src/teams/drizzle/personal-organization.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the teams tables. The product owns the user and\n * workspace tables; this factory creates the tenancy/membership tables and\n * wires their foreign keys into the passed-in tables so the whole graph lives\n * in one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror gtm's hand-rolled tables so a product\n * with those tables can adopt the factory without rewriting rows.\n *\n * The three tables and their roles:\n * - `organization` — the TENANT/ownership primitive. `kind` is 'personal'\n * (one auto-created per user; see ensurePersonalOrganization) or 'team'.\n * This is what owns workspaces and what billing/seats attach to.\n * - `organizationMember` — who belongs to an org and at what org role.\n * - `workspaceMember` — the additive invite/member surface: per-workspace\n * access grants, including pending email-only invites carrying a token.\n *\n * Imports `drizzle-orm` at module top — that is WHY this lives behind the\n * `/teams/drizzle` sub-subpath. The pure `./teams` leaf imports none of this,\n * so a consumer that never touches the DB never pulls the optional peer.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable } from 'drizzle-orm/sqlite-core'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type TeamParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options specifying user and workspace tables for creating team-related tables */\nexport interface CreateTeamTablesOptions {\n /** The product's user table — org/member rows reference `userTable.id`. */\n userTable: TeamParentTable\n /** The product's workspace table — workspace members reference `workspaceTable.id`. */\n workspaceTable: TeamParentTable\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\n/** Build SQLite tables for organizations and related team structures using provided options */\nexport function createTeamTables(opts: CreateTeamTablesOptions) {\n const { userTable, workspaceTable } = opts\n\n const organizations = sqliteTable('organization', {\n id: hexId(),\n name: text('name').notNull(),\n slug: text('slug').notNull().unique(),\n kind: text('kind', { enum: ['personal', 'team'] }).notNull().default('personal'),\n createdBy: text('created_by').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n index('idx_organization_created_by').on(table.createdBy),\n ])\n\n const organizationMembers = sqliteTable('organization_member', {\n id: hexId(),\n organizationId: text('organization_id').notNull().references(() => organizations.id, { onDelete: 'cascade' }),\n userId: text('user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['owner', 'admin', 'member', 'billing'] }).notNull().default('member'),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_org_member_user').on(table.organizationId, table.userId),\n index('idx_org_member_user').on(table.userId),\n ])\n\n const workspaceMembers = sqliteTable('workspace_member', {\n id: hexId(),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n organizationMemberId: text('organization_member_id').references(() => organizationMembers.id, { onDelete: 'cascade' }),\n userId: text('user_id').references(() => userTable.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['owner', 'admin', 'editor', 'viewer'] }).notNull().default('editor'),\n invitedBy: text('invited_by'),\n inviteEmail: text('invite_email'),\n inviteToken: text('invite_token'),\n invitedAt: integer('invited_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),\n acceptedAt: integer('accepted_at', { mode: 'timestamp' }),\n }, (table) => [\n uniqueIndex('uniq_workspace_member_org_member').on(table.workspaceId, table.organizationMemberId),\n index('idx_workspace_member_user').on(table.workspaceId, table.userId),\n index('idx_member_user').on(table.userId),\n uniqueIndex('idx_member_invite_token').on(table.inviteToken),\n ])\n\n return { organizations, organizationMembers, workspaceMembers }\n}\n\n/** Resolve team tables by deriving the return type of createTeamTables */\nexport type TeamTables = ReturnType<typeof createTeamTables>\n\n/** Resolve the structure of an organization row from the organizations table in TeamTables */\nexport type OrganizationRow = TeamTables['organizations']['$inferSelect']\n/** Resolve the structure of an organization member row from the team tables selection */\nexport type OrganizationMemberRow = TeamTables['organizationMembers']['$inferSelect']\n/** Resolve a workspace member row with selected fields from the workspaceMembers table */\nexport type WorkspaceMemberRow = TeamTables['workspaceMembers']['$inferSelect']\n","/**\n * Drizzle schema factory for the dedicated workspace-invitation table. Separate\n * from `createTeamTables` on purpose: an app opts into the rich email-invitation\n * lifecycle (status / expiry / resend / revoke / preview) by calling this, and an\n * app that doesn't want it never grows the table — `createTeamTables`'s output and\n * migrations stay untouched.\n *\n * The product owns the user and workspace tables; `organizations` comes from\n * `createTeamTables`. Pass all three so the invitation's FKs wire into the same\n * drizzle schema with real cascade semantics. Call this AFTER `createTeamTables`\n * (so `organizations` exists) — the lazy `.references(() => ...)` closures make\n * the ordering within one module safe.\n *\n * Columns / enums / indexes mirror creative-agent's validated hand-rolled table,\n * so an app already running that table adopts this factory with no row rewrite.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { TeamParentTable } from './schema'\n\n/** Define options for creating a workspace invitation table with user, workspace, and organization references */\nexport interface CreateWorkspaceInvitationTableOptions {\n /** The product's user table — `invitedByUserId` references `userTable.id`. */\n userTable: TeamParentTable\n /** The product's workspace table — `workspaceId` references `workspaceTable.id`. */\n workspaceTable: TeamParentTable\n /** The `organizations` table from `createTeamTables` — `organizationId` references it. */\n organizationTable: TeamParentTable\n}\n\n/** Build a workspace invitation table with defined columns and foreign key constraints */\nexport function createWorkspaceInvitationTable(opts: CreateWorkspaceInvitationTableOptions) {\n const { userTable, workspaceTable, organizationTable } = opts\n\n const workspaceInvitations = sqliteTable('workspace_invitation', {\n id: text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n organizationId: text('organization_id').notNull().references(() => organizationTable.id, { onDelete: 'cascade' }),\n email: text('email').notNull(),\n invitedByUserId: text('invited_by_user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n permissions: text('permissions', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'),\n token: text('token').notNull().unique(),\n status: text('status', { enum: ['pending', 'accepted', 'expired', 'revoked'] }).notNull().default('pending'),\n emailStatus: text('email_status', { enum: ['not_sent', 'sent', 'failed'] }).notNull().default('not_sent'),\n expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),\n acceptedAt: integer('accepted_at', { mode: 'timestamp' }),\n revokedAt: integer('revoked_at', { mode: 'timestamp' }),\n lastSentAt: integer('last_sent_at', { mode: 'timestamp' }),\n }, (table) => [\n uniqueIndex('uniq_workspace_invitation_token').on(table.token),\n index('idx_workspace_invitation_workspace').on(table.workspaceId, table.createdAt),\n index('idx_workspace_invitation_email').on(table.email),\n index('idx_workspace_invitation_workspace_email_status').on(table.workspaceId, table.email, table.status),\n ])\n\n return { workspaceInvitations }\n}\n\n/** Resolve the structure of workspace invitation tables from the creation function */\nexport type WorkspaceInvitationTables = ReturnType<typeof createWorkspaceInvitationTable>\n/** Resolve the structure of a workspace invitation row from the workspaceInvitations table */\nexport type WorkspaceInvitationRow = WorkspaceInvitationTables['workspaceInvitations']['$inferSelect']\n","/**\n * RBAC access builders over the teams tables. `createWorkspaceAccess` and\n * `createOrganizationAccess` close over the product's `db`, the tables from\n * `createTeamTables`, and the product's `workspace` table, returning the exact\n * `getWorkspaceAccess` / `requireWorkspaceAccess` / `listUserWorkspaces` /\n * `getOrganizationAccess` / `requireOrganizationAccess` functions a consumer\n * already calls — so adoption is a one-line import swap, every call site stays\n * identical.\n *\n * Defense in depth: org owners/admins are workspace owners across the org;\n * everyone else gets their explicit per-workspace role. Effective role is the\n * fold in `resolveWorkspaceRole` (pure, from `../roles`). Every query pins\n * `userId`, so a leaked id can never read across the tenancy boundary — it\n * surfaces as \"not found\".\n *\n * Driver-agnostic: builders are awaited, never `.run()`/`.all()`, so\n * better-sqlite3, D1, and libsql handles all behave identically.\n */\n\nimport { and, desc, eq, isNotNull } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport {\n type OrganizationRole,\n type WorkspaceRole,\n hasOrganizationRole,\n hasWorkspaceRole,\n resolveWorkspaceRole,\n} from '../roles'\nimport type { TeamParentTable } from './schema'\nimport type { OrganizationMemberRow, OrganizationRow, TeamTables } from './schema'\n\n/** Any SQLite drizzle database — `any` erases driver-specific generics so\n * better-sqlite3, D1, and libsql handles all fit. */\nexport type TeamDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any>\n\n/**\n * The product's workspace table, narrowed to the columns the access joins read.\n * Adopters pass their real drizzle workspace table — it carries these columns\n * (gtm's does); the type only asserts the minimum the joins touch.\n */\nexport interface WorkspaceAccessTable {\n id: any\n organizationId: any\n name: any\n updatedAt: any\n}\n\n/** Define options required to create access with database, tables, and workspace table references */\nexport interface CreateAccessOptions {\n db: TeamDatabase\n tables: TeamTables\n /** The product's workspace table (the FK target passed to createTeamTables). */\n workspaceTable: TeamParentTable & WorkspaceAccessTable\n}\n\n/** Define access details including workspace data, organization info, member info, and role within workspace */\nexport interface WorkspaceAccess {\n workspace: Record<string, unknown>\n organization: OrganizationRow\n organizationMember: OrganizationMemberRow\n role: WorkspaceRole\n}\n\n/** Describe a user's workspace details including organization and role information */\nexport interface UserWorkspaceSummary extends Record<string, unknown> {\n organizationId: string\n organizationName: string\n role: WorkspaceRole\n}\n\n/** Define methods to retrieve and enforce user access permissions within workspaces */\nexport interface WorkspaceAccessApi {\n getWorkspaceAccess(workspaceId: string, userId: string, minRole?: WorkspaceRole): Promise<WorkspaceAccess | null>\n requireWorkspaceAccess(workspaceId: string, userId: string, minRole?: WorkspaceRole): Promise<WorkspaceAccess>\n listUserWorkspaces(userId: string): Promise<UserWorkspaceSummary[]>\n}\n\n/** Authorization fields consumed by membership writers; products own their access policy. */\nexport interface WorkspaceAccessReader {\n getWorkspaceAccess(...args: Parameters<WorkspaceAccessApi['getWorkspaceAccess']>): Promise<Pick<WorkspaceAccess, 'workspace' | 'organization' | 'role'> | null>\n}\n\n/** Create workspace access API to manage user roles and permissions within a workspace */\nexport function createWorkspaceAccess(opts: CreateAccessOptions): WorkspaceAccessApi {\n const { db, tables, workspaceTable } = opts\n const { organizations, organizationMembers, workspaceMembers } = tables\n const workspaces = workspaceTable\n\n async function getWorkspaceAccess(\n workspaceId: string,\n userId: string,\n minRole: WorkspaceRole = 'viewer',\n ): Promise<WorkspaceAccess | null> {\n const [row] = await db\n .select({\n workspace: workspaces,\n organization: organizations,\n organizationMember: organizationMembers,\n projectRole: workspaceMembers.role,\n acceptedAt: workspaceMembers.acceptedAt,\n })\n .from(workspaces)\n .innerJoin(organizations, eq(organizations.id, workspaces.organizationId))\n .innerJoin(organizationMembers, and(\n eq(organizationMembers.organizationId, workspaces.organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .leftJoin(workspaceMembers, and(\n eq(workspaceMembers.workspaceId, workspaces.id),\n eq(workspaceMembers.organizationMemberId, organizationMembers.id),\n isNotNull(workspaceMembers.acceptedAt),\n ))\n .where(eq(workspaces.id, workspaceId))\n .limit(1)\n\n if (!row) return null\n\n const role = resolveWorkspaceRole(row.organizationMember.role, row.projectRole as WorkspaceRole | null)\n if (!role) return null\n if (!hasWorkspaceRole(role, minRole)) return null\n\n return {\n workspace: row.workspace as Record<string, unknown>,\n organization: row.organization,\n organizationMember: row.organizationMember,\n role,\n }\n }\n\n async function requireWorkspaceAccess(\n workspaceId: string,\n userId: string,\n minRole: WorkspaceRole = 'viewer',\n ): Promise<WorkspaceAccess> {\n const access = await getWorkspaceAccess(workspaceId, userId, minRole)\n if (!access) throw new Response('Workspace not found', { status: 404 })\n return access\n }\n\n async function listUserWorkspaces(userId: string): Promise<UserWorkspaceSummary[]> {\n const rows = await db\n .select({\n workspace: workspaces,\n organization: organizations,\n organizationMember: organizationMembers,\n projectRole: workspaceMembers.role,\n })\n .from(workspaces)\n .innerJoin(organizations, eq(organizations.id, workspaces.organizationId))\n .innerJoin(organizationMembers, and(\n eq(organizationMembers.organizationId, workspaces.organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .leftJoin(workspaceMembers, and(\n eq(workspaceMembers.workspaceId, workspaces.id),\n eq(workspaceMembers.organizationMemberId, organizationMembers.id),\n isNotNull(workspaceMembers.acceptedAt),\n ))\n // Most recently active workspace first, so callers taking rows[0] as the\n // default pick the freshest one.\n .orderBy(desc(workspaces.updatedAt))\n\n return rows.flatMap((row: any) => {\n const role = resolveWorkspaceRole(row.organizationMember.role, row.projectRole as WorkspaceRole | null)\n if (!role) return []\n return [{\n ...(row.workspace as Record<string, unknown>),\n organizationId: row.organization.id,\n organizationName: row.organization.name,\n role,\n } as UserWorkspaceSummary]\n })\n }\n\n return { getWorkspaceAccess, requireWorkspaceAccess, listUserWorkspaces }\n}\n\n/** Define access details linking an organization, its member, and the member's role */\nexport interface OrganizationAccess {\n organization: OrganizationRow\n member: OrganizationMemberRow\n role: OrganizationRole\n}\n\n/** Define methods to retrieve and enforce user access levels within an organization */\nexport interface OrganizationAccessApi {\n getOrganizationAccess(organizationId: string, userId: string, minRole?: OrganizationRole): Promise<OrganizationAccess | null>\n requireOrganizationAccess(organizationId: string, userId: string, minRole?: OrganizationRole): Promise<OrganizationAccess>\n}\n\n/** Define options required to create access for an organization including database and tables */\nexport interface CreateOrganizationAccessOptions {\n db: TeamDatabase\n tables: TeamTables\n}\n\n/** Resolve organization access API with specified database and table options */\nexport function createOrganizationAccess(opts: CreateOrganizationAccessOptions): OrganizationAccessApi {\n const { db, tables } = opts\n const { organizations, organizationMembers } = tables\n\n async function getOrganizationAccess(\n organizationId: string,\n userId: string,\n minRole: OrganizationRole = 'member',\n ): Promise<OrganizationAccess | null> {\n const [row] = await db\n .select({\n organization: organizations,\n member: organizationMembers,\n })\n .from(organizationMembers)\n .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))\n .where(and(\n eq(organizationMembers.organizationId, organizationId),\n eq(organizationMembers.userId, userId),\n ))\n .limit(1)\n\n if (!row) return null\n const role = row.member.role as OrganizationRole\n if (!hasOrganizationRole(role, minRole)) return null\n return { organization: row.organization, member: row.member, role }\n }\n\n async function requireOrganizationAccess(\n organizationId: string,\n userId: string,\n minRole: OrganizationRole = 'member',\n ): Promise<OrganizationAccess> {\n const access = await getOrganizationAccess(organizationId, userId, minRole)\n if (!access) throw new Response('Organization not found', { status: 404 })\n return access\n }\n\n return { getOrganizationAccess, requireOrganizationAccess }\n}\n","/**\n * `ensurePersonalOrganization` — the substrate that makes solo-user adoption\n * work. Every user gets exactly one auto-created `kind: 'personal'` org with\n * the user as `owner` member. That org is the tenant that owns their\n * workspaces, so role resolution (`getWorkspaceAccess`) has an org row to read\n * even before any team/invite exists.\n *\n * Idempotent by construction: the org `slug` is derived from the user id\n * (`personal-<id>`) and upserted; the membership upserts on the\n * (organizationId, userId) unique index. Concurrent calls converge — no\n * duplicate personal orgs, no duplicate owner rows.\n *\n * ADOPTION CONTRACT: call this once per user at first authenticated entry\n * (e.g. right after sign-in / session bootstrap) BEFORE the first\n * `getWorkspaceAccess`. Role resolution requires the org membership to exist;\n * an adopter that skips this will see `null` access for a brand-new user with\n * no org row. There is no implicit creation inside the access builders — that\n * keeps reads side-effect-free; provisioning is this explicit call.\n */\n\nimport { and, eq } from 'drizzle-orm'\nimport type { OrganizationRole } from '../roles'\nimport type { TeamDatabase } from './access'\nimport type { OrganizationMemberRow, OrganizationRow, TeamTables } from './schema'\n\n/** Define the structure for a user within a personal organization context */\nexport interface EnsurePersonalOrganizationUser {\n id: string\n name?: string | null\n email?: string | null\n}\n\n/** Describe a personal organization result including organization, member, and role details */\nexport interface PersonalOrganizationResult {\n organization: OrganizationRow\n member: OrganizationMemberRow\n role: OrganizationRole\n}\n\n/** Define options required to create a personal organization including database and tables references */\nexport interface CreatePersonalOrganizationOptions {\n db: TeamDatabase\n tables: TeamTables\n}\n\n/** Ensure a user has a personal organization by creating or retrieving it as needed */\nexport function createEnsurePersonalOrganization(opts: CreatePersonalOrganizationOptions) {\n const { db, tables } = opts\n const { organizations, organizationMembers } = tables\n\n return async function ensurePersonalOrganization(\n user: EnsurePersonalOrganizationUser,\n ): Promise<PersonalOrganizationResult> {\n const [existing] = await db\n .select({\n organization: organizations,\n member: organizationMembers,\n })\n .from(organizationMembers)\n .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))\n .where(and(\n eq(organizationMembers.userId, user.id),\n eq(organizations.kind, 'personal'),\n ))\n .limit(1)\n\n if (existing) {\n return {\n organization: existing.organization,\n member: existing.member,\n role: existing.member.role as OrganizationRole,\n }\n }\n\n const orgName = user.name?.trim() || user.email?.split('@')[0] || 'Personal'\n const slug = `personal-${user.id}`\n const [organization] = await db\n .insert(organizations)\n .values({\n name: `${orgName}'s Organization`,\n slug,\n kind: 'personal',\n createdBy: user.id,\n })\n .onConflictDoUpdate({\n target: organizations.slug,\n set: { updatedAt: new Date() },\n })\n .returning()\n\n // A single-row upsert with .returning() always yields exactly one row.\n const org = organization as OrganizationRow\n\n const [member] = await db\n .insert(organizationMembers)\n .values({\n organizationId: org.id,\n userId: user.id,\n role: 'owner',\n })\n .onConflictDoUpdate({\n target: [organizationMembers.organizationId, organizationMembers.userId],\n set: { role: 'owner', updatedAt: new Date() },\n })\n .returning()\n\n return { organization: org, member: member as OrganizationMemberRow, role: 'owner' }\n }\n}\n"],"mappings":"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","names":["sql","index","integer","sqliteTable","text","uniqueIndex","and","eq"]}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { type InvitationEmailStatus, type InvitationPermission, type InvitationStatus } from './invitations';
|
|
27
27
|
import { type EnforceSeatSeam, type MemberSyncSeam } from './members-api';
|
|
28
|
-
import type { TeamDatabase,
|
|
28
|
+
import type { TeamDatabase, WorkspaceAccessReader } from './drizzle/access';
|
|
29
29
|
import type { TeamParentTable, TeamTables } from './drizzle/schema';
|
|
30
30
|
import type { WorkspaceInvitationTables } from './drizzle/invitations-schema';
|
|
31
31
|
export { SeatLimitError } from './members-api';
|
|
@@ -60,16 +60,19 @@ export interface InvitationUserTable {
|
|
|
60
60
|
export interface InvitationWorkspaceTable {
|
|
61
61
|
id: any;
|
|
62
62
|
name: any;
|
|
63
|
+
organizationId: any;
|
|
63
64
|
}
|
|
64
65
|
/** Define configuration options required to manage workspace invitations and related data sources */
|
|
65
66
|
export interface InvitationsApiOptions {
|
|
66
|
-
db: TeamDatabase
|
|
67
|
+
db: TeamDatabase & {
|
|
68
|
+
batch?: (statements: [any, ...any[]]) => Promise<any[]>;
|
|
69
|
+
};
|
|
67
70
|
tables: TeamTables;
|
|
68
71
|
/** The invitation table from `createWorkspaceInvitationTable`. */
|
|
69
72
|
invitationsTable: WorkspaceInvitationTables;
|
|
70
73
|
userTable: TeamParentTable & InvitationUserTable;
|
|
71
74
|
workspaceTable: TeamParentTable & InvitationWorkspaceTable;
|
|
72
|
-
access:
|
|
75
|
+
access: WorkspaceAccessReader;
|
|
73
76
|
/** REQUIRED — the app's mail transport (e.g. Resend + `inviteEmail` from `./email`). */
|
|
74
77
|
sendInvitationEmail: SendInvitationEmailSeam;
|
|
75
78
|
/** OPTIONAL — seat-limit gate at create time; mirrors members-api. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SeatLimitError
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-ZBB5KQ6B.js";
|
|
4
4
|
import "../chunk-DJ4VJIH5.js";
|
|
5
5
|
import {
|
|
6
6
|
INVITATION_EMAIL_HINT,
|
|
@@ -179,10 +179,7 @@ function createInvitationsApi(opts) {
|
|
|
179
179
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
180
180
|
const row = await getInvitationByToken(input.token);
|
|
181
181
|
if (!row) return { succeeded: false, status: 404, error: "Invitation not found" };
|
|
182
|
-
|
|
183
|
-
if (invitation.status === "pending" && invitation.expiresAt <= now) {
|
|
184
|
-
invitation = await markInvitationExpired(invitation.id);
|
|
185
|
-
}
|
|
182
|
+
const invitation = row.invitation;
|
|
186
183
|
const [currentUser] = await db.select({ id: users.id, email: users.email, emailVerified: users.emailVerified }).from(users).where(eq(users.id, input.userId)).limit(1);
|
|
187
184
|
if (!currentUser) return { succeeded: false, status: 401, error: "Authentication required" };
|
|
188
185
|
if (normalizeInvitationEmail(currentUser.email) !== invitation.email) {
|
|
@@ -196,7 +193,7 @@ function createInvitationsApi(opts) {
|
|
|
196
193
|
error: "Multiple accounts use this email. Contact support to reconcile before accepting this invitation."
|
|
197
194
|
};
|
|
198
195
|
}
|
|
199
|
-
|
|
196
|
+
const orgMember = await getOrganizationMember(invitation.organizationId, currentUser.id);
|
|
200
197
|
if (orgMember?.role === "owner" || orgMember?.role === "admin") {
|
|
201
198
|
return { succeeded: false, status: 409, error: "This user already has account-level access." };
|
|
202
199
|
}
|
|
@@ -207,42 +204,78 @@ function createInvitationsApi(opts) {
|
|
|
207
204
|
}
|
|
208
205
|
if (invitation.status === "expired") return { succeeded: false, status: 409, error: "Invitation has expired" };
|
|
209
206
|
if (invitation.status === "revoked") return { succeeded: false, status: 409, error: "Invitation has been revoked" };
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
207
|
+
if (invitation.expiresAt <= now) return { succeeded: false, status: 409, error: "Invitation has expired" };
|
|
208
|
+
if (!db.batch) return { succeeded: false, status: 500, error: "Atomic invitation acceptance is unavailable" };
|
|
209
|
+
const eligible = and(
|
|
210
|
+
eq(workspaceInvitations.id, invitation.id),
|
|
211
|
+
eq(workspaceInvitations.token, input.token),
|
|
212
|
+
eq(workspaceInvitations.status, "pending"),
|
|
213
|
+
eq(workspaceInvitations.organizationId, invitation.organizationId),
|
|
214
|
+
eq(workspaceInvitations.workspaceId, invitation.workspaceId),
|
|
215
|
+
eq(workspaceInvitations.email, invitation.email),
|
|
216
|
+
eq(workspaceInvitations.permissions, invitation.permissions),
|
|
217
|
+
sql`${workspaceInvitations.expiresAt} > ${Math.floor(now.getTime() / 1e3)}`,
|
|
218
|
+
sql`exists (select 1 from ${workspaces} where ${workspaces.id} = ${workspaceInvitations.workspaceId}
|
|
219
|
+
and ${workspaces.organizationId} = ${invitation.organizationId})`,
|
|
220
|
+
sql`exists (select 1 from ${users} where ${users.id} = ${currentUser.id}
|
|
221
|
+
and lower(trim(${users.email})) = ${invitation.email}
|
|
222
|
+
and ${users.emailVerified} = ${currentUser.emailVerified ? 1 : 0})`,
|
|
223
|
+
sql`(select count(*) from ${users} where lower(trim(${users.email})) = ${invitation.email}) = 1`,
|
|
224
|
+
sql`not exists (select 1 from ${workspaceMembers} inner join ${organizationMembers}
|
|
225
|
+
on ${organizationMembers.id} = ${workspaceMembers.organizationMemberId}
|
|
226
|
+
where ${workspaceMembers.workspaceId} = ${invitation.workspaceId}
|
|
227
|
+
and ${organizationMembers.organizationId} = ${invitation.organizationId}
|
|
228
|
+
and ${organizationMembers.userId} = ${currentUser.id}
|
|
229
|
+
and ${workspaceMembers.acceptedAt} is null)`,
|
|
230
|
+
sql`not exists (select 1 from ${workspaceMembers} where ${workspaceMembers.inviteToken} = ${invitation.token}
|
|
231
|
+
and (${workspaceMembers.workspaceId} != ${invitation.workspaceId}
|
|
232
|
+
or ${workspaceMembers.organizationMemberId} is null
|
|
233
|
+
or ${workspaceMembers.organizationMemberId} not in (select ${organizationMembers.id} from ${organizationMembers}
|
|
234
|
+
where ${organizationMembers.organizationId} = ${invitation.organizationId}
|
|
235
|
+
and ${organizationMembers.userId} = ${currentUser.id})))`,
|
|
236
|
+
sql`not exists (select 1 from ${organizationMembers}
|
|
237
|
+
where ${organizationMembers.organizationId} = ${invitation.organizationId}
|
|
238
|
+
and ${organizationMembers.userId} = ${currentUser.id}
|
|
239
|
+
and ${organizationMembers.role} in ('owner', 'admin'))`
|
|
240
|
+
);
|
|
241
|
+
const eligibleInvitation = sql`exists (select 1 from ${workspaceInvitations} where ${eligible})`;
|
|
242
|
+
const timestamp = sql`${Math.floor(now.getTime() / 1e3)}`;
|
|
243
|
+
const [, insertedMembers, accepted] = await db.batch([
|
|
244
|
+
db.insert(organizationMembers).select(db.select({
|
|
245
|
+
id: sql`lower(hex(randomblob(16)))`.as(organizationMembers.id.name),
|
|
246
|
+
organizationId: sql`${invitation.organizationId}`.as(organizationMembers.organizationId.name),
|
|
247
|
+
userId: sql`${currentUser.id}`.as(organizationMembers.userId.name),
|
|
248
|
+
role: sql`'member'`.as(organizationMembers.role.name),
|
|
249
|
+
createdAt: timestamp.as(organizationMembers.createdAt.name),
|
|
250
|
+
updatedAt: timestamp.as(organizationMembers.updatedAt.name)
|
|
251
|
+
}).from(workspaceInvitations).where(eligible)).onConflictDoNothing({ target: [organizationMembers.organizationId, organizationMembers.userId] }).returning(),
|
|
252
|
+
db.insert(workspaceMembers).select(db.select({
|
|
253
|
+
id: sql`lower(hex(randomblob(16)))`.as(workspaceMembers.id.name),
|
|
254
|
+
workspaceId: sql`${invitation.workspaceId}`.as(workspaceMembers.workspaceId.name),
|
|
255
|
+
organizationMemberId: organizationMembers.id,
|
|
256
|
+
userId: sql`${currentUser.id}`.as(workspaceMembers.userId.name),
|
|
257
|
+
role: sql`${invitation.permissions}`.as(workspaceMembers.role.name),
|
|
258
|
+
invitedBy: sql`null`.as(workspaceMembers.invitedBy.name),
|
|
259
|
+
inviteEmail: sql`${invitation.email}`.as(workspaceMembers.inviteEmail.name),
|
|
260
|
+
inviteToken: sql`${invitation.token}`.as(workspaceMembers.inviteToken.name),
|
|
261
|
+
invitedAt: timestamp.as(workspaceMembers.invitedAt.name),
|
|
262
|
+
acceptedAt: timestamp.as(workspaceMembers.acceptedAt.name)
|
|
263
|
+
}).from(organizationMembers).where(and(
|
|
264
|
+
eq(organizationMembers.organizationId, invitation.organizationId),
|
|
265
|
+
eq(organizationMembers.userId, currentUser.id),
|
|
266
|
+
eligibleInvitation
|
|
267
|
+
))).onConflictDoNothing({ target: [workspaceMembers.workspaceId, workspaceMembers.organizationMemberId] }).returning(),
|
|
268
|
+
db.update(workspaceInvitations).set({ status: "accepted", acceptedAt: now }).where(eligible).returning()
|
|
269
|
+
]);
|
|
270
|
+
if (!accepted.length) {
|
|
271
|
+
return { succeeded: false, status: 409, error: "The invitation changed. Reload it before continuing." };
|
|
220
272
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await db.insert(workspaceMembers).values({
|
|
273
|
+
if (insertedMembers.length && memberSyncSeam?.add) {
|
|
274
|
+
fireSync(() => memberSyncSeam.add({
|
|
224
275
|
workspaceId: invitation.workspaceId,
|
|
225
|
-
organizationMemberId: orgMember.id,
|
|
226
276
|
userId: currentUser.id,
|
|
227
|
-
role: invitation.permissions
|
|
228
|
-
|
|
229
|
-
// members-api nulls the token on accept to release the unique slot; the
|
|
230
|
-
// invitations flow keeps it (its `inv_` tokens never collide), so the
|
|
231
|
-
// materialized row carries the originating token for audit.
|
|
232
|
-
inviteToken: invitation.token,
|
|
233
|
-
invitedAt: now,
|
|
234
|
-
acceptedAt: now
|
|
235
|
-
});
|
|
236
|
-
if (memberSyncSeam?.add) {
|
|
237
|
-
const userId = currentUser.id;
|
|
238
|
-
const role = invitation.permissions;
|
|
239
|
-
fireSync(() => memberSyncSeam.add({ workspaceId: invitation.workspaceId, userId, role }));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const [accepted] = await db.update(workspaceInvitations).set({ status: "accepted", acceptedAt: now }).where(and(eq(workspaceInvitations.id, invitation.id), eq(workspaceInvitations.status, "pending"))).returning();
|
|
243
|
-
if (!accepted) {
|
|
244
|
-
await db.delete(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, invitation.workspaceId), eq(workspaceMembers.userId, currentUser.id)));
|
|
245
|
-
return { succeeded: false, status: 409, error: "The invitation changed. Reload it before continuing." };
|
|
277
|
+
role: invitation.permissions
|
|
278
|
+
}));
|
|
246
279
|
}
|
|
247
280
|
return { succeeded: true, value: { workspaceId: invitation.workspaceId } };
|
|
248
281
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/teams/invitations-api.ts"],"sourcesContent":["/**\n * Framework-neutral email-invitation lifecycle for the teams module: create /\n * list / resend / revoke / preview / accept over the dedicated\n * `workspace_invitation` table (from `createWorkspaceInvitationTable`). Lifted out\n * of any one app's route file — each app binds this once to its own db/tables/\n * access and mounts the handlers in its own routes with its own auth.\n *\n * Unlike `members-api` (whose \"invite\" is a pending workspaceMember row, no\n * email), this models the rich lifecycle: a dedicated invitation row with status,\n * 7-day expiry, emailStatus, resend, and revoke. On accept it materializes the\n * `organizationMembers` + `workspaceMembers` rows, so `members-api.listMembers`\n * still surfaces accepted members; pending invites live only here.\n *\n * Handlers return a discriminated `InvitationOutcome` (not a `Response`) so the\n * route adapter maps `{ status }` to its own framework's response. Three seams:\n * - `sendInvitationEmail` (REQUIRED) — the app's mail transport. Returns a\n * typed outcome; a failed send never blocks invitation creation (emailStatus\n * is recorded as 'failed' and the invite link is still returned).\n * - `enforceSeat` (OPTIONAL) — billing/seat-limit gate, called at create time\n * only when the invite would consume a NEW seat. Reused from members-api.\n * - `memberSyncSeam.add` (OPTIONAL) — fire-and-forget propagation of the new\n * member to a sandbox/external system on accept. Fail-soft by contract.\n *\n * Imports `drizzle-orm`, so this is a subpath, never re-exported from root.\n */\n\nimport { and, eq, lte, sql } from 'drizzle-orm'\nimport {\n type InvitationEmailStatus,\n type InvitationPermission,\n type InvitationStatus,\n generateInvitationToken,\n getInvitationExpiresAt,\n inviteUrlForToken,\n INVITATION_EMAIL_HINT,\n isInvitationEmail,\n normalizeInvitationEmail,\n parseInvitationPermission,\n} from './invitations'\nimport { type WorkspaceRole, hasWorkspaceRole } from './roles'\nimport { type EnforceSeatSeam, type MemberSyncSeam, SeatLimitError } from './members-api'\nimport type { TeamDatabase, WorkspaceAccessApi } from './drizzle/access'\nimport type { TeamParentTable, TeamTables } from './drizzle/schema'\nimport type { WorkspaceInvitationRow, WorkspaceInvitationTables } from './drizzle/invitations-schema'\n\n// Re-export the seams an adopter wires, so a consumer can import everything the\n// invitations API needs from this one subpath.\nexport { SeatLimitError } from './members-api'\nexport type { EnforceSeatSeam, MemberSyncSeam } from './members-api'\n\n/** The app's mail transport. Returns a typed outcome; never throws to the API. */\nexport interface SendInvitationEmailInput {\n to: string\n workspaceName: string\n inviterEmail: string\n permission: InvitationPermission\n inviteUrl: string\n expiresAt: Date\n}\n/** Represent the outcome of sending an invitation email with success status and optional error message */\nexport type SendInvitationEmailResult = { succeeded: true } | { succeeded: false; error: string }\n/** Resolve sending an invitation email and return the result asynchronously */\nexport interface SendInvitationEmailSeam {\n (input: SendInvitationEmailInput): Promise<SendInvitationEmailResult>\n}\n\n/** The product's user table, narrowed to the columns the queries read. */\nexport interface InvitationUserTable {\n id: any\n email: any\n emailVerified: any\n}\n\n/** The product's workspace table, narrowed to the columns the queries read. */\nexport interface InvitationWorkspaceTable {\n id: any\n name: any\n}\n\n/** Define configuration options required to manage workspace invitations and related data sources */\nexport interface InvitationsApiOptions {\n db: TeamDatabase\n tables: TeamTables\n /** The invitation table from `createWorkspaceInvitationTable`. */\n invitationsTable: WorkspaceInvitationTables\n userTable: TeamParentTable & InvitationUserTable\n workspaceTable: TeamParentTable & InvitationWorkspaceTable\n access: Pick<WorkspaceAccessApi, 'getWorkspaceAccess'>\n /** REQUIRED — the app's mail transport (e.g. Resend + `inviteEmail` from `./email`). */\n sendInvitationEmail: SendInvitationEmailSeam\n /** OPTIONAL — seat-limit gate at create time; mirrors members-api. */\n enforceSeat?: EnforceSeatSeam\n /** OPTIONAL — fire-and-forget member propagation on accept. */\n memberSyncSeam?: Pick<MemberSyncSeam, 'add'>\n /** Inviter display fallback when the inviter's email is unknown. */\n productDisplayName?: string\n}\n\n/** Represent a workspace invitation with details about inviter, permissions, status, and timestamps */\nexport interface WorkspaceInvitationView {\n id: string\n workspaceId: string\n organizationId: string\n email: string\n invitedByUserId: string\n inviterEmail: string | null\n permissions: InvitationPermission\n token: string\n status: InvitationStatus\n emailStatus: InvitationEmailStatus\n expiresAt: Date\n createdAt: Date\n acceptedAt: Date | null\n revokedAt: Date | null\n lastSentAt: Date | null\n}\n\n/** Describe the structure of an invitation preview with workspace, email, permissions, status, and expiration details */\nexport interface InvitationPreview {\n workspaceId: string\n workspaceName: string\n email: string\n inviterEmail: string | null\n permissions: InvitationPermission\n status: InvitationStatus\n expiresAt: Date\n}\n\n/** Resolve the result of an invitation as success with a value or failure with status and error details */\nexport type InvitationOutcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; status: number; error: string }\n\n/**\n * Build the invitations API bound to one product's db/tables/access/seams.\n * Returns the six lifecycle handlers an app mounts in its routes.\n */\nexport function createInvitationsApi(opts: InvitationsApiOptions) {\n const { db, tables, access, sendInvitationEmail, enforceSeat, memberSyncSeam } = opts\n const { organizationMembers, workspaceMembers } = tables\n const { workspaceInvitations } = opts.invitationsTable\n const users = opts.userTable\n const workspaces = opts.workspaceTable\n const productDisplayName = opts.productDisplayName ?? 'A workspace admin'\n\n function fireSync(op: () => Promise<void> | void) {\n try {\n Promise.resolve(op()).catch(() => {})\n } catch {\n // seam threw synchronously — swallow; sync is best-effort by contract.\n }\n }\n\n async function createInvitation(input: {\n workspaceId: string\n email: string\n permissions: string | undefined\n invitedByUserId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView; inviteUrl: string; emailError?: string }>> {\n const now = input.now ?? new Date()\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.invitedByUserId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n const permissions = parseInvitationPermission(input.permissions ?? 'editor')\n if (!permissions) return { succeeded: false, status: 400, error: 'Invalid permissions' }\n if (!hasWorkspaceRole(accessRow.role, permissions)) {\n return { succeeded: false, status: 403, error: 'Cannot assign higher permissions than your own' }\n }\n\n const email = normalizeInvitationEmail(input.email)\n if (!email) return { succeeded: false, status: 400, error: 'Missing email' }\n if (!isInvitationEmail(email)) return { succeeded: false, status: 400, error: INVITATION_EMAIL_HINT }\n\n const organizationId = accessRow.organization.id\n const workspaceName = (accessRow.workspace as { name: string }).name\n\n await expirePendingInvitations(input.workspaceId, email, now)\n\n const existingPending = await getPendingInvitation(input.workspaceId, email)\n if (existingPending) return { succeeded: false, status: 409, error: 'A pending invitation already exists for this email' }\n\n const accountCheck = await checkExistingAccountAccess(organizationId, input.workspaceId, email, input.invitedByUserId)\n if (!accountCheck.succeeded) return accountCheck\n\n // Seat gate: only a genuinely NEW seat. The checks above reject re-invites,\n // existing workspace members, and org admins; an existing plain org member is\n // already billable (no new seat), so skip the gate for them — mirrors\n // members-api. The pending row is not inserted yet, so the seam's count\n // reflects current usage and denies the invite that would push OVER the limit\n // (at invite time, not accept time).\n if (enforceSeat && !accountCheck.isExistingOrgMember) {\n try {\n await enforceSeat({ actorId: input.invitedByUserId, organizationId })\n } catch (err) {\n if (err instanceof SeatLimitError) return { succeeded: false, status: err.status, error: err.message }\n throw err\n }\n }\n\n const token = generateInvitationToken()\n const expiresAt = getInvitationExpiresAt(now)\n\n const [created] = await db.insert(workspaceInvitations).values({\n workspaceId: input.workspaceId,\n organizationId,\n email,\n invitedByUserId: input.invitedByUserId,\n permissions,\n token,\n status: 'pending',\n emailStatus: 'not_sent',\n expiresAt,\n createdAt: now,\n }).returning()\n\n const inviteUrl = inviteUrlForToken(input.origin, token)\n const emailResult = await sendInvitationEmail({\n to: email,\n workspaceName,\n inviterEmail: accountCheck.inviterEmail ?? productDisplayName,\n permission: permissions,\n inviteUrl,\n expiresAt,\n })\n\n const emailStatus: InvitationEmailStatus = emailResult.succeeded ? 'sent' : 'failed'\n const [updated] = await db.update(workspaceInvitations)\n .set({ emailStatus, lastSentAt: emailResult.succeeded ? now : null })\n .where(eq(workspaceInvitations.id, created!.id))\n .returning()\n\n return {\n succeeded: true,\n value: {\n invitation: toInvitationView(updated!, accountCheck.inviterEmail),\n inviteUrl,\n emailError: emailResult.succeeded ? undefined : emailResult.error,\n },\n }\n }\n\n async function listInvitations(input: {\n workspaceId: string\n userId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitations: Array<WorkspaceInvitationView & { inviteUrl: string }> }>> {\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n await expirePendingInvitations(input.workspaceId, undefined, input.now ?? new Date())\n\n const rows = await db\n .select({ invitation: workspaceInvitations, inviterEmail: users.email })\n .from(workspaceInvitations)\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.workspaceId, input.workspaceId))\n .orderBy(sql`${workspaceInvitations.createdAt} desc`)\n\n return {\n succeeded: true,\n value: {\n invitations: rows.map((row: { invitation: WorkspaceInvitationRow; inviterEmail: string | null }) => ({\n ...toInvitationView(row.invitation, row.inviterEmail),\n inviteUrl: inviteUrlForToken(input.origin, row.invitation.token),\n })),\n },\n }\n }\n\n async function resendInvitation(input: {\n invitationId: string\n userId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView; inviteUrl: string; emailError?: string }>> {\n const row = await getInvitationById(input.invitationId)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n const accessRow = await access.getWorkspaceAccess(row.invitation.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n const now = input.now ?? new Date()\n if (row.invitation.status !== 'pending') {\n return { succeeded: false, status: 409, error: 'Only pending invitations can be resent' }\n }\n if (row.invitation.expiresAt <= now) {\n await markInvitationExpired(row.invitation.id)\n return { succeeded: false, status: 409, error: 'Invitation has expired' }\n }\n\n const workspaceName = (accessRow.workspace as { name: string }).name\n const inviteUrl = inviteUrlForToken(input.origin, row.invitation.token)\n const emailResult = await sendInvitationEmail({\n to: row.invitation.email,\n workspaceName,\n inviterEmail: row.inviterEmail ?? productDisplayName,\n permission: row.invitation.permissions,\n inviteUrl,\n expiresAt: row.invitation.expiresAt,\n })\n\n const [updated] = await db.update(workspaceInvitations)\n .set({\n emailStatus: emailResult.succeeded ? 'sent' : 'failed',\n lastSentAt: emailResult.succeeded ? now : row.invitation.lastSentAt,\n })\n .where(eq(workspaceInvitations.id, row.invitation.id))\n .returning()\n\n return {\n succeeded: true,\n value: {\n invitation: toInvitationView(updated!, row.inviterEmail),\n inviteUrl,\n emailError: emailResult.succeeded ? undefined : emailResult.error,\n },\n }\n }\n\n async function revokeInvitation(input: {\n invitationId: string\n userId: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView }>> {\n const row = await getInvitationById(input.invitationId)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n const accessRow = await access.getWorkspaceAccess(row.invitation.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n if (row.invitation.status !== 'pending') {\n return { succeeded: false, status: 409, error: 'Only pending invitations can be revoked' }\n }\n\n const [updated] = await db.update(workspaceInvitations)\n .set({ status: 'revoked', revokedAt: input.now ?? new Date() })\n .where(and(eq(workspaceInvitations.id, row.invitation.id), eq(workspaceInvitations.status, 'pending')))\n .returning()\n\n // The status read above is advisory. A concurrent accept may win between\n // that read and this update, so the write must be compare-and-set.\n if (!updated) return { succeeded: false, status: 409, error: 'The invitation changed. Reload it before continuing.' }\n\n return { succeeded: true, value: { invitation: toInvitationView(updated!, row.inviterEmail) } }\n }\n\n async function getPreview(token: string, now: Date = new Date()): Promise<InvitationOutcome<InvitationPreview>> {\n const row = await getInvitationByToken(token)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n let invitation = row.invitation\n if (invitation.status === 'pending' && invitation.expiresAt <= now) {\n invitation = await markInvitationExpired(invitation.id)\n }\n\n return {\n succeeded: true,\n value: {\n workspaceId: invitation.workspaceId,\n workspaceName: row.workspaceName,\n email: invitation.email,\n inviterEmail: row.inviterEmail,\n permissions: invitation.permissions,\n status: invitation.status,\n expiresAt: invitation.expiresAt,\n },\n }\n }\n\n async function acceptInvitation(input: {\n token: string\n userId: string\n now?: Date\n }): Promise<InvitationOutcome<{ workspaceId: string }>> {\n const now = input.now ?? new Date()\n const row = await getInvitationByToken(input.token)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n let invitation = row.invitation\n if (invitation.status === 'pending' && invitation.expiresAt <= now) {\n invitation = await markInvitationExpired(invitation.id)\n }\n\n const [currentUser] = await db\n .select({ id: users.id, email: users.email, emailVerified: users.emailVerified })\n .from(users)\n .where(eq(users.id, input.userId))\n .limit(1)\n if (!currentUser) return { succeeded: false, status: 401, error: 'Authentication required' }\n\n if (normalizeInvitationEmail(currentUser.email) !== invitation.email) {\n return { succeeded: false, status: 403, error: 'Sign in with the invited email address to accept this invitation' }\n }\n\n const collisionCheck = await findUsersByNormalizedEmail(invitation.email)\n if (collisionCheck.length > 1) {\n return {\n succeeded: false,\n status: 409,\n error: 'Multiple accounts use this email. Contact support to reconcile before accepting this invitation.',\n }\n }\n\n let orgMember = await getOrganizationMember(invitation.organizationId, currentUser.id)\n if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {\n return { succeeded: false, status: 409, error: 'This user already has account-level access.' }\n }\n\n if (invitation.status === 'accepted') {\n const existing = orgMember ? await getWorkspaceMemberByOrganizationMember(invitation.workspaceId, orgMember.id) : null\n if (existing) return { succeeded: true, value: { workspaceId: invitation.workspaceId } }\n return { succeeded: false, status: 409, error: 'Invitation has already been accepted' }\n }\n if (invitation.status === 'expired') return { succeeded: false, status: 409, error: 'Invitation has expired' }\n if (invitation.status === 'revoked') return { succeeded: false, status: 409, error: 'Invitation has been revoked' }\n\n if (!orgMember) {\n const [inserted] = await db.insert(organizationMembers).values({\n organizationId: invitation.organizationId,\n userId: currentUser.id,\n role: 'member',\n createdAt: now,\n updatedAt: now,\n }).returning()\n if (!inserted) return { succeeded: false, status: 500, error: 'Failed to create organization membership' }\n orgMember = inserted\n }\n\n const existingMember = await getWorkspaceMemberByOrganizationMember(invitation.workspaceId, orgMember.id)\n if (!existingMember) {\n await db.insert(workspaceMembers).values({\n workspaceId: invitation.workspaceId,\n organizationMemberId: orgMember.id,\n userId: currentUser.id,\n role: invitation.permissions,\n inviteEmail: invitation.email,\n // members-api nulls the token on accept to release the unique slot; the\n // invitations flow keeps it (its `inv_` tokens never collide), so the\n // materialized row carries the originating token for audit.\n inviteToken: invitation.token,\n invitedAt: now,\n acceptedAt: now,\n })\n if (memberSyncSeam?.add) {\n const userId = currentUser.id\n const role = invitation.permissions as WorkspaceRole\n fireSync(() => memberSyncSeam.add!({ workspaceId: invitation.workspaceId, userId, role }))\n }\n }\n\n const [accepted] = await db.update(workspaceInvitations)\n .set({ status: 'accepted', acceptedAt: now })\n .where(and(eq(workspaceInvitations.id, invitation.id), eq(workspaceInvitations.status, 'pending')))\n .returning()\n\n if (!accepted) {\n // Membership was materialized before the final compare-and-set. Remove\n // only the row created by this attempt before reporting the conflict.\n await db.delete(workspaceMembers)\n .where(and(eq(workspaceMembers.workspaceId, invitation.workspaceId), eq(workspaceMembers.userId, currentUser.id)))\n return { succeeded: false, status: 409, error: 'The invitation changed. Reload it before continuing.' }\n }\n\n return { succeeded: true, value: { workspaceId: invitation.workspaceId } }\n }\n\n // ── internal query helpers ──\n\n async function expirePendingInvitations(workspaceId: string, email: string | undefined, now: Date) {\n const conditions = [\n eq(workspaceInvitations.workspaceId, workspaceId),\n eq(workspaceInvitations.status, 'pending' as const),\n lte(workspaceInvitations.expiresAt, now),\n ]\n if (email) conditions.push(eq(workspaceInvitations.email, email))\n await db.update(workspaceInvitations).set({ status: 'expired' }).where(and(...conditions))\n }\n\n async function getPendingInvitation(workspaceId: string, email: string) {\n const [invitation] = await db\n .select({ id: workspaceInvitations.id })\n .from(workspaceInvitations)\n .where(and(\n eq(workspaceInvitations.workspaceId, workspaceId),\n eq(workspaceInvitations.email, email),\n eq(workspaceInvitations.status, 'pending' as const),\n ))\n .limit(1)\n return invitation ?? null\n }\n\n async function checkExistingAccountAccess(\n organizationId: string,\n workspaceId: string,\n email: string,\n invitedByUserId: string,\n ): Promise<\n | { succeeded: false; status: number; error: string }\n | { succeeded: true; inviterEmail: string | null; isExistingOrgMember: boolean }\n > {\n const matchingUsers = await findUsersByNormalizedEmail(email)\n const [inviter] = await db\n .select({ email: users.email })\n .from(users)\n .where(eq(users.id, invitedByUserId))\n .limit(1)\n\n if (matchingUsers.length > 1) {\n return {\n succeeded: false,\n status: 409,\n error: 'Multiple accounts use this email. Contact support to reconcile before inviting this user.',\n }\n }\n const existingUser = matchingUsers[0]\n if (!existingUser) return { succeeded: true, inviterEmail: inviter?.email ?? null, isExistingOrgMember: false }\n\n const orgMember = await getOrganizationMember(organizationId, existingUser.id)\n if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {\n return { succeeded: false, status: 409, error: 'This user already has account-level access.' }\n }\n if (orgMember) {\n const workspaceMember = await getWorkspaceMemberByOrganizationMember(workspaceId, orgMember.id)\n if (workspaceMember) return { succeeded: false, status: 409, error: 'User is already a workspace member' }\n }\n\n return { succeeded: true, inviterEmail: inviter?.email ?? null, isExistingOrgMember: Boolean(orgMember) }\n }\n\n async function findUsersByNormalizedEmail(email: string) {\n return db\n .select({ id: users.id, email: users.email, emailVerified: users.emailVerified })\n .from(users)\n .where(sql`lower(${users.email}) = ${email}`)\n }\n\n async function getOrganizationMember(organizationId: string, userId: string) {\n const [member] = await db\n .select()\n .from(organizationMembers)\n .where(and(eq(organizationMembers.organizationId, organizationId), eq(organizationMembers.userId, userId)))\n .limit(1)\n return member ?? null\n }\n\n async function getWorkspaceMemberByOrganizationMember(workspaceId: string, organizationMemberId: string) {\n const [member] = await db\n .select({ id: workspaceMembers.id })\n .from(workspaceMembers)\n .where(and(\n eq(workspaceMembers.workspaceId, workspaceId),\n eq(workspaceMembers.organizationMemberId, organizationMemberId),\n ))\n .limit(1)\n return member ?? null\n }\n\n async function getInvitationById(invitationId: string) {\n const [row] = await db\n .select({ invitation: workspaceInvitations, inviterEmail: users.email })\n .from(workspaceInvitations)\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.id, invitationId))\n .limit(1)\n return row ?? null\n }\n\n async function getInvitationByToken(token: string) {\n const [row] = await db\n .select({\n invitation: workspaceInvitations,\n workspaceName: workspaces.name,\n inviterEmail: users.email,\n })\n .from(workspaceInvitations)\n .innerJoin(workspaces, eq(workspaces.id, workspaceInvitations.workspaceId))\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.token, token))\n .limit(1)\n return row ?? null\n }\n\n async function markInvitationExpired(invitationId: string) {\n const [updated] = await db.update(workspaceInvitations)\n .set({ status: 'expired' })\n .where(eq(workspaceInvitations.id, invitationId))\n .returning()\n return updated!\n }\n\n function toInvitationView(invitation: WorkspaceInvitationRow, inviterEmail: string | null): WorkspaceInvitationView {\n return {\n id: invitation.id,\n workspaceId: invitation.workspaceId,\n organizationId: invitation.organizationId,\n email: invitation.email,\n invitedByUserId: invitation.invitedByUserId,\n inviterEmail,\n permissions: invitation.permissions,\n token: invitation.token,\n status: invitation.status,\n emailStatus: invitation.emailStatus,\n expiresAt: invitation.expiresAt,\n createdAt: invitation.createdAt,\n acceptedAt: invitation.acceptedAt,\n revokedAt: invitation.revokedAt,\n lastSentAt: invitation.lastSentAt,\n }\n }\n\n return { createInvitation, listInvitations, resendInvitation, revokeInvitation, getPreview, acceptInvitation }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/teams/invitations-api.ts"],"sourcesContent":["/**\n * Framework-neutral email-invitation lifecycle for the teams module: create /\n * list / resend / revoke / preview / accept over the dedicated\n * `workspace_invitation` table (from `createWorkspaceInvitationTable`). Lifted out\n * of any one app's route file — each app binds this once to its own db/tables/\n * access and mounts the handlers in its own routes with its own auth.\n *\n * Unlike `members-api` (whose \"invite\" is a pending workspaceMember row, no\n * email), this models the rich lifecycle: a dedicated invitation row with status,\n * 7-day expiry, emailStatus, resend, and revoke. On accept it materializes the\n * `organizationMembers` + `workspaceMembers` rows, so `members-api.listMembers`\n * still surfaces accepted members; pending invites live only here.\n *\n * Handlers return a discriminated `InvitationOutcome` (not a `Response`) so the\n * route adapter maps `{ status }` to its own framework's response. Three seams:\n * - `sendInvitationEmail` (REQUIRED) — the app's mail transport. Returns a\n * typed outcome; a failed send never blocks invitation creation (emailStatus\n * is recorded as 'failed' and the invite link is still returned).\n * - `enforceSeat` (OPTIONAL) — billing/seat-limit gate, called at create time\n * only when the invite would consume a NEW seat. Reused from members-api.\n * - `memberSyncSeam.add` (OPTIONAL) — fire-and-forget propagation of the new\n * member to a sandbox/external system on accept. Fail-soft by contract.\n *\n * Imports `drizzle-orm`, so this is a subpath, never re-exported from root.\n */\n\nimport { and, eq, lte, sql } from 'drizzle-orm'\nimport {\n type InvitationEmailStatus,\n type InvitationPermission,\n type InvitationStatus,\n generateInvitationToken,\n getInvitationExpiresAt,\n inviteUrlForToken,\n INVITATION_EMAIL_HINT,\n isInvitationEmail,\n normalizeInvitationEmail,\n parseInvitationPermission,\n} from './invitations'\nimport { type WorkspaceRole, hasWorkspaceRole } from './roles'\nimport { type EnforceSeatSeam, type MemberSyncSeam, SeatLimitError } from './members-api'\nimport type { TeamDatabase, WorkspaceAccessReader } from './drizzle/access'\nimport type { TeamParentTable, TeamTables } from './drizzle/schema'\nimport type { WorkspaceInvitationRow, WorkspaceInvitationTables } from './drizzle/invitations-schema'\n\n// Re-export the seams an adopter wires, so a consumer can import everything the\n// invitations API needs from this one subpath.\nexport { SeatLimitError } from './members-api'\nexport type { EnforceSeatSeam, MemberSyncSeam } from './members-api'\n\n/** The app's mail transport. Returns a typed outcome; never throws to the API. */\nexport interface SendInvitationEmailInput {\n to: string\n workspaceName: string\n inviterEmail: string\n permission: InvitationPermission\n inviteUrl: string\n expiresAt: Date\n}\n/** Represent the outcome of sending an invitation email with success status and optional error message */\nexport type SendInvitationEmailResult = { succeeded: true } | { succeeded: false; error: string }\n/** Resolve sending an invitation email and return the result asynchronously */\nexport interface SendInvitationEmailSeam {\n (input: SendInvitationEmailInput): Promise<SendInvitationEmailResult>\n}\n\n/** The product's user table, narrowed to the columns the queries read. */\nexport interface InvitationUserTable {\n id: any\n email: any\n emailVerified: any\n}\n\n/** The product's workspace table, narrowed to the columns the queries read. */\nexport interface InvitationWorkspaceTable {\n id: any\n name: any\n organizationId: any\n}\n\n/** Define configuration options required to manage workspace invitations and related data sources */\nexport interface InvitationsApiOptions {\n db: TeamDatabase & { batch?: (statements: [any, ...any[]]) => Promise<any[]> }\n tables: TeamTables\n /** The invitation table from `createWorkspaceInvitationTable`. */\n invitationsTable: WorkspaceInvitationTables\n userTable: TeamParentTable & InvitationUserTable\n workspaceTable: TeamParentTable & InvitationWorkspaceTable\n access: WorkspaceAccessReader\n /** REQUIRED — the app's mail transport (e.g. Resend + `inviteEmail` from `./email`). */\n sendInvitationEmail: SendInvitationEmailSeam\n /** OPTIONAL — seat-limit gate at create time; mirrors members-api. */\n enforceSeat?: EnforceSeatSeam\n /** OPTIONAL — fire-and-forget member propagation on accept. */\n memberSyncSeam?: Pick<MemberSyncSeam, 'add'>\n /** Inviter display fallback when the inviter's email is unknown. */\n productDisplayName?: string\n}\n\n/** Represent a workspace invitation with details about inviter, permissions, status, and timestamps */\nexport interface WorkspaceInvitationView {\n id: string\n workspaceId: string\n organizationId: string\n email: string\n invitedByUserId: string\n inviterEmail: string | null\n permissions: InvitationPermission\n token: string\n status: InvitationStatus\n emailStatus: InvitationEmailStatus\n expiresAt: Date\n createdAt: Date\n acceptedAt: Date | null\n revokedAt: Date | null\n lastSentAt: Date | null\n}\n\n/** Describe the structure of an invitation preview with workspace, email, permissions, status, and expiration details */\nexport interface InvitationPreview {\n workspaceId: string\n workspaceName: string\n email: string\n inviterEmail: string | null\n permissions: InvitationPermission\n status: InvitationStatus\n expiresAt: Date\n}\n\n/** Resolve the result of an invitation as success with a value or failure with status and error details */\nexport type InvitationOutcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; status: number; error: string }\n\n/**\n * Build the invitations API bound to one product's db/tables/access/seams.\n * Returns the six lifecycle handlers an app mounts in its routes.\n */\nexport function createInvitationsApi(opts: InvitationsApiOptions) {\n const { db, tables, access, sendInvitationEmail, enforceSeat, memberSyncSeam } = opts\n const { organizationMembers, workspaceMembers } = tables\n const { workspaceInvitations } = opts.invitationsTable\n const users = opts.userTable\n const workspaces = opts.workspaceTable\n const productDisplayName = opts.productDisplayName ?? 'A workspace admin'\n\n function fireSync(op: () => Promise<void> | void) {\n try {\n Promise.resolve(op()).catch(() => {})\n } catch {\n // seam threw synchronously — swallow; sync is best-effort by contract.\n }\n }\n\n async function createInvitation(input: {\n workspaceId: string\n email: string\n permissions: string | undefined\n invitedByUserId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView; inviteUrl: string; emailError?: string }>> {\n const now = input.now ?? new Date()\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.invitedByUserId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n const permissions = parseInvitationPermission(input.permissions ?? 'editor')\n if (!permissions) return { succeeded: false, status: 400, error: 'Invalid permissions' }\n if (!hasWorkspaceRole(accessRow.role, permissions)) {\n return { succeeded: false, status: 403, error: 'Cannot assign higher permissions than your own' }\n }\n\n const email = normalizeInvitationEmail(input.email)\n if (!email) return { succeeded: false, status: 400, error: 'Missing email' }\n if (!isInvitationEmail(email)) return { succeeded: false, status: 400, error: INVITATION_EMAIL_HINT }\n\n const organizationId = accessRow.organization.id\n const workspaceName = (accessRow.workspace as { name: string }).name\n\n await expirePendingInvitations(input.workspaceId, email, now)\n\n const existingPending = await getPendingInvitation(input.workspaceId, email)\n if (existingPending) return { succeeded: false, status: 409, error: 'A pending invitation already exists for this email' }\n\n const accountCheck = await checkExistingAccountAccess(organizationId, input.workspaceId, email, input.invitedByUserId)\n if (!accountCheck.succeeded) return accountCheck\n\n // Seat gate: only a genuinely NEW seat. The checks above reject re-invites,\n // existing workspace members, and org admins; an existing plain org member is\n // already billable (no new seat), so skip the gate for them — mirrors\n // members-api. The pending row is not inserted yet, so the seam's count\n // reflects current usage and denies the invite that would push OVER the limit\n // (at invite time, not accept time).\n if (enforceSeat && !accountCheck.isExistingOrgMember) {\n try {\n await enforceSeat({ actorId: input.invitedByUserId, organizationId })\n } catch (err) {\n if (err instanceof SeatLimitError) return { succeeded: false, status: err.status, error: err.message }\n throw err\n }\n }\n\n const token = generateInvitationToken()\n const expiresAt = getInvitationExpiresAt(now)\n\n const [created] = await db.insert(workspaceInvitations).values({\n workspaceId: input.workspaceId,\n organizationId,\n email,\n invitedByUserId: input.invitedByUserId,\n permissions,\n token,\n status: 'pending',\n emailStatus: 'not_sent',\n expiresAt,\n createdAt: now,\n }).returning()\n\n const inviteUrl = inviteUrlForToken(input.origin, token)\n const emailResult = await sendInvitationEmail({\n to: email,\n workspaceName,\n inviterEmail: accountCheck.inviterEmail ?? productDisplayName,\n permission: permissions,\n inviteUrl,\n expiresAt,\n })\n\n const emailStatus: InvitationEmailStatus = emailResult.succeeded ? 'sent' : 'failed'\n const [updated] = await db.update(workspaceInvitations)\n .set({ emailStatus, lastSentAt: emailResult.succeeded ? now : null })\n .where(eq(workspaceInvitations.id, created!.id))\n .returning()\n\n return {\n succeeded: true,\n value: {\n invitation: toInvitationView(updated!, accountCheck.inviterEmail),\n inviteUrl,\n emailError: emailResult.succeeded ? undefined : emailResult.error,\n },\n }\n }\n\n async function listInvitations(input: {\n workspaceId: string\n userId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitations: Array<WorkspaceInvitationView & { inviteUrl: string }> }>> {\n const accessRow = await access.getWorkspaceAccess(input.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n await expirePendingInvitations(input.workspaceId, undefined, input.now ?? new Date())\n\n const rows = await db\n .select({ invitation: workspaceInvitations, inviterEmail: users.email })\n .from(workspaceInvitations)\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.workspaceId, input.workspaceId))\n .orderBy(sql`${workspaceInvitations.createdAt} desc`)\n\n return {\n succeeded: true,\n value: {\n invitations: rows.map((row: { invitation: WorkspaceInvitationRow; inviterEmail: string | null }) => ({\n ...toInvitationView(row.invitation, row.inviterEmail),\n inviteUrl: inviteUrlForToken(input.origin, row.invitation.token),\n })),\n },\n }\n }\n\n async function resendInvitation(input: {\n invitationId: string\n userId: string\n origin: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView; inviteUrl: string; emailError?: string }>> {\n const row = await getInvitationById(input.invitationId)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n const accessRow = await access.getWorkspaceAccess(row.invitation.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n\n const now = input.now ?? new Date()\n if (row.invitation.status !== 'pending') {\n return { succeeded: false, status: 409, error: 'Only pending invitations can be resent' }\n }\n if (row.invitation.expiresAt <= now) {\n await markInvitationExpired(row.invitation.id)\n return { succeeded: false, status: 409, error: 'Invitation has expired' }\n }\n\n const workspaceName = (accessRow.workspace as { name: string }).name\n const inviteUrl = inviteUrlForToken(input.origin, row.invitation.token)\n const emailResult = await sendInvitationEmail({\n to: row.invitation.email,\n workspaceName,\n inviterEmail: row.inviterEmail ?? productDisplayName,\n permission: row.invitation.permissions,\n inviteUrl,\n expiresAt: row.invitation.expiresAt,\n })\n\n const [updated] = await db.update(workspaceInvitations)\n .set({\n emailStatus: emailResult.succeeded ? 'sent' : 'failed',\n lastSentAt: emailResult.succeeded ? now : row.invitation.lastSentAt,\n })\n .where(eq(workspaceInvitations.id, row.invitation.id))\n .returning()\n\n return {\n succeeded: true,\n value: {\n invitation: toInvitationView(updated!, row.inviterEmail),\n inviteUrl,\n emailError: emailResult.succeeded ? undefined : emailResult.error,\n },\n }\n }\n\n async function revokeInvitation(input: {\n invitationId: string\n userId: string\n now?: Date\n }): Promise<InvitationOutcome<{ invitation: WorkspaceInvitationView }>> {\n const row = await getInvitationById(input.invitationId)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n const accessRow = await access.getWorkspaceAccess(row.invitation.workspaceId, input.userId, 'admin')\n if (!accessRow) return { succeeded: false, status: 404, error: 'Workspace not found' }\n if (row.invitation.status !== 'pending') {\n return { succeeded: false, status: 409, error: 'Only pending invitations can be revoked' }\n }\n\n const [updated] = await db.update(workspaceInvitations)\n .set({ status: 'revoked', revokedAt: input.now ?? new Date() })\n .where(and(eq(workspaceInvitations.id, row.invitation.id), eq(workspaceInvitations.status, 'pending')))\n .returning()\n\n // The status read above is advisory. A concurrent accept may win between\n // that read and this update, so the write must be compare-and-set.\n if (!updated) return { succeeded: false, status: 409, error: 'The invitation changed. Reload it before continuing.' }\n\n return { succeeded: true, value: { invitation: toInvitationView(updated!, row.inviterEmail) } }\n }\n\n async function getPreview(token: string, now: Date = new Date()): Promise<InvitationOutcome<InvitationPreview>> {\n const row = await getInvitationByToken(token)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n let invitation = row.invitation\n if (invitation.status === 'pending' && invitation.expiresAt <= now) {\n invitation = await markInvitationExpired(invitation.id)\n }\n\n return {\n succeeded: true,\n value: {\n workspaceId: invitation.workspaceId,\n workspaceName: row.workspaceName,\n email: invitation.email,\n inviterEmail: row.inviterEmail,\n permissions: invitation.permissions,\n status: invitation.status,\n expiresAt: invitation.expiresAt,\n },\n }\n }\n\n async function acceptInvitation(input: {\n token: string\n userId: string\n now?: Date\n }): Promise<InvitationOutcome<{ workspaceId: string }>> {\n const now = input.now ?? new Date()\n const row = await getInvitationByToken(input.token)\n if (!row) return { succeeded: false, status: 404, error: 'Invitation not found' }\n\n const invitation = row.invitation\n\n const [currentUser] = await db\n .select({ id: users.id, email: users.email, emailVerified: users.emailVerified })\n .from(users)\n .where(eq(users.id, input.userId))\n .limit(1)\n if (!currentUser) return { succeeded: false, status: 401, error: 'Authentication required' }\n\n if (normalizeInvitationEmail(currentUser.email) !== invitation.email) {\n return { succeeded: false, status: 403, error: 'Sign in with the invited email address to accept this invitation' }\n }\n\n const collisionCheck = await findUsersByNormalizedEmail(invitation.email)\n if (collisionCheck.length > 1) {\n return {\n succeeded: false,\n status: 409,\n error: 'Multiple accounts use this email. Contact support to reconcile before accepting this invitation.',\n }\n }\n\n const orgMember = await getOrganizationMember(invitation.organizationId, currentUser.id)\n if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {\n return { succeeded: false, status: 409, error: 'This user already has account-level access.' }\n }\n\n if (invitation.status === 'accepted') {\n const existing = orgMember ? await getWorkspaceMemberByOrganizationMember(invitation.workspaceId, orgMember.id) : null\n if (existing) return { succeeded: true, value: { workspaceId: invitation.workspaceId } }\n return { succeeded: false, status: 409, error: 'Invitation has already been accepted' }\n }\n if (invitation.status === 'expired') return { succeeded: false, status: 409, error: 'Invitation has expired' }\n if (invitation.status === 'revoked') return { succeeded: false, status: 409, error: 'Invitation has been revoked' }\n\n if (invitation.expiresAt <= now) return { succeeded: false, status: 409, error: 'Invitation has expired' }\n if (!db.batch) return { succeeded: false, status: 500, error: 'Atomic invitation acceptance is unavailable' }\n\n // All three writes share this live fence, evaluated inside one atomic D1\n // batch. A workspace transfer or revoke between the reads and batch wins\n // cleanly: there is no membership to repair or runtime access to retract.\n const eligible = and(\n eq(workspaceInvitations.id, invitation.id),\n eq(workspaceInvitations.token, input.token),\n eq(workspaceInvitations.status, 'pending'),\n eq(workspaceInvitations.organizationId, invitation.organizationId),\n eq(workspaceInvitations.workspaceId, invitation.workspaceId),\n eq(workspaceInvitations.email, invitation.email),\n eq(workspaceInvitations.permissions, invitation.permissions),\n sql`${workspaceInvitations.expiresAt} > ${Math.floor(now.getTime() / 1000)}`,\n sql`exists (select 1 from ${workspaces} where ${workspaces.id} = ${workspaceInvitations.workspaceId}\n and ${workspaces.organizationId} = ${invitation.organizationId})`,\n sql`exists (select 1 from ${users} where ${users.id} = ${currentUser.id}\n and lower(trim(${users.email})) = ${invitation.email}\n and ${users.emailVerified} = ${currentUser.emailVerified ? 1 : 0})`,\n sql`(select count(*) from ${users} where lower(trim(${users.email})) = ${invitation.email}) = 1`,\n sql`not exists (select 1 from ${workspaceMembers} inner join ${organizationMembers}\n on ${organizationMembers.id} = ${workspaceMembers.organizationMemberId}\n where ${workspaceMembers.workspaceId} = ${invitation.workspaceId}\n and ${organizationMembers.organizationId} = ${invitation.organizationId}\n and ${organizationMembers.userId} = ${currentUser.id}\n and ${workspaceMembers.acceptedAt} is null)`,\n sql`not exists (select 1 from ${workspaceMembers} where ${workspaceMembers.inviteToken} = ${invitation.token}\n and (${workspaceMembers.workspaceId} != ${invitation.workspaceId}\n or ${workspaceMembers.organizationMemberId} is null\n or ${workspaceMembers.organizationMemberId} not in (select ${organizationMembers.id} from ${organizationMembers}\n where ${organizationMembers.organizationId} = ${invitation.organizationId}\n and ${organizationMembers.userId} = ${currentUser.id})))`,\n sql`not exists (select 1 from ${organizationMembers}\n where ${organizationMembers.organizationId} = ${invitation.organizationId}\n and ${organizationMembers.userId} = ${currentUser.id}\n and ${organizationMembers.role} in ('owner', 'admin'))`,\n )\n const eligibleInvitation = sql`exists (select 1 from ${workspaceInvitations} where ${eligible})`\n const timestamp = sql`${Math.floor(now.getTime() / 1000)}`\n const [, insertedMembers, accepted] = await db.batch([\n db.insert(organizationMembers).select(db.select({\n id: sql`lower(hex(randomblob(16)))`.as(organizationMembers.id.name),\n organizationId: sql`${invitation.organizationId}`.as(organizationMembers.organizationId.name),\n userId: sql`${currentUser.id}`.as(organizationMembers.userId.name),\n role: sql`'member'`.as(organizationMembers.role.name),\n createdAt: timestamp.as(organizationMembers.createdAt.name),\n updatedAt: timestamp.as(organizationMembers.updatedAt.name),\n }).from(workspaceInvitations).where(eligible)).onConflictDoNothing({ target: [organizationMembers.organizationId, organizationMembers.userId] }).returning(),\n db.insert(workspaceMembers).select(db.select({\n id: sql`lower(hex(randomblob(16)))`.as(workspaceMembers.id.name),\n workspaceId: sql`${invitation.workspaceId}`.as(workspaceMembers.workspaceId.name),\n organizationMemberId: organizationMembers.id,\n userId: sql`${currentUser.id}`.as(workspaceMembers.userId.name),\n role: sql`${invitation.permissions}`.as(workspaceMembers.role.name),\n invitedBy: sql`null`.as(workspaceMembers.invitedBy.name),\n inviteEmail: sql`${invitation.email}`.as(workspaceMembers.inviteEmail.name),\n inviteToken: sql`${invitation.token}`.as(workspaceMembers.inviteToken.name),\n invitedAt: timestamp.as(workspaceMembers.invitedAt.name),\n acceptedAt: timestamp.as(workspaceMembers.acceptedAt.name),\n }).from(organizationMembers).where(and(\n eq(organizationMembers.organizationId, invitation.organizationId),\n eq(organizationMembers.userId, currentUser.id),\n eligibleInvitation,\n ))).onConflictDoNothing({ target: [workspaceMembers.workspaceId, workspaceMembers.organizationMemberId] }).returning(),\n db.update(workspaceInvitations)\n .set({ status: 'accepted', acceptedAt: now })\n .where(eligible)\n .returning(),\n ])\n if (!accepted.length) {\n return { succeeded: false, status: 409, error: 'The invitation changed. Reload it before continuing.' }\n }\n if (insertedMembers.length && memberSyncSeam?.add) {\n fireSync(() => memberSyncSeam.add!({\n workspaceId: invitation.workspaceId,\n userId: currentUser.id,\n role: invitation.permissions as WorkspaceRole,\n }))\n }\n\n return { succeeded: true, value: { workspaceId: invitation.workspaceId } }\n }\n\n // ── internal query helpers ──\n\n async function expirePendingInvitations(workspaceId: string, email: string | undefined, now: Date) {\n const conditions = [\n eq(workspaceInvitations.workspaceId, workspaceId),\n eq(workspaceInvitations.status, 'pending' as const),\n lte(workspaceInvitations.expiresAt, now),\n ]\n if (email) conditions.push(eq(workspaceInvitations.email, email))\n await db.update(workspaceInvitations).set({ status: 'expired' }).where(and(...conditions))\n }\n\n async function getPendingInvitation(workspaceId: string, email: string) {\n const [invitation] = await db\n .select({ id: workspaceInvitations.id })\n .from(workspaceInvitations)\n .where(and(\n eq(workspaceInvitations.workspaceId, workspaceId),\n eq(workspaceInvitations.email, email),\n eq(workspaceInvitations.status, 'pending' as const),\n ))\n .limit(1)\n return invitation ?? null\n }\n\n async function checkExistingAccountAccess(\n organizationId: string,\n workspaceId: string,\n email: string,\n invitedByUserId: string,\n ): Promise<\n | { succeeded: false; status: number; error: string }\n | { succeeded: true; inviterEmail: string | null; isExistingOrgMember: boolean }\n > {\n const matchingUsers = await findUsersByNormalizedEmail(email)\n const [inviter] = await db\n .select({ email: users.email })\n .from(users)\n .where(eq(users.id, invitedByUserId))\n .limit(1)\n\n if (matchingUsers.length > 1) {\n return {\n succeeded: false,\n status: 409,\n error: 'Multiple accounts use this email. Contact support to reconcile before inviting this user.',\n }\n }\n const existingUser = matchingUsers[0]\n if (!existingUser) return { succeeded: true, inviterEmail: inviter?.email ?? null, isExistingOrgMember: false }\n\n const orgMember = await getOrganizationMember(organizationId, existingUser.id)\n if (orgMember?.role === 'owner' || orgMember?.role === 'admin') {\n return { succeeded: false, status: 409, error: 'This user already has account-level access.' }\n }\n if (orgMember) {\n const workspaceMember = await getWorkspaceMemberByOrganizationMember(workspaceId, orgMember.id)\n if (workspaceMember) return { succeeded: false, status: 409, error: 'User is already a workspace member' }\n }\n\n return { succeeded: true, inviterEmail: inviter?.email ?? null, isExistingOrgMember: Boolean(orgMember) }\n }\n\n async function findUsersByNormalizedEmail(email: string) {\n return db\n .select({ id: users.id, email: users.email, emailVerified: users.emailVerified })\n .from(users)\n .where(sql`lower(${users.email}) = ${email}`)\n }\n\n async function getOrganizationMember(organizationId: string, userId: string) {\n const [member] = await db\n .select()\n .from(organizationMembers)\n .where(and(eq(organizationMembers.organizationId, organizationId), eq(organizationMembers.userId, userId)))\n .limit(1)\n return member ?? null\n }\n\n async function getWorkspaceMemberByOrganizationMember(workspaceId: string, organizationMemberId: string) {\n const [member] = await db\n .select({ id: workspaceMembers.id })\n .from(workspaceMembers)\n .where(and(\n eq(workspaceMembers.workspaceId, workspaceId),\n eq(workspaceMembers.organizationMemberId, organizationMemberId),\n ))\n .limit(1)\n return member ?? null\n }\n\n async function getInvitationById(invitationId: string) {\n const [row] = await db\n .select({ invitation: workspaceInvitations, inviterEmail: users.email })\n .from(workspaceInvitations)\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.id, invitationId))\n .limit(1)\n return row ?? null\n }\n\n async function getInvitationByToken(token: string) {\n const [row] = await db\n .select({\n invitation: workspaceInvitations,\n workspaceName: workspaces.name,\n inviterEmail: users.email,\n })\n .from(workspaceInvitations)\n .innerJoin(workspaces, eq(workspaces.id, workspaceInvitations.workspaceId))\n .leftJoin(users, eq(users.id, workspaceInvitations.invitedByUserId))\n .where(eq(workspaceInvitations.token, token))\n .limit(1)\n return row ?? null\n }\n\n async function markInvitationExpired(invitationId: string) {\n const [updated] = await db.update(workspaceInvitations)\n .set({ status: 'expired' })\n .where(eq(workspaceInvitations.id, invitationId))\n .returning()\n return updated!\n }\n\n function toInvitationView(invitation: WorkspaceInvitationRow, inviterEmail: string | null): WorkspaceInvitationView {\n return {\n id: invitation.id,\n workspaceId: invitation.workspaceId,\n organizationId: invitation.organizationId,\n email: invitation.email,\n invitedByUserId: invitation.invitedByUserId,\n inviterEmail,\n permissions: invitation.permissions,\n token: invitation.token,\n status: invitation.status,\n emailStatus: invitation.emailStatus,\n expiresAt: invitation.expiresAt,\n createdAt: invitation.createdAt,\n acceptedAt: invitation.acceptedAt,\n revokedAt: invitation.revokedAt,\n lastSentAt: invitation.lastSentAt,\n }\n }\n\n return { createInvitation, listInvitations, resendInvitation, revokeInvitation, getPreview, acceptInvitation }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Imports `drizzle-orm`, so this is a subpath, never re-exported from root.
|
|
21
21
|
*/
|
|
22
22
|
import { type WorkspaceRole } from './roles';
|
|
23
|
-
import type { TeamDatabase,
|
|
23
|
+
import type { TeamDatabase, WorkspaceAccessReader } from './drizzle/access';
|
|
24
24
|
import type { TeamParentTable, TeamTables } from './drizzle/schema';
|
|
25
25
|
/** The authenticated caller — apps resolve this from their own session layer. */
|
|
26
26
|
export interface MembersApiActor {
|
|
@@ -100,7 +100,7 @@ export interface MembersApiOptions {
|
|
|
100
100
|
/** The product's workspace table (FK target passed to createTeamTables). */
|
|
101
101
|
workspaceTable: TeamParentTable & WorkspaceLookupTable;
|
|
102
102
|
/** Workspace-access API from createWorkspaceAccess — RBAC stays one source. */
|
|
103
|
-
access:
|
|
103
|
+
access: WorkspaceAccessReader;
|
|
104
104
|
enforceSeat?: EnforceSeatSeam;
|
|
105
105
|
memberSyncSeam?: MemberSyncSeam;
|
|
106
106
|
}
|