@fuzdev/fuz_app 0.139.0 → 0.141.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +62 -17
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +25 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +8 -1
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +11 -9
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +4 -0
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +4 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -3
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +1 -0
- package/dist/testing/CLAUDE.md +154 -22
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/package.json +12 -12
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credential deadline — the moment a resolved credential stops
|
|
3
|
+
* authenticating, which a long-lived connection opened on it must not outlive.
|
|
4
|
+
*
|
|
5
|
+
* A WebSocket or an SSE stream is authorized once, at open, and closed on
|
|
6
|
+
* revocation by identity. Expiry has no revocation event: nothing is written
|
|
7
|
+
* when a session or token passes its `expires_at`. So the auth middleware
|
|
8
|
+
* computes the deadline once (`CREDENTIAL_DEADLINE_KEY`, gathered onto
|
|
9
|
+
* `ResolvedAuth.credential_deadline`) and the transport closes the connection
|
|
10
|
+
* when it arrives (`schedule_credential_deadline`).
|
|
11
|
+
*
|
|
12
|
+
* The deadline is the earliest of what the credential carries:
|
|
13
|
+
*
|
|
14
|
+
* - **Session** — the signed cookie's embedded expiry and the `auth_session`
|
|
15
|
+
* row's `expires_at`.
|
|
16
|
+
* - **API token** — the `api_token` row's `expires_at`; an eternal token
|
|
17
|
+
* (`expires_at IS NULL`) has none.
|
|
18
|
+
* - **Daemon token** — none: it has no row and no expiry, and a rotation
|
|
19
|
+
* closes no connection.
|
|
20
|
+
*
|
|
21
|
+
* A row's expiry is read as **remaining time measured on the database clock**
|
|
22
|
+
* (`EXPIRES_IN_MS_SQL`, `expires_at - NOW()` in the same statement that checks
|
|
23
|
+
* `expires_at > NOW()`), then anchored to this process's monotonic clock
|
|
24
|
+
* (`performance.now()`) — so a skew between the database's clock and the
|
|
25
|
+
* server's moves no deadline. The cookie's expiry is a wall-clock second this
|
|
26
|
+
* server signed, read against the same wall clock the cookie check reads.
|
|
27
|
+
*
|
|
28
|
+
* The monotonic clock stops while the host is suspended, so a deadline on it
|
|
29
|
+
* alone would hold a connection past its credential by the time asleep. The
|
|
30
|
+
* wait (`schedule_credential_deadline`) therefore also carries the deadline on
|
|
31
|
+
* the wall clock (`Date.now()`), which keeps running through a suspend, and
|
|
32
|
+
* fires at whichever is reached first: after a resume, within one timer step
|
|
33
|
+
* (`CREDENTIAL_DEADLINE_TIMER_CAP_MS`, an hour). The wall deadline is taken
|
|
34
|
+
* from the time left when the wait starts, so a skew between the database's
|
|
35
|
+
* clock and the server's still moves nothing. A wall clock stepped forward
|
|
36
|
+
* can fire it early; the client reconnects through the gates, the fail-closed
|
|
37
|
+
* side of the trade.
|
|
38
|
+
*
|
|
39
|
+
* The twin of the Rust spine's `fuz_auth::ResolvedAuth::credential_deadline`
|
|
40
|
+
* and `fuz_realtime`'s deadline wait.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* SQL expression for a row's remaining lifetime in milliseconds, measured on
|
|
46
|
+
* the database clock — `NULL` when `expires_at` is `NULL` (an eternal API
|
|
47
|
+
* token). `float8`, so every driver reads it as a JS number.
|
|
48
|
+
*/
|
|
49
|
+
export const EXPIRES_IN_MS_SQL = '(EXTRACT(EPOCH FROM (expires_at - NOW())) * 1000)::float8';
|
|
50
|
+
/**
|
|
51
|
+
* The longest single timer the deadline wait sets — one hour, far inside
|
|
52
|
+
* `setTimeout`'s ceiling (2^31 - 1 ms, about 24.8 days, past which a timer
|
|
53
|
+
* fires at once) and the Rust twin's tokio ceiling, and short enough that a
|
|
54
|
+
* resume from suspend is noticed within it. A longer deadline is reached
|
|
55
|
+
* through several.
|
|
56
|
+
*/
|
|
57
|
+
export const CREDENTIAL_DEADLINE_TIMER_CAP_MS = 3_600_000;
|
|
58
|
+
/**
|
|
59
|
+
* The deadline `now` plus the shortest of `remaining_ms`, as a monotonic
|
|
60
|
+
* `performance.now()` timestamp, or `null` when no entry carries one.
|
|
61
|
+
*
|
|
62
|
+
* A negative or `NaN` entry — a row its own `expires_at > NOW()` check
|
|
63
|
+
* already refused, read at the boundary — counts as zero.
|
|
64
|
+
*
|
|
65
|
+
* @param remaining_ms - each source's remaining lifetime in ms, `null` for none
|
|
66
|
+
* @param now - the monotonic clock reading to anchor to
|
|
67
|
+
* @returns the deadline, or `null` for a credential with no expiry
|
|
68
|
+
*/
|
|
69
|
+
export const credential_deadline_after = (remaining_ms, now = performance.now()) => {
|
|
70
|
+
let shortest = null;
|
|
71
|
+
for (const ms of remaining_ms) {
|
|
72
|
+
if (ms === null)
|
|
73
|
+
continue;
|
|
74
|
+
const clamped = Number.isNaN(ms) ? 0 : Math.max(0, ms);
|
|
75
|
+
if (shortest === null || clamped < shortest)
|
|
76
|
+
shortest = clamped;
|
|
77
|
+
}
|
|
78
|
+
return shortest === null ? null : now + shortest;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The next timer toward `deadline` from `now`: what remains, capped at
|
|
82
|
+
* `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, or `null` once the deadline is reached.
|
|
83
|
+
*
|
|
84
|
+
* @param deadline - the monotonic deadline
|
|
85
|
+
* @param now - the monotonic clock reading
|
|
86
|
+
* @returns the next delay in ms, or `null` when the deadline has passed
|
|
87
|
+
*/
|
|
88
|
+
export const next_credential_deadline_delay = (deadline, now) => {
|
|
89
|
+
const remaining = deadline - now;
|
|
90
|
+
if (!(remaining > 0))
|
|
91
|
+
return null;
|
|
92
|
+
return Math.min(remaining, CREDENTIAL_DEADLINE_TIMER_CAP_MS);
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Call `on_expire` once `deadline` has passed, waiting in timers of at most
|
|
96
|
+
* `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, each re-reading the clock against the
|
|
97
|
+
* absolute deadline — so a session's 30 days (past `setTimeout`'s ceiling)
|
|
98
|
+
* does not fire at once. Each wake-up also checks the same deadline on the
|
|
99
|
+
* wall clock, so a host suspend, which stops the monotonic clock, delays the
|
|
100
|
+
* close by at most one timer after the resume (module doc).
|
|
101
|
+
*
|
|
102
|
+
* A `null` deadline schedules nothing. The returned function cancels the wait
|
|
103
|
+
* and is safe to call more than once, before or after it fires.
|
|
104
|
+
*
|
|
105
|
+
* @param deadline - the monotonic deadline (`ResolvedAuth.credential_deadline`)
|
|
106
|
+
* @param on_expire - run once, when the deadline passes
|
|
107
|
+
* @returns a cancel function
|
|
108
|
+
*/
|
|
109
|
+
export const schedule_credential_deadline = (deadline, on_expire) => {
|
|
110
|
+
if (deadline === null)
|
|
111
|
+
return () => { };
|
|
112
|
+
// the same deadline on the wall clock, which a suspend does not stop
|
|
113
|
+
const wall_deadline = Date.now() + (deadline - performance.now());
|
|
114
|
+
let timer = null;
|
|
115
|
+
let cancelled = false;
|
|
116
|
+
const step = () => {
|
|
117
|
+
timer = null;
|
|
118
|
+
if (cancelled)
|
|
119
|
+
return;
|
|
120
|
+
const delay = next_credential_deadline_delay(deadline, performance.now());
|
|
121
|
+
if (delay === null || Date.now() >= wall_deadline) {
|
|
122
|
+
cancelled = true;
|
|
123
|
+
on_expire();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
timer = setTimeout(step, delay);
|
|
127
|
+
// a pending deadline never holds the process open on its own
|
|
128
|
+
if (typeof timer === 'object' && 'unref' in timer)
|
|
129
|
+
timer.unref();
|
|
130
|
+
};
|
|
131
|
+
step();
|
|
132
|
+
return () => {
|
|
133
|
+
cancelled = true;
|
|
134
|
+
if (timer !== null) {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
timer = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
};
|
|
@@ -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;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAYtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAEtF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,EACf,KAAK,MAAM,KACT,iBA6EF,CAAC"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
import { DEV } from 'esm-env';
|
|
17
|
-
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY } from '../hono_context.js';
|
|
17
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_DEADLINE_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY } from '../hono_context.js';
|
|
18
18
|
import { token_scope_full } from './token_scope.js';
|
|
19
19
|
import { is_browser_context } from '../http/origin.js';
|
|
20
20
|
import { query_role_grant_find_account_id_for_role } from './role_grant_queries.js';
|
|
@@ -141,6 +141,9 @@ export const create_daemon_token_middleware = (state, deps, log) => {
|
|
|
141
141
|
// narrowing could have been chosen.
|
|
142
142
|
c.set(TOKEN_SCOPE_KEY, token_scope_full());
|
|
143
143
|
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
144
|
+
// No row, no expiry — and it overrides a session cookie sent beside it,
|
|
145
|
+
// whose deadline must not ride along.
|
|
146
|
+
c.set(CREDENTIAL_DEADLINE_KEY, null);
|
|
144
147
|
await next();
|
|
145
148
|
};
|
|
146
149
|
};
|
|
@@ -9,26 +9,36 @@
|
|
|
9
9
|
import type { SessionOptions } from './session_cookie.js';
|
|
10
10
|
import type { AppDeps } from './deps.js';
|
|
11
11
|
import type { DaemonTokenState } from './daemon_token.js';
|
|
12
|
-
import type { MiddlewareSpec } from '../http/middleware_spec.js';
|
|
12
|
+
import type { MiddlewareCarveOut, MiddlewareSpec } from '../http/middleware_spec.js';
|
|
13
|
+
import type { RouteSpec } from '../http/route_spec.js';
|
|
14
|
+
/**
|
|
15
|
+
* The Hono path pattern the origin check mounts on — every path. A request to
|
|
16
|
+
* any route, the static fallback included, carrying a foreign `Origin` is
|
|
17
|
+
* refused; one with no `Origin` (a probe, a CLI client) passes. The only way
|
|
18
|
+
* out is a named carve-out with a reason (`AuthMiddlewareOptions.origin_carve_outs`).
|
|
19
|
+
* The Rust spine's `fuz_http::compose_stack` checks every route group the
|
|
20
|
+
* same way.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ORIGIN_MIDDLEWARE_PATH = "*";
|
|
13
23
|
/**
|
|
14
24
|
* The Hono path pattern the auth middleware stack mounts on by default —
|
|
15
|
-
*
|
|
16
|
-
*
|
|
25
|
+
* session parsing, request context, bearer and daemon token auth all run for
|
|
26
|
+
* the paths it matches, and no others (the origin check runs on every path,
|
|
27
|
+
* `ORIGIN_MIDDLEWARE_PATH`).
|
|
17
28
|
*
|
|
18
29
|
* `create_app_server` mounts the stack here, and refuses at assembly (via
|
|
19
30
|
* `assert_endpoint_in_auth_scope`, which `create_test_app_surface_spec` also
|
|
20
31
|
* runs) an `rpc_endpoints`, `ws_endpoints`, or bootstrap path the pattern does
|
|
21
32
|
* not match (tested with `middleware_applies`, which follows Hono: `'/api/*'`
|
|
22
33
|
* matches `/api` and everything under `/api/`): an endpoint outside it would
|
|
23
|
-
* get no
|
|
24
|
-
* unauthenticated
|
|
34
|
+
* get no session or bearer parsing, so every call or upgrade would be
|
|
35
|
+
* unauthenticated.
|
|
25
36
|
*/
|
|
26
37
|
export declare const AUTH_MIDDLEWARE_PATH = "/api/*";
|
|
27
38
|
/**
|
|
28
39
|
* Throw when an endpoint path sits outside the auth middleware's scope
|
|
29
|
-
* (`AUTH_MIDDLEWARE_PATH`) — there it would get no
|
|
30
|
-
*
|
|
31
|
-
* unauthenticated and any page could send it.
|
|
40
|
+
* (`AUTH_MIDDLEWARE_PATH`) — there it would get no session or bearer
|
|
41
|
+
* parsing, so every call or upgrade would be unauthenticated.
|
|
32
42
|
* `middleware_applies` matches the way Hono matches the middleware's pattern.
|
|
33
43
|
*
|
|
34
44
|
* `create_app_server` calls it for every `rpc_endpoints` and `ws_endpoints`
|
|
@@ -41,6 +51,60 @@ export declare const AUTH_MIDDLEWARE_PATH = "/api/*";
|
|
|
41
51
|
* @throws Error when `path` is outside `AUTH_MIDDLEWARE_PATH`
|
|
42
52
|
*/
|
|
43
53
|
export declare const assert_endpoint_in_auth_scope: (option: "rpc_endpoints" | "ws_endpoints" | "bootstrap.route_prefix", path: string) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Throw when a route that reads or mints a credential sits outside the auth
|
|
56
|
+
* middleware's scope (`AUTH_MIDDLEWARE_PATH`). A route reads one when its
|
|
57
|
+
* `auth.account` is not `'none'`, or when it is marked `reads_credential` (a
|
|
58
|
+
* handler that reads the resolved account behind no gate: account status) —
|
|
59
|
+
* outside the scope no session or bearer parsing runs, so it would answer
|
|
60
|
+
* every caller as unauthenticated. A route
|
|
61
|
+
* mints one when it is marked `mints_credential` (login, signup, bootstrap) —
|
|
62
|
+
* outside the scope an origin carve-out could cover it
|
|
63
|
+
* (`assert_origin_carve_outs_valid` refuses one only inside), and a forged
|
|
64
|
+
* cross-site request to it is login CSRF.
|
|
65
|
+
*
|
|
66
|
+
* Account and signup route specs are relative and the consumer prefixes them,
|
|
67
|
+
* so this is what holds them inside the scope whatever prefix they get.
|
|
68
|
+
* `create_app_server` runs it over every route it registers, consumer and
|
|
69
|
+
* factory alike, and `create_test_app_surface_spec` does the same.
|
|
70
|
+
*
|
|
71
|
+
* @param route_specs - every route the server registers
|
|
72
|
+
* @throws Error naming the first route that reads or mints a credential outside `AUTH_MIDDLEWARE_PATH`
|
|
73
|
+
*/
|
|
74
|
+
export declare const assert_credential_routes_in_auth_scope: (route_specs: ReadonlyArray<RouteSpec>) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Throw unless every origin carve-out names a path and a reason, and stays
|
|
77
|
+
* off the auth middleware's scope: a path that starts with `/` (a Hono pattern
|
|
78
|
+
* like `/hooks/*` or `/health`), a non-blank reason — a carve-out is an
|
|
79
|
+
* argument someone wrote down, never an empty placeholder — and a pattern that
|
|
80
|
+
* matches nothing under `auth_path` (`middleware_paths_overlap`).
|
|
81
|
+
*
|
|
82
|
+
* The scope rule is what keeps a carve-out off every credentialed route.
|
|
83
|
+
* Assembly holds every route that reads a credential (`auth.account` not
|
|
84
|
+
* `'none'`, or `reads_credential`) or mints one (`mints_credential`: login,
|
|
85
|
+
* signup, bootstrap) inside
|
|
86
|
+
* `AUTH_MIDDLEWARE_PATH` (`assert_credential_routes_in_auth_scope`), and every
|
|
87
|
+
* RPC, WS, and bootstrap endpoint besides (`assert_endpoint_in_auth_scope`).
|
|
88
|
+
* The minting routes declare no auth but are what a forged cross-site request
|
|
89
|
+
* targets, so refusing by route `auth` would miss them; refusing by scope
|
|
90
|
+
* catches them. A route outside the scope reads no credential the auth
|
|
91
|
+
* middleware parses, and mints none, so a carve-out there removes no
|
|
92
|
+
* credentialed check.
|
|
93
|
+
*
|
|
94
|
+
* @param carve_outs - the carve-outs to check
|
|
95
|
+
* @param auth_path - the auth middleware's path pattern (default `AUTH_MIDDLEWARE_PATH`)
|
|
96
|
+
* @throws Error naming the first carve-out with no path, no reason, or a pattern overlapping `auth_path`
|
|
97
|
+
*/
|
|
98
|
+
export declare const assert_origin_carve_outs_valid: (carve_outs: ReadonlyArray<MiddlewareCarveOut>, auth_path?: string) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Copy origin carve-outs into a frozen array of frozen `{path, reason}`
|
|
101
|
+
* records, so a caller that mutates its own array or objects after assembly
|
|
102
|
+
* cannot change what was validated or what the check skips.
|
|
103
|
+
*
|
|
104
|
+
* @param carve_outs - the caller's carve-outs
|
|
105
|
+
* @returns the frozen copy
|
|
106
|
+
*/
|
|
107
|
+
export declare const freeze_origin_carve_outs: (carve_outs: ReadonlyArray<MiddlewareCarveOut>) => ReadonlyArray<MiddlewareCarveOut>;
|
|
44
108
|
/**
|
|
45
109
|
* Throw unless a middleware list still mounts every spec of the stack it was
|
|
46
110
|
* built from — the trusted-proxy spec and the auth middleware — exactly once,
|
|
@@ -77,21 +141,35 @@ export declare const assert_middleware_stack_mounted: (stack_specs: ReadonlyArra
|
|
|
77
141
|
export interface AuthMiddlewareOptions {
|
|
78
142
|
allowed_origins: Array<RegExp>;
|
|
79
143
|
session_options: SessionOptions<string>;
|
|
80
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Path pattern for the session, request-context, bearer, and daemon-token
|
|
146
|
+
* layers (default: `AUTH_MIDDLEWARE_PATH`, `'/api/*'`). The origin check
|
|
147
|
+
* always mounts on every path (`ORIGIN_MIDDLEWARE_PATH`).
|
|
148
|
+
*/
|
|
81
149
|
path?: string;
|
|
150
|
+
/**
|
|
151
|
+
* Paths the origin check skips, each with its reason (default none). Copied
|
|
152
|
+
* at assembly (`freeze_origin_carve_outs`), then checked by
|
|
153
|
+
* `assert_origin_carve_outs_valid` — none may overlap `path`; recorded on
|
|
154
|
+
* the origin spec, so the generated surface leaves the check's `403` off
|
|
155
|
+
* the carved-out routes.
|
|
156
|
+
*/
|
|
157
|
+
origin_carve_outs?: ReadonlyArray<MiddlewareCarveOut>;
|
|
82
158
|
/** Daemon token state for keeper auth. Omit to disable daemon token middleware. */
|
|
83
159
|
daemon_token_state?: DaemonTokenState;
|
|
84
160
|
}
|
|
85
161
|
/**
|
|
86
162
|
* Create the auth middleware stack.
|
|
87
163
|
*
|
|
88
|
-
* Returns `[origin, session, request_context, bearer_auth]` middleware specs
|
|
89
|
-
*
|
|
164
|
+
* Returns `[origin, session, request_context, bearer_auth]` middleware specs:
|
|
165
|
+
* the origin check on every path less its carve-outs, the rest for the given
|
|
166
|
+
* path pattern. When `daemon_token_state` is provided, appends
|
|
90
167
|
* a `daemon_token` layer. Apps can append extra entries for non-standard
|
|
91
168
|
* paths (e.g., tx's `/tx` binary endpoint).
|
|
92
169
|
*
|
|
93
170
|
* @param deps - stateless capabilities (keyring, db)
|
|
94
|
-
* @param options - middleware configuration (allowed_origins, session_options, path, daemon_token_state)
|
|
171
|
+
* @param options - middleware configuration (allowed_origins, session_options, path, daemon_token_state, origin_carve_outs)
|
|
172
|
+
* @throws Error when an origin carve-out has no path or no reason, or overlaps `path`
|
|
95
173
|
* @returns the middleware spec array
|
|
96
174
|
*/
|
|
97
175
|
export declare const create_auth_middleware_specs: (deps: AppDeps, options: AuthMiddlewareOptions) => Promise<Array<MiddlewareSpec>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACrF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,6BAA6B,GACzC,QAAQ,eAAe,GAAG,cAAc,GAAG,wBAAwB,EACnE,MAAM,MAAM,KACV,IAQF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,sCAAsC,GAClD,aAAa,aAAa,CAAC,SAAS,CAAC,KACnC,IAcF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,aAAa,CAAC,kBAAkB,CAAC,EAC7C,YAAW,MAA6B,KACtC,IAsBF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GACpC,YAAY,aAAa,CAAC,kBAAkB,CAAC,KAC3C,aAAa,CAAC,kBAAkB,CACkD,CAAC;AAEtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,+BAA+B,GAC3C,aAAa,aAAa,CAAC,cAAc,CAAC,EAC1C,kBAAkB,aAAa,CAAC,cAAc,CAAC,KAC7C,IA+BF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACtD,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAgF/B,CAAC"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -7,26 +7,35 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { ApiError } from '../http/error_schemas.js';
|
|
10
|
-
import { middleware_applies } from '../http/schema_helpers.js';
|
|
10
|
+
import { middleware_applies, middleware_paths_overlap } from '../http/schema_helpers.js';
|
|
11
|
+
/**
|
|
12
|
+
* The Hono path pattern the origin check mounts on — every path. A request to
|
|
13
|
+
* any route, the static fallback included, carrying a foreign `Origin` is
|
|
14
|
+
* refused; one with no `Origin` (a probe, a CLI client) passes. The only way
|
|
15
|
+
* out is a named carve-out with a reason (`AuthMiddlewareOptions.origin_carve_outs`).
|
|
16
|
+
* The Rust spine's `fuz_http::compose_stack` checks every route group the
|
|
17
|
+
* same way.
|
|
18
|
+
*/
|
|
19
|
+
export const ORIGIN_MIDDLEWARE_PATH = '*';
|
|
11
20
|
/**
|
|
12
21
|
* The Hono path pattern the auth middleware stack mounts on by default —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
22
|
+
* session parsing, request context, bearer and daemon token auth all run for
|
|
23
|
+
* the paths it matches, and no others (the origin check runs on every path,
|
|
24
|
+
* `ORIGIN_MIDDLEWARE_PATH`).
|
|
15
25
|
*
|
|
16
26
|
* `create_app_server` mounts the stack here, and refuses at assembly (via
|
|
17
27
|
* `assert_endpoint_in_auth_scope`, which `create_test_app_surface_spec` also
|
|
18
28
|
* runs) an `rpc_endpoints`, `ws_endpoints`, or bootstrap path the pattern does
|
|
19
29
|
* not match (tested with `middleware_applies`, which follows Hono: `'/api/*'`
|
|
20
30
|
* matches `/api` and everything under `/api/`): an endpoint outside it would
|
|
21
|
-
* get no
|
|
22
|
-
* unauthenticated
|
|
31
|
+
* get no session or bearer parsing, so every call or upgrade would be
|
|
32
|
+
* unauthenticated.
|
|
23
33
|
*/
|
|
24
34
|
export const AUTH_MIDDLEWARE_PATH = '/api/*';
|
|
25
35
|
/**
|
|
26
36
|
* Throw when an endpoint path sits outside the auth middleware's scope
|
|
27
|
-
* (`AUTH_MIDDLEWARE_PATH`) — there it would get no
|
|
28
|
-
*
|
|
29
|
-
* unauthenticated and any page could send it.
|
|
37
|
+
* (`AUTH_MIDDLEWARE_PATH`) — there it would get no session or bearer
|
|
38
|
+
* parsing, so every call or upgrade would be unauthenticated.
|
|
30
39
|
* `middleware_applies` matches the way Hono matches the middleware's pattern.
|
|
31
40
|
*
|
|
32
41
|
* `create_app_server` calls it for every `rpc_endpoints` and `ws_endpoints`
|
|
@@ -41,10 +50,94 @@ export const AUTH_MIDDLEWARE_PATH = '/api/*';
|
|
|
41
50
|
export const assert_endpoint_in_auth_scope = (option, path) => {
|
|
42
51
|
if (!middleware_applies(AUTH_MIDDLEWARE_PATH, path)) {
|
|
43
52
|
throw new Error(`${option} path ${path} is outside the auth middleware's scope ` +
|
|
44
|
-
`(${AUTH_MIDDLEWARE_PATH}) — it would
|
|
45
|
-
`
|
|
53
|
+
`(${AUTH_MIDDLEWARE_PATH}) — it would see every caller as unauthenticated — ` +
|
|
54
|
+
`mount it under /api/`);
|
|
46
55
|
}
|
|
47
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Throw when a route that reads or mints a credential sits outside the auth
|
|
59
|
+
* middleware's scope (`AUTH_MIDDLEWARE_PATH`). A route reads one when its
|
|
60
|
+
* `auth.account` is not `'none'`, or when it is marked `reads_credential` (a
|
|
61
|
+
* handler that reads the resolved account behind no gate: account status) —
|
|
62
|
+
* outside the scope no session or bearer parsing runs, so it would answer
|
|
63
|
+
* every caller as unauthenticated. A route
|
|
64
|
+
* mints one when it is marked `mints_credential` (login, signup, bootstrap) —
|
|
65
|
+
* outside the scope an origin carve-out could cover it
|
|
66
|
+
* (`assert_origin_carve_outs_valid` refuses one only inside), and a forged
|
|
67
|
+
* cross-site request to it is login CSRF.
|
|
68
|
+
*
|
|
69
|
+
* Account and signup route specs are relative and the consumer prefixes them,
|
|
70
|
+
* so this is what holds them inside the scope whatever prefix they get.
|
|
71
|
+
* `create_app_server` runs it over every route it registers, consumer and
|
|
72
|
+
* factory alike, and `create_test_app_surface_spec` does the same.
|
|
73
|
+
*
|
|
74
|
+
* @param route_specs - every route the server registers
|
|
75
|
+
* @throws Error naming the first route that reads or mints a credential outside `AUTH_MIDDLEWARE_PATH`
|
|
76
|
+
*/
|
|
77
|
+
export const assert_credential_routes_in_auth_scope = (route_specs) => {
|
|
78
|
+
for (const spec of route_specs) {
|
|
79
|
+
const reads = spec.auth.account !== 'none' || spec.reads_credential === true;
|
|
80
|
+
if (!reads && !spec.mints_credential)
|
|
81
|
+
continue;
|
|
82
|
+
if (middleware_applies(AUTH_MIDDLEWARE_PATH, spec.path))
|
|
83
|
+
continue;
|
|
84
|
+
throw new Error(`route ${spec.method} ${spec.path} ${reads ? 'reads' : 'mints'} a credential but is ` +
|
|
85
|
+
`outside the auth middleware's scope (${AUTH_MIDDLEWARE_PATH}) — ` +
|
|
86
|
+
(reads
|
|
87
|
+
? 'it would see every caller as unauthenticated'
|
|
88
|
+
: 'an origin carve-out could leave it open to cross-site requests') +
|
|
89
|
+
' — mount it under /api/');
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Throw unless every origin carve-out names a path and a reason, and stays
|
|
94
|
+
* off the auth middleware's scope: a path that starts with `/` (a Hono pattern
|
|
95
|
+
* like `/hooks/*` or `/health`), a non-blank reason — a carve-out is an
|
|
96
|
+
* argument someone wrote down, never an empty placeholder — and a pattern that
|
|
97
|
+
* matches nothing under `auth_path` (`middleware_paths_overlap`).
|
|
98
|
+
*
|
|
99
|
+
* The scope rule is what keeps a carve-out off every credentialed route.
|
|
100
|
+
* Assembly holds every route that reads a credential (`auth.account` not
|
|
101
|
+
* `'none'`, or `reads_credential`) or mints one (`mints_credential`: login,
|
|
102
|
+
* signup, bootstrap) inside
|
|
103
|
+
* `AUTH_MIDDLEWARE_PATH` (`assert_credential_routes_in_auth_scope`), and every
|
|
104
|
+
* RPC, WS, and bootstrap endpoint besides (`assert_endpoint_in_auth_scope`).
|
|
105
|
+
* The minting routes declare no auth but are what a forged cross-site request
|
|
106
|
+
* targets, so refusing by route `auth` would miss them; refusing by scope
|
|
107
|
+
* catches them. A route outside the scope reads no credential the auth
|
|
108
|
+
* middleware parses, and mints none, so a carve-out there removes no
|
|
109
|
+
* credentialed check.
|
|
110
|
+
*
|
|
111
|
+
* @param carve_outs - the carve-outs to check
|
|
112
|
+
* @param auth_path - the auth middleware's path pattern (default `AUTH_MIDDLEWARE_PATH`)
|
|
113
|
+
* @throws Error naming the first carve-out with no path, no reason, or a pattern overlapping `auth_path`
|
|
114
|
+
*/
|
|
115
|
+
export const assert_origin_carve_outs_valid = (carve_outs, auth_path = AUTH_MIDDLEWARE_PATH) => {
|
|
116
|
+
for (const carve_out of carve_outs) {
|
|
117
|
+
if (!carve_out.path.startsWith('/')) {
|
|
118
|
+
throw new Error(`origin carve-out path ${JSON.stringify(carve_out.path)} must start with / ` +
|
|
119
|
+
'(a Hono pattern like /hooks/* or /health)');
|
|
120
|
+
}
|
|
121
|
+
if (carve_out.reason.trim() === '') {
|
|
122
|
+
throw new Error(`origin carve-out ${carve_out.path} needs a reason — say why the paths are safe ` +
|
|
123
|
+
'without the origin check');
|
|
124
|
+
}
|
|
125
|
+
if (middleware_paths_overlap(carve_out.path, auth_path)) {
|
|
126
|
+
throw new Error(`origin carve-out ${carve_out.path} overlaps the auth middleware's scope ` +
|
|
127
|
+
`(${auth_path}) — the routes there read or mint credentials and always take ` +
|
|
128
|
+
'the origin check; carve out a path outside it');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Copy origin carve-outs into a frozen array of frozen `{path, reason}`
|
|
134
|
+
* records, so a caller that mutates its own array or objects after assembly
|
|
135
|
+
* cannot change what was validated or what the check skips.
|
|
136
|
+
*
|
|
137
|
+
* @param carve_outs - the caller's carve-outs
|
|
138
|
+
* @returns the frozen copy
|
|
139
|
+
*/
|
|
140
|
+
export const freeze_origin_carve_outs = (carve_outs) => Object.freeze(carve_outs.map(({ path, reason }) => Object.freeze({ path, reason })));
|
|
48
141
|
/**
|
|
49
142
|
* Throw unless a middleware list still mounts every spec of the stack it was
|
|
50
143
|
* built from — the trusted-proxy spec and the auth middleware — exactly once,
|
|
@@ -107,18 +200,23 @@ export const assert_middleware_stack_mounted = (stack_specs, middleware_specs) =
|
|
|
107
200
|
/**
|
|
108
201
|
* Create the auth middleware stack.
|
|
109
202
|
*
|
|
110
|
-
* Returns `[origin, session, request_context, bearer_auth]` middleware specs
|
|
111
|
-
*
|
|
203
|
+
* Returns `[origin, session, request_context, bearer_auth]` middleware specs:
|
|
204
|
+
* the origin check on every path less its carve-outs, the rest for the given
|
|
205
|
+
* path pattern. When `daemon_token_state` is provided, appends
|
|
112
206
|
* a `daemon_token` layer. Apps can append extra entries for non-standard
|
|
113
207
|
* paths (e.g., tx's `/tx` binary endpoint).
|
|
114
208
|
*
|
|
115
209
|
* @param deps - stateless capabilities (keyring, db)
|
|
116
|
-
* @param options - middleware configuration (allowed_origins, session_options, path, daemon_token_state)
|
|
210
|
+
* @param options - middleware configuration (allowed_origins, session_options, path, daemon_token_state, origin_carve_outs)
|
|
211
|
+
* @throws Error when an origin carve-out has no path or no reason, or overlaps `path`
|
|
117
212
|
* @returns the middleware spec array
|
|
118
213
|
*/
|
|
119
214
|
export const create_auth_middleware_specs = async (deps, options) => {
|
|
120
215
|
const { keyring, db } = deps;
|
|
121
216
|
const { allowed_origins, session_options, path = AUTH_MIDDLEWARE_PATH, daemon_token_state } = options;
|
|
217
|
+
// copied before it is checked, so what is checked is what runs
|
|
218
|
+
const origin_carve_outs = freeze_origin_carve_outs(options.origin_carve_outs ?? []);
|
|
219
|
+
assert_origin_carve_outs_valid(origin_carve_outs, path);
|
|
122
220
|
const query_deps = { db };
|
|
123
221
|
// Dynamic imports preserve the bundle-split for type-only consumers of
|
|
124
222
|
// this module (`MiddlewareSpec`, `AuthMiddlewareOptions`, etc.). The
|
|
@@ -137,9 +235,10 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
137
235
|
const specs = [
|
|
138
236
|
{
|
|
139
237
|
name: 'origin',
|
|
140
|
-
path,
|
|
141
|
-
handler: verify_request_source(allowed_origins),
|
|
142
|
-
errors: { 403: ApiError }
|
|
238
|
+
path: ORIGIN_MIDDLEWARE_PATH,
|
|
239
|
+
handler: verify_request_source(allowed_origins, origin_carve_outs),
|
|
240
|
+
errors: { 403: ApiError },
|
|
241
|
+
...(origin_carve_outs.length > 0 && { carve_outs: origin_carve_outs })
|
|
143
242
|
},
|
|
144
243
|
{ name: 'session', path, handler: session_middleware },
|
|
145
244
|
{ name: 'request_context', path, handler: request_context_middleware },
|
|
@@ -222,7 +222,9 @@ export declare const resolve_acting_actor: (deps: QueryDeps, account_id: string,
|
|
|
222
222
|
*
|
|
223
223
|
* Reads the session identity (set by session middleware), looks up the
|
|
224
224
|
* `auth_session`, and on a valid session sets `c.var.auth_account_id`,
|
|
225
|
-
* `CREDENTIAL_TYPE_KEY = 'session'`,
|
|
225
|
+
* `CREDENTIAL_TYPE_KEY = 'session'`, `AUTH_SESSION_TOKEN_HASH_KEY`, and
|
|
226
|
+
* `CREDENTIAL_DEADLINE_KEY` — the earlier of the cookie's signed expiry
|
|
227
|
+
* (`SESSION_COOKIE_EXPIRES_AT_KEY`) and the row's.
|
|
226
228
|
* Touches the session (fire-and-forget). Does not load actor or role_grants;
|
|
227
229
|
* `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
|
|
228
230
|
* authorization phase resolves the acting actor and builds the full
|
|
@@ -233,7 +235,7 @@ export declare const resolve_acting_actor: (deps: QueryDeps, account_id: string,
|
|
|
233
235
|
*
|
|
234
236
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
235
237
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
236
|
-
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `
|
|
238
|
+
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, `AUTH_API_TOKEN_ID_KEY`, and `CREDENTIAL_DEADLINE_KEY`
|
|
237
239
|
*/
|
|
238
240
|
export declare const create_request_context_middleware: (deps: QueryDeps, session_context_key?: string) => MiddlewareHandler;
|
|
239
241
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,KAAK,EAEV,KAAK,SAAS,EACd,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,7 +38,8 @@ import { is_role_grant_active } from './account_schema.js';
|
|
|
38
38
|
import { hash_session_token, query_session_get_valid } from './session_queries.js';
|
|
39
39
|
import { query_account_by_id, query_actor_by_id, query_active_actors_by_account } from './account_queries.js';
|
|
40
40
|
import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
|
|
41
|
-
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
41
|
+
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_DEADLINE_KEY, CREDENTIAL_TYPE_KEY, SESSION_COOKIE_EXPIRES_AT_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
42
|
+
import { credential_deadline_after } from './credential_deadline.js';
|
|
42
43
|
import { parse_token_scope_capability, token_scope_admits_capability, token_scope_denied_body, token_scope_full } from './token_scope.js';
|
|
43
44
|
import { is_public_auth, needs_actor, parse_acting } from '../http/auth_shape.js';
|
|
44
45
|
import { is_null_schema } from '../http/schema_helpers.js';
|
|
@@ -207,7 +208,9 @@ export const resolve_acting_actor = async (deps, account_id, acting_actor_id) =>
|
|
|
207
208
|
*
|
|
208
209
|
* Reads the session identity (set by session middleware), looks up the
|
|
209
210
|
* `auth_session`, and on a valid session sets `c.var.auth_account_id`,
|
|
210
|
-
* `CREDENTIAL_TYPE_KEY = 'session'`,
|
|
211
|
+
* `CREDENTIAL_TYPE_KEY = 'session'`, `AUTH_SESSION_TOKEN_HASH_KEY`, and
|
|
212
|
+
* `CREDENTIAL_DEADLINE_KEY` — the earlier of the cookie's signed expiry
|
|
213
|
+
* (`SESSION_COOKIE_EXPIRES_AT_KEY`) and the row's.
|
|
211
214
|
* Touches the session (fire-and-forget). Does not load actor or role_grants;
|
|
212
215
|
* `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
|
|
213
216
|
* authorization phase resolves the acting actor and builds the full
|
|
@@ -218,7 +221,7 @@ export const resolve_acting_actor = async (deps, account_id, acting_actor_id) =>
|
|
|
218
221
|
*
|
|
219
222
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
220
223
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
221
|
-
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `
|
|
224
|
+
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, `AUTH_API_TOKEN_ID_KEY`, and `CREDENTIAL_DEADLINE_KEY`
|
|
222
225
|
*/
|
|
223
226
|
export const create_request_context_middleware = (deps, session_context_key = 'auth_session_id') => {
|
|
224
227
|
return async (c, next) => {
|
|
@@ -227,6 +230,7 @@ export const create_request_context_middleware = (deps, session_context_key = 'a
|
|
|
227
230
|
c.set(CREDENTIAL_TYPE_KEY, null);
|
|
228
231
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
|
|
229
232
|
c.set(AUTH_API_TOKEN_ID_KEY, null);
|
|
233
|
+
c.set(CREDENTIAL_DEADLINE_KEY, null);
|
|
230
234
|
const session_token = c.get(session_context_key) ?? null;
|
|
231
235
|
if (!session_token) {
|
|
232
236
|
await next();
|
|
@@ -243,6 +247,13 @@ export const create_request_context_middleware = (deps, session_context_key = 'a
|
|
|
243
247
|
// A session is full account authority by construction.
|
|
244
248
|
c.set(TOKEN_SCOPE_KEY, token_scope_full());
|
|
245
249
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
250
|
+
// The earlier of the signed cookie's expiry (this server's wall clock,
|
|
251
|
+
// which signed it) and the row's (read on the database clock).
|
|
252
|
+
const cookie_expires_at = c.get(SESSION_COOKIE_EXPIRES_AT_KEY) ?? null;
|
|
253
|
+
c.set(CREDENTIAL_DEADLINE_KEY, credential_deadline_after([
|
|
254
|
+
cookie_expires_at === null ? null : cookie_expires_at * 1000 - Date.now(),
|
|
255
|
+
session.expires_in_ms
|
|
256
|
+
]));
|
|
246
257
|
await next();
|
|
247
258
|
};
|
|
248
259
|
};
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* credential and leaves it on the Hono context as separate keys.
|
|
7
7
|
* `ResolvedAuth` is that credential gathered into one value
|
|
8
8
|
* (`get_resolved_auth`), so the connections that outlive their request — a
|
|
9
|
-
* WebSocket, an SSE stream — can carry it past the handshake
|
|
10
|
-
*
|
|
9
|
+
* WebSocket, an SSE stream — can carry it past the handshake, re-read it with
|
|
10
|
+
* `revalidate_resolved_auth`, and close at its `credential_deadline`.
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
@@ -31,6 +31,17 @@ export interface ResolvedAuth {
|
|
|
31
31
|
token_hash: string | null;
|
|
32
32
|
/** `api_token.id` for a bearer credential, else `null`. */
|
|
33
33
|
api_token_id: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* When the credential stops authenticating, as a monotonic
|
|
36
|
+
* `performance.now()` timestamp — the moment a long-lived connection opened
|
|
37
|
+
* on it is closed (`schedule_credential_deadline`), since expiry writes
|
|
38
|
+
* nothing a revocation listener could hear. The earliest of what the
|
|
39
|
+
* credential carries: a session's cookie and row expiry, an API token's row
|
|
40
|
+
* expiry (`null` for an eternal one); `null` for the daemon token. A row's
|
|
41
|
+
* expiry is read as time remaining on the database clock
|
|
42
|
+
* (`auth/credential_deadline.ts`). The Rust twin's `credential_deadline`.
|
|
43
|
+
*/
|
|
44
|
+
credential_deadline: number | null;
|
|
34
45
|
}
|
|
35
46
|
/**
|
|
36
47
|
* Gather the credential the auth middleware resolved for this request.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolved_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/resolved_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,
|
|
1
|
+
{"version":3,"file":"resolved_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/resolved_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAKN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC5B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,eAAe,EAAE,cAAc,CAAC;IAChC,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;;;;;OASG;IACH,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,OAAO,KAAG,YAAY,GAAG,IAY7D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,EACf,UAAU,YAAY,KACpB,OAAO,CAAC,OAAO,CAsBjB,CAAC"}
|