1- import type { PrismaClient , RuntimeEnvironment } from "@trigger.dev/database" ;
1+ import type {
2+ PrismaClient ,
3+ PrismaTransactionClient ,
4+ RuntimeEnvironment ,
5+ } from "@trigger.dev/database" ;
26import type { HostRbacController } from "@trigger.dev/rbac" ;
37import { customAlphabet } from "nanoid" ;
48import { MAX_API_KEY_TASK_IDENTIFIERS } from "~/consts" ;
5- import { boundedIn , prisma } from "~/db.server" ;
9+ import { $transaction , boundedIn , prisma } from "~/db.server" ;
610import { RuntimeEnvironmentType } from "~/database-types" ;
711import { canIssueAdditionalApiKeys } from "~/services/additionalApiKeyIssuance.server" ;
812import { apiKeyTelemetry , type ApiKeyTelemetry } from "~/services/apiKeyTelemetry.server" ;
@@ -17,87 +21,187 @@ const apiKeyId = customAlphabet(
1721
1822const REVOKED_API_KEY_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000 ;
1923
20- type RegenerateAPIKeyInput = {
24+ type RootApiKeyMutationInput = {
2125 userId : string ;
2226 environmentId : string ;
2327} ;
2428
25- export async function regenerateApiKey ( { userId, environmentId } : RegenerateAPIKeyInput ) {
26- const environment = await prisma . runtimeEnvironment . findUnique ( {
29+ export class RootApiKeyNotVisibleError extends Error {
30+ constructor ( ) {
31+ super ( "The root API key is no longer visible" ) ;
32+ this . name = "RootApiKeyNotVisibleError" ;
33+ }
34+ }
35+
36+ async function findRootApiKeyEnvironment (
37+ { userId, environmentId } : RootApiKeyMutationInput ,
38+ prismaClient : PrismaClient
39+ ) {
40+ const requestedEnvironment = await prismaClient . runtimeEnvironment . findFirst ( {
2741 where : {
2842 id : environmentId ,
43+ organization : { members : { some : { userId } } } ,
44+ OR : [
45+ { type : { not : RuntimeEnvironmentType . DEVELOPMENT } } ,
46+ {
47+ type : RuntimeEnvironmentType . DEVELOPMENT ,
48+ orgMember : { userId } ,
49+ } ,
50+ ] ,
2951 } ,
30- include : {
31- organization : true ,
32- project : true ,
33- } ,
52+ select : { id : true , parentEnvironmentId : true } ,
3453 } ) ;
3554
36- if ( ! environment ) {
37- throw new Error ( "Environment does not exist " ) ;
55+ if ( ! requestedEnvironment ) {
56+ throw new Error ( "User does not have permission to manage this root API key " ) ;
3857 }
3958
40- // check if the user is part of the org
41- const organization = await prisma . organization . findFirst ( {
59+ const environment = await prismaClient . runtimeEnvironment . findFirst ( {
4260 where : {
43- id : environment . organization . id ,
44- members : { some : { userId } } ,
61+ id : requestedEnvironment . parentEnvironmentId ?? requestedEnvironment . id ,
62+ organization : { members : { some : { userId } } } ,
63+ OR : [
64+ { type : { not : RuntimeEnvironmentType . DEVELOPMENT } } ,
65+ {
66+ type : RuntimeEnvironmentType . DEVELOPMENT ,
67+ orgMember : { userId } ,
68+ } ,
69+ ] ,
70+ } ,
71+ select : {
72+ id : true ,
73+ apiKey : true ,
74+ pkApiKey : true ,
75+ rootApiKeyHiddenAt : true ,
76+ type : true ,
77+ projectId : true ,
78+ branchName : true ,
4579 } ,
4680 } ) ;
4781
48- if ( ! organization ) {
49- throw new Error ( "User does not have permission to regenerate API key" ) ;
82+ if ( ! environment ) {
83+ throw new Error ( "User does not have permission to manage this root API key" ) ;
5084 }
5185
52- // check if it is the user's dev environment
53- if ( environment . type === RuntimeEnvironmentType . DEVELOPMENT ) {
54- if ( ! environment . orgMemberId ) {
55- throw new Error ( "User does not have permission to regenerate API key" ) ;
56- }
86+ if ( environment . rootApiKeyHiddenAt ) {
87+ throw new RootApiKeyNotVisibleError ( ) ;
88+ }
5789
58- const orgMember = await prisma . orgMember . findFirst ( {
59- where : {
60- organizationId : organization . id ,
61- userId : userId ,
62- id : environment . orgMemberId ,
63- } ,
64- } ) ;
90+ return environment ;
91+ }
92+
93+ async function lockVisibleRootApiKeyEnvironment (
94+ prismaClient : PrismaTransactionClient ,
95+ environmentId : string
96+ ) {
97+ const [ environment ] = await prismaClient . $queryRaw <
98+ Array < { apiKey : string ; rootApiKeyHiddenAt : Date | null } >
99+ > `
100+ SELECT "apiKey", "rootApiKeyHiddenAt"
101+ FROM "public"."RuntimeEnvironment"
102+ WHERE "id" = ${ environmentId }
103+ FOR UPDATE
104+ ` ;
105+
106+ if ( ! environment || environment . rootApiKeyHiddenAt ) {
107+ throw new RootApiKeyNotVisibleError ( ) ;
108+ }
109+
110+ return environment ;
111+ }
65112
66- if ( ! orgMember ) {
67- throw new Error ( "User does not have permission to regenerate API key" ) ;
113+ export async function regenerateApiKey (
114+ input : RootApiKeyMutationInput ,
115+ { prismaClient = prisma } : { prismaClient ?: PrismaClient } = { }
116+ ) {
117+ const environment = await findRootApiKeyEnvironment ( input , prismaClient ) ;
118+ const newApiKey = createApiKeyForEnv ( environment . type ) ;
119+ const newPkApiKey = createPkApiKeyForEnv ( environment . type ) ;
120+ const revokedApiKeyExpiresAt = new Date ( Date . now ( ) + REVOKED_API_KEY_GRACE_PERIOD_MS ) ;
121+
122+ const updatedEnvironment = await $transaction (
123+ prismaClient ,
124+ "regenerate root API key" ,
125+ async ( tx ) => {
126+ const currentEnvironment = await lockVisibleRootApiKeyEnvironment ( tx , environment . id ) ;
127+
128+ await tx . runtimeEnvironment . update ( {
129+ where : { id : environment . id } ,
130+ data : {
131+ apiKey : newApiKey ,
132+ pkApiKey : newPkApiKey ,
133+ } ,
134+ } ) ;
135+
136+ await tx . revokedApiKey . create ( {
137+ data : {
138+ apiKey : currentEnvironment . apiKey ,
139+ runtimeEnvironmentId : environment . id ,
140+ expiresAt : revokedApiKeyExpiresAt ,
141+ } ,
142+ } ) ;
143+
144+ return { ...environment , apiKey : newApiKey , pkApiKey : newPkApiKey } ;
68145 }
146+ ) ;
147+
148+ if ( ! updatedEnvironment ) {
149+ throw new Error ( "The root API key could not be regenerated" ) ;
69150 }
70151
71- // generate and store new keys
152+ controlPlaneResolver . invalidateEnvironment ( environment . id ) ;
153+
154+ return updatedEnvironment ;
155+ }
156+
157+ export async function disableRootApiKeyVisibility (
158+ input : RootApiKeyMutationInput ,
159+ { prismaClient = prisma } : { prismaClient ?: PrismaClient } = { }
160+ ) {
161+ const environment = await findRootApiKeyEnvironment ( input , prismaClient ) ;
72162 const newApiKey = createApiKeyForEnv ( environment . type ) ;
73163 const newPkApiKey = createPkApiKeyForEnv ( environment . type ) ;
164+ const rootApiKeyHiddenAt = new Date ( ) ;
74165
75- const revokedApiKeyExpiresAt = new Date ( Date . now ( ) + REVOKED_API_KEY_GRACE_PERIOD_MS ) ;
166+ const updatedEnvironment = await $transaction (
167+ prismaClient ,
168+ "disable root API key visibility" ,
169+ async ( tx ) => {
170+ const currentEnvironment = await lockVisibleRootApiKeyEnvironment ( tx , environment . id ) ;
76171
77- const updatedEnviroment = await prisma . $transaction ( async ( tx ) => {
78- await tx . revokedApiKey . create ( {
79- data : {
80- apiKey : environment . apiKey ,
81- runtimeEnvironmentId : environment . id ,
82- expiresAt : revokedApiKeyExpiresAt ,
83- } ,
84- } ) ;
172+ await tx . runtimeEnvironment . update ( {
173+ where : { id : environment . id } ,
174+ data : {
175+ apiKey : newApiKey ,
176+ pkApiKey : newPkApiKey ,
177+ rootApiKeyHiddenAt,
178+ } ,
179+ } ) ;
180+
181+ await tx . revokedApiKey . create ( {
182+ data : {
183+ apiKey : currentEnvironment . apiKey ,
184+ runtimeEnvironmentId : environment . id ,
185+ expiresAt : new Date ( Date . now ( ) + REVOKED_API_KEY_GRACE_PERIOD_MS ) ,
186+ } ,
187+ } ) ;
85188
86- return tx . runtimeEnvironment . update ( {
87- data : {
189+ return {
190+ ... environment ,
88191 apiKey : newApiKey ,
89192 pkApiKey : newPkApiKey ,
90- } ,
91- where : {
92- id : environmentId ,
93- } ,
94- } ) ;
95- } ) ;
193+ rootApiKeyHiddenAt,
194+ } ;
195+ }
196+ ) ;
197+
198+ if ( ! updatedEnvironment ) {
199+ throw new Error ( "Root API key visibility could not be disabled" ) ;
200+ }
96201
97- // The env's apiKey changed in the control-plane; drop any cached copy.
98- controlPlaneResolver . invalidateEnvironment ( environmentId ) ;
202+ controlPlaneResolver . invalidateEnvironment ( environment . id ) ;
99203
100- return updatedEnviroment ;
204+ return updatedEnvironment ;
101205}
102206
103207export async function createEnvironmentApiKey (
0 commit comments