Skip to content

Commit

Permalink
improve a bunch of docs (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ycmjason authored Oct 10, 2024
1 parent 2163a85 commit c431b3c
Show file tree
Hide file tree
Showing 13 changed files with 475 additions and 254 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ jobs:
deno-version: v2.x # Run with latest stable Deno.
- run: deno fmt --check
- run: deno lint
- run: deno check --doc .
- run: deno test -A
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ This value is computed with

await challenge.submit();

await order.pollOrderStatus({
await order.pollStatus({
pollUntil: "ready",
onBeforeAttempt: () => {
console.log("this runs before every attempt");
Expand All @@ -231,16 +231,15 @@ const csrKeyPair = await order.finalize();
```

Once you have updated your DNS record according to the
`await challenge.digestToken()`, you can now submit the challenges!
`await challenge.digestToken()`, you can now submit the challenge!

You can use the `pollOrderStatus()` which return a promise that resolves when
the order status becomes `"ready"`, meaning that the CA has acknowledge your
completion of the challenges.
After submitting the challenge, you can use `acmeOrder.pollStatus()` to ensure
your order is `"ready"`, meaning that the CA has verified your challenge.

When the order status is `"ready"`, finalize the order by calling
Once the order status is `"ready"`, finalize the order by calling
`order.finalize()`. Under the hood, a
[Certificate Signing Request (CSR)](https://datatracker.ietf.org/doc/html/rfc2986)
will be generated and submit it to the CA. The CA would then verify and sign it,
is generated and submit it to the CA. The CA would then verify and sign it,
that's your certificate.

The private key for the CSR / the certificate you are going to obtain is
Expand All @@ -249,7 +248,9 @@ available at `csrKeyPair.privateKey`.
### 0x06: Download your CERTIFICATE!!!!

```ts
await order.pollOrderStatus({ pollUntil: "valid" });
//...

await order.pollStatus({ pollUntil: "valid" });

const certificatePemContent = await order.getCertificate();
```
Expand Down
6 changes: 3 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@fishballpkg/acme",
"version": "0.2.0",
"exports": "./src/mod.ts",
"tasks": {
"check": "deno check ."
"exports": {
".": "./src/mod.ts",
"./Dns01ChallengeUtils": "./src/Dns01ChallengeUtils.ts"
},
"compilerOptions": {
"strict": true,
Expand Down
11 changes: 7 additions & 4 deletions examples/acme-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
ACME_DIRECTORY_URLS,
AcmeClient,
Dns01ChallengeUtils,
} from "../src/mod.ts";
} from "@fishballpkg/acme";

export const DOMAIN = "dynm.link";
const EMAIL = "[email protected]";
Expand Down Expand Up @@ -49,7 +49,7 @@ console.log("Polling DNS to verify txt is updated...");
await Dns01ChallengeUtils.pollDnsTxtRecord({
domain: expectedRecord.domain,
pollUntil: expectedRecord.content,
onBeforeEachAttempt: () => {
onBeforeAttempt: () => {
console.log(`Looking up DNS records for ${expectedRecord.domain}...`);
},
onAfterFailAttempt: (records) => {
Expand All @@ -70,11 +70,14 @@ await Dns01ChallengeUtils.pollDnsTxtRecord({

console.log("Records found!");

console.log("Waiting for a further 15 before submitting, just to be safe...");
await new Promise((res) => setTimeout(res, 15000));

console.log("Submitting challenge...");
await dns01Challenge.submit();

console.log('Polling order status until "ready"...');
await acmeOrder.pollOrderStatus(
await acmeOrder.pollStatus(
{
pollUntil: "ready",
onBeforeAttempt: () => console.log("Fetching order..."),
Expand All @@ -92,7 +95,7 @@ const _csrKeyPair = await acmeOrder.finalize();
console.log("CSR submitted! Polling order till certificate is ready...");

await acmeOrder
.pollOrderStatus(
.pollStatus(
{
pollUntil: "valid",
onBeforeAttempt: () => console.log("polling order status..."),
Expand Down
3 changes: 3 additions & 0 deletions src/ACME_DIRECTORY_URLS.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* A Collection of common Certificate Authorities (CA) directories.
*/
export const ACME_DIRECTORY_URLS = {
BUYPASS: "https://api.buypass.com/acme/directory",
BUYPASS_STAGING: "https://api.test4.buypass.no/acme/directory",
Expand Down
23 changes: 14 additions & 9 deletions src/AcmeAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { AcmeClient } from "./AcmeClient.ts";
import { AcmeOrder, type AcmeOrderObjectSnapshot } from "./AcmeOrder.ts";

/**
* {@link AcmeAccount} represents an account you have created with {@link AcmeClient#createAccount}.
* {@link AcmeAccount} represents an account you have created with {@link AcmeClient.createAccount}.
*/
export class AcmeAccount {
client: AcmeClient;
keyPair: CryptoKeyPair;
url: string;
readonly client: AcmeClient;
readonly keyPair: CryptoKeyPair;
readonly url: string;

/**
* @internal AcmeAccount should be constructed with {@link AcmeClient#createAccount}
* @internal AcmeAccount should be constructed with {@link AcmeClient.createAccount}
*/
constructor({ client, keyPair, url }: {
client: AcmeClient;
Expand Down Expand Up @@ -45,14 +45,19 @@ export class AcmeAccount {
});
}

/**
* Create a certificate order to the Certificate Authority.
*/
async createOrder(
{ domains }: { domains: string[] },
{
domains,
}: {
domains: string[];
},
): Promise<AcmeOrder> {
const response = await this.client.jwsFetch(
const response = await this.jwsFetch(
this.client.directory.newOrder,
{
privateKey: this.keyPair.privateKey,
protected: { kid: this.url },
payload: {
identifiers: domains.map((domain) => ({
type: "dns",
Expand Down
153 changes: 95 additions & 58 deletions src/AcmeAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,80 @@ import {
type AcmeChallengeObjectSnapshot,
type AcmeChallengeType,
} from "./AcmeChallenge.ts";
import type { AcmeClient } from "./AcmeClient.ts";
import type { AcmeOrder } from "./AcmeOrder.ts";

/**
* Represents the status of an authorization.
*
* - `pending`: Waiting for challenge completion.
* - `valid`: Challenge successfully completed.
* - `invalid`: Certificate Authority (CA) cannot verify challenge and gave up.
* - `deactivated`: Manually deactivated.
* - `expired`: Not used in time.
* - `revoked`: Revoked for security reasons.
*
* @see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6
*/
export type AcmeAuthorizationStatus =
| "pending"
| "valid"
| "invalid"
| "deactivated"
| "expired"
| "revoked";

/**
* A snapshot of the authorization object retrieved from a Certificate Authority (CA).
*
* This can be retrieved by {@link AcmeAuthorization.fetch}
*
* @see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4
*/
export type AcmeAuthorizationObjectSnapshot = {
/**
* - `pending`: Waiting for challenge completion.
* - `valid`: Challenge successfully completed.
* - `invalid`: Certificate Authority (CA) cannot verify challenge and gave up.
* - `deactivated`: Manually deactivated.
* - `expired`: Not used in time.
* - `revoked`: Revoked for security reasons.
* @see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6
* The status of the authorization.
*/
status: AcmeAuthorizationStatus;
/**
* The timestamp after which the server will consider this authorization `invalid`.
*
* This field is REQUIRED for objects with `valid` in the `status` field.
*
* Format: ISO 8601 (e.g., `2024-10-10T14:30:00Z`).
*/
expires?: string;

/**
* The domain for which the certificate is being requested.
*
* Each object contains the type (typically "dns") and the value,
* which is the domain name.
*/
status:
| "pending"
| "valid"
| "invalid"
| "deactivated"
| "expired"
| "revoked";
expires?: string; // the expiry date of the authorization, optional if not applicable
identifier: {
type: "dns"; // identifier type, usually DNS
value: string; // the domain name
type: "dns";
/**
* The domain for this authorization.
*
* For wildcard authorizations, the value will be the base domain
* without the `*.` prefix. Instead, {@link AcmeAuthorizationObjectSnapshot.wildcard}
* will be set to `true`.
*/
value: string;
};
challenges: AcmeChallengeObjectSnapshot[];
wildcard?: boolean; // optional, true if the identifier is a wildcard domain
};

const fetchAuthorization = async (
{ url, account }: {
url: string;
account: AcmeAccount;
},
): Promise<AcmeAuthorizationObjectSnapshot> => {
const response = await account.jwsFetch(url);
/**
* A list of challenge objects for this authorization.
*
* Each challenge object contains details about a specific type
* of ACME challenge (e.g., DNS-01). The client must complete
* one of the challenges to prove control over the domain.
*/
challenges: AcmeChallengeObjectSnapshot[];

return await response.json();
/**
* Whether the authorization is for a wildcard domain.
*/
wildcard?: boolean;
};

/**
Expand All @@ -50,10 +86,15 @@ const fetchAuthorization = async (
* Tip: Think of it as a containment of some {@link AcmeChallenge}.
*/
export class AcmeAuthorization {
/** The {@link AcmeOrder} this authorization belongs to. */
readonly order: AcmeOrder;
/** The authorization url uniquely identifies the authorization and for retrieving {@link AcmeAuthorizationObjectSnapshot}. */
readonly url: string;
#challenges?: readonly AcmeChallenge[];

/**
* A list of {@link AcmeChallenge} the Certificate Authority can accept to verify control over this authorization / domain.
*/
get challenges(): readonly AcmeChallenge[] {
if (this.#challenges === undefined) {
throw new Error(
Expand All @@ -75,7 +116,10 @@ export class AcmeAuthorization {
this.url = url;
}

/** @internal */
/**
* Initialize the AcmeAuthorization object by fetching from the given authorization url and instantiate a list of {@link AcmeChallenge} accessible from {@link AcmeAuthorization.challenges}.
* @internal
*/
static async init({ order, url }: {
order: AcmeOrder;
url: string;
Expand All @@ -87,44 +131,37 @@ export class AcmeAuthorization {

const authorizationResponse = await authorization.fetch();

authorization.init({
challenges: authorizationResponse.challenges.map(
({ token, type, url }) =>
new AcmeChallenge({
authorization,
token,
type,
url,
}),
),
});
authorization.#challenges = authorizationResponse.challenges.map(
({ token, type, url }) =>
new AcmeChallenge({
authorization,
token,
type,
url,
}),
);

return authorization;
}

/**
* @internal
*/
init({
challenges,
}: {
challenges: AcmeChallenge[];
}) {
this.#challenges = challenges;
}

get client(): AcmeClient {
return this.order.client;
}

get account(): AcmeAccount {
get #account(): AcmeAccount {
return this.order.account;
}

/**
* Fetches a snapshot of the authorization object from the Certificate Authority (CA).
*/
async fetch(): Promise<AcmeAuthorizationObjectSnapshot> {
return await fetchAuthorization(this);
const response = await this.#account.jwsFetch(this.url);

return await response.json();
}

/**
* Find the {@link AcmeChallenge} as specified in `type`.
*
* To get the list of challenges, use {@link AcmeAuthorization.challenges}
*/
findChallenge(type: AcmeChallengeType): AcmeChallenge | undefined {
return this.challenges.find((challenge) => {
return challenge.type === type;
Expand Down
Loading

0 comments on commit c431b3c

Please sign in to comment.