@fuzdev/fuz_app 0.128.0 → 0.130.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/dist/actions/CLAUDE.md +23 -6
- package/dist/actions/connection_closer.d.ts +3 -1
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/register_action_ws.d.ts +46 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +105 -14
- package/dist/actions/transports.d.ts +2 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +2 -1
- package/dist/actions/transports_ws_backend.d.ts +37 -4
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +68 -4
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/actor_lookup_action_specs.d.ts +2 -2
- package/dist/auth/actor_lookup_action_specs.js +2 -2
- package/dist/auth/audit_emitter.d.ts +14 -7
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +6 -8
- package/dist/auth/audit_log_routes.d.ts +7 -3
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +48 -26
- package/dist/auth/bearer_auth.d.ts +3 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +3 -2
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/request_context.js +1 -1
- package/dist/auth/token_scope.d.ts +4 -2
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +10 -2
- package/dist/http/CLAUDE.md +4 -5
- package/dist/http/auth_shape.d.ts +3 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +3 -2
- package/dist/http/common_routes.d.ts +1 -14
- package/dist/http/common_routes.d.ts.map +1 -1
- package/dist/http/common_routes.js +1 -19
- package/dist/http/ip_canonical.d.ts +1 -1
- package/dist/http/ip_canonical.js +1 -1
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +7 -5
- package/dist/realtime/sse.d.ts +14 -1
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse.js +77 -1
- package/dist/realtime/sse_auth_guard.d.ts +3 -1
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +4 -2
- package/dist/realtime/subscriber_registry.d.ts +44 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +64 -10
- package/dist/server/app_server.d.ts +122 -8
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +136 -91
- package/dist/server/surface_route.d.ts +32 -0
- package/dist/server/surface_route.d.ts.map +1 -0
- package/dist/server/surface_route.js +46 -0
- package/dist/testing/CLAUDE.md +33 -12
- package/dist/testing/app_server.d.ts +39 -4
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +84 -68
- package/dist/testing/audit_completeness.js +1 -1
- package/dist/testing/audit_drift_guard.d.ts +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts +2 -2
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +5 -2
- package/dist/testing/cross_backend/identity_parity.js +1 -1
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +4 -1
- package/dist/testing/cross_backend/method_coverage.d.ts +1 -1
- package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.js +2 -2
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +4 -4
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.d.ts +4 -4
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts +1 -1
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +24 -14
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +40 -32
- package/dist/testing/cross_backend/testing_server_bun.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_bun.js +80 -64
- package/dist/testing/cross_backend/testing_server_core.d.ts +10 -4
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_deno.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_deno.js +1 -1
- package/dist/testing/cross_backend/testing_server_node.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_node.js +2 -2
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +20 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +177 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +1 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +11 -9
- package/dist/testing/stubs.d.ts +45 -7
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +60 -31
- package/dist/testing/ws_round_trip.d.ts +1 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/ui/AdminSurface.svelte +3 -1
- package/dist/ui/AdminSurface.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +3 -1
- package/dist/ui/async_slot.svelte.d.ts +4 -4
- package/dist/ui/async_slot.svelte.js +4 -4
- package/dist/ui/keyed_async_slot.svelte.d.ts +4 -4
- package/dist/ui/keyed_async_slot.svelte.js +3 -3
- package/package.json +1 -1
|
@@ -44,6 +44,21 @@
|
|
|
44
44
|
* uses the two-phase form. The twin of the Rust `fuz_realtime`
|
|
45
45
|
* `ConnectionRegistry`.
|
|
46
46
|
*
|
|
47
|
+
* ## Shutdown
|
|
48
|
+
*
|
|
49
|
+
* `close_all_sockets` — the close `AppServer.close` runs — closes every
|
|
50
|
+
* connection, pending or admitted, with `WS_CLOSE_GOING_AWAY`, and leaves the
|
|
51
|
+
* transport **closing**: from then on every registration is born closed. A
|
|
52
|
+
* `register_pending` or `add_connection` inserts nothing — it aborts the
|
|
53
|
+
* caller's controller and closes the socket with `WS_CLOSE_GOING_AWAY` at once
|
|
54
|
+
* — so an upgrade still in flight when the server shuts down is closed rather
|
|
55
|
+
* than outliving it. An upgrade registered before the close-all is closed by
|
|
56
|
+
* it; one registered after is born closed; none is admitted. The flag is set
|
|
57
|
+
* in the same synchronous step that closes the live connections, so an `admit`
|
|
58
|
+
* that finds its entry gone and reads `is_closing()` sees the shutdown, never
|
|
59
|
+
* a revocation. Closing is permanent: a closed transport hosts no further
|
|
60
|
+
* connections.
|
|
61
|
+
*
|
|
47
62
|
* @module
|
|
48
63
|
*/
|
|
49
64
|
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
@@ -78,6 +93,9 @@ export class BackendWebsocketTransport {
|
|
|
78
93
|
// "oldest"). Adding a new identity scope (e.g. `device_id`) means adding a
|
|
79
94
|
// field to `ConnectionIdentity`, not a parallel map.
|
|
80
95
|
#connections = new Map();
|
|
96
|
+
// set once by `close_all_sockets`, never cleared — every later
|
|
97
|
+
// registration is born closed (module doc, "Shutdown")
|
|
98
|
+
#closing = false;
|
|
81
99
|
// Server→client request correlation (ActionPeer). The transport owns the
|
|
82
100
|
// sockets + the send; the registry owns the pending map, id allocation,
|
|
83
101
|
// deadlines, and the per-connection in-flight cap (see `peer_request.ts`).
|
|
@@ -109,6 +127,11 @@ export class BackendWebsocketTransport {
|
|
|
109
127
|
* The caller owns the entry until it is admitted: every refusal path calls
|
|
110
128
|
* `remove_connection` with the returned id.
|
|
111
129
|
*
|
|
130
|
+
* Once the transport is closing (`close_all_sockets`) the registration is
|
|
131
|
+
* born closed: nothing is inserted, `abort_controller` is aborted, and `ws`
|
|
132
|
+
* is closed with `WS_CLOSE_GOING_AWAY` before this returns. The id names
|
|
133
|
+
* nothing, so `admit` refuses it (module doc, "Shutdown").
|
|
134
|
+
*
|
|
112
135
|
* @param ws - the socket to close on revocation
|
|
113
136
|
* @param token_hash - blake3 session token hash, or `null` for non-session credentials
|
|
114
137
|
* @param account_id - the authenticated account
|
|
@@ -116,9 +139,12 @@ export class BackendWebsocketTransport {
|
|
|
116
139
|
* @param abort_controller - the socket-scoped controller to abort when the
|
|
117
140
|
* transport closes this connection (revocation or cap eviction)
|
|
118
141
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
119
|
-
* @mutates this - inserts an un-admitted entry into `#connections
|
|
142
|
+
* @mutates this - inserts an un-admitted entry into `#connections`, unless
|
|
143
|
+
* the transport is closing
|
|
120
144
|
*/
|
|
121
145
|
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
146
|
+
if (this.#closing)
|
|
147
|
+
return this.#close_born_closed(ws, abort_controller);
|
|
122
148
|
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, false);
|
|
123
149
|
}
|
|
124
150
|
/**
|
|
@@ -174,7 +200,8 @@ export class BackendWebsocketTransport {
|
|
|
174
200
|
* inserted. A removed connection gets no further broadcast or peer request,
|
|
175
201
|
* stops counting toward the cap at once, and has its `abort_controller`
|
|
176
202
|
* aborted; its socket is closed when the close handshake completes. The new
|
|
177
|
-
* connection is always admitted
|
|
203
|
+
* connection is always admitted — unless the transport is closing: then it
|
|
204
|
+
* is born closed, as in `register_pending`, and evicts nothing.
|
|
178
205
|
*
|
|
179
206
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
180
207
|
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
@@ -182,6 +209,8 @@ export class BackendWebsocketTransport {
|
|
|
182
209
|
* their `WSContext`
|
|
183
210
|
*/
|
|
184
211
|
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
212
|
+
if (this.#closing)
|
|
213
|
+
return this.#close_born_closed(ws, abort_controller);
|
|
185
214
|
if (this.#max_connections_per_account !== null) {
|
|
186
215
|
this.#evict_oldest_for_account(account_id, this.#max_connections_per_account);
|
|
187
216
|
}
|
|
@@ -305,14 +334,49 @@ export class BackendWebsocketTransport {
|
|
|
305
334
|
* revocation does, so its heartbeat timer stops and nothing more dispatches
|
|
306
335
|
* on it. A socket whose `close` throws does not stop the loop.
|
|
307
336
|
*
|
|
337
|
+
* Leaves the transport closing, for good: every later registration is born
|
|
338
|
+
* closed (module doc, "Shutdown"). A second call closes nothing.
|
|
339
|
+
*
|
|
308
340
|
* @returns the number of sockets closed
|
|
309
|
-
* @mutates this - removes every connection,
|
|
310
|
-
* controller, and closes its underlying `WSContext`
|
|
341
|
+
* @mutates this - sets the transport closing, removes every connection,
|
|
342
|
+
* aborts its registered controller, and closes its underlying `WSContext`
|
|
343
|
+
* with `WS_CLOSE_GOING_AWAY`
|
|
311
344
|
* @throws the first error a socket's `close` threw, after every connection was attempted
|
|
312
345
|
*/
|
|
313
346
|
close_all_sockets() {
|
|
347
|
+
// before any close, in the same synchronous step: an `admit` that finds
|
|
348
|
+
// its entry gone reads the shutdown, never a revocation — and set even
|
|
349
|
+
// when a socket's close throws
|
|
350
|
+
this.#closing = true;
|
|
314
351
|
return this.#close_where(() => true, WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON);
|
|
315
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Whether `close_all_sockets` has run — the transport is shutting down and
|
|
355
|
+
* every registration from now on is born closed (module doc, "Shutdown").
|
|
356
|
+
* For telemetry and logs: `register_action_ws` reads it to say why an
|
|
357
|
+
* upgrade was refused.
|
|
358
|
+
*/
|
|
359
|
+
is_closing() {
|
|
360
|
+
return this.#closing;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Close a registration that arrived after `close_all_sockets`, without
|
|
364
|
+
* inserting it. A socket whose `close` throws is logged, not thrown: the
|
|
365
|
+
* registration is refused either way, and its caller sees the aborted
|
|
366
|
+
* controller.
|
|
367
|
+
*
|
|
368
|
+
* @returns a fresh id that names no entry
|
|
369
|
+
*/
|
|
370
|
+
#close_born_closed(ws, abort_controller) {
|
|
371
|
+
abort_controller?.abort();
|
|
372
|
+
try {
|
|
373
|
+
ws.close(WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
this.#log?.error('error closing a socket registered during shutdown:', error);
|
|
377
|
+
}
|
|
378
|
+
return create_uuid();
|
|
379
|
+
}
|
|
316
380
|
#cleanup_connection(connection_id) {
|
|
317
381
|
this.#connections.delete(connection_id);
|
|
318
382
|
// Wake any handler still awaiting a reply on this socket — the peer is
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -124,7 +124,7 @@ upstream in `auth/signup_routes.ts` via `query_invite_find_unclaimed_match_for_u
|
|
|
124
124
|
its registered listeners + optional `AuditLogConfig`. Its methods:
|
|
125
125
|
|
|
126
126
|
- `emit(ctx, input)` — fire-and-forget pool write, pushes to `ctx.pending_effects`; a success row's listener fan-out goes on `ctx.post_commit_effects`
|
|
127
|
-
- `emit_role_grant_target(ctx, auth, input)` — lifts `actor_id` / `account_id` / `ip` boilerplate for role-grant-shape events
|
|
127
|
+
- `emit_role_grant_target(ctx, auth, input)` — lifts `actor_id` / `account_id` / `ip` boilerplate for role-grant-shape events; `input` is an `AuditEmitRoleGrantInput`
|
|
128
128
|
- `emit_pool(input)` — awaitable pool write for code paths without a request context (ad-hoc maintenance scripts; not for success audits paired with a mutation — those write in-tx and `notify` post-commit). Writes, then notifies, whatever the outcome
|
|
129
129
|
- `notify(event)` — fan out an already-written row to listeners (used by in-tx audit batches like `query_accept_offer.audit_events`)
|
|
130
130
|
- `add_listener(listener)` — listener registration, returning a remover for that registration (idempotent; the same function registered twice is two registrations, each with its own remover). Twin of Rust `fuz_auth` `AuditEmitter::add_listener` in firing order and snapshot-at-notify
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Account-grain — only that the caller is signed in matters, not which
|
|
13
13
|
* actor is calling, so resolution skips the actor phase. The auth gate
|
|
14
14
|
* + per-account rate limit (default 1200/15min) + the
|
|
15
|
-
*
|
|
15
|
+
* per-call cap (`ACTOR_LOOKUP_IDS_MAX`) bound the batched
|
|
16
16
|
* username-enumeration surface that the `cell_list` ↔ `actor_lookup`
|
|
17
17
|
* pair would otherwise present.
|
|
18
18
|
*
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* ids), bounded by:
|
|
39
39
|
*
|
|
40
40
|
* 1. rate-limit (per-account, see above),
|
|
41
|
-
* 2.
|
|
41
|
+
* 2. `ACTOR_LOOKUP_IDS_MAX` cap per call,
|
|
42
42
|
* 3. actor-uuid intractability (122-bit random),
|
|
43
43
|
* 4. hard-deleted actors are indistinguishable from never-existed (no
|
|
44
44
|
* tombstone oracle — see `auth/actor_lookup_queries.ts`).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Account-grain — only that the caller is signed in matters, not which
|
|
13
13
|
* actor is calling, so resolution skips the actor phase. The auth gate
|
|
14
14
|
* + per-account rate limit (default 1200/15min) + the
|
|
15
|
-
*
|
|
15
|
+
* per-call cap (`ACTOR_LOOKUP_IDS_MAX`) bound the batched
|
|
16
16
|
* username-enumeration surface that the `cell_list` ↔ `actor_lookup`
|
|
17
17
|
* pair would otherwise present.
|
|
18
18
|
*
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* ids), bounded by:
|
|
39
39
|
*
|
|
40
40
|
* 1. rate-limit (per-account, see above),
|
|
41
|
-
* 2.
|
|
41
|
+
* 2. `ACTOR_LOOKUP_IDS_MAX` cap per call,
|
|
42
42
|
* 3. actor-uuid intractability (122-bit random),
|
|
43
43
|
* 4. hard-deleted actors are indistinguishable from never-existed (no
|
|
44
44
|
* tombstone oracle — see `auth/actor_lookup_queries.ts`).
|
|
@@ -102,6 +102,19 @@ export interface AuditEmitRoleGrantContext extends AuditEmitterContext {
|
|
|
102
102
|
/** Resolved client IP from the trusted-proxy middleware — `'unknown'` if not resolved. */
|
|
103
103
|
client_ip: string;
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Input to `AuditEmitter.emit_role_grant_target` — the event fields that
|
|
107
|
+
* vary per call site; `actor_id`, `account_id`, and `ip` are lifted from
|
|
108
|
+
* the auth and request contexts.
|
|
109
|
+
*/
|
|
110
|
+
export interface AuditEmitRoleGrantInput<T extends string> {
|
|
111
|
+
event_type: T;
|
|
112
|
+
target_account_id: Uuid | null;
|
|
113
|
+
target_actor_id: Uuid | null;
|
|
114
|
+
metadata: AuditLogInput<T>['metadata'];
|
|
115
|
+
/** Default `'success'`, as for `emit`. */
|
|
116
|
+
outcome?: 'success' | 'failure';
|
|
117
|
+
}
|
|
105
118
|
/**
|
|
106
119
|
* Bound audit-emit capability. Built once at backend assembly via
|
|
107
120
|
* `create_audit_emitter`; lives on `AppDeps.audit` so factories never see
|
|
@@ -137,13 +150,7 @@ export interface AuditEmitter {
|
|
|
137
150
|
* Reach for the lower-level `emit` only when the event is non-role-grant
|
|
138
151
|
* shape (e.g. `app_settings_update`, bootstrap, signup).
|
|
139
152
|
*/
|
|
140
|
-
emit_role_grant_target<T extends string>(ctx: AuditEmitRoleGrantContext, auth: RequestActorContext, input:
|
|
141
|
-
event_type: T;
|
|
142
|
-
target_account_id: Uuid | null;
|
|
143
|
-
target_actor_id: Uuid | null;
|
|
144
|
-
metadata: AuditLogInput<T>['metadata'];
|
|
145
|
-
outcome?: 'success' | 'failure';
|
|
146
|
-
}): void;
|
|
153
|
+
emit_role_grant_target<T extends string>(ctx: AuditEmitRoleGrantContext, auth: RequestActorContext, input: AuditEmitRoleGrantInput<T>): void;
|
|
147
154
|
/**
|
|
148
155
|
* Awaitable pool write for code paths without a request context.
|
|
149
156
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IAClE,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"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"}
|
|
@@ -187,16 +187,14 @@ export const create_audit_emitter = (options) => {
|
|
|
187
187
|
};
|
|
188
188
|
const listener_count = () => listeners.length;
|
|
189
189
|
// Freeze the slot layout so consumers cannot hot-patch `emit` /
|
|
190
|
-
// `emit_role_grant_target` / `emit_pool` / `notify` after construction
|
|
191
|
-
//
|
|
192
|
-
// and only happened to work because the four slots were writable —
|
|
190
|
+
// `emit_role_grant_target` / `emit_pool` / `notify` after construction —
|
|
191
|
+
// a patched `emit` would miss role-grant-shape emits anyway, since
|
|
193
192
|
// `emit_role_grant_target` calls the closed-over inner `emit`, not
|
|
194
|
-
// `this.emit
|
|
195
|
-
//
|
|
196
|
-
// is captured by the closure before the freeze. The listener list stays
|
|
193
|
+
// `this.emit`. Tests that need instrumentation pass `emit_decorator` so the
|
|
194
|
+
// wrap is captured by the closure before the freeze. The listener list stays
|
|
197
195
|
// closure-private; it changes only through `add_listener` and the remover
|
|
198
|
-
// it returns (`create_app_server` registers its SSE + WS listeners
|
|
199
|
-
//
|
|
196
|
+
// it returns (`create_app_server` registers its SSE + WS listeners after
|
|
197
|
+
// the emitter is built, by design, and removes them on close).
|
|
200
198
|
return Object.freeze({
|
|
201
199
|
emit,
|
|
202
200
|
emit_role_grant_target,
|
|
@@ -56,9 +56,13 @@ export interface AuditLogRouteOptions {
|
|
|
56
56
|
* nothing else — to an `EventSource`, a stream closed an instant after it
|
|
57
57
|
* opened (the client's reconnect is answered by the gates, with the precise
|
|
58
58
|
* status). A pending registration receives no audit row, counts toward no cap,
|
|
59
|
-
* and is removed when a re-read refuses the request.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* and is removed when a re-read refuses the request. Once the server is
|
|
60
|
+
* shutting down (`SubscriberRegistry` class doc, "Shutdown") the registration
|
|
61
|
+
* is born closed, the re-reads are skipped, and the answer is that same
|
|
62
|
+
* connect comment alone — as it is when the shutdown closes a registration
|
|
63
|
+
* whose re-read is in flight, even if the re-read then fails. The twin of the
|
|
64
|
+
* Rust spine's `audit_stream_router`, which registers before its role read
|
|
65
|
+
* rather than repeating it.
|
|
62
66
|
*
|
|
63
67
|
* @param options - optional stream wiring + role override
|
|
64
68
|
* @returns the SSE route spec (when `options.stream` is provided) or an empty array
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAUvD,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAU7E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACR,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;QAC9C,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED
|
|
1
|
+
{"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAUvD,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAU7E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACR,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;QAC9C,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CAiG5F,CAAC"}
|
|
@@ -42,9 +42,13 @@ import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
|
|
|
42
42
|
* nothing else — to an `EventSource`, a stream closed an instant after it
|
|
43
43
|
* opened (the client's reconnect is answered by the gates, with the precise
|
|
44
44
|
* status). A pending registration receives no audit row, counts toward no cap,
|
|
45
|
-
* and is removed when a re-read refuses the request.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* and is removed when a re-read refuses the request. Once the server is
|
|
46
|
+
* shutting down (`SubscriberRegistry` class doc, "Shutdown") the registration
|
|
47
|
+
* is born closed, the re-reads are skipped, and the answer is that same
|
|
48
|
+
* connect comment alone — as it is when the shutdown closes a registration
|
|
49
|
+
* whose re-read is in flight, even if the re-read then fails. The twin of the
|
|
50
|
+
* Rust spine's `audit_stream_router`, which registers before its role read
|
|
51
|
+
* rather than repeating it.
|
|
48
52
|
*
|
|
49
53
|
* @param options - optional stream wiring + role override
|
|
50
54
|
* @returns the SSE route spec (when `options.stream` is provided) or an empty array
|
|
@@ -87,34 +91,52 @@ export const create_audit_log_route_specs = (options) => {
|
|
|
87
91
|
// a revocation that closed before the registration is committed,
|
|
88
92
|
// so these see it. A throw here fails closed — no stream opens
|
|
89
93
|
// unchecked. Skipped under the test-preset escape hatch, whose
|
|
90
|
-
// pre-baked context has no rows behind it to re-read
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
// pre-baked context has no rows behind it to re-read, and for a
|
|
95
|
+
// registration born closed by the shutdown, which admission
|
|
96
|
+
// refuses whatever they answer.
|
|
97
|
+
if (!c.get(TEST_CONTEXT_PRESET_KEY) && !registry.closing) {
|
|
98
|
+
const reread = async () => {
|
|
99
|
+
const resolved = get_resolved_auth(c);
|
|
100
|
+
const credential_is_live = resolved !== null && (await revalidate_resolved_auth(route, resolved));
|
|
101
|
+
if (!credential_is_live) {
|
|
102
|
+
log.info('audit stream: credential revoked during the request', ctx.account.id);
|
|
103
|
+
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
104
|
+
}
|
|
105
|
+
// Global grants only, like the `require_role` gate that ran in
|
|
106
|
+
// the pipeline — a scoped grant must not open the instance-wide
|
|
107
|
+
// stream.
|
|
108
|
+
if (!has_scoped_role(await refresh_role_grants(ctx, route), required_role, null)) {
|
|
109
|
+
log.info('audit stream: role revoked during the request', ctx.account.id);
|
|
110
|
+
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: [required_role] }, 403);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
const refusal = await reread().catch((error) => {
|
|
115
|
+
// the shutdown closed the registration and then the database
|
|
116
|
+
// under the re-read: the answer is the shutdown's — admission
|
|
117
|
+
// below refuses, since a closing registry admits nothing —
|
|
118
|
+
// not a failure
|
|
119
|
+
if (registry.closing)
|
|
120
|
+
return null;
|
|
121
|
+
throw error;
|
|
122
|
+
});
|
|
123
|
+
if (refusal)
|
|
124
|
+
return refusal;
|
|
105
125
|
}
|
|
106
126
|
// Admit — the cap's eviction happens here, after every gate. A
|
|
107
|
-
// registration a revocation closed while it was
|
|
108
|
-
//
|
|
127
|
+
// registration a revocation or the shutdown closed while it was
|
|
128
|
+
// pending, or one born closed, is not admitted: the stream is
|
|
129
|
+
// closed at once, so the body is the
|
|
109
130
|
// connect comment and nothing else, and the client reconnects
|
|
110
|
-
// into the gates.
|
|
131
|
+
// into the gates. The close listener goes on first: a client that
|
|
132
|
+
// left during the re-reads gets a stream that is already closed,
|
|
133
|
+
// whose listener runs at once and removes the registration, so
|
|
134
|
+
// `admit` refuses it rather than evicting a live stream for it.
|
|
111
135
|
const { response, stream } = create_sse_response(c, log);
|
|
136
|
+
stream.on_close(pending.unsubscribe);
|
|
112
137
|
admitted = registry.admit(pending, stream);
|
|
113
|
-
if (admitted) {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
log.info('audit stream: closed by a revocation before admission', ctx.account.id);
|
|
138
|
+
if (!admitted) {
|
|
139
|
+
log.info('audit stream: closed before admission', ctx.account.id);
|
|
118
140
|
stream.close();
|
|
119
141
|
}
|
|
120
142
|
return response;
|
|
@@ -28,8 +28,9 @@ import type { QueryDeps } from '../db/query_deps.js';
|
|
|
28
28
|
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
29
29
|
* and ensures public actions are not blocked by bad credentials.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Discards bearer tokens when an `Origin` or `Referer` header is present —
|
|
32
|
+
* `next()` with no identity, as for a bad token — so browsers must use cookie
|
|
33
|
+
* auth, reducing attack surface.
|
|
33
34
|
* Auth scheme matching is case-insensitive per RFC 7235.
|
|
34
35
|
* On success, sets `c.var.auth_account_id`, `CREDENTIAL_TYPE_KEY = 'api_token'`,
|
|
35
36
|
* and `AUTH_API_TOKEN_ID_KEY`. Skips when an account is already authenticated
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD
|
|
1
|
+
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,iBAuE5E,CAAC"}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -31,8 +31,9 @@ import { is_browser_context } from '../http/origin.js';
|
|
|
31
31
|
* (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
|
|
32
32
|
* and ensures public actions are not blocked by bad credentials.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Discards bearer tokens when an `Origin` or `Referer` header is present —
|
|
35
|
+
* `next()` with no identity, as for a bad token — so browsers must use cookie
|
|
36
|
+
* auth, reducing attack surface.
|
|
36
37
|
* Auth scheme matching is case-insensitive per RFC 7235.
|
|
37
38
|
* On success, sets `c.var.auth_account_id`, `CREDENTIAL_TYPE_KEY = 'api_token'`,
|
|
38
39
|
* and `AUTH_API_TOKEN_ID_KEY`. Skips when an account is already authenticated
|
|
@@ -251,7 +251,7 @@ export declare const create_request_context_middleware: (deps: QueryDeps, sessio
|
|
|
251
251
|
* across route-spec declarations and direct calls alike, and the twin of the
|
|
252
252
|
* Rust signature. It needs no parse: the surface arm's identifier is pure label,
|
|
253
253
|
* since rule 3 never asks *which* surface. And it is deliberately not narrowed
|
|
254
|
-
* to the spine's
|
|
254
|
+
* to the spine's own — the name decides nothing, so there is nothing for a
|
|
255
255
|
* closed set to protect, while a consumer surface would have nothing to pass.
|
|
256
256
|
* fuz_app's own uses are pinned two ways by the surface census: the exact call
|
|
257
257
|
* site, and the scan asserting every `'surface:<name>'` literal in `src/lib`
|
|
@@ -281,7 +281,7 @@ const token_scope_denial = (c, capability) => {
|
|
|
281
281
|
* across route-spec declarations and direct calls alike, and the twin of the
|
|
282
282
|
* Rust signature. It needs no parse: the surface arm's identifier is pure label,
|
|
283
283
|
* since rule 3 never asks *which* surface. And it is deliberately not narrowed
|
|
284
|
-
* to the spine's
|
|
284
|
+
* to the spine's own — the name decides nothing, so there is nothing for a
|
|
285
285
|
* closed set to protect, while a consumer surface would have nothing to pass.
|
|
286
286
|
* fuz_app's own uses are pinned two ways by the surface census: the exact call
|
|
287
287
|
* site, and the scan asserting every `'surface:<name>'` literal in `src/lib`
|
|
@@ -106,7 +106,9 @@ export declare const token_scope_admits_method: (scope: TokenScope, method: stri
|
|
|
106
106
|
export declare const token_scope_admits_non_rpc: (scope: TokenScope) => boolean;
|
|
107
107
|
/**
|
|
108
108
|
* The non-RPC surfaces the **spine itself** mounts (rule 3). Twin of the Rust
|
|
109
|
-
* `TokenSurface` enum, name for name
|
|
109
|
+
* `TokenSurface` enum, name for name, except `app_surface` — the opt-in
|
|
110
|
+
* `GET /api/surface` explorer (`server/surface_route.ts`), which only the TS
|
|
111
|
+
* spine serves.
|
|
110
112
|
*
|
|
111
113
|
* Not a registry a consumer registers into. `RouteAuth.required_scope` accepts
|
|
112
114
|
* any well-formed `surface:<name>` (see `parse_token_scope_capability`), so a
|
|
@@ -120,7 +122,7 @@ export declare const token_scope_admits_non_rpc: (scope: TokenScope) => boolean;
|
|
|
120
122
|
* refused, in the `required_scope` a denial reports. That is the same field
|
|
121
123
|
* whose `rpc:<method>` arm has always carried arbitrary consumer method names.
|
|
122
124
|
*/
|
|
123
|
-
export declare const TOKEN_SURFACES: readonly ["db_admin", "fact_bare", "audit_stream", "ws_upgrade"];
|
|
125
|
+
export declare const TOKEN_SURFACES: readonly ["db_admin", "fact_bare", "audit_stream", "ws_upgrade", "app_surface"];
|
|
124
126
|
export type TokenSurface = (typeof TOKEN_SURFACES)[number];
|
|
125
127
|
/**
|
|
126
128
|
* Is `value` one of the surfaces the spine mounts?
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UACgB,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG
|
|
1
|
+
{"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UACgB,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,iFAMjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,YACA,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAQtE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAC3B,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,oBAAoB,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,MAAM,KAAG,oBAAoB,GAAG,IAUnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,UAAU,EACjB,YAAY,oBAAoB,KAC9B,OAGkC,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,YAAY,MAAM,KAChB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
|
package/dist/auth/token_scope.js
CHANGED
|
@@ -112,7 +112,9 @@ export const token_scope_admits_method = (scope, method) => scope.kind === 'full
|
|
|
112
112
|
export const token_scope_admits_non_rpc = (scope) => scope.kind === 'full';
|
|
113
113
|
/**
|
|
114
114
|
* The non-RPC surfaces the **spine itself** mounts (rule 3). Twin of the Rust
|
|
115
|
-
* `TokenSurface` enum, name for name
|
|
115
|
+
* `TokenSurface` enum, name for name, except `app_surface` — the opt-in
|
|
116
|
+
* `GET /api/surface` explorer (`server/surface_route.ts`), which only the TS
|
|
117
|
+
* spine serves.
|
|
116
118
|
*
|
|
117
119
|
* Not a registry a consumer registers into. `RouteAuth.required_scope` accepts
|
|
118
120
|
* any well-formed `surface:<name>` (see `parse_token_scope_capability`), so a
|
|
@@ -126,7 +128,13 @@ export const token_scope_admits_non_rpc = (scope) => scope.kind === 'full';
|
|
|
126
128
|
* refused, in the `required_scope` a denial reports. That is the same field
|
|
127
129
|
* whose `rpc:<method>` arm has always carried arbitrary consumer method names.
|
|
128
130
|
*/
|
|
129
|
-
export const TOKEN_SURFACES = [
|
|
131
|
+
export const TOKEN_SURFACES = [
|
|
132
|
+
'db_admin',
|
|
133
|
+
'fact_bare',
|
|
134
|
+
'audit_stream',
|
|
135
|
+
'ws_upgrade',
|
|
136
|
+
'app_surface'
|
|
137
|
+
];
|
|
130
138
|
/**
|
|
131
139
|
* Is `value` one of the surfaces the spine mounts?
|
|
132
140
|
*
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -28,7 +28,7 @@ effects, see ../../../docs/architecture.md.
|
|
|
28
28
|
- `http/jsonrpc.ts` — JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta`.
|
|
29
29
|
- `http/jsonrpc_errors.ts` — `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings.
|
|
30
30
|
- `http/jsonrpc_helpers.ts` — message builders, type guards, input/result normalizers.
|
|
31
|
-
- `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status
|
|
31
|
+
- `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status route specs (the surface explorer route lives in `server/surface_route.ts`).
|
|
32
32
|
- `http/db_routes.ts` — allowlist-gated keeper-only table browser route specs (`public` schema, minus the `NON_BROWSABLE_TABLES` credential floor).
|
|
33
33
|
- `http/pending_effects.ts` — `emit_after_commit` + `dispatch_with_post_commit_rollback` (the shared rollback-discard wrapper both dispatch sites use) + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
|
|
34
34
|
|
|
@@ -383,7 +383,7 @@ own by casting `as JsonrpcErrorCode`):
|
|
|
383
383
|
- `rate_limited` (-32006, HTTP 429) — server-side policy.
|
|
384
384
|
- `service_unavailable` (-32007, HTTP 503) — upstream down / maintenance.
|
|
385
385
|
- `timeout` (-32008, HTTP 504) — handler exceeded time budget.
|
|
386
|
-
- `queue_overflow` (-32009, HTTP 429) —
|
|
386
|
+
- `queue_overflow` (-32009, HTTP 429) — backpressure: a buffer refused a new request because it was full — client-side (the WS reconnect queue) or server-side (a socket past `MAX_INBOUND_DISPATCHES_PER_CONNECTION` in-flight requests).
|
|
387
387
|
- `request_cancelled` (-32010, HTTP 499) — caller-initiated cancellation (nginx "client closed").
|
|
388
388
|
|
|
389
389
|
**`invalid_params` vs `validation_error`** — use `invalid_params` (standard
|
|
@@ -484,13 +484,12 @@ at send time.
|
|
|
484
484
|
|
|
485
485
|
## Common Routes
|
|
486
486
|
|
|
487
|
-
`http/common_routes.ts` exposes
|
|
487
|
+
`http/common_routes.ts` exposes three generic route-spec factories with no
|
|
488
488
|
auth-domain dependencies:
|
|
489
489
|
|
|
490
490
|
- `create_health_route_spec()` — `GET /health`, public, returns `{status: 'ok'}`. Infrastructure endpoint for uptime monitors
|
|
491
|
-
- `create_ready_route_spec({expected, log})` — `GET /ready`, public, the deploy gate. Introspects the live DB's columns (`db/schema_ready.ts`) and compares against the committed `expected` column map: `200 {ready: true}` on match, else `503 {error: 'schema_drift' | 'db_unreachable'}`. Detailed drift logs server-side only (minimal body — no schema leak, mirrors why `/api/surface` is
|
|
491
|
+
- `create_ready_route_spec({expected, log})` — `GET /ready`, public, the deploy gate. Introspects the live DB's columns (`db/schema_ready.ts`) and compares against the committed `expected` column map: `200 {ready: true}` on match, else `503 {error: 'schema_drift' | 'db_unreachable'}`. Detailed drift logs server-side only (minimal body — no schema leak, mirrors why `/api/surface` is admin-only). Throws at assembly on an empty `expected` (fail-loud). Opt-in like `/health`; pair with `load_expected_schema(url)` — loads + URL-caches a consumer's committed `expected_schema.json` and throws on an empty map
|
|
492
492
|
- `create_server_status_route_spec({version, get_uptime_ms})` — `GET /api/server/status`, authenticated, returns `{version, uptime_ms}`
|
|
493
|
-
- `create_surface_route_spec({surface})` — `GET /api/surface`, authenticated, serves the `AppSurface` JSON. Authenticated because surface data reveals API structure (schemas, auth, routes)
|
|
494
493
|
|
|
495
494
|
Auth-aware variants (account status, bootstrap status) live in `auth/` —
|
|
496
495
|
`http/common_routes.ts` stays generic.
|
|
@@ -141,8 +141,9 @@ export declare const RouteAuth: z.ZodObject<{
|
|
|
141
141
|
*
|
|
142
142
|
* - `surface:<name>` — a non-RPC surface, refused to every narrowed token
|
|
143
143
|
* whatever its method list says (token scoping's rule that a narrowed
|
|
144
|
-
* token is RPC-only). `surface:audit_stream
|
|
145
|
-
* the spine's; a consumer names
|
|
144
|
+
* token is RPC-only). `surface:audit_stream`, `surface:fact_bare`, and
|
|
145
|
+
* `surface:app_surface` are the spine's route-spec ones; a consumer names
|
|
146
|
+
* its own without registering it.
|
|
146
147
|
* - `rpc:<method>` — one action method, refused unless the token lists it.
|
|
147
148
|
* What a route bridged from an `ActionSpec` declares: it carries the
|
|
148
149
|
* method's identity but never reaches the dispatcher's per-method gate.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;IAMpB
|
|
1
|
+
{"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;IAMpB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;;kBAyCF,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
|
package/dist/http/auth_shape.js
CHANGED
|
@@ -135,8 +135,9 @@ export const RouteAuth = z
|
|
|
135
135
|
*
|
|
136
136
|
* - `surface:<name>` — a non-RPC surface, refused to every narrowed token
|
|
137
137
|
* whatever its method list says (token scoping's rule that a narrowed
|
|
138
|
-
* token is RPC-only). `surface:audit_stream
|
|
139
|
-
* the spine's; a consumer names
|
|
138
|
+
* token is RPC-only). `surface:audit_stream`, `surface:fact_bare`, and
|
|
139
|
+
* `surface:app_surface` are the spine's route-spec ones; a consumer names
|
|
140
|
+
* its own without registering it.
|
|
140
141
|
* - `rpc:<method>` — one action method, refused unless the token lists it.
|
|
141
142
|
* What a route bridged from an `ActionSpec` declares: it carries the
|
|
142
143
|
* method's identity but never reaches the dispatcher's per-method gate.
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
10
10
|
import type { RouteSpec } from './route_spec.js';
|
|
11
|
-
import type { AppSurface } from './surface.js';
|
|
12
11
|
import { type ExpectedSchema } from '../db/schema_ready.js';
|
|
13
12
|
/**
|
|
14
13
|
* Create a public health check route spec.
|
|
@@ -57,7 +56,7 @@ export interface ReadyRouteOptions {
|
|
|
57
56
|
* else `503 {error}` (`schema_drift` when columns are missing, `db_unreachable`
|
|
58
57
|
* when the introspection query throws). The detailed drift goes to the server
|
|
59
58
|
* log only — the public body stays a minimal code so the endpoint doesn't leak
|
|
60
|
-
* schema structure (mirrors why `/api/surface` is
|
|
59
|
+
* schema structure (mirrors why `/api/surface` is admin-only). A deploy poll
|
|
61
60
|
* treats `503` as a failed release and rolls back, turning a silent
|
|
62
61
|
* schema-drift auth outage into a loud blocked deploy. See `db/schema_ready.ts`
|
|
63
62
|
* for the column-presence rationale and `auth/migrations.ts` for the
|
|
@@ -78,16 +77,4 @@ export interface ServerStatusOptions {
|
|
|
78
77
|
* this requires authentication.
|
|
79
78
|
*/
|
|
80
79
|
export declare const create_server_status_route_spec: (options: ServerStatusOptions) => RouteSpec;
|
|
81
|
-
/** Options for the surface explorer route. */
|
|
82
|
-
export interface SurfaceRouteOptions {
|
|
83
|
-
/** The generated app surface to serve. */
|
|
84
|
-
surface: AppSurface;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Create an authenticated route spec that serves the `AppSurface` as JSON.
|
|
88
|
-
*
|
|
89
|
-
* Surface data reveals API structure (routes, auth, schemas), so this
|
|
90
|
-
* requires authentication like the server status route.
|
|
91
|
-
*/
|
|
92
|
-
export declare const create_surface_route_spec: (options: SurfaceRouteOptions) => RouteSpec;
|
|
93
80
|
//# sourceMappingURL=common_routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/common_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"common_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/common_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAAO,SAQ1C,CAAC;AAKH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,GAAG,GAAG,MAAM,KAAG,cAkBxD,CAAC;AAEF,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,iBAAiB,KAAG,SAoCpE,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,mBAAmB,KAAG,SAQ7E,CAAC"}
|