@trigger.dev/sdk, webapp: Adding more granular error messages around unauthorized requests

This commit is contained in:
Eric Allam
2023-06-28 17:53:40 +01:00
parent 85108cfcb5
commit f2f4d4b827
6 changed files with 134 additions and 28 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Adding more granular error messages around unauthorized requests
@@ -62,12 +62,29 @@ export async function action({ request, params }: ActionArgs) {
const service = new IndexEndpointService();
const { data, ...index } = await service.call(
endpoint.id,
"API",
parsedBody.data.reason,
parsedBody.data.data
);
try {
const { data, ...index } = await service.call(
endpoint.id,
"API",
parsedBody.data.reason,
parsedBody.data.data
);
return json(index);
return json(index);
} catch (error) {
if (error instanceof Error) {
logger.error("Error indexing endpoint", {
url: request.url,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
});
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
+38 -9
View File
@@ -1,21 +1,20 @@
import {
ApiEventLog,
DeliverEventResponseSchema,
ErrorWithStackSchema,
HttpSourceRequest,
HttpSourceResponseSchema,
IndexEndpointResponseSchema,
PongResponse,
PongResponseSchema,
PreprocessRunBody,
PreprocessRunResponseSchema,
RegisterTriggerBody,
RegisterTriggerBodySchema,
RunJobBody,
} from "@trigger.dev/internal";
import {
DeliverEventResponseSchema,
ErrorWithStackSchema,
IndexEndpointResponseSchema,
HttpSourceResponseSchema,
PongResponseSchema,
RunJobResponseSchema,
} from "@trigger.dev/internal";
import { safeBodyFromResponse } from "~/utils/json";
import { logger } from "./logger.server";
export class EndpointApiError extends Error {
@@ -53,10 +52,19 @@ export class EndpointApi {
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
};
} as const;
}
if (!response.ok) {
@@ -89,6 +97,22 @@ export class EndpointApi {
throw new Error(`Could not connect to endpoint ${this.url}`);
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
} as const;
}
if (!response.ok) {
throw new Error(
`Could not connect to endpoint ${this.url}. Status code: ${response.status}`
@@ -101,7 +125,12 @@ export class EndpointApi {
body: anyBody,
});
return IndexEndpointResponseSchema.parse(anyBody);
const data = IndexEndpointResponseSchema.parse(anyBody);
return {
ok: true,
data,
} as const;
}
async deliverEvent(event: ApiEventLog) {
@@ -33,8 +33,14 @@ export class IndexEndpointService {
endpoint.slug
);
const indexResponse = await client.indexEndpoint();
if (!indexResponse.ok) {
throw new Error(indexResponse.error);
}
const { jobs, sources, dynamicTriggers, dynamicSchedules } =
await client.indexEndpoint();
indexResponse.data;
const queueName = `endpoint-${endpoint.id}`;
+18
View File
@@ -25,3 +25,21 @@ export async function safeJsonFromResponse(response: Response) {
const json = await response.text();
return safeJsonParse(json);
}
export async function safeBodyFromResponse<T>(
response: Response,
schema: z.Schema<T>
): Promise<T | undefined> {
const json = await response.text();
const unknownJson = safeJsonParse(json);
if (!unknownJson) {
return;
}
const parsedJson = schema.safeParse(unknownJson);
if (parsedJson.success) {
return parsedJson.data;
}
}
+42 -11
View File
@@ -111,20 +111,45 @@ export class TriggerClient {
const apiKey = request.headers.get("x-trigger-api-key");
if (!this.authorized(apiKey)) {
return {
status: 401,
body: {
message: "Unauthorized",
},
};
const authorization = this.authorized(apiKey);
switch (authorization) {
case "authorized": {
break;
}
case "missing-client": {
return {
status: 401,
body: {
message: "Unauthorized: client missing apiKey",
},
};
}
case "missing-header": {
return {
status: 401,
body: {
message: "Unauthorized: missing x-trigger-api-key header",
},
};
}
case "unauthorized": {
return {
status: 401,
body: {
message: `Forbidden: client apiKey mismatch: Expected ${
this.#options.apiKey
}, got ${apiKey}`,
},
};
}
}
if (request.method !== "POST") {
return {
status: 405,
body: {
message: "Method not allowed",
message: "Method not allowed (only POST is allowed)",
},
};
}
@@ -529,14 +554,20 @@ export class TriggerClient {
return this.#client.unregisterSchedule(this.id, id, key);
}
authorized(apiKey?: string | null) {
authorized(
apiKey?: string | null
): "authorized" | "unauthorized" | "missing-client" | "missing-header" {
if (typeof apiKey !== "string") {
return "missing-header";
}
const localApiKey = this.#options.apiKey ?? process.env.TRIGGER_API_KEY;
if (!localApiKey) {
return false;
return "missing-client";
}
return apiKey === localApiKey;
return apiKey === localApiKey ? "authorized" : "unauthorized";
}
apiKey() {