mirror of
https://github.com/quartz-community/encrypted-pages.git
synced 2026-07-22 03:00:24 +00:00
Stealth pages are encrypted pages that are hidden from every listing surface permanently, even after a successful client-side decryption. Their metadata is never written to the shadow content index, so there is nothing for the client to recover -- the page remains invisible to graph, explorer, search, backlinks, recent-notes, folder-page, and tag-page for the entire browser session. This fills a gap between "unlisted" (hidden pre-decryption, revealed post-decryption via shadow index) and the desire for "secret door" pages that should only be accessible to users who already know the exact URL. Semantics: - `stealth: true` implies `unlisted: true`. Setting stealth on a page automatically hides it from listings regardless of the `unlisted` field or the `unlistWhenEncrypted` plugin option. - `stealth: true` overrides explicit `unlisted: false`. A user who writes `stealth: true, unlisted: false` gets a hidden stealth page, because stealth without unlisted is semantically incoherent. - `stealth: true` has no effect on non-encrypted pages. Stealth only controls shadow-index emission, which only happens for encrypted unlisted pages. Non-encrypted pages with `stealth: true` are silently ignored (use `unlisted: true` for non-encrypted hiding, via the `unlisted-pages` plugin). - Non-boolean stealth values are ignored. Implementation: - transformer.ts: after setting `data.unlisted`, check if `frontmatter.stealth === true` and if so set `data.stealth = true` and force `data.unlisted = true`. - emitter.ts: in the shadow-index loop, skip any page where `data.stealth === true`. Tests: 8 new (6 transformer + 2 emitter) covering stealth -> unlisted implication, stealth overriding explicit unlisted: false, stealth on non-encrypted pages being a no-op, non-boolean stealth values being ignored, stealth pages being excluded from the shadow index while non-stealth pages pass through unchanged, and mixed stealth/non-stealth content being correctly partitioned. 29 tests total passing.
4.5 KiB
4.5 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[Unreleased]
Added
EncryptedContentIndexemitter that writes a versioned shadow content index (static/encryptedContentIndex.json) containing opaque encrypted blobs of per-page metadata (slug, title, links, tags) for pages markedunlisted: true. The shadow index uses a flat array format so no slugs or titles leak to anonymous visitors.unlistWhenEncryptedoption on theEncryptedPagestransformer. Whentrue, encrypted pages are markedfile.data.unlisted = true, hiding them from every build-time listing surface that respects theunlistedconvention (contentIndex, RSS, sitemap, backlinks, recent-notes, folder-page, tag-page, graph, explorer, search) while still emitting the HTML so the page remains accessible by direct URL.- Per-page
unlisted: true | falsefrontmatter override. Explicitunlisted: falseforces the page listed even whenunlistWhenEncryptedis set. - Per-page
stealth: truefrontmatter field. Stealth pages are encrypted pages that are hidden from every listing surface permanently, even after a successful client-side decryption — their metadata is never written to the shadow content index, so there is nothing for the client to recover. Useful for "secret door" pages that should only be accessible to users who already know the exact URL.stealth: trueimpliesunlisted: trueand overrides any explicitunlisted: false. No effect on non-encrypted pages. - Client-side shadow-index unlocking: on every page load with cached passwords, the client script fetches the shadow index, decrypts entries with cached passwords, patches the resolved
fetchDataobject in place, and dispatchescontent-index-updatedso graph, explorer, and search re-initialize with the unlocked pages. encryptAesGcm,decrypt, andSHADOW_INDEX_VERSIONexports for test and extension use.ShadowIndexBlob,ShadowIndexFile, andShadowContentIndexEntrytype exports.EncryptedContentIndexOptions.passwordField(default"password") so the emitter reads the frontmatter password field from its own merged options rather than spelunkingctx.cfg.plugins.transformersat emit time.outputPathfield in the package manifest'sdefaultOptionsandoptionSchemaso users can override the shadow-index output path fromquartz.config.yaml.
Fixed
- Shadow content index emitter now actually runs. Changed
package.json>quartz.categoryfrom"transformer"to["transformer", "emitter"]so Quartz v5's plugin loader instantiates both the transformer and the emitter from the single config entry. Previously, Quartz read"transformer"and only ever calledfindFactory(module, "transformer"), soEncryptedContentIndexwas exported from the module but never invoked —static/encryptedContentIndex.jsonwas never written. A user's single- source: github:quartz-community/encrypted-pagesentry inquartz.config.yamlnow correctly produces both the transformer and the emitter instances, and both factories receive the same merged options object.
Removed
- Breaking:
EncryptedPageFilter. It used Quartz'sshouldPublishfilter mechanism, which removes pages from the entire build (no HTML emitted), directly contradicting the plugin's own README. UseunlistWhenEncrypted: trueor per-pageunlisted: truefrontmatter instead. - Breaking:
visibilityoption on theEncryptedPagestransformer. It set afile.data.encryptedVisibilityflag that nothing in the Quartz v5 ecosystem read — the option had no effect. - Breaking:
EncryptedPagesOptions.visibilitytype field. - Breaking:
EncryptedPageFilterOptionstype export. - The build-time warning for a missing
EncryptedContentIndexemitter. Quartz v5's plugin loader now instantiates both the transformer and the emitter automatically from the same config entry (via thecategory: ["transformer", "emitter"]manifest), so the warning is obsolete.
Changed
- Plugin ordering requirement:
EncryptedPagesmust run afterCrawlLinks(or any other transformer that populatesfile.data.links) in thehtmlPluginschain. Use theorderfield inquartz.config.yamlto control this. Documented in README. - The README no longer instructs users to register the emitter separately — Quartz v5 handles that automatically from a single config entry.