@fuzdev/fuz_app 0.109.0 → 0.110.1
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/auth/CLAUDE.md +6 -5
- package/dist/auth/account_action_specs.d.ts +18 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +14 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +6 -2
- package/dist/auth/bootstrap_account.d.ts +7 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +9 -4
- package/dist/auth/bootstrap_routes.d.ts +12 -4
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -6
- package/dist/auth/daemon_token_middleware.d.ts +7 -69
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +8 -104
- package/dist/auth/deps.d.ts +9 -5
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.js +1 -1
- package/dist/auth/request_context.d.ts +1 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +2 -5
- package/dist/auth/session_cookie.d.ts +18 -36
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +16 -26
- 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 +1 -4
- package/dist/auth/session_queries.d.ts +14 -30
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +15 -45
- package/dist/auth/token_lifetime.d.ts +51 -0
- package/dist/auth/token_lifetime.d.ts.map +1 -0
- package/dist/auth/token_lifetime.js +54 -0
- package/dist/dev/setup.d.ts +5 -9
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +18 -21
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +27 -1
- package/dist/runtime/deps.d.ts +38 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/fs.d.ts +24 -5
- package/dist/runtime/fs.d.ts.map +1 -1
- package/dist/runtime/fs.js +38 -7
- package/dist/runtime/mock.d.ts +7 -0
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +41 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -1
- package/dist/runtime/secure_file.d.ts +71 -0
- package/dist/runtime/secure_file.d.ts.map +1 -0
- package/dist/runtime/secure_file.js +125 -0
- package/dist/server/app_backend.d.ts +6 -5
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +2 -3
- package/dist/testing/CLAUDE.md +15 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +57 -20
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +13 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +34 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +2 -1
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_lifetime.js +99 -0
- package/dist/testing/cross_backend/token_scope_surface.js +1 -1
- package/dist/testing/daemon_token_rotation.d.ts +83 -0
- package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
- package/dist/testing/daemon_token_rotation.js +99 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -4
- package/dist/ui/AccountSessions.svelte +0 -5
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +7 -4
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +0 -5
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auth/deps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACvB;;;;;;;OAOG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,yBAAyB;IACzB,EAAE,EAAE,EAAE,CAAC;IACP,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,YAAY,CAAC;CACpB"}
|
package/dist/auth/middleware.js
CHANGED
|
@@ -35,7 +35,7 @@ export const create_auth_middleware_specs = async (deps, options) => {
|
|
|
35
35
|
import('./bearer_auth.js')
|
|
36
36
|
]);
|
|
37
37
|
const session_middleware = create_session_middleware(keyring, session_options);
|
|
38
|
-
const request_context_middleware = create_request_context_middleware(query_deps
|
|
38
|
+
const request_context_middleware = create_request_context_middleware(query_deps);
|
|
39
39
|
const bearer_auth_middleware = create_bearer_auth_middleware(query_deps, deps.log);
|
|
40
40
|
const specs = [
|
|
41
41
|
{
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
* @module
|
|
36
36
|
*/
|
|
37
37
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
38
|
-
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
39
38
|
import { type Account, type Actor, type RoleGrant } from './account_schema.js';
|
|
40
39
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
41
40
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
@@ -224,11 +223,10 @@ export declare const resolve_acting_actor: (deps: QueryDeps, account_id: string,
|
|
|
224
223
|
* `require_auth` / `require_role` enforce.
|
|
225
224
|
*
|
|
226
225
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
227
|
-
* @param log - the logger instance
|
|
228
226
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
229
227
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
|
|
230
228
|
*/
|
|
231
|
-
export declare const create_request_context_middleware: (deps: QueryDeps,
|
|
229
|
+
export declare const create_request_context_middleware: (deps: QueryDeps, session_context_key?: string) => MiddlewareHandler;
|
|
232
230
|
/**
|
|
233
231
|
* Refuse a non-RPC surface to a narrowed token — rule 3, for callers that are
|
|
234
232
|
* not route specs. Under the Rust twin's name
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"request_context.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/request_context.ts"],"names":[],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* @module
|
|
36
36
|
*/
|
|
37
37
|
import { is_role_grant_active } from './account_schema.js';
|
|
38
|
-
import { hash_session_token,
|
|
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
41
|
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
@@ -208,11 +208,10 @@ export const resolve_acting_actor = async (deps, account_id, acting_actor_id) =>
|
|
|
208
208
|
* `require_auth` / `require_role` enforce.
|
|
209
209
|
*
|
|
210
210
|
* @param deps - query dependencies (pool-level db for middleware)
|
|
211
|
-
* @param log - the logger instance
|
|
212
211
|
* @param session_context_key - the Hono context key where session middleware stored the session token
|
|
213
212
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
|
|
214
213
|
*/
|
|
215
|
-
export const create_request_context_middleware = (deps,
|
|
214
|
+
export const create_request_context_middleware = (deps, session_context_key = 'auth_session_id') => {
|
|
216
215
|
return async (c, next) => {
|
|
217
216
|
c.set(REQUEST_CONTEXT_KEY, null);
|
|
218
217
|
c.set(ACCOUNT_ID_KEY, null);
|
|
@@ -235,8 +234,6 @@ export const create_request_context_middleware = (deps, log, session_context_key
|
|
|
235
234
|
// A session is full account authority by construction.
|
|
236
235
|
c.set(TOKEN_SCOPE_KEY, token_scope_full());
|
|
237
236
|
c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
|
|
238
|
-
// Touch session (fire-and-forget, don't block the request)
|
|
239
|
-
void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
|
|
240
237
|
await next();
|
|
241
238
|
};
|
|
242
239
|
};
|
|
@@ -10,16 +10,15 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
import type { Keyring } from './keyring.js';
|
|
13
|
-
/** Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS). */
|
|
14
|
-
export declare const SESSION_AGE_MAX: number;
|
|
15
13
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS).
|
|
15
|
+
*
|
|
16
|
+
* This is an **absolute** session lifetime: the cookie is signed once at
|
|
17
|
+
* mint (`expires_at = mint + max_age`) and never re-signed, matching the DB
|
|
18
|
+
* row's fixed `expires_at`. There is deliberately no sliding renewal on
|
|
19
|
+
* either leg — see `docs/security.md` §Session Security.
|
|
21
20
|
*/
|
|
22
|
-
export declare const
|
|
21
|
+
export declare const SESSION_AGE_MAX: number;
|
|
23
22
|
/**
|
|
24
23
|
* Cookie options for session cookies.
|
|
25
24
|
*/
|
|
@@ -91,14 +90,6 @@ export interface SessionOptions<TIdentity> {
|
|
|
91
90
|
* cannot carry `maxAge` so the two values can't drift.
|
|
92
91
|
*/
|
|
93
92
|
max_age?: number;
|
|
94
|
-
/**
|
|
95
|
-
* Threshold (seconds) for expiration-based cookie refresh. When a parsed
|
|
96
|
-
* cookie's `expires_at - now <= refresh_threshold_seconds`,
|
|
97
|
-
* `process_session_cookie` returns `action: 'refresh'` with a freshly-signed
|
|
98
|
-
* value (extending the embedded expiration by `max_age`). Defaults to
|
|
99
|
-
* `SESSION_REFRESH_THRESHOLD_S` (1 day). Set to `0` to disable.
|
|
100
|
-
*/
|
|
101
|
-
refresh_threshold_seconds?: number;
|
|
102
93
|
cookie_options?: Partial<Omit<SessionCookieOptions, 'maxAge'>>;
|
|
103
94
|
/** Encode identity into the cookie payload (before the `:expires_at` suffix). */
|
|
104
95
|
encode_identity: (identity: TIdentity) => string;
|
|
@@ -111,26 +102,14 @@ export interface SessionOptions<TIdentity> {
|
|
|
111
102
|
export interface ParsedSession<TIdentity> {
|
|
112
103
|
/** The decoded identity. */
|
|
113
104
|
identity: TIdentity;
|
|
114
|
-
/**
|
|
115
|
-
should_refresh_signature: boolean;
|
|
116
|
-
/**
|
|
117
|
-
* True if the embedded `expires_at` is within
|
|
118
|
-
* `options.refresh_threshold_seconds` of `now`. Signals that the cookie is
|
|
119
|
-
* valid but should be re-signed to extend its lifetime — mirrors
|
|
120
|
-
* `query_session_touch`'s DB-side extension so the cookie and server
|
|
121
|
-
* session don't drift. Always false when the threshold is `0`.
|
|
122
|
-
*/
|
|
123
|
-
should_refresh_expiration: boolean;
|
|
124
|
-
/** Index of the key that verified the signature. */
|
|
105
|
+
/** Index of the key that verified the signature (0 = primary). */
|
|
125
106
|
key_index: number;
|
|
126
107
|
}
|
|
127
108
|
/**
|
|
128
109
|
* Parse a signed session cookie value.
|
|
129
110
|
*
|
|
130
111
|
* The signed value format is `${encode(identity)}:${expires_at}`.
|
|
131
|
-
* Tries all keys in order to support key rotation.
|
|
132
|
-
* `should_refresh_expiration` flag fires when the cookie is within
|
|
133
|
-
* `options.refresh_threshold_seconds` of `expires_at`.
|
|
112
|
+
* Tries all keys in order to support key rotation.
|
|
134
113
|
*
|
|
135
114
|
* @param signed_value - the raw cookie value (signed)
|
|
136
115
|
* @param keyring - key ring for verification
|
|
@@ -159,9 +138,7 @@ export interface ProcessSessionResult<TIdentity> {
|
|
|
159
138
|
/** Whether the session is valid. */
|
|
160
139
|
valid: boolean;
|
|
161
140
|
/** Action the adapter should take. */
|
|
162
|
-
action: 'none' | 'clear'
|
|
163
|
-
/** New signed value when action is 'refresh'. */
|
|
164
|
-
new_signed_value?: string;
|
|
141
|
+
action: 'none' | 'clear';
|
|
165
142
|
/** The decoded identity if the cookie was valid. */
|
|
166
143
|
identity?: TIdentity;
|
|
167
144
|
}
|
|
@@ -181,9 +158,14 @@ export declare const fuz_session_config: SessionOptions<string>;
|
|
|
181
158
|
/**
|
|
182
159
|
* Process a session cookie and determine what action to take.
|
|
183
160
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* `
|
|
161
|
+
* There is deliberately no refresh action: any re-sign computes a fresh
|
|
162
|
+
* `now + max_age` expiry, extending the cookie past the DB row's fixed
|
|
163
|
+
* `expires_at` — the holder would go silently anonymous while carrying a
|
|
164
|
+
* self-perpetuating cookie. The absolute lifetime is one signing at mint,
|
|
165
|
+
* on both legs, matching the Rust spine. A cookie signed by a retired
|
|
166
|
+
* (non-primary) key keeps verifying until the key leaves the keyring;
|
|
167
|
+
* retired keys are safe to drop after `SESSION_AGE_MAX` (see
|
|
168
|
+
* `docs/security.md` §Cookie Key Rotation).
|
|
187
169
|
*
|
|
188
170
|
* @param signed_value - the raw cookie value (may be undefined)
|
|
189
171
|
* @param keyring - key ring for verification and signing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C
|
|
1
|
+
{"version":3,"file":"session_cookie.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAoB,CAAC;AAQjD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,oBAMpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,cAAc,CAAC,SAAS;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,iFAAiF;IACjF,eAAe,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,MAAM,CAAC;IACjD,mEAAmE;IACnE,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,SAAS;IACvC,4BAA4B;IAC5B,QAAQ,EAAE,SAAS,CAAC;IACpB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAU,SAAS,EAC5C,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,CA+BrD,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GAAU,SAAS,EAC1D,SAAS,OAAO,EAChB,UAAU,SAAS,EACnB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS;IAC9C,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,sCAAsC;IACtC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,MAAM,KAAG,cAAc,CAAC,MAAM,CAK/E,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAE/F;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,sBAAsB,GAAU,SAAS,EACrD,cAAc,MAAM,GAAG,SAAS,EAChC,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,EAClC,cAAc,MAAM,KAClB,OAAO,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAgBzC,CAAC"}
|
|
@@ -9,16 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
-
/** Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS). */
|
|
13
|
-
export const SESSION_AGE_MAX = 60 * 60 * 24 * 30;
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Cookie max age in seconds (30 days — aligned with AUTH_SESSION_LIFETIME_MS).
|
|
14
|
+
*
|
|
15
|
+
* This is an **absolute** session lifetime: the cookie is signed once at
|
|
16
|
+
* mint (`expires_at = mint + max_age`) and never re-signed, matching the DB
|
|
17
|
+
* row's fixed `expires_at`. There is deliberately no sliding renewal on
|
|
18
|
+
* either leg — see `docs/security.md` §Session Security.
|
|
20
19
|
*/
|
|
21
|
-
export const
|
|
20
|
+
export const SESSION_AGE_MAX = 60 * 60 * 24 * 30;
|
|
22
21
|
/** Separator between identity payload and expires_at in signed value. */
|
|
23
22
|
const VALUE_SEPARATOR = ':';
|
|
24
23
|
/** Strict integer matcher for the embedded `expires_at` field. */
|
|
@@ -42,9 +41,7 @@ export const session_cookie_options = {
|
|
|
42
41
|
* Parse a signed session cookie value.
|
|
43
42
|
*
|
|
44
43
|
* The signed value format is `${encode(identity)}:${expires_at}`.
|
|
45
|
-
* Tries all keys in order to support key rotation.
|
|
46
|
-
* `should_refresh_expiration` flag fires when the cookie is within
|
|
47
|
-
* `options.refresh_threshold_seconds` of `expires_at`.
|
|
44
|
+
* Tries all keys in order to support key rotation.
|
|
48
45
|
*
|
|
49
46
|
* @param signed_value - the raw cookie value (signed)
|
|
50
47
|
* @param keyring - key ring for verification
|
|
@@ -79,12 +76,8 @@ export const parse_session = async (signed_value, keyring, options, now_seconds)
|
|
|
79
76
|
const now = now_seconds ?? Math.floor(Date.now() / 1000);
|
|
80
77
|
if (expires_at <= now)
|
|
81
78
|
return null;
|
|
82
|
-
const refresh_threshold = options.refresh_threshold_seconds ?? SESSION_REFRESH_THRESHOLD_S;
|
|
83
|
-
const should_refresh_expiration = refresh_threshold > 0 && expires_at - now <= refresh_threshold;
|
|
84
79
|
return {
|
|
85
80
|
identity,
|
|
86
|
-
should_refresh_signature: result.key_index > 0,
|
|
87
|
-
should_refresh_expiration,
|
|
88
81
|
key_index: result.key_index
|
|
89
82
|
};
|
|
90
83
|
};
|
|
@@ -128,9 +121,14 @@ export const fuz_session_config = create_session_config('fuz_session');
|
|
|
128
121
|
/**
|
|
129
122
|
* Process a session cookie and determine what action to take.
|
|
130
123
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* `
|
|
124
|
+
* There is deliberately no refresh action: any re-sign computes a fresh
|
|
125
|
+
* `now + max_age` expiry, extending the cookie past the DB row's fixed
|
|
126
|
+
* `expires_at` — the holder would go silently anonymous while carrying a
|
|
127
|
+
* self-perpetuating cookie. The absolute lifetime is one signing at mint,
|
|
128
|
+
* on both legs, matching the Rust spine. A cookie signed by a retired
|
|
129
|
+
* (non-primary) key keeps verifying until the key leaves the keyring;
|
|
130
|
+
* retired keys are safe to drop after `SESSION_AGE_MAX` (see
|
|
131
|
+
* `docs/security.md` §Cookie Key Rotation).
|
|
134
132
|
*
|
|
135
133
|
* @param signed_value - the raw cookie value (may be undefined)
|
|
136
134
|
* @param keyring - key ring for verification and signing
|
|
@@ -149,13 +147,5 @@ export const process_session_cookie = async (signed_value, keyring, options, now
|
|
|
149
147
|
// Invalid cookie - should be cleared
|
|
150
148
|
return { valid: false, action: 'clear' };
|
|
151
149
|
}
|
|
152
|
-
// Valid session — re-sign if the verifying key isn't primary OR if the
|
|
153
|
-
// embedded expiration is approaching the threshold. The latter mirrors
|
|
154
|
-
// `query_session_touch`'s DB-side extension so a continuously-active user's
|
|
155
|
-
// cookie doesn't hard-expire while their server session is still alive.
|
|
156
|
-
if (parsed.should_refresh_signature || parsed.should_refresh_expiration) {
|
|
157
|
-
const new_signed_value = await create_session_cookie_value(keyring, parsed.identity, options, now);
|
|
158
|
-
return { valid: true, action: 'refresh', new_signed_value, identity: parsed.identity };
|
|
159
|
-
}
|
|
160
150
|
return { valid: true, action: 'none', identity: parsed.identity };
|
|
161
151
|
};
|
|
@@ -35,7 +35,7 @@ export declare const clear_session_cookie: <T>(c: Context, options: SessionOptio
|
|
|
35
35
|
*
|
|
36
36
|
* @param keyring - key ring for cookie verification
|
|
37
37
|
* @param options - session configuration
|
|
38
|
-
* @mutates Hono context - sets `options.context_key` and may
|
|
38
|
+
* @mutates Hono context - sets `options.context_key` and may clear the session cookie
|
|
39
39
|
*/
|
|
40
40
|
export declare const create_session_middleware: <TIdentity>(keyring: Keyring, options: SessionOptions<TIdentity>) => MiddlewareHandler;
|
|
41
41
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,
|
|
1
|
+
{"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAcF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
|
|
@@ -49,7 +49,7 @@ export const clear_session_cookie = (c, options) => {
|
|
|
49
49
|
*
|
|
50
50
|
* @param keyring - key ring for cookie verification
|
|
51
51
|
* @param options - session configuration
|
|
52
|
-
* @mutates Hono context - sets `options.context_key` and may
|
|
52
|
+
* @mutates Hono context - sets `options.context_key` and may clear the session cookie
|
|
53
53
|
*/
|
|
54
54
|
export const create_session_middleware = (keyring, options) => {
|
|
55
55
|
return async (c, next) => {
|
|
@@ -60,9 +60,6 @@ export const create_session_middleware = (keyring, options) => {
|
|
|
60
60
|
if (result.action === 'clear') {
|
|
61
61
|
clear_session_cookie(c, options);
|
|
62
62
|
}
|
|
63
|
-
else if (result.action === 'refresh' && result.new_signed_value) {
|
|
64
|
-
set_session_cookie(c, result.new_signed_value, options);
|
|
65
|
-
}
|
|
66
63
|
await next();
|
|
67
64
|
};
|
|
68
65
|
};
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
10
9
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
11
10
|
import type { AuthSession, SessionId } from './account_schema.js';
|
|
12
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Session lifetime in milliseconds (30 days).
|
|
13
|
+
*
|
|
14
|
+
* An **absolute** cap: `expires_at` is set once at mint and never extended —
|
|
15
|
+
* there is deliberately no touch/renewal query on either spine (a sliding
|
|
16
|
+
* window renews a leaked cookie forever; see `docs/security.md` §Session
|
|
17
|
+
* Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
|
|
18
|
+
*/
|
|
13
19
|
export declare const AUTH_SESSION_LIFETIME_MS: number;
|
|
14
|
-
/** Extend session when it has less than this remaining (1 day in ms). */
|
|
15
|
-
export declare const AUTH_SESSION_EXTEND_THRESHOLD_MS: number;
|
|
16
20
|
/**
|
|
17
21
|
* Hash a session token to its storage key using blake3.
|
|
18
22
|
*
|
|
@@ -46,16 +50,6 @@ export declare const query_create_session: (deps: QueryDeps, token_hash: string,
|
|
|
46
50
|
* @param token_hash - blake3 hash of the session token
|
|
47
51
|
*/
|
|
48
52
|
export declare const query_session_get_valid: (deps: QueryDeps, token_hash: string) => Promise<AuthSession | undefined>;
|
|
49
|
-
/**
|
|
50
|
-
* Update `last_seen_at` and optionally extend expiry for a session.
|
|
51
|
-
*
|
|
52
|
-
* Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
|
|
53
|
-
*
|
|
54
|
-
* @param deps - query dependencies
|
|
55
|
-
* @param token_hash - blake3 hash of the session token
|
|
56
|
-
* @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
|
|
57
|
-
*/
|
|
58
|
-
export declare const query_session_touch: (deps: QueryDeps, token_hash: string) => Promise<void>;
|
|
59
53
|
/**
|
|
60
54
|
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
61
55
|
*
|
|
@@ -137,9 +131,14 @@ export declare const query_session_enforce_limit: (deps: QueryDeps, account_id:
|
|
|
137
131
|
/**
|
|
138
132
|
* List all active sessions across all accounts with usernames.
|
|
139
133
|
*
|
|
134
|
+
* Ordered by `created_at DESC` (newest session first), matching the
|
|
135
|
+
* per-account listing. `last_seen_at` is still selected for the wire shape
|
|
136
|
+
* but is decorative — nothing updates it post-mint (it always equals
|
|
137
|
+
* `created_at`); it is slated for removal on its own twin migration.
|
|
138
|
+
*
|
|
140
139
|
* @param deps - query dependencies
|
|
141
140
|
* @param limit - maximum entries to return
|
|
142
|
-
* @returns active sessions joined with account usernames, newest
|
|
141
|
+
* @returns active sessions joined with account usernames, newest first
|
|
143
142
|
*/
|
|
144
143
|
export declare const query_session_list_all_active: (deps: QueryDeps, limit?: number) => Promise<Array<AuthSession & {
|
|
145
144
|
username: string;
|
|
@@ -151,19 +150,4 @@ export declare const query_session_list_all_active: (deps: QueryDeps, limit?: nu
|
|
|
151
150
|
* @mutates `auth_session` table - deletes every row past `expires_at`
|
|
152
151
|
*/
|
|
153
152
|
export declare const query_session_cleanup_expired: (deps: QueryDeps) => Promise<number>;
|
|
154
|
-
/**
|
|
155
|
-
* Touch a session without blocking the caller.
|
|
156
|
-
*
|
|
157
|
-
* Errors are logged to console — session touching never breaks request flows.
|
|
158
|
-
* Pass `pending_effects` (from `c.var.pending_effects`) to register
|
|
159
|
-
* the promise for test flushing.
|
|
160
|
-
*
|
|
161
|
-
* @param deps - query dependencies
|
|
162
|
-
* @param token_hash - blake3 hash of the session token
|
|
163
|
-
* @param pending_effects - optional array to register the effect for later awaiting
|
|
164
|
-
* @param log - the logger instance
|
|
165
|
-
* @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
|
|
166
|
-
* @mutates `pending_effects` - pushes the in-flight settled promise when provided
|
|
167
|
-
*/
|
|
168
|
-
export declare const session_touch_fire_and_forget: (deps: QueryDeps, token_hash: string, pending_effects: Array<Promise<void>> | undefined, log: Logger) => Promise<void>;
|
|
169
153
|
//# sourceMappingURL=session_queries.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
|
|
10
10
|
import { generate_random_base64url } from '../crypto.js';
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Session lifetime in milliseconds (30 days).
|
|
13
|
+
*
|
|
14
|
+
* An **absolute** cap: `expires_at` is set once at mint and never extended —
|
|
15
|
+
* there is deliberately no touch/renewal query on either spine (a sliding
|
|
16
|
+
* window renews a leaked cookie forever; see `docs/security.md` §Session
|
|
17
|
+
* Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
|
|
18
|
+
*/
|
|
12
19
|
export const AUTH_SESSION_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
|
|
13
|
-
/** Extend session when it has less than this remaining (1 day in ms). */
|
|
14
|
-
export const AUTH_SESSION_EXTEND_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
15
20
|
/**
|
|
16
21
|
* Hash a session token to its storage key using blake3.
|
|
17
22
|
*
|
|
@@ -57,25 +62,6 @@ export const query_create_session = async (deps, token_hash, account_id, expires
|
|
|
57
62
|
export const query_session_get_valid = async (deps, token_hash) => {
|
|
58
63
|
return deps.db.query_one(`SELECT * FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
|
|
59
64
|
};
|
|
60
|
-
/**
|
|
61
|
-
* Update `last_seen_at` and optionally extend expiry for a session.
|
|
62
|
-
*
|
|
63
|
-
* Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
|
|
64
|
-
*
|
|
65
|
-
* @param deps - query dependencies
|
|
66
|
-
* @param token_hash - blake3 hash of the session token
|
|
67
|
-
* @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
|
|
68
|
-
*/
|
|
69
|
-
export const query_session_touch = async (deps, token_hash) => {
|
|
70
|
-
const new_expires = new Date(Date.now() + AUTH_SESSION_LIFETIME_MS);
|
|
71
|
-
await deps.db.query(`UPDATE auth_session
|
|
72
|
-
SET last_seen_at = NOW(),
|
|
73
|
-
expires_at = CASE
|
|
74
|
-
WHEN expires_at - NOW() < INTERVAL '1 day' THEN $2::timestamptz
|
|
75
|
-
ELSE expires_at
|
|
76
|
-
END
|
|
77
|
-
WHERE id = $1`, [token_hash, new_expires.toISOString()]);
|
|
78
|
-
};
|
|
79
65
|
/**
|
|
80
66
|
* Revoke (delete) a session by its token hash, with no account scoping.
|
|
81
67
|
*
|
|
@@ -176,16 +162,21 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
|
|
|
176
162
|
/**
|
|
177
163
|
* List all active sessions across all accounts with usernames.
|
|
178
164
|
*
|
|
165
|
+
* Ordered by `created_at DESC` (newest session first), matching the
|
|
166
|
+
* per-account listing. `last_seen_at` is still selected for the wire shape
|
|
167
|
+
* but is decorative — nothing updates it post-mint (it always equals
|
|
168
|
+
* `created_at`); it is slated for removal on its own twin migration.
|
|
169
|
+
*
|
|
179
170
|
* @param deps - query dependencies
|
|
180
171
|
* @param limit - maximum entries to return
|
|
181
|
-
* @returns active sessions joined with account usernames, newest
|
|
172
|
+
* @returns active sessions joined with account usernames, newest first
|
|
182
173
|
*/
|
|
183
174
|
export const query_session_list_all_active = async (deps, limit = 200) => {
|
|
184
175
|
return deps.db.query(`SELECT s.id, s.account_id, s.created_at, s.expires_at, s.last_seen_at, a.username
|
|
185
176
|
FROM auth_session s
|
|
186
177
|
JOIN account a ON a.id = s.account_id
|
|
187
178
|
WHERE s.expires_at > NOW()
|
|
188
|
-
ORDER BY s.
|
|
179
|
+
ORDER BY s.created_at DESC, s.id DESC LIMIT $1`, [limit]);
|
|
189
180
|
};
|
|
190
181
|
/**
|
|
191
182
|
* Delete expired sessions.
|
|
@@ -197,24 +188,3 @@ export const query_session_cleanup_expired = async (deps) => {
|
|
|
197
188
|
const rows = await deps.db.query(`DELETE FROM auth_session WHERE expires_at <= NOW() RETURNING id`);
|
|
198
189
|
return rows.length;
|
|
199
190
|
};
|
|
200
|
-
/**
|
|
201
|
-
* Touch a session without blocking the caller.
|
|
202
|
-
*
|
|
203
|
-
* Errors are logged to console — session touching never breaks request flows.
|
|
204
|
-
* Pass `pending_effects` (from `c.var.pending_effects`) to register
|
|
205
|
-
* the promise for test flushing.
|
|
206
|
-
*
|
|
207
|
-
* @param deps - query dependencies
|
|
208
|
-
* @param token_hash - blake3 hash of the session token
|
|
209
|
-
* @param pending_effects - optional array to register the effect for later awaiting
|
|
210
|
-
* @param log - the logger instance
|
|
211
|
-
* @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
|
|
212
|
-
* @mutates `pending_effects` - pushes the in-flight settled promise when provided
|
|
213
|
-
*/
|
|
214
|
-
export const session_touch_fire_and_forget = (deps, token_hash, pending_effects, log) => {
|
|
215
|
-
const p = query_session_touch(deps, token_hash).catch((err) => {
|
|
216
|
-
log.error('Session touch failed:', err);
|
|
217
|
-
});
|
|
218
|
-
pending_effects?.push(p);
|
|
219
|
-
return p;
|
|
220
|
-
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token lifetime — the temporal axis of a minted credential.
|
|
3
|
+
*
|
|
4
|
+
* `lifetime` is **required** on `account_token_create`, mirroring `scope`:
|
|
5
|
+
* there is deliberately no default. A caller who wants a never-expiring token
|
|
6
|
+
* spells `{kind: 'eternal'}`, and the request records that they did — an
|
|
7
|
+
* omitted lifetime is a validation error upstream, not an eternal token.
|
|
8
|
+
* `expires_at IS NULL` therefore means "deliberately eternal", never "minted
|
|
9
|
+
* before the policy existed".
|
|
10
|
+
*
|
|
11
|
+
* Lifetime is deliberately **not** part of the scope document — `TokenScope`
|
|
12
|
+
* is strict on both spines and sits on the hot resolve path. The expiry lives
|
|
13
|
+
* in the indexed `api_token.expires_at` column where the SQL validation gate
|
|
14
|
+
* already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
|
|
15
|
+
*
|
|
16
|
+
* A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
|
|
17
|
+
* has an expiry story (an expiry-aware read + a 401 hint); see
|
|
18
|
+
* `docs/security.md` §API tokens.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/**
|
|
24
|
+
* Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
|
|
25
|
+
* Shared with the Rust twin so out-of-range requests 400 identically on both
|
|
26
|
+
* spines.
|
|
27
|
+
*/
|
|
28
|
+
export declare const TOKEN_TTL_DAYS_MAX = 36500;
|
|
29
|
+
/**
|
|
30
|
+
* Caller-provided lifetime for `account_token_create`.
|
|
31
|
+
*
|
|
32
|
+
* `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
|
|
33
|
+
* to `days` from mint time. `eternal` is deliberately the first variant so
|
|
34
|
+
* schema-driven test generators produce it by default.
|
|
35
|
+
*/
|
|
36
|
+
export declare const TokenLifetimeInput: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
37
|
+
kind: z.ZodLiteral<"eternal">;
|
|
38
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"ttl">;
|
|
40
|
+
days: z.ZodNumber;
|
|
41
|
+
}, z.core.$strict>], "kind">;
|
|
42
|
+
export type TokenLifetimeInput = z.infer<typeof TokenLifetimeInput>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a lifetime input to the `expires_at` value the mint query stores.
|
|
45
|
+
*
|
|
46
|
+
* @param lifetime - the validated lifetime input
|
|
47
|
+
* @param now - mint time (injectable for tests; defaults to the current time)
|
|
48
|
+
* @returns the expiry `Date`, or `null` for an eternal token
|
|
49
|
+
*/
|
|
50
|
+
export declare const token_lifetime_to_expires_at: (lifetime: TokenLifetimeInput, now?: Date) => Date | null;
|
|
51
|
+
//# sourceMappingURL=token_lifetime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_lifetime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;4BAM7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAU,kBAAkB,EAC5B,MAAK,IAAiB,KACpB,IAAI,GAAG,IAGT,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token lifetime — the temporal axis of a minted credential.
|
|
3
|
+
*
|
|
4
|
+
* `lifetime` is **required** on `account_token_create`, mirroring `scope`:
|
|
5
|
+
* there is deliberately no default. A caller who wants a never-expiring token
|
|
6
|
+
* spells `{kind: 'eternal'}`, and the request records that they did — an
|
|
7
|
+
* omitted lifetime is a validation error upstream, not an eternal token.
|
|
8
|
+
* `expires_at IS NULL` therefore means "deliberately eternal", never "minted
|
|
9
|
+
* before the policy existed".
|
|
10
|
+
*
|
|
11
|
+
* Lifetime is deliberately **not** part of the scope document — `TokenScope`
|
|
12
|
+
* is strict on both spines and sits on the hot resolve path. The expiry lives
|
|
13
|
+
* in the indexed `api_token.expires_at` column where the SQL validation gate
|
|
14
|
+
* already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
|
|
15
|
+
*
|
|
16
|
+
* A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
|
|
17
|
+
* has an expiry story (an expiry-aware read + a 401 hint); see
|
|
18
|
+
* `docs/security.md` §API tokens.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/**
|
|
24
|
+
* Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
|
|
25
|
+
* Shared with the Rust twin so out-of-range requests 400 identically on both
|
|
26
|
+
* spines.
|
|
27
|
+
*/
|
|
28
|
+
export const TOKEN_TTL_DAYS_MAX = 36_500;
|
|
29
|
+
/**
|
|
30
|
+
* Caller-provided lifetime for `account_token_create`.
|
|
31
|
+
*
|
|
32
|
+
* `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
|
|
33
|
+
* to `days` from mint time. `eternal` is deliberately the first variant so
|
|
34
|
+
* schema-driven test generators produce it by default.
|
|
35
|
+
*/
|
|
36
|
+
export const TokenLifetimeInput = z.discriminatedUnion('kind', [
|
|
37
|
+
z.strictObject({ kind: z.literal('eternal') }),
|
|
38
|
+
z.strictObject({
|
|
39
|
+
kind: z.literal('ttl'),
|
|
40
|
+
days: z.number().int().min(1).max(TOKEN_TTL_DAYS_MAX)
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a lifetime input to the `expires_at` value the mint query stores.
|
|
45
|
+
*
|
|
46
|
+
* @param lifetime - the validated lifetime input
|
|
47
|
+
* @param now - mint time (injectable for tests; defaults to the current time)
|
|
48
|
+
* @returns the expiry `Date`, or `null` for an eternal token
|
|
49
|
+
*/
|
|
50
|
+
export const token_lifetime_to_expires_at = (lifetime, now = new Date()) => {
|
|
51
|
+
if (lifetime.kind === 'eternal')
|
|
52
|
+
return null;
|
|
53
|
+
return new Date(now.getTime() + lifetime.days * 86_400_000);
|
|
54
|
+
};
|