shelving 1.266.0 → 1.267.1

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.
@@ -79,5 +79,8 @@ export class CloudflareKVProvider extends DBProvider {
79
79
  }
80
80
  }
81
81
  function _getKey(collection, id) {
82
- return `${collection}:${id}`;
82
+ // Percent-encode both parts so the `:` separator is unambiguous: without this, an `id` containing `:`
83
+ // (e.g. collection `a` + id `b:c`) collides with a different collection/id pair (collection `a:b` + id `c`).
84
+ // Plain identifiers (letters, digits, `-`, `_`, `.`) are left unchanged, so keys for typical names/UUIDs are stable.
85
+ return `${encodeURIComponent(collection)}:${encodeURIComponent(id)}`;
83
86
  }
@@ -3,6 +3,7 @@
3
3
  * - `#` 1-6 hashes, then one or more spaces, then the title.
4
4
  * - `#` must be the first character on the line.
5
5
  * - Markdown's underline syntax is not supported (for simplification).
6
+ * - Each heading gets a `getSlug()` `id` derived from its text, so same-page `#anchor` links resolve to it (e.g. `## React integration` becomes `id="react-integration"`). An all-punctuation heading that slugs to nothing gets no `id`.
6
7
  *
7
8
  * @example new MarkupParser({ rules: [HEADING_RULE] }).parse("# Title")
8
9
  * @see https://shelving.cc/markup/HEADING_RULE
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getSlug } from "../../util/string.js";
2
3
  import { createMarkupRule } from "../MarkupRule.js";
3
4
  import { createLineRegExp, LINE_CONTENT_REGEXP, LINE_SPACE_REGEXP } from "../util/regexp.js";
4
5
  /**
@@ -6,6 +7,7 @@ import { createLineRegExp, LINE_CONTENT_REGEXP, LINE_SPACE_REGEXP } from "../uti
6
7
  * - `#` 1-6 hashes, then one or more spaces, then the title.
7
8
  * - `#` must be the first character on the line.
8
9
  * - Markdown's underline syntax is not supported (for simplification).
10
+ * - Each heading gets a `getSlug()` `id` derived from its text, so same-page `#anchor` links resolve to it (e.g. `## React integration` becomes `id="react-integration"`). An all-punctuation heading that slugs to nothing gets no `id`.
9
11
  *
10
12
  * @example new MarkupParser({ rules: [HEADING_RULE] }).parse("# Title")
11
13
  * @see https://shelving.cc/markup/HEADING_RULE
@@ -13,5 +15,7 @@ import { createLineRegExp, LINE_CONTENT_REGEXP, LINE_SPACE_REGEXP } from "../uti
13
15
  export const HEADING_RULE = createMarkupRule(createLineRegExp(`(?<prefix>#{1,6})(?:${LINE_SPACE_REGEXP}+(?<heading>${LINE_CONTENT_REGEXP}))?`), (key, { prefix, heading = "" }, parser) => {
14
16
  // The hash count picks the heading level; cast the dynamic tag to the known `h1`–`h6` set.
15
17
  const Heading = `h${prefix.length}`;
16
- return _jsx(Heading, { children: parser.parse(heading.trim(), "inline") }, key);
18
+ const title = heading.trim();
19
+ // Slug the raw title (before inline parsing) for the `id`, so `#anchor` fragment links land on the heading.
20
+ return (_jsx(Heading, { id: getSlug(title), children: parser.parse(title, "inline") }, key));
17
21
  }, ["block"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.266.0",
3
+ "version": "1.267.1",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,13 +9,13 @@
9
9
  "main": "./index.js",
10
10
  "module": "./index.js",
11
11
  "devDependencies": {
12
- "@biomejs/biome": "^2.5.1",
12
+ "@biomejs/biome": "^2.5.2",
13
13
  "@google-cloud/firestore": "^8.6.0",
14
14
  "@heroicons/react": "^2.2.0",
15
15
  "@types/bun": "^1.3.14",
16
16
  "@types/react": "^19.2.17",
17
17
  "@types/react-dom": "^19.2.3",
18
- "@typescript/native-preview": "^7.0.0-dev.20260627.2",
18
+ "@typescript/native-preview": "^7.0.0-dev.20260704.1",
19
19
  "firebase": "^12.15.0",
20
20
  "react": "^19.3.0-canary-fef12a01-20260413",
21
21
  "react-dom": "^19.3.0-canary-fef12a01-20260413",
package/util/crypto.js CHANGED
@@ -4,6 +4,7 @@ import { requireBytes } from "./bytes.js";
4
4
  // Constants.
5
5
  const ALGORITHM = { name: "PBKDF2", hash: "SHA-512" };
6
6
  const ITERATIONS = 500000;
7
+ const MAX_ITERATIONS = 10000000; // Upper bound on iterations accepted from a stored hash — guards against a malicious hash forcing an arbitrarily expensive derivation (DoS). Comfortably above the 500k default.
7
8
  const SALT_LENGTH = 16; // 16 bytes = 128 bits
8
9
  const HASH_LENGTH = 64; // 64 bytes = 512 bits
9
10
  const PASSWORD_LENGTH = 6;
@@ -42,8 +43,8 @@ export async function hashPassword(password, iterations = ITERATIONS) {
42
43
  received: password.length,
43
44
  caller: hashPassword,
44
45
  });
45
- if (iterations < 1)
46
- throw new ValueError("Iterations must be number greater than 0", { received: iterations, caller: hashPassword });
46
+ if (iterations < 1 || iterations > MAX_ITERATIONS)
47
+ throw new ValueError(`Iterations must be between 1 and ${MAX_ITERATIONS}`, { received: iterations, caller: hashPassword });
47
48
  // Hash the password.
48
49
  const key = await _getKey(password);
49
50
  const salt = getRandomBytes(SALT_LENGTH);
@@ -70,7 +71,7 @@ export async function verifyPassword(password, hash) {
70
71
  const hashBytes = decodeBase64URLBytes(h);
71
72
  // Check iterations.
72
73
  const iterations = Number.parseInt(i, 10);
73
- if (!Number.isFinite(iterations) || iterations < 1)
74
+ if (!Number.isFinite(iterations) || iterations < 1 || iterations > MAX_ITERATIONS)
74
75
  return false;
75
76
  // Derive the hash.
76
77
  const key = await _getKey(password);