diff --git a/src/components/Discussion.astro b/src/components/Discussion.astro new file mode 100644 index 0000000..599cb89 --- /dev/null +++ b/src/components/Discussion.astro @@ -0,0 +1,137 @@ +--- +import { getDiscussion } from '../lib/discussion'; +import { site } from '../site.config'; + +interface Props { + discussionKey: string; +} + +const { discussionKey } = Astro.props; +const discussion = await getDiscussion(discussionKey); +--- + +{discussion && ( +
+
+

On the record

+ {discussion.comments.length === 0 ? 'No comments yet' : `${discussion.comments.length} ${discussion.comments.length === 1 ? 'comment' : 'comments'}`} +
+ {discussion.comments.length > 0 && ( +
    + {discussion.comments.map((comment) => ( +
  1. +

    + {comment.author} + {comment.date} +

    +

    {comment.body}

    +
  2. + ))} +
+ )} +
+ + {discussion.threadUrl ? 'Reply on the record ↗' : 'Start the discussion ↗'} + +

+ Comments are issue comments in the public + discussion repository and render here at each site rebuild — part of the record, not a widget. +

+
+
+)} + + diff --git a/src/components/Header.astro b/src/components/Header.astro index 5b36747..a756a8e 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -13,7 +13,7 @@ const visibleNavigation = navigation.filter((item) => { if (item.href === '/opine') return stories.some(({ data }) => data.type === 'essay'); if (item.href === '/podcast') return episodes.length > 0 || Boolean(site.podcastFeedUrl); if (item.href === '/guides') return guides.length > 0; - if (item.href === '/book') return site.book.enabled; + if (item.href === '/book') return true; if (item.href === '/forum') return Boolean(site.forumUrl); return true; }); diff --git a/src/content/guides/passkeys.md b/src/content/guides/passkeys.md index de1e49e..92d01bd 100644 --- a/src/content/guides/passkeys.md +++ b/src/content/guides/passkeys.md @@ -7,32 +7,57 @@ author: "Ana" maintainers: - "Ana" status: developing -draft: true +draft: false tags: - identity - authentication + - recovery --- -> Draft field-guide template. Do not publish until every section has been sourced and reviewed. - ## What it is +A passkey is a cryptographic key pair that replaces a password. The private half stays on your device (or inside your platform account); the website keeps only the public half. Signing in means proving possession of the private key — nothing secret ever travels to the site or sits in its database. The underlying standards are WebAuthn and FIDO2, and every major browser and platform now ships them. + ## Why people care +Passwords fail at industrial scale: they get phished, reused, guessed, and leaked by the billion. A passkey cannot be phished in the ordinary way because it is bound to the real website's origin — a convincing fake site receives nothing usable — and a breach of the site's database yields only public keys. This is not marginal improvement; it removes the two most common ways accounts actually get taken over. + ## How it works, without the marketing +When you register, your device generates a fresh key pair for that one site and hands over the public key. When you sign in, the site sends a challenge; your device signs it after you unlock with a fingerprint, face, or PIN. The biometric never leaves your device — it only unlocks the local key. + +The consequential design choice is what happens to the private key afterwards. **Device-bound** passkeys (security keys, some enterprise setups) live and die with one piece of hardware. **Synced** passkeys — the consumer default — are copied through your platform account: iCloud Keychain, Google Password Manager, or a third-party manager like Bitwarden or 1Password. Syncing is what makes passkeys survivable for ordinary people, and it is also where the trust moves. + ## Who must trust whom +The password asked you to trust your own memory and every site's password handling. The passkey asks you to trust your platform vendor instead. Whoever operates your sync fabric — Apple, Google, Microsoft, or a password manager — can restore your keys, which means their account recovery process, their view of your identity, and their cooperation with whoever pressures them are all now part of your login. The site you sign into trusts the platform too: it never sees your key, only the platform's word that a valid signature arrived. + ## What it knows about you +Better than the reputation of most authentication tech. Each site gets its own key pair, so passkeys do not create a cross-site identifier; two sites cannot correlate you by your credential. Consumer attestation is deliberately blurred, so a site generally cannot even tell which brand of authenticator you used. What your *platform* knows is another matter: your sync provider necessarily knows every site you hold a passkey for. + ## What happens when you lose access +This is the honest centre of the guide. With synced passkeys, losing the phone is survivable: sign into your platform account on a new device and the keys return. What you have actually done is move the cliff — everything now hangs on recovering the *platform account*, and platform recovery is exactly where documented horror stories live: locked Google accounts with no human to appeal to, Apple accounts frozen by suspected fraud, and recovery flows that quietly fall back to SMS or security questions weaker than the passkey they protect. + +Moving between ecosystems is the second cliff. Passkey export and cross-ecosystem transfer are only now being standardised; in practice, leaving a platform today usually means re-registering, site by site, while you still can. Registering passkeys on two independent ecosystems — or one platform plus a hardware key kept in a drawer — is the practical hedge. + ## The strongest argument for it +Phishing resistance that does not depend on user vigilance. Every other mainstream defence — training, password managers, most one-time codes — still fails when a tired person meets a good fake. Origin-bound keys fail closed. For the attacks that actually empty accounts, passkeys are the largest real-world improvement since two-factor authentication, and they are easier to use, not harder. + ## The strongest argument against it +Concentration. Passkeys convert millions of small, survivable password failures into a dependence on a handful of platform recovery systems whose decisions are opaque and mostly unappealable. If your platform account dies, everything attached to it dies at once — and unlike a forgotten password, there is no site-by-site way back that doesn't rely on each site's own (often weaker) fallback. The security is real; so is the new landlord. + ## Known incidents and disputes +No mass compromise of the passkey protocol itself has been documented. The disputes are structural: security researchers have criticised the gap between device-bound FIDO's original threat model and the synced consumer reality; ecosystems have been criticised for making passkeys easy to create and hard to move; and sites that keep weaker fallback methods (SMS reset, security questions) undercut most of the benefit, since attackers simply use the fallback. + ## What changed recently +The FIDO Alliance published draft specifications for secure credential exchange, which would make passkeys portable between password managers and platforms — the answer to the lock-in criticism, arriving slowly. Third-party password managers now register and sync passkeys on both major mobile platforms, which softens (without removing) single-vendor dependence. + ## Maintainers and contributors + +Drafted by the publication; maintained by Ana. Corrections and challenges are welcome through the public discussion thread or a pull request — the review date above means what the editorial commitments say it means. diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index 70c05e4..63415b6 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -1,4 +1,5 @@ --- +import Discussion from '../components/Discussion.astro'; import MaintenanceMeta from '../components/MaintenanceMeta.astro'; import TrustPattern from '../components/TrustPattern.astro'; import BaseLayout from './BaseLayout.astro'; @@ -56,6 +57,7 @@ const props = Astro.props;
+ diff --git a/src/lib/discussion.ts b/src/lib/discussion.ts new file mode 100644 index 0000000..fc3aee0 --- /dev/null +++ b/src/lib/discussion.ts @@ -0,0 +1,75 @@ +import { site } from '../site.config'; + +export interface DiscussionComment { + author: string; + date: string; + body: string; +} + +export interface Discussion { + threadUrl: string | null; + newThreadUrl: string; + comments: DiscussionComment[]; +} + +interface GiteaIssue { + number: number; + title: string; + comments: number; + html_url: string; +} + +const API = 'https://git.frrn.life/api/v1'; +const PAGE_SIZE = 50; + +// One issue index shared across every page of a build. +let issueIndex: Promise> | null = null; + +async function fetchIssueIndex(): Promise> { + const index = new Map(); + for (let page = 1; page <= 40; page++) { + const res = await fetch( + `${API}/repos/${site.discussionRepo}/issues?state=all&type=issues&limit=${PAGE_SIZE}&page=${page}`, + ); + if (!res.ok) break; + const batch: GiteaIssue[] = await res.json(); + for (const issue of batch) index.set(issue.title, issue); + if (batch.length < PAGE_SIZE) break; + } + return index; +} + +/** + * Build-time lookup of the public discussion thread for an article. + * `key` is the content path, e.g. `stories/who-sent-this-agent` — it is + * also the exact title of the thread's issue in the discussion repo. + * Failures degrade to an empty thread; they never fail the build. + */ +export async function getDiscussion(key: string): Promise { + if (!site.discussionRepo) return null; + const newThreadUrl = `https://git.frrn.life/${site.discussionRepo}/issues/new?title=${encodeURIComponent(key)}`; + try { + issueIndex ??= fetchIssueIndex(); + const issue = (await issueIndex).get(key); + if (!issue) return { threadUrl: null, newThreadUrl, comments: [] }; + + let comments: DiscussionComment[] = []; + if (issue.comments > 0) { + const res = await fetch( + `${API}/repos/${site.discussionRepo}/issues/${issue.number}/comments`, + ); + if (res.ok) { + const raw: Array<{ user?: { login?: string }; created_at?: string; body?: string }> = + await res.json(); + comments = raw.map((comment) => ({ + author: comment.user?.login ?? 'unknown', + date: comment.created_at?.slice(0, 10) ?? '', + body: comment.body ?? '', + })); + } + } + return { threadUrl: issue.html_url, newThreadUrl, comments }; + } catch { + return { threadUrl: null, newThreadUrl, comments: [] }; + } +} diff --git a/src/pages/book.astro b/src/pages/book.astro index 005a12a..8298484 100644 --- a/src/pages/book.astro +++ b/src/pages/book.astro @@ -10,8 +10,8 @@ import { site } from '../site.config';

The book

-

{site.book.enabled ? site.book.title : 'The book belongs at the centre of the work.'}

-

{site.book.enabled ? site.book.description : 'This surface is ready for the actual cover, title, argument, sample chapter, endorsements, and purchase route. Those details are intentionally not fabricated in the scaffold.'}

+

{site.book.enabled ? site.book.title : 'A book is being written.'}

+

{site.book.enabled ? site.book.description : 'The long-form version of this publication’s argument: where trust actually lives in the systems that claim to have removed it, and what that costs the people inside them. It gets a title, a cover, and a date when those things are real — the RSS feed will say so first.'}

{site.book.enabled && (
{site.book.sampleUrl && Read a sample} @@ -19,6 +19,7 @@ import { site } from '../site.config';
)}
+

What this page will hold

01

Why I wrote it

02

Read a real chapter

03

Choose a purchase route

@@ -35,6 +36,7 @@ import { site } from '../site.config'; .lede { max-width: 58ch; color: var(--muted); font-size: 1.1rem; } .actions { display: flex; gap: .7rem; margin: 2rem 0; } .book-anatomy { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 3rem; border-top: 1px solid var(--rule); } + .anatomy-label { grid-column: 1 / -1; margin: 0.9rem 0 0; padding: 0 1rem; color: var(--muted); font-family: 'IBM Plex Mono', monospace; font-size: 0.62rem; letter-spacing: 0.07em; text-transform: uppercase; } .book-anatomy div { padding: 1rem; border-right: 1px solid var(--rule); } .book-anatomy div:last-child { border-right: 0; } .book-anatomy span { color: var(--amend); font-family: 'IBM Plex Mono', monospace; font-size: .68rem; } diff --git a/src/site.config.ts b/src/site.config.ts index 6faee8a..ffe32dd 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -13,6 +13,7 @@ export const site = { podcastFeedUrl: '/podcast/feed.xml', forumUrl: '', contactUrl: '', + discussionRepo: 'ana/trust-issues-discussion', book: { enabled: false, title: '',