You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add WordPress-style password protection as a core feature: authors set a password on a content item, read APIs return a "locked" envelope until the caller submits the correct password.
Why core, not a plugin
The plugin API has no content:beforeRead hook, can't extend content table schemas, and can't inject middleware. Best a plugin can do is expose its own gated route — every frontend would have to opt in, and the live API would still leak protected content.
WXR importer already parses wp:post_password (cli/wxr/parser.ts:345) but drops it. Anyone migrating from WordPress silently loses protected posts.
Scope
In: one password per item, gates admin + live read APIs, admin UI toggle, WXR importer wire-up, EDITOR+ bypasses gating.
Out: multiple passwords, encryption at rest, password recovery, replacing auth.
Design
Schema
One nullable column password_hash TEXT on every content table.
Add to schema/registry.ts standard columns + new migration that loops _emdash_collections (same pattern as 013_scheduled_publishing.ts).
Hashing
PBKDF2-HMAC-SHA256, 600k iterations (OWASP 2023). Only KDF in SubtleCrypto, so it works on Workers without a native dep.
Format: pbkdf2$sha256$<iter>$<salt>$<hash> so iterations can be bumped later.
New utils/password-hash.ts with hashPassword / verifyPassword (constant-time compare).
Unlock tokens
Reuse the HMAC token pattern from preview/tokens.ts. Same payload shape, signed with EMDASH_SESSION_SECRET — no new env var.
Stored in HTTP-only SecureSameSite=Lax cookie. Cookie strategy is an open question (see below).
TTL default 7d, configurable per collection.
Read API
password_hash null OR caller is EDITOR+ OR valid unlock cookie → full content.
Otherwise → 200 with locked envelope: { locked: true, id, slug, status, fields: { /* publicWhenLocked only */ } }. Not 401 — frontends need to render the locked page.
New per-field flag publicWhenLocked (default false). Standard columns like id, slug, status are always public.
Verify endpoint
POST /_emdash/api/content/[collection]/[id]/unlock, body { password }, requires X-EmDash-Request: 1.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Category: Ideas
Summary
Add WordPress-style password protection as a core feature: authors set a password on a content item, read APIs return a "locked" envelope until the caller submits the correct password.
Why core, not a plugin
content:beforeReadhook, can't extend content table schemas, and can't inject middleware. Best a plugin can do is expose its own gated route — every frontend would have to opt in, and the live API would still leak protected content.wp:post_password(cli/wxr/parser.ts:345) but drops it. Anyone migrating from WordPress silently loses protected posts.Scope
In: one password per item, gates admin + live read APIs, admin UI toggle, WXR importer wire-up, EDITOR+ bypasses gating.
Out: multiple passwords, encryption at rest, password recovery, replacing auth.
Design
Schema
password_hash TEXTon every content table.schema/registry.tsstandard columns + new migration that loops_emdash_collections(same pattern as013_scheduled_publishing.ts).Hashing
pbkdf2$sha256$<iter>$<salt>$<hash>so iterations can be bumped later.utils/password-hash.tswithhashPassword/verifyPassword(constant-time compare).Unlock tokens
preview/tokens.ts. Same payload shape, signed withEMDASH_SESSION_SECRET— no new env var.SecureSameSite=Laxcookie. Cookie strategy is an open question (see below).7d, configurable per collection.Read API
password_hashnull OR caller is EDITOR+ OR valid unlock cookie → full content.{ locked: true, id, slug, status, fields: { /* publicWhenLocked only */ } }. Not 401 — frontends need to render the locked page.publicWhenLocked(defaultfalse). Standard columns likeid,slug,statusare always public.Verify endpoint
POST /_emdash/api/content/[collection]/[id]/unlock, body{ password }, requiresX-EmDash-Request: 1.Retry-After.Admin UI
WXR importer
item.postPasswordis set, hash it on import. Plaintext is never persisted.Config
Test plan
password-hashround-trip,unlock-tokenssign/verify/expire/tamper.<wp:post_password>,publicWhenLockedfield visibility.Open questions
{ locked: true, fields: {...} }vs full envelope with private fields nulled out?titledefault topublicWhenLocked: true? WP shows it by default.Implementation order
password-hash.ts+ testsunlock-tokens.ts+ testsschema/registry.tsstandard columnpublicWhenLockedfield flagemdash:minor)Steps 1–6 are backend-only and can land first.
Asking maintainers: is the scope and the PBKDF2 hashing choice OK? Happy to adjust before writing code.
All reactions