@quartz-community/encrypted-pages 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 43a26d8: Encrypts the pages that page types render from files (quartz-community/bases-page#11), through Quartz 5.1's `virtualPageHtmlPlugins`: a drawing stored as Markdown with a `password` in its frontmatter is encrypted like a note, and a `.base`, `.canvas` or `.excalidraw` page, which has no frontmatter, is encrypted with the password of the encrypted published note that links to it (the first by slug when several do). Such pages are also added to the shadow content index when unlisted. Before, these pages were always served in the clear.
8
+
3
9
  ## 1.0.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -71,6 +71,21 @@ All options below are set at the single plugin-entry level and are shared betwee
71
71
  | `unlisted` | `boolean` | Overrides `unlistWhenEncrypted`. `true` forces the page unlisted; `false` forces it listed even when `unlistWhenEncrypted` is also set. |
72
72
  | `stealth` | `boolean` | When `true` and the page is encrypted, suppresses the page's entry in the shadow content index. The page stays hidden from every listing surface **even after successful client-side decryption**. Implies `unlisted: true` (overrides any explicit `unlisted: false`). No effect on non-encrypted pages. |
73
73
 
74
+ ## Bases, canvases and drawings
75
+
76
+ Pages that page types render from files are encrypted too (Quartz 5.1 or later):
77
+
78
+ - A drawing stored as Markdown (`.excalidraw.md`) has frontmatter: a `password` there encrypts its
79
+ page, as for a note.
80
+ - A `.base`, `.canvas` or `.excalidraw` file has no frontmatter. Its page is encrypted with the
81
+ password of the encrypted published note that links to it (`[[Board.canvas]]`), so a board
82
+ linked from a protected note is protected with it. When several encrypted notes with different
83
+ passwords link to it, the first by slug wins. Linked only from unencrypted notes, it stays
84
+ unencrypted; with `explicit-publish`, a file that no published note links to is not published.
85
+
86
+ An embed in an encrypted note (`![[Board.canvas]]`) shows as a link to the embedded page after
87
+ unlocking the note: embeds are resolved when the page is rendered, after the note was encrypted.
88
+
74
89
  ## How `unlisted` works
75
90
 
76
91
  When an encrypted page is marked `unlisted: true`:
package/dist/index.js CHANGED
@@ -2254,50 +2254,82 @@ function decrypt(encryptedBase64, password, iterations) {
2254
2254
  decipher.setAuthTag(authTag);
2255
2255
  return decipher.update(ciphertext, void 0, "utf8") + decipher.final("utf8");
2256
2256
  }
2257
+ var inheritedPasswords = /* @__PURE__ */ new WeakMap();
2258
+ function encryptTree(tree, file, password, options) {
2259
+ const frontmatter = file.data?.frontmatter ?? {};
2260
+ const html5 = toHtml(tree, { allowDangerousHtml: true });
2261
+ const encryptedData = encryptAesGcm(html5, password, options.iterations);
2262
+ const encryptedContainer = {
2263
+ type: "element",
2264
+ tagName: "div",
2265
+ properties: {
2266
+ className: ["encrypted-page", "popover-hint"],
2267
+ "data-encrypted": encryptedData,
2268
+ "data-iterations": String(options.iterations)
2269
+ },
2270
+ children: []
2271
+ };
2272
+ tree.children = [encryptedContainer];
2273
+ const data = file.data;
2274
+ data.encrypted = true;
2275
+ data.text = "";
2276
+ data.description = "";
2277
+ const frontmatterUnlisted = frontmatter.unlisted;
2278
+ if (typeof frontmatterUnlisted === "boolean") {
2279
+ data.unlisted = frontmatterUnlisted;
2280
+ } else if (options.unlistWhenEncrypted) {
2281
+ data.unlisted = true;
2282
+ }
2283
+ if (frontmatter.stealth === true) {
2284
+ data.stealth = true;
2285
+ data.unlisted = true;
2286
+ }
2287
+ }
2288
+ function passwordIn(frontmatter, options) {
2289
+ const password = frontmatter?.[options.passwordField];
2290
+ return typeof password === "string" && password.length > 0 ? password : void 0;
2291
+ }
2257
2292
  var rehypeEncryptedPages = (options) => {
2258
2293
  return () => (tree, file) => {
2259
- const frontmatter = file.data?.frontmatter ?? {};
2260
- const password = frontmatter[options.passwordField];
2261
- if (typeof password !== "string" || password.length === 0) {
2294
+ const password = passwordIn(file.data?.frontmatter, options);
2295
+ if (password) encryptTree(tree, file, password, options);
2296
+ };
2297
+ };
2298
+ var rehypeEncryptedVirtualPages = (options, content) => {
2299
+ const passwordBySlug = /* @__PURE__ */ new Map();
2300
+ for (const [, file] of content) {
2301
+ const password = passwordIn(file.data?.frontmatter, options);
2302
+ if (password && typeof file.data?.slug === "string")
2303
+ passwordBySlug.set(file.data.slug, password);
2304
+ }
2305
+ return () => (tree, file) => {
2306
+ const own4 = passwordIn(file.data?.frontmatter, options);
2307
+ if (own4) {
2308
+ encryptTree(tree, file, own4, options);
2262
2309
  return;
2263
2310
  }
2264
- const html5 = toHtml(tree, { allowDangerousHtml: true });
2265
- const encryptedData = encryptAesGcm(html5, password, options.iterations);
2266
- const encryptedContainer = {
2267
- type: "element",
2268
- tagName: "div",
2269
- properties: {
2270
- className: ["encrypted-page", "popover-hint"],
2271
- "data-encrypted": encryptedData,
2272
- "data-iterations": String(options.iterations)
2273
- },
2274
- children: []
2275
- };
2276
- tree.children = [encryptedContainer];
2277
- const data = file.data;
2278
- data.encrypted = true;
2279
- data.text = "";
2280
- data.description = "";
2281
- const frontmatterUnlisted = frontmatter.unlisted;
2282
- if (typeof frontmatterUnlisted === "boolean") {
2283
- data.unlisted = frontmatterUnlisted;
2284
- } else if (options.unlistWhenEncrypted) {
2285
- data.unlisted = true;
2286
- }
2287
- if (frontmatter.stealth === true) {
2288
- data.stealth = true;
2289
- data.unlisted = true;
2290
- }
2311
+ const relativePath = file.data?.relativePath;
2312
+ if (typeof relativePath !== "string" || relativePath.toLowerCase().endsWith(".md")) return;
2313
+ const referencedBy = file.data.referencedBy;
2314
+ if (!Array.isArray(referencedBy)) return;
2315
+ const inherited = [...referencedBy].filter((slug) => typeof slug === "string").sort().map((slug) => passwordBySlug.get(slug)).find((password) => password !== void 0);
2316
+ if (!inherited) return;
2317
+ encryptTree(tree, file, inherited, options);
2318
+ inheritedPasswords.set(file.data, inherited);
2291
2319
  };
2292
2320
  };
2293
2321
  var EncryptedPages = (userOptions) => {
2294
2322
  const options = { ...defaultOptions, ...userOptions };
2295
- return {
2323
+ const instance = {
2296
2324
  name: "EncryptedPages",
2297
2325
  htmlPlugins() {
2298
2326
  return [rehypeEncryptedPages(options)];
2327
+ },
2328
+ virtualPageHtmlPlugins(_ctx, content) {
2329
+ return [rehypeEncryptedVirtualPages(options, content)];
2299
2330
  }
2300
2331
  };
2332
+ return instance;
2301
2333
  };
2302
2334
 
2303
2335
  // node_modules/@quartz-community/types/dist/index.js
@@ -2343,7 +2375,7 @@ var EncryptedContentIndex = (userOptions) => {
2343
2375
  if (data.unlisted !== true) continue;
2344
2376
  if (data.stealth === true) continue;
2345
2377
  const frontmatter = data.frontmatter ?? {};
2346
- const password = frontmatter[passwordField];
2378
+ const password = frontmatter[passwordField] ?? inheritedPasswords.get(file.data);
2347
2379
  if (typeof password !== "string" || password.length === 0) continue;
2348
2380
  const iterations = extractIterationsFromTree(tree);
2349
2381
  const shadowEntry = buildShadowEntry(data);