diff options
author | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:08:41 -0300 |
---|---|---|
committer | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:50:43 -0300 |
commit | f9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch) | |
tree | d545e325ba1ae756fc2eac66fac1001b6753c40d /src/components/signature/Authors.astro |
feat!: initial commit
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/components/signature/Authors.astro')
-rw-r--r-- | src/components/signature/Authors.astro | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro new file mode 100644 index 0000000..43a2b36 --- /dev/null +++ b/src/components/signature/Authors.astro @@ -0,0 +1,281 @@ +--- +import { toPK } from "@lib/pgp"; +import { createKeyFromArmor } from "@lib/pgp/create"; +import type { Verification } from "@lib/pgp/verify"; +import { defined, get, instanciate } from "@utils/anonymous"; +import { type CollectionEntry, z } from "astro:content"; +import type { EntityTypesEnum } from "src/consts"; +import qrcode from "yaqrcode"; + +interface Props { + verifications: NonNullable<Verification["verifications"]>; + expectedSigners: { + entity: CollectionEntry<"entity">; + role: z.infer<typeof EntityTypesEnum>; + }[]; + commitSignerKey?: string; +} + +const { + verifications: verificationsPromise, + expectedSigners, + commitSignerKey, +} = Astro.props; + +const fingerprintToData = new Map< + string, + { websites: URL[]; role: z.infer<typeof EntityTypesEnum> } +>(); + +for (const { entity, role } of expectedSigners) { + const key = await createKeyFromArmor(entity.data.publickey.armor); + const fingerprint = key.getFingerprint(); + fingerprintToData.set(fingerprint, { + websites: entity.data.websites?.map(instanciate(URL)) ?? [], + role, + }); +} + +let verifications = await Promise.all( + verificationsPromise.map(async ({ key, keyID, userID, verified }) => { + return { + key: await key, + keyID, + userID: await userID, + verified: await verified.catch(() => false), + }; + }), +); + +const expectedKeys = await Promise.all( + expectedSigners.map(get("entity")).map(({ data }) => + createKeyFromArmor(data.publickey.armor) + ), +); + +const expectedFingerprints = new Set( + expectedKeys.map((key) => key.getFingerprint()), +); + +const verifiedFingerprints = new Set( + verifications.map((v) => v.key).filter(defined).map(toPK).map((key) => + key.getFingerprint() + ), +); + +if (!expectedFingerprints.isSubsetOf(verifiedFingerprints)) { + throw new Error( + `Missing signature from expected signers: ${[ + ...expectedFingerprints.difference(verifiedFingerprints).values(), + ]}`, + ); +} +--- + +<div> + <table> + <caption> + <strong>Assinaturas</strong> + <p> + Para verificar uma assinatura é necessário a <a href="#message" + >mensagem</a>, a <a href="#signature">assinatura digital</a> e as <em + >chaves públicas</em> dos assinantes. Esta tabela mostra algumas + informações sobre os assinantes e as suas chaves públicas. + </p> + </caption> + <colgroup> + <col /> + <col /> + </colgroup> + <colgroup> + <col /> + <col /> + <col /> + </colgroup> + <thead> + <tr> + <th scope="col">Assinante</th> + <th scope="col">Função</th> + <th scope="col">Fingerprint</th> + <th scope="col">Válido</th> + <th scope="col">Commiter</th> + </tr> + </thead> + <tbody> + { + verifications.map(({ userID, key, keyID, verified }) => { + const fingerprint = key + ? toPK(key).getFingerprint() + : undefined; + const info = fingerprint + ? fingerprintToData.get(fingerprint) + : undefined; + const primary = userID?.[0]; + let role = ""; + switch (info?.role) { + case "author": { + role = "Autor"; + break; + } + case "co-author": { + role = "Co-autor"; + break; + } + case "translator": { + role = "Tradutor"; + break; + } + } + return ( + <tr> + <th scope="row"> + <address + itemprop="author" + itemscope + itemtype="https://schema.org/Person" + > + { + primary?.name + ? info?.websites[0] ? ( + <a + itemprop="url" + rel="author external noreferrer" + target="_blank" + href={info.websites[0]} + ><span itemprop="name">{primary.name}</span></a> + ) : ( + <span itemprop="name">{primary.name}</span> + ) + : primary?.email + ? ( + <><<a + itemprop="email" + rel="author external noreferrer" + target="_blank" + href={primary?.email && `mailto:${primary.email}`} + >{primary?.email}</a>></> + ) + : "" + } + { + primary && ( + <> + <button + popovertarget={`user-id-${fingerprint}`} + class="emoji" + > + ➕ + </button> + <section + class="user-id" + popover + id={`user-id-${fingerprint}`} + > + { + userID && ( + <><p><code>UserID</code>s</p><ul> + {userID.map((x) => <li>{x.userID}</li>)} + </ul></> + ) + } + { + info?.websites && ( + <><p>Websites</p><ul> + { + info.websites.map(( + x, + ) => ( + <li><a href={x}>{x}</a></li> + )) + } + </ul></> + ) + } + </section> + </> + ) + } + </address> + </th> + <td>{role}</td> + <td> + <><span title={fingerprint?.replace(/(....)/g, "$1 ")}>{ + key + ? "0x" + toPK(key).getKeyID().toHex() + : "0x" + keyID.toHex() + }</span> + { + key && false && ( + <img + src={qrcode(toPK(key).armor(), { + typeNumber: 40, + errorCorrectLevel: "L", + })} + /> + ) + } + { + key && + ( + <button popovertarget={`armor-${fingerprint}`}> + Armor + </button> + <section class="armor" popover id={`armor-${fingerprint}`}> + <pre><code>{toPK(key).armor()}</code></pre> + </section> + ) + } + </> + </td> + <td>{verified ? "✅" : "❌"}</td> + <td> + { + commitSignerKey && + key?.getFingerprint().toUpperCase()?.endsWith( + commitSignerKey.toUpperCase(), + ) && "✅" + } + </td> + </tr> + ); + }) + } + </tbody> + </table> +</div> + +<style> + div { + overflow-x: auto; + } + + table { + table-layout: fixed; + border-collapse: collapse; + border: 3px solid; + margin-inline: auto; + max-width: 90svw; + } + + th, + td { + padding: 1rem; + text-align: center; + } + + tbody tr:nth-child(odd) { + background-color: #e7e7e7; + } + + section[popover] { + text-align: initial; + } + section[popover].armor { + max-height: calc(200dvh / 3); + } + @media (prefers-color-scheme: dark) { + tbody tr:nth-child(odd) { + background-color: #181818; + } + } +</style> |