From 02f24fc86851b99db633c52513d910ca8a6d6735 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Fri, 10 Jul 2026 15:40:25 -0400 Subject: [PATCH] fix(api): stop Trigger.dev deploy failing on @trycompai/auth/participation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API's Trigger.dev deploy bundles task notifiers with esbuild, which failed with "Could not resolve @trycompai/auth/participation". The auth package's dist is not built in the trigger deploy workflow (it builds db/email/integration- platform only), and the API's org-participation code — reachable from the task notifiers — imported that subpath, so esbuild had nothing to resolve. Mirror the app's approach for Trigger.dev-bundled code: use a dependency-free local copy of the tiny participation predicate (`org-participation-rule.ts`) instead of importing `@trycompai/auth`. This keeps the trigger bundle free of the auth package (which also pulls better-auth, unbundleable). A drift-guard spec asserts the mirror stays identical to `@trycompai/auth/participation`, and a jest moduleNameMapper resolves that subpath to source so the guard runs without needing the auth dist built. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XzEVwhhoFghng6SDMGecMq --- apps/api/package.json | 1 + .../task-item-assignment-notifier.service.ts | 2 +- .../src/utils/org-participation-rule.spec.ts | 56 +++++++++++++++++++ apps/api/src/utils/org-participation-rule.ts | 25 +++++++++ apps/api/src/utils/org-participation.ts | 11 ++-- 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/utils/org-participation-rule.spec.ts create mode 100644 apps/api/src/utils/org-participation-rule.ts diff --git a/apps/api/package.json b/apps/api/package.json index 665d61ff6..9db847a0b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -175,6 +175,7 @@ "^@db$": "/../prisma/index", "^@/(.*)$": "/$1", "^\\./sku-definitions\\.js$": "/../../../packages/billing/src/sku-definitions.ts", + "^@trycompai/auth/participation$": "/../../../packages/auth/src/participation.ts", "^@trycompai/auth$": "/../../../packages/auth/src/index.ts", "^@trycompai/billing$": "/../../../packages/billing/src/index.ts", "^@trycompai/company$": "/../../../packages/company/src/index.ts", diff --git a/apps/api/src/task-management/task-item-assignment-notifier.service.ts b/apps/api/src/task-management/task-item-assignment-notifier.service.ts index 5fe78653b..44fc28953 100644 --- a/apps/api/src/task-management/task-item-assignment-notifier.service.ts +++ b/apps/api/src/task-management/task-item-assignment-notifier.service.ts @@ -1,5 +1,5 @@ import { db } from '@db'; -import { isOrgParticipant } from '@trycompai/auth/participation'; +import { isOrgParticipant } from '../utils/org-participation-rule'; import { Injectable, Logger } from '@nestjs/common'; import { isUserUnsubscribed } from '@trycompai/email'; import { triggerEmail } from '../email/trigger-email'; diff --git a/apps/api/src/utils/org-participation-rule.spec.ts b/apps/api/src/utils/org-participation-rule.spec.ts new file mode 100644 index 000000000..16c1af824 --- /dev/null +++ b/apps/api/src/utils/org-participation-rule.spec.ts @@ -0,0 +1,56 @@ +import { + PLATFORM_ADMIN_ROLE as AUTH_PLATFORM_ADMIN_ROLE, + isOrgParticipant as authIsOrgParticipant, +} from '@trycompai/auth/participation'; +import { + isOrgParticipant, + PLATFORM_ADMIN_ROLE, +} from './org-participation-rule'; + +describe('org-participation-rule (API mirror)', () => { + it('excludes platform admins in a customer org', () => { + expect(isOrgParticipant(PLATFORM_ADMIN_ROLE, { orgIsInternal: false })).toBe( + false, + ); + }); + + it('includes platform admins in an internal org', () => { + expect(isOrgParticipant(PLATFORM_ADMIN_ROLE, { orgIsInternal: true })).toBe( + true, + ); + }); + + it('includes non-admin and null roles in a customer org', () => { + expect(isOrgParticipant('user', { orgIsInternal: false })).toBe(true); + expect(isOrgParticipant('owner', { orgIsInternal: false })).toBe(true); + expect(isOrgParticipant(null, { orgIsInternal: false })).toBe(true); + expect(isOrgParticipant(undefined, { orgIsInternal: false })).toBe(true); + }); +}); + +// Drift guard: this API-local rule is a deliberate dependency-free mirror of +// `@trycompai/auth/participation` (files in the Trigger.dev bundle can't import +// the auth package — its dist isn't built in that deploy). Fail CI if the two +// ever diverge. +describe('API rule stays in sync with @trycompai/auth', () => { + const roles: Array = [ + 'admin', + 'owner', + 'user', + 'auditor', + '', + null, + undefined, + ]; + + it('matches the canonical predicate for every role × internal flag', () => { + expect(PLATFORM_ADMIN_ROLE).toBe(AUTH_PLATFORM_ADMIN_ROLE); + for (const role of roles) { + for (const orgIsInternal of [true, false]) { + expect(isOrgParticipant(role, { orgIsInternal })).toBe( + authIsOrgParticipant(role, { orgIsInternal }), + ); + } + } + }); +}); diff --git a/apps/api/src/utils/org-participation-rule.ts b/apps/api/src/utils/org-participation-rule.ts new file mode 100644 index 000000000..52b3e00a7 --- /dev/null +++ b/apps/api/src/utils/org-participation-rule.ts @@ -0,0 +1,25 @@ +/** + * Pure, dependency-free org-participation rule. + * + * This is a deliberate mirror of `packages/auth/src/participation.ts`. Files + * that end up in the API's Trigger.dev bundle (e.g. task notifiers) cannot + * import `@trycompai/auth/participation`: the auth package's dist is not built + * in the Trigger.dev deploy workflow, so esbuild fails to resolve the subpath + * ("Could not resolve @trycompai/auth/participation"). Keeping this rule free of + * imports lets both NestJS services and Trigger.dev tasks share one + * implementation. KEEP IN SYNC with the auth package version — the drift-guard + * test in `org-participation-rule.spec.ts` fails CI if they diverge. + * + * Platform admins (`User.role === 'admin'`) are Comp AI staff embedded in + * customer orgs for support; they are excluded from an org's business logic + * UNLESS the org is internal (platform-operated, e.g. Comp AI's own org). + */ +export const PLATFORM_ADMIN_ROLE = 'admin'; + +export function isOrgParticipant( + userRole: string | null | undefined, + { orgIsInternal }: { orgIsInternal: boolean }, +): boolean { + if (orgIsInternal) return true; + return userRole !== PLATFORM_ADMIN_ROLE; +} diff --git a/apps/api/src/utils/org-participation.ts b/apps/api/src/utils/org-participation.ts index f8413eac8..1811bfe9c 100644 --- a/apps/api/src/utils/org-participation.ts +++ b/apps/api/src/utils/org-participation.ts @@ -1,10 +1,9 @@ import { db, Prisma } from '@db'; -// Import from the dedicated subpath (not the package index) so this stays free -// of better-auth — keeps the util light and Jest-loadable without ESM interop. -import { - PLATFORM_ADMIN_ROLE, - isOrgParticipant, -} from '@trycompai/auth/participation'; +// Use the dependency-free local mirror (not @trycompai/auth) so this file is +// safe in the API's Trigger.dev bundle — the auth package's dist isn't built in +// that deploy, so esbuild can't resolve `@trycompai/auth/participation`. The +// mirror is kept in sync with the auth package by org-participation-rule.spec.ts. +import { PLATFORM_ADMIN_ROLE, isOrgParticipant } from './org-participation-rule'; /** * Resolve whether an organization is platform-operated ("internal", e.g. Comp