From f0463229a1507fe5da1adc39cd38c5c912df8a39 Mon Sep 17 00:00:00 2001 From: Julian Lechner Date: Fri, 15 May 2026 15:35:31 +0200 Subject: [PATCH] adding opgenpgp key --- .dockerignore | 1 + .gitignore | 1 + angular.json | 41 ++++- package.json | 3 +- scripts/generate-openpgp-assets.cjs | 180 +++++++++++++++++++++ scripts/static-server.mjs | 61 ++----- src/app/generated/openpgp-key.generated.ts | 5 + src/app/home/home.component.html | 16 ++ src/app/home/home.component.ts | 2 + src/assets/unoptimized/public.asc | 91 +++++++++++ src/languages/de.ts | 4 + src/languages/en.ts | 4 + src/languages/language.types.ts | 4 + src/styles.scss | 68 ++++++++ tsconfig.json | 29 ++-- 15 files changed, 442 insertions(+), 68 deletions(-) create mode 100644 scripts/generate-openpgp-assets.cjs create mode 100644 src/app/generated/openpgp-key.generated.ts create mode 100644 src/assets/unoptimized/public.asc diff --git a/.dockerignore b/.dockerignore index b93678b..cb7a1ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,7 @@ .angular node_modules dist +public .npm-cache coverage .eslintcache diff --git a/.gitignore b/.gitignore index 7c8d8ab..16c0f78 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Generierte Build- und Test-Ausgaben /dist /coverage +/public /src/assets/optimized # Betriebssystem-Dateileichen diff --git a/angular.json b/angular.json index c4b4ee9..8a82556 100644 --- a/angular.json +++ b/angular.json @@ -17,12 +17,29 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist", - "index": "src/index.html", + "outputPath": { + "base": "dist", + "browser": "browser", + "media": "media" + }, + "outputMode": "static", + "index": { + "input": "src/index.html", + "output": "index.html", + "preloadInitial": true + }, "browser": "src/main.ts", + "server": false, "polyfills": [], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["src"] + }, + "security": { + "autoCsp": true + }, + "clearScreen": false, "assets": [ { "glob": "**/*", @@ -35,7 +52,8 @@ } ], "styles": ["src/styles.scss"], - "scripts": [] + "scripts": [], + "serviceWorker": false }, "configurations": { "production": { @@ -43,13 +61,16 @@ "scripts": true, "styles": { "minify": true, - "inlineCritical": true + "inlineCritical": true, + "removeSpecialComments": true }, "fonts": { "inline": true } }, "outputHashing": "all", + "crossOrigin": "anonymous", + "subresourceIntegrity": true, "sourceMap": false, "namedChunks": false, "extractLicenses": true, @@ -64,13 +85,23 @@ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" + }, + { + "type": "anyScript", + "maximumWarning": "350kB", + "maximumError": "500kB" } ] }, "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true, + "sourceMap": { + "scripts": true, + "styles": true, + "vendor": false, + "sourcesContent": true + }, "namedChunks": true } }, diff --git a/package.json b/package.json index f842080..cabaa3c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "------ BUILD": "", "generate:static-files": "node scripts/generate-static-files.cjs", "generate:optimized-assets": "node scripts/generate-optimized-assets.cjs", - "generate:assets": "npm run generate:static-files && npm run generate:optimized-assets", + "generate:openpgp-assets": "node scripts/generate-openpgp-assets.cjs", + "generate:assets": "npm run generate:static-files && npm run generate:optimized-assets && npm run generate:openpgp-assets", "build": "npm run generate:assets && ng build --configuration production && node scripts/copy-static-root-files.cjs", "------ STARTUP": "", "test": "npm run generate:assets && ng test", diff --git a/scripts/generate-openpgp-assets.cjs b/scripts/generate-openpgp-assets.cjs new file mode 100644 index 0000000..1a67e61 --- /dev/null +++ b/scripts/generate-openpgp-assets.cjs @@ -0,0 +1,180 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +const workspaceRoot = process.cwd(); +const sourceKeyPath = path.join(workspaceRoot, 'src', 'assets', 'unoptimized', 'public.asc'); +const publicKeyOutputPath = path.join(workspaceRoot, 'public', 'keys', 'public.asc'); +const generatedModulePath = path.join( + workspaceRoot, + 'src', + 'app', + 'generated', + 'openpgp-key.generated.ts', +); + +function ensureDirectory(directoryPath) { + fs.mkdirSync(directoryPath, { recursive: true }); +} + +function readAsciiArmoredKey(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error(`Missing OpenPGP public key file at ${filePath}`); + } + + return fs.readFileSync(filePath, 'utf8'); +} + +function decodeArmoredKey(armoredKey) { + const base64Lines = armoredKey + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter( + (line) => + line.length > 0 && + !line.startsWith('-----BEGIN ') && + !line.startsWith('-----END ') && + !line.includes(':') && + !line.startsWith('='), + ); + + if (base64Lines.length === 0) { + throw new Error('OpenPGP public key armor did not contain any base64 payload.'); + } + + return Buffer.from(base64Lines.join(''), 'base64'); +} + +function parsePacketLength(buffer, offset, newFormat, lengthType) { + if (newFormat) { + const firstLengthOctet = buffer[offset]; + if (firstLengthOctet < 192) { + return { headerLength: 1, packetLength: firstLengthOctet }; + } + + if (firstLengthOctet < 224) { + const secondLengthOctet = buffer[offset + 1]; + const packetLength = ((firstLengthOctet - 192) << 8) + secondLengthOctet + 192; + return { headerLength: 2, packetLength }; + } + + if (firstLengthOctet === 255) { + return { + headerLength: 5, + packetLength: buffer.readUInt32BE(offset + 1), + }; + } + + throw new Error('OpenPGP partial body lengths are not supported by this generator.'); + } + + if (lengthType === 0) { + return { headerLength: 1, packetLength: buffer[offset] }; + } + + if (lengthType === 1) { + return { headerLength: 2, packetLength: buffer.readUInt16BE(offset) }; + } + + if (lengthType === 2) { + return { headerLength: 4, packetLength: buffer.readUInt32BE(offset) }; + } + + return { headerLength: 0, packetLength: buffer.length - offset }; +} + +function readPackets(buffer) { + const packets = []; + let offset = 0; + + while (offset < buffer.length) { + const headerOctet = buffer[offset]; + if ((headerOctet & 0x80) === 0) { + throw new Error(`Invalid OpenPGP packet header at byte offset ${offset}.`); + } + + offset += 1; + + const newFormat = (headerOctet & 0x40) !== 0; + const tag = newFormat ? headerOctet & 0x3f : (headerOctet >> 2) & 0x0f; + const lengthType = headerOctet & 0x03; + const { headerLength, packetLength } = parsePacketLength(buffer, offset, newFormat, lengthType); + + offset += headerLength; + const body = buffer.subarray(offset, offset + packetLength); + packets.push({ tag, body }); + offset += packetLength; + } + + return packets; +} + +function calculateFingerprintFromPacket(publicKeyPacketBody) { + const version = publicKeyPacketBody[0]; + + if (version === 4) { + const lengthBytes = Buffer.alloc(2); + lengthBytes.writeUInt16BE(publicKeyPacketBody.length, 0); + return crypto + .createHash('sha1') + .update(Buffer.concat([Buffer.from([0x99]), lengthBytes, publicKeyPacketBody])) + .digest('hex') + .toUpperCase(); + } + + if (version === 5 || version === 6) { + const lengthBytes = Buffer.alloc(4); + lengthBytes.writeUInt32BE(publicKeyPacketBody.length, 0); + return crypto + .createHash('sha256') + .update(Buffer.concat([Buffer.from([0x9a]), lengthBytes, publicKeyPacketBody])) + .digest('hex') + .toUpperCase(); + } + + throw new Error(`Unsupported OpenPGP public key version ${version}.`); +} + +function formatFingerprint(fingerprint) { + return fingerprint.match(/.{1,4}/gu)?.join(' ') ?? fingerprint; +} + +function buildGeneratedModule(assetPath, fingerprint) { + return [ + 'export const openPgpKey = {', + ` assetPath: '${assetPath}',`, + ` fingerprint: '${fingerprint}',`, + ` formattedFingerprint: '${formatFingerprint(fingerprint)}',`, + '} as const;', + '', + ].join('\n'); +} + +function run() { + const armoredKey = readAsciiArmoredKey(sourceKeyPath); + const binaryKey = decodeArmoredKey(armoredKey); + const packets = readPackets(binaryKey); + const publicKeyPacket = packets.find((packet) => packet.tag === 6); + + if (!publicKeyPacket) { + throw new Error('Could not find a public-key packet in the provided OpenPGP key.'); + } + + const fingerprint = calculateFingerprintFromPacket(publicKeyPacket.body); + + ensureDirectory(path.dirname(publicKeyOutputPath)); + ensureDirectory(path.dirname(generatedModulePath)); + + fs.copyFileSync(sourceKeyPath, publicKeyOutputPath); + fs.writeFileSync( + generatedModulePath, + buildGeneratedModule('/keys/public.asc', fingerprint), + 'utf8', + ); + + console.log(`Copied ${path.relative(workspaceRoot, publicKeyOutputPath)}`); + console.log(`Generated ${path.relative(workspaceRoot, generatedModulePath)}`); + console.log(`OpenPGP fingerprint: ${formatFingerprint(fingerprint)}`); +} + +run(); diff --git a/scripts/static-server.mjs b/scripts/static-server.mjs index 7866344..a61dfa3 100644 --- a/scripts/static-server.mjs +++ b/scripts/static-server.mjs @@ -1,5 +1,5 @@ import { createReadStream } from 'node:fs'; -import { access, readdir, stat } from 'node:fs/promises'; +import { access, stat } from 'node:fs/promises'; import { createServer } from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,15 +9,13 @@ const __dirname = path.dirname(__filename); const appRoot = path.resolve(__dirname, '..'); const distRoot = path.join(appRoot, 'dist'); -const port = Number.parseInt(process.env.PORT ?? '3000', 10); +const browserRoot = path.join(distRoot, 'browser'); +const port = 3000; const mimeTypes = new Map([ ['.css', 'text/css; charset=utf-8'], - ['.gif', 'image/gif'], ['.html', 'text/html; charset=utf-8'], ['.ico', 'image/x-icon'], - ['.jpg', 'image/jpeg'], - ['.jpeg', 'image/jpeg'], ['.js', 'text/javascript; charset=utf-8'], ['.json', 'application/json; charset=utf-8'], ['.map', 'application/json; charset=utf-8'], @@ -25,8 +23,6 @@ const mimeTypes = new Map([ ['.svg', 'image/svg+xml'], ['.txt', 'text/plain; charset=utf-8'], ['.webp', 'image/webp'], - ['.woff', 'font/woff'], - ['.woff2', 'font/woff2'], ['.xml', 'application/xml; charset=utf-8'], ]); @@ -48,23 +44,9 @@ function getCacheControl(filePath) { } if ( - [ - '.css', - '.gif', - '.ico', - '.jpg', - '.jpeg', - '.js', - '.json', - '.map', - '.png', - '.svg', - '.txt', - '.webp', - '.woff', - '.woff2', - '.xml', - ].includes(extension) + ['.css', '.ico', '.js', '.json', '.map', '.png', '.svg', '.txt', '.webp', '.xml'].includes( + extension, + ) ) { return normalizedPath.includes('/browser/') ? 'public, max-age=604800, stale-while-revalidate=86400' @@ -83,36 +65,13 @@ async function pathExists(targetPath) { } } -async function resolveBrowserRoot() { - const directBrowserRoot = path.join(distRoot, 'browser'); - if (await pathExists(path.join(directBrowserRoot, 'index.html'))) { - return directBrowserRoot; - } - - const distEntries = await readdir(distRoot, { withFileTypes: true }); - for (const entry of distEntries) { - if (!entry.isDirectory()) { - continue; - } - - const nestedBrowserRoot = path.join(distRoot, entry.name, 'browser'); - if (await pathExists(path.join(nestedBrowserRoot, 'index.html'))) { - return nestedBrowserRoot; - } - } - - throw new Error(`Could not locate a browser build under ${distRoot}`); -} - -const browserRoot = await resolveBrowserRoot(); - async function resolveFile(urlPath) { const normalized = path.posix.normalize(decodeURIComponent(urlPath)); const relativePath = normalized.replace(/^(\.\.(\/|\\|$))+/, '').replace(/^\/+/, ''); - const distCandidate = path.join(distRoot, relativePath); - if ((await pathExists(distCandidate)) && (await stat(distCandidate)).isFile()) { - return distCandidate; + const rootCandidate = path.join(distRoot, relativePath); + if ((await pathExists(rootCandidate)) && (await stat(rootCandidate)).isFile()) { + return rootCandidate; } const browserCandidate = path.join(browserRoot, relativePath); @@ -151,5 +110,5 @@ const server = createServer(async (request, response) => { }); server.listen(port, '0.0.0.0', () => { - console.log(`Serving static app from ${browserRoot} on port ${port}`); + console.log(`Serving static app on port ${port}`); }); diff --git a/src/app/generated/openpgp-key.generated.ts b/src/app/generated/openpgp-key.generated.ts new file mode 100644 index 0000000..98ab7d2 --- /dev/null +++ b/src/app/generated/openpgp-key.generated.ts @@ -0,0 +1,5 @@ +export const openPgpKey = { + assetPath: '/keys/public.asc', + fingerprint: '98FA30F85F2030A70F45AAEB3CDEA2DE5424B8E7', + formattedFingerprint: '98FA 30F8 5F20 30A7 0F45 AAEB 3CDE A2DE 5424 B8E7', +} as const; diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index cd32c88..847f21c 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -75,6 +75,22 @@ +
+
+

{{ content().home.openPgpTitle }}

+

{{ content().home.openPgpDescription }}

+
+
+ + {{ content().home.openPgpDownload }} + +

+ {{ content().home.openPgpFingerprintLabel }} + {{ openPgpKey.formattedFingerprint }} +

+
+
+

{{ content().home.aboutEyebrow }}

diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index c6a0124..0bc15f2 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -12,6 +12,7 @@ import { interval } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { global } from '../../global'; +import { openPgpKey } from '../generated/openpgp-key.generated'; import type { LanguageBioTextEntry, LanguagePortraitHighlight, @@ -71,6 +72,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy { protected readonly global = global; protected readonly age = this.calculateAge(global.birthdate); protected readonly contactMailHref = `mailto:${global.contactMail}`; + protected readonly openPgpKey = openPgpKey; protected readonly arrowRightIcon = arrowRightIcon; protected readonly arrowDownIcon = arrowDownIcon; protected readonly envelopeIcon = envelopeIcon; diff --git a/src/assets/unoptimized/public.asc b/src/assets/unoptimized/public.asc new file mode 100644 index 0000000..a9fb050 --- /dev/null +++ b/src/assets/unoptimized/public.asc @@ -0,0 +1,91 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGoC8R8BEACq5XrqV728z2hYvEZCep8T3729VoFia70l7qoz1o6aDCovlIRG +PGMpcRHin0rqI2pAVHha8bMJnm34JdlPY4Dk/KI31hwLNEoTw8xzfl+58ehEnSvG +o1v3OkXC7bx4zxarFllo4/zgbiKRi1SihXZe/Ytonkm/OX0oHmwEsWlL4Gu2rX+p +Wwg/ks+l6hJ9NiB3D+F9rGfryrFgFTzV90C+1P2bOn/4NXUrr50T92Dn9zXSA1oe +k4ebnZ9df9uRlRaftNtcL/8nY7KGYr1tDBsgL2HWxknzhNoZQiFc0QJ5EjN4dz+s +kkn2aKSlzlOzBGgDeCfk6xMAyLKEewoCtagD0Fo64Mg6W5nPa8lg9xG4soGXBC2d +gUCyQPAcIw7sW71rawuFon12K+Sn02kZ0gHZVtCULh1Z66zTnw6D+fT2khbcAE85 +81tuBDVrfv5WjZtSH6t7FqHOmyvLDEPDpudJKOxxuVJfjpfhhXhK17K/8148zlJH +Vmgar3Ilx/Ef0RerDD3boCXEWT0SRyFETY58iK1Vhu5hCojcn10cs/QK8X6RdQ6D +Y2WtbQ7mebqFJWyetuV1ZAFDsNnXtonODBSvhHBtqzI4IL8hFZwiM6cgM3WqfGfk +svTC5O1lePp0L/jrP0fiYjw8wv/f4wEKeM+0xOTkvXbFxlibLxZ9AS8NfQARAQAB +tCNKdWxpYW4gTGVjaG5lciA8anVsaWFuQGxlY2huZXIudG9wPokCUQQTAQgAOxYh +BJj6MPhfIDCnD0Wq6zzeot5UJLjnBQJqAvEfAhsDBQsJCAcCAiICBhUKCQgLAgQW +AgMBAh4HAheAAAoJEDzeot5UJLjnBBYP/ilO+Fmxh3P9IUWC9MHi22RQUDTVUXFY +5FZdRwcwISnZQiEO3OWcwUAPREL2LksWT18Hi+PQRsa/dHU7cePEKRVdfuBl0CU8 +P0aUdJA2/zFIAy7CdKZ8gnNcAkkb3xhuKcX1UPgzWP4N2E7qOEt/Vst4JFL8cU3T +/MHQEq7iERtpLSXR/IcI+gcFKDxABYyb073W8rzt0CKAaLFjg2DXPHhFPKn5H6p4 +DvXHrKfa5F5YlO+hPKcSA2L73FOmMbB8sT6TVTgPmKce8c/nWfQq3oNF775k1vJk +NBNB6Hy4cVjOOXBMxfAySWnBYaYHNzApXIyH6CUYesSWSY2wfkmlPNSstODah8Gd +YT/DK+ATmjj6JMpM7ELKZvT5iZOaau7NS7bXn9DDiwttFgM9+lxbA6hq75NTMwLF +PyLrl2Yne7y+jr9s4/v/1NxOjDwkpVyJFva4UPwR8RL93F+iomYzG3RTrjTV5j21 ++zxM1hdTtbmsdh8HQi9G4XF9QpG62W0uYmTgidfQvPvUDrF041yG2bc+tSh21Xcg +EEnZuRxdKK4EtceJ20dUaG3PCy/oqg8fkGiBoHQJ3NtZDBGbYZIVXMz4mJzPOtPh +3M/7GoodKkCSJ+KedngvgAFSY6QVJCeeW0H3bGpdTYXQIYt8JwEGuUt7yU8ucEsY +NKz7VtAoYhHztCdKdWxpYW4gTGVjaG5lciA8ZnJhdWp1bGlhbkBsZWNobmVyLnRv +cD6JAlEEEwEIADsWIQSY+jD4XyAwpw9Fqus83qLeVCS45wUCagL3pgIbAwULCQgH +AgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRA83qLeVCS45zqDD/9pHFvqCrqhF/HH +xcwh3JbXzn8sEZ1vtbrQVrXeJsGo4NNpWem3B29ZVk3P2JHTVXEiBuzkBee+cd5a +TfGTTY9c5HR1Z54j3ydFnCMUNIqqtB+WIeYNr+OU0oNHplLHZYj+MDutqP8MLZMX +X400ITTd4nz7iZmc3cgREIFGbi8QuLRhNEIn6oNkPEqumRnKweLL3TqedyAEtG+k +qsgDAMDb2LzOTWR+YLVM6g9zXmWI8fXxAC2Pda1IeTOCJd+GtldOkkbQjPLNuARi +1uLjZav92vZ/Iw75TTAQzsM9eSuRHv1JxWjUbnNORoiGEQTvyyZ7VHWiQmOLKy8q +z4OGAj2lpSgrSacqK9BD0CXi9y/DNVv1BR+akD5y77KTuF7n0d4E9dWrbr4DkLkx +8ZKyLJfQX3Ma7A0uQkCEwF7epfAuWSGDvB9wCXKUtjyCb6gRMzNOu6JKHZ9pDdO1 +Hp8jGyOrs/4+Jx7RvzOeE4HlU8yv86qmjdEy7c1iJGKi3d4CQG5fVvVjZieIOhOv +RUGTgS40CPAWoKD/SIejVvSjRgt6OMS75+/ZnqGZJX7DxWlbEsIDV455Kvitw21f +WhTy8B+Hx+cbAJuRszEO1z3GGcIb61Sy4ctJZjn84wUXgJdspZtwP29a5zHfKH6x +ap9z0lNZREN6G32L5txfvbqJ9js41rQqTGVjaG5lci1TeXN0ZW1zIDxhYnVzZUBs +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 +=kK3g +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/languages/de.ts b/src/languages/de.ts index 7e8b1ec..ae38613 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -18,6 +18,10 @@ export const deLanguage: LanguagePack = { heroIntro: 'Ich entwickle zuverlässige Systeme, übersichtliche Benutzeroberflächen und praktische digitale Lösungen, die sich in realen Kundensystemen bewähren.', contactTitle: 'Kontaktiere mich', + openPgpTitle: 'OpenPGP', + openPgpDescription: 'Verwende meinen Public Key für verschlüsselte Kommunikation.', + openPgpDownload: 'Public Key herunterladen', + openPgpFingerprintLabel: 'Fingerprint', viewProjects: 'Projekte', aboutMe: 'Über mich', nextHighlightAriaLabel: 'Nächstes Bild anzeigen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 857a02e..512f04f 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -18,6 +18,10 @@ export const enLanguage: LanguagePack = { heroIntro: 'I build reliable systems, clean interfaces, and practical digital solutions that actually hold up in production.', contactTitle: 'Contact me', + openPgpTitle: 'OpenPGP', + openPgpDescription: 'Download my public key for encrypted communication.', + openPgpDownload: 'Download public key', + openPgpFingerprintLabel: 'Fingerprint', viewProjects: 'View projects', aboutMe: 'About me', nextHighlightAriaLabel: 'Show next image', diff --git a/src/languages/language.types.ts b/src/languages/language.types.ts index 3b0066d..d625f4a 100644 --- a/src/languages/language.types.ts +++ b/src/languages/language.types.ts @@ -44,6 +44,10 @@ export interface LanguagePack { portfolioEyebrow: string; heroIntro: string; contactTitle: string; + openPgpTitle: string; + openPgpDescription: string; + openPgpDownload: string; + openPgpFingerprintLabel: string; viewProjects: string; aboutMe: string; nextHighlightAriaLabel: string; diff --git a/src/styles.scss b/src/styles.scss index 962cdf7..5c58522 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -317,6 +317,61 @@ hr { aspect-ratio: 1; } +.openpgp-section { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-4); + align-items: center; + margin-top: var(--space-4); + padding: 1.25rem 1.4rem; +} + +.openpgp-copy, +.openpgp-actions { + min-width: 0; +} + +.openpgp-copy { + display: grid; + gap: 0.45rem; +} + +.openpgp-description, +.openpgp-fingerprint { + margin: 0; +} + +.openpgp-description { + color: var(--text-muted); + line-height: 1.6; +} + +.openpgp-actions { + display: grid; + gap: 0.7rem; + justify-items: end; +} + +.openpgp-download-link { + width: 100%; + min-width: min(100%, 15rem); +} + +.openpgp-fingerprint { + display: grid; + gap: 0.2rem; + text-align: right; + color: var(--text-muted); + font-size: 0.8rem; +} + +.openpgp-fingerprint strong { + color: var(--text-primary); + line-height: 1.55; + letter-spacing: 0.06em; + word-break: break-word; +} + .action-link { display: inline-flex; align-items: center; @@ -1202,6 +1257,19 @@ hr { text-align: center; } + .openpgp-section { + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + } + + .openpgp-actions, + .openpgp-fingerprint { + width: 100%; + justify-items: center; + text-align: center; + } + .hero-meta div { width: min(100%, 24rem); } diff --git a/tsconfig.json b/tsconfig.json index 00a5208..2ab7442 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ { "compileOnSave": false, "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, - "esModuleInterop": true, - "sourceMap": true, - "declaration": false, - "moduleResolution": "bundler", + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022", "DOM"], - "types": [] + "module": "preserve" }, "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, "strictTemplates": true }, - "exclude": ["node_modules"] + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] }