@fuzdev/fuz_app 0.117.1 → 0.119.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 +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +125 -31
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +72 -44
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +60 -16
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/bootstrap_account.d.ts +3 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +3 -3
- package/dist/auth/cleanup.d.ts +219 -30
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +260 -45
- package/dist/auth/deps.d.ts +29 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +27 -0
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +10 -3
- package/dist/auth/role_grant_offer_queries.d.ts +16 -7
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +33 -15
- package/dist/auth/role_grant_offer_schema.d.ts +9 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/migrate.d.ts +8 -13
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -38
- package/dist/db/status.d.ts +58 -7
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -97
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +9 -6
- package/dist/runtime/deps.d.ts +3 -2
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/secure_file.d.ts +23 -9
- package/dist/runtime/secure_file.d.ts.map +1 -1
- package/dist/runtime/secure_file.js +29 -10
- package/dist/server/app_backend.d.ts +13 -2
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +3 -52
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +2 -46
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/expected_schema.json +1 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +2 -2
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* transactional `db` (which would be rolled back with the parent on a
|
|
12
12
|
* handler throw).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* These methods cover every fan-out shape the auth domain needs
|
|
15
|
+
* (`add_listener` and `listener_count` manage the listener chain):
|
|
15
16
|
*
|
|
16
17
|
* - `emit(ctx, input)` — fire-and-forget pool write. Pushes the in-flight
|
|
17
18
|
* promise onto `ctx.pending_effects` for post-response flushing. Errors are
|
|
@@ -22,23 +23,46 @@
|
|
|
22
23
|
* `actor_id` / `account_id` / `ip` boilerplate every role-grant-shape audit
|
|
23
24
|
* site repeated. Delegates to `emit`.
|
|
24
25
|
* - `emit_pool(input)` — awaitable pool write for code paths without a
|
|
25
|
-
*
|
|
26
|
-
* Same write-then-notify semantics as `emit`, just synchronous-with-await.
|
|
26
|
+
* request context (ad-hoc maintenance scripts). Writes, then notifies.
|
|
27
27
|
* - `notify(event)` — fan out an already-written audit row (e.g. rows
|
|
28
28
|
* returned by `query_accept_offer` that were inserted in-transaction by
|
|
29
|
-
* the query layer
|
|
30
|
-
*
|
|
29
|
+
* the query layer, or the offer-expiry rows `auth/cleanup.ts` writes in
|
|
30
|
+
* its sweep transaction). Runs every registered listener; per-listener
|
|
31
|
+
* throws are isolated.
|
|
32
|
+
*
|
|
33
|
+
* ## When listeners hear about a row
|
|
34
|
+
*
|
|
35
|
+
* `emit` writes the row at once, on the pool, whatever its outcome — the write
|
|
36
|
+
* is outside the request's transaction, so it survives a rollback. Listener
|
|
37
|
+
* fan-out is a separate step, and it depends on the outcome:
|
|
38
|
+
*
|
|
39
|
+
* - A **failure** row is announced as soon as it is written. It records an
|
|
40
|
+
* attempt, which happened whether or not the request's transaction commits.
|
|
41
|
+
* - A **success** row is announced only after the request's transaction
|
|
42
|
+
* commits, and never when the handler throws: the fan-out is queued on
|
|
43
|
+
* `ctx.post_commit_effects` and awaits the write. A success row reports a
|
|
44
|
+
* state change, and the listeners act on it — the revocation listeners close
|
|
45
|
+
* connections, the audit stream broadcasts it. Announced before the commit,
|
|
46
|
+
* a close could run while the revoked credential still reads as valid, and a
|
|
47
|
+
* rolled-back revocation would close connections anyway.
|
|
48
|
+
*
|
|
49
|
+
* A success row emitted by a handler that later throws is still in the table —
|
|
50
|
+
* the write is outside the transaction — but is never announced.
|
|
51
|
+
*
|
|
52
|
+
* The Rust spine reaches the same order by writing a success row inside the
|
|
53
|
+
* transaction and queueing its fan-out behind the commit.
|
|
31
54
|
*
|
|
32
55
|
* Listeners are a documented registration seam — `create_app_server`
|
|
33
56
|
* registers additional listeners via `add_listener` after the backend is
|
|
34
|
-
* built (the factory-managed audit-log SSE, per-endpoint WS auth guards
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
57
|
+
* built (the factory-managed audit-log SSE, per-endpoint WS auth guards, any
|
|
58
|
+
* `extra_audit_handlers` on a `WsEndpointSpec`) before the first request
|
|
59
|
+
* runs. Consumers can also register listeners directly on the emitter they
|
|
60
|
+
* return from `audit_factory` for setups that don't pass through
|
|
61
|
+
* `create_app_server`.
|
|
39
62
|
*
|
|
40
63
|
* @module
|
|
41
64
|
*/
|
|
65
|
+
import { emit_after_commit } from '../http/pending_effects.js';
|
|
42
66
|
import { query_audit_log } from './audit_log_queries.js';
|
|
43
67
|
import { builtin_audit_log_config } from './audit_log_schema.js';
|
|
44
68
|
/**
|
|
@@ -71,17 +95,38 @@ export const create_audit_emitter = (options) => {
|
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
97
|
};
|
|
74
|
-
|
|
98
|
+
// The pool write. Never rejects: a failed write is logged and resolves
|
|
99
|
+
// `null`, so there is no row to announce.
|
|
100
|
+
const write = async (input) => {
|
|
75
101
|
try {
|
|
76
|
-
|
|
77
|
-
notify(event);
|
|
102
|
+
return await query_audit_log({ db }, input, audit_log_config);
|
|
78
103
|
}
|
|
79
104
|
catch (err) {
|
|
80
105
|
log.error('Audit log write failed:', err);
|
|
106
|
+
return null;
|
|
81
107
|
}
|
|
82
108
|
};
|
|
109
|
+
const emit_pool = async (input) => {
|
|
110
|
+
const event = await write(input);
|
|
111
|
+
if (event)
|
|
112
|
+
notify(event);
|
|
113
|
+
};
|
|
83
114
|
const base_emit = (ctx, input) => {
|
|
84
|
-
|
|
115
|
+
if (input.outcome === 'failure') {
|
|
116
|
+
// an attempt happened whether or not the transaction commits
|
|
117
|
+
ctx.pending_effects.push(emit_pool(input));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// A success row is written now and announced after the commit — and
|
|
121
|
+
// not at all on rollback, when the queue is discarded. See the module
|
|
122
|
+
// doc, "When listeners hear about a row".
|
|
123
|
+
const written = write(input);
|
|
124
|
+
ctx.pending_effects.push(written.then(() => undefined));
|
|
125
|
+
emit_after_commit(ctx, async () => {
|
|
126
|
+
const event = await written;
|
|
127
|
+
if (event)
|
|
128
|
+
notify(event);
|
|
129
|
+
});
|
|
85
130
|
};
|
|
86
131
|
// The decorated `emit` is what `emit_role_grant_target` captures below
|
|
87
132
|
// and what gets exposed on the returned object — both call shapes
|
|
@@ -111,8 +156,7 @@ export const create_audit_emitter = (options) => {
|
|
|
111
156
|
// `emit_role_grant_target` calls the closed-over inner `emit`, not
|
|
112
157
|
// `this.emit`, so the patch silently bypassed role-grant-shape emits.
|
|
113
158
|
// Tests that need instrumentation pass `emit_decorator` so the wrap
|
|
114
|
-
// is captured by the closure before the freeze
|
|
115
|
-
// `create_emit_ordering_audit_factory`). The listener list stays
|
|
159
|
+
// is captured by the closure before the freeze. The listener list stays
|
|
116
160
|
// closure-private; registration is append-only via `add_listener`
|
|
117
161
|
// (`create_app_server` registers its SSE + WS listeners post-assembly,
|
|
118
162
|
// by design).
|
|
@@ -32,13 +32,13 @@ export const create_audit_log_route_shape = (required_role = DEFAULT_AUDIT_STREA
|
|
|
32
32
|
account: 'required',
|
|
33
33
|
actor: 'required',
|
|
34
34
|
roles: [required_role],
|
|
35
|
-
// The stream is a session channel
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// `
|
|
41
|
-
//
|
|
35
|
+
// The stream is a session channel: a long-lived, instance-wide admin
|
|
36
|
+
// feed is not handed to a credential channel the deployment never
|
|
37
|
+
// chose. Widening the gate loses no close — the handler registers a
|
|
38
|
+
// bearer's stream under its API token id, and a token revocation closes
|
|
39
|
+
// by that id (`create_sse_connection_closer`, and the `token` scope in
|
|
40
|
+
// `create_sse_auth_guard`) — so widening is a consumer's explicit
|
|
41
|
+
// choice about the channel, not a hole in revocation.
|
|
42
42
|
credential_types: ['session'],
|
|
43
43
|
// Rule 3 — a narrowed token holds no audit feed. Same reasoning as the
|
|
44
44
|
// WS upgrade: this is a long-lived server→client stream whose contents
|
|
@@ -13,19 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
15
15
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
16
|
-
import { type
|
|
17
|
-
import type {
|
|
16
|
+
import { type SseNotification } from '../realtime/sse.js';
|
|
17
|
+
import type { SubscriberRegistry } from '../realtime/subscriber_registry.js';
|
|
18
18
|
/** Options for audit log route specs. */
|
|
19
19
|
export interface AuditLogRouteOptions {
|
|
20
20
|
/** Role required to access audit routes. Default `'admin'`. */
|
|
21
21
|
required_role?: string;
|
|
22
22
|
/**
|
|
23
|
-
* When provided, includes an SSE route at `/audit/stream` for realtime audit
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
23
|
+
* When provided, includes an SSE route at `/audit/stream` for realtime audit
|
|
24
|
+
* events. The route registers each stream on `registry` under the
|
|
25
|
+
* subscriber's session hash (`scope`) and account id (`groups`, with the
|
|
26
|
+
* API token id beside it for a bearer) — the keys `close_by_identity()`
|
|
27
|
+
* closes on for auth revocation. An `AuditLogSse`
|
|
28
|
+
* (`create_audit_log_sse`, or `AppServerContext.audit_sse`) satisfies it.
|
|
26
29
|
*/
|
|
27
30
|
stream?: {
|
|
28
|
-
|
|
31
|
+
registry: SubscriberRegistry<SseNotification>;
|
|
29
32
|
log: Logger;
|
|
30
33
|
};
|
|
31
34
|
}
|
|
@@ -35,6 +38,28 @@ export interface AuditLogRouteOptions {
|
|
|
35
38
|
* Returns an empty array when `options.stream` is not set — no REST routes
|
|
36
39
|
* live here apart from the stream.
|
|
37
40
|
*
|
|
41
|
+
* ## Admission
|
|
42
|
+
*
|
|
43
|
+
* A stream is authorized once, so the handler must not open one on a
|
|
44
|
+
* credential or role revoked while the request was in flight. The route's
|
|
45
|
+
* gates (401, credential channel, token scope, acting actor, role) run in the
|
|
46
|
+
* route-spec pipeline, before the handler and so before any stream is
|
|
47
|
+
* registered — a revocation landing after them would close nothing. So the
|
|
48
|
+
* handler registers the stream **pending**
|
|
49
|
+
* (`SubscriberRegistry.subscribe_pending`), then re-reads the credential
|
|
50
|
+
* (`revalidate_resolved_auth`) and the acting actor's role grants, and only
|
|
51
|
+
* then admits (`SubscriberRegistry.admit`). A revocation that committed before
|
|
52
|
+
* those reads is seen by them — `401 authentication_required` for a dead
|
|
53
|
+
* credential, `403 insufficient_permissions` for a lost role — and one whose
|
|
54
|
+
* close ran after the registration found the pending entry: admission then
|
|
55
|
+
* fails, and the response is a `200` whose body is the connect comment and
|
|
56
|
+
* nothing else — to an `EventSource`, a stream closed an instant after it
|
|
57
|
+
* opened (the client's reconnect is answered by the gates, with the precise
|
|
58
|
+
* status). A pending registration receives no audit row, counts toward no cap,
|
|
59
|
+
* and is removed when a re-read refuses the request. The twin of the Rust
|
|
60
|
+
* spine's `audit_stream_router`, which registers before its role read rather
|
|
61
|
+
* than repeating it.
|
|
62
|
+
*
|
|
38
63
|
* @param options - optional stream wiring + role override
|
|
39
64
|
* @returns the SSE route spec (when `options.stream` is provided) or an empty array
|
|
40
65
|
*/
|
|
@@ -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;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CAgF5F,CAAC"}
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS } from '../http/error_schemas.js';
|
|
15
|
+
import { AUTH_API_TOKEN_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
16
|
+
import { create_audit_log_route_shape, DEFAULT_AUDIT_STREAM_ROLE } from './audit_log_route_schema.js';
|
|
15
17
|
import { create_sse_response } from '../realtime/sse.js';
|
|
16
|
-
import { AUTH_SESSION_TOKEN_HASH_KEY, require_request_context } from './request_context.js';
|
|
18
|
+
import { AUTH_SESSION_TOKEN_HASH_KEY, has_scoped_role, refresh_role_grants, require_request_context } from './request_context.js';
|
|
19
|
+
import { get_resolved_auth, revalidate_resolved_auth } from './resolved_auth.js';
|
|
17
20
|
import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
|
|
18
21
|
/**
|
|
19
22
|
* Create the optional audit-log SSE route spec.
|
|
@@ -21,17 +24,40 @@ import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
|
|
|
21
24
|
* Returns an empty array when `options.stream` is not set — no REST routes
|
|
22
25
|
* live here apart from the stream.
|
|
23
26
|
*
|
|
27
|
+
* ## Admission
|
|
28
|
+
*
|
|
29
|
+
* A stream is authorized once, so the handler must not open one on a
|
|
30
|
+
* credential or role revoked while the request was in flight. The route's
|
|
31
|
+
* gates (401, credential channel, token scope, acting actor, role) run in the
|
|
32
|
+
* route-spec pipeline, before the handler and so before any stream is
|
|
33
|
+
* registered — a revocation landing after them would close nothing. So the
|
|
34
|
+
* handler registers the stream **pending**
|
|
35
|
+
* (`SubscriberRegistry.subscribe_pending`), then re-reads the credential
|
|
36
|
+
* (`revalidate_resolved_auth`) and the acting actor's role grants, and only
|
|
37
|
+
* then admits (`SubscriberRegistry.admit`). A revocation that committed before
|
|
38
|
+
* those reads is seen by them — `401 authentication_required` for a dead
|
|
39
|
+
* credential, `403 insufficient_permissions` for a lost role — and one whose
|
|
40
|
+
* close ran after the registration found the pending entry: admission then
|
|
41
|
+
* fails, and the response is a `200` whose body is the connect comment and
|
|
42
|
+
* nothing else — to an `EventSource`, a stream closed an instant after it
|
|
43
|
+
* opened (the client's reconnect is answered by the gates, with the precise
|
|
44
|
+
* status). A pending registration receives no audit row, counts toward no cap,
|
|
45
|
+
* and is removed when a re-read refuses the request. The twin of the Rust
|
|
46
|
+
* spine's `audit_stream_router`, which registers before its role read rather
|
|
47
|
+
* than repeating it.
|
|
48
|
+
*
|
|
24
49
|
* @param options - optional stream wiring + role override
|
|
25
50
|
* @returns the SSE route spec (when `options.stream` is provided) or an empty array
|
|
26
51
|
*/
|
|
27
52
|
export const create_audit_log_route_specs = (options) => {
|
|
28
53
|
if (!options?.stream)
|
|
29
54
|
return [];
|
|
30
|
-
const {
|
|
55
|
+
const { registry, log } = options.stream;
|
|
56
|
+
const required_role = options.required_role ?? DEFAULT_AUDIT_STREAM_ROLE;
|
|
31
57
|
return [
|
|
32
58
|
{
|
|
33
|
-
...create_audit_log_route_shape(
|
|
34
|
-
handler: (c) => {
|
|
59
|
+
...create_audit_log_route_shape(required_role),
|
|
60
|
+
handler: async (c, route) => {
|
|
35
61
|
// Rule 3 is declared on the route shape's `auth.required_scope`,
|
|
36
62
|
// so a narrowed token is refused ahead of the role gate rather
|
|
37
63
|
// than here.
|
|
@@ -39,16 +65,64 @@ export const create_audit_log_route_specs = (options) => {
|
|
|
39
65
|
// scope = session hash (capped → tabs-per-session limit and
|
|
40
66
|
// session-specific `session_revoke` close). groups = [account_id]
|
|
41
67
|
// (uncapped → coarse close on role_grant_revoke / session_revoke_all
|
|
42
|
-
// / password_change).
|
|
68
|
+
// / password_change / account delete). The route shape admits
|
|
69
|
+
// sessions only, so `api_token_id` is null here; it is registered
|
|
70
|
+
// when present so that a consumer widening the credential gate
|
|
71
|
+
// still has a revoked token's stream closed by `token_revoke`.
|
|
43
72
|
const token_hash = c.get(AUTH_SESSION_TOKEN_HASH_KEY) ?? null;
|
|
44
|
-
const
|
|
45
|
-
|
|
73
|
+
const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY) ?? null;
|
|
74
|
+
// Register pending first: from here every revocation finds the
|
|
75
|
+
// stream (TSDoc, "Admission").
|
|
76
|
+
const pending = registry.subscribe_pending({
|
|
46
77
|
channels: [AUDIT_LOG_CHANNEL],
|
|
47
78
|
scope: token_hash ?? undefined,
|
|
48
|
-
groups: [ctx.account.id]
|
|
79
|
+
groups: api_token_id === null ? [ctx.account.id] : [ctx.account.id, api_token_id]
|
|
49
80
|
});
|
|
50
|
-
|
|
51
|
-
|
|
81
|
+
// The pending registration is released on every path that does not
|
|
82
|
+
// admit it — a refusal, a failed re-read, a throw while building the
|
|
83
|
+
// response — so none of them can leak an entry nothing delivers to.
|
|
84
|
+
let admitted = false;
|
|
85
|
+
try {
|
|
86
|
+
// Re-read what the gates read, now that the stream is registered:
|
|
87
|
+
// a revocation that closed before the registration is committed,
|
|
88
|
+
// so these see it. A throw here fails closed — no stream opens
|
|
89
|
+
// unchecked. Skipped under the test-preset escape hatch, whose
|
|
90
|
+
// pre-baked context has no rows behind it to re-read.
|
|
91
|
+
if (!c.get(TEST_CONTEXT_PRESET_KEY)) {
|
|
92
|
+
const resolved = get_resolved_auth(c);
|
|
93
|
+
const credential_is_live = resolved !== null && (await revalidate_resolved_auth(route, resolved));
|
|
94
|
+
if (!credential_is_live) {
|
|
95
|
+
log.info('audit stream: credential revoked during the request', ctx.account.id);
|
|
96
|
+
return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
|
|
97
|
+
}
|
|
98
|
+
// Global grants only, like the `require_role` gate that ran in
|
|
99
|
+
// the pipeline — a scoped grant must not open the instance-wide
|
|
100
|
+
// stream.
|
|
101
|
+
if (!has_scoped_role(await refresh_role_grants(ctx, route), required_role, null)) {
|
|
102
|
+
log.info('audit stream: role revoked during the request', ctx.account.id);
|
|
103
|
+
return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: [required_role] }, 403);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Admit — the cap's eviction happens here, after every gate. A
|
|
107
|
+
// registration a revocation closed while it was pending is not
|
|
108
|
+
// admitted: the stream is closed at once, so the body is the
|
|
109
|
+
// connect comment and nothing else, and the client reconnects
|
|
110
|
+
// into the gates.
|
|
111
|
+
const { response, stream } = create_sse_response(c, log);
|
|
112
|
+
admitted = registry.admit(pending, stream);
|
|
113
|
+
if (admitted) {
|
|
114
|
+
stream.on_close(pending.unsubscribe);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.info('audit stream: closed by a revocation before admission', ctx.account.id);
|
|
118
|
+
stream.close();
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (!admitted)
|
|
124
|
+
pending.unsubscribe();
|
|
125
|
+
}
|
|
52
126
|
}
|
|
53
127
|
}
|
|
54
128
|
];
|
|
@@ -49,6 +49,54 @@ export declare const AuditEventType: z.ZodEnum<{
|
|
|
49
49
|
db_admin_row_delete: "db_admin_row_delete";
|
|
50
50
|
}>;
|
|
51
51
|
export type AuditEventType = z.infer<typeof AuditEventType>;
|
|
52
|
+
/**
|
|
53
|
+
* What a **successful** audit row invalidates — the axis both revocation
|
|
54
|
+
* listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) dispatch on.
|
|
55
|
+
*
|
|
56
|
+
* - `none` — the row invalidates no live connection.
|
|
57
|
+
* - `session` — one session: the connections whose session-token hash equals
|
|
58
|
+
* the row's `metadata.session_id`.
|
|
59
|
+
* - `token` — one API token: the connections whose `api_token.id` equals the
|
|
60
|
+
* row's `metadata.token_id`.
|
|
61
|
+
* - `account` — every connection of `target_account_id ?? account_id`.
|
|
62
|
+
* - `role` — the target account's connections gated on the row's
|
|
63
|
+
* `metadata.role`.
|
|
64
|
+
*
|
|
65
|
+
* The scope says what the row invalidated, not what a transport must do about
|
|
66
|
+
* it. The two listeners differ on `role` on purpose: a WebSocket message is
|
|
67
|
+
* re-authorized per dispatch, so the socket stays open and its next message
|
|
68
|
+
* is refused, while a stream is authorized once at open and must be closed.
|
|
69
|
+
*
|
|
70
|
+
* Twin of the Rust spine's `RevocationScope`.
|
|
71
|
+
*/
|
|
72
|
+
export type RevocationScope = 'none' | 'session' | 'token' | 'account' | 'role';
|
|
73
|
+
/**
|
|
74
|
+
* The `RevocationScope` of every builtin audit event — declared once, so the
|
|
75
|
+
* WebSocket and SSE revocation listeners read one answer instead of keeping a
|
|
76
|
+
* list each. Typed over `AuditEventType`, so a new event type does not compile
|
|
77
|
+
* until it declares its scope here.
|
|
78
|
+
*
|
|
79
|
+
* A scope other than `none` marks an event whose success invalidates a live
|
|
80
|
+
* connection's credential or access: `logout`, `password_change`,
|
|
81
|
+
* `session_revoke_all`, `token_revoke_all`, `account_delete`, and
|
|
82
|
+
* `account_purge` are account-wide, `session_revoke` and `token_revoke` name
|
|
83
|
+
* the one credential they ended, and `role_grant_revoke` names the lost role.
|
|
84
|
+
* Granting access revokes nothing, and neither does an event about a subject
|
|
85
|
+
* other than a connection's identity.
|
|
86
|
+
*
|
|
87
|
+
* Twin of the `revocation_scope` column of the Rust spine's
|
|
88
|
+
* `AUDIT_EVENT_SPECS`.
|
|
89
|
+
*/
|
|
90
|
+
export declare const audit_event_revocation_scopes: Readonly<Record<AuditEventType, RevocationScope>>;
|
|
91
|
+
/**
|
|
92
|
+
* The `RevocationScope` of an `audit_log.event_type` string.
|
|
93
|
+
*
|
|
94
|
+
* An event type with no builtin entry — a consumer's own event — resolves to
|
|
95
|
+
* `none`: an event fuz_app has never heard of must not close connections. A
|
|
96
|
+
* consumer whose own event revokes closes the connections from its handler,
|
|
97
|
+
* with `queue_connection_close`.
|
|
98
|
+
*/
|
|
99
|
+
export declare const to_revocation_scope: (event_type: string) => RevocationScope;
|
|
52
100
|
/**
|
|
53
101
|
* Letter start, then letters, digits, `_`, `.`, `/`, `-`. Accepts snake_case,
|
|
54
102
|
* dotted, and namespaced consumer conventions; rejects empty strings, leading
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,6 +62,64 @@ export const AUDIT_EVENT_TYPES = Object.freeze([
|
|
|
62
62
|
]);
|
|
63
63
|
/** Zod schema for audit event types. */
|
|
64
64
|
export const AuditEventType = z.enum(AUDIT_EVENT_TYPES);
|
|
65
|
+
/**
|
|
66
|
+
* The `RevocationScope` of every builtin audit event — declared once, so the
|
|
67
|
+
* WebSocket and SSE revocation listeners read one answer instead of keeping a
|
|
68
|
+
* list each. Typed over `AuditEventType`, so a new event type does not compile
|
|
69
|
+
* until it declares its scope here.
|
|
70
|
+
*
|
|
71
|
+
* A scope other than `none` marks an event whose success invalidates a live
|
|
72
|
+
* connection's credential or access: `logout`, `password_change`,
|
|
73
|
+
* `session_revoke_all`, `token_revoke_all`, `account_delete`, and
|
|
74
|
+
* `account_purge` are account-wide, `session_revoke` and `token_revoke` name
|
|
75
|
+
* the one credential they ended, and `role_grant_revoke` names the lost role.
|
|
76
|
+
* Granting access revokes nothing, and neither does an event about a subject
|
|
77
|
+
* other than a connection's identity.
|
|
78
|
+
*
|
|
79
|
+
* Twin of the `revocation_scope` column of the Rust spine's
|
|
80
|
+
* `AUDIT_EVENT_SPECS`.
|
|
81
|
+
*/
|
|
82
|
+
export const audit_event_revocation_scopes = Object.freeze({
|
|
83
|
+
login: 'none',
|
|
84
|
+
logout: 'account',
|
|
85
|
+
bootstrap: 'none',
|
|
86
|
+
signup: 'none',
|
|
87
|
+
password_change: 'account',
|
|
88
|
+
session_revoke: 'session',
|
|
89
|
+
session_revoke_all: 'account',
|
|
90
|
+
token_create: 'none',
|
|
91
|
+
token_revoke: 'token',
|
|
92
|
+
token_revoke_all: 'account',
|
|
93
|
+
role_grant_create: 'none',
|
|
94
|
+
role_grant_revoke: 'role',
|
|
95
|
+
role_grant_offer_create: 'none',
|
|
96
|
+
role_grant_offer_accept: 'none',
|
|
97
|
+
role_grant_offer_decline: 'none',
|
|
98
|
+
role_grant_offer_retract: 'none',
|
|
99
|
+
role_grant_offer_expire: 'none',
|
|
100
|
+
role_grant_offer_supersede: 'none',
|
|
101
|
+
invite_create: 'none',
|
|
102
|
+
invite_delete: 'none',
|
|
103
|
+
account_delete: 'account',
|
|
104
|
+
account_purge: 'account',
|
|
105
|
+
account_undelete: 'none',
|
|
106
|
+
actor_delete: 'none',
|
|
107
|
+
actor_purge: 'none',
|
|
108
|
+
actor_undelete: 'none',
|
|
109
|
+
app_settings_update: 'none',
|
|
110
|
+
db_admin_row_delete: 'none'
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* The `RevocationScope` of an `audit_log.event_type` string.
|
|
114
|
+
*
|
|
115
|
+
* An event type with no builtin entry — a consumer's own event — resolves to
|
|
116
|
+
* `none`: an event fuz_app has never heard of must not close connections. A
|
|
117
|
+
* consumer whose own event revokes closes the connections from its handler,
|
|
118
|
+
* with `queue_connection_close`.
|
|
119
|
+
*/
|
|
120
|
+
export const to_revocation_scope = (event_type) => Object.hasOwn(audit_event_revocation_scopes, event_type)
|
|
121
|
+
? audit_event_revocation_scopes[event_type]
|
|
122
|
+
: 'none';
|
|
65
123
|
/**
|
|
66
124
|
* Letter start, then letters, digits, `_`, `.`, `/`, `-`. Accepts snake_case,
|
|
67
125
|
* dotted, and namespaced consumer conventions; rejects empty strings, leading
|
|
@@ -53,8 +53,9 @@ export interface BootstrapAccountDeps {
|
|
|
53
53
|
token_path: string;
|
|
54
54
|
/**
|
|
55
55
|
* Hardened secret-file read (see `FsSecureReadDeps.read_secure_file`) —
|
|
56
|
-
* the token mints the keeper account, so a symlinked,
|
|
57
|
-
* or oversized file must refuse rather than be
|
|
56
|
+
* the token mints the keeper account, so a symlinked, non-regular,
|
|
57
|
+
* group/other-readable, or oversized file must refuse rather than be
|
|
58
|
+
* honored. Throws on refusal;
|
|
58
59
|
* every throw reads as `TOKEN_FILE_MISSING` upstream.
|
|
59
60
|
*/
|
|
60
61
|
read_secure_file: (path: string) => Promise<Uint8Array>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB
|
|
1
|
+
{"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA2EhC,CAAC"}
|
|
@@ -36,9 +36,9 @@ import { query_create_role_grant } from './role_grant_queries.js';
|
|
|
36
36
|
export const bootstrap_account = async (deps, provided_token, input) => {
|
|
37
37
|
const { db, token_path, read_secure_file, delete_file, password, log } = deps;
|
|
38
38
|
// 1. Read and verify token (non-destructive, before transaction). The
|
|
39
|
-
// secure read fails loud on a symlink /
|
|
40
|
-
// every refusal masks as the same TOKEN_FILE_MISSING the
|
|
41
|
-
// carried, with the specific reason logged for the operator.
|
|
39
|
+
// secure read fails loud on a symlink / non-regular file / permissive mode /
|
|
40
|
+
// oversized file — every refusal masks as the same TOKEN_FILE_MISSING the
|
|
41
|
+
// wire always carried, with the specific reason logged for the operator.
|
|
42
42
|
let expected_token;
|
|
43
43
|
try {
|
|
44
44
|
expected_token = new TextDecoder().decode(await read_secure_file(token_path)).trim();
|