Add link headers

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:19:16 -08:00
parent d14d64e11e
commit ea96cc87bb
4 changed files with 101 additions and 45 deletions
+9 -4
View File
@@ -424,7 +424,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// Respond to Challenge (RFC 8555 Section 7.5.1) // Respond to Challenge (RFC 8555 Section 7.5.1)
server.route({ server.route({
method: "POST", method: "POST",
url: "/profiles/:profileId/authorizations/:authzId/challenges/http-01", url: "/profiles/:profileId/authorizations/:authzId/challenges/:challengeId",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -433,7 +433,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiAcme], tags: [ApiDocsTags.PkiAcme],
description: "ACME Respond to Challenge - let ACME server know challenge is ready", description: "ACME Respond to Challenge - let ACME server know challenge is ready",
params: SharedParamsSchema.extend({ params: SharedParamsSchema.extend({
authzId: z.string().uuid() authzId: z.string().uuid(),
challengeId: z.string().uuid()
}), }),
response: { response: {
200: RespondToAcmeChallengeResponseSchema 200: RespondToAcmeChallengeResponseSchema
@@ -447,9 +448,13 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
return sendAcmeResponse( return sendAcmeResponse(
res, res,
profileId, profileId,
await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId }) await server.services.pkiAcme.respondToAcmeChallenge({
profileId,
accountId,
authzId: req.params.authzId,
challengeId: req.params.challengeId
})
); );
return challenge;
} }
}); });
}; };
@@ -1,13 +1,41 @@
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { Knex } from "knex";
export type TPkiAcmeChallengeDALFactory = ReturnType<typeof pkiAcmeChallengeDALFactory>; export type TPkiAcmeChallengeDALFactory = ReturnType<typeof pkiAcmeChallengeDALFactory>;
export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge);
const findByAccountAuthAndChallengeIdWithToken = async (
accountId: string,
authId: string,
challengeId: string,
tx?: Knex
) => {
try {
const challenge = await (tx || db)(TableName.PkiAcmeChallenge)
.join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`)
.select(
selectAllTableCols(TableName.PkiAcmeChallenge),
db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("token")
)
.where(`${TableName.PkiAcmeChallenge}.id`, challengeId)
.where(`${TableName.PkiAcmeChallenge}.authId`, authId)
.where(`${TableName.PkiAcmeAuth}.accountId`, accountId)
.first();
if (!challenge) {
return null;
}
return challenge;
} catch (error) {
throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" });
}
};
return { return {
...pkiAcmeChallengeOrm ...pkiAcmeChallengeOrm,
findByAccountAuthAndChallengeIdWithToken
}; };
}; };
@@ -61,7 +61,7 @@ type TPkiAcmeServiceFactoryDep = {
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">; acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">;
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">; acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">; acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">;
acmeChallengeDAL: Pick<TPkiAcmeChallengeDALFactory, "create">; acmeChallengeDAL: Pick<TPkiAcmeChallengeDALFactory, "create" | "findByAccountAuthAndChallengeIdWithToken">;
}; };
export const pkiAcmeServiceFactory = ({ export const pkiAcmeServiceFactory = ({
@@ -300,9 +300,10 @@ export const pkiAcmeServiceFactory = ({
contact: existingAccount.emails, contact: existingAccount.emails,
orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`) orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`)
}, },
headers: { headers: [
Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`) ["Location", buildUrl(profile.id, `/accounts/${existingAccount.id}`)],
} ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`]
]
}; };
} }
@@ -321,9 +322,10 @@ export const pkiAcmeServiceFactory = ({
contact: newAccount.emails, contact: newAccount.emails,
orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`) orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`)
}, },
headers: { headers: [
Location: buildUrl(profile.id, `/accounts/${newAccount.id}`) ["Location", buildUrl(profile.id, `/accounts/${newAccount.id}`)],
} ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -343,9 +345,10 @@ export const pkiAcmeServiceFactory = ({
body: { body: {
status: "deactivated" status: "deactivated"
}, },
headers: { headers: [
Location: buildUrl(profileId, `/accounts/${accountId}`) ["Location", buildUrl(profileId, `/accounts/${accountId}`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -429,9 +432,10 @@ export const pkiAcmeServiceFactory = ({
profileId, profileId,
order order
}), }),
headers: { headers: [
Location: buildUrl(profileId, `/orders/${order.id}`) ["Location", buildUrl(profileId, `/orders/${order.id}`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -451,7 +455,10 @@ export const pkiAcmeServiceFactory = ({
return { return {
status: 200, status: 200,
body: buildAcmeOrderResource({ profileId, order }), body: buildAcmeOrderResource({ profileId, order }),
headers: { Location: buildUrl(profileId, `/orders/${orderId}`) } headers: [
["Location", buildUrl(profileId, `/orders/${orderId}`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -475,9 +482,10 @@ export const pkiAcmeServiceFactory = ({
return { return {
status: 200, status: 200,
body: buildAcmeOrderResource({ profileId, order }), body: buildAcmeOrderResource({ profileId, order }),
headers: { headers: [
Location: buildUrl(profileId, `/orders/${orderId}`) ["Location", buildUrl(profileId, `/orders/${orderId}`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -499,9 +507,10 @@ export const pkiAcmeServiceFactory = ({
return { return {
status: 200, status: 200,
body: "FIXME-certificate-pem", body: "FIXME-certificate-pem",
headers: { headers: [
Location: buildUrl(profileId, `/orders/${orderId}/certificate`) ["Location", buildUrl(profileId, `/orders/${orderId}/certificate`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -519,9 +528,10 @@ export const pkiAcmeServiceFactory = ({
body: { body: {
orders: [] orders: []
}, },
headers: { headers: [
Location: buildUrl(profileId, `/accounts/${accountId}/orders`) ["Location", buildUrl(profileId, `/accounts/${accountId}/orders`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -559,33 +569,42 @@ export const pkiAcmeServiceFactory = ({
}; };
}) })
}, },
headers: { headers: [
Location: buildUrl(profileId, `/authorizations/${authzId}`) ["Location", buildUrl(profileId, `/authorizations/${authzId}`)],
} ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
const respondToAcmeChallenge = async ({ const respondToAcmeChallenge = async ({
profileId, profileId,
authzId accountId,
authzId,
challengeId
}: { }: {
profileId: string; profileId: string;
accountId: string;
authzId: string; authzId: string;
challengeId: string;
}): Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>> => { }): Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>> => {
const profile = await validateAcmeProfile(profileId); const challenge = await acmeChallengeDAL.findByAccountAuthAndChallengeIdWithToken(accountId, authzId, challengeId);
// FIXME: Implement ACME challenge response if (!challenge) {
// Trigger verification process throw new NotFoundError({ message: "ACME challenge not found" });
}
// TODO: Implement ACME challenge response
return { return {
status: 200, status: 200,
body: { body: {
type: "http-01", type: challenge.type,
url: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`), url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`),
status: "pending", status: challenge.status,
token: "FIXME-challenge-token" token: challenge.token
}, },
headers: { headers: [
Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`) ["Location", buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)],
} ["Link", `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
}; };
}; };
@@ -44,7 +44,7 @@ export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
}; };
export type TAcmeResponse<TPayload> = { export type TAcmeResponse<TPayload> = {
status: number; status: number;
headers: Record<string, string>; headers: [string, string][];
body: TPayload; body: TPayload;
}; };
@@ -164,9 +164,13 @@ export type TPkiAcmeServiceFactory = {
}) => Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>>; }) => Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>>;
respondToAcmeChallenge: ({ respondToAcmeChallenge: ({
profileId, profileId,
authzId accountId,
authzId,
challengeId
}: { }: {
profileId: string; profileId: string;
accountId: string;
authzId: string; authzId: string;
challengeId: string;
}) => Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>>; }) => Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>>;
}; };