Skip to content

Commit 1ba1d20

Browse files
tofikwestclaude
andauthored
fix(api): stop Trigger.dev deploy failing on @trycompai/auth/participation (#3393)
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. Claude-Session: https://claude.ai/code/session_01XzEVwhhoFghng6SDMGecMq Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c013515 commit 1ba1d20

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

‎apps/api/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
"^@db$": "<rootDir>/../prisma/index",
176176
"^@/(.*)$": "<rootDir>/$1",
177177
"^\\./sku-definitions\\.js$": "<rootDir>/../../../packages/billing/src/sku-definitions.ts",
178+
"^@trycompai/auth/participation$": "<rootDir>/../../../packages/auth/src/participation.ts",
178179
"^@trycompai/auth$": "<rootDir>/../../../packages/auth/src/index.ts",
179180
"^@trycompai/billing$": "<rootDir>/../../../packages/billing/src/index.ts",
180181
"^@trycompai/company$": "<rootDir>/../../../packages/company/src/index.ts",

‎apps/api/src/task-management/task-item-assignment-notifier.service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@db';
2-
import { isOrgParticipant } from '@trycompai/auth/participation';
2+
import { isOrgParticipant } from '../utils/org-participation-rule';
33
import { Injectable, Logger } from '@nestjs/common';
44
import { isUserUnsubscribed } from '@trycompai/email';
55
import { triggerEmail } from '../email/trigger-email';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
PLATFORM_ADMIN_ROLE as AUTH_PLATFORM_ADMIN_ROLE,
3+
isOrgParticipant as authIsOrgParticipant,
4+
} from '@trycompai/auth/participation';
5+
import {
6+
isOrgParticipant,
7+
PLATFORM_ADMIN_ROLE,
8+
} from './org-participation-rule';
9+
10+
describe('org-participation-rule (API mirror)', () => {
11+
it('excludes platform admins in a customer org', () => {
12+
expect(isOrgParticipant(PLATFORM_ADMIN_ROLE, { orgIsInternal: false })).toBe(
13+
false,
14+
);
15+
});
16+
17+
it('includes platform admins in an internal org', () => {
18+
expect(isOrgParticipant(PLATFORM_ADMIN_ROLE, { orgIsInternal: true })).toBe(
19+
true,
20+
);
21+
});
22+
23+
it('includes non-admin and null roles in a customer org', () => {
24+
expect(isOrgParticipant('user', { orgIsInternal: false })).toBe(true);
25+
expect(isOrgParticipant('owner', { orgIsInternal: false })).toBe(true);
26+
expect(isOrgParticipant(null, { orgIsInternal: false })).toBe(true);
27+
expect(isOrgParticipant(undefined, { orgIsInternal: false })).toBe(true);
28+
});
29+
});
30+
31+
// Drift guard: this API-local rule is a deliberate dependency-free mirror of
32+
// `@trycompai/auth/participation` (files in the Trigger.dev bundle can't import
33+
// the auth package — its dist isn't built in that deploy). Fail CI if the two
34+
// ever diverge.
35+
describe('API rule stays in sync with @trycompai/auth', () => {
36+
const roles: Array<string | null | undefined> = [
37+
'admin',
38+
'owner',
39+
'user',
40+
'auditor',
41+
'',
42+
null,
43+
undefined,
44+
];
45+
46+
it('matches the canonical predicate for every role × internal flag', () => {
47+
expect(PLATFORM_ADMIN_ROLE).toBe(AUTH_PLATFORM_ADMIN_ROLE);
48+
for (const role of roles) {
49+
for (const orgIsInternal of [true, false]) {
50+
expect(isOrgParticipant(role, { orgIsInternal })).toBe(
51+
authIsOrgParticipant(role, { orgIsInternal }),
52+
);
53+
}
54+
}
55+
});
56+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Pure, dependency-free org-participation rule.
3+
*
4+
* This is a deliberate mirror of `packages/auth/src/participation.ts`. Files
5+
* that end up in the API's Trigger.dev bundle (e.g. task notifiers) cannot
6+
* import `@trycompai/auth/participation`: the auth package's dist is not built
7+
* in the Trigger.dev deploy workflow, so esbuild fails to resolve the subpath
8+
* ("Could not resolve @trycompai/auth/participation"). Keeping this rule free of
9+
* imports lets both NestJS services and Trigger.dev tasks share one
10+
* implementation. KEEP IN SYNC with the auth package version — the drift-guard
11+
* test in `org-participation-rule.spec.ts` fails CI if they diverge.
12+
*
13+
* Platform admins (`User.role === 'admin'`) are Comp AI staff embedded in
14+
* customer orgs for support; they are excluded from an org's business logic
15+
* UNLESS the org is internal (platform-operated, e.g. Comp AI's own org).
16+
*/
17+
export const PLATFORM_ADMIN_ROLE = 'admin';
18+
19+
export function isOrgParticipant(
20+
userRole: string | null | undefined,
21+
{ orgIsInternal }: { orgIsInternal: boolean },
22+
): boolean {
23+
if (orgIsInternal) return true;
24+
return userRole !== PLATFORM_ADMIN_ROLE;
25+
}

‎apps/api/src/utils/org-participation.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { db, Prisma } from '@db';
2-
// Import from the dedicated subpath (not the package index) so this stays free
3-
// of better-auth — keeps the util light and Jest-loadable without ESM interop.
4-
import {
5-
PLATFORM_ADMIN_ROLE,
6-
isOrgParticipant,
7-
} from '@trycompai/auth/participation';
2+
// Use the dependency-free local mirror (not @trycompai/auth) so this file is
3+
// safe in the API's Trigger.dev bundle — the auth package's dist isn't built in
4+
// that deploy, so esbuild can't resolve `@trycompai/auth/participation`. The
5+
// mirror is kept in sync with the auth package by org-participation-rule.spec.ts.
6+
import { PLATFORM_ADMIN_ROLE, isOrgParticipant } from './org-participation-rule';
87

98
/**
109
* Resolve whether an organization is platform-operated ("internal", e.g. Comp

0 commit comments

Comments
 (0)