@fuzdev/fuz_app 0.108.0 → 0.110.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/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +7 -6
- 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/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- 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 +26 -7
- 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/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +25 -8
- 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/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- 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/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +12 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -1
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- 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 +36 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +10 -2
- 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 +10 -2
- 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/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -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/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- 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/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- package/package.json +1 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon token
|
|
2
|
+
* Daemon token middleware — the credential **consumer**.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Validates a presented `X-Daemon-Token` and resolves the keeper account.
|
|
5
|
+
* The producer half (rotation + file persistence) lives in
|
|
6
|
+
* `testing/daemon_token_rotation.ts` behind the dev-env guard: no production
|
|
7
|
+
* assembly mints daemon tokens — the credential's only remaining role is the
|
|
8
|
+
* cross-process test harness's keeper channel — mirroring the Rust spine,
|
|
9
|
+
* whose producer is confined to `fuz_testing`.
|
|
6
10
|
*
|
|
7
11
|
* Pure token primitives (schema, generation, validation) live in `auth/daemon_token.ts`.
|
|
8
12
|
* See docs/identity.md for design rationale.
|
|
@@ -11,40 +15,8 @@
|
|
|
11
15
|
*/
|
|
12
16
|
import type { MiddlewareHandler } from 'hono';
|
|
13
17
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
|
-
import { type FsWriteDeps, type FsRemoveDeps, type EnvDeps } from '../runtime/deps.js';
|
|
15
18
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
16
19
|
import { type DaemonTokenState } from './daemon_token.js';
|
|
17
|
-
/** Default rotation interval in milliseconds (30 seconds). */
|
|
18
|
-
export declare const DEFAULT_ROTATION_INTERVAL_MS = 30000;
|
|
19
|
-
/** Deps for writing the daemon token to disk. */
|
|
20
|
-
export type DaemonTokenWriteDeps = Pick<EnvDeps, 'env_get'> & Pick<FsWriteDeps, 'mkdir' | 'write_text_file' | 'rename'> & {
|
|
21
|
-
/** Set file permissions. Optional — consumers provide when available (e.g. `Deno.chmod`). */
|
|
22
|
-
chmod?: (path: string, mode: number) => Promise<void>;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Get the daemon token file path (`~/.{name}/run/daemon_token`).
|
|
26
|
-
*
|
|
27
|
-
* @param runtime - runtime with `env_get` capability
|
|
28
|
-
* @param name - application name
|
|
29
|
-
* @returns path to `daemon_token`, or `null` if `$HOME` is not set
|
|
30
|
-
*/
|
|
31
|
-
export declare const get_daemon_token_path: (runtime: Pick<EnvDeps, "env_get">, name: string) => string | null;
|
|
32
|
-
/**
|
|
33
|
-
* Write the current token to disk atomically.
|
|
34
|
-
*
|
|
35
|
-
* Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
|
|
36
|
-
*
|
|
37
|
-
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
38
|
-
* future fields (rotated_at, version) without changing every reader. Both
|
|
39
|
-
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
40
|
-
* and the Rust daemon-token writer match this shape.
|
|
41
|
-
*
|
|
42
|
-
* @param runtime - runtime with file write capabilities
|
|
43
|
-
* @param token_path - path to write the token
|
|
44
|
-
* @param token - the raw token string
|
|
45
|
-
* @mutates filesystem - writes `token_path` atomically and `chmod 0600` when supported
|
|
46
|
-
*/
|
|
47
|
-
export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_path: string, token: string) => Promise<void>;
|
|
48
20
|
/**
|
|
49
21
|
* Resolve the keeper account ID by querying for the account with an active
|
|
50
22
|
* keeper role_grant.
|
|
@@ -61,40 +33,6 @@ export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_p
|
|
|
61
33
|
* @returns the keeper account ID, or `null` if no keeper exists yet (pre-bootstrap)
|
|
62
34
|
*/
|
|
63
35
|
export declare const resolve_keeper_account_id: (deps: QueryDeps) => Promise<string | null>;
|
|
64
|
-
/** Options for daemon token rotation. */
|
|
65
|
-
export interface DaemonTokenRotationOptions {
|
|
66
|
-
/**
|
|
67
|
-
* Absolute path the token file is written to. Caller computes from
|
|
68
|
-
* its own conventions — e.g. `get_daemon_token_path(runtime, app_name)`
|
|
69
|
-
* for the standard `~/.{name}/run/daemon_token` layout, or a path
|
|
70
|
-
* derived from `PUBLIC_<APP>_DIR` for cross-process test setups that
|
|
71
|
-
* isolate the app dir to a tmpdir.
|
|
72
|
-
*/
|
|
73
|
-
token_path: string;
|
|
74
|
-
/** Rotation interval in ms. Default: `30000` (30s). */
|
|
75
|
-
rotation_interval_ms?: number;
|
|
76
|
-
}
|
|
77
|
-
/** Result of starting daemon token rotation. */
|
|
78
|
-
export interface DaemonTokenRotation {
|
|
79
|
-
/** The mutable runtime state. Pass to `create_daemon_token_middleware`. */
|
|
80
|
-
state: DaemonTokenState;
|
|
81
|
-
/** Stop rotation, clean up the interval, and delete the token file. Call on graceful shutdown. */
|
|
82
|
-
stop: () => Promise<void>;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Start daemon token rotation.
|
|
86
|
-
*
|
|
87
|
-
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
88
|
-
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
89
|
-
*
|
|
90
|
-
* @param runtime - runtime with file and remove capabilities
|
|
91
|
-
* @param deps - query dependencies for resolving keeper account
|
|
92
|
-
* @param options - rotation configuration
|
|
93
|
-
* @param log - the logger instance
|
|
94
|
-
* @returns rotation state and stop function
|
|
95
|
-
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
96
|
-
*/
|
|
97
|
-
export declare const start_daemon_token_rotation: (runtime: DaemonTokenWriteDeps & FsRemoveDeps, deps: QueryDeps, options: DaemonTokenRotationOptions, log: Logger) => Promise<DaemonTokenRotation>;
|
|
98
36
|
/**
|
|
99
37
|
* Create middleware that authenticates via daemon token.
|
|
100
38
|
*
|
|
@@ -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
|
|
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;AAWtD,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,iBA0EF,CAAC"}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon token
|
|
2
|
+
* Daemon token middleware — the credential **consumer**.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Validates a presented `X-Daemon-Token` and resolves the keeper account.
|
|
5
|
+
* The producer half (rotation + file persistence) lives in
|
|
6
|
+
* `testing/daemon_token_rotation.ts` behind the dev-env guard: no production
|
|
7
|
+
* assembly mints daemon tokens — the credential's only remaining role is the
|
|
8
|
+
* cross-process test harness's keeper channel — mirroring the Rust spine,
|
|
9
|
+
* whose producer is confined to `fuz_testing`.
|
|
6
10
|
*
|
|
7
11
|
* Pure token primitives (schema, generation, validation) live in `auth/daemon_token.ts`.
|
|
8
12
|
* See docs/identity.md for design rationale.
|
|
@@ -10,49 +14,12 @@
|
|
|
10
14
|
* @module
|
|
11
15
|
*/
|
|
12
16
|
import { DEV } from 'esm-env';
|
|
13
|
-
import {} from '../runtime/deps.js';
|
|
14
|
-
import { write_file_atomic } from '../runtime/fs.js';
|
|
15
|
-
import { get_app_dir } from '../cli/config.js';
|
|
16
17
|
import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY } from '../hono_context.js';
|
|
17
18
|
import { token_scope_full } from './token_scope.js';
|
|
18
19
|
import { is_browser_context } from '../http/origin.js';
|
|
19
20
|
import { query_role_grant_find_account_id_for_role } from './role_grant_queries.js';
|
|
20
21
|
import { ROLE_KEEPER } from './role_schema.js';
|
|
21
|
-
import { DaemonToken, DAEMON_TOKEN_HEADER,
|
|
22
|
-
/** Default rotation interval in milliseconds (30 seconds). */
|
|
23
|
-
export const DEFAULT_ROTATION_INTERVAL_MS = 30_000;
|
|
24
|
-
/**
|
|
25
|
-
* Get the daemon token file path (`~/.{name}/run/daemon_token`).
|
|
26
|
-
*
|
|
27
|
-
* @param runtime - runtime with `env_get` capability
|
|
28
|
-
* @param name - application name
|
|
29
|
-
* @returns path to `daemon_token`, or `null` if `$HOME` is not set
|
|
30
|
-
*/
|
|
31
|
-
export const get_daemon_token_path = (runtime, name) => {
|
|
32
|
-
const app_dir = get_app_dir(runtime, name);
|
|
33
|
-
return app_dir ? `${app_dir}/run/daemon_token` : null;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Write the current token to disk atomically.
|
|
37
|
-
*
|
|
38
|
-
* Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
|
|
39
|
-
*
|
|
40
|
-
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
41
|
-
* future fields (rotated_at, version) without changing every reader. Both
|
|
42
|
-
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
43
|
-
* and the Rust daemon-token writer match this shape.
|
|
44
|
-
*
|
|
45
|
-
* @param runtime - runtime with file write capabilities
|
|
46
|
-
* @param token_path - path to write the token
|
|
47
|
-
* @param token - the raw token string
|
|
48
|
-
* @mutates filesystem - writes `token_path` atomically and `chmod 0600` when supported
|
|
49
|
-
*/
|
|
50
|
-
export const write_daemon_token = async (runtime, token_path, token) => {
|
|
51
|
-
await write_file_atomic(runtime, token_path, JSON.stringify({ token }) + '\n');
|
|
52
|
-
if (runtime.chmod) {
|
|
53
|
-
await runtime.chmod(token_path, 0o600);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
22
|
+
import { DaemonToken, DAEMON_TOKEN_HEADER, validate_daemon_token } from './daemon_token.js';
|
|
56
23
|
/**
|
|
57
24
|
* Resolve the keeper account ID by querying for the account with an active
|
|
58
25
|
* keeper role_grant.
|
|
@@ -71,69 +38,6 @@ export const write_daemon_token = async (runtime, token_path, token) => {
|
|
|
71
38
|
export const resolve_keeper_account_id = async (deps) => {
|
|
72
39
|
return query_role_grant_find_account_id_for_role(deps, ROLE_KEEPER);
|
|
73
40
|
};
|
|
74
|
-
/**
|
|
75
|
-
* Start daemon token rotation.
|
|
76
|
-
*
|
|
77
|
-
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
78
|
-
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
79
|
-
*
|
|
80
|
-
* @param runtime - runtime with file and remove capabilities
|
|
81
|
-
* @param deps - query dependencies for resolving keeper account
|
|
82
|
-
* @param options - rotation configuration
|
|
83
|
-
* @param log - the logger instance
|
|
84
|
-
* @returns rotation state and stop function
|
|
85
|
-
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
86
|
-
*/
|
|
87
|
-
export const start_daemon_token_rotation = async (runtime, deps, options, log) => {
|
|
88
|
-
const { token_path, rotation_interval_ms = DEFAULT_ROTATION_INTERVAL_MS } = options;
|
|
89
|
-
// ensure parent directory exists
|
|
90
|
-
const last_slash = token_path.lastIndexOf('/');
|
|
91
|
-
if (last_slash > 0) {
|
|
92
|
-
await runtime.mkdir(token_path.slice(0, last_slash), { recursive: true });
|
|
93
|
-
}
|
|
94
|
-
// resolve keeper account (may be null pre-bootstrap; the middleware
|
|
95
|
-
// lazily refreshes on the first null hit to cover the
|
|
96
|
-
// rotation-starts-before-bootstrap case)
|
|
97
|
-
const keeper_account_id = await resolve_keeper_account_id(deps);
|
|
98
|
-
// generate initial token and write to disk
|
|
99
|
-
const initial_token = generate_daemon_token();
|
|
100
|
-
await write_daemon_token(runtime, token_path, initial_token);
|
|
101
|
-
const state = {
|
|
102
|
-
current_token: initial_token,
|
|
103
|
-
previous_token: null,
|
|
104
|
-
rotated_at: new Date(),
|
|
105
|
-
keeper_account_id
|
|
106
|
-
};
|
|
107
|
-
let writing = false;
|
|
108
|
-
const interval_id = setInterval(async () => {
|
|
109
|
-
if (writing)
|
|
110
|
-
return; // skip if previous rotation write still in progress
|
|
111
|
-
writing = true;
|
|
112
|
-
try {
|
|
113
|
-
const new_token = generate_daemon_token();
|
|
114
|
-
state.previous_token = state.current_token;
|
|
115
|
-
state.current_token = new_token;
|
|
116
|
-
state.rotated_at = new Date();
|
|
117
|
-
await write_daemon_token(runtime, token_path, new_token);
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
log.error('Failed to write rotated token:', err);
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
writing = false;
|
|
124
|
-
}
|
|
125
|
-
}, rotation_interval_ms);
|
|
126
|
-
const stop = async () => {
|
|
127
|
-
clearInterval(interval_id);
|
|
128
|
-
try {
|
|
129
|
-
await runtime.remove(token_path);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// already removed or never written
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
return { state, stop };
|
|
136
|
-
};
|
|
137
41
|
/**
|
|
138
42
|
* Create middleware that authenticates via daemon token.
|
|
139
43
|
*
|
package/dist/auth/deps.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ import type { FactStore } from '@fuzdev/fuz_util/fact_store.ts';
|
|
|
12
12
|
import type { Keyring } from './keyring.js';
|
|
13
13
|
import type { PasswordHashDeps } from './password.js';
|
|
14
14
|
import type { Db } from '../db/db.js';
|
|
15
|
-
import type { StatResult } from '../runtime/deps.js';
|
|
16
15
|
import type { AuditEmitter } from './audit_emitter.js';
|
|
17
16
|
/**
|
|
18
17
|
* Stateless capabilities bundle for fuz_app backends.
|
|
@@ -21,10 +20,15 @@ import type { AuditEmitter } from './audit_emitter.js';
|
|
|
21
20
|
* Does not contain config (static values) or runtime state (mutable refs).
|
|
22
21
|
*/
|
|
23
22
|
export interface AppDeps {
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Hardened secret-file read — used for the bootstrap token (the file that
|
|
25
|
+
* mints the keeper account). Production wiring passes the runtime's
|
|
26
|
+
* `read_secure_file` (`FsSecureReadDeps`), which rejects symlinks,
|
|
27
|
+
* group/other-accessible modes, and oversized files; both the boot-time
|
|
28
|
+
* availability probe and the request-time read go through this one
|
|
29
|
+
* capability so the probe can never be laxer than the read it gates.
|
|
30
|
+
*/
|
|
31
|
+
read_secure_file: (path: string) => Promise<Uint8Array>;
|
|
28
32
|
/** Delete a file. */
|
|
29
33
|
delete_file: (path: string) => Promise<void>;
|
|
30
34
|
/** HMAC-SHA256 cookie signing keyring. */
|
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
|
{
|
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
* DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
|
|
38
38
|
* the freeze is the contract for the deployed chain, not local iteration.
|
|
39
39
|
*
|
|
40
|
+
* **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
|
|
41
|
+
* and the two names must match byte for byte: the `_testing_migration_tracker`
|
|
42
|
+
* parity gate compares the `(namespace, name, sequence)` rows both spines
|
|
43
|
+
* record, and a divergence there breaks the swap-freely invariant (a consumer
|
|
44
|
+
* pointing either implementation at one database). Append on both sides in the
|
|
45
|
+
* same change, so no entry below repeats the rule.
|
|
46
|
+
*
|
|
40
47
|
* @module
|
|
41
48
|
*/
|
|
42
49
|
import type { Migration, MigrationNamespace } from '../db/migrate.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAmK5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/auth/migrations.js
CHANGED
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
* DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
|
|
38
38
|
* the freeze is the contract for the deployed chain, not local iteration.
|
|
39
39
|
*
|
|
40
|
+
* **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
|
|
41
|
+
* and the two names must match byte for byte: the `_testing_migration_tracker`
|
|
42
|
+
* parity gate compares the `(namespace, name, sequence)` rows both spines
|
|
43
|
+
* record, and a divergence there breaks the swap-freely invariant (a consumer
|
|
44
|
+
* pointing either implementation at one database). Append on both sides in the
|
|
45
|
+
* same change, so no entry below repeats the rule.
|
|
46
|
+
*
|
|
40
47
|
* @module
|
|
41
48
|
*/
|
|
42
49
|
import { ACCOUNT_SCHEMA, ACCOUNT_EMAIL_INDEX, ACCOUNT_USERNAME_CI_INDEX, ACTOR_SCHEMA, ACTOR_INDEX, ACTOR_NAME_LOWER_INDEX, ROLE_GRANT_SCHEMA, ROLE_GRANT_INDEXES, AUTH_SESSION_SCHEMA, AUTH_SESSION_INDEXES, API_TOKEN_SCHEMA, API_TOKEN_INDEX, BOOTSTRAP_LOCK_SCHEMA, BOOTSTRAP_LOCK_SEED, INVITE_SCHEMA, INVITE_INDEXES, APP_SETTINGS_SCHEMA, APP_SETTINGS_SEED } from './auth_ddl.js';
|
|
@@ -156,10 +163,6 @@ export const auth_migrations = [
|
|
|
156
163
|
},
|
|
157
164
|
// v2: per-credential token scoping — `api_token.scope JSONB NOT NULL`.
|
|
158
165
|
//
|
|
159
|
-
// Name must match the Rust twin's migration byte for byte
|
|
160
|
-
// (`fuz_auth/src/migrations.rs`) or the `_testing_migration_tracker` parity
|
|
161
|
-
// gate fails and the swap-freely invariant breaks.
|
|
162
|
-
//
|
|
163
166
|
// Three statements, and the middle one is the whole compatibility story:
|
|
164
167
|
//
|
|
165
168
|
// 1. Add the column nullable, so the ALTER is instant on a populated table.
|
|
@@ -201,15 +204,29 @@ export const auth_migrations = [
|
|
|
201
204
|
// would mean the two impls could silently disagree about which credential
|
|
202
205
|
// answered; the constraint makes that unrepresentable instead of merely
|
|
203
206
|
// improbable.
|
|
204
|
-
//
|
|
205
|
-
// The name must match the Rust twin's `api_token_hash_unique_index` byte for
|
|
206
|
-
// byte or the `_testing_migration_tracker` parity gate fails and the
|
|
207
|
-
// swap-freely invariant breaks.
|
|
208
207
|
{
|
|
209
208
|
name: 'api_token_hash_unique_index',
|
|
210
209
|
up: async (db) => {
|
|
211
210
|
await db.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_api_token_hash ON api_token(token_hash)');
|
|
212
211
|
}
|
|
212
|
+
},
|
|
213
|
+
// The GIN the per-cell audit timeline assumes.
|
|
214
|
+
//
|
|
215
|
+
// `full_auth_schema` ships only btree indexes (seq, account, event_type,
|
|
216
|
+
// targets), but `db/cell_audit_queries.ts` reconstructs the per-cell
|
|
217
|
+
// timeline by OR-ing `metadata @> '{...}'::jsonb` containment clauses —
|
|
218
|
+
// sequential scans without this index, degrading with audit volume.
|
|
219
|
+
//
|
|
220
|
+
// `jsonb_path_ops` rather than the default `jsonb_ops`: it serves exactly
|
|
221
|
+
// the `@>` containment operator these queries use, with a smaller, faster
|
|
222
|
+
// index. Server PG and PGlite honor the opclass; PG-compatible engines
|
|
223
|
+
// without operator classes parse and ignore it, serving `@>` from their
|
|
224
|
+
// generic GIN.
|
|
225
|
+
{
|
|
226
|
+
name: 'audit_log_metadata_gin_index',
|
|
227
|
+
up: async (db) => {
|
|
228
|
+
await db.query('CREATE INDEX IF NOT EXISTS idx_audit_log_metadata ON audit_log USING gin (metadata jsonb_path_ops)');
|
|
229
|
+
}
|
|
213
230
|
}
|
|
214
231
|
];
|
|
215
232
|
/** Pre-composed migration namespace for auth tables. */
|
|
@@ -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
|
};
|