diff --git a/astro.config.mjs b/astro.config.mjs index c6a0b5a..3b45241 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -12,44 +12,162 @@ export default defineConfig({ htaccessIntegration({ generateHtaccessFile: import.meta.env.APACHE_CONFIG === "true", redirects: [ - { match: /^\/@\/(aryion|ekas?(portal)?)\b/, url: "https://aryion.com/g4/user/BadManners" }, - { match: /^\/@\/(blue[_-]?sky|bsky)\b/, url: "https://bsky.app/profile/badmanners.xyz" }, - { match: /^\/@\/(buymeacoffee|buy_me_a_coffee|buy-me-a-coffee|bmac)\b/, url: "https://www.buymeacoffee.com/BadMannersXYZ" }, - { match: /^\/@\/carrd\b/, url: "https://badmanners.carrd.co" }, - { match: /^\/@\/code[_-]?berg\b/, url: "https://codeberg.org/BadManners" }, - { match: /^\/@\/co[_-]?host\b/, url: "https://cohost.org/BadManners" }, - { match: /^\/@\/discord\b/, url: "/#discord" }, - { match: /^\/@\/e[_-]?mail\b/, url: "/#e-mail" }, - { match: /^\/@\/(fur[_-]?affinity|fa)\b/, url: "https://www.furaffinity.net/user/badmanners" }, - { match: /^\/@\/(gallery|stor(y|ies)|games?)\b/, url: "https://gallery.badmanners.xyz" }, - { match: /^\/@\/(git[_-]?hub|gh)\b/, url: "https://github.com/BadMannersXYZ" }, - { match: /^\/@\/git[_-]?lab\b/, url: "https://gitlab.com/Bad_Manners" }, - { match: /^\/@\/(git[_-]?gud|sapp?hire)\b/, url: "https://gitgud.io/BadMannersXYZ" }, - { match: /^\/@\/(google|g[_-]?mail)\b/, url: "/#gmail" }, - { match: /^\/@\/gum[_-]?road\b/, url: "https://badmanners.gumroad.com" }, - { match: /^\/@\/(ink[_-]?bunny|ib)\b/, url: "https://inkbunny.net/BadManners" }, - { match: /^\/@\/itaku\b/, url: "https://itaku.ee/profile/badmanners" }, - { match: /^\/@\/itch\b/, url: "https://bad-manners.itch.io" }, - { match: /^\/@\/keybase\b/, url: "https://keybase.io/badmanners" }, - { match: /^\/@\/keyoxide\b/, url: "https://keyoxide.org/aspe:keyoxide.org:UWYBVFCBFXTVUF2U6FS6AYJHLU" }, - { match: /^\/@\/ko[._-]?fi\b/, url: "https://ko-fi.com/badmanners" }, - { match: /^\/@\/(mastodon|meow[._-]?social|gulp[._-]?cafe)\b/, url: "https://meow.social/@BadManners" }, - { match: /^\/@\/neo[_-]?cities\b/, url: "https://badmanners.neocities.org" }, - { match: /^\/@\/picarto\b/, url: "https://www.picarto.tv/BadManners" }, - { match: /^\/@\/pillow[_-]?fort\b/, url: "https://www.pillowfort.social/BadManners" }, - { match: /^\/@\/pronouns?\b/, url: "https://pronouns.cc/@BadManners" }, - { match: /^\/@\/redd\.?it\b/, url: "https://www.reddit.com/user/BadManners_" }, - { match: /^\/@\/signal\b/, url: "https://signal.me/#eu/ytt_rk0fFmAB2JAW-x2PbUiJyc_H3kYmfL_Pq4QNh5QIDsiFtjdFHaqFRs1D36tB" }, - { match: /^\/@\/(so[_-]?furry|sf)\b/, url: "https://bad-manners.sofurry.com" }, - { match: /^\/@\/ssh\b/, url: "/ssh.pub" }, - { match: /^\/@\/steam(community|powered)?\b/, url: "https://steamcommunity.com/id/badmanners_" }, - { match: /^\/@\/subscribe[_-]?star\b/, url: "https://subscribestar.adult/bad-manners" }, - { match: /^\/@\/(telegram|t\.me)\b/, url: "https://t.me/bad_manners" }, - { match: /^\/@\/tumblr\b/, url: "https://www.tumblr.com/badmannersxyz" }, - { match: /^\/@\/twitch\b/, url: "https://www.twitch.tv/bad__manners" }, - { match: /^\/@\/weasyl\b/, url: "https://www.weasyl.com/~badmanners" }, - { match: /^\/@\/(x|twitter)\b/, url: "https://x.com/BadManners__" }, - { match: /^\/@\/(you[_-]?tube|youtu\.be|yt)\b/, url: "https://www.youtube.com/@BadMannersXYZ" }, + { + match: /^\/@\/(aryion|ekas?(portal)?)\b/, + url: "https://aryion.com/g4/user/BadManners", + }, + { + match: /^\/@\/(blue[_-]?sky|bsky)\b/, + url: "https://bsky.app/profile/badmanners.xyz", + }, + { + match: /^\/@\/(buymeacoffee|buy_me_a_coffee|buy-me-a-coffee|bmac)\b/, + url: "https://www.buymeacoffee.com/BadMannersXYZ", + }, + { + match: /^\/@\/carrd\b/, + url: "https://badmanners.carrd.co", + }, + { + match: /^\/@\/code[_-]?berg\b/, + url: "https://codeberg.org/BadManners", + }, + { + match: /^\/@\/co[_-]?host\b/, + url: "https://cohost.org/BadManners", + }, + { + match: /^\/@\/discord\b/, + url: "/#discord", + }, + { + match: /^\/@\/e[_-]?mail\b/, + url: "/#e-mail", + }, + { + match: /^\/@\/(fur[_-]?affinity|fa)\b/, + url: "https://www.furaffinity.net/user/badmanners", + }, + { + match: /^\/@\/(gallery|stor(y|ies)|games?)\b/, + url: "https://gallery.badmanners.xyz", + }, + { + match: /^\/@\/(git[_-]?hub|gh)\b/, + url: "https://github.com/BadMannersXYZ", + }, + { + match: /^\/@\/git[_-]?lab\b/, + url: "https://gitlab.com/Bad_Manners", + }, + { + match: /^\/@\/(git[_-]?gud|sapp?hire)\b/, + url: "https://gitgud.io/BadMannersXYZ", + }, + { + match: /^\/@\/(google|g[_-]?mail)\b/, + url: "/#gmail", + }, + { + match: /^\/@\/(gpg|pgp)\b/, + url: "/gpg.pub", + }, + { + match: /^\/@\/gum[_-]?road\b/, + url: "https://badmanners.gumroad.com", + }, + { + match: /^\/@\/(ink[_-]?bunny|ib)\b/, + url: "https://inkbunny.net/BadManners", + }, + { + match: /^\/@\/itaku\b/, + url: "https://itaku.ee/profile/badmanners", + }, + { + match: /^\/@\/itch\b/, + url: "https://bad-manners.itch.io", + }, + { + match: /^\/@\/keybase\b/, + url: "https://keybase.io/badmanners", + }, + { + match: /^\/@\/keyoxide\b/, + url: "https://keyoxide.org/aspe:keyoxide.org:UWYBVFCBFXTVUF2U6FS6AYJHLU", + }, + { + match: /^\/@\/ko[._-]?fi\b/, + url: "https://ko-fi.com/badmanners", + }, + { + match: /^\/@\/(mastodon|meow[._-]?social|gulp[._-]?cafe)\b/, + url: "https://meow.social/@BadManners", + }, + { + match: /^\/@\/neo[_-]?cities\b/, + url: "https://badmanners.neocities.org", + }, + { + match: /^\/@\/picarto\b/, + url: "https://www.picarto.tv/BadManners", + }, + { + match: /^\/@\/pillow[_-]?fort\b/, + url: "https://www.pillowfort.social/BadManners", + }, + { + match: /^\/@\/pronouns?\b/, + url: "https://pronouns.cc/@BadManners", + }, + { + match: /^\/@\/redd\.?it\b/, + url: "https://www.reddit.com/user/BadManners_", + }, + { + match: /^\/@\/signal\b/, + url: "https://signal.me/#eu/ytt_rk0fFmAB2JAW-x2PbUiJyc_H3kYmfL_Pq4QNh5QIDsiFtjdFHaqFRs1D36tB", + }, + { + match: /^\/@\/(so[_-]?furry|sf)\b/, + url: "https://bad-manners.sofurry.com", + }, + { + match: /^\/@\/ssh\b/, + url: "/ssh.pub", + }, + { + match: /^\/@\/steam(community|powered)?\b/, + url: "https://steamcommunity.com/id/badmanners_", + }, + { + match: /^\/@\/subscribe[_-]?star\b/, + url: "https://subscribestar.adult/bad-manners", + }, + { + match: /^\/@\/(telegram|t\.me)\b/, + url: "https://t.me/bad_manners", + }, + { + match: /^\/@\/tumblr\b/, + url: "https://www.tumblr.com/badmannersxyz", + }, + { + match: /^\/@\/twitch\b/, + url: "https://www.twitch.tv/bad__manners", + }, + { + match: /^\/@\/weasyl\b/, + url: "https://www.weasyl.com/~badmanners", + }, + { + match: /^\/@\/(x|twitter)\b/, + url: "https://x.com/BadManners__", + }, + { + match: /^\/@\/(you[_-]?tube|youtu\.be|yt)\b/, + url: "https://www.youtube.com/@BadMannersXYZ", + }, ], }), ], diff --git a/package-lock.json b/package-lock.json index ecb9d98..6d47db2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "badmanners.xyz", - "version": "2.1.9", + "version": "2.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "badmanners.xyz", - "version": "2.1.9", + "version": "2.1.10", "hasInstallScript": true, "dependencies": { "@astrojs/check": "^0.9.2", "@astrojs/rss": "^4.0.7", "@astrojs/tailwind": "^5.1.0", "astro": "^4.13.3", - "astro-htaccess": "^0.1.1", + "astro-htaccess": "^0.1.2", "date-fns": "^3.6.0", "tailwindcss": "^3.4.9", "tippy.js": "^6.3.7", @@ -2239,9 +2239,9 @@ } }, "node_modules/astro-htaccess": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/astro-htaccess/-/astro-htaccess-0.1.1.tgz", - "integrity": "sha512-GbsQZkAk62DW1CzZ5KgEuRYuZXqZXFeKt9w8MMAacmJoV5F+BfKws4x4gszpKqUi1wXSKG6psaPRPQrYIGsu0Q==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/astro-htaccess/-/astro-htaccess-0.1.2.tgz", + "integrity": "sha512-ki0y7bjhfeocMkPefQA7OT/BsFMYu6hgljkiK2No58FMgALn9w/nj+N9NMWykHMUe9pHUa209/EVtOuoO5UkQw==", "license": "MIT", "peerDependencies": { "astro": "^4.0.0" diff --git a/package.json b/package.json index 21dc457..f922585 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "badmanners.xyz", "type": "module", - "version": "2.1.9", + "version": "2.1.10", "scripts": { "postinstall": "astro sync", "dev": "astro dev", @@ -19,7 +19,7 @@ "@astrojs/rss": "^4.0.7", "@astrojs/tailwind": "^5.1.0", "astro": "^4.13.3", - "astro-htaccess": "^0.1.1", + "astro-htaccess": "^0.1.2", "date-fns": "^3.6.0", "tailwindcss": "^3.4.9", "tippy.js": "^6.3.7", diff --git a/public/gpg.pub b/public/gpg.pub new file mode 100644 index 0000000..f12ddf8 --- /dev/null +++ b/public/gpg.pub @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGbTDZABEADkemuZMb4ZFop4K4AhhY+ePo7nhr7HWSl2su77nEThe/Ebt68c +mqHmAdyBQncceUQPqyqYkymxAQw7vKG2ELsGc6Fs2zkQQvRftqbQDfrPxjRyL6Tf +6wbqFk1QRN6TaPRXL96xJ6haqG4GmJ8RFIqjqkZLwkjxEEtmdmgueMuOr1K2b0cS +8UkZz0Dgan16ZhANhi6xoqvDFISbahPg5sHJ7gR9dDxXj/fp5My1QzahRrva5SvY +fA8Z49GCjcgp68/EDzJYx0e7v4ovW19pbOnNeUZsDNbIsgGJBUbSSYkkcABrWx5T +KE4RhgmKPWI+nX3k4ABM/Y22ACjbf4s4dQeHquvkyQHWJ08OKjOs5pcz48c4yQwj +Te3/y1qEfaMMFveKfRoaN7vAmbqCmpDfl55JMJuHNj/0Msba9HrpBPVHd+FWxmhy +tE0XPUgk/gMeHBFRXs4/Vrc8QdFja0WvmHkO/HT+gztv7v9GOQYuWaoUQp8Z0AQj +8BKGXLebjukWgMRCC1x/Y1TQmEvV/fqlBEb7L6DFrYe44ra8T3CsA4EOCh5nO1nr +o88vZs+L1CK0UbuZCl15gvy4HanZpfuCXT/pgBkFhTCqvpZDnU7gKQLvrF07gACI +d9ZltbsHFyo17pruSAgOGJaJg8L2LLQQus5FXACaxb46xd7KbNwiPoDp2wARAQAB +tB9CYWQgTWFubmVycyA8bWVAYmFkbWFubmVycy54eXo+iQJUBBMBCgA+FiEEM7TV +jETRbsNkmQgfjIgpLMsHVgkFAmbTDZACGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYC +AwECHgECF4AACgkQjIgpLMsHVglazA/+INund6J+f8aXx2z6isAwU+/f4VwwbuEG +5GspV7yQFYzI8x+rnME/wX3i9GbdWEKzzoX9nzeAJqA3RgGd450XxA6Ir08lPqAz +658FisZIRCJo/pX2+VnHjXQO1aucBdksf4a43dDFgI3lElRFmOBMRR0C5ICJGmQh +ra25t9ivd7qjOoJIrldOukYiW0J55Qr7d/4X5zhio5at6pZkI6m5tMgTy/7hjuB+ +t6Yy8wDVfVSZLsZ2Q1rWR5jI9K7BdkmXXx+/VpZjF4N+3tpFclmw7NZRMp4qUvvg +74fBfVmW9LNVTQRT7H7Wd8arHxbDsKfc5ldHaDc5ex31pELtwtWqyicNHmvVKRKa +Mq2AjrnGLsglD/Rk/yrsEZrOHiq7Ree9SxKbAqQz23ikbChANFmE3pHmhTIiX1r7 +iLLWFcrTdGgSkic+C6d411YhGvZcDdK7msCIB7GZhxPBj62ttLGNXtkgUmK0lvzC +YAmZnqsiCirFLw8rQTL8o+/RM+iKiKd0iSNSkTpQQ+U8GwzYQjyiDdJwJjsWepbE +mog7H6j9prCXxmghqpu4WBNsnNvkE2DMykfz493KognQaT1om5+EIFfl9m5amw3w +nEYQNdj7Raa1yI4/keYDgzDV0K908NGQZgeXtjWoCGvBcxVnLlV3gbR+CfIaC/Vb +7rB7VJDFXN+5Ag0EZtMNkAEQAKCYLbuxNaaWC/EXVjLVoHs3R81ZWA0RFQXV1u2P +wgCwI7f9Nt6iljUiadXxOQZEZKFsLSOeWfPrb4mz1cJqc0DulImz2Z9Li55OAXC7 +XPjVTuqqdlR1z+PL/cLrRw3+fa/0U3/Xwp4PYQ5LOGr/VH1Y+NXSMH9mQ5ocLXOJ +Txy8ZT6KQ9NM4rbdgxnlWs4Wt9wj+dz/5bSxAwGbfKmI8BeVpCIu2dURrqGO/sL9 +6/StGCgYQT7odAuci5h23UC6e//YcKAfa4fs2YGpFZLfDNU3CiWrIOXGsURDP50X +ZK9Rz4xFYIhOj2xN7A+Fw7+A/npRzQYISNFQmEFYPZ0vFAawUa9sWvhvik7petnl +h/KN2DNELZQir2/HwXdqiG23Fl/P/93DO9lVuIwmx/i1biFmd8Wx3lrOdSp98T2M +gPDogGjolYK1Yr6cdlGx+Ir5x1M9JdZCkgo+pCvQ22lBDOVcrVM0KOAaTdV51NnC +1d/2dUmYygqy9Hq5i9Uf9F6LmoEIxpkx8TCr3m1FjW8oqu7vBZ6xpG8Dr67rpodd +yckpPAWvZpAzCCo+qAxiGgQlQauaIorf2Vx3HSctzVV2FwGQuMWCWY7Go1oJiQRN +8tqDIedhkWi7cCAS95kEX6utghfwxFun8Bi/vTWVA8Sl6tzBIiQJujFA1KCRDEdC +MprdABEBAAGJAjwEGAEKACYWIQQztNWMRNFuw2SZCB+MiCksywdWCQUCZtMNkAIb +DAUJA8JnAAAKCRCMiCksywdWCdM2D/kB1E46UKILIc0nVaCDwFcAk+rsRl5VGLvs +5QnAtccqS2bB4h+9RQCE4W2f19OeGVxet7dlLuqKGJGpVtGhMW0tx9q2Xzk2HiUa +4NQ3ugtTGDKDT0S+NcnUIKAN5MJi2VXANjdV+vjn4Zr2YqGP/XUtZO/xptGiyvrl +gAAOD+162ut6tT4CAHV+CJQk+45EjRxZ3DAhRlbxCqI77vn8mie6tXblTn7Mrear +7i2CPvDnencAFt+lBnr3rswvMLmriUzDHiT35uUQDLMj6YQsAX1fxRgpzOxcCxcz +xWi6Zh3E6RnjFh8MJb0vMRSbdmh1v7MrbmQtGQ3GwSv81GoPSwIkkUO4r6X+2o5f +eYcwfEewQQy0BrC+TmY5j2nahLWVI+4F53p5a0Jyeooryny0kZnp1D3njIa41+K3 +GOhS5oB56qvu5xThJ3xm38xGt9RBGjy6Z4JJWB52JNqB9k62MymyRWIzKUVqIs/P +/1h9NKxE45iPTnJ7FmuIWp7/ZACcI7b8etzO7+tMpiiM5yv20CzdD1xNJ+BUPOLm +j2dpaDdE54JbZSzPzdAGpsfFkLIO+eBw0OV79q7XjUjtWOvRZY/rEYjR/gyS4Gpe +Aorn78rt3isKA5fZarBKgTjPORXjyXVClMG2FtEsFfyfwVgaityNnnes6pgsjha7 +ASNiWqvMfg== +=VLiL +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/src/components/icons/IconKey.astro b/src/components/icons/IconKey.astro new file mode 100644 index 0000000..f59a1d2 --- /dev/null +++ b/src/components/icons/IconKey.astro @@ -0,0 +1,15 @@ +--- +import SVGIcon from "./SVGIcon.astro"; + +type Props = { + width: string; + height: string; + class?: string; +}; +--- + + + + diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index a1c0743..28475fa 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -2,6 +2,7 @@ export { default as IconArrowUpRightFromSquare } from "./IconArrowUpRightFromSqu export { default as IconBriefcase } from "./IconBriefcase.astro"; export { default as IconCommentDots } from "./IconCommentDots.astro"; export { default as IconEnvelope } from "./IconEnvelope.astro"; +export { default as IconKey } from "./IconKey.astro"; export { default as IconLink } from "./IconLink.astro"; export { default as IconMoon } from "./IconMoon.astro"; export { default as IconSquareRSS } from "./IconSquareRSS.astro"; diff --git a/src/data/licenses.toml b/src/data/licenses.toml index e7d2ee3..666eac2 100644 --- a/src/data/licenses.toml +++ b/src/data/licenses.toml @@ -15,7 +15,6 @@ type = "logo" notes = "The briefcase logo is copyrighted and trademarked by me. All rights reserved." [[copyright.additional]] -title = "Sam Brendan" type = "character" notes = "The character/fursona Sam Brendan is copyrighted and trademarked by me. All rights reserved." @@ -108,6 +107,7 @@ items = [ "briefcase", "comment-dots", "envelope", + "key", "link", "moon", "square-rss", diff --git a/src/pages/index.astro b/src/pages/index.astro index 16ad22e..fa23179 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,6 +1,7 @@ --- +import { readFile } from "node:fs/promises"; import BaseLayout from "../layouts/BaseLayout.astro"; -import { IconEnvelope, IconBriefcase, IconLink, IconCommentDots, IconSSH } from "../components/icons"; +import { IconEnvelope, IconBriefcase, IconKey, IconLink, IconCommentDots, IconSSH } from "../components/icons"; import { IconBluesky, IconCodeberg, @@ -33,6 +34,9 @@ import { IconX, IconYouTube, } from "../components/icons/brands"; + +const gpgKey = await readFile("./public/gpg.pub", { encoding: "utf-8" }); +const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" }); --- @@ -301,6 +305,23 @@ import {

badmanners.vore@gmail.com

+
  • + + +

    GPG public key

    +
    +
  • -

    aspe:keyoxide.org:UWYBVFCBFXTVUF2U6FS6AYJHLU on Keyoxide

    +

    + aspe:keyoxide.org:UWYBVFCBFXTVUF2U6FS6AYJHLU on Keyoxide +

  • @@ -499,7 +522,7 @@ import { class="u-key text-link group block w-full py-2 transition-colors motion-reduce:transition-none" href="/ssh.pub" aria-label="SSH public key" - data-clipboard="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ3QAZd3E95gxef2kiXppWa/xhcwBtnKMZJaW6s4d7Tm Bad Manners " + data-clipboard={sshKey} data-noun="SSH key" > { + if (!author) { + throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(copyright)})`); + } + if (typeof author !== "object" && typeof author !== "string") { + throw new Error( + `Invalid "${typeof author}" type for "author"${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + if (typeof author === "object" && !(author.name || author.url)) { + throw new Error( + `Missing both name and URL for "author" ${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + }); + // Source must be a valid string or list of strings + const sources = [copyright.source].flat(); + if (sources.length === 0) { + throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(copyright)})`); + } + sources.forEach((source) => { + if (!source) { + throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(copyright)})`); + } + if (typeof source !== "object" && typeof source !== "string") { + throw new Error( + `Invalid "${typeof source}" type for "source"${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + if (typeof source === "object" && !source.url) { + throw new Error( + `Missing URL for "source" ${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + }); + // License must be a valid string or object or list + const licenses = [copyright.license].flat(); + if (licenses.length === 0) { + throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(copyright)})`); + } + licenses.forEach((license) => { + if (!license) { + throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(copyright)})`); + } + if (typeof license !== "object" && typeof license !== "string") { + throw new Error( + `Invalid "${typeof license}" type for "license"${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + if (typeof license === "object" && !(license.name || license.url)) { + throw new Error( + `Missing both name and URL for "license" ${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(copyright)})`, + ); + } + }); +} + +/** + * Verifies attributions and copyright according to the [Creative Commons recommended practices](https://wiki.creativecommons.org/wiki/Recommended_practices_for_attribution) * @param copyright Unparsed TOML copyright information. */ function verifyAttributions(licenses: string) { const { copyright, attributions } = parse(licenses); - // Make sure each copyright and attribution follows the TASL format. - // - T: title (or description) - // - A: author - // - S: source - // - L: license - // - other fields that have custom validation: type, notes, items + // Make sure each copyright and attribution follows the TASL format, + // and that other fields (type, notes, items) pass their custom validation. [copyright, attributions].flat().forEach((value) => { - // Title or description must be a valid string - const title = value.title ?? value.description; - if (typeof title !== "string" || !title) { - throw new Error(`Missing "title" and/or "description" for attribution (${JSON.stringify(value)})`); - } - // Author must be a valid string or object or list - const authors = [value.author].flat(); - if (authors.length === 0) { - throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`); - } - authors.forEach((author) => { - if (!author) { - throw new Error(`Missing "author" for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof author !== "object" && typeof author !== "string") { - throw new Error(`Invalid "${typeof author}" type for "author"${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof author === "object" && !(author.name || author.url)) { - throw new Error(`Missing both name and URL for "author" ${JSON.stringify(author)} for attribution "${title}" (${JSON.stringify(value)})`); - } - }); - // Source must be a valid string or list of strings - const sources = [value.source].flat(); - if (sources.length === 0) { - throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`); - } - sources.forEach((source) => { - if (!source) { - throw new Error(`Missing "source" for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof source !== "object" && typeof source !== "string") { - throw new Error(`Invalid "${typeof source}" type for "source"${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof source === "object" && !(source.url)) { - throw new Error(`Missing URL for "source" ${JSON.stringify(source)} for attribution "${title}" (${JSON.stringify(value)})`); - } - }); - // License must be a valid string or object or list - const licenses = [value.license].flat(); - if (licenses.length === 0) { - throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`); - } - licenses.forEach((license) => { - if (!license) { - throw new Error(`Missing "license" for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof license !== "object" && typeof license !== "string") { - throw new Error(`Invalid "${typeof license}" type for "license"${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`); - } - if (typeof license === "object" && !(license.name || license.url)) { - throw new Error(`Missing both name and URL for "license" ${JSON.stringify(license)} for attribution "${title}" (${JSON.stringify(value)})`); - } - }); + // Validate TASL + validateTASL(value); + const title = copyright.title ?? copyright.description; // Validate extra optional fields // 1. Type must be a valid string if (typeof value.type !== "string") { @@ -80,13 +97,17 @@ function verifyAttributions(licenses: string) { if ("items" in value) { const items = value.items; if (!Array.isArray(items)) { - throw new Error(`Invalid non-array "items" ${JSON.stringify(items)} for attribution "${title}" (${JSON.stringify(value)})`); + throw new Error( + `Invalid non-array "items" ${JSON.stringify(items)} for attribution "${title}" (${JSON.stringify(value)})`, + ); } items.forEach((item) => { if (!item) { - throw new Error(`Invalid item ${JSON.stringify} in "items" for attribution "${title}" (${JSON.stringify(value)})`); + throw new Error( + `Invalid item ${JSON.stringify} in "items" for attribution "${title}" (${JSON.stringify(value)})`, + ); } - }) + }); } // 3. Type must be a valid string if ("notes" in value) { @@ -106,18 +127,33 @@ function verifyAttributions(licenses: string) { if ("additional" in copyright) { const additionals = copyright.additional; if (!Array.isArray(additionals)) { - throw new Error(`Invalid non-array "additional" ${JSON.stringify(additionals)} for copyright (${JSON.stringify(copyright)})`); + throw new Error( + `Invalid non-array "additional" ${JSON.stringify(additionals)} for copyright (${JSON.stringify(copyright)})`, + ); } additionals.forEach((additional) => { if (typeof additional.notes !== "string" || !additional.notes) { throw new Error(`Invalid "notes" for additional copyright (${JSON.stringify(additional)})`); } - }) + if (typeof additional.type !== "string" || !additional.type) { + throw new Error(`Invalid "type" for additional copyright (${JSON.stringify(additional)})`); + } + // Check TASL + date if title or description is present + if (additional.title || additional.description) { + validateTASL(additional); + if (typeof additional.date !== "string") { + throw new Error(`Invalid "date" for additional (${JSON.stringify(additional)})`); + } + if (!additional.date) { + throw new Error(`Missing "date" for additional (${JSON.stringify(additional)})`); + } + } + }); } } export const GET: APIRoute = async () => { const licenses = await readFile("./src/data/licenses.toml", { encoding: "utf-8" }); verifyAttributions(licenses); - return new Response(licenses, { headers: { "Content-Type": "application/toml; charset=utf-8" } }) + return new Response(licenses, { headers: { "Content-Type": "application/toml; charset=utf-8" } }); };