@fuzdev/fuz_app 0.14.0 → 0.16.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/register_action_ws.d.ts +108 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -0
- package/dist/actions/register_action_ws.js +187 -0
- package/dist/actions/transports_ws_auth_guard.d.ts +43 -0
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -0
- package/dist/actions/transports_ws_auth_guard.js +86 -0
- package/dist/actions/transports_ws_backend.d.ts +32 -3
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +36 -28
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +2 -1
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +2 -1
- package/dist/auth/keyring.d.ts +1 -1
- package/dist/auth/keyring.js +1 -1
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +5 -1
- package/dist/hono_context.d.ts +10 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +2 -0
- package/dist/realtime/sse_auth_guard.d.ts +7 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +10 -3
- package/dist/testing/middleware.d.ts +2 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +6 -1
- package/dist/ui/form_state.svelte.d.ts +1 -1
- package/dist/ui/form_state.svelte.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket JSON-RPC dispatch — the canonical WS transport binding.
|
|
3
|
+
*
|
|
4
|
+
* Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
|
|
5
|
+
* consumer supplies action specs + a handler map, the dispatcher parses the
|
|
6
|
+
* envelope, checks per-action auth, validates input, invokes the handler with
|
|
7
|
+
* a per-request context, and writes the response.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from zzz's `register_websocket_actions` to converge pattern drift
|
|
10
|
+
* across consumers (zzz, tx, undying). Broadcast-style notifications remain
|
|
11
|
+
* domain-shaped today — this module only covers per-request dispatch + the
|
|
12
|
+
* socket-scoped `ctx.notify` + per-socket `ctx.signal`. See
|
|
13
|
+
* `BackendWebsocketTransport.send` for broadcast.
|
|
14
|
+
*
|
|
15
|
+
* ## Auth expectations
|
|
16
|
+
*
|
|
17
|
+
* The consumer is responsible for rejecting unauthenticated upgrades *before*
|
|
18
|
+
* routing to this handler (fuz_app's `require_auth` middleware). Inside the
|
|
19
|
+
* dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
|
|
20
|
+
* per-action auth is enforced on each message.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { Context, Hono } from 'hono';
|
|
25
|
+
import type { UpgradeWebSocket } from 'hono/ws';
|
|
26
|
+
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.js';
|
|
27
|
+
import { type JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
28
|
+
import type { ActionSpecUnion } from './action_spec.js';
|
|
29
|
+
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
30
|
+
/**
|
|
31
|
+
* Minimum per-request context every handler receives.
|
|
32
|
+
*
|
|
33
|
+
* Consumers extend this with domain-specific fields via
|
|
34
|
+
* `RegisterActionWsOptions.extend_context` (e.g., a `backend` singleton
|
|
35
|
+
* or the authenticated `RequestContext`). Keeping the base minimal matches
|
|
36
|
+
* the HTTP-side `ActionContext` (from `actions/action_rpc.ts`) and mirrors
|
|
37
|
+
* Rust's `Ctx<'a>` shape (`request_id` + `NotifyFn` + `CancellationToken`).
|
|
38
|
+
*/
|
|
39
|
+
export interface BaseHandlerContext {
|
|
40
|
+
/** JSON-RPC envelope request id — echoed back on the response. */
|
|
41
|
+
request_id: JsonrpcRequestId;
|
|
42
|
+
/**
|
|
43
|
+
* Send a request-scoped JSON-RPC notification to the originating socket.
|
|
44
|
+
* Not a broadcast — the message only reaches the client whose request
|
|
45
|
+
* triggered this handler. Streaming handlers (e.g. `completion_progress`)
|
|
46
|
+
* route chunks through this.
|
|
47
|
+
*/
|
|
48
|
+
notify: (method: string, params: unknown) => void;
|
|
49
|
+
/** Fires on socket close; streaming handlers poll for early termination. */
|
|
50
|
+
signal: AbortSignal;
|
|
51
|
+
}
|
|
52
|
+
/** Handler signature — receives validated input and per-request context. */
|
|
53
|
+
export type WsActionHandler<TCtx extends BaseHandlerContext> = (input: unknown, ctx: TCtx) => unknown;
|
|
54
|
+
/** Options for `register_action_ws`. */
|
|
55
|
+
export interface RegisterActionWsOptions<TCtx extends BaseHandlerContext> {
|
|
56
|
+
/** Mount path (e.g., `/api/ws`). */
|
|
57
|
+
path: string;
|
|
58
|
+
/** The Hono app to mount on. */
|
|
59
|
+
app: Hono;
|
|
60
|
+
/** Hono's `upgradeWebSocket` helper from the runtime adapter. */
|
|
61
|
+
upgradeWebSocket: UpgradeWebSocket;
|
|
62
|
+
/** Action specs — drives method lookup, per-action auth, and input/output validation. */
|
|
63
|
+
specs: ReadonlyArray<ActionSpecUnion>;
|
|
64
|
+
/** Handler map keyed by `spec.method`. */
|
|
65
|
+
handlers: Record<string, WsActionHandler<TCtx>>;
|
|
66
|
+
/**
|
|
67
|
+
* Build the per-request context from the base and the upgrade-time Hono
|
|
68
|
+
* context. Called once per incoming message. Consumers use this to attach
|
|
69
|
+
* domain singletons (`backend`) or per-socket auth (`auth`,
|
|
70
|
+
* `credential_type`) without re-reading them from `c` inside every handler.
|
|
71
|
+
*/
|
|
72
|
+
extend_context: (base: BaseHandlerContext, c: Context) => TCtx;
|
|
73
|
+
/**
|
|
74
|
+
* Existing transport to register connections with. When omitted, a fresh
|
|
75
|
+
* one is created and returned in the result. Pass your own to keep a
|
|
76
|
+
* handle for `create_ws_auth_guard` and `send_to`/`broadcast`.
|
|
77
|
+
*/
|
|
78
|
+
transport?: BackendWebsocketTransport;
|
|
79
|
+
/** Optional per-message delay for testing loading states. Ignored when `0`. */
|
|
80
|
+
artificial_delay?: number;
|
|
81
|
+
/** Optional logger; defaults to `[ws]` namespace. */
|
|
82
|
+
log?: LoggerType;
|
|
83
|
+
}
|
|
84
|
+
/** Result of `register_action_ws`. */
|
|
85
|
+
export interface RegisterActionWsResult {
|
|
86
|
+
/** The transport bound to the endpoint — supplied or freshly created. */
|
|
87
|
+
transport: BackendWebsocketTransport;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mount a JSON-RPC WebSocket endpoint that dispatches to the supplied handler
|
|
91
|
+
* map. Per-request context is built from the base + consumer-provided
|
|
92
|
+
* `RegisterActionWsOptions.extend_context`.
|
|
93
|
+
*
|
|
94
|
+
* Wire behavior:
|
|
95
|
+
* - Batch JSON-RPC is rejected (single-message only).
|
|
96
|
+
* - Notifications (method + no id) are silently dropped per JSON-RPC spec.
|
|
97
|
+
* - Per-action auth: `public` / `authenticated` pass through (upgrade auth
|
|
98
|
+
* already verified identity); `keeper` requires `daemon_token` credential
|
|
99
|
+
* type *and* the keeper role; role-based `{role}` is currently rejected as
|
|
100
|
+
* not-yet-supported.
|
|
101
|
+
* - DEV mode validates handler output against the spec's `output` schema and
|
|
102
|
+
* warns on mismatches.
|
|
103
|
+
*
|
|
104
|
+
* @returns the transport (supplied or freshly created) — retain it to wire
|
|
105
|
+
* `create_ws_auth_guard` or broadcast on audit events.
|
|
106
|
+
*/
|
|
107
|
+
export declare const register_action_ws: <TCtx extends BaseHandlerContext>(options: RegisterActionWsOptions<TCtx>) => RegisterActionWsResult;
|
|
108
|
+
//# sourceMappingURL=register_action_ws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAE9C,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAK1E,OAAO,EAAkB,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAY1E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAErE;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,4EAA4E;IAC5E,MAAM,EAAE,WAAW,CAAC;CACpB;AAED,4EAA4E;AAC5E,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,kBAAkB,IAAI,CAC9D,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,IAAI,KACL,OAAO,CAAC;AAEb,wCAAwC;AACxC,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,kBAAkB;IACvE,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,yFAAyF;IACzF,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IACtC,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IAChD;;;;;OAKG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,kBAAkB,EACjE,SAAS,uBAAuB,CAAC,IAAI,CAAC,KACpC,sBA0NF,CAAC"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket JSON-RPC dispatch — the canonical WS transport binding.
|
|
3
|
+
*
|
|
4
|
+
* Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
|
|
5
|
+
* consumer supplies action specs + a handler map, the dispatcher parses the
|
|
6
|
+
* envelope, checks per-action auth, validates input, invokes the handler with
|
|
7
|
+
* a per-request context, and writes the response.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from zzz's `register_websocket_actions` to converge pattern drift
|
|
10
|
+
* across consumers (zzz, tx, undying). Broadcast-style notifications remain
|
|
11
|
+
* domain-shaped today — this module only covers per-request dispatch + the
|
|
12
|
+
* socket-scoped `ctx.notify` + per-socket `ctx.signal`. See
|
|
13
|
+
* `BackendWebsocketTransport.send` for broadcast.
|
|
14
|
+
*
|
|
15
|
+
* ## Auth expectations
|
|
16
|
+
*
|
|
17
|
+
* The consumer is responsible for rejecting unauthenticated upgrades *before*
|
|
18
|
+
* routing to this handler (fuz_app's `require_auth` middleware). Inside the
|
|
19
|
+
* dispatcher, `get_request_context(c)` is treated as guaranteed non-null and
|
|
20
|
+
* per-action auth is enforced on each message.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { DEV } from 'esm-env';
|
|
25
|
+
import { wait } from '@fuzdev/fuz_util/async.js';
|
|
26
|
+
import { Logger } from '@fuzdev/fuz_util/log.js';
|
|
27
|
+
import { get_request_context, has_role } from '../auth/request_context.js';
|
|
28
|
+
import { hash_session_token } from '../auth/session_queries.js';
|
|
29
|
+
import { ROLE_KEEPER } from '../auth/role_schema.js';
|
|
30
|
+
import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
31
|
+
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
32
|
+
import { create_jsonrpc_error_response, create_jsonrpc_error_response_from_thrown, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
|
|
33
|
+
import { CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY } from '../hono_context.js';
|
|
34
|
+
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
35
|
+
/**
|
|
36
|
+
* Mount a JSON-RPC WebSocket endpoint that dispatches to the supplied handler
|
|
37
|
+
* map. Per-request context is built from the base + consumer-provided
|
|
38
|
+
* `RegisterActionWsOptions.extend_context`.
|
|
39
|
+
*
|
|
40
|
+
* Wire behavior:
|
|
41
|
+
* - Batch JSON-RPC is rejected (single-message only).
|
|
42
|
+
* - Notifications (method + no id) are silently dropped per JSON-RPC spec.
|
|
43
|
+
* - Per-action auth: `public` / `authenticated` pass through (upgrade auth
|
|
44
|
+
* already verified identity); `keeper` requires `daemon_token` credential
|
|
45
|
+
* type *and* the keeper role; role-based `{role}` is currently rejected as
|
|
46
|
+
* not-yet-supported.
|
|
47
|
+
* - DEV mode validates handler output against the spec's `output` schema and
|
|
48
|
+
* warns on mismatches.
|
|
49
|
+
*
|
|
50
|
+
* @returns the transport (supplied or freshly created) — retain it to wire
|
|
51
|
+
* `create_ws_auth_guard` or broadcast on audit events.
|
|
52
|
+
*/
|
|
53
|
+
export const register_action_ws = (options) => {
|
|
54
|
+
const { path, app, upgradeWebSocket, specs, handlers, extend_context, transport = new BackendWebsocketTransport(), artificial_delay = 0, log = new Logger('[ws]'), } = options;
|
|
55
|
+
// Build spec lookup for per-action auth and input validation.
|
|
56
|
+
const spec_by_method = new Map(specs.map((spec) => [spec.method, spec]));
|
|
57
|
+
app.get(path, upgradeWebSocket((c) => {
|
|
58
|
+
// Upgrade-time auth extraction — `require_auth` middleware has already
|
|
59
|
+
// rejected unauthenticated requests, so request_context is guaranteed
|
|
60
|
+
// non-null by the time we get here.
|
|
61
|
+
const request_context = get_request_context(c);
|
|
62
|
+
const account_id = request_context.account.id;
|
|
63
|
+
const credential_type = c.get(CREDENTIAL_TYPE_KEY);
|
|
64
|
+
// Session-based connections have a token hash for targeted revocation.
|
|
65
|
+
// Bearer/daemon connections pass null — still reachable via
|
|
66
|
+
// `close_sockets_for_account` / `close_sockets_for_token`.
|
|
67
|
+
const token_hash = credential_type === 'session' ? hash_session_token(c.get('auth_session_id')) : null;
|
|
68
|
+
// `api_token.id` — set only for bearer connections; enables
|
|
69
|
+
// `close_sockets_for_token` to tear down just this socket on
|
|
70
|
+
// `token_revoke` without affecting the account's other sockets.
|
|
71
|
+
const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY);
|
|
72
|
+
// Per-socket abort controller — fires on socket close, threaded into
|
|
73
|
+
// every in-flight handler's ctx.signal on this connection. A
|
|
74
|
+
// dedicated per-request controller linked to this is future work;
|
|
75
|
+
// a single socket-scoped signal is sufficient today since cancel
|
|
76
|
+
// granularity tracks connection lifetime, not individual requests.
|
|
77
|
+
const socket_abort_controller = new AbortController();
|
|
78
|
+
return {
|
|
79
|
+
onOpen: (event, ws) => {
|
|
80
|
+
const connection_id = transport.add_connection(ws, token_hash, account_id, api_token_id);
|
|
81
|
+
log.debug('ws opened', connection_id, event);
|
|
82
|
+
},
|
|
83
|
+
onMessage: async (event, ws) => {
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = JSON.parse(String(event.data)); // eslint-disable-line @typescript-eslint/no-base-to-string
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
log.error('JSON parse error:', error);
|
|
90
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.parse_error())));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Batch JSON-RPC is not supported on the WebSocket path.
|
|
94
|
+
if (Array.isArray(json)) {
|
|
95
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'))));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Only handle requests (method + id). Notifications (no id) are silenced per JSON-RPC spec.
|
|
99
|
+
if (!is_jsonrpc_request(json)) {
|
|
100
|
+
if (typeof json === 'object' && json !== null && 'method' in json && !('id' in json)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request())));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const { method, id, params } = json;
|
|
107
|
+
// Per-action auth check — enforce auth level from spec.
|
|
108
|
+
const spec = spec_by_method.get(method);
|
|
109
|
+
if (!spec) {
|
|
110
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.method_not_found(method))));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { auth } = spec;
|
|
114
|
+
if (auth === 'keeper') {
|
|
115
|
+
if (credential_type !== 'daemon_token' || !has_role(request_context, ROLE_KEEPER)) {
|
|
116
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.forbidden('keeper actions require daemon_token credential with keeper role'))));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (typeof auth === 'object' && auth !== null) {
|
|
121
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.internal_error('role-based action auth is not yet supported on WebSocket'))));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Look up handler — method is validated against spec_by_method above.
|
|
125
|
+
const handler = handlers[method];
|
|
126
|
+
if (!handler) {
|
|
127
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.method_not_found(method))));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Validate input against spec schema.
|
|
131
|
+
const parsed = spec.input.safeParse(params);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.invalid_params(`invalid params for ${method}`, {
|
|
134
|
+
issues: parsed.error.issues,
|
|
135
|
+
}))));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const validated_input = parsed.data;
|
|
139
|
+
if (artificial_delay > 0) {
|
|
140
|
+
log.debug(`throttling ${artificial_delay}ms`);
|
|
141
|
+
await wait(artificial_delay);
|
|
142
|
+
}
|
|
143
|
+
// Socket-scoped notification — routes to originator only, not
|
|
144
|
+
// broadcast. Future work (websockets quest Phase 3/4): other
|
|
145
|
+
// audiences — account-scoped, ACL-filtered, broadcast —
|
|
146
|
+
// likely via a transport-level policy hook.
|
|
147
|
+
const notify = (notify_method, notify_params) => {
|
|
148
|
+
try {
|
|
149
|
+
const notification = create_jsonrpc_notification(notify_method, to_jsonrpc_params(notify_params));
|
|
150
|
+
ws.send(JSON.stringify(notification));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error('notify send failed:', notify_method, error);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const base = {
|
|
157
|
+
request_id: id,
|
|
158
|
+
notify,
|
|
159
|
+
signal: socket_abort_controller.signal,
|
|
160
|
+
};
|
|
161
|
+
const ctx = extend_context(base, c);
|
|
162
|
+
try {
|
|
163
|
+
const output = await handler(validated_input, ctx);
|
|
164
|
+
// DEV-only output validation — catches handler bugs during development.
|
|
165
|
+
if (DEV) {
|
|
166
|
+
const output_parsed = spec.output.safeParse(output);
|
|
167
|
+
if (!output_parsed.success) {
|
|
168
|
+
log.error(`output validation failed for ${method}:`, output_parsed.error.issues);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Send result directly — null stays null, matching the HTTP RPC path.
|
|
172
|
+
ws.send(JSON.stringify({ jsonrpc: JSONRPC_VERSION, id, result: output }));
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
log.error('handler error:', method, error);
|
|
176
|
+
ws.send(JSON.stringify(create_jsonrpc_error_response_from_thrown(id, error)));
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
onClose: (event, ws) => {
|
|
180
|
+
socket_abort_controller.abort();
|
|
181
|
+
transport.remove_connection(ws);
|
|
182
|
+
log.debug('ws closed', event);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}));
|
|
186
|
+
return { transport };
|
|
187
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket auth guard — bridges audit events to `BackendWebsocketTransport`.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket transport.
|
|
5
|
+
* Dispatches audit events to the right `close_sockets_for_*` method so
|
|
6
|
+
* consumers do not re-implement the switch themselves.
|
|
7
|
+
*
|
|
8
|
+
* Consumers wire it as `on_audit_event` on their `AppBackend` (or compose
|
|
9
|
+
* it with other callbacks via `create_app_server`'s `audit_log_sse` path).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { Logger } from '@fuzdev/fuz_util/log.js';
|
|
14
|
+
import type { AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
15
|
+
import type { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
16
|
+
/**
|
|
17
|
+
* Audit event types that trigger WebSocket socket closure.
|
|
18
|
+
*
|
|
19
|
+
* - `session_revoke` — close only the socket tied to the revoked session hash.
|
|
20
|
+
* - `token_revoke` — close only the socket(s) authenticated with the revoked `api_token.id`.
|
|
21
|
+
* - `session_revoke_all` / `token_revoke_all` / `password_change` — close every socket
|
|
22
|
+
* for the affected account (all credentials invalidated).
|
|
23
|
+
*
|
|
24
|
+
* `permit_revoke` is intentionally omitted: the WS transport does not track
|
|
25
|
+
* per-connection role requirements, so role-scoped disconnection would
|
|
26
|
+
* require either closing all sockets (too aggressive) or new tracking
|
|
27
|
+
* (out of scope). Consumers that need it compose their own callback.
|
|
28
|
+
*/
|
|
29
|
+
export declare const WS_DISCONNECT_EVENT_TYPES: ReadonlySet<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Create an audit event handler that closes WebSocket connections on auth changes.
|
|
32
|
+
*
|
|
33
|
+
* Ignores `outcome === 'failure'` events — they carry attacker-controlled
|
|
34
|
+
* identifiers (e.g. a `session_revoke` that the DB rejected still records
|
|
35
|
+
* the submitted session_id), so reacting to them would let any authenticated
|
|
36
|
+
* user close another user's socket by guessing a session hash or token id.
|
|
37
|
+
*
|
|
38
|
+
* @param transport - the backend WebSocket transport to guard
|
|
39
|
+
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
40
|
+
* @returns an `on_audit_event` callback suitable for `CreateAppBackendOptions`
|
|
41
|
+
*/
|
|
42
|
+
export declare const create_ws_auth_guard: (transport: BackendWebsocketTransport, log: Logger) => ((event: AuditLogEvent) => void);
|
|
43
|
+
//# sourceMappingURL=transports_ws_auth_guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAE/D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAMxD,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket auth guard — bridges audit events to `BackendWebsocketTransport`.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket transport.
|
|
5
|
+
* Dispatches audit events to the right `close_sockets_for_*` method so
|
|
6
|
+
* consumers do not re-implement the switch themselves.
|
|
7
|
+
*
|
|
8
|
+
* Consumers wire it as `on_audit_event` on their `AppBackend` (or compose
|
|
9
|
+
* it with other callbacks via `create_app_server`'s `audit_log_sse` path).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Audit event types that trigger WebSocket socket closure.
|
|
15
|
+
*
|
|
16
|
+
* - `session_revoke` — close only the socket tied to the revoked session hash.
|
|
17
|
+
* - `token_revoke` — close only the socket(s) authenticated with the revoked `api_token.id`.
|
|
18
|
+
* - `session_revoke_all` / `token_revoke_all` / `password_change` — close every socket
|
|
19
|
+
* for the affected account (all credentials invalidated).
|
|
20
|
+
*
|
|
21
|
+
* `permit_revoke` is intentionally omitted: the WS transport does not track
|
|
22
|
+
* per-connection role requirements, so role-scoped disconnection would
|
|
23
|
+
* require either closing all sockets (too aggressive) or new tracking
|
|
24
|
+
* (out of scope). Consumers that need it compose their own callback.
|
|
25
|
+
*/
|
|
26
|
+
export const WS_DISCONNECT_EVENT_TYPES = new Set([
|
|
27
|
+
'session_revoke',
|
|
28
|
+
'token_revoke',
|
|
29
|
+
'session_revoke_all',
|
|
30
|
+
'token_revoke_all',
|
|
31
|
+
'password_change',
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Create an audit event handler that closes WebSocket connections on auth changes.
|
|
35
|
+
*
|
|
36
|
+
* Ignores `outcome === 'failure'` events — they carry attacker-controlled
|
|
37
|
+
* identifiers (e.g. a `session_revoke` that the DB rejected still records
|
|
38
|
+
* the submitted session_id), so reacting to them would let any authenticated
|
|
39
|
+
* user close another user's socket by guessing a session hash or token id.
|
|
40
|
+
*
|
|
41
|
+
* @param transport - the backend WebSocket transport to guard
|
|
42
|
+
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
43
|
+
* @returns an `on_audit_event` callback suitable for `CreateAppBackendOptions`
|
|
44
|
+
*/
|
|
45
|
+
export const create_ws_auth_guard = (transport, log) => {
|
|
46
|
+
return (event) => {
|
|
47
|
+
if (!WS_DISCONNECT_EVENT_TYPES.has(event.event_type))
|
|
48
|
+
return;
|
|
49
|
+
// Failed mutations carry attacker-controlled metadata — never act on them.
|
|
50
|
+
if (event.outcome === 'failure')
|
|
51
|
+
return;
|
|
52
|
+
if (event.event_type === 'session_revoke') {
|
|
53
|
+
const session_id = event.metadata?.session_id;
|
|
54
|
+
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const closed = transport.close_sockets_for_session(session_id);
|
|
57
|
+
if (closed > 0) {
|
|
58
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for session ${session_id} (session_revoke)`);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (event.event_type === 'token_revoke') {
|
|
63
|
+
const token_id = event.metadata?.token_id;
|
|
64
|
+
if (typeof token_id !== 'string' || token_id.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const closed = transport.close_sockets_for_token(token_id);
|
|
67
|
+
if (closed > 0) {
|
|
68
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for token ${token_id} (token_revoke)`);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// session_revoke_all / token_revoke_all / password_change — all of the
|
|
73
|
+
// account's credentials are invalidated; close every socket on the account.
|
|
74
|
+
// Admin actions set `target_account_id`; self-service actions only set `account_id`.
|
|
75
|
+
const target = event.target_account_id ?? event.account_id;
|
|
76
|
+
if (!target)
|
|
77
|
+
return;
|
|
78
|
+
// `target` is a DB account id (string); the transport's account map is
|
|
79
|
+
// keyed by the branded `Uuid` used elsewhere in fuz_app. Same value,
|
|
80
|
+
// differing type disciplines across the audit-log and transport layers.
|
|
81
|
+
const closed = transport.close_sockets_for_account(target);
|
|
82
|
+
if (closed > 0) {
|
|
83
|
+
log.info(`WS auth guard: closed ${closed} socket(s) for account ${target} (${event.event_type})`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -8,16 +8,35 @@ import type { WSContext } from 'hono/ws';
|
|
|
8
8
|
import type { JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
9
9
|
import { type Uuid } from '../uuid.js';
|
|
10
10
|
import { type Transport } from './transports.js';
|
|
11
|
+
/**
|
|
12
|
+
* Auth identity attached to a single WebSocket connection.
|
|
13
|
+
*
|
|
14
|
+
* One record per connection. `token_hash` is set for cookie-session
|
|
15
|
+
* connections, `api_token_id` for bearer (`api_token`) connections, and
|
|
16
|
+
* both are null for daemon-token connections (reachable only via
|
|
17
|
+
* `BackendWebsocketTransport.close_sockets_for_account`).
|
|
18
|
+
*/
|
|
19
|
+
export interface ConnectionIdentity {
|
|
20
|
+
/** Blake3 session token hash, or null for non-session credentials. */
|
|
21
|
+
token_hash: string | null;
|
|
22
|
+
/** Authenticated account id. Always set. */
|
|
23
|
+
account_id: Uuid;
|
|
24
|
+
/** `api_token.id` for bearer-authenticated connections, else null. */
|
|
25
|
+
api_token_id: string | null;
|
|
26
|
+
}
|
|
11
27
|
export declare class BackendWebsocketTransport implements Transport {
|
|
12
28
|
#private;
|
|
13
29
|
readonly transport_name: "backend_websocket_rpc";
|
|
14
30
|
/**
|
|
15
31
|
* Add a new WebSocket connection with auth info.
|
|
16
32
|
* Session connections pass a token hash for targeted revocation.
|
|
17
|
-
* Bearer token connections (api_token
|
|
18
|
-
*
|
|
33
|
+
* Bearer token connections (api_token) pass the `api_token.id` so the
|
|
34
|
+
* socket can be closed when that specific token is revoked without
|
|
35
|
+
* tearing down the account's other sockets. Daemon-token connections
|
|
36
|
+
* pass `null` for both — they're only reachable via
|
|
37
|
+
* `close_sockets_for_account`.
|
|
19
38
|
*/
|
|
20
|
-
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid): Uuid;
|
|
39
|
+
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null): Uuid;
|
|
21
40
|
/**
|
|
22
41
|
* Remove a WebSocket connection and its auth tracking data.
|
|
23
42
|
* Idempotent — safe to call after revocation has already cleaned up.
|
|
@@ -35,6 +54,16 @@ export declare class BackendWebsocketTransport implements Transport {
|
|
|
35
54
|
* @returns the number of sockets closed
|
|
36
55
|
*/
|
|
37
56
|
close_sockets_for_account(account_id: Uuid): number;
|
|
57
|
+
/**
|
|
58
|
+
* Close all sockets associated with a specific API token.
|
|
59
|
+
*
|
|
60
|
+
* Used on `token_revoke` audit events so revoking one token doesn't
|
|
61
|
+
* tear down the account's session-authenticated sockets or other
|
|
62
|
+
* tokens' sockets.
|
|
63
|
+
*
|
|
64
|
+
* @returns the number of sockets closed
|
|
65
|
+
*/
|
|
66
|
+
close_sockets_for_token(api_token_id: string): number;
|
|
38
67
|
send(message: JsonrpcRequest): Promise<JsonrpcResponseOrError>;
|
|
39
68
|
send(message: JsonrpcNotification): Promise<JsonrpcErrorResponse | null>;
|
|
40
69
|
is_ready(): boolean;
|
|
@@ -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,EAGX,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;AAIzE,qBAAa,yBAA0B,YAAW,SAAS;;IAC1D,QAAQ,CAAC,cAAc,EAAG,uBAAuB,CAAU;IAY3D
|
|
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,EAGX,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;AAIzE;;;;;;;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,qBAAa,yBAA0B,YAAW,SAAS;;IAC1D,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,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA4C9E,QAAQ,IAAI,OAAO;CAGnB"}
|
|
@@ -8,30 +8,29 @@ import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
|
8
8
|
import { create_jsonrpc_error_response, to_jsonrpc_message_id, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
|
|
9
9
|
import { create_uuid } from '../uuid.js';
|
|
10
10
|
import { WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
11
|
-
// TODO support a SSE backend transport
|
|
12
11
|
export class BackendWebsocketTransport {
|
|
13
12
|
transport_name = 'backend_websocket_rpc';
|
|
14
13
|
// Map connection IDs to WebSocket contexts
|
|
15
14
|
#connections = new Map();
|
|
16
15
|
// Reverse map to find connection ID by socket
|
|
17
16
|
#connection_ids = new WeakMap();
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
#
|
|
17
|
+
// Auth identity per connection. Adding a new identity scope (e.g.
|
|
18
|
+
// `device_id`) means adding a field here, not a new parallel map.
|
|
19
|
+
#connection_identities = new Map();
|
|
21
20
|
/**
|
|
22
21
|
* Add a new WebSocket connection with auth info.
|
|
23
22
|
* Session connections pass a token hash for targeted revocation.
|
|
24
|
-
* Bearer token connections (api_token
|
|
25
|
-
*
|
|
23
|
+
* Bearer token connections (api_token) pass the `api_token.id` so the
|
|
24
|
+
* socket can be closed when that specific token is revoked without
|
|
25
|
+
* tearing down the account's other sockets. Daemon-token connections
|
|
26
|
+
* pass `null` for both — they're only reachable via
|
|
27
|
+
* `close_sockets_for_account`.
|
|
26
28
|
*/
|
|
27
|
-
add_connection(ws, token_hash, account_id) {
|
|
29
|
+
add_connection(ws, token_hash, account_id, api_token_id = null) {
|
|
28
30
|
const connection_id = create_uuid();
|
|
29
31
|
this.#connections.set(connection_id, ws);
|
|
30
32
|
this.#connection_ids.set(ws, connection_id);
|
|
31
|
-
|
|
32
|
-
this.#connection_token_hashes.set(connection_id, token_hash);
|
|
33
|
-
}
|
|
34
|
-
this.#connection_account_ids.set(connection_id, account_id);
|
|
33
|
+
this.#connection_identities.set(connection_id, { token_hash, account_id, api_token_id });
|
|
35
34
|
return connection_id;
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
@@ -45,14 +44,14 @@ export class BackendWebsocketTransport {
|
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
/**
|
|
48
|
-
* Close
|
|
47
|
+
* Close every connection whose identity matches the predicate.
|
|
49
48
|
*
|
|
50
49
|
* @returns the number of sockets closed
|
|
51
50
|
*/
|
|
52
|
-
|
|
51
|
+
#close_where(predicate) {
|
|
53
52
|
let count = 0;
|
|
54
|
-
for (const [connection_id,
|
|
55
|
-
if (
|
|
53
|
+
for (const [connection_id, identity] of this.#connection_identities) {
|
|
54
|
+
if (predicate(identity)) {
|
|
56
55
|
const ws = this.#connections.get(connection_id);
|
|
57
56
|
if (ws) {
|
|
58
57
|
this.#revoke_connection(connection_id, ws);
|
|
@@ -62,23 +61,33 @@ export class BackendWebsocketTransport {
|
|
|
62
61
|
}
|
|
63
62
|
return count;
|
|
64
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Close all sockets associated with a specific session token hash.
|
|
66
|
+
*
|
|
67
|
+
* @returns the number of sockets closed
|
|
68
|
+
*/
|
|
69
|
+
close_sockets_for_session(token_hash) {
|
|
70
|
+
return this.#close_where((id) => id.token_hash === token_hash);
|
|
71
|
+
}
|
|
65
72
|
/**
|
|
66
73
|
* Close all sockets associated with a specific account.
|
|
67
74
|
*
|
|
68
75
|
* @returns the number of sockets closed
|
|
69
76
|
*/
|
|
70
77
|
close_sockets_for_account(account_id) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
return this.#close_where((id) => id.account_id === account_id);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Close all sockets associated with a specific API token.
|
|
82
|
+
*
|
|
83
|
+
* Used on `token_revoke` audit events so revoking one token doesn't
|
|
84
|
+
* tear down the account's session-authenticated sockets or other
|
|
85
|
+
* tokens' sockets.
|
|
86
|
+
*
|
|
87
|
+
* @returns the number of sockets closed
|
|
88
|
+
*/
|
|
89
|
+
close_sockets_for_token(api_token_id) {
|
|
90
|
+
return this.#close_where((id) => id.api_token_id === api_token_id);
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
84
93
|
* Remove all tracking state for a connection.
|
|
@@ -86,8 +95,7 @@ export class BackendWebsocketTransport {
|
|
|
86
95
|
#cleanup_connection(connection_id, ws) {
|
|
87
96
|
this.#connections.delete(connection_id);
|
|
88
97
|
this.#connection_ids.delete(ws);
|
|
89
|
-
this.#
|
|
90
|
-
this.#connection_account_ids.delete(connection_id);
|
|
98
|
+
this.#connection_identities.delete(connection_id);
|
|
91
99
|
}
|
|
92
100
|
/**
|
|
93
101
|
* Clean up a connection and close its socket with a revocation code.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,
|
|
1
|
+
{"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,iBAAiB,WAAW,GAAG,IAAI,EACnC,KAAK,MAAM,KACT,iBAsFF,CAAC"}
|
package/dist/auth/bearer_auth.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
import { REQUEST_CONTEXT_KEY, build_request_context } from './request_context.js';
|
|
14
|
-
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
14
|
+
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
15
15
|
import { query_validate_api_token } from './api_token_queries.js';
|
|
16
16
|
import { get_client_ip } from '../http/proxy.js';
|
|
17
17
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
@@ -107,6 +107,7 @@ export const create_bearer_auth_middleware = (deps, ip_rate_limiter, log) => {
|
|
|
107
107
|
}
|
|
108
108
|
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
109
109
|
c.set(CREDENTIAL_TYPE_KEY, 'api_token');
|
|
110
|
+
c.set(AUTH_API_TOKEN_ID_KEY, api_token.id);
|
|
110
111
|
await next();
|
|
111
112
|
};
|
|
112
113
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAWrF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAC1D,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IAC3D,6FAA6F;IAC7F,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,MAAM,KACV,MAAM,GAAG,IAGX,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAS,oBAAoB,EAC7B,YAAY,MAAM,EAClB,OAAO,MAAM,KACX,OAAO,CAAC,IAAI,CAKd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAEtF,CAAC;AAEF,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IAC1C,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IACnC,2EAA2E;IAC3E,KAAK,EAAE,gBAAgB,CAAC;IACxB,kGAAkG;IAClG,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,oBAAoB,GAAG,YAAY,EAC5C,MAAM,SAAS,EACf,SAAS,0BAA0B,EACnC,KAAK,MAAM,KACT,OAAO,CAAC,mBAAmB,CAwD7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,KACb,
|
|
1
|
+
{"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAWrF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAC1D,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IAC3D,6FAA6F;IAC7F,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,MAAM,KACV,MAAM,GAAG,IAGX,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAS,oBAAoB,EAC7B,YAAY,MAAM,EAClB,OAAO,MAAM,KACX,OAAO,CAAC,IAAI,CAKd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAEtF,CAAC;AAEF,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IAC1C,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IACnC,2EAA2E;IAC3E,KAAK,EAAE,gBAAgB,CAAC;IACxB,kGAAkG;IAClG,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,oBAAoB,GAAG,YAAY,EAC5C,MAAM,SAAS,EACf,SAAS,0BAA0B,EACnC,KAAK,MAAM,KACT,OAAO,CAAC,mBAAmB,CAwD7B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,KACb,iBAqCF,CAAC"}
|
|
@@ -13,7 +13,7 @@ import {} from '../runtime/deps.js';
|
|
|
13
13
|
import { write_file_atomic } from '../runtime/fs.js';
|
|
14
14
|
import { get_app_dir } from '../cli/config.js';
|
|
15
15
|
import { REQUEST_CONTEXT_KEY, build_request_context } from './request_context.js';
|
|
16
|
-
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
16
|
+
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
17
17
|
import { ERROR_INVALID_DAEMON_TOKEN, ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED, ERROR_KEEPER_ACCOUNT_NOT_FOUND, } from '../http/error_schemas.js';
|
|
18
18
|
import { query_permit_find_account_id_for_role } from './permit_queries.js';
|
|
19
19
|
import { ROLE_KEEPER } from './role_schema.js';
|
|
@@ -162,6 +162,7 @@ export const create_daemon_token_middleware = (state, deps) => {
|
|
|
162
162
|
}
|
|
163
163
|
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
164
164
|
c.set(CREDENTIAL_TYPE_KEY, 'daemon_token');
|
|
165
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
165
166
|
await next();
|
|
166
167
|
};
|
|
167
168
|
};
|
package/dist/auth/keyring.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export declare const create_keyring: (env_value: string | undefined) => Keyring
|
|
|
61
61
|
*
|
|
62
62
|
* Returns an error when no keys are configured (undefined, empty string,
|
|
63
63
|
* or all-separator input like `'____'`), and for each key shorter than
|
|
64
|
-
*
|
|
64
|
+
* `MIN_KEY_LENGTH` characters.
|
|
65
65
|
*
|
|
66
66
|
* @param env_value - the SECRET_COOKIE_KEYS environment variable
|
|
67
67
|
* @returns array of validation errors (empty if valid)
|
package/dist/auth/keyring.js
CHANGED
|
@@ -75,7 +75,7 @@ export const create_keyring = (env_value) => {
|
|
|
75
75
|
*
|
|
76
76
|
* Returns an error when no keys are configured (undefined, empty string,
|
|
77
77
|
* or all-separator input like `'____'`), and for each key shorter than
|
|
78
|
-
*
|
|
78
|
+
* `MIN_KEY_LENGTH` characters.
|
|
79
79
|
*
|
|
80
80
|
* @param env_value - the SECRET_COOKIE_KEYS environment variable
|
|
81
81
|
* @returns array of validation errors (empty if valid)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,OAAO,EAAE,KAAK,KAAK,EAAoB,KAAK,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAQ5F,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAOnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,4BAA4B,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,cAAc,GAAG,IAEjE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,OAAO,KAAG,cAMpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,cAAc,EAAE,MAAM,MAAM,EAAE,MAAK,IAAiB,KAAG,OAChB,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iCAAiC,GAC7C,MAAM,SAAS,EACf,KAAK,MAAM,EACX,4BAAuC,KACrC,
|
|
1
|
+
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,OAAO,EAAE,KAAK,KAAK,EAAoB,KAAK,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAQ5F,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAOnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,4BAA4B,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,cAAc,GAAG,IAEjE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GAAI,GAAG,OAAO,KAAG,cAMpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,cAAc,EAAE,MAAM,MAAM,EAAE,MAAK,IAAiB,KAAG,OAChB,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iCAAiC,GAC7C,MAAM,SAAS,EACf,KAAK,MAAM,EACX,4BAAuC,KACrC,iBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,iBAM1B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,iBAW3C,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,cAAc,EACnB,MAAM,SAAS,KACb,OAAO,CAAC,cAAc,CAGxB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAS/B,CAAC"}
|
|
@@ -15,7 +15,7 @@ import { is_permit_active } from './account_schema.js';
|
|
|
15
15
|
import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
|
|
16
16
|
import { query_actor_by_account, query_account_by_id } from './account_queries.js';
|
|
17
17
|
import { query_permit_find_active_for_actor } from './permit_queries.js';
|
|
18
|
-
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
18
|
+
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
19
19
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, } from '../http/error_schemas.js';
|
|
20
20
|
/** Hono context variable name for the request context. */
|
|
21
21
|
export const REQUEST_CONTEXT_KEY = 'request_context';
|
|
@@ -89,6 +89,7 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
89
89
|
c.set(REQUEST_CONTEXT_KEY, null);
|
|
90
90
|
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
91
91
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
92
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
92
93
|
await next();
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
@@ -98,6 +99,7 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
98
99
|
c.set(REQUEST_CONTEXT_KEY, null);
|
|
99
100
|
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
100
101
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
102
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
101
103
|
await next();
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
@@ -106,12 +108,14 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
106
108
|
c.set(REQUEST_CONTEXT_KEY, null);
|
|
107
109
|
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
108
110
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
111
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
109
112
|
await next();
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
112
115
|
c.set(REQUEST_CONTEXT_KEY, ctx);
|
|
113
116
|
c.set(CREDENTIAL_TYPE_KEY, 'session');
|
|
114
117
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
118
|
+
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
115
119
|
// Touch session (fire-and-forget, don't block the request)
|
|
116
120
|
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
117
121
|
await next();
|
package/dist/hono_context.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const CredentialType: z.ZodEnum<{
|
|
|
26
26
|
export type CredentialType = z.infer<typeof CredentialType>;
|
|
27
27
|
/** Hono context variable name for the credential type. */
|
|
28
28
|
export declare const CREDENTIAL_TYPE_KEY = "credential_type";
|
|
29
|
+
/** Hono context variable name for the authenticated API token id. */
|
|
30
|
+
export declare const AUTH_API_TOKEN_ID_KEY = "auth_api_token_id";
|
|
29
31
|
declare module 'hono' {
|
|
30
32
|
interface ContextVariableMap {
|
|
31
33
|
/** Resolved client IP, set by the trusted proxy middleware. */
|
|
@@ -44,6 +46,14 @@ declare module 'hono' {
|
|
|
44
46
|
* disconnection) without re-hashing the cookie in every handler.
|
|
45
47
|
*/
|
|
46
48
|
auth_session_token_hash: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* `api_token.id` when the request authenticated via `Authorization: Bearer`,
|
|
51
|
+
* or `null` for session/daemon-token/unauthenticated requests. Set by
|
|
52
|
+
* `create_bearer_auth_middleware`. Used to scope per-token resources
|
|
53
|
+
* (e.g., WS connection revocation on `token_revoke`) without re-looking
|
|
54
|
+
* up the token.
|
|
55
|
+
*/
|
|
56
|
+
auth_api_token_id: string | null;
|
|
47
57
|
/**
|
|
48
58
|
* Pending fire-and-forget effects for this request (audit logs, usage tracking, etc.).
|
|
49
59
|
* Initialized by `create_app_server`. In test mode (`await_pending_effects: true`),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;WAIG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;KACtC;CACD"}
|
|
1
|
+
{"version":3,"file":"hono_context.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/hono_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,yDAAyD;AACzD,eAAO,MAAM,cAAc;;;;EAA2B,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD,OAAO,QAAQ,MAAM,CAAC;IACrB,UAAU,kBAAkB;QAC3B,+DAA+D;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC,eAAe,EAAE,OAAO,CAAC;QACzB,gBAAgB,EAAE,OAAO,CAAC;QAC1B,eAAe,EAAE,OAAO,CAAC;QACzB,2FAA2F;QAC3F,eAAe,EAAE,cAAc,GAAG,IAAI,CAAC;QACvC;;;;;WAKG;QACH,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;QACvC;;;;;;WAMG;QACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC;;;;WAIG;QACH,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;KACtC;CACD"}
|
package/dist/hono_context.js
CHANGED
|
@@ -20,3 +20,5 @@ export const CREDENTIAL_TYPES = ['session', 'api_token', 'daemon_token'];
|
|
|
20
20
|
export const CredentialType = z.enum(CREDENTIAL_TYPES);
|
|
21
21
|
/** Hono context variable name for the credential type. */
|
|
22
22
|
export const CREDENTIAL_TYPE_KEY = 'credential_type';
|
|
23
|
+
/** Hono context variable name for the authenticated API token id. */
|
|
24
|
+
export const AUTH_API_TOKEN_ID_KEY = 'auth_api_token_id';
|
|
@@ -17,7 +17,9 @@ import type { SseStream, SseNotification, EventSpec } from './sse.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Audit event types that trigger SSE stream disconnection.
|
|
19
19
|
*
|
|
20
|
-
* `permit_revoke` requires the revoked role to match the guard's `required_role
|
|
20
|
+
* `permit_revoke` requires the revoked role to match the guard's `required_role`
|
|
21
|
+
* (or is skipped entirely when `required_role` is `null` — useful for streams
|
|
22
|
+
* not gated by any specific permit).
|
|
21
23
|
* `session_revoke_all` and `password_change` close every stream for the target account.
|
|
22
24
|
* `session_revoke` closes only the stream tied to the specific revoked session
|
|
23
25
|
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
@@ -36,11 +38,13 @@ export declare const DISCONNECT_EVENT_TYPES: ReadonlySet<string>;
|
|
|
36
38
|
* (passed as the third argument to `registry.subscribe()`).
|
|
37
39
|
*
|
|
38
40
|
* @param registry - the subscriber registry to guard
|
|
39
|
-
* @param required_role - the role that grants access to the SSE endpoint
|
|
41
|
+
* @param required_role - the role that grants access to the SSE endpoint,
|
|
42
|
+
* or `null` to skip `permit_revoke` handling entirely (for streams not gated
|
|
43
|
+
* by a specific permit)
|
|
40
44
|
* @param log - logger for disconnect events
|
|
41
45
|
* @returns an `on_audit_event` callback
|
|
42
46
|
*/
|
|
43
|
-
export declare const create_sse_auth_guard: <T>(registry: SubscriberRegistry<T>, required_role: string, log: Logger) => ((event: AuditLogEvent) => void);
|
|
47
|
+
export declare const create_sse_auth_guard: <T>(registry: SubscriberRegistry<T>, required_role: string | null, log: Logger) => ((event: AuditLogEvent) => void);
|
|
44
48
|
/**
|
|
45
49
|
* Convenience factory result for audit log SSE.
|
|
46
50
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE
|
|
1
|
+
{"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,kGAAkG;IAClG,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
|
|
@@ -15,7 +15,9 @@ import { SubscriberRegistry } from './subscriber_registry.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* Audit event types that trigger SSE stream disconnection.
|
|
17
17
|
*
|
|
18
|
-
* `permit_revoke` requires the revoked role to match the guard's `required_role
|
|
18
|
+
* `permit_revoke` requires the revoked role to match the guard's `required_role`
|
|
19
|
+
* (or is skipped entirely when `required_role` is `null` — useful for streams
|
|
20
|
+
* not gated by any specific permit).
|
|
19
21
|
* `session_revoke_all` and `password_change` close every stream for the target account.
|
|
20
22
|
* `session_revoke` closes only the stream tied to the specific revoked session
|
|
21
23
|
* (matched by the blake3 session hash in `event.metadata.session_id`) — closing
|
|
@@ -39,7 +41,9 @@ export const DISCONNECT_EVENT_TYPES = new Set([
|
|
|
39
41
|
* (passed as the third argument to `registry.subscribe()`).
|
|
40
42
|
*
|
|
41
43
|
* @param registry - the subscriber registry to guard
|
|
42
|
-
* @param required_role - the role that grants access to the SSE endpoint
|
|
44
|
+
* @param required_role - the role that grants access to the SSE endpoint,
|
|
45
|
+
* or `null` to skip `permit_revoke` handling entirely (for streams not gated
|
|
46
|
+
* by a specific permit)
|
|
43
47
|
* @param log - logger for disconnect events
|
|
44
48
|
* @returns an `on_audit_event` callback
|
|
45
49
|
*/
|
|
@@ -67,8 +71,11 @@ export const create_sse_auth_guard = (registry, required_role, log) => {
|
|
|
67
71
|
}
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
// permit_revoke requires matching the specific role
|
|
74
|
+
// permit_revoke requires matching the specific role. `null` means the
|
|
75
|
+
// stream isn't gated by a specific permit, so permit_revoke is a no-op.
|
|
71
76
|
if (event.event_type === 'permit_revoke') {
|
|
77
|
+
if (required_role === null)
|
|
78
|
+
return;
|
|
72
79
|
if (event.metadata?.role !== required_role)
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
@@ -42,6 +42,8 @@ export interface BearerAuthTestCase extends BearerAuthTestOptions {
|
|
|
42
42
|
validate_expectation: 'called' | 'not_called';
|
|
43
43
|
/** If true, assert `REQUEST_CONTEXT_KEY` and `CREDENTIAL_TYPE_KEY` were set to api_token values. */
|
|
44
44
|
assert_context_set?: boolean;
|
|
45
|
+
/** If set, assert `AUTH_API_TOKEN_ID_KEY` was set to this value after a successful bearer auth. */
|
|
46
|
+
expected_api_token_id?: string;
|
|
45
47
|
/** If true, assert the pre-existing session context and credential type are preserved. */
|
|
46
48
|
assert_context_preserved?: boolean;
|
|
47
49
|
/** Optional callback for custom spy assertions on the mocks bundle. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,EAAE,EAAyB,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC1B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAU3B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAqBpF,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACrC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oEAAoE;IACpE,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,4CAA4C;IAC5C,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,+CAA+C;IAC/C,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,2DAA2D;IAC3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gFAAgF;IAChF,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAChE,+EAA+E;IAC/E,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9C,oGAAoG;IACpG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0FAA0F;IAC1F,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD;AAID,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,oBAAoB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/C,0BAA0B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACrD;AAKD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GAAI,IAAI,qBAAqB,KAAG,eAoBpE,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,cAAc,cAAc,CAAC;AAE1C;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,IAAI,qBAAqB,EACzB,kBAAiB,WAAW,GAAG,IAAW,KACxC;IAAC,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,EAAE,EAAyB,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC1B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAU3B,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAqBpF,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACrC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oEAAoE;IACpE,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,4CAA4C;IAC5C,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,+CAA+C;IAC/C,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,2DAA2D;IAC3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gFAAgF;IAChF,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClC;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAChE,+EAA+E;IAC/E,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9C,oGAAoG;IACpG,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mGAAmG;IACnG,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0FAA0F;IAC1F,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uEAAuE;IACvE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD;AAID,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,oBAAoB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/C,0BAA0B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACrD;AAKD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GAAI,IAAI,qBAAqB,KAAG,eAoBpE,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,cAAc,cAAc,CAAC;AAE1C;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,IAAI,qBAAqB,EACzB,kBAAiB,WAAW,GAAG,IAAW,KACxC;IAAC,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CA6CpC,CAAC;AAIF;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GACtC,YAAY,MAAM,EAClB,OAAO,KAAK,CAAC,kBAAkB,CAAC,EAChC,kBAAiB,WAAW,GAAG,IAAW,KACxC,IAkEF,CAAC;AAIF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAEhD,sDAAsD;AACtD,MAAM,WAAW,0BAA0B;IAC1C,iDAAiD;IACjD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IACpD,oDAAoD;IACpD,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACrC;AAED,yDAAyD;AACzD,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,IAAI,CAAC;IACV,aAAa,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1C,oBAAoB,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/C,0BAA0B,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,GAC5C,UAAU,0BAA0B,KAClC,sBAiDF,CAAC"}
|
|
@@ -18,7 +18,7 @@ import { query_permit_find_active_for_actor } from '../auth/permit_queries.js';
|
|
|
18
18
|
import { create_proxy_middleware, get_client_ip } from '../http/proxy.js';
|
|
19
19
|
import { verify_request_source, parse_allowed_origins } from '../http/origin.js';
|
|
20
20
|
import { REQUEST_CONTEXT_KEY } from '../auth/request_context.js';
|
|
21
|
-
import { CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
21
|
+
import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
22
22
|
import { ApiError } from '../http/error_schemas.js';
|
|
23
23
|
// Mock the query modules so test cases can control return values.
|
|
24
24
|
// vi.mock() is hoisted by vitest, so these run before any imports resolve.
|
|
@@ -97,6 +97,7 @@ export const create_bearer_auth_test_app = (tc, ip_rate_limiter = null) => {
|
|
|
97
97
|
app.get('/api/test', (c) => {
|
|
98
98
|
const ctx = c.get(REQUEST_CONTEXT_KEY);
|
|
99
99
|
const cred = c.get(CREDENTIAL_TYPE_KEY);
|
|
100
|
+
const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY);
|
|
100
101
|
return c.json({
|
|
101
102
|
ok: true,
|
|
102
103
|
has_context: ctx != null,
|
|
@@ -104,6 +105,7 @@ export const create_bearer_auth_test_app = (tc, ip_rate_limiter = null) => {
|
|
|
104
105
|
account_id: ctx?.account.id ?? null,
|
|
105
106
|
actor_id: ctx?.actor.id ?? null,
|
|
106
107
|
permit_count: ctx?.permits.length ?? 0,
|
|
108
|
+
api_token_id: api_token_id ?? null,
|
|
107
109
|
});
|
|
108
110
|
});
|
|
109
111
|
return { app, mocks };
|
|
@@ -149,6 +151,9 @@ export const describe_bearer_auth_cases = (suite_name, cases, ip_rate_limiter =
|
|
|
149
151
|
assert.strictEqual(body.has_context, true, 'REQUEST_CONTEXT_KEY should be set');
|
|
150
152
|
assert.strictEqual(body.credential_type, 'api_token', 'CREDENTIAL_TYPE_KEY should be api_token');
|
|
151
153
|
}
|
|
154
|
+
if (tc.expected_api_token_id !== undefined) {
|
|
155
|
+
assert.strictEqual(body.api_token_id, tc.expected_api_token_id, 'AUTH_API_TOKEN_ID_KEY should match the validated api_token.id');
|
|
156
|
+
}
|
|
152
157
|
if (tc.assert_context_preserved) {
|
|
153
158
|
assert.strictEqual(body.has_context, true, 'original context should be preserved');
|
|
154
159
|
assert.strictEqual(body.credential_type, 'session', 'credential type should remain session');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `attempted` state (set on submit attempt). Errors show after a field is blurred or
|
|
6
6
|
* after a submit attempt, avoiding premature validation while the user is still typing.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
8
|
+
* The `FormState.form` attachment also handles Enter key advancing
|
|
9
9
|
* between focusable elements.
|
|
10
10
|
*
|
|
11
11
|
* All trackable inputs must have a `name` attribute — an error is thrown in dev
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `attempted` state (set on submit attempt). Errors show after a field is blurred or
|
|
6
6
|
* after a submit attempt, avoiding premature validation while the user is still typing.
|
|
7
7
|
*
|
|
8
|
-
* The
|
|
8
|
+
* The `FormState.form` attachment also handles Enter key advancing
|
|
9
9
|
* between focusable elements.
|
|
10
10
|
*
|
|
11
11
|
* All trackable inputs must have a `name` attribute — an error is thrown in dev
|