@fuzdev/fuz_app 0.23.0 → 0.25.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/action_codegen.d.ts +25 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +39 -0
- package/dist/actions/action_peer.d.ts +7 -0
- package/dist/actions/action_peer.d.ts.map +1 -1
- package/dist/actions/action_peer.js +1 -1
- package/dist/actions/action_types.d.ts +72 -0
- package/dist/actions/action_types.d.ts.map +1 -0
- package/dist/actions/action_types.js +11 -0
- package/dist/actions/cancel.d.ts +78 -0
- package/dist/actions/cancel.d.ts.map +1 -0
- package/dist/actions/cancel.js +79 -0
- package/dist/actions/heartbeat.d.ts +51 -0
- package/dist/actions/heartbeat.d.ts.map +1 -0
- package/dist/actions/heartbeat.js +50 -0
- package/dist/actions/register_action_ws.d.ts +28 -30
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +103 -20
- package/dist/actions/rpc_client.d.ts +10 -0
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +22 -7
- package/dist/actions/socket.svelte.d.ts +88 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +322 -6
- package/dist/actions/transports.d.ts +18 -3
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +4 -0
- package/dist/actions/transports_http.d.ts +3 -3
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_http.js +4 -3
- package/dist/actions/transports_ws.d.ts +33 -6
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +43 -46
- package/dist/actions/transports_ws_backend.d.ts +12 -3
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +12 -1
- package/dist/auth/bearer_auth.js +0 -1
- package/dist/auth/keyring.d.ts.map +1 -1
- package/dist/auth/keyring.js +0 -2
- package/dist/auth/migrations.js +4 -4
- package/dist/db/migrate.d.ts +12 -2
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +25 -16
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +0 -2
- package/dist/dev/setup.js +2 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +0 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -3
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/data_exposure.js +6 -8
- package/dist/testing/db.js +1 -1
- package/dist/testing/integration.js +0 -1
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +0 -6
- package/dist/testing/rpc_round_trip.js +4 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -2
- package/dist/testing/ws_round_trip.d.ts +15 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +3 -3
- package/package.json +2 -2
|
@@ -1,82 +1,79 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WebSocket transport —
|
|
2
|
+
* Frontend WebSocket transport — thin adapter over `WebsocketRpcConnection`.
|
|
3
|
+
*
|
|
4
|
+
* Delegates request/response correlation, the durable queue, the heartbeat,
|
|
5
|
+
* and `AbortSignal`-driven cancel to the underlying connection (the
|
|
6
|
+
* canonical implementation is `FrontendWebsocketClient`). The transport's
|
|
7
|
+
* own job is the `Transport` contract: route inbound server-pushed
|
|
8
|
+
* messages into `peer.receive` and translate the connection's
|
|
9
|
+
* `Promise<R>`/`ThrownJsonrpcError` shape into `JsonrpcResponseOrError`
|
|
10
|
+
* envelopes. No parallel pending-request map.
|
|
3
11
|
*
|
|
4
12
|
* @module
|
|
5
13
|
*/
|
|
6
|
-
import { ThrownJsonrpcError, jsonrpc_error_messages
|
|
7
|
-
import { is_jsonrpc_notification, is_jsonrpc_request,
|
|
8
|
-
import { RequestTracker } from './request_tracker.svelte.js';
|
|
14
|
+
import { ThrownJsonrpcError, jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
15
|
+
import { is_jsonrpc_notification, is_jsonrpc_request, to_jsonrpc_message_id, to_jsonrpc_result, create_jsonrpc_response, create_jsonrpc_error_response, } from '../http/jsonrpc_helpers.js';
|
|
9
16
|
export class FrontendWebsocketTransport {
|
|
10
17
|
transport_name = 'frontend_websocket_rpc';
|
|
11
18
|
#connection;
|
|
12
19
|
#receive;
|
|
13
|
-
#request_tracker;
|
|
14
20
|
#remove_message_handler;
|
|
15
21
|
#remove_error_handler;
|
|
16
|
-
constructor(connection, receive
|
|
22
|
+
constructor(connection, receive) {
|
|
17
23
|
this.#connection = connection;
|
|
18
24
|
this.#receive = receive;
|
|
19
|
-
|
|
20
|
-
//
|
|
25
|
+
// Inbound dispatch — only server-pushed requests/notifications need
|
|
26
|
+
// routing here. Responses to requests we sent are correlated by the
|
|
27
|
+
// connection's own `request()` pending map.
|
|
21
28
|
this.#remove_message_handler = connection.add_message_handler(async (event) => {
|
|
22
29
|
try {
|
|
23
30
|
const data = JSON.parse(event.data);
|
|
24
|
-
|
|
25
|
-
// Check if this is a response to one of our requests
|
|
26
|
-
if (is_jsonrpc_response(data) || (is_jsonrpc_error_response(data) && data.id !== null)) {
|
|
27
|
-
// This is a response to a request we sent
|
|
28
|
-
this.#request_tracker.handle_message(data);
|
|
29
|
-
}
|
|
30
|
-
else if (is_jsonrpc_request(data) || is_jsonrpc_notification(data)) {
|
|
31
|
-
// This is a new request/notification from the server
|
|
31
|
+
if (is_jsonrpc_request(data) || is_jsonrpc_notification(data)) {
|
|
32
32
|
await this.#receive(data);
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
console.warn('[ws_transport] received unknown message type:', data);
|
|
36
|
-
}
|
|
34
|
+
// Responses are owned by `connection.request()` — ignore here.
|
|
37
35
|
}
|
|
38
36
|
catch (error) {
|
|
39
37
|
console.error('[ws_transport] error parsing WebSocket message:', error);
|
|
40
|
-
// TODO maybe send the whole thing back wrapped in an error?
|
|
41
|
-
// can't reference anything else for a response
|
|
42
38
|
}
|
|
43
39
|
});
|
|
44
40
|
this.#remove_error_handler = connection.add_error_handler((event) => {
|
|
45
41
|
console.error('[ws_transport] WebSocket error:', event);
|
|
46
42
|
});
|
|
47
43
|
}
|
|
48
|
-
async send(message) {
|
|
44
|
+
async send(message, options) {
|
|
45
|
+
// Fail-fast at the transport boundary. The connection's own queue
|
|
46
|
+
// would buffer the request and flush on reconnect; that's the right
|
|
47
|
+
// default for direct `client.request()` callers but the typed Proxy
|
|
48
|
+
// path expects "service unavailable" semantics when the WS is down.
|
|
49
49
|
if (!this.is_ready()) {
|
|
50
50
|
return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.service_unavailable('WebSocket not connected'));
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return result;
|
|
52
|
+
if (is_jsonrpc_request(message)) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await this.#connection.request(message.method, message.params, {
|
|
55
|
+
id: message.id,
|
|
56
|
+
signal: options?.signal,
|
|
57
|
+
queue: false,
|
|
58
|
+
});
|
|
59
|
+
return create_jsonrpc_response(message.id, to_jsonrpc_result(result));
|
|
61
60
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof ThrownJsonrpcError) {
|
|
63
|
+
return create_jsonrpc_error_response(message.id, {
|
|
64
|
+
code: error.code,
|
|
65
|
+
message: error.message,
|
|
66
|
+
data: error.data,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return create_jsonrpc_error_response(message.id, jsonrpc_error_messages.internal_error(error instanceof Error ? error.message : String(error)));
|
|
66
70
|
}
|
|
67
|
-
// Invalid message type - return error with id if available
|
|
68
|
-
return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.invalid_request());
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
code: error.code,
|
|
74
|
-
message: error.message,
|
|
75
|
-
data: error.data,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.internal_error(error.message || UNKNOWN_ERROR_MESSAGE));
|
|
72
|
+
if (is_jsonrpc_notification(message)) {
|
|
73
|
+
this.#connection.send(message);
|
|
74
|
+
return null;
|
|
79
75
|
}
|
|
76
|
+
return create_jsonrpc_error_response(to_jsonrpc_message_id(message), jsonrpc_error_messages.invalid_request());
|
|
80
77
|
}
|
|
81
78
|
is_ready() {
|
|
82
79
|
return this.#connection.connected;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { WSContext } from 'hono/ws';
|
|
8
8
|
import type { JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
9
9
|
import { type Uuid } from '../uuid.js';
|
|
10
|
-
import { type Transport } from './transports.js';
|
|
10
|
+
import { type Transport, type TransportSendOptions } from './transports.js';
|
|
11
11
|
/**
|
|
12
12
|
* Auth identity attached to a single WebSocket connection.
|
|
13
13
|
*
|
|
@@ -78,8 +78,8 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
78
78
|
* @returns the number of sockets closed
|
|
79
79
|
*/
|
|
80
80
|
close_sockets_for_token(api_token_id: string): number;
|
|
81
|
-
send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
|
|
82
|
-
send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
|
|
81
|
+
send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
|
|
82
|
+
send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
|
|
83
83
|
/**
|
|
84
84
|
* Broadcast to connections whose identity satisfies a predicate.
|
|
85
85
|
*
|
|
@@ -92,5 +92,14 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
92
92
|
*/
|
|
93
93
|
broadcast_filtered(message: JsonrpcMessageFromServerToClient, predicate: (identity: ConnectionIdentity) => boolean): number;
|
|
94
94
|
is_ready(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Number of currently tracked WebSocket connections.
|
|
97
|
+
*
|
|
98
|
+
* Read-only counter intended for telemetry, logging, and tests.
|
|
99
|
+
* Counts every entry in the connection map — including connections
|
|
100
|
+
* that have been closed by the peer but not yet removed by the WS
|
|
101
|
+
* adapter's `onClose` callback.
|
|
102
|
+
*/
|
|
103
|
+
get_connection_count(): number;
|
|
95
104
|
}
|
|
96
105
|
//# sourceMappingURL=transports_ws_backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,YAAY,CAAC;AAClD,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAc,KAAK,IAAI,EAAC,MAAM,YAAY,CAAC;AAClD,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAIpG;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,sEAAsE;IACtE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,IAAI,CAAC;IACjB,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA6B,SAAQ,SAAS;IAC9D,kBAAkB,EAAE,CACnB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,KAChD,MAAM,CAAC;CACZ;AAED,qDAAqD;AACrD,eAAO,MAAM,iCAAiC,GAC7C,WAAW,SAAS,KAClB,SAAS,IAAI,4BAEqE,CAAC;AAEtF,qBAAa,yBAA0B,YAAW,4BAA4B;;IAC7E,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAY3D;;;;;;;;OAQG;IACH,cAAc,CACb,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,IAAI,EAChB,YAAY,GAAE,MAAM,GAAG,IAAW,GAChC,IAAI;IAQP;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IA0BtC;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAIrD;;;;OAIG;IACH,yBAAyB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM;IAInD;;;;;;;;OAQG;IACH,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAsB/C,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA6CvC;;;;;;;;;OASG;IACH,kBAAkB,CACjB,OAAO,EAAE,gCAAgC,EACzC,SAAS,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,GAClD,MAAM;IAoBT,QAAQ,IAAI,OAAO;IAInB;;;;;;;OAOG;IACH,oBAAoB,IAAI,MAAM;CAG9B"}
|
|
@@ -107,7 +107,7 @@ export class BackendWebsocketTransport {
|
|
|
107
107
|
this.#cleanup_connection(connection_id, ws);
|
|
108
108
|
ws.close(WS_CLOSE_SESSION_REVOKED, 'Session revoked');
|
|
109
109
|
}
|
|
110
|
-
async send(message) {
|
|
110
|
+
async send(message, _options) {
|
|
111
111
|
// TODO currently just broadcasts all messages to all clients, the transport abstraction is still a WIP
|
|
112
112
|
if (is_jsonrpc_request(message)) {
|
|
113
113
|
return create_jsonrpc_error_response(message.id,
|
|
@@ -170,4 +170,15 @@ export class BackendWebsocketTransport {
|
|
|
170
170
|
is_ready() {
|
|
171
171
|
return this.#connections.size > 0;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Number of currently tracked WebSocket connections.
|
|
175
|
+
*
|
|
176
|
+
* Read-only counter intended for telemetry, logging, and tests.
|
|
177
|
+
* Counts every entry in the connection map — including connections
|
|
178
|
+
* that have been closed by the peer but not yet removed by the WS
|
|
179
|
+
* adapter's `onClose` callback.
|
|
180
|
+
*/
|
|
181
|
+
get_connection_count() {
|
|
182
|
+
return this.#connections.size;
|
|
183
|
+
}
|
|
173
184
|
}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -50,7 +50,6 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
50
50
|
// Case-insensitive scheme matching per RFC 7235 §2.1 — defense-in-depth:
|
|
51
51
|
// without this, a `bearer` (lowercase) header silently bypasses token
|
|
52
52
|
// validation and browser-context rejection instead of being processed.
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
54
53
|
if (!auth_header || auth_header.slice(0, 7).toLowerCase() !== 'bearer ') {
|
|
55
54
|
await next();
|
|
56
55
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC,CAAC;CACrF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"keyring.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB;;;OAGG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;OAIG;IACH,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC,CAAC;CACrF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,OAAO,GAAG,IAiCxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAc5E,CAAC;AA6CF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAC/B;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,GAC5B;IAAC,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAC,CAAC;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,sBAUxE,CAAC"}
|
package/dist/auth/keyring.js
CHANGED
|
@@ -58,9 +58,7 @@ export const create_keyring = (env_value) => {
|
|
|
58
58
|
},
|
|
59
59
|
async verify(signed_value) {
|
|
60
60
|
for (let i = 0; i < secrets.length; i++) {
|
|
61
|
-
// eslint-disable-next-line no-await-in-loop
|
|
62
61
|
const key = await get_key(i);
|
|
63
|
-
// eslint-disable-next-line no-await-in-loop
|
|
64
62
|
const result = await verify_with_crypto_key(signed_value, key);
|
|
65
63
|
if (result !== false) {
|
|
66
64
|
return { value: result, key_index: i };
|
package/dist/auth/migrations.js
CHANGED
|
@@ -49,23 +49,23 @@ export const AUTH_MIGRATIONS = [
|
|
|
49
49
|
await db.query(ACTOR_INDEX);
|
|
50
50
|
await db.query(PERMIT_SCHEMA);
|
|
51
51
|
for (const sql of PERMIT_INDEXES) {
|
|
52
|
-
await db.query(sql);
|
|
52
|
+
await db.query(sql);
|
|
53
53
|
}
|
|
54
54
|
await db.query(AUTH_SESSION_SCHEMA);
|
|
55
55
|
for (const sql of AUTH_SESSION_INDEXES) {
|
|
56
|
-
await db.query(sql);
|
|
56
|
+
await db.query(sql);
|
|
57
57
|
}
|
|
58
58
|
await db.query(API_TOKEN_SCHEMA);
|
|
59
59
|
await db.query(API_TOKEN_INDEX);
|
|
60
60
|
await db.query(AUDIT_LOG_SCHEMA);
|
|
61
61
|
for (const sql of AUDIT_LOG_INDEXES) {
|
|
62
|
-
await db.query(sql);
|
|
62
|
+
await db.query(sql);
|
|
63
63
|
}
|
|
64
64
|
await db.query(BOOTSTRAP_LOCK_SCHEMA);
|
|
65
65
|
await db.query(BOOTSTRAP_LOCK_SEED);
|
|
66
66
|
await db.query(INVITE_SCHEMA);
|
|
67
67
|
for (const sql of INVITE_INDEXES) {
|
|
68
|
-
await db.query(sql);
|
|
68
|
+
await db.query(sql);
|
|
69
69
|
}
|
|
70
70
|
await db.query(APP_SETTINGS_SCHEMA);
|
|
71
71
|
await db.query(APP_SETTINGS_SEED);
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Migrations are functions in ordered arrays, grouped by namespace.
|
|
5
5
|
* A `schema_version` table tracks progress per namespace.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* **Chain-level transactions**: All pending migrations in a namespace run in a
|
|
8
|
+
* single transaction. Any failure rolls back every migration in that run —
|
|
9
|
+
* no partial-state recovery. This rules out non-transactional DDL (e.g.,
|
|
10
|
+
* `CREATE INDEX CONCURRENTLY`); run those out of band.
|
|
7
11
|
*
|
|
8
12
|
* **Forward-only**: No down-migrations. Schema changes are additive.
|
|
9
13
|
* For pre-release development, collapse migrations into a single v0.
|
|
@@ -53,9 +57,15 @@ export interface MigrationResult {
|
|
|
53
57
|
*
|
|
54
58
|
* Creates the `schema_version` tracking table if it does not exist,
|
|
55
59
|
* then for each namespace: acquires an advisory lock, reads the current
|
|
56
|
-
* version, runs pending migrations in order
|
|
60
|
+
* version, runs all pending migrations in order inside a single transaction,
|
|
57
61
|
* updates the stored version, and releases the lock.
|
|
58
62
|
*
|
|
63
|
+
* **Atomicity**: The pending chain for each namespace runs in one transaction —
|
|
64
|
+
* any failure rolls back every migration that ran in that invocation. The
|
|
65
|
+
* next run starts from the previously-stored version, re-running the whole
|
|
66
|
+
* (fixed) chain. Namespaces are independent: a later namespace's failure
|
|
67
|
+
* does not roll back an earlier namespace that already committed.
|
|
68
|
+
*
|
|
59
69
|
* **Concurrency**: Uses PostgreSQL advisory locks to serialize concurrent
|
|
60
70
|
* callers on the same namespace. Safe for multi-instance deployments.
|
|
61
71
|
*
|
package/dist/db/migrate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AA8BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,GAC1B,IAAI,EAAE,EACN,YAAY,KAAK,CAAC,kBAAkB,CAAC,KACnC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CA0EhC,CAAC"}
|
package/dist/db/migrate.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Migrations are functions in ordered arrays, grouped by namespace.
|
|
5
5
|
* A `schema_version` table tracks progress per namespace.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* **Chain-level transactions**: All pending migrations in a namespace run in a
|
|
8
|
+
* single transaction. Any failure rolls back every migration in that run —
|
|
9
|
+
* no partial-state recovery. This rules out non-transactional DDL (e.g.,
|
|
10
|
+
* `CREATE INDEX CONCURRENTLY`); run those out of band.
|
|
7
11
|
*
|
|
8
12
|
* **Forward-only**: No down-migrations. Schema changes are additive.
|
|
9
13
|
* For pre-release development, collapse migrations into a single v0.
|
|
@@ -46,9 +50,15 @@ const namespace_lock_key = (namespace) => {
|
|
|
46
50
|
*
|
|
47
51
|
* Creates the `schema_version` tracking table if it does not exist,
|
|
48
52
|
* then for each namespace: acquires an advisory lock, reads the current
|
|
49
|
-
* version, runs pending migrations in order
|
|
53
|
+
* version, runs all pending migrations in order inside a single transaction,
|
|
50
54
|
* updates the stored version, and releases the lock.
|
|
51
55
|
*
|
|
56
|
+
* **Atomicity**: The pending chain for each namespace runs in one transaction —
|
|
57
|
+
* any failure rolls back every migration that ran in that invocation. The
|
|
58
|
+
* next run starts from the previously-stored version, re-running the whole
|
|
59
|
+
* (fixed) chain. Namespaces are independent: a later namespace's failure
|
|
60
|
+
* does not roll back an earlier namespace that already committed.
|
|
61
|
+
*
|
|
52
62
|
* **Concurrency**: Uses PostgreSQL advisory locks to serialize concurrent
|
|
53
63
|
* callers on the same namespace. Safe for multi-instance deployments.
|
|
54
64
|
*
|
|
@@ -59,7 +69,6 @@ const namespace_lock_key = (namespace) => {
|
|
|
59
69
|
export const run_migrations = async (db, namespaces) => {
|
|
60
70
|
await db.query(SCHEMA_VERSION_DDL);
|
|
61
71
|
const results = [];
|
|
62
|
-
/* eslint-disable no-await-in-loop */
|
|
63
72
|
for (const { namespace, migrations } of namespaces) {
|
|
64
73
|
const lock_key = namespace_lock_key(namespace);
|
|
65
74
|
// Acquire advisory lock — serializes concurrent migration runs
|
|
@@ -78,24 +87,25 @@ export const run_migrations = async (db, namespaces) => {
|
|
|
78
87
|
if (current_version === migrations.length) {
|
|
79
88
|
continue; // up to date
|
|
80
89
|
}
|
|
81
|
-
// run pending migrations
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
// run all pending migrations in a single transaction — any failure
|
|
91
|
+
// rolls back the whole pending chain
|
|
92
|
+
await db.transaction(async (tx) => {
|
|
93
|
+
for (let i = current_version; i < migrations.length; i++) {
|
|
94
|
+
const { fn, name } = resolve_migration(migrations[i]);
|
|
95
|
+
const label = name != null ? `"${name}"` : '';
|
|
96
|
+
try {
|
|
87
97
|
await fn(tx);
|
|
88
98
|
await tx.query(`INSERT INTO schema_version (namespace, version, applied_at)
|
|
89
99
|
VALUES ($1, $2, NOW())
|
|
90
100
|
ON CONFLICT (namespace)
|
|
91
101
|
DO UPDATE SET version = $2, applied_at = NOW()`, [namespace, i + 1]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const name_part = label ? ` ${label}` : '';
|
|
105
|
+
throw new Error(`Migration ${namespace}[${i}]${name_part} failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
106
|
+
}
|
|
97
107
|
}
|
|
98
|
-
}
|
|
108
|
+
});
|
|
99
109
|
results.push({
|
|
100
110
|
namespace,
|
|
101
111
|
from_version: current_version,
|
|
@@ -113,6 +123,5 @@ export const run_migrations = async (db, namespaces) => {
|
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
|
-
/* eslint-enable no-await-in-loop */
|
|
117
126
|
return results;
|
|
118
127
|
};
|
package/dist/db/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,yCAAyC;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC3B,sCAAsC;IACtC,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAC3B,IAAI,EAAE,EACN,aAAa,KAAK,CAAC,kBAAkB,CAAC,KACpC,OAAO,CAAC,QAAQ,
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,yCAAyC;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC3B,sCAAsC;IACtC,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAC3B,IAAI,EAAE,EACN,aAAa,KAAK,CAAC,kBAAkB,CAAC,KACpC,OAAO,CAAC,QAAQ,CA8ElB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,QAAQ,KAAG,MA6BnD,CAAC"}
|
package/dist/db/status.js
CHANGED
|
@@ -36,7 +36,6 @@ export const query_db_status = async (db, namespaces) => {
|
|
|
36
36
|
const tables = [];
|
|
37
37
|
for (const { table_name } of table_rows) {
|
|
38
38
|
// table_name from information_schema is trusted
|
|
39
|
-
// eslint-disable-next-line no-await-in-loop
|
|
40
39
|
const result = await db.query_one(`SELECT COUNT(*) as count FROM "${table_name}"`);
|
|
41
40
|
tables.push({
|
|
42
41
|
name: table_name,
|
|
@@ -53,7 +52,6 @@ export const query_db_status = async (db, namespaces) => {
|
|
|
53
52
|
) as exists`);
|
|
54
53
|
if (sv_exists?.exists) {
|
|
55
54
|
for (const { namespace, migrations: ns_migrations } of namespaces) {
|
|
56
|
-
// eslint-disable-next-line no-await-in-loop
|
|
57
55
|
const row = await db.query_one('SELECT version FROM schema_version WHERE namespace = $1', [namespace]);
|
|
58
56
|
const current_version = row?.version ?? 0;
|
|
59
57
|
migrations.push({
|
package/dist/dev/setup.js
CHANGED
|
@@ -93,7 +93,7 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
|
93
93
|
for (const [key, generate] of Object.entries(replacements)) {
|
|
94
94
|
const pattern = new RegExp(`^${key}=$`, 'm');
|
|
95
95
|
if (pattern.test(content)) {
|
|
96
|
-
const value = await generate();
|
|
96
|
+
const value = await generate();
|
|
97
97
|
content = content.replace(pattern, `${key}=${value}`);
|
|
98
98
|
changed = true;
|
|
99
99
|
log.ok(`Generated ${key} in existing ${env_path}`);
|
|
@@ -114,7 +114,7 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
|
|
|
114
114
|
for (const [key, generate] of Object.entries(replacements)) {
|
|
115
115
|
const pattern = new RegExp(`^${key}=$`, 'm');
|
|
116
116
|
if (pattern.test(content)) {
|
|
117
|
-
const value = await generate();
|
|
117
|
+
const value = await generate();
|
|
118
118
|
content = content.replace(pattern, `${key}=${value}`);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAmB,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAWjE;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,cAAc,KAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAmB,KAAK,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAWjE;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,cAAc,KAAG,KAAK,CAAC,SAAS,CAsN9E,CAAC"}
|
package/dist/http/db_routes.js
CHANGED
|
@@ -66,7 +66,6 @@ export const create_db_route_specs = (options) => {
|
|
|
66
66
|
ORDER BY table_name`);
|
|
67
67
|
const tables = [];
|
|
68
68
|
for (const { table_name } of table_names) {
|
|
69
|
-
// eslint-disable-next-line no-await-in-loop
|
|
70
69
|
const result = await route.db.query_one(`SELECT COUNT(*) as count FROM "${assert_valid_sql_identifier(table_name)}"`);
|
|
71
70
|
tables.push({
|
|
72
71
|
name: table_name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,
|
|
1
|
+
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAsnCF,CAAC"}
|
|
@@ -804,7 +804,6 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
804
804
|
const admin_routes = test_app.route_specs.filter((s) => s.path.startsWith(prefix) && s.auth.type === 'role' && s.auth.role === 'admin');
|
|
805
805
|
// Hit admin routes without auth to exercise 401 error schemas
|
|
806
806
|
for (const route of admin_routes.slice(0, 5)) {
|
|
807
|
-
// eslint-disable-next-line no-await-in-loop
|
|
808
807
|
const res = await test_app.app.request(route.path, {
|
|
809
808
|
method: route.method,
|
|
810
809
|
headers: { host: 'localhost' },
|
|
@@ -826,12 +825,10 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
826
825
|
s.auth.role === 'admin');
|
|
827
826
|
assert.ok(admin_get_routes.length > 0, 'Expected at least one admin GET route — ensure create_route_specs includes admin routes');
|
|
828
827
|
for (const route of admin_get_routes) {
|
|
829
|
-
// eslint-disable-next-line no-await-in-loop
|
|
830
828
|
const res = await test_app.app.request(route.path, {
|
|
831
829
|
headers: test_app.create_session_headers(),
|
|
832
830
|
});
|
|
833
831
|
assert.strictEqual(res.status, 200, `${route.method} ${route.path} should return 200`);
|
|
834
|
-
// eslint-disable-next-line no-await-in-loop
|
|
835
832
|
await assert_response_matches_spec(test_app.route_specs, route.method, route.path, res);
|
|
836
833
|
}
|
|
837
834
|
});
|
|
@@ -51,7 +51,7 @@ export const bootstrap_test_account = async (options) => {
|
|
|
51
51
|
});
|
|
52
52
|
// Grant roles
|
|
53
53
|
for (const role of roles) {
|
|
54
|
-
await query_grant_permit(deps, { actor_id: actor.id, role, granted_by: null });
|
|
54
|
+
await query_grant_permit(deps, { actor_id: actor.id, role, granted_by: null });
|
|
55
55
|
}
|
|
56
56
|
// Create API token
|
|
57
57
|
const { token: api_token, id: token_id, token_hash } = generate_api_token();
|
|
@@ -171,18 +171,17 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
171
171
|
if (skip_set.has(route_key))
|
|
172
172
|
continue;
|
|
173
173
|
const url = resolve_valid_path(spec.path, spec.params);
|
|
174
|
-
// eslint-disable-next-line no-await-in-loop
|
|
175
174
|
const res = await test_app.app.request(url, {
|
|
176
175
|
method: spec.method,
|
|
177
176
|
headers: { host: 'localhost', origin: 'http://localhost:5173' },
|
|
178
177
|
});
|
|
179
178
|
if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
180
|
-
await res.body?.cancel();
|
|
179
|
+
await res.body?.cancel();
|
|
181
180
|
continue;
|
|
182
181
|
}
|
|
183
182
|
let error_body;
|
|
184
183
|
try {
|
|
185
|
-
error_body = await res.clone().json();
|
|
184
|
+
error_body = await res.clone().json();
|
|
186
185
|
}
|
|
187
186
|
catch {
|
|
188
187
|
continue;
|
|
@@ -200,7 +199,6 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
200
199
|
continue;
|
|
201
200
|
const url = resolve_valid_path(spec.path, spec.params);
|
|
202
201
|
const headers = authed_account.create_session_headers();
|
|
203
|
-
// eslint-disable-next-line no-await-in-loop
|
|
204
202
|
const res = await test_app.app.request(url, {
|
|
205
203
|
method: spec.method,
|
|
206
204
|
headers,
|
|
@@ -208,7 +206,7 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
208
206
|
assert.strictEqual(res.status, 403, `${route_key} should return 403 for non-admin user`);
|
|
209
207
|
let error_body;
|
|
210
208
|
try {
|
|
211
|
-
error_body = await res.clone().json();
|
|
209
|
+
error_body = await res.clone().json();
|
|
212
210
|
}
|
|
213
211
|
catch {
|
|
214
212
|
continue;
|
|
@@ -247,16 +245,16 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
247
245
|
},
|
|
248
246
|
...(body ? { body: JSON.stringify(body) } : {}),
|
|
249
247
|
};
|
|
250
|
-
const res = await test_app.app.request(url, request_init);
|
|
248
|
+
const res = await test_app.app.request(url, request_init);
|
|
251
249
|
if (res.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
252
|
-
await res.body?.cancel();
|
|
250
|
+
await res.body?.cancel();
|
|
253
251
|
continue;
|
|
254
252
|
}
|
|
255
253
|
if (!res.ok)
|
|
256
254
|
continue;
|
|
257
255
|
let response_body;
|
|
258
256
|
try {
|
|
259
|
-
response_body = await res.clone().json();
|
|
257
|
+
response_body = await res.clone().json();
|
|
260
258
|
}
|
|
261
259
|
catch {
|
|
262
260
|
continue;
|
package/dist/testing/db.js
CHANGED
|
@@ -206,7 +206,7 @@ export const AUTH_DROP_TABLES = [
|
|
|
206
206
|
*/
|
|
207
207
|
export const drop_auth_schema = async (db) => {
|
|
208
208
|
for (const table of AUTH_DROP_TABLES) {
|
|
209
|
-
await db.query(`DROP TABLE IF EXISTS ${assert_valid_sql_identifier(table)} CASCADE`);
|
|
209
|
+
await db.query(`DROP TABLE IF EXISTS ${assert_valid_sql_identifier(table)} CASCADE`);
|
|
210
210
|
}
|
|
211
211
|
await db.query('DROP TABLE IF EXISTS schema_version CASCADE');
|
|
212
212
|
};
|
|
@@ -840,7 +840,6 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
840
840
|
const route = find_auth_route(test_app.route_specs, suffix, suffix === '/tokens/create' || suffix === '/sessions/revoke-all' ? 'POST' : 'GET');
|
|
841
841
|
if (!route)
|
|
842
842
|
continue;
|
|
843
|
-
// eslint-disable-next-line no-await-in-loop
|
|
844
843
|
const res = await test_app.app.request(route.path, {
|
|
845
844
|
method: route.method,
|
|
846
845
|
headers: { host: 'localhost' },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,
|
|
1
|
+
{"version":3,"file":"rate_limiting.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rate_limiting.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAKjB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IA4N/E,CAAC"}
|
|
@@ -64,7 +64,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
64
64
|
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
65
65
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
66
66
|
// Fire max_attempts failed login requests (sequential — must exhaust the window)
|
|
67
|
-
/* eslint-disable no-await-in-loop */
|
|
68
67
|
for (let i = 0; i < max_attempts; i++) {
|
|
69
68
|
const res = await test_app.app.request(login_route.path, {
|
|
70
69
|
method: 'POST',
|
|
@@ -77,7 +76,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
77
76
|
});
|
|
78
77
|
assert.notStrictEqual(res.status, 429, `Request ${i + 1}/${max_attempts} should not be rate limited`);
|
|
79
78
|
}
|
|
80
|
-
/* eslint-enable no-await-in-loop */
|
|
81
79
|
// The next request should be rate limited
|
|
82
80
|
const blocked_res = await test_app.app.request(login_route.path, {
|
|
83
81
|
method: 'POST',
|
|
@@ -119,7 +117,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
119
117
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
120
118
|
const target_username = 'rate_limit_target';
|
|
121
119
|
// Fire max_attempts failed login requests for the same username
|
|
122
|
-
/* eslint-disable no-await-in-loop */
|
|
123
120
|
for (let i = 0; i < max_attempts; i++) {
|
|
124
121
|
const res = await test_app.app.request(login_route.path, {
|
|
125
122
|
method: 'POST',
|
|
@@ -132,7 +129,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
132
129
|
});
|
|
133
130
|
assert.notStrictEqual(res.status, 429, `Request ${i + 1}/${max_attempts} should not be rate limited`);
|
|
134
131
|
}
|
|
135
|
-
/* eslint-enable no-await-in-loop */
|
|
136
132
|
// The next request for the same username should be rate limited
|
|
137
133
|
const blocked_res = await test_app.app.request(login_route.path, {
|
|
138
134
|
method: 'POST',
|
|
@@ -184,7 +180,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
184
180
|
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
185
181
|
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
186
182
|
// Fire max_attempts invalid bearer requests (sequential — must exhaust the window)
|
|
187
|
-
/* eslint-disable no-await-in-loop */
|
|
188
183
|
for (let i = 0; i < max_attempts; i++) {
|
|
189
184
|
const res = await test_app.app.request(verify_route.path, {
|
|
190
185
|
headers: {
|
|
@@ -194,7 +189,6 @@ export const describe_rate_limiting_tests = (options) => {
|
|
|
194
189
|
});
|
|
195
190
|
assert.notStrictEqual(res.status, 429, `Request ${i + 1}/${max_attempts} should not be rate limited`);
|
|
196
191
|
}
|
|
197
|
-
/* eslint-enable no-await-in-loop */
|
|
198
192
|
// The next request should be rate limited
|
|
199
193
|
const blocked_res = await test_app.app.request(verify_route.path, {
|
|
200
194
|
headers: {
|