Work on order

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:21 -08:00
parent ac82e8071d
commit 2b8a6b801d
5 changed files with 100 additions and 84 deletions
@@ -26,24 +26,9 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
}
};
const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => {
const findById = async (profileId: string, id: string, tx?: Knex) => {
try {
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*");
const [account] = result;
if (!account) {
return null;
}
return account;
} catch (error) {
throw new DatabaseError({ error, name: "Update PKI ACME account" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first();
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first();
return account || null;
} catch (error) {
@@ -51,16 +36,6 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
}
};
const findByProfileId = async (profileId: string, tx?: Knex) => {
try {
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first();
return account || null;
} catch (error) {
throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" });
}
};
const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => {
try {
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first();
@@ -71,35 +46,12 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
}
};
const findManyByProfileId = async (profileId: string, tx?: Knex) => {
try {
const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId });
return accounts;
} catch (error) {
throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" });
}
};
const deleteById = async (id: string, tx?: Knex) => {
try {
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*");
const [account] = result;
return account || null;
} catch (error) {
throw new DatabaseError({ error, name: "Delete PKI ACME account by id" });
}
};
return {
...pkiAcmeAccountOrm,
create,
updateById,
findById,
findByProfileId,
findByPublicKey,
findManyByProfileId,
deleteById
findByPublicKey
};
};
@@ -23,6 +23,7 @@ import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas";
import {
TAcmeResponse,
TAuthenciatedJwsPayload,
TCreateAcmeAccountPayload,
TCreateAcmeAccountResponse,
TCreateAcmeOrderPayload,
@@ -43,7 +44,7 @@ import {
type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById" | "findByPublicKey" | "create">;
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">;
};
@@ -69,6 +70,14 @@ export const pkiAcmeServiceFactory = ({
return `${baseUrl}${path}`;
};
const extractAccountIdFromKid = (kid: string, profileId: string): string => {
const kidPrefix = buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/`);
if (!kid.startsWith(kidPrefix)) {
throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" });
}
return kid.slice(kidPrefix.length);
};
const validateJwsPayload = async <T>(
rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
@@ -120,7 +129,7 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload,
async (protectedHeader) => {
if (!protectedHeader.jwk) {
throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" });
throw new AcmeMalformedError({ detail: "JWK is required in the protected header" });
}
return protectedHeader.jwk as unknown as JsonWebKey;
},
@@ -128,6 +137,36 @@ export const pkiAcmeServiceFactory = ({
);
};
const validateExistingAccountJwsPayload = async <T>(
profileId: string,
rawJwsPayload: TRawJwsPayload,
schema: z.ZodSchema<T>
): Promise<TAuthenciatedJwsPayload<T>> => {
const profile = await validateAcmeProfile(profileId);
const result = await validateJwsPayload(
rawJwsPayload,
async (protectedHeader) => {
if (!protectedHeader.kid) {
throw new AcmeMalformedError({ detail: "KID is required in the protected header" });
}
const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId);
const account = await acmeAccountDAL.findById(profile.id, accountId);
if (!account) {
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
}
if (account.alg !== protectedHeader.alg) {
throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" });
}
return account.publicKey as JsonWebKey;
},
schema
);
return {
...result,
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId)
};
};
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
await validateAcmeProfile(profileId);
return {
@@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({
};
};
const createAcmeOrder = async (
profileId: string,
account: TPkiAcmeAccounts,
payload: TCreateAcmeOrderPayload
): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
const profile = await validateAcmeProfile(profileId);
const createAcmeOrder = async ({
profileId,
accountId,
payload
}: {
profileId: string;
accountId: string;
payload: TCreateAcmeOrderPayload;
}): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
const account = await acmeAccountDAL.findById(profileId, accountId)!;
// TODO: check and see if we have existing orders for this account that meet the criteria
// if we do, return the existing order
orders = await acmeOrderDAL.create({
profileId,
accountId,
status: "pending"
});
// FIXME: Implement ACME new order creation
const orderId = "FIXME-order-id";
return {
@@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({
return {
validateJwsPayload,
validateNewAccountJwsPayload,
validateExistingAccountJwsPayload,
getAcmeDirectory,
getAcmeNewNonce,
createAcmeAccount,
@@ -42,6 +42,9 @@ export type TJwsPayload<T> = {
protectedHeader: TProtectedHeader;
payload: T;
};
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
accountId: string;
};
export type TAcmeResponse<TPayload> = {
status: number;
headers: Record<string, string>;
@@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = {
schema: z.ZodSchema<T>
) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: <T>(
profileId: string,
rawJwsPayload: TRawJwsPayload,
schema: z.ZodSchema<T>
) => Promise<TAuthenciatedJwsPayload<T>>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
getAcmeNewNonce: (profileId: string) => Promise<string>;
createAcmeAccount: ({
@@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = {
jwk: JsonWebKey;
payload: TCreateAcmeAccountPayload;
}) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
createAcmeOrder: (
profileId: string,
body: TCreateAcmeOrderPayload
) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
createAcmeOrder: ({
profileId,
accountId,
payload
}: {
profileId: string;
accountId: string;
payload: TCreateAcmeOrderPayload;
}) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
deactivateAcmeAccount: (
profileId: string,
accountId: string,