Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

30 lines · 1.7 KBRaw History
1import { devAuth } from "./dev.js";
2import type { Env } from "../env.js";
3import { hmacSha256, matchesAnyKey, signingSecrets } from "../lib/crypto.js";
4import { b64url } from "../lib/util.js";
5
6/**
7 * Proof that someone can read the invited address's email. The invite link shown to the
8 * person who sent it creates nothing on its own: only the copy emailed to the invited
9 * address carries this proof, so nobody can join as an address they don't own (and so
10 * collect its other invitations, or a bootstrap administrator's rights).
11 * The key is derived from MASTER_KEY, so emailed links stay valid across deploys, and across a
12 * rotation of MASTER_KEY while the old one is kept as MASTER_KEY_PREV.
13 */
14function secrets(env: Env): string[] {
15 // Local development has no MASTER_KEY unless one is passed; production always does.
16 return signingSecrets(env, devAuth(env) ? "diffinery-dev-invite-proof" : null, "MASTER_KEY is not set, so invitation emails can't be signed");
17}
18
19const proofWith = async (secret: string, tokenHash: string): Promise<string> => b64url(await hmacSha256(`diffinery-invite-proof-v1:${secret}`, tokenHash));
20
21/** The proof for the invite whose token hashes to `tokenHash`. */
22export async function inviteProof(env: Env, tokenHash: string): Promise<string> {
23 return proofWith(secrets(env)[0]!, tokenHash);
24}
25
26/** Whether `proof` is the invite's proof, made with MASTER_KEY or, while it is being rotated, MASTER_KEY_PREV. */
27export async function validInviteProof(env: Env, tokenHash: string, proof: string | undefined | null): Promise<boolean> {
28 if (!proof || proof.length > 100) return false;
29 return matchesAnyKey(secrets(env), proof, (s) => proofWith(s, tokenHash));
30}