Add link headers

This commit is contained in:
Fang-Pen Lin
2025-10-30 21:56:00 -07:00
parent d14d64e11e
commit ea96cc87bb
4 changed files with 101 additions and 45 deletions

View File

@@ -424,7 +424,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// Respond to Challenge (RFC 8555 Section 7.5.1)
server.route({
method: "POST",
url: "/profiles/:profileId/authorizations/:authzId/challenges/http-01",
url: "/profiles/:profileId/authorizations/:authzId/challenges/:challengeId",
config: {
rateLimit: writeLimit
},
@@ -433,7 +433,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiAcme],
description: "ACME Respond to Challenge - let ACME server know challenge is ready",
params: SharedParamsSchema.extend({
authzId: z.string().uuid()
authzId: z.string().uuid(),
challengeId: z.string().uuid()
}),
response: {
200: RespondToAcmeChallengeResponseSchema
@@ -447,9 +448,13 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
return sendAcmeResponse(
res,
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;
}
});
};

View File

@@ -1,13 +1,41 @@
import { TDbClient } from "@app/db";
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 const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
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 {
...pkiAcmeChallengeOrm
...pkiAcmeChallengeOrm,
findByAccountAuthAndChallengeIdWithToken
};
};

View File

@@ -61,7 +61,7 @@ type TPkiAcmeServiceFactoryDep = {
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create" | "transaction" | "findByAccountAndOrderIdWithAuthorizations">;
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
acmeOrderAuthDAL: Pick<TPkiAcmeOrderAuthDALFactory, "insertMany">;
acmeChallengeDAL: Pick<TPkiAcmeChallengeDALFactory, "create">;
acmeChallengeDAL: Pick<TPkiAcmeChallengeDALFactory, "create" | "findByAccountAuthAndChallengeIdWithToken">;
};
export const pkiAcmeServiceFactory = ({
@@ -300,9 +300,10 @@ export const pkiAcmeServiceFactory = ({
contact: existingAccount.emails,
orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`)
},
headers: {
Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`)
}
headers: [
["Location", buildUrl(profile.id, `/accounts/${existingAccount.id}`)],
["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`]
]
};
}
@@ -321,9 +322,10 @@ export const pkiAcmeServiceFactory = ({
contact: newAccount.emails,
orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`)
},
headers: {
Location: buildUrl(profile.id, `/accounts/${newAccount.id}`)
}
headers: [
["Location", buildUrl(profile.id, `/accounts/${newAccount.id}`)],
["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`]
]
};
};
@@ -343,9 +345,10 @@ export const pkiAcmeServiceFactory = ({
body: {
status: "deactivated"
},
headers: {
Location: buildUrl(profileId, `/accounts/${accountId}`)
}
headers: [
["Location", buildUrl(profileId, `/accounts/${accountId}`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
@@ -429,9 +432,10 @@ export const pkiAcmeServiceFactory = ({
profileId,
order
}),
headers: {
Location: buildUrl(profileId, `/orders/${order.id}`)
}
headers: [
["Location", buildUrl(profileId, `/orders/${order.id}`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
@@ -451,7 +455,10 @@ export const pkiAcmeServiceFactory = ({
return {
status: 200,
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 {
status: 200,
body: buildAcmeOrderResource({ profileId, order }),
headers: {
Location: buildUrl(profileId, `/orders/${orderId}`)
}
headers: [
["Location", buildUrl(profileId, `/orders/${orderId}`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
@@ -499,9 +507,10 @@ export const pkiAcmeServiceFactory = ({
return {
status: 200,
body: "FIXME-certificate-pem",
headers: {
Location: buildUrl(profileId, `/orders/${orderId}/certificate`)
}
headers: [
["Location", buildUrl(profileId, `/orders/${orderId}/certificate`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
@@ -519,9 +528,10 @@ export const pkiAcmeServiceFactory = ({
body: {
orders: []
},
headers: {
Location: buildUrl(profileId, `/accounts/${accountId}/orders`)
}
headers: [
["Location", buildUrl(profileId, `/accounts/${accountId}/orders`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
@@ -559,33 +569,42 @@ export const pkiAcmeServiceFactory = ({
};
})
},
headers: {
Location: buildUrl(profileId, `/authorizations/${authzId}`)
}
headers: [
["Location", buildUrl(profileId, `/authorizations/${authzId}`)],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};
const respondToAcmeChallenge = async ({
profileId,
authzId
accountId,
authzId,
challengeId
}: {
profileId: string;
accountId: string;
authzId: string;
challengeId: string;
}): Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>> => {
const profile = await validateAcmeProfile(profileId);
// FIXME: Implement ACME challenge response
// Trigger verification process
const challenge = await acmeChallengeDAL.findByAccountAuthAndChallengeIdWithToken(accountId, authzId, challengeId);
if (!challenge) {
throw new NotFoundError({ message: "ACME challenge not found" });
}
// TODO: Implement ACME challenge response
return {
status: 200,
body: {
type: "http-01",
url: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`),
status: "pending",
token: "FIXME-challenge-token"
type: challenge.type,
url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`),
status: challenge.status,
token: challenge.token
},
headers: {
Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)
}
headers: [
["Location", buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)],
["Link", `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`],
["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`]
]
};
};

View File

@@ -44,7 +44,7 @@ export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
};
export type TAcmeResponse<TPayload> = {
status: number;
headers: Record<string, string>;
headers: [string, string][];
body: TPayload;
};
@@ -164,9 +164,13 @@ export type TPkiAcmeServiceFactory = {
}) => Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>>;
respondToAcmeChallenge: ({
profileId,
authzId
accountId,
authzId,
challengeId
}: {
profileId: string;
accountId: string;
authzId: string;
challengeId: string;
}) => Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>>;
};