@@ -5,7 +5,10 @@ import {
55 ProjectAlertEmailProperties ,
66 ProjectAlertSlackProperties ,
77} from "~/models/projectAlert.server" ;
8- import { resolveAgentAlertContext } from "~/services/dashboardAgentAlertContext.server" ;
8+ import {
9+ resolveAgentAlertContext ,
10+ type AgentAlertContextError ,
11+ } from "~/services/dashboardAgentAlertContext.server" ;
912import {
1013 canUseDashboardAgentEmailAlerts ,
1114 DASHBOARD_AGENT_WATCH_ALERT_TYPE ,
@@ -21,7 +24,8 @@ import { CreateAlertChannelService } from "~/v3/services/alerts/createAlertChann
2124 *
2225 * Accepts ONLY the dashboard agent's delegated user-actor token, like the watches
2326 * endpoint: POST creates something that later mails a person, so the only caller
24- * it trusts is the agent acting for the signed-in user in a live chat.
27+ * it trusts is the agent acting for the signed-in user in a live chat. The
28+ * environment is the token's, not the body's (see `resolveAgentAlertContext`).
2529 *
2630 * The plan/flag gate is enforced here AND at delivery, and its denial carries a
2731 * machine-readable `reason` so the agent can say why instead of guessing.
@@ -42,18 +46,38 @@ const CreateBodySchema = z.object({
4246 projectRef : z . string ( ) . min ( 1 ) . optional ( ) ,
4347} ) ;
4448
45- async function authenticate ( request : Request ) {
49+ /**
50+ * The preamble both handlers share: a dashboard-agent token, plus the environment
51+ * scope the dashboard minted it with — which is the authority for everything here,
52+ * so a token without one can't be used at all.
53+ */
54+ async function authenticate (
55+ request : Request
56+ ) : Promise < { userId : string ; environmentId : string } | { error : Response } > {
4657 const authentication = await authenticateUatOrApiRequest ( request ) ;
47- if ( ! authentication ?. userActor ) return undefined ;
48- if ( authentication . userActor . client !== "dashboard-agent" ) return undefined ;
49- return authentication . userActor . userId ;
58+ const actor = authentication ?. userActor ;
59+ if ( ! actor || actor . client !== "dashboard-agent" ) {
60+ return { error : json ( { error : "Invalid or missing access token" } , { status : 401 } ) } ;
61+ }
62+ if ( ! actor . environmentId ) {
63+ return {
64+ error : json (
65+ { error : "This chat has no environment context." , code : "invalid_target" } ,
66+ { status : 400 }
67+ ) ,
68+ } ;
69+ }
70+ return { userId : actor . userId , environmentId : actor . environmentId } ;
71+ }
72+
73+ /** Context failures: a mismatched claim is the caller's error, the rest are 404s. */
74+ function contextStatus ( code : AgentAlertContextError ) {
75+ return code === "environment_mismatch" ? 400 : 404 ;
5076}
5177
5278export async function loader ( { request } : LoaderFunctionArgs ) {
53- const userId = await authenticate ( request ) ;
54- if ( ! userId ) {
55- return json ( { error : "Invalid or missing access token" } , { status : 401 } ) ;
56- }
79+ const auth = await authenticate ( request ) ;
80+ if ( "error" in auth ) return auth . error ;
5781
5882 const query = ListQuerySchema . safeParse (
5983 Object . fromEntries ( new URL ( request . url ) . searchParams . entries ( ) )
@@ -62,9 +86,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
6286 return json ( { error : "Invalid request" , code : "invalid_request" } , { status : 400 } ) ;
6387 }
6488
65- const context = await resolveAgentAlertContext ( { userId, ...query . data } ) ;
89+ const context = await resolveAgentAlertContext ( {
90+ userId : auth . userId ,
91+ environmentId : auth . environmentId ,
92+ chatId : query . data . chatId ,
93+ claimedEnvironmentId : query . data . environmentId ,
94+ claimedProjectRef : query . data . projectRef ,
95+ } ) ;
6696 if ( ! context . ok ) {
67- return json ( { error : context . error , code : context . code } , { status : 404 } ) ;
97+ return json (
98+ { error : context . error , code : context . code } ,
99+ { status : contextStatus ( context . code ) }
100+ ) ;
68101 }
69102
70103 const channels = await $replica . projectAlertChannel . findMany ( {
@@ -92,10 +125,9 @@ export async function action({ request }: ActionFunctionArgs) {
92125 return json ( { error : "Method not allowed" } , { status : 405 } ) ;
93126 }
94127
95- const userId = await authenticate ( request ) ;
96- if ( ! userId ) {
97- return json ( { error : "Invalid or missing access token" } , { status : 401 } ) ;
98- }
128+ const auth = await authenticate ( request ) ;
129+ if ( "error" in auth ) return auth . error ;
130+ const { userId } = auth ;
99131
100132 let body : z . infer < typeof CreateBodySchema > ;
101133 try {
@@ -110,12 +142,16 @@ export async function action({ request }: ActionFunctionArgs) {
110142
111143 const context = await resolveAgentAlertContext ( {
112144 userId,
145+ environmentId : auth . environmentId ,
113146 chatId : body . chatId ,
114- environmentId : body . environmentId ,
115- projectRef : body . projectRef ,
147+ claimedEnvironmentId : body . environmentId ,
148+ claimedProjectRef : body . projectRef ,
116149 } ) ;
117150 if ( ! context . ok ) {
118- return json ( { error : context . error , code : context . code } , { status : 404 } ) ;
151+ return json (
152+ { error : context . error , code : context . code } ,
153+ { status : contextStatus ( context . code ) }
154+ ) ;
119155 }
120156 const { environment } = context ;
121157
0 commit comments