@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.
Files changed (84) hide show
  1. package/dist/auth/CLAUDE.md +6 -5
  2. package/dist/auth/account_action_specs.d.ts +18 -0
  3. package/dist/auth/account_action_specs.d.ts.map +1 -1
  4. package/dist/auth/account_action_specs.js +14 -1
  5. package/dist/auth/account_actions.d.ts.map +1 -1
  6. package/dist/auth/account_actions.js +6 -2
  7. package/dist/auth/bootstrap_account.d.ts +7 -2
  8. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  9. package/dist/auth/bootstrap_account.js +9 -4
  10. package/dist/auth/bootstrap_routes.d.ts +12 -4
  11. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  12. package/dist/auth/bootstrap_routes.js +15 -6
  13. package/dist/auth/daemon_token_middleware.d.ts +7 -69
  14. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  15. package/dist/auth/daemon_token_middleware.js +8 -104
  16. package/dist/auth/deps.d.ts +9 -5
  17. package/dist/auth/deps.d.ts.map +1 -1
  18. package/dist/auth/middleware.js +1 -1
  19. package/dist/auth/request_context.d.ts +1 -3
  20. package/dist/auth/request_context.d.ts.map +1 -1
  21. package/dist/auth/request_context.js +2 -5
  22. package/dist/auth/session_cookie.d.ts +18 -36
  23. package/dist/auth/session_cookie.d.ts.map +1 -1
  24. package/dist/auth/session_cookie.js +16 -26
  25. package/dist/auth/session_middleware.d.ts +1 -1
  26. package/dist/auth/session_middleware.d.ts.map +1 -1
  27. package/dist/auth/session_middleware.js +1 -4
  28. package/dist/auth/session_queries.d.ts +14 -30
  29. package/dist/auth/session_queries.d.ts.map +1 -1
  30. package/dist/auth/session_queries.js +15 -45
  31. package/dist/auth/token_lifetime.d.ts +51 -0
  32. package/dist/auth/token_lifetime.d.ts.map +1 -0
  33. package/dist/auth/token_lifetime.js +54 -0
  34. package/dist/dev/setup.d.ts +5 -9
  35. package/dist/dev/setup.d.ts.map +1 -1
  36. package/dist/dev/setup.js +18 -21
  37. package/dist/runtime/deno.d.ts.map +1 -1
  38. package/dist/runtime/deno.js +27 -1
  39. package/dist/runtime/deps.d.ts +38 -4
  40. package/dist/runtime/deps.d.ts.map +1 -1
  41. package/dist/runtime/fs.d.ts +24 -5
  42. package/dist/runtime/fs.d.ts.map +1 -1
  43. package/dist/runtime/fs.js +38 -7
  44. package/dist/runtime/mock.d.ts +7 -0
  45. package/dist/runtime/mock.d.ts.map +1 -1
  46. package/dist/runtime/mock.js +41 -1
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +7 -1
  49. package/dist/runtime/secure_file.d.ts +71 -0
  50. package/dist/runtime/secure_file.d.ts.map +1 -0
  51. package/dist/runtime/secure_file.js +125 -0
  52. package/dist/server/app_backend.d.ts +6 -5
  53. package/dist/server/app_backend.d.ts.map +1 -1
  54. package/dist/server/app_backend.js +2 -3
  55. package/dist/testing/CLAUDE.md +15 -1
  56. package/dist/testing/admin_integration.d.ts.map +1 -1
  57. package/dist/testing/admin_integration.js +57 -20
  58. package/dist/testing/app_server.d.ts.map +1 -1
  59. package/dist/testing/app_server.js +13 -8
  60. package/dist/testing/audit_completeness.d.ts.map +1 -1
  61. package/dist/testing/audit_completeness.js +5 -1
  62. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  63. package/dist/testing/cross_backend/cell_grant_role.js +34 -1
  64. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/conformance_table.js +2 -1
  66. package/dist/testing/cross_backend/setup.js +1 -1
  67. package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
  68. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
  69. package/dist/testing/cross_backend/token_lifetime.js +99 -0
  70. package/dist/testing/cross_backend/token_scope_surface.js +1 -1
  71. package/dist/testing/daemon_token_rotation.d.ts +83 -0
  72. package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
  73. package/dist/testing/daemon_token_rotation.js +99 -0
  74. package/dist/testing/integration.d.ts.map +1 -1
  75. package/dist/testing/integration.js +15 -3
  76. package/dist/testing/stubs.d.ts.map +1 -1
  77. package/dist/testing/stubs.js +4 -4
  78. package/dist/ui/AccountSessions.svelte +0 -5
  79. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  80. package/dist/ui/AdminOverview.svelte +7 -4
  81. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  82. package/dist/ui/AdminSessions.svelte +0 -5
  83. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  84. 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,IA86CF,CAAC"}
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: { name: 'test-revoke', scope: { kind: 'full' } },
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: { name: 'unauth-probe', scope: { kind: 'full' } },
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: { name: 'test-token', scope: { kind: 'full' } },
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":"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"}
1
+ {"version":3,"file":"stubs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/stubs.ts"],"names":[],"mappings":"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"}
@@ -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
- stat: create_throwing_stub('stat'),
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
- stat: async () => null,
150
- read_text_file: async () => '',
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":"AAiHA,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"}
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.last_seen_at) > new Date(b.last_seen_at) ? a : b
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">last active:</span>
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.last_seen_at)}
152
- >{format_relative_time(most_recent.last_seen_at)}</span
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":"AA2UA,QAAA,MAAM,aAAa,2DAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
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":"AAkGA,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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.109.0",
3
+ "version": "0.110.1",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",