@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the **API token lifetime** axis of
|
|
4
|
+
* `account_token_create` — the round-trip that keeps a one-sided landing
|
|
5
|
+
* impossible.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this suite exists
|
|
8
|
+
*
|
|
9
|
+
* The action-manifest parity gate captures `{method, side_effects, auth}` and
|
|
10
|
+
* **no param schemas**, while the TS spine validates params against a Zod
|
|
11
|
+
* `strictObject` and the Rust spine hand-parses with `params.get(...)`. So a
|
|
12
|
+
* spine that silently ignored the `lifetime` field would answer the same mint
|
|
13
|
+
* request with a **200 and an eternal token** — same method, same status,
|
|
14
|
+
* nothing red anywhere. The forcing function has to observe the *stored*
|
|
15
|
+
* state: mint with a TTL, then read `expires_at` back off the wire.
|
|
16
|
+
*
|
|
17
|
+
* ## What it pins
|
|
18
|
+
*
|
|
19
|
+
* - **A TTL mint stores a bounded expiry** — `account_token_create` with
|
|
20
|
+
* `{kind:'ttl', days}` returns a non-null `expires_at` (create output *and*
|
|
21
|
+
* `account_token_list`), landing within a generous window around
|
|
22
|
+
* `now + days`. A spine that dropped the field returns `null` and fails.
|
|
23
|
+
* - **An eternal mint stores NULL** — the control: `{kind:'eternal'}` yields
|
|
24
|
+
* `expires_at: null` on both reads.
|
|
25
|
+
* - **An omitted lifetime is refused** — `lifetime` is required; a mint
|
|
26
|
+
* without it must 400 `invalid_params` on both spines. This is the direct
|
|
27
|
+
* pin on the silently-ignoring-unknown-shapes hazard.
|
|
28
|
+
* - **Out-of-range days are refused** — `days: 0` 400s identically, so the
|
|
29
|
+
* two hand-rolled bounds checks can't drift.
|
|
30
|
+
*
|
|
31
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
32
|
+
* cross-backend suites. Runs both legs — in-process (`gro test`) and
|
|
33
|
+
* cross-process — through the shared `{setup_test}` protocol.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import { describe, test, assert } from 'vitest';
|
|
38
|
+
import { account_token_create_action_spec, account_token_list_action_spec, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
|
|
39
|
+
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
40
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
41
|
+
/** TTL used by the bounded-mint case. */
|
|
42
|
+
const TTL_DAYS = 30;
|
|
43
|
+
/**
|
|
44
|
+
* Window slack around `now + days` for the expiry assertion — generous (1h)
|
|
45
|
+
* because the TS spine computes the expiry from the handler clock while the
|
|
46
|
+
* Rust spine binds a `SystemTime` computed at dispatch; only gross divergence
|
|
47
|
+
* (wrong unit, wrong sign, silently-dropped field) should fail.
|
|
48
|
+
*/
|
|
49
|
+
const EXPIRY_SLACK_MS = 60 * 60 * 1000;
|
|
50
|
+
export const describe_token_lifetime_cross_tests = (options) => {
|
|
51
|
+
const { setup_test } = options;
|
|
52
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
53
|
+
describe('api token lifetime parity', () => {
|
|
54
|
+
test('a ttl mint stores a bounded expiry (create output + list round-trip)', async () => {
|
|
55
|
+
const fixture = await setup_test();
|
|
56
|
+
const before = Date.now();
|
|
57
|
+
const create_res = await cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, {
|
|
58
|
+
name: 'ttl-token',
|
|
59
|
+
scope: { kind: 'full' },
|
|
60
|
+
lifetime: { kind: 'ttl', days: TTL_DAYS }
|
|
61
|
+
}, fixture.create_session_headers());
|
|
62
|
+
const created = expect_output(create_res, TokenCreateOutput);
|
|
63
|
+
assert.ok(created.expires_at !== null, 'a ttl mint must return a non-null expires_at — null means the spine dropped the field');
|
|
64
|
+
const expires_ms = Date.parse(created.expires_at);
|
|
65
|
+
assert.ok(!Number.isNaN(expires_ms), `expires_at must parse as a date: ${created.expires_at}`);
|
|
66
|
+
const expected = before + TTL_DAYS * 86_400_000;
|
|
67
|
+
assert.ok(Math.abs(expires_ms - expected) < EXPIRY_SLACK_MS, `expires_at must land near now + ${TTL_DAYS}d (got ${created.expires_at})`);
|
|
68
|
+
// The read-back — the half a silently-ignoring spine can't fake.
|
|
69
|
+
const list_res = await cross_rpc_call(fixture.transport, rpc_path, account_token_list_action_spec.method, undefined, fixture.create_session_headers());
|
|
70
|
+
const listed = expect_output(list_res, TokenListOutput);
|
|
71
|
+
const row = listed.tokens.find((t) => t.id === created.id);
|
|
72
|
+
assert.ok(row, 'the minted token must appear in account_token_list');
|
|
73
|
+
assert.ok(row.expires_at !== null, 'account_token_list must report the stored expiry, not null');
|
|
74
|
+
});
|
|
75
|
+
test('an eternal mint stores NULL expiry', async () => {
|
|
76
|
+
const fixture = await setup_test();
|
|
77
|
+
const create_res = await cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, { name: 'eternal-token', scope: { kind: 'full' }, lifetime: { kind: 'eternal' } }, fixture.create_session_headers());
|
|
78
|
+
const created = expect_output(create_res, TokenCreateOutput);
|
|
79
|
+
assert.strictEqual(created.expires_at, null, 'an eternal mint must return expires_at: null');
|
|
80
|
+
const list_res = await cross_rpc_call(fixture.transport, rpc_path, account_token_list_action_spec.method, undefined, fixture.create_session_headers());
|
|
81
|
+
const listed = expect_output(list_res, TokenListOutput);
|
|
82
|
+
const row = listed.tokens.find((t) => t.id === created.id);
|
|
83
|
+
assert.ok(row, 'the minted token must appear in account_token_list');
|
|
84
|
+
assert.strictEqual(row.expires_at, null, 'the stored token must be eternal');
|
|
85
|
+
});
|
|
86
|
+
test('an omitted lifetime is refused with invalid_params on every spine', async () => {
|
|
87
|
+
const fixture = await setup_test();
|
|
88
|
+
const res = await cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, { name: 'no-lifetime', scope: { kind: 'full' } }, fixture.create_session_headers());
|
|
89
|
+
assert.ok(!res.ok, 'a mint without lifetime must be refused, never defaulted');
|
|
90
|
+
assert.strictEqual(res.error?.code, -32602, `expected invalid_params, got ${JSON.stringify(res.error)} (reason: ${String(error_reason(res))})`);
|
|
91
|
+
});
|
|
92
|
+
test('out-of-range ttl days are refused identically', async () => {
|
|
93
|
+
const fixture = await setup_test();
|
|
94
|
+
const res = await cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, { name: 'zero-days', scope: { kind: 'full' }, lifetime: { kind: 'ttl', days: 0 } }, fixture.create_session_headers());
|
|
95
|
+
assert.ok(!res.ok, 'days: 0 must be refused');
|
|
96
|
+
assert.strictEqual(res.error?.code, -32602, `expected invalid_params: ${JSON.stringify(res.error)}`);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -87,7 +87,7 @@ const surface_denial_body = (surface) => ({
|
|
|
87
87
|
* narrowed token on an account that lacks the gating role.
|
|
88
88
|
*/
|
|
89
89
|
const mint_narrowed_token = async (transport, rpc_path, session_headers) => {
|
|
90
|
-
const res = await cross_rpc_call(transport, rpc_path, account_token_create_action_spec.method, { scope: { kind: 'methods', methods: [ADMITTED_METHOD] } }, session_headers);
|
|
90
|
+
const res = await cross_rpc_call(transport, rpc_path, account_token_create_action_spec.method, { scope: { kind: 'methods', methods: [ADMITTED_METHOD] }, lifetime: { kind: 'eternal' } }, session_headers);
|
|
91
91
|
assert.ok(res.ok, `minting a narrowed token failed: ${JSON.stringify(res.error)}`);
|
|
92
92
|
const token = res.result?.token;
|
|
93
93
|
assert.ok(typeof token === 'string' && token.length > 0, 'mint returned no token');
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Daemon-token **producer** — rotation + file persistence, test-harness only.
|
|
4
|
+
*
|
|
5
|
+
* No production assembly mints daemon tokens: the credential's only remaining
|
|
6
|
+
* role is the cross-process test harness's keeper channel (`_testing_reset`
|
|
7
|
+
* etc.), so the producer lives here behind `assert_dev_env` where it cannot
|
|
8
|
+
* reach a production bundle. The *consumer* half — validating a presented
|
|
9
|
+
* `X-Daemon-Token` — stays in `auth/daemon_token_middleware.ts`, mirroring
|
|
10
|
+
* the Rust spine, whose producer is confined to `fuz_testing`.
|
|
11
|
+
*
|
|
12
|
+
* The token file is written atomically at mode `0600` via
|
|
13
|
+
* `write_file_atomic` (unique temp name + exclusive create), so it never
|
|
14
|
+
* exists group/other-readable — the old optional-`chmod` pattern let a
|
|
15
|
+
* default umask land it at `0644`.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
20
|
+
import type { EnvDeps, FsWriteDeps, FsRemoveDeps } from '../runtime/deps.js';
|
|
21
|
+
import type { QueryDeps } from '../db/query_deps.js';
|
|
22
|
+
import { type DaemonTokenState } from '../auth/daemon_token.js';
|
|
23
|
+
/** Default rotation interval in milliseconds (30 seconds). */
|
|
24
|
+
export declare const DEFAULT_ROTATION_INTERVAL_MS = 30000;
|
|
25
|
+
/** Deps for writing the daemon token to disk. */
|
|
26
|
+
export type DaemonTokenWriteDeps = Pick<EnvDeps, 'env_get'> & Pick<FsWriteDeps, 'mkdir' | 'write_text_file' | 'rename'>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the daemon token file path (`~/.{name}/run/daemon_token`).
|
|
29
|
+
*
|
|
30
|
+
* @param runtime - runtime with `env_get` capability
|
|
31
|
+
* @param name - application name
|
|
32
|
+
* @returns path to `daemon_token`, or `null` if `$HOME` is not set
|
|
33
|
+
*/
|
|
34
|
+
export declare const get_daemon_token_path: (runtime: Pick<EnvDeps, "env_get">, name: string) => string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Write the current token to disk atomically at mode `0600`.
|
|
37
|
+
*
|
|
38
|
+
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
39
|
+
* future fields (rotated_at, version) without changing every reader. Both
|
|
40
|
+
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
41
|
+
* and the Rust daemon-token writer match this shape.
|
|
42
|
+
*
|
|
43
|
+
* @param runtime - runtime with file write capabilities
|
|
44
|
+
* @param token_path - path to write the token
|
|
45
|
+
* @param token - the raw token string
|
|
46
|
+
* @mutates filesystem - writes `token_path` atomically at mode `0600`
|
|
47
|
+
*/
|
|
48
|
+
export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_path: string, token: string) => Promise<void>;
|
|
49
|
+
/** Options for daemon token rotation. */
|
|
50
|
+
export interface DaemonTokenRotationOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Absolute path the token file is written to. Caller computes from
|
|
53
|
+
* its own conventions — e.g. `get_daemon_token_path(runtime, app_name)`
|
|
54
|
+
* for the standard `~/.{name}/run/daemon_token` layout, or a path
|
|
55
|
+
* derived from `PUBLIC_<APP>_DIR` for cross-process test setups that
|
|
56
|
+
* isolate the app dir to a tmpdir.
|
|
57
|
+
*/
|
|
58
|
+
token_path: string;
|
|
59
|
+
/** Rotation interval in ms. Default: `30000` (30s). */
|
|
60
|
+
rotation_interval_ms?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Result of starting daemon token rotation. */
|
|
63
|
+
export interface DaemonTokenRotation {
|
|
64
|
+
/** The mutable runtime state. Pass to `create_daemon_token_middleware`. */
|
|
65
|
+
state: DaemonTokenState;
|
|
66
|
+
/** Stop rotation, clean up the interval, and delete the token file. Call on graceful shutdown. */
|
|
67
|
+
stop: () => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start daemon token rotation.
|
|
71
|
+
*
|
|
72
|
+
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
73
|
+
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
74
|
+
*
|
|
75
|
+
* @param runtime - runtime with file and remove capabilities
|
|
76
|
+
* @param deps - query dependencies for resolving keeper account
|
|
77
|
+
* @param options - rotation configuration
|
|
78
|
+
* @param log - the logger instance
|
|
79
|
+
* @returns rotation state and stop function
|
|
80
|
+
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
81
|
+
*/
|
|
82
|
+
export declare const start_daemon_token_rotation: (runtime: DaemonTokenWriteDeps & FsRemoveDeps, deps: QueryDeps, options: DaemonTokenRotationOptions, log: Logger) => Promise<DaemonTokenRotation>;
|
|
83
|
+
//# sourceMappingURL=daemon_token_rotation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon_token_rotation.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/daemon_token_rotation.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGvF,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAC1D,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,iBAAiB,GAAG,QAAQ,CAAC,CAAC;AAE3D;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,MAAM,KACV,MAAM,GAAG,IAGX,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAS,oBAAoB,EAC7B,YAAY,MAAM,EAClB,OAAO,MAAM,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAEF,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IACnC,2EAA2E;IAC3E,KAAK,EAAE,gBAAgB,CAAC;IACxB,kGAAkG;IAClG,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,oBAAoB,GAAG,YAAY,EAC5C,MAAM,SAAS,EACf,SAAS,0BAA0B,EACnC,KAAK,MAAM,KACT,OAAO,CAAC,mBAAmB,CAqD7B,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { write_file_atomic } from '../runtime/fs.js';
|
|
3
|
+
import { get_app_dir } from '../cli/config.js';
|
|
4
|
+
import { generate_daemon_token } from '../auth/daemon_token.js';
|
|
5
|
+
import { resolve_keeper_account_id } from '../auth/daemon_token_middleware.js';
|
|
6
|
+
/** Default rotation interval in milliseconds (30 seconds). */
|
|
7
|
+
export const DEFAULT_ROTATION_INTERVAL_MS = 30_000;
|
|
8
|
+
/**
|
|
9
|
+
* Get the daemon token file path (`~/.{name}/run/daemon_token`).
|
|
10
|
+
*
|
|
11
|
+
* @param runtime - runtime with `env_get` capability
|
|
12
|
+
* @param name - application name
|
|
13
|
+
* @returns path to `daemon_token`, or `null` if `$HOME` is not set
|
|
14
|
+
*/
|
|
15
|
+
export const get_daemon_token_path = (runtime, name) => {
|
|
16
|
+
const app_dir = get_app_dir(runtime, name);
|
|
17
|
+
return app_dir ? `${app_dir}/run/daemon_token` : null;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Write the current token to disk atomically at mode `0600`.
|
|
21
|
+
*
|
|
22
|
+
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
23
|
+
* future fields (rotated_at, version) without changing every reader. Both
|
|
24
|
+
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
25
|
+
* and the Rust daemon-token writer match this shape.
|
|
26
|
+
*
|
|
27
|
+
* @param runtime - runtime with file write capabilities
|
|
28
|
+
* @param token_path - path to write the token
|
|
29
|
+
* @param token - the raw token string
|
|
30
|
+
* @mutates filesystem - writes `token_path` atomically at mode `0600`
|
|
31
|
+
*/
|
|
32
|
+
export const write_daemon_token = async (runtime, token_path, token) => {
|
|
33
|
+
await write_file_atomic(runtime, token_path, JSON.stringify({ token }) + '\n', {
|
|
34
|
+
mode: 0o600
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Start daemon token rotation.
|
|
39
|
+
*
|
|
40
|
+
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
41
|
+
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
42
|
+
*
|
|
43
|
+
* @param runtime - runtime with file and remove capabilities
|
|
44
|
+
* @param deps - query dependencies for resolving keeper account
|
|
45
|
+
* @param options - rotation configuration
|
|
46
|
+
* @param log - the logger instance
|
|
47
|
+
* @returns rotation state and stop function
|
|
48
|
+
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
49
|
+
*/
|
|
50
|
+
export const start_daemon_token_rotation = async (runtime, deps, options, log) => {
|
|
51
|
+
const { token_path, rotation_interval_ms = DEFAULT_ROTATION_INTERVAL_MS } = options;
|
|
52
|
+
// ensure parent directory exists
|
|
53
|
+
const last_slash = token_path.lastIndexOf('/');
|
|
54
|
+
if (last_slash > 0) {
|
|
55
|
+
await runtime.mkdir(token_path.slice(0, last_slash), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
// resolve keeper account (may be null pre-bootstrap; the middleware
|
|
58
|
+
// lazily refreshes on the first null hit to cover the
|
|
59
|
+
// rotation-starts-before-bootstrap case)
|
|
60
|
+
const keeper_account_id = await resolve_keeper_account_id(deps);
|
|
61
|
+
// generate initial token and write to disk
|
|
62
|
+
const initial_token = generate_daemon_token();
|
|
63
|
+
await write_daemon_token(runtime, token_path, initial_token);
|
|
64
|
+
const state = {
|
|
65
|
+
current_token: initial_token,
|
|
66
|
+
previous_token: null,
|
|
67
|
+
rotated_at: new Date(),
|
|
68
|
+
keeper_account_id
|
|
69
|
+
};
|
|
70
|
+
let writing = false;
|
|
71
|
+
const interval_id = setInterval(async () => {
|
|
72
|
+
if (writing)
|
|
73
|
+
return; // skip if previous rotation write still in progress
|
|
74
|
+
writing = true;
|
|
75
|
+
try {
|
|
76
|
+
const new_token = generate_daemon_token();
|
|
77
|
+
state.previous_token = state.current_token;
|
|
78
|
+
state.current_token = new_token;
|
|
79
|
+
state.rotated_at = new Date();
|
|
80
|
+
await write_daemon_token(runtime, token_path, new_token);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
log.error('Failed to write rotated token:', err);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
writing = false;
|
|
87
|
+
}
|
|
88
|
+
}, rotation_interval_ms);
|
|
89
|
+
const stop = async () => {
|
|
90
|
+
clearInterval(interval_id);
|
|
91
|
+
try {
|
|
92
|
+
await runtime.remove(token_path);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// already removed or never written
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return { state, stop };
|
|
99
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAMhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAW,KAAK,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IA07CF,CAAC"}
|
|
@@ -634,7 +634,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
634
634
|
app: { request: fixture.transport },
|
|
635
635
|
path: rpc_path,
|
|
636
636
|
spec: account_token_create_action_spec,
|
|
637
|
-
params: {
|
|
637
|
+
params: {
|
|
638
|
+
name: 'test-revoke',
|
|
639
|
+
scope: { kind: 'full' },
|
|
640
|
+
lifetime: { kind: 'eternal' }
|
|
641
|
+
},
|
|
638
642
|
headers: fixture.create_session_headers()
|
|
639
643
|
});
|
|
640
644
|
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
@@ -943,7 +947,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
943
947
|
app: { request: unauthed },
|
|
944
948
|
path: rpc_path,
|
|
945
949
|
spec: account_token_create_action_spec,
|
|
946
|
-
params: {
|
|
950
|
+
params: {
|
|
951
|
+
name: 'unauth-probe',
|
|
952
|
+
scope: { kind: 'full' },
|
|
953
|
+
lifetime: { kind: 'eternal' }
|
|
954
|
+
},
|
|
947
955
|
headers: { host: 'localhost' }
|
|
948
956
|
});
|
|
949
957
|
assert.strictEqual(token_create.status, 401);
|
|
@@ -1039,7 +1047,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1039
1047
|
app: { request: fixture.transport },
|
|
1040
1048
|
path: rpc_path,
|
|
1041
1049
|
spec: account_token_create_action_spec,
|
|
1042
|
-
params: {
|
|
1050
|
+
params: {
|
|
1051
|
+
name: 'test-token',
|
|
1052
|
+
scope: { kind: 'full' },
|
|
1053
|
+
lifetime: { kind: 'eternal' }
|
|
1054
|
+
},
|
|
1043
1055
|
headers: fixture.create_session_headers()
|
|
1044
1056
|
});
|
|
1045
1057
|
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAa7B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAG3E,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAI9F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,GAAG,GAAG,EAAE,OAAO,MAAM,KAAG,CAqBtD,CAAC;AAET;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,GAAG,GAAG,EAAE,QAAQ,MAAM,EAAE,YAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,CAOxF,CAAC;AAET,iEAAiE;AACjE,eAAO,MAAM,IAAI,EAAE,GAAkC,CAAC;AAEtD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,QAAO,EAI/B,CAAC;AAEJ,gDAAgD;AAChD,eAAO,MAAM,YAAY,QAAO,QAAgC,CAAC;AAEjE,2CAA2C;AAC3C,eAAO,MAAM,OAAO,GAAU,IAAI,GAAG,EAAE,MAAM,GAAG,KAAG,OAAO,CAAC,IAAI,CAAW,CAAC;AAI3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,QAAO,YAW3C,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,QAAO,WAUxC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/stubs.js
CHANGED
|
@@ -133,8 +133,7 @@ export const create_stub_audit_sse = () => {
|
|
|
133
133
|
};
|
|
134
134
|
/** Stub `AppDeps` for auth surface tests — throws on any method access. */
|
|
135
135
|
export const stub_app_deps = {
|
|
136
|
-
|
|
137
|
-
read_text_file: create_throwing_stub('read_text_file'),
|
|
136
|
+
read_secure_file: create_throwing_stub('read_secure_file'),
|
|
138
137
|
delete_file: create_throwing_stub('delete_file'),
|
|
139
138
|
keyring: create_throwing_stub('keyring'),
|
|
140
139
|
password: create_throwing_stub('password'),
|
|
@@ -146,8 +145,9 @@ export const stub_app_deps = {
|
|
|
146
145
|
* Create no-op `AppDeps` for auth surface testing.
|
|
147
146
|
*/
|
|
148
147
|
export const create_stub_app_deps = () => ({
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
read_secure_file: async (path) => {
|
|
149
|
+
throw new Error(`ENOENT: no such file or directory: ${path}`);
|
|
150
|
+
},
|
|
151
151
|
delete_file: async (_path) => { },
|
|
152
152
|
keyring: create_noop_stub('keyring'),
|
|
153
153
|
password: create_noop_stub('password'),
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
const columns: Array<DatatableColumn<AuthSessionJson>> = [
|
|
35
35
|
{ key: 'id', label: 'session', width: 140 },
|
|
36
36
|
{ key: 'created_at', label: 'created', width: 120 },
|
|
37
|
-
{ key: 'last_seen_at', label: 'last seen', width: 120 },
|
|
38
37
|
{ key: 'expires_at', label: 'expires', width: 120 },
|
|
39
38
|
{ key: 'account_id', label: '', width: 100 }
|
|
40
39
|
];
|
|
@@ -71,10 +70,6 @@
|
|
|
71
70
|
<span title={format_datetime_local(row.created_at)}>
|
|
72
71
|
{format_relative_time(row.created_at)}
|
|
73
72
|
</span>
|
|
74
|
-
{:else if column.key === 'last_seen_at'}
|
|
75
|
-
<span title={format_datetime_local(row.last_seen_at)}>
|
|
76
|
-
{format_relative_time(row.last_seen_at)}
|
|
77
|
-
</span>
|
|
78
73
|
{:else if column.key === 'expires_at'}
|
|
79
74
|
<span title={format_datetime_local(row.expires_at)}>
|
|
80
75
|
{format_relative_time(row.expires_at)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AccountSessions.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AccountSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AccountSessions.svelte"],"names":[],"mappings":"AA4GA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,eAAe;;kBAA+E,CAAC;AACnF,KAAK,eAAe,GAAG,YAAY,CAAC,OAAO,eAAe,CAAC,CAAC;AAC9D,eAAe,eAAe,CAAC"}
|
|
@@ -52,10 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
// sessions
|
|
54
54
|
const unique_users = $derived(new Set(sessions.sessions.map((s) => s.username)).size);
|
|
55
|
+
// newest session by mint time — sessions carry no activity signal
|
|
56
|
+
// (`last_seen_at` is decorative post-hard-cap; see `docs/security.md`
|
|
57
|
+
// §Session Security)
|
|
55
58
|
const most_recent = $derived(
|
|
56
59
|
sessions.sessions.length > 0
|
|
57
60
|
? sessions.sessions.reduce((a, b) =>
|
|
58
|
-
new Date(a.
|
|
61
|
+
new Date(a.created_at) > new Date(b.created_at) ? a : b
|
|
59
62
|
)
|
|
60
63
|
: null
|
|
61
64
|
);
|
|
@@ -146,10 +149,10 @@
|
|
|
146
149
|
</div>
|
|
147
150
|
{#if most_recent}
|
|
148
151
|
<div class="baseline-row gap_xs font_size_sm mt_sm">
|
|
149
|
-
<span class="text_50">
|
|
152
|
+
<span class="text_50">newest session:</span>
|
|
150
153
|
<strong>{most_recent.username}</strong>
|
|
151
|
-
<span class="text_50" title={format_datetime_local(most_recent.
|
|
152
|
-
>{format_relative_time(most_recent.
|
|
154
|
+
<span class="text_50" title={format_datetime_local(most_recent.created_at)}
|
|
155
|
+
>{format_relative_time(most_recent.created_at)}</span
|
|
153
156
|
>
|
|
154
157
|
</div>
|
|
155
158
|
{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminOverview.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminOverview.svelte"],"names":[],"mappings":"AA8UA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
{ key: 'username', label: 'user', width: 120 },
|
|
27
27
|
{ key: 'id', label: 'session', width: 130 },
|
|
28
28
|
{ key: 'created_at', label: 'created', width: 100 },
|
|
29
|
-
{ key: 'last_seen_at', label: 'last seen', width: 100 },
|
|
30
29
|
{ key: 'expires_at', label: 'expires', width: 100 },
|
|
31
30
|
{ key: 'account_id', label: '', width: 220 }
|
|
32
31
|
];
|
|
@@ -53,10 +52,6 @@
|
|
|
53
52
|
<span title={format_datetime_local(row.created_at)}>
|
|
54
53
|
{format_relative_time(row.created_at)}
|
|
55
54
|
</span>
|
|
56
|
-
{:else if column.key === 'last_seen_at'}
|
|
57
|
-
<span title={format_datetime_local(row.last_seen_at)}>
|
|
58
|
-
{format_relative_time(row.last_seen_at)}
|
|
59
|
-
</span>
|
|
60
55
|
{:else if column.key === 'expires_at'}
|
|
61
56
|
<span title={format_datetime_local(row.expires_at)}>
|
|
62
57
|
{format_relative_time(row.expires_at)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSessions.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminSessions.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/AdminSessions.svelte"],"names":[],"mappings":"AA6FA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,aAAa;;kBAA+E,CAAC;AACjF,KAAK,aAAa,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,CAAC;AAC1D,eAAe,aAAa,CAAC"}
|