@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.
- package/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/account_route_schema.d.ts +10 -1
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +10 -1
- package/dist/auth/account_routes.d.ts +18 -4
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +32 -29
- package/dist/auth/api_token_queries.d.ts +4 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +5 -1
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bearer_auth.d.ts +9 -6
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +12 -21
- package/dist/auth/bootstrap_routes.d.ts +12 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +26 -10
- package/dist/auth/middleware.d.ts +0 -3
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +11 -10
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +45 -4
- package/dist/auth/session_queries.d.ts +13 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +14 -3
- package/dist/auth/signup_routes.d.ts +10 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -11
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +13 -2
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +13 -2
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/rate_limiter.d.ts +36 -3
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +36 -3
- package/dist/server/app_server.d.ts +27 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +30 -20
- package/dist/server/app_server_context.d.ts +10 -2
- package/dist/server/app_server_context.d.ts.map +1 -1
- package/dist/testing/CLAUDE.md +42 -8
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +6 -4
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +9 -8
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +59 -0
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/testing/cross_backend/session_cap.d.ts +19 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cap.js +93 -0
- package/dist/testing/middleware.d.ts +2 -5
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +5 -5
- package/dist/testing/rate_limiting.d.ts +10 -16
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +11 -95
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +3 -1
- package/dist/testing/testing_rate_limiter.d.ts +7 -1
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -1
- package/dist/testing/testing_rate_limiter.js +7 -1
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- 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,
|
|
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 =
|
|
72
|
-
if (
|
|
73
|
-
const check =
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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,
|
|
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"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { ApiError
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
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
|
|
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"}
|
package/dist/auth/migrations.js
CHANGED
|
@@ -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
|
|
117
|
-
*
|
|
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
|
|
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
|
|
147
|
-
*
|
|
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,
|
|
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,
|
|
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 =
|
|
67
|
-
if (
|
|
68
|
-
const check =
|
|
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 (
|
|
161
|
-
|
|
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 (
|
|
171
|
-
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
9
|
-
* `audit_log.metadata
|
|
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
|
|
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
|
|
9
|
-
* `audit_log.metadata
|
|
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
|
*/
|
package/dist/db/pg_error.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/db/pg_error.js
CHANGED
|
@@ -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
|
|
29
|
+
export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';
|