@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* A raw-socket WebSocket client for tests that need to misbehave, or to
|
|
4
|
+
* control exactly which bytes reach the server when.
|
|
5
|
+
*
|
|
6
|
+
* A client library answers a close frame itself and sends nothing until its
|
|
7
|
+
* `open` event, so two server behaviors can't be reached through one: what
|
|
8
|
+
* the server does with a client that **withholds its close frame**, and what
|
|
9
|
+
* it does with frames that arrive **with the handshake**, before the
|
|
10
|
+
* connection can have been admitted. This speaks just enough RFC 6455 over
|
|
11
|
+
* `node:net` to do both — the upgrade request, masked client text frames, and
|
|
12
|
+
* unmasked server frames — and deliberately never replies to a close.
|
|
13
|
+
*
|
|
14
|
+
* Small frames only (payloads under 64 KiB, unfragmented), which is all the
|
|
15
|
+
* JSON-RPC traffic the suites send.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
import { connect } from 'node:net';
|
|
21
|
+
/** RFC 6455 opcode of a text frame. */
|
|
22
|
+
export const RAW_WS_OPCODE_TEXT = 0x1;
|
|
23
|
+
/** RFC 6455 opcode of a close frame. */
|
|
24
|
+
export const RAW_WS_OPCODE_CLOSE = 0x8;
|
|
25
|
+
/** Whether a frame is a close frame. */
|
|
26
|
+
export const is_raw_ws_close = (frame) => frame.opcode === RAW_WS_OPCODE_CLOSE;
|
|
27
|
+
/** The close code a close frame carries, or `null` when it carries none. */
|
|
28
|
+
export const raw_ws_close_code = (frame) => frame.payload.length >= 2 ? frame.payload.readUInt16BE(0) : null;
|
|
29
|
+
/** Encode one masked client text frame. */
|
|
30
|
+
const encode_client_text = (text) => {
|
|
31
|
+
const payload = Buffer.from(text, 'utf-8');
|
|
32
|
+
if (payload.length >= 65_536)
|
|
33
|
+
throw new Error('raw ws: payload too large for this test client');
|
|
34
|
+
const first = 0x80 | RAW_WS_OPCODE_TEXT;
|
|
35
|
+
const header = payload.length < 126
|
|
36
|
+
? Buffer.from([first, 0x80 | payload.length])
|
|
37
|
+
: Buffer.from([first, 0x80 | 126, payload.length >> 8, payload.length & 0xff]);
|
|
38
|
+
const mask = randomBytes(4);
|
|
39
|
+
const masked = Buffer.from(payload);
|
|
40
|
+
for (let i = 0; i < masked.length; i++)
|
|
41
|
+
masked[i] ^= mask[i % 4];
|
|
42
|
+
return Buffer.concat([header, mask, masked]);
|
|
43
|
+
};
|
|
44
|
+
/** Split complete server frames off the front of `buffer` (server frames are unmasked). */
|
|
45
|
+
const take_frames = (buffer) => {
|
|
46
|
+
const frames = [];
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (;;) {
|
|
49
|
+
if (buffer.length - offset < 2)
|
|
50
|
+
break;
|
|
51
|
+
const opcode = buffer[offset] & 0x0f;
|
|
52
|
+
let length = buffer[offset + 1] & 0x7f;
|
|
53
|
+
let header = 2;
|
|
54
|
+
if (length === 126) {
|
|
55
|
+
if (buffer.length - offset < 4)
|
|
56
|
+
break;
|
|
57
|
+
length = buffer.readUInt16BE(offset + 2);
|
|
58
|
+
header = 4;
|
|
59
|
+
}
|
|
60
|
+
else if (length === 127) {
|
|
61
|
+
throw new Error('raw ws: 64-bit frame lengths are not supported by this test client');
|
|
62
|
+
}
|
|
63
|
+
if (buffer.length - offset < header + length)
|
|
64
|
+
break;
|
|
65
|
+
frames.push({ opcode, payload: buffer.subarray(offset + header, offset + header + length) });
|
|
66
|
+
offset += header + length;
|
|
67
|
+
}
|
|
68
|
+
return { frames, rest: buffer.subarray(offset) };
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Upgrade a raw TCP socket to a WebSocket.
|
|
72
|
+
*
|
|
73
|
+
* Resolves once the server answers `101`; rejects on any other status or a
|
|
74
|
+
* connection error.
|
|
75
|
+
*
|
|
76
|
+
* @throws Error when the upgrade is refused or the connection fails
|
|
77
|
+
*/
|
|
78
|
+
export const connect_raw_ws = (options) => new Promise((resolve, reject) => {
|
|
79
|
+
const { port, hostname = '127.0.0.1', path, headers = {}, pipelined = [] } = options;
|
|
80
|
+
const socket = connect(port, hostname);
|
|
81
|
+
const frames = [];
|
|
82
|
+
const waiters = [];
|
|
83
|
+
let buffer = Buffer.alloc(0);
|
|
84
|
+
let upgraded = false;
|
|
85
|
+
const client = {
|
|
86
|
+
frames,
|
|
87
|
+
send_text: (text) => {
|
|
88
|
+
socket.write(encode_client_text(text));
|
|
89
|
+
},
|
|
90
|
+
wait_for: (predicate, timeout_ms = 2000) => {
|
|
91
|
+
const found = frames.find(predicate);
|
|
92
|
+
if (found)
|
|
93
|
+
return Promise.resolve(found);
|
|
94
|
+
return new Promise((resolve_frame, reject_frame) => {
|
|
95
|
+
const waiter = {
|
|
96
|
+
predicate,
|
|
97
|
+
resolve: (frame) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
resolve_frame(frame);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
waiters.splice(waiters.indexOf(waiter), 1);
|
|
104
|
+
reject_frame(new Error(`raw ws: no matching frame within ${timeout_ms}ms`));
|
|
105
|
+
}, timeout_ms);
|
|
106
|
+
waiters.push(waiter);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
destroy: () => socket.destroy()
|
|
110
|
+
};
|
|
111
|
+
socket.on('error', reject);
|
|
112
|
+
socket.on('connect', () => {
|
|
113
|
+
const request = [
|
|
114
|
+
`GET ${path} HTTP/1.1`,
|
|
115
|
+
`Host: ${hostname}:${port}`,
|
|
116
|
+
'Upgrade: websocket',
|
|
117
|
+
'Connection: Upgrade',
|
|
118
|
+
`Sec-WebSocket-Key: ${randomBytes(16).toString('base64')}`,
|
|
119
|
+
'Sec-WebSocket-Version: 13',
|
|
120
|
+
...Object.entries(headers).map(([name, value]) => `${name}: ${value}`),
|
|
121
|
+
'',
|
|
122
|
+
''
|
|
123
|
+
].join('\r\n');
|
|
124
|
+
// one write, so the pipelined frames can't trail the handshake
|
|
125
|
+
socket.write(Buffer.concat([Buffer.from(request, 'utf-8'), ...pipelined.map(encode_client_text)]));
|
|
126
|
+
});
|
|
127
|
+
socket.on('data', (chunk) => {
|
|
128
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
129
|
+
if (!upgraded) {
|
|
130
|
+
const end = buffer.indexOf('\r\n\r\n');
|
|
131
|
+
if (end === -1)
|
|
132
|
+
return;
|
|
133
|
+
const status_line = buffer.subarray(0, end).toString('utf-8').split('\r\n')[0];
|
|
134
|
+
if (!/^HTTP\/1\.1 101\b/u.test(status_line)) {
|
|
135
|
+
reject(new Error(`raw ws: upgrade refused: ${status_line}`));
|
|
136
|
+
socket.destroy();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
upgraded = true;
|
|
140
|
+
buffer = buffer.subarray(end + 4);
|
|
141
|
+
resolve(client);
|
|
142
|
+
}
|
|
143
|
+
const taken = take_frames(buffer);
|
|
144
|
+
buffer = taken.rest;
|
|
145
|
+
for (const frame of taken.frames) {
|
|
146
|
+
frames.push(frame);
|
|
147
|
+
// Deliberately no reply to a close frame — this client withholds it.
|
|
148
|
+
for (const waiter of waiters.slice()) {
|
|
149
|
+
if (waiter.predicate(frame)) {
|
|
150
|
+
waiters.splice(waiters.indexOf(waiter), 1);
|
|
151
|
+
waiter.resolve(frame);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -44,9 +44,37 @@ export interface WsTransportOptions {
|
|
|
44
44
|
* `messages` array; `wait_for` checks already-received messages first
|
|
45
45
|
* before waiting for new arrivals.
|
|
46
46
|
*
|
|
47
|
+
* An open socket is not yet an **admitted** connection: both spines answer
|
|
48
|
+
* the handshake first and admit the connection a credential re-check later,
|
|
49
|
+
* and deliver no server-initiated message to it until then. Requests sent
|
|
50
|
+
* meanwhile are queued and answered, so a suite that only round-trips can
|
|
51
|
+
* use this directly; one that waits for a push (a broadcast, a targeted
|
|
52
|
+
* notification) opens with `create_admitted_ws_transport` instead.
|
|
53
|
+
*
|
|
47
54
|
* @throws Error if the upgrade fails (status, network) — the rejection
|
|
48
55
|
* message carries the underlying error so the test surfaces the real
|
|
49
56
|
* cause rather than hanging.
|
|
50
57
|
*/
|
|
51
58
|
export declare const create_ws_transport: (options: WsTransportOptions) => Promise<WsClient>;
|
|
59
|
+
/** The request id `create_admitted_ws_transport` round-trips `heartbeat` under. */
|
|
60
|
+
export declare const WS_ADMISSION_PROBE_ID = "admitted";
|
|
61
|
+
/**
|
|
62
|
+
* Open a real-upgrade WS client and round-trip `heartbeat` on it, so the
|
|
63
|
+
* backend has **admitted** the connection by the time this resolves.
|
|
64
|
+
*
|
|
65
|
+
* Both spines answer the upgrade handshake before the connection is
|
|
66
|
+
* registered and admitted (the credential is re-read in between), and deliver
|
|
67
|
+
* no broadcast or targeted notification to a connection still pending
|
|
68
|
+
* admission. A request is answered only once the connection is admitted, so
|
|
69
|
+
* the round trip is the one signal a client has. Suites that open a socket and
|
|
70
|
+
* then wait for a server-initiated message, or that need connections
|
|
71
|
+
* registered in a known order, open through this.
|
|
72
|
+
*
|
|
73
|
+
* `heartbeat` is a protocol action, present on every WS endpoint. Its response
|
|
74
|
+
* frame (id `WS_ADMISSION_PROBE_ID`) stays in the client's `messages`.
|
|
75
|
+
*
|
|
76
|
+
* @throws Error if the upgrade fails, or the heartbeat is refused or times out
|
|
77
|
+
* — the socket is closed first
|
|
78
|
+
*/
|
|
79
|
+
export declare const create_admitted_ws_transport: (options: WsTransportOptions) => Promise<WsClient>;
|
|
52
80
|
//# sourceMappingURL=ws_transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"ws_transport.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/ws_transport.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,OAAO,EAMN,KAAK,QAAQ,EACb,KAAK,kBAAkB,EAEvB,MAAM,gBAAgB,CAAC;AAIxB,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,GAAU,SAAS,kBAAkB,KAAG,OAAO,CAAC,QAAQ,CAwJvF,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,kBAAkB,KACzB,OAAO,CAAC,QAAQ,CASlB,CAAC"}
|
|
@@ -20,6 +20,7 @@ import '../assert_dev_env.js';
|
|
|
20
20
|
import { WebSocket } from 'ws';
|
|
21
21
|
import { WS_CLIENT_DEFAULT_TIMEOUT_MS, deliver_inbound, is_response_for } from './ws_client.js';
|
|
22
22
|
import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
23
|
+
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
23
24
|
/**
|
|
24
25
|
* Build a real-upgrade WS client pinned to `options.base_url` + `ws_path`.
|
|
25
26
|
*
|
|
@@ -29,6 +30,13 @@ import { create_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
|
29
30
|
* `messages` array; `wait_for` checks already-received messages first
|
|
30
31
|
* before waiting for new arrivals.
|
|
31
32
|
*
|
|
33
|
+
* An open socket is not yet an **admitted** connection: both spines answer
|
|
34
|
+
* the handshake first and admit the connection a credential re-check later,
|
|
35
|
+
* and deliver no server-initiated message to it until then. Requests sent
|
|
36
|
+
* meanwhile are queued and answered, so a suite that only round-trips can
|
|
37
|
+
* use this directly; one that waits for a push (a broadcast, a targeted
|
|
38
|
+
* notification) opens with `create_admitted_ws_transport` instead.
|
|
39
|
+
*
|
|
32
40
|
* @throws Error if the upgrade fails (status, network) — the rejection
|
|
33
41
|
* message carries the underlying error so the test surfaces the real
|
|
34
42
|
* cause rather than hanging.
|
|
@@ -70,8 +78,10 @@ export const create_ws_transport = async (options) => {
|
|
|
70
78
|
});
|
|
71
79
|
});
|
|
72
80
|
let close_code = null;
|
|
73
|
-
|
|
81
|
+
let close_reason = null;
|
|
82
|
+
socket.on('close', (code, reason) => {
|
|
74
83
|
close_code = code;
|
|
84
|
+
close_reason = reason.toString('utf-8');
|
|
75
85
|
for (const resolve of close_resolvers)
|
|
76
86
|
resolve();
|
|
77
87
|
close_resolvers = [];
|
|
@@ -145,6 +155,9 @@ export const create_ws_transport = async (options) => {
|
|
|
145
155
|
get close_code() {
|
|
146
156
|
return close_code;
|
|
147
157
|
},
|
|
158
|
+
get close_reason() {
|
|
159
|
+
return close_reason;
|
|
160
|
+
},
|
|
148
161
|
send: send_impl,
|
|
149
162
|
async request(id, method, params, timeout_ms) {
|
|
150
163
|
await send_impl(create_jsonrpc_request(method, params, id));
|
|
@@ -168,3 +181,34 @@ export const create_ws_transport = async (options) => {
|
|
|
168
181
|
wait_for_close
|
|
169
182
|
};
|
|
170
183
|
};
|
|
184
|
+
/** The request id `create_admitted_ws_transport` round-trips `heartbeat` under. */
|
|
185
|
+
export const WS_ADMISSION_PROBE_ID = 'admitted';
|
|
186
|
+
/**
|
|
187
|
+
* Open a real-upgrade WS client and round-trip `heartbeat` on it, so the
|
|
188
|
+
* backend has **admitted** the connection by the time this resolves.
|
|
189
|
+
*
|
|
190
|
+
* Both spines answer the upgrade handshake before the connection is
|
|
191
|
+
* registered and admitted (the credential is re-read in between), and deliver
|
|
192
|
+
* no broadcast or targeted notification to a connection still pending
|
|
193
|
+
* admission. A request is answered only once the connection is admitted, so
|
|
194
|
+
* the round trip is the one signal a client has. Suites that open a socket and
|
|
195
|
+
* then wait for a server-initiated message, or that need connections
|
|
196
|
+
* registered in a known order, open through this.
|
|
197
|
+
*
|
|
198
|
+
* `heartbeat` is a protocol action, present on every WS endpoint. Its response
|
|
199
|
+
* frame (id `WS_ADMISSION_PROBE_ID`) stays in the client's `messages`.
|
|
200
|
+
*
|
|
201
|
+
* @throws Error if the upgrade fails, or the heartbeat is refused or times out
|
|
202
|
+
* — the socket is closed first
|
|
203
|
+
*/
|
|
204
|
+
export const create_admitted_ws_transport = async (options) => {
|
|
205
|
+
const client = await create_ws_transport(options);
|
|
206
|
+
try {
|
|
207
|
+
await client.request(WS_ADMISSION_PROBE_ID, heartbeat_action_spec.method, {});
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
await client.close().catch(() => { });
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return client;
|
|
214
|
+
};
|
|
@@ -82,8 +82,9 @@ export interface FakeHonoContextOptions {
|
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Build a fake Hono `Context` exposing the auth keys the dispatcher
|
|
85
|
-
* reads via `c.get(...)
|
|
86
|
-
*
|
|
85
|
+
* reads via `c.get(...)` — the ones the production auth middleware sets,
|
|
86
|
+
* including the session token hash a session connection is registered under.
|
|
87
|
+
* Only `.get()` is populated — no other Hono context surface is simulated.
|
|
87
88
|
*/
|
|
88
89
|
export declare const create_fake_hono_context: (opts: FakeHonoContextOptions) => Context;
|
|
89
90
|
/** The return of `create_stub_upgrade` — fake `upgradeWebSocket` + factory capture. */
|
|
@@ -121,7 +122,11 @@ export interface WsConnectIdentity {
|
|
|
121
122
|
account_id?: Uuid;
|
|
122
123
|
/** Credential type. Defaults to `'session'`. Keeper actions require `'daemon_token'`. */
|
|
123
124
|
credential_type?: CredentialType;
|
|
124
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Session id (any string). Defaults to a fresh uuid. The connection is
|
|
127
|
+
* registered under `hash_session_token(session_id)`, the hash the request
|
|
128
|
+
* context middleware would have resolved.
|
|
129
|
+
*/
|
|
125
130
|
session_id?: string;
|
|
126
131
|
/** Api token id; set for bearer connections, null otherwise. */
|
|
127
132
|
api_token_id?: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,MAAM,CAAC;AAC1C,OAAO,EACN,SAAS,EAET,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAE/E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAEpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,
|
|
1
|
+
{"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/ws_round_trip.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,8 @@ import { create_broadcast_api } from '../actions/broadcast_api.js';
|
|
|
7
7
|
import { register_action_ws } from '../actions/register_action_ws.js';
|
|
8
8
|
import { create_stub_db } from './stubs.js';
|
|
9
9
|
import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
|
|
10
|
-
import { REQUEST_CONTEXT_KEY } from '../auth/request_context.js';
|
|
10
|
+
import { AUTH_SESSION_TOKEN_HASH_KEY, REQUEST_CONTEXT_KEY } from '../auth/request_context.js';
|
|
11
|
+
import { hash_session_token } from '../auth/session_queries.js';
|
|
11
12
|
import { ROLE_KEEPER } from '../auth/role_schema.js';
|
|
12
13
|
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
13
14
|
import { create_jsonrpc_request } from '../http/jsonrpc_helpers.js';
|
|
@@ -34,16 +35,21 @@ export const create_fake_ws = () => {
|
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
36
37
|
* Build a fake Hono `Context` exposing the auth keys the dispatcher
|
|
37
|
-
* reads via `c.get(...)
|
|
38
|
-
*
|
|
38
|
+
* reads via `c.get(...)` — the ones the production auth middleware sets,
|
|
39
|
+
* including the session token hash a session connection is registered under.
|
|
40
|
+
* Only `.get()` is populated — no other Hono context surface is simulated.
|
|
39
41
|
*/
|
|
40
42
|
export const create_fake_hono_context = (opts) => {
|
|
41
43
|
const request_context = opts.request_context ?? build_simple_request_context(opts.role);
|
|
44
|
+
const auth_session_id = opts.auth_session_id ?? (opts.credential_type === 'session' ? 's1' : null);
|
|
42
45
|
const vars = {
|
|
43
46
|
[ACCOUNT_ID_KEY]: request_context.account.id,
|
|
44
47
|
[REQUEST_CONTEXT_KEY]: request_context,
|
|
45
48
|
[CREDENTIAL_TYPE_KEY]: opts.credential_type,
|
|
46
|
-
auth_session_id
|
|
49
|
+
auth_session_id,
|
|
50
|
+
// what `create_request_context_middleware` sets beside the account id —
|
|
51
|
+
// the key the dispatcher registers a session connection under
|
|
52
|
+
[AUTH_SESSION_TOKEN_HASH_KEY]: auth_session_id === null ? null : hash_session_token(auth_session_id),
|
|
47
53
|
[AUTH_API_TOKEN_ID_KEY]: opts.api_token_id ?? null,
|
|
48
54
|
[TEST_CONTEXT_PRESET_KEY]: true
|
|
49
55
|
};
|
|
@@ -186,6 +192,9 @@ export const create_ws_test_harness = (options) => {
|
|
|
186
192
|
actions,
|
|
187
193
|
db: stub_db,
|
|
188
194
|
transport,
|
|
195
|
+
// the harness drives the dispatcher against a stub db — no revocation
|
|
196
|
+
// handler runs here, so there is no closer to reach this transport
|
|
197
|
+
connection_closer: null,
|
|
189
198
|
heartbeat,
|
|
190
199
|
log,
|
|
191
200
|
on_socket_open,
|
|
@@ -203,6 +212,7 @@ export const create_ws_test_harness = (options) => {
|
|
|
203
212
|
[REQUEST_CONTEXT_KEY, build_multi_role_request_context(account_id, roles)],
|
|
204
213
|
[CREDENTIAL_TYPE_KEY, credential_type],
|
|
205
214
|
['auth_session_id', session_id],
|
|
215
|
+
[AUTH_SESSION_TOKEN_HASH_KEY, hash_session_token(session_id)],
|
|
206
216
|
[AUTH_API_TOKEN_ID_KEY, api_token_id],
|
|
207
217
|
[TEST_CONTEXT_PRESET_KEY, true]
|
|
208
218
|
]);
|
|
@@ -224,6 +234,7 @@ export const create_ws_test_harness = (options) => {
|
|
|
224
234
|
const close_waiters = [];
|
|
225
235
|
let is_closed = false;
|
|
226
236
|
let close_code = null;
|
|
237
|
+
let close_reason = null;
|
|
227
238
|
// Captured in `ws.close` below; `client.close(...)` returns it so
|
|
228
239
|
// tests can await async `on_socket_close` cleanup.
|
|
229
240
|
let close_pending = null;
|
|
@@ -242,6 +253,7 @@ export const create_ws_test_harness = (options) => {
|
|
|
242
253
|
return;
|
|
243
254
|
is_closed = true;
|
|
244
255
|
close_code = code ?? 1000;
|
|
256
|
+
close_reason = reason ?? '';
|
|
245
257
|
for (const resolve of close_waiters.splice(0))
|
|
246
258
|
resolve();
|
|
247
259
|
const close_event = new Event('close');
|
|
@@ -331,6 +343,9 @@ export const create_ws_test_harness = (options) => {
|
|
|
331
343
|
get close_code() {
|
|
332
344
|
return close_code;
|
|
333
345
|
},
|
|
346
|
+
get close_reason() {
|
|
347
|
+
return close_reason;
|
|
348
|
+
},
|
|
334
349
|
wait_for: wait_for_impl,
|
|
335
350
|
wait_for_close: (timeout_ms = WS_CLIENT_DEFAULT_TIMEOUT_MS) => {
|
|
336
351
|
if (is_closed)
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and `format_scope_context` for label rendering. Per-row actions:
|
|
6
6
|
* grant role (`role_grant_offer_create`), revoke role_grant (`role_grant_revoke`,
|
|
7
7
|
* keyed by `actor_id`), retract pending offer (`role_grant_offer_retract`).
|
|
8
|
+
* The grant button offers the role globally, so `can_offer_global_role`
|
|
9
|
+
* hides it only for a global grant or pending global offer of that role.
|
|
8
10
|
*
|
|
9
11
|
* @module
|
|
10
12
|
*/
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
import {
|
|
13
15
|
AdminAccountsState,
|
|
14
16
|
admin_accounts_rpc_context,
|
|
17
|
+
can_offer_global_role,
|
|
15
18
|
grant_key
|
|
16
19
|
} from './admin_accounts_state.svelte.js';
|
|
17
20
|
import ConfirmButton from './ConfirmButton.svelte';
|
|
@@ -158,9 +161,7 @@
|
|
|
158
161
|
{#each admin_accounts.grantable_roles as role (role)}
|
|
159
162
|
{@const key = grant_key(row.account.id, role)}
|
|
160
163
|
{@const grant_error = admin_accounts.grant.error(key)}
|
|
161
|
-
{#if
|
|
162
|
-
!row.pending_offers.some((o) => o.role === role)
|
|
163
|
-
}
|
|
164
|
+
{#if can_offer_global_role(row, role)}
|
|
164
165
|
<ConfirmButton
|
|
165
166
|
onconfirm={() => admin_accounts.submit_grant(row.account.id, role)}
|
|
166
167
|
title="offer {role}"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminAccounts.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminAccounts.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminAccounts.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminAccounts.svelte"],"names":[],"mappings":"AAuMA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
const audit_log = new AuditLogState({ get_rpc: get_audit_log_rpc });
|
|
35
35
|
const app_settings = new AppSettingsState({ get_rpc: get_app_settings_rpc });
|
|
36
36
|
|
|
37
|
-
// accounts - dynamic role breakdown
|
|
37
|
+
// accounts - dynamic role breakdown; display only, and scope-blind on purpose:
|
|
38
|
+
// an account counts under each role it holds at any scope, so a count is not
|
|
39
|
+
// "who has this role globally" (that's `has_global_role` over the row's grants)
|
|
38
40
|
const role_counts = $derived.by(() => {
|
|
39
41
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
40
42
|
const counts = new Map<string, number>();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"AAiVA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
package/dist/ui/CLAUDE.md
CHANGED
|
@@ -281,8 +281,10 @@ destructive actions.
|
|
|
281
281
|
- `AdminAccounts.svelte` — accounts + role_grants + pending offers.
|
|
282
282
|
Consumes `admin_accounts_rpc_context`. Per-row actions: grant (+role
|
|
283
283
|
chip with `ConfirmButton`), revoke (`actor_id` + `role_grant_id`),
|
|
284
|
-
retract pending offer.
|
|
285
|
-
|
|
284
|
+
retract pending offer. The grant button offers the role globally, so it
|
|
285
|
+
shows per `can_offer_global_role` — hidden by a global grant or pending
|
|
286
|
+
global offer of that role, not by a scoped one. Reads per-row spinner +
|
|
287
|
+
error state via `state.grant.loading(key)` / `state.revoke.loading(role_grant_id)` /
|
|
286
288
|
`state.retract.loading(offer_id)` and their `.error(key)` siblings —
|
|
287
289
|
per-row error displays inline next to the failing button (no
|
|
288
290
|
top-level rollup).
|
|
@@ -297,7 +299,8 @@ destructive actions.
|
|
|
297
299
|
invites / recent activity / security / system). Consumes all four
|
|
298
300
|
RPC contexts plus `auth_state_context`; fetches in parallel on mount.
|
|
299
301
|
Derives `role_counts`, `failed_logins`, `role_grant_changes` from
|
|
300
|
-
the audit log.
|
|
302
|
+
the audit log. `role_counts` is display-only and scope-blind — an account
|
|
303
|
+
counts under each role it holds at any scope.
|
|
301
304
|
- `AdminRoleGrantHistory.svelte` — role-grant-create/revoke history table.
|
|
302
305
|
Consumes `audit_log_rpc_context`, calls
|
|
303
306
|
`audit_log.fetch_role_grant_history()` once on mount.
|
|
@@ -401,10 +404,21 @@ destructive actions.
|
|
|
401
404
|
Fields: `verifying`, `verified`, `verify_error`, `account`, `actor`
|
|
402
405
|
(the caller's own `ActorSummaryJson` — surfaced directly so consumers
|
|
403
406
|
don't derive `actor_id` from the role_grant list), `role_grants`,
|
|
404
|
-
`active_role_grants` (derived via `is_role_grant_active
|
|
407
|
+
`active_role_grants` (derived via `is_role_grant_active`, every scope),
|
|
408
|
+
`global_roles` (derived `ReadonlySet` — the roles of active grants with
|
|
409
|
+
`scope_id === null`), `is_admin` (derived — a global `admin` grant),
|
|
405
410
|
`needs_bootstrap`. Methods: `check_session()`
|
|
406
411
|
(GET `/api/account/status`), `login`, `bootstrap`, `signup`,
|
|
407
|
-
`logout`. Handles 401/403/409/429 translations
|
|
412
|
+
`logout`, `has_global_role(role)`. Handles 401/403/409/429 translations
|
|
413
|
+
inline.
|
|
414
|
+
**Role gates in the UI read the global view** — `is_admin` /
|
|
415
|
+
`has_global_role(role)`, the client form of the server's
|
|
416
|
+
`has_scoped_role(ctx, role, null)`. There is no scope-blind list of role
|
|
417
|
+
names: matching a grant on its role alone reads one scoped to a single
|
|
418
|
+
resource as global (and a scoped `admin` grant confers nothing). A scoped
|
|
419
|
+
check reads `active_role_grants` by `scope_id`; outside the class, the pure
|
|
420
|
+
`has_global_role(role_grants, role)` (`auth/account_schema.ts`) answers the
|
|
421
|
+
same question over any grant list.
|
|
408
422
|
- `table_state.svelte.ts` — `TableState`. Paginated DB browser state.
|
|
409
423
|
Holds one `AsyncSlot` (`list`) + payload fields (`table_name`,
|
|
410
424
|
`columns`, `rows`, `total`, `offset`, `limit` capped by
|
|
@@ -470,7 +484,9 @@ the `submit_*` prefix where the verb collides with a slot name.
|
|
|
470
484
|
(`KeyedAsyncSlot<Uuid, void>` keyed by `offer_id`). `submit_revoke`
|
|
471
485
|
takes `actor_id` as the first arg (role_grants are actor-scoped —
|
|
472
486
|
matches `row.actor.id` straight from the listing) with optional
|
|
473
|
-
`reason`.
|
|
487
|
+
`reason`. Also exports the pure `can_offer_global_role(entry, role)` —
|
|
488
|
+
whether a listing row has neither a global grant nor a pending global
|
|
489
|
+
offer of the role.
|
|
474
490
|
- `admin_invites_state.svelte.ts` — `AdminInvitesState` +
|
|
475
491
|
`admin_invites_rpc_context` + narrow `AdminInvitesRpc` (`list`,
|
|
476
492
|
`create`, `delete`). Slots: `list`, `create` (both AsyncSlot),
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
16
16
|
import { AsyncSlot } from './async_slot.svelte.js';
|
|
17
17
|
import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
|
|
18
|
-
import type
|
|
18
|
+
import { type AdminAccountEntryJson } from '../auth/account_schema.js';
|
|
19
19
|
import type { RoleName } from '../auth/role_schema.js';
|
|
20
20
|
import type { RoleGrantOfferJson } from '../auth/role_grant_offer_schema.js';
|
|
21
21
|
import type { AdminAccountListOutput, AccountDeleteOutput, AccountUndeleteOutput, AdminSessionListOutput, AdminSessionRevokeAllInput, AdminSessionRevokeAllOutput, AdminTokenRevokeAllInput, AdminTokenRevokeAllOutput } from '../auth/admin_action_specs.js';
|
|
@@ -78,6 +78,20 @@ export interface AdminAccountsStateOptions {
|
|
|
78
78
|
* colliding on per-row spinners.
|
|
79
79
|
*/
|
|
80
80
|
export declare const grant_key: (account_id: Uuid, role: RoleName, to_actor_id?: Uuid | null) => string;
|
|
81
|
+
/**
|
|
82
|
+
* Whether a listing row can be offered a global grant of `role` — the test
|
|
83
|
+
* behind `AdminAccounts`' `+ role` button, which `submit_grant` backs with an
|
|
84
|
+
* unscoped offer. Only what the offer would duplicate rules it out: a global
|
|
85
|
+
* grant of the role, or a pending global offer of it. A grant or offer scoped
|
|
86
|
+
* to one resource is a different grant, so it doesn't.
|
|
87
|
+
*
|
|
88
|
+
* @param entry - the account's listing row
|
|
89
|
+
* @param role - the role the button would offer
|
|
90
|
+
* @param now - current time for the grant-expiry check (defaults to `new Date()`,
|
|
91
|
+
* pass for testability); pending offers are taken as listed — the listing
|
|
92
|
+
* returns only unexpired ones
|
|
93
|
+
*/
|
|
94
|
+
export declare const can_offer_global_role: (entry: Pick<AdminAccountEntryJson, "role_grants" | "pending_offers">, role: string, now?: Date) => boolean;
|
|
81
95
|
export declare class AdminAccountsState {
|
|
82
96
|
#private;
|
|
83
97
|
readonly list: AsyncSlot<void, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin_accounts_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_accounts_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"admin_accounts_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/admin_accounts_state.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { create_context } from '@fuzdev/fuz_ui/context_helpers.ts';
|
|
16
16
|
import { AsyncSlot } from './async_slot.svelte.js';
|
|
17
17
|
import { KeyedAsyncSlot } from './keyed_async_slot.svelte.js';
|
|
18
|
+
import { has_global_role } from '../auth/account_schema.js';
|
|
18
19
|
/**
|
|
19
20
|
* Svelte context carrying the reactive `AdminAccountsRpc` accessor. The
|
|
20
21
|
* provisioner (typically the admin route shell) calls `set(() => rpc)`;
|
|
@@ -32,6 +33,21 @@ export const admin_accounts_rpc_context = create_context();
|
|
|
32
33
|
* colliding on per-row spinners.
|
|
33
34
|
*/
|
|
34
35
|
export const grant_key = (account_id, role, to_actor_id) => to_actor_id ? `${account_id}:${role}:${to_actor_id}` : `${account_id}:${role}`;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a listing row can be offered a global grant of `role` — the test
|
|
38
|
+
* behind `AdminAccounts`' `+ role` button, which `submit_grant` backs with an
|
|
39
|
+
* unscoped offer. Only what the offer would duplicate rules it out: a global
|
|
40
|
+
* grant of the role, or a pending global offer of it. A grant or offer scoped
|
|
41
|
+
* to one resource is a different grant, so it doesn't.
|
|
42
|
+
*
|
|
43
|
+
* @param entry - the account's listing row
|
|
44
|
+
* @param role - the role the button would offer
|
|
45
|
+
* @param now - current time for the grant-expiry check (defaults to `new Date()`,
|
|
46
|
+
* pass for testability); pending offers are taken as listed — the listing
|
|
47
|
+
* returns only unexpired ones
|
|
48
|
+
*/
|
|
49
|
+
export const can_offer_global_role = (entry, role, now = new Date()) => !has_global_role(entry.role_grants, role, now) &&
|
|
50
|
+
!entry.pending_offers.some((o) => o.role === role && o.scope_id === null);
|
|
35
51
|
export class AdminAccountsState {
|
|
36
52
|
#get_rpc;
|
|
37
53
|
list = new AsyncSlot();
|
|
@@ -48,10 +48,34 @@ export declare class AuthState {
|
|
|
48
48
|
account: SessionAccount | null;
|
|
49
49
|
actor: ActorSummaryJson | null;
|
|
50
50
|
role_grants: Array<RoleGrantSummaryJson>;
|
|
51
|
+
/**
|
|
52
|
+
* The live subset of `role_grants`, at every scope. Liveness is judged when
|
|
53
|
+
* `role_grants` is assigned, so a grant that expires while the page stays
|
|
54
|
+
* open reads as active until the next `check_session`.
|
|
55
|
+
*/
|
|
51
56
|
readonly active_role_grants: Array<RoleGrantSummaryJson>;
|
|
52
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The roles held globally — the role of each active grant whose `scope_id`
|
|
59
|
+
* is `null`. A grant scoped to one resource is not in here: it confers its
|
|
60
|
+
* role on that resource only, and a scoped grant of a builtin role confers
|
|
61
|
+
* nothing, so a role gate reads the global grant. Scoped grants are read
|
|
62
|
+
* from `active_role_grants`, by `scope_id`.
|
|
63
|
+
*
|
|
64
|
+
* Shapes the UI only — the server re-checks every request.
|
|
65
|
+
*/
|
|
66
|
+
readonly global_roles: ReadonlySet<string>;
|
|
67
|
+
/** True for an active global `admin` grant, the rule the server's admin gates apply. */
|
|
68
|
+
readonly is_admin: boolean;
|
|
53
69
|
/** True when bootstrap is available (no accounts exist yet). */
|
|
54
70
|
needs_bootstrap: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the account holds an active global grant of `role` — reactive
|
|
73
|
+
* when read in a `$derived` or the template. A scoped grant doesn't count;
|
|
74
|
+
* see `global_roles`.
|
|
75
|
+
*
|
|
76
|
+
* @param role - the role to check
|
|
77
|
+
*/
|
|
78
|
+
has_global_role(role: string): boolean;
|
|
55
79
|
/**
|
|
56
80
|
* Check auth state and bootstrap availability.
|
|
57
81
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"auth_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/auth_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,MAAM,2BAA2B,CAAC;AAGnC;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;CAA8B,CAAC;AAE9D,qBAAa,SAAS;IACrB,SAAS,UAAqB;IAC9B,QAAQ,UAAqB;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC/C,OAAO,EAAE,cAAc,GAAG,IAAI,CAAoB;IAClD,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAoB;IAClD,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAkB;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAEtD;IACF;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAExC;IACF,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAA+C;IAEzE,gEAAgE;IAChE,eAAe,UAAqB;IAEpC;;;;;;OAMG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAItC;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IA6BpC;;;;;;;OAOG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CjE;;;;;OAKG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpF;;;;;;;OAOG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8ClF;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAW7B"}
|