@fuzdev/fuz_app 0.107.0 → 0.109.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 (95) hide show
  1. package/README.md +4 -0
  2. package/dist/actions/CLAUDE.md +1 -1
  3. package/dist/auth/CLAUDE.md +1 -1
  4. package/dist/auth/account_route_schema.d.ts +10 -1
  5. package/dist/auth/account_route_schema.d.ts.map +1 -1
  6. package/dist/auth/account_route_schema.js +10 -1
  7. package/dist/auth/account_routes.d.ts +18 -4
  8. package/dist/auth/account_routes.d.ts.map +1 -1
  9. package/dist/auth/account_routes.js +32 -29
  10. package/dist/auth/api_token_queries.d.ts +4 -0
  11. package/dist/auth/api_token_queries.d.ts.map +1 -1
  12. package/dist/auth/api_token_queries.js +5 -1
  13. package/dist/auth/audit_emitter.d.ts +9 -5
  14. package/dist/auth/audit_emitter.d.ts.map +1 -1
  15. package/dist/auth/audit_log_schema.d.ts +7 -1
  16. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  17. package/dist/auth/audit_log_schema.js +16 -1
  18. package/dist/auth/bearer_auth.d.ts +9 -6
  19. package/dist/auth/bearer_auth.d.ts.map +1 -1
  20. package/dist/auth/bearer_auth.js +12 -21
  21. package/dist/auth/bootstrap_routes.d.ts +12 -2
  22. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  23. package/dist/auth/bootstrap_routes.js +26 -10
  24. package/dist/auth/middleware.d.ts +0 -3
  25. package/dist/auth/middleware.d.ts.map +1 -1
  26. package/dist/auth/middleware.js +11 -10
  27. package/dist/auth/migrations.d.ts +7 -0
  28. package/dist/auth/migrations.d.ts.map +1 -1
  29. package/dist/auth/migrations.js +45 -4
  30. package/dist/auth/session_queries.d.ts +13 -2
  31. package/dist/auth/session_queries.d.ts.map +1 -1
  32. package/dist/auth/session_queries.js +14 -3
  33. package/dist/auth/signup_routes.d.ts +10 -0
  34. package/dist/auth/signup_routes.d.ts.map +1 -1
  35. package/dist/auth/signup_routes.js +14 -11
  36. package/dist/db/CLAUDE.md +9 -5
  37. package/dist/db/cell_audit_queries.d.ts +4 -2
  38. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  39. package/dist/db/cell_audit_queries.js +4 -2
  40. package/dist/db/pg_error.d.ts +12 -0
  41. package/dist/db/pg_error.d.ts.map +1 -1
  42. package/dist/db/pg_error.js +15 -1
  43. package/dist/http/CLAUDE.md +78 -18
  44. package/dist/http/db_routes.d.ts +96 -3
  45. package/dist/http/db_routes.d.ts.map +1 -1
  46. package/dist/http/db_routes.js +263 -46
  47. package/dist/http/error_schemas.d.ts +13 -2
  48. package/dist/http/error_schemas.d.ts.map +1 -1
  49. package/dist/http/error_schemas.js +13 -2
  50. package/dist/http/pending_effects.d.ts +5 -5
  51. package/dist/http/pending_effects.d.ts.map +1 -1
  52. package/dist/http/pending_effects.js +1 -1
  53. package/dist/rate_limiter.d.ts +36 -3
  54. package/dist/rate_limiter.d.ts.map +1 -1
  55. package/dist/rate_limiter.js +36 -3
  56. package/dist/server/app_server.d.ts +27 -10
  57. package/dist/server/app_server.d.ts.map +1 -1
  58. package/dist/server/app_server.js +30 -20
  59. package/dist/server/app_server_context.d.ts +10 -2
  60. package/dist/server/app_server_context.d.ts.map +1 -1
  61. package/dist/testing/CLAUDE.md +42 -8
  62. package/dist/testing/app_server.d.ts.map +1 -1
  63. package/dist/testing/app_server.js +6 -4
  64. package/dist/testing/audit_completeness.d.ts.map +1 -1
  65. package/dist/testing/audit_completeness.js +5 -1
  66. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  67. package/dist/testing/cross_backend/cell_relations.js +53 -1
  68. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  69. package/dist/testing/cross_backend/default_spine_surface.js +9 -8
  70. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  71. package/dist/testing/cross_backend/login_security.js +59 -0
  72. package/dist/testing/cross_backend/query_shape.d.ts +10 -0
  73. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
  74. package/dist/testing/cross_backend/query_shape.js +128 -0
  75. package/dist/testing/cross_backend/session_cap.d.ts +19 -0
  76. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
  77. package/dist/testing/cross_backend/session_cap.js +93 -0
  78. package/dist/testing/middleware.d.ts +2 -5
  79. package/dist/testing/middleware.d.ts.map +1 -1
  80. package/dist/testing/middleware.js +5 -5
  81. package/dist/testing/rate_limiting.d.ts +10 -16
  82. package/dist/testing/rate_limiting.d.ts.map +1 -1
  83. package/dist/testing/rate_limiting.js +11 -95
  84. package/dist/testing/stubs.d.ts.map +1 -1
  85. package/dist/testing/stubs.js +3 -1
  86. package/dist/testing/testing_rate_limiter.d.ts +7 -1
  87. package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
  88. package/dist/testing/testing_rate_limiter.js +7 -1
  89. package/dist/ui/CLAUDE.md +9 -4
  90. package/dist/ui/table_state.svelte.d.ts +18 -5
  91. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  92. package/dist/ui/table_state.svelte.js +22 -7
  93. package/dist/ui/ui_format.d.ts.map +1 -1
  94. package/dist/ui/ui_format.js +4 -0
  95. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { bootstrap_route_shape } from './bootstrap_route_schema.js';
13
13
  import { get_route_input } from '../http/route_spec.js';
14
14
  import { get_client_ip } from '../http/client_ip.js';
15
15
  import { rate_limit_exceeded_response } from '../rate_limiter.js';
16
- import { ERROR_ALREADY_BOOTSTRAPPED } from '../http/error_schemas.js';
16
+ import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/error_schemas.js';
17
17
  /**
18
18
  * Check bootstrap availability at startup.
19
19
  *
@@ -54,7 +54,7 @@ export const check_bootstrap_status = async (deps, options) => {
54
54
  */
55
55
  export const create_bootstrap_route_specs = (deps, options) => {
56
56
  const { keyring } = deps;
57
- const { session_options, bootstrap_status, on_bootstrap, ip_rate_limiter } = options;
57
+ const { session_options, bootstrap_status, on_bootstrap, bootstrap_ip_rate_limiter } = options;
58
58
  const { token_path } = bootstrap_status;
59
59
  return [
60
60
  {
@@ -68,9 +68,9 @@ export const create_bootstrap_route_specs = (deps, options) => {
68
68
  return c.json({ error: ERROR_ALREADY_BOOTSTRAPPED }, 403);
69
69
  }
70
70
  // Per-IP rate limit check (before any token/DB work)
71
- const ip = ip_rate_limiter ? get_client_ip(c) : null;
72
- if (ip_rate_limiter && ip) {
73
- const check = ip_rate_limiter.check(ip);
71
+ const ip = bootstrap_ip_rate_limiter ? get_client_ip(c) : null;
72
+ if (bootstrap_ip_rate_limiter && ip) {
73
+ const check = bootstrap_ip_rate_limiter.check(ip);
74
74
  if (!check.allowed) {
75
75
  return rate_limit_exceeded_response(c, check.retry_after);
76
76
  }
@@ -87,8 +87,18 @@ export const create_bootstrap_route_specs = (deps, options) => {
87
87
  log: deps.log
88
88
  }, input.token, input);
89
89
  if (!result.ok) {
90
- if (ip_rate_limiter && ip)
91
- ip_rate_limiter.record(ip);
90
+ // An unreadable token file closes the window. Bootstrap cannot
91
+ // succeed without one, and `check_bootstrap_status` already reads
92
+ // an unreadable file as unavailable at startup — so leaving the
93
+ // flag set means the two disagree, and every later request takes
94
+ // this leg and writes another audit row. The limiter would bound
95
+ // that channel; closing it ends it, and stops `/status`
96
+ // advertising a window that can't be walked through. Twin of the
97
+ // Rust spine's `bootstrap_handler`, which does the same.
98
+ if (result.error === ERROR_TOKEN_FILE_MISSING)
99
+ bootstrap_status.available = false;
100
+ if (bootstrap_ip_rate_limiter && ip)
101
+ bootstrap_ip_rate_limiter.record(ip);
92
102
  deps.audit.emit(route, {
93
103
  event_type: 'bootstrap',
94
104
  outcome: 'failure',
@@ -97,9 +107,15 @@ export const create_bootstrap_route_specs = (deps, options) => {
97
107
  });
98
108
  return c.json({ error: result.error }, result.status);
99
109
  }
100
- // Successful bootstrap — update state immediately
101
- if (ip_rate_limiter && ip)
102
- ip_rate_limiter.reset(ip);
110
+ // Successful bootstrap — update state immediately. Nothing is
111
+ // reset: bootstrap has no account-grain bucket to forgive (it
112
+ // predates any account), and the IP bucket is never refunded on
113
+ // success (see `RateLimiter.reset`). So this is the one auth
114
+ // surface where a success clears nothing at all. Bootstrap is
115
+ // one-shot, so at most `max_attempts - 1` residual entries
116
+ // survive — and they sit on this route's own limiter, not
117
+ // login's, so a fumbled token can't spend the operator's login
118
+ // budget on a deployment where their account is the only one.
103
119
  bootstrap_status.available = false;
104
120
  await create_session_and_set_cookie({
105
121
  keyring,
@@ -9,7 +9,6 @@
9
9
  import type { SessionOptions } from './session_cookie.js';
10
10
  import type { AppDeps } from './deps.js';
11
11
  import type { DaemonTokenState } from './daemon_token.js';
12
- import type { RateLimiter } from '../rate_limiter.js';
13
12
  import type { MiddlewareSpec } from '../http/middleware_spec.js';
14
13
  /**
15
14
  * Per-factory configuration for the standard auth middleware stack.
@@ -21,8 +20,6 @@ export interface AuthMiddlewareOptions {
21
20
  path?: string;
22
21
  /** Daemon token state for keeper auth. Omit to disable daemon token middleware. */
23
22
  daemon_token_state?: DaemonTokenState;
24
- /** Rate limiter for bearer token auth attempts (per-IP). Pass `null` to disable. */
25
- bearer_ip_rate_limiter: RateLimiter | null;
26
23
  }
27
24
  /**
28
25
  * Create the auth middleware stack.
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,oFAAoF;IACpF,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAgF/B,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAuE/B,CAAC"}
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { ApiError, RateLimitError } from '../http/error_schemas.js';
9
+ import { ApiError } from '../http/error_schemas.js';
10
10
  /**
11
11
  * Create the auth middleware stack.
12
12
  *
@@ -21,7 +21,7 @@ import { ApiError, RateLimitError } from '../http/error_schemas.js';
21
21
  */
22
22
  export const create_auth_middleware_specs = async (deps, options) => {
23
23
  const { keyring, db } = deps;
24
- const { allowed_origins, session_options, path = '/api/*', daemon_token_state, bearer_ip_rate_limiter } = options;
24
+ const { allowed_origins, session_options, path = '/api/*', daemon_token_state } = options;
25
25
  const query_deps = { db };
26
26
  // Dynamic imports preserve the bundle-split for type-only consumers of
27
27
  // this module (`MiddlewareSpec`, `AuthMiddlewareOptions`, etc.). The
@@ -36,7 +36,7 @@ export const create_auth_middleware_specs = async (deps, options) => {
36
36
  ]);
37
37
  const session_middleware = create_session_middleware(keyring, session_options);
38
38
  const request_context_middleware = create_request_context_middleware(query_deps, deps.log);
39
- const bearer_auth_middleware = create_bearer_auth_middleware(query_deps, bearer_ip_rate_limiter, deps.log);
39
+ const bearer_auth_middleware = create_bearer_auth_middleware(query_deps, deps.log);
40
40
  const specs = [
41
41
  {
42
42
  name: 'origin',
@@ -50,13 +50,14 @@ export const create_auth_middleware_specs = async (deps, options) => {
50
50
  name: 'bearer_auth',
51
51
  path,
52
52
  handler: bearer_auth_middleware,
53
- // Bearer middleware soft-fails for invalid/expired tokens (calls next()
54
- // without setting context). Only 429 is a hard-fail from this layer.
55
- // Auth enforcement (401/403) happens downstream — the RPC dispatcher's
56
- // pre-authorization / post-authorization auth gates, or `require_auth` /
57
- // `require_role` on REST — producing consistent JSON-RPC or
58
- // route-level errors.
59
- errors: { 429: RateLimitError }
53
+ // Soft-fails on every non-success path — browser context, malformed,
54
+ // invalid, and expired tokens all `next()` through without setting
55
+ // context. The layer returns no error response of its own (it carries
56
+ // no rate limiter, so not even a 429). Auth enforcement (401/403)
57
+ // happens downstream — the RPC dispatcher's pre-authorization /
58
+ // post-authorization auth gates, or `require_auth` / `require_role` on
59
+ // REST — producing consistent JSON-RPC or route-level errors.
60
+ errors: {}
60
61
  }
61
62
  ];
62
63
  if (daemon_token_state) {
@@ -37,6 +37,13 @@
37
37
  * DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
38
38
  * the freeze is the contract for the deployed chain, not local iteration.
39
39
  *
40
+ * **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
41
+ * and the two names must match byte for byte: the `_testing_migration_tracker`
42
+ * parity gate compares the `(namespace, name, sequence)` rows both spines
43
+ * record, and a divergence there breaks the swap-freely invariant (a consumer
44
+ * pointing either implementation at one database). Append on both sides in the
45
+ * same change, so no entry below repeats the rule.
46
+ *
40
47
  * @module
41
48
  */
42
49
  import type { Migration, MigrationNamespace } from '../db/migrate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CA6H5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAmK5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -37,6 +37,13 @@
37
37
  * DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
38
38
  * the freeze is the contract for the deployed chain, not local iteration.
39
39
  *
40
+ * **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
41
+ * and the two names must match byte for byte: the `_testing_migration_tracker`
42
+ * parity gate compares the `(namespace, name, sequence)` rows both spines
43
+ * record, and a divergence there breaks the swap-freely invariant (a consumer
44
+ * pointing either implementation at one database). Append on both sides in the
45
+ * same change, so no entry below repeats the rule.
46
+ *
40
47
  * @module
41
48
  */
42
49
  import { ACCOUNT_SCHEMA, ACCOUNT_EMAIL_INDEX, ACCOUNT_USERNAME_CI_INDEX, ACTOR_SCHEMA, ACTOR_INDEX, ACTOR_NAME_LOWER_INDEX, ROLE_GRANT_SCHEMA, ROLE_GRANT_INDEXES, AUTH_SESSION_SCHEMA, AUTH_SESSION_INDEXES, API_TOKEN_SCHEMA, API_TOKEN_INDEX, BOOTSTRAP_LOCK_SCHEMA, BOOTSTRAP_LOCK_SEED, INVITE_SCHEMA, INVITE_INDEXES, APP_SETTINGS_SCHEMA, APP_SETTINGS_SEED } from './auth_ddl.js';
@@ -156,10 +163,6 @@ export const auth_migrations = [
156
163
  },
157
164
  // v2: per-credential token scoping — `api_token.scope JSONB NOT NULL`.
158
165
  //
159
- // Name must match the Rust twin's migration byte for byte
160
- // (`fuz_auth/src/migrations.rs`) or the `_testing_migration_tracker` parity
161
- // gate fails and the swap-freely invariant breaks.
162
- //
163
166
  // Three statements, and the middle one is the whole compatibility story:
164
167
  //
165
168
  // 1. Add the column nullable, so the ALTER is instant on a populated table.
@@ -186,6 +189,44 @@ export const auth_migrations = [
186
189
  WHERE scope IS NULL`);
187
190
  await db.query('ALTER TABLE api_token ALTER COLUMN scope SET NOT NULL');
188
191
  }
192
+ },
193
+ // Index the column every bearer request looks up.
194
+ //
195
+ // `full_auth_schema` indexes `api_token(account_id)` and nothing else, so
196
+ // `query_validate_api_token`'s `WHERE token_hash = $1` — the lookup on the
197
+ // hot path of *every* bearer-authenticated request, valid or not — has been
198
+ // a sequential scan since the table shipped. The asymmetry is accidental:
199
+ // `auth_session` gets the same access pattern for free because its session
200
+ // hash *is* the primary key.
201
+ //
202
+ // UNIQUE rather than a plain index, because both spines resolve a token with
203
+ // an at-most-one read (`query_one` / `query_opt`). Two rows sharing a hash
204
+ // would mean the two impls could silently disagree about which credential
205
+ // answered; the constraint makes that unrepresentable instead of merely
206
+ // improbable.
207
+ {
208
+ name: 'api_token_hash_unique_index',
209
+ up: async (db) => {
210
+ await db.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_api_token_hash ON api_token(token_hash)');
211
+ }
212
+ },
213
+ // The GIN the per-cell audit timeline assumes.
214
+ //
215
+ // `full_auth_schema` ships only btree indexes (seq, account, event_type,
216
+ // targets), but `db/cell_audit_queries.ts` reconstructs the per-cell
217
+ // timeline by OR-ing `metadata @> '{...}'::jsonb` containment clauses —
218
+ // sequential scans without this index, degrading with audit volume.
219
+ //
220
+ // `jsonb_path_ops` rather than the default `jsonb_ops`: it serves exactly
221
+ // the `@>` containment operator these queries use, with a smaller, faster
222
+ // index. Server PG and PGlite honor the opclass; PG-compatible engines
223
+ // without operator classes parse and ignore it, serving `@>` from their
224
+ // generic GIN.
225
+ {
226
+ name: 'audit_log_metadata_gin_index',
227
+ up: async (db) => {
228
+ await db.query('CREATE INDEX IF NOT EXISTS idx_audit_log_metadata ON audit_log USING gin (metadata jsonb_path_ops)');
229
+ }
189
230
  }
190
231
  ];
191
232
  /** Pre-composed migration namespace for auth tables. */
@@ -113,8 +113,19 @@ export declare const query_session_list_for_account: (deps: QueryDeps, account_i
113
113
  * both commit above `max_sessions`. A transaction is necessary here but not
114
114
  * sufficient. Closing this needs one serialization point per
115
115
  * `(account_id, credential_kind)` — a locked parent row or a transaction-scoped
116
- * advisory lock — taken before count/evict/insert, plus a stable tie-breaker
117
- * alongside `created_at` so the survivors are deterministic.
116
+ * advisory lock — taken before count/evict/insert.
117
+ *
118
+ * Ordering is `created_at DESC, id DESC`. The `id` leg is a **stability**
119
+ * tie-breaker, not a recency one: `created_at` defaults to `NOW()`, the
120
+ * *transaction* timestamp, so two sessions born in one transaction — or in two
121
+ * transactions that started in the same microsecond — tie, and an untied
122
+ * `OFFSET` may keep a different set on two evaluations of the same rows. `id` is
123
+ * the blake3 token hash, so the tie-break is arbitrary but deterministic; it
124
+ * makes the survivors reproducible, it does not make the row just inserted a
125
+ * guaranteed survivor. Only the serialization point above does that.
126
+ *
127
+ * Expired-but-unreaped rows count toward the cap (the predicate is `account_id`
128
+ * alone). Matches the Rust twin `query_session_enforce_limit`.
118
129
  *
119
130
  * @param deps - query dependencies (must be transaction-scoped)
120
131
  * @param account_id - the account to enforce the limit for
@@ -1 +1 @@
1
- {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EACjD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAMd,CAAC"}
1
+ {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EACjD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAMd,CAAC"}
@@ -143,8 +143,19 @@ export const query_session_list_for_account = async (deps, account_id, limit = 5
143
143
  * both commit above `max_sessions`. A transaction is necessary here but not
144
144
  * sufficient. Closing this needs one serialization point per
145
145
  * `(account_id, credential_kind)` — a locked parent row or a transaction-scoped
146
- * advisory lock — taken before count/evict/insert, plus a stable tie-breaker
147
- * alongside `created_at` so the survivors are deterministic.
146
+ * advisory lock — taken before count/evict/insert.
147
+ *
148
+ * Ordering is `created_at DESC, id DESC`. The `id` leg is a **stability**
149
+ * tie-breaker, not a recency one: `created_at` defaults to `NOW()`, the
150
+ * *transaction* timestamp, so two sessions born in one transaction — or in two
151
+ * transactions that started in the same microsecond — tie, and an untied
152
+ * `OFFSET` may keep a different set on two evaluations of the same rows. `id` is
153
+ * the blake3 token hash, so the tie-break is arbitrary but deterministic; it
154
+ * makes the survivors reproducible, it does not make the row just inserted a
155
+ * guaranteed survivor. Only the serialization point above does that.
156
+ *
157
+ * Expired-but-unreaped rows count toward the cap (the predicate is `account_id`
158
+ * alone). Matches the Rust twin `query_session_enforce_limit`.
148
159
  *
149
160
  * @param deps - query dependencies (must be transaction-scoped)
150
161
  * @param account_id - the account to enforce the limit for
@@ -157,7 +168,7 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
157
168
  WHERE id IN (
158
169
  SELECT id FROM auth_session
159
170
  WHERE account_id = $1
160
- ORDER BY created_at DESC
171
+ ORDER BY created_at DESC, id DESC
161
172
  OFFSET $2
162
173
  ) RETURNING id`, [account_id, max_sessions]);
163
174
  return rows.length;
@@ -36,6 +36,16 @@ export declare const DEFAULT_SIGNUP_FAIL_JITTER_MS = 25;
36
36
  * Per-factory configuration for signup route specs.
37
37
  */
38
38
  export interface SignupRouteOptions extends AuthSessionRouteOptions {
39
+ /**
40
+ * Rate limiter for signup attempts, keyed by client IP. Pass `null` to
41
+ * disable. Its own instance, not login's: signup failures bound username
42
+ * *enumeration* (the 403/409 split), not credential guessing, and an
43
+ * open-signup deployment lets any unauthenticated caller spend this budget
44
+ * — which must not cost anyone their login attempts. Never refunded on
45
+ * success (see `RateLimiter.reset`). Mirrors the Rust spine's
46
+ * `signup_ip_rate_limiter`.
47
+ */
48
+ signup_ip_rate_limiter: RateLimiter | null;
39
49
  /** Rate limiter for signup attempts, keyed by submitted username. Pass `null` to disable. */
40
50
  signup_account_rate_limiter: RateLimiter | null;
41
51
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAQhD;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CA4KjB,CAAC"}
1
+ {"version":3,"file":"signup_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/signup_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAQhD;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAClE;;;;;;;;OAQG;IACH,sBAAsB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3C,6FAA6F;IAC7F,2BAA2B,EAAE,WAAW,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,gBAAgB,EACtB,SAAS,kBAAkB,KACzB,KAAK,CAAC,SAAS,CAgLjB,CAAC"}
@@ -55,7 +55,7 @@ const signup_fail_delay = (floor_ms, jitter_ms) => {
55
55
  */
56
56
  export const create_signup_route_specs = (deps, options) => {
57
57
  const { keyring, password } = deps;
58
- const { session_options, ip_rate_limiter, signup_account_rate_limiter, signup_fail_floor_ms = DEFAULT_SIGNUP_FAIL_FLOOR_MS, signup_fail_jitter_ms = DEFAULT_SIGNUP_FAIL_JITTER_MS } = options;
58
+ const { session_options, signup_ip_rate_limiter, signup_account_rate_limiter, signup_fail_floor_ms = DEFAULT_SIGNUP_FAIL_FLOOR_MS, signup_fail_jitter_ms = DEFAULT_SIGNUP_FAIL_JITTER_MS } = options;
59
59
  return [
60
60
  {
61
61
  ...create_signup_route_shape({
@@ -63,9 +63,9 @@ export const create_signup_route_specs = (deps, options) => {
63
63
  }),
64
64
  handler: async (c, route) => {
65
65
  // Per-IP rate limit check (before any work). 429 stays fast.
66
- const ip = ip_rate_limiter ? get_client_ip(c) : null;
67
- if (ip_rate_limiter && ip) {
68
- const check = ip_rate_limiter.check(ip);
66
+ const ip = signup_ip_rate_limiter ? get_client_ip(c) : null;
67
+ if (signup_ip_rate_limiter && ip) {
68
+ const check = signup_ip_rate_limiter.check(ip);
69
69
  if (!check.allowed) {
70
70
  return rate_limit_exceeded_response(c, check.retry_after);
71
71
  }
@@ -157,8 +157,8 @@ export const create_signup_route_specs = (deps, options) => {
157
157
  }
158
158
  catch (e) {
159
159
  if (e instanceof NoMatchingInviteError) {
160
- if (ip_rate_limiter && ip)
161
- ip_rate_limiter.record(ip);
160
+ if (signup_ip_rate_limiter && ip)
161
+ signup_ip_rate_limiter.record(ip);
162
162
  if (signup_account_rate_limiter)
163
163
  signup_account_rate_limiter.record(account_key);
164
164
  emit_failure_audit('no_match');
@@ -167,8 +167,8 @@ export const create_signup_route_specs = (deps, options) => {
167
167
  }
168
168
  // Unique constraint violation: username or email already exists.
169
169
  if (is_pg_unique_violation(e)) {
170
- if (ip_rate_limiter && ip)
171
- ip_rate_limiter.record(ip);
170
+ if (signup_ip_rate_limiter && ip)
171
+ signup_ip_rate_limiter.record(ip);
172
172
  if (signup_account_rate_limiter)
173
173
  signup_account_rate_limiter.record(account_key);
174
174
  emit_failure_audit('signup_conflict');
@@ -185,9 +185,12 @@ export const create_signup_route_specs = (deps, options) => {
185
185
  emit_failure_audit('internal_error');
186
186
  throw e;
187
187
  }
188
- // Reset rate limiters on success
189
- if (ip_rate_limiter && ip)
190
- ip_rate_limiter.reset(ip);
188
+ // Clear the account-grain bucket only — the IP aggregate stands.
189
+ // This is the cheapest version of the spray path `RateLimiter.reset`
190
+ // describes: where login at least costs the attacker a credential,
191
+ // an open-signup deployment let an *unauthenticated* caller zero a
192
+ // shared IP budget by creating throwaway accounts, and the
193
+ // per-account limiter never applies to the attacker's own signups.
191
194
  if (signup_account_rate_limiter)
192
195
  signup_account_rate_limiter.reset(account_key);
193
196
  deps.audit.emit(route, {
package/dist/db/CLAUDE.md CHANGED
@@ -25,7 +25,7 @@ the code.
25
25
  `baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
26
26
  - `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
27
27
  - `assert_row.ts` — `assert_row(row, context)` for INSERT … RETURNING.
28
- - `pg_error.ts` — `is_pg_unique_violation` (Postgres `23505`).
28
+ - `pg_error.ts` — `pg_error_code` (SQLSTATE extractor for `pg`/PGlite errors), `is_pg_unique_violation` (Postgres `23505`).
29
29
  - `sql_identifier.ts` — `assert_valid_sql_identifier`.
30
30
  - `status.ts` — CLI DB status utility.
31
31
  - `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
@@ -95,7 +95,8 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
95
95
  - **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
96
96
  `audit_log` rows whose `metadata` names the cell on any of six cell-domain
97
97
  keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
98
- `target_id`), bitmap-OR over the metadata GIN.
98
+ `target_id`), bitmap-OR over `idx_audit_log_metadata` (the `jsonb_path_ops`
99
+ GIN the `audit_log_metadata_gin_index` auth migration ships).
99
100
 
100
101
  ## Fact layer
101
102
 
@@ -155,6 +156,9 @@ auth_migration_ns (built-in, fuz_auth)
155
156
 
156
157
  ### Pre-stable migration policy
157
158
 
158
- Pre-stable: when an introducing migration needs to change shape, **rewrite
159
- the original in place** rather than appending a follow-up — consumers
160
- re-bootstrap dev DBs on upgrade. See ../../../docs/migrations.md.
159
+ Scoped to the namespaces on this page (`fuz_cell` / `fuz_cell_history` /
160
+ `fuz_facts`): each is a single `full_*_schema` entry, so when it needs to
161
+ change shape, **rewrite the original in place** rather than appending a
162
+ follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
163
+ namespace is the opposite — frozen and append-only, since consumers hold
164
+ deployed databases on it. See ../../../docs/migrations.md §Schema stability.
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
1
+ {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -6,6 +6,18 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export declare const pg_error_code: (error: unknown) => string | null;
9
21
  /**
10
22
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OACmC,CAAC"}
1
+ {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAGhD,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAA2C,CAAC"}
@@ -6,10 +6,24 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export const pg_error_code = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string'
21
+ ? error.code
22
+ : null;
9
23
  /**
10
24
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
25
  *
12
26
  * @param error - the caught error
13
27
  * @returns `true` if the error is a unique constraint violation
14
28
  */
15
- export const is_pg_unique_violation = (error) => error instanceof Error && 'code' in error && error.code === '23505';
29
+ export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';