@fuzdev/fuz_app 0.146.1 → 0.147.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.
Files changed (48) hide show
  1. package/dist/auth/CLAUDE.md +1 -1
  2. package/dist/auth/bootstrap_account.d.ts +2 -1
  3. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  4. package/dist/auth/bootstrap_account.js +17 -6
  5. package/dist/auth/keyring.d.ts +35 -38
  6. package/dist/auth/keyring.d.ts.map +1 -1
  7. package/dist/auth/keyring.js +43 -50
  8. package/dist/auth/password_argon2.d.ts.map +1 -1
  9. package/dist/auth/password_argon2.js +11 -8
  10. package/dist/auth/session_cookie.d.ts.map +1 -1
  11. package/dist/auth/session_cookie.js +5 -2
  12. package/dist/db/CLAUDE.md +18 -16
  13. package/dist/db/cell_queries.d.ts +3 -2
  14. package/dist/db/cell_queries.d.ts.map +1 -1
  15. package/dist/db/cell_queries.js +3 -2
  16. package/dist/db/fact_disk_storage.d.ts +15 -15
  17. package/dist/db/fact_disk_storage.d.ts.map +1 -1
  18. package/dist/db/fact_disk_storage.js +22 -28
  19. package/dist/db/fact_queries.d.ts +2 -3
  20. package/dist/db/fact_queries.d.ts.map +1 -1
  21. package/dist/db/fact_queries.js +2 -3
  22. package/dist/db/fact_store.d.ts +20 -18
  23. package/dist/db/fact_store.d.ts.map +1 -1
  24. package/dist/db/fact_store.js +30 -44
  25. package/dist/db/file_fact_url.d.ts +19 -4
  26. package/dist/db/file_fact_url.d.ts.map +1 -1
  27. package/dist/db/file_fact_url.js +19 -4
  28. package/dist/server/env.d.ts.map +1 -1
  29. package/dist/server/env.js +2 -2
  30. package/dist/server/serve_fact_route.d.ts +9 -5
  31. package/dist/server/serve_fact_route.d.ts.map +1 -1
  32. package/dist/server/serve_fact_route.js +15 -7
  33. package/dist/testing/CLAUDE.md +19 -2
  34. package/dist/testing/app_server.d.ts.map +1 -1
  35. package/dist/testing/app_server.js +2 -2
  36. package/dist/testing/cross_backend/bootstrap_backend.d.ts +11 -1
  37. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -1
  38. package/dist/testing/cross_backend/bootstrap_backend.js +2 -1
  39. package/dist/testing/cross_backend/fact_serving.d.ts +5 -0
  40. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  41. package/dist/testing/cross_backend/fact_serving.js +25 -1
  42. package/dist/testing/cross_backend/session_cookie_codec.d.ts +19 -0
  43. package/dist/testing/cross_backend/session_cookie_codec.d.ts.map +1 -0
  44. package/dist/testing/cross_backend/session_cookie_codec.js +128 -0
  45. package/dist/testing/cross_backend/testing_reset_actions.d.ts +7 -3
  46. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  47. package/dist/testing/cross_backend/testing_reset_actions.js +13 -8
  48. package/package.json +6 -6
@@ -31,7 +31,7 @@ over the pool so rows persist when request transactions roll back. See root
31
31
 
32
32
  ### Crypto primitives (pure, I/O-free)
33
33
 
34
- - `auth/keyring.ts` — `Keyring`, `create_keyring`, `validate_keyring`, `create_validated_keyring`.
34
+ - `auth/keyring.ts` — `Keyring`, `create_keyring` (validates — the one way to build a `Keyring`), `validate_keyring`.
35
35
  - `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie` (`'none' | 'clear'` — no refresh action; the 30-day lifetime is an absolute cap signed once at mint), `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`.
36
36
  - `auth/password.ts` — `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300).
37
37
  - `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
@@ -72,7 +72,8 @@ export interface BootstrapAccountDeps {
72
72
  * Uses an atomic `bootstrap_lock` UPDATE to prevent concurrent bootstrap
73
73
  * attempts (TOCTOU). The full flow runs in a single transaction:
74
74
  *
75
- * 1. Read and verify the bootstrap token (before transaction)
75
+ * 1. Read and verify the bootstrap token (before transaction) — the file's
76
+ * raw bytes less one optional trailing LF, compared in constant time
76
77
  * 2. Hash the password (CPU-intensive, before transaction)
77
78
  * 3. Acquire the bootstrap lock atomically (inside transaction)
78
79
  * 4. Create account + actor
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA2EhC,CAAC"}
1
+ {"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA6EhC,CAAC"}
@@ -12,13 +12,24 @@ import { ERROR_INVALID_TOKEN, ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSI
12
12
  import { ROLE_ADMIN, ROLE_KEEPER } from './role_schema.js';
13
13
  import { query_create_account_with_actor } from './account_queries.js';
14
14
  import { query_create_role_grant } from './role_grant_queries.js';
15
+ /**
16
+ * Strip exactly one trailing `\n` from the operator-written token bytes.
17
+ *
18
+ * Not `trim()`: trimming widens the set of accepted tokens — `tok\r\n`,
19
+ * `tok \n`, or a leading newline would all pass. One trailing LF is the
20
+ * newline a POSIX editor appends; anything else stays in the bytes and fails
21
+ * the compare, so operators write the token exactly. Twin of the Rust
22
+ * `strip_one_trailing_newline` (`fuz_auth`).
23
+ */
24
+ const strip_one_trailing_newline = (bytes) => bytes.at(-1) === 0x0a ? bytes.subarray(0, -1) : bytes;
15
25
  /**
16
26
  * Bootstrap the first account with keeper and admin privileges.
17
27
  *
18
28
  * Uses an atomic `bootstrap_lock` UPDATE to prevent concurrent bootstrap
19
29
  * attempts (TOCTOU). The full flow runs in a single transaction:
20
30
  *
21
- * 1. Read and verify the bootstrap token (before transaction)
31
+ * 1. Read and verify the bootstrap token (before transaction) — the file's
32
+ * raw bytes less one optional trailing LF, compared in constant time
22
33
  * 2. Hash the password (CPU-intensive, before transaction)
23
34
  * 3. Acquire the bootstrap lock atomically (inside transaction)
24
35
  * 4. Create account + actor
@@ -41,17 +52,17 @@ export const bootstrap_account = async (deps, provided_token, input) => {
41
52
  // wire always carried, with the specific reason logged for the operator.
42
53
  let expected_token;
43
54
  try {
44
- expected_token = new TextDecoder().decode(await read_secure_file(token_path)).trim();
55
+ expected_token = strip_one_trailing_newline(await read_secure_file(token_path));
45
56
  }
46
57
  catch (err) {
47
58
  log.warn(`bootstrap token file rejected: ${to_error_message(err)}`);
48
59
  return { ok: false, error: ERROR_TOKEN_FILE_MISSING, status: 404 };
49
60
  }
50
- // Defense-in-depth: no .trim() on provided_token — tokens must match exactly.
51
- // The expected_token is already trimmed above.
61
+ // raw bytes on both sides, nothing trimmed — the provided token must be the
62
+ // file's bytes exactly, less the one newline an editor appends
52
63
  const provided_buf = Buffer.from(provided_token);
53
- const expected_buf = Buffer.from(expected_token);
54
- if (provided_buf.length !== expected_buf.length || !timingSafeEqual(provided_buf, expected_buf)) {
64
+ if (provided_buf.length !== expected_token.length ||
65
+ !timingSafeEqual(provided_buf, expected_token)) {
55
66
  return { ok: false, error: ERROR_INVALID_TOKEN, status: 401 };
56
67
  }
57
68
  // 2. Hash password (CPU-intensive, before transaction)
@@ -7,12 +7,12 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * const keyring = create_keyring(process.env.SECRET_FUZ_COOKIE_KEYS);
11
- * if (!keyring) throw new Error('No keys configured');
10
+ * const result = create_keyring(process.env.SECRET_FUZ_COOKIE_KEYS);
11
+ * if (!result.ok) throw new Error(`SECRET_FUZ_COOKIE_KEYS: ${result.errors.join('; ')}`);
12
12
  *
13
- * const signed = await keyring.sign('user:123:1700000000');
14
- * const result = await keyring.verify(signed);
15
- * // result = { value: 'user:123:1700000000', key_index: 0 }
13
+ * const signed = await result.keyring.sign('user:123:1700000000');
14
+ * const verified = await result.keyring.verify(signed);
15
+ * // verified = { value: 'user:123:1700000000', key_index: 0 }
16
16
  * ```
17
17
  *
18
18
  * @module
@@ -38,24 +38,42 @@ export interface Keyring {
38
38
  } | null>;
39
39
  }
40
40
  /**
41
- * Create a keyring from environment variable.
41
+ * Result of `create_keyring`.
42
+ * Discriminated union — callers handle the error case their own way.
43
+ */
44
+ export type CreateKeyringResult = {
45
+ ok: true;
46
+ keyring: Keyring;
47
+ } | {
48
+ ok: false;
49
+ errors: Array<string>;
50
+ };
51
+ /**
52
+ * Validate a keys value and create a keyring from it — the one way to build a
53
+ * `Keyring`.
42
54
  *
43
- * Keys are separated by `__` for rotation support. First key is used
44
- * for signing, all keys are tried for verification.
55
+ * Keys are separated by `__` for rotation support; empty entries are skipped.
56
+ * The first key signs, every key verifies. A value with no key, or with any key
57
+ * shorter than `MIN_KEY_LENGTH` characters, is refused with every reason
58
+ * (`validate_keyring`), so a weak key fails loud at startup rather than signing
59
+ * cookies. Twin of the Rust `Keyring::new`.
45
60
  *
46
- * CryptoKeys are cached on first use for performance.
61
+ * Returns a discriminated union so callers handle exit/logging their own way
62
+ * (e.g. `Deno.exit(1)` vs `runtime.exit(1)`). CryptoKeys are cached on first use.
47
63
  *
48
- * **Security: key rotation is an operational concern.** Old keys remain valid
49
- * for verification indefinitely — a leaked old key can forge session cookies
50
- * until it is removed from `SECRET_FUZ_COOKIE_KEYS`. After rotating to a new
51
- * signing key, remove the old key within a grace period (e.g. 24–48 hours,
52
- * long enough for active sessions to re-sign with the new key via cookie
53
- * refresh). Treat `SECRET_FUZ_COOKIE_KEYS` changes as security-critical deploys.
64
+ * **Key rotation is an operational concern.** Put the new key first and keep
65
+ * the old one after it: new cookies are signed with the new key, and cookies
66
+ * the old key signed keep verifying. Nothing re-signs a cookie, so an old key
67
+ * stays live until the last cookie it signed expires — `SESSION_AGE_MAX` after
68
+ * the rotation — and dropping it sooner signs out every session it still
69
+ * carries. A key that leaked is dropped at once, since it can forge cookies for
70
+ * as long as it is listed. Treat `SECRET_FUZ_COOKIE_KEYS` changes as
71
+ * security-critical deploys.
54
72
  *
55
73
  * @param env_value - the SECRET_FUZ_COOKIE_KEYS environment variable
56
- * @returns keyring or null if no keys configured
74
+ * @returns `{ok: true, keyring}` or `{ok: false, errors}`
57
75
  */
58
- export declare const create_keyring: (env_value: string | undefined) => Keyring | null;
76
+ export declare const create_keyring: (env_value: string | undefined) => CreateKeyringResult;
59
77
  /**
60
78
  * Validate key ring configuration.
61
79
  *
@@ -67,25 +85,4 @@ export declare const create_keyring: (env_value: string | undefined) => Keyring
67
85
  * @returns array of validation errors (empty if valid)
68
86
  */
69
87
  export declare const validate_keyring: (env_value: string | undefined) => Array<string>;
70
- /**
71
- * Result of `create_validated_keyring`.
72
- * Discriminated union — callers handle the error case their own way.
73
- */
74
- export type ValidatedKeyringResult = {
75
- ok: true;
76
- keyring: Keyring;
77
- } | {
78
- ok: false;
79
- errors: Array<string>;
80
- };
81
- /**
82
- * Validate and create a keyring in one step.
83
- *
84
- * Returns a discriminated union so callers handle exit/logging their own way
85
- * (e.g. `Deno.exit(1)` vs `runtime.exit(1)`).
86
- *
87
- * @param env_value - the SECRET_FUZ_COOKIE_KEYS environment variable
88
- * @returns `{ok: true, keyring}` or `{ok: false, errors}`
89
- */
90
- export declare const create_validated_keyring: (env_value: string | undefined) => ValidatedKeyringResult;
91
88
  //# sourceMappingURL=keyring.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACvF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAAO,GAAG,IAiCxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAc5E,CAAC;AA6CF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GACjC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEvE;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,sBAUxE,CAAC"}
1
+ {"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACvF;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,mBAmC9D,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAc5E,CAAC"}
@@ -7,41 +7,58 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * const keyring = create_keyring(process.env.SECRET_FUZ_COOKIE_KEYS);
11
- * if (!keyring) throw new Error('No keys configured');
10
+ * const result = create_keyring(process.env.SECRET_FUZ_COOKIE_KEYS);
11
+ * if (!result.ok) throw new Error(`SECRET_FUZ_COOKIE_KEYS: ${result.errors.join('; ')}`);
12
12
  *
13
- * const signed = await keyring.sign('user:123:1700000000');
14
- * const result = await keyring.verify(signed);
15
- * // result = { value: 'user:123:1700000000', key_index: 0 }
13
+ * const signed = await result.keyring.sign('user:123:1700000000');
14
+ * const verified = await result.keyring.verify(signed);
15
+ * // verified = { value: 'user:123:1700000000', key_index: 0 }
16
16
  * ```
17
17
  *
18
18
  * @module
19
19
  */
20
20
  const KEY_SEPARATOR = '__';
21
21
  const MIN_KEY_LENGTH = 32;
22
+ /**
23
+ * The one spelling of an HMAC-SHA256 signature the keyring accepts: 32 bytes as
24
+ * padded standard base64 — 43 characters, the last holding the final 2 bits
25
+ * with zero low bits, then one `=`. Stricter than `atob`, which skips
26
+ * whitespace, accepts missing padding, and ignores trailing bits, so several
27
+ * strings would otherwise verify as one signature. Matches the Rust twin's
28
+ * strict decode.
29
+ */
30
+ const SIGNATURE_BASE64_PATTERN = /^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/;
22
31
  const encoder = new TextEncoder();
23
32
  /**
24
- * Create a keyring from environment variable.
33
+ * Validate a keys value and create a keyring from it — the one way to build a
34
+ * `Keyring`.
25
35
  *
26
- * Keys are separated by `__` for rotation support. First key is used
27
- * for signing, all keys are tried for verification.
36
+ * Keys are separated by `__` for rotation support; empty entries are skipped.
37
+ * The first key signs, every key verifies. A value with no key, or with any key
38
+ * shorter than `MIN_KEY_LENGTH` characters, is refused with every reason
39
+ * (`validate_keyring`), so a weak key fails loud at startup rather than signing
40
+ * cookies. Twin of the Rust `Keyring::new`.
28
41
  *
29
- * CryptoKeys are cached on first use for performance.
42
+ * Returns a discriminated union so callers handle exit/logging their own way
43
+ * (e.g. `Deno.exit(1)` vs `runtime.exit(1)`). CryptoKeys are cached on first use.
30
44
  *
31
- * **Security: key rotation is an operational concern.** Old keys remain valid
32
- * for verification indefinitely — a leaked old key can forge session cookies
33
- * until it is removed from `SECRET_FUZ_COOKIE_KEYS`. After rotating to a new
34
- * signing key, remove the old key within a grace period (e.g. 24–48 hours,
35
- * long enough for active sessions to re-sign with the new key via cookie
36
- * refresh). Treat `SECRET_FUZ_COOKIE_KEYS` changes as security-critical deploys.
45
+ * **Key rotation is an operational concern.** Put the new key first and keep
46
+ * the old one after it: new cookies are signed with the new key, and cookies
47
+ * the old key signed keep verifying. Nothing re-signs a cookie, so an old key
48
+ * stays live until the last cookie it signed expires — `SESSION_AGE_MAX` after
49
+ * the rotation — and dropping it sooner signs out every session it still
50
+ * carries. A key that leaked is dropped at once, since it can forge cookies for
51
+ * as long as it is listed. Treat `SECRET_FUZ_COOKIE_KEYS` changes as
52
+ * security-critical deploys.
37
53
  *
38
54
  * @param env_value - the SECRET_FUZ_COOKIE_KEYS environment variable
39
- * @returns keyring or null if no keys configured
55
+ * @returns `{ok: true, keyring}` or `{ok: false, errors}`
40
56
  */
41
57
  export const create_keyring = (env_value) => {
58
+ const errors = validate_keyring(env_value);
59
+ if (errors.length > 0)
60
+ return { ok: false, errors };
42
61
  const secrets = parse_keys(env_value);
43
- if (secrets.length === 0)
44
- return null;
45
62
  // Cache CryptoKey promises - imported once on first use
46
63
  const key_cache = [];
47
64
  const get_key = (index) => {
@@ -51,7 +68,7 @@ export const create_keyring = (env_value) => {
51
68
  return key_cache[index];
52
69
  };
53
70
  // Create the opaque key ring - secrets captured in closure, never exposed
54
- return {
71
+ const keyring = {
55
72
  async sign(value) {
56
73
  const key = await get_key(0);
57
74
  return sign_with_crypto_key(value, key);
@@ -67,6 +84,7 @@ export const create_keyring = (env_value) => {
67
84
  return null;
68
85
  }
69
86
  };
87
+ return { ok: true, keyring };
70
88
  };
71
89
  /**
72
90
  * Validate key ring configuration.
@@ -105,41 +123,16 @@ const verify_with_crypto_key = async (signed_value, key) => {
105
123
  return false;
106
124
  const value = signed_value.slice(0, dot_index);
107
125
  const signature_base64 = signed_value.slice(dot_index + 1);
108
- let signature;
109
- try {
110
- const decoded = atob(signature_base64);
111
- const bytes = new Uint8Array(decoded.length);
112
- for (let i = 0; i < decoded.length; i++) {
113
- bytes[i] = decoded.charCodeAt(i);
114
- }
115
- signature = bytes.buffer;
116
- }
117
- catch {
126
+ if (!SIGNATURE_BASE64_PATTERN.test(signature_base64))
118
127
  return false;
128
+ const decoded = atob(signature_base64);
129
+ const bytes = new Uint8Array(decoded.length);
130
+ for (let i = 0; i < decoded.length; i++) {
131
+ bytes[i] = decoded.charCodeAt(i);
119
132
  }
120
- const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(value));
133
+ const valid = await crypto.subtle.verify('HMAC', key, bytes, encoder.encode(value));
121
134
  return valid ? value : false;
122
135
  };
123
- /**
124
- * Validate and create a keyring in one step.
125
- *
126
- * Returns a discriminated union so callers handle exit/logging their own way
127
- * (e.g. `Deno.exit(1)` vs `runtime.exit(1)`).
128
- *
129
- * @param env_value - the SECRET_FUZ_COOKIE_KEYS environment variable
130
- * @returns `{ok: true, keyring}` or `{ok: false, errors}`
131
- */
132
- export const create_validated_keyring = (env_value) => {
133
- const errors = validate_keyring(env_value);
134
- if (errors.length > 0) {
135
- return { ok: false, errors };
136
- }
137
- const keyring = create_keyring(env_value);
138
- if (!keyring) {
139
- return { ok: false, errors: ['SECRET_FUZ_COOKIE_KEYS is required'] };
140
- }
141
- return { ok: true, keyring };
142
- };
143
136
  const parse_keys = (env_value) => {
144
137
  if (!env_value)
145
138
  return [];
@@ -1 +1 @@
1
- {"version":3,"file":"password_argon2.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/password_argon2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAiBtD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAEpE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC3B,UAAU,MAAM,EAChB,eAAe,MAAM,KACnB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAKF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAMpE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,gBAIlC,CAAC"}
1
+ {"version":3,"file":"password_argon2.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/password_argon2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AA2BtD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAEpE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC3B,UAAU,MAAM,EAChB,eAAe,MAAM,KACnB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAGpE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,gBAIlC,CAAC"}
@@ -9,19 +9,27 @@
9
9
  * @module
10
10
  */
11
11
  import { hash, verify } from '@node-rs/argon2';
12
+ /** @node-rs/argon2 `Algorithm.Argon2id` — const enum, cannot import with isolatedModules. */
13
+ const ARGON2ID = 2;
12
14
  /**
13
15
  * Argon2id options following OWASP recommendations.
14
16
  *
15
17
  * @see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
16
18
  */
17
- /** @node-rs/argon2 `Algorithm.Argon2id` — const enum, cannot import with isolatedModules. */
18
- const ARGON2ID = 2;
19
19
  const ARGON2_OPTIONS = {
20
20
  algorithm: ARGON2ID,
21
21
  memoryCost: 19456, // 19 MiB
22
22
  timeCost: 2,
23
23
  parallelism: 1
24
24
  };
25
+ /**
26
+ * A fixed Argon2id PHC string with the production parameters, verified against
27
+ * on an account-lookup miss so that path does the same work as a real
28
+ * verification. No password matches it in practice (its digest is all zero
29
+ * bytes). Precomputed, so the first miss costs no hash either. Byte-identical
30
+ * with the Rust `DUMMY_HASH` (`fuz_auth`).
31
+ */
32
+ const DUMMY_HASH = '$argon2id$v=19$m=19456,t=2,p=1$AAAAAAAAAAAAAAAAAAAAAA$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
25
33
  /**
26
34
  * Hash a password using Argon2id.
27
35
  *
@@ -46,8 +54,6 @@ export const verify_password = async (password, password_hash) => {
46
54
  return false;
47
55
  }
48
56
  };
49
- /** Cached dummy hash for timing attack resistance. */
50
- let dummy_hash = null;
51
57
  /**
52
58
  * Verify a password against a dummy hash for timing attack resistance.
53
59
  *
@@ -58,10 +64,7 @@ let dummy_hash = null;
58
64
  * @returns always `false`
59
65
  */
60
66
  export const verify_dummy = async (password) => {
61
- if (!dummy_hash) {
62
- dummy_hash = await hash_password('dummy_password_for_timing_resistance');
63
- }
64
- await verify_password(password, dummy_hash);
67
+ await verify_password(password, DUMMY_HASH);
65
68
  return false;
66
69
  };
67
70
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAoB,CAAC;AAQjD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBAMpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,cAAc,CAAC,SAAS;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,iFAAiF;IACjF,eAAe,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,MAAM,CAAC;IACjD,mEAAmE;IACnE,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,SAAS;IACvC,4BAA4B;IAC5B,QAAQ,EAAE,SAAS,CAAC;IACpB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAU,SAAS,EAC5C,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,CAgCrD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GAAU,SAAS,EAC1D,SAAS,OAAO,EAChB,UAAU,SAAS,EACnB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS;IAC9C,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,MAAM,KAAG,cAAc,CAAC,MAAM,CAK/E,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAE/F;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,sBAAsB,GAAU,SAAS,EACrD,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAgBzC,CAAC"}
1
+ {"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAoB,CAAC;AAWjD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBAMpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,cAAc,CAAC,SAAS;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,iFAAiF;IACjF,eAAe,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,MAAM,CAAC;IACjD,mEAAmE;IACnE,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,SAAS;IACvC,4BAA4B;IAC5B,QAAQ,EAAE,SAAS,CAAC;IACpB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAU,SAAS,EAC5C,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,CAiCrD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GAAU,SAAS,EAC1D,SAAS,OAAO,EAChB,UAAU,SAAS,EACnB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS;IAC9C,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,MAAM,KAAG,cAAc,CAAC,MAAM,CAK/E,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAE/F;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,sBAAsB,GAAU,SAAS,EACrD,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAgBzC,CAAC"}
@@ -22,6 +22,8 @@ export const SESSION_AGE_MAX = 60 * 60 * 24 * 30;
22
22
  const VALUE_SEPARATOR = ':';
23
23
  /** Strict integer matcher for the embedded `expires_at` field. */
24
24
  const EXPIRES_AT_INTEGER_RE = /^\d+$/;
25
+ /** The largest `expires_at` the Rust spine's `u64` parse accepts. */
26
+ const EXPIRES_AT_MAX = 18446744073709551615n;
25
27
  /**
26
28
  * Default cookie options for session cookies.
27
29
  *
@@ -69,9 +71,10 @@ export const parse_session = async (signed_value, keyring, options, now_seconds)
69
71
  // theoretical, but stricter parsing closes the gap as defense-in-depth.
70
72
  if (!EXPIRES_AT_INTEGER_RE.test(expires_at_str))
71
73
  return null;
72
- const expires_at = parseInt(expires_at_str, 10);
73
- if (!Number.isFinite(expires_at))
74
+ // refuse what the Rust spine's `u64` parse refuses, leading zeros allowed
75
+ if (BigInt(expires_at_str) > EXPIRES_AT_MAX)
74
76
  return null;
77
+ const expires_at = parseInt(expires_at_str, 10);
75
78
  // Check expiration
76
79
  const now = now_seconds ?? Math.floor(Date.now() / 1000);
77
80
  if (expires_at <= now)
package/dist/db/CLAUDE.md CHANGED
@@ -187,32 +187,34 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
187
187
  `query_fact_holder_unheld` (answered from holder rows alone).
188
188
  - **`fact_store.ts`** — `PgFactStore`, implementing `HeldFactStore` (reads per
189
189
  the `@fuzdev/fuz_util/fact_store.ts` `FactStore` contract; puts take
190
- `HeldFactPutOptions`, whose required `holder` —
191
- an actor id, or `null` for a system put — is recorded in `fact_holder` on
192
- every put, a dedup hit included, in one transaction with the fact row and its
193
- refs): size-routed writes (embedded ≤
194
- `embedded_threshold` / disk CAS above it), JSON ref auto-extract, idempotent
195
- put, verify-on-read for disk-backed content. With `disk_root` + `fs` (the
190
+ `HeldFactPutOptions`, whose required `holder` — an actor id, or `null` for a
191
+ system put — is recorded in `fact_holder` on every put, a dedup hit included,
192
+ in one transaction with the fact row and its refs): size-routed writes
193
+ (embedded ≤ `embedded_threshold` / disk CAS above it), refs exactly as each
194
+ put declares them (the bytes are never scanned), idempotent put,
195
+ verify-on-read for disk-backed content. With `disk_root` + `fs` (the
196
196
  `runtime/*Deps`) configured, oversize `put` and the streaming `put_stream`
197
197
  write to the `<shard>/<rest>` disk CAS and `get` reads from it; without them
198
198
  an oversize write throws (`put_stream` needs `fs` for a body of any size).
199
199
  The disk CAS is the only place `get` reads external bytes from; the store
200
200
  makes no network request. A row whose `external_url` is not a
201
201
  `file:<shard>/<rest>` URL (the store never writes one), and any external row
202
- on a store with no disk CAS, reads as `null` with a warning; the serve route
203
- 404s the former.
202
+ on a store with no disk CAS, reads as `null` with a warning; one naming another
203
+ fact's file reads as `null` with an error. The serve route 404s both.
204
204
  - **`file_fact_url.ts`** — the canonical `file:<shard>/<rest>` URL shape
205
205
  (`FileFactUrl` brand, `mint_file_fact_url` / `parse_file_fact_url` /
206
206
  `FILE_FACT_URL_PATTERN`) plus `fact_disk_path(hash) → {shard, rest}`, the
207
- single source of truth for the on-disk layout (twins the Rust `fuz_fact`).
207
+ single source of truth for the on-disk layout, and `file_fact_url_addresses`,
208
+ the check that a URL names its own hash's file (twins the Rust `fuz_fact`).
208
209
  - **`fact_disk_storage.ts`** — the filesystem CAS over `runtime/{FsStream,FsWrite,FsRemove,FsRead}Deps`
209
- (not raw `node:fs`): `stream_fact_to_disk` (bounded-memory blake3+sha256 single
210
- pass, buffer→spill, fsync-then-atomic-rename, dedup-drop if the CAS path already
211
- exists), `write_fact_bytes_to_disk` (buffering twin), `read_fact_bytes_from_disk`,
212
- and `sweep_orphan_temps` (reaps stale `.tmp` spills by mtime). The temp is
213
- `fsync`ed before the rename publishes it (twins the Rust `fuz_fact` §fsync
214
- posture: data-sync before rename, parent-dir fsync waived) — the serve path
215
- streams the file without re-hashing, so write-time durability is the guard.
210
+ (not raw `node:fs`): `stream_fact_to_disk` (bounded-memory blake3 as it
211
+ streams, buffer→spill, fsync-then-atomic-rename — over a file already at the
212
+ CAS path, so a repeat put heals a corrupt one), `write_fact_bytes_to_disk`
213
+ (buffering twin), `read_fact_bytes_from_disk`, and `sweep_orphan_temps`
214
+ (reaps stale `.tmp` spills by mtime). The temp is `fsync`ed before the
215
+ rename publishes it (twins the Rust `fuz_fact` §fsync posture: data-sync
216
+ before rename, parent-dir fsync waived) — the serve path streams the file
217
+ without re-hashing, so write-time durability is the guard.
216
218
  - **`fact_store_errors.ts`** — `FactPayloadTooLargeError` / `FactStorageFullError` (+
217
219
  `is_enospc_error`) thrown by `put_stream`, for a consumer route's 413 / 507.
218
220
  - The HTTP serving plumbing lives under `server/` (`serve_fact_route.ts`,
@@ -421,8 +421,9 @@ export interface CellListParams {
421
421
  * uppercase-hex digest, or a `blake3:` string of any other length is not a
422
422
  * ref. Refs are what fact serving admits a read through and what cell ref
423
423
  * admission gates on a write, so the grammar is exact: a string either is an
424
- * address or isn't one. Object keys are not walked — only values. (JSON facts'
425
- * `fact_ref` edges keep `fact_hash_extract_refs`, the substring scan.)
424
+ * address or isn't one. Object keys are not walked — only values. (A fact's
425
+ * own `fact_ref` edges are never derived from its bytes — they are the refs its
426
+ * put declares.)
426
427
  *
427
428
  * Returns `null` (rather than `[]`) when no refs are present — the column
428
429
  * is nullable and the `idx_cell_refs` partial index is `WHERE refs IS NOT
@@ -1 +1 @@
1
- {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
@@ -494,8 +494,9 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
494
494
  * uppercase-hex digest, or a `blake3:` string of any other length is not a
495
495
  * ref. Refs are what fact serving admits a read through and what cell ref
496
496
  * admission gates on a write, so the grammar is exact: a string either is an
497
- * address or isn't one. Object keys are not walked — only values. (JSON facts'
498
- * `fact_ref` edges keep `fact_hash_extract_refs`, the substring scan.)
497
+ * address or isn't one. Object keys are not walked — only values. (A fact's
498
+ * own `fact_ref` edges are never derived from its bytes — they are the refs its
499
+ * put declares.)
499
500
  *
500
501
  * Returns `null` (rather than `[]`) when no refs are present — the column
501
502
  * is nullable and the `idx_cell_refs` partial index is `WHERE refs IS NOT
@@ -20,10 +20,12 @@
20
20
  * re-hashing it (`server/serve_fact_route.ts`). This twins the Rust `fuz_fact`
21
21
  * §fsync posture: data-sync before the rename; the parent-dir fsync stays
22
22
  * deliberately waived (a lost dirent is regenerable under content addressing).
23
- * If the final path already exists the temp is dropped instead of renamed over
24
- * — idempotent dedup (same hash → byte-identical content), mirroring the Rust
25
- * commit path. `.tmp/` is a sibling of `<shard>/` under the same `facts_dir` so
26
- * `rename` is always same-filesystem (no EXDEV).
23
+ * The temp is renamed over the final path even when a file is already there:
24
+ * the temp's bytes are the ones just hashed, so the rename is a no-op for a
25
+ * sound file and replaces a corrupt one (a torn write, bit rot) with bytes that
26
+ * match their name — a re-upload heals the CAS. `.tmp/` is a sibling of
27
+ * `<shard>/` under the same `facts_dir` so `rename` is always same-filesystem
28
+ * (no EXDEV).
27
29
  *
28
30
  * @module
29
31
  */
@@ -56,22 +58,20 @@ export type StreamPlacement = {
56
58
  };
57
59
  /**
58
60
  * Outcome of streaming an upload to storage: the `blake3:`-prefixed fact hash,
59
- * the bare-hex SHA-256, the byte count, and where the bytes landed.
61
+ * the byte count, and where the bytes landed.
60
62
  * `PgFactStore.put_stream` turns this into the `fact` row insert.
61
63
  */
62
64
  export interface StreamFactToDiskResult {
63
65
  hash: FactHash;
64
- sha256: string;
65
66
  size: number;
66
67
  placement: StreamPlacement;
67
68
  }
68
69
  /**
69
- * Stream `source` to storage with bounded memory: hash BLAKE3 + SHA-256
70
- * incrementally in one pass, buffer in memory until the bytes cross
71
- * `embedded_threshold`, then spill the buffer + remaining chunks through a temp
72
- * file and atomically land it in the disk CAS. Peak heap is
73
- * `O(chunk + embedded_threshold)`, never `O(artifact)`, so a multi-GB upload
74
- * never buffers in RAM.
70
+ * Stream `source` to storage with bounded memory: hash BLAKE3 incrementally,
71
+ * buffer in memory until the bytes cross `embedded_threshold`, then spill the
72
+ * buffer + remaining chunks through a temp file and atomically land it in the
73
+ * disk CAS. Peak heap is `O(chunk + embedded_threshold)`, never `O(artifact)`, so a
74
+ * multi-GB upload never buffers in RAM.
75
75
  *
76
76
  * - **Embedded vs disk.** A body `<= embedded_threshold` stays in memory and is
77
77
  * returned as `{kind: 'embedded'}` for the PG `bytes` column. Above it (with a
@@ -87,17 +87,17 @@ export interface StreamFactToDiskResult {
87
87
  *
88
88
  * @mutates `facts_dir` filesystem
89
89
  */
90
- export declare const stream_fact_to_disk: (deps: Pick<FactDiskStorageDeps, "mkdir" | "rename" | "remove" | "write_file_stream" | "fsync" | "stat">, facts_dir: string | undefined, source: ReadableStream<Uint8Array>, max_bytes: number, embedded_threshold: number) => Promise<StreamFactToDiskResult>;
90
+ export declare const stream_fact_to_disk: (deps: Pick<FactDiskStorageDeps, "mkdir" | "rename" | "remove" | "write_file_stream" | "fsync">, facts_dir: string | undefined, source: ReadableStream<Uint8Array>, max_bytes: number, embedded_threshold: number) => Promise<StreamFactToDiskResult>;
91
91
  /**
92
92
  * Write fully-buffered `bytes` for `hash` to the canonical
93
93
  * `<facts_dir>/<shard>/<rest>` path, then publish via `commit_temp_to_cas`
94
- * (fsync'd temp + atomic rename, dedup-aware). The buffering twin of
94
+ * (fsync'd temp + atomic rename). The buffering twin of
95
95
  * `stream_fact_to_disk`, used by `PgFactStore.put` for oversize sync bytes.
96
96
  * Returns the `file:` `external_url` for the `fact` row.
97
97
  *
98
98
  * @mutates `facts_dir` filesystem
99
99
  */
100
- export declare const write_fact_bytes_to_disk: (deps: Pick<FactDiskStorageDeps, "mkdir" | "rename" | "remove" | "write_file" | "fsync" | "stat">, facts_dir: string, hash: FactHash, bytes: Uint8Array) => Promise<FileFactUrl>;
100
+ export declare const write_fact_bytes_to_disk: (deps: Pick<FactDiskStorageDeps, "mkdir" | "rename" | "remove" | "write_file" | "fsync">, facts_dir: string, hash: FactHash, bytes: Uint8Array) => Promise<FileFactUrl>;
101
101
  /**
102
102
  * Read a disk-backed fact's bytes from the `<facts_dir>/<shard>/<rest>` layout
103
103
  * the writers above produce, addressed by the row's `file:<shard>/<rest>`
@@ -1 +1 @@
1
- {"version":3,"file":"fact_disk_storage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_disk_storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AASH,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAIN,KAAK,WAAW,EAChB,MAAM,oBAAoB,CAAC;AAO5B,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,QAAiB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC,GACnF,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC,GAC9D,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC,GACvC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAC1B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,WAAW,CAAA;CAAE,CAAC;AAEvF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,IAAI,CACT,mBAAmB,EACnB,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,mBAAmB,GAAG,OAAO,GAAG,MAAM,CACtE,EACD,WAAW,MAAM,GAAG,SAAS,EAC7B,QAAQ,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,MAAM,EACjB,oBAAoB,MAAM,KACxB,OAAO,CAAC,sBAAsB,CA8GhC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC,EAChG,WAAW,MAAM,EACjB,MAAM,QAAQ,EACd,OAAO,UAAU,KACf,OAAO,CAAC,WAAW,CAgBrB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,EAC5C,WAAW,MAAM,EACjB,KAAK,MAAM,KACT,OAAO,CAAC,UAAU,CAIpB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC,EAC9D,WAAW,MAAM,EACjB,UAAU;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,KAC3D,OAAO,CAAC,MAAM,CA0BhB,CAAC"}
1
+ {"version":3,"file":"fact_disk_storage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_disk_storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAQH,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAIN,KAAK,WAAW,EAChB,MAAM,oBAAoB,CAAC;AAO5B,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,QAAiB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC,GACnF,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC,GAC9D,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC,GACvC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAC1B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,WAAW,CAAA;CAAE,CAAC;AAEvF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,mBAAmB,GAAG,OAAO,CAAC,EAC9F,WAAW,MAAM,GAAG,SAAS,EAC7B,QAAQ,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,MAAM,EACjB,oBAAoB,MAAM,KACxB,OAAO,CAAC,sBAAsB,CA0GhC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC,EACvF,WAAW,MAAM,EACjB,MAAM,QAAQ,EACd,OAAO,UAAU,KACf,OAAO,CAAC,WAAW,CAgBrB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,EAC5C,WAAW,MAAM,EACjB,KAAK,MAAM,KACT,OAAO,CAAC,UAAU,CAIpB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC,EAC9D,WAAW,MAAM,EACjB,UAAU;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,KAC3D,OAAO,CAAC,MAAM,CA0BhB,CAAC"}