summaryrefslogtreecommitdiff
path: root/src/components/signature/Authors.astro
diff options
context:
space:
mode:
authorJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:08:41 -0300
committerJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:50:43 -0300
commitf9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch)
treed545e325ba1ae756fc2eac66fac1001b6753c40d /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.astro281
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
+ ? (
+ <>&lt;<a
+ itemprop="email"
+ rel="author external noreferrer"
+ target="_blank"
+ href={primary?.email && `mailto:${primary.email}`}
+ >{primary?.email}</a>&gt;</>
+ )
+ : ""
+ }
+ {
+ 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>