@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.
Files changed (122) hide show
  1. package/README.md +4 -0
  2. package/dist/actions/CLAUDE.md +1 -1
  3. package/dist/auth/CLAUDE.md +7 -6
  4. package/dist/auth/account_action_specs.d.ts +18 -0
  5. package/dist/auth/account_action_specs.d.ts.map +1 -1
  6. package/dist/auth/account_action_specs.js +14 -1
  7. package/dist/auth/account_actions.d.ts.map +1 -1
  8. package/dist/auth/account_actions.js +6 -2
  9. package/dist/auth/audit_emitter.d.ts +9 -5
  10. package/dist/auth/audit_emitter.d.ts.map +1 -1
  11. package/dist/auth/audit_log_schema.d.ts +7 -1
  12. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  13. package/dist/auth/audit_log_schema.js +16 -1
  14. package/dist/auth/bootstrap_account.d.ts +7 -2
  15. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  16. package/dist/auth/bootstrap_account.js +9 -4
  17. package/dist/auth/bootstrap_routes.d.ts +12 -4
  18. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  19. package/dist/auth/bootstrap_routes.js +26 -7
  20. package/dist/auth/daemon_token_middleware.d.ts +7 -69
  21. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  22. package/dist/auth/daemon_token_middleware.js +8 -104
  23. package/dist/auth/deps.d.ts +9 -5
  24. package/dist/auth/deps.d.ts.map +1 -1
  25. package/dist/auth/middleware.js +1 -1
  26. package/dist/auth/migrations.d.ts +7 -0
  27. package/dist/auth/migrations.d.ts.map +1 -1
  28. package/dist/auth/migrations.js +25 -8
  29. package/dist/auth/request_context.d.ts +1 -3
  30. package/dist/auth/request_context.d.ts.map +1 -1
  31. package/dist/auth/request_context.js +2 -5
  32. package/dist/auth/session_cookie.d.ts +18 -36
  33. package/dist/auth/session_cookie.d.ts.map +1 -1
  34. package/dist/auth/session_cookie.js +16 -26
  35. package/dist/auth/session_middleware.d.ts +1 -1
  36. package/dist/auth/session_middleware.d.ts.map +1 -1
  37. package/dist/auth/session_middleware.js +1 -4
  38. package/dist/auth/session_queries.d.ts +14 -30
  39. package/dist/auth/session_queries.d.ts.map +1 -1
  40. package/dist/auth/session_queries.js +15 -45
  41. package/dist/auth/token_lifetime.d.ts +51 -0
  42. package/dist/auth/token_lifetime.d.ts.map +1 -0
  43. package/dist/auth/token_lifetime.js +54 -0
  44. package/dist/db/CLAUDE.md +9 -5
  45. package/dist/db/cell_audit_queries.d.ts +4 -2
  46. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  47. package/dist/db/cell_audit_queries.js +4 -2
  48. package/dist/db/pg_error.d.ts +12 -0
  49. package/dist/db/pg_error.d.ts.map +1 -1
  50. package/dist/db/pg_error.js +15 -1
  51. package/dist/dev/setup.d.ts +5 -9
  52. package/dist/dev/setup.d.ts.map +1 -1
  53. package/dist/dev/setup.js +18 -21
  54. package/dist/http/CLAUDE.md +78 -18
  55. package/dist/http/db_routes.d.ts +96 -3
  56. package/dist/http/db_routes.d.ts.map +1 -1
  57. package/dist/http/db_routes.js +263 -46
  58. package/dist/http/error_schemas.d.ts +12 -1
  59. package/dist/http/error_schemas.d.ts.map +1 -1
  60. package/dist/http/error_schemas.js +12 -1
  61. package/dist/http/pending_effects.d.ts +5 -5
  62. package/dist/http/pending_effects.d.ts.map +1 -1
  63. package/dist/http/pending_effects.js +1 -1
  64. package/dist/runtime/deno.d.ts.map +1 -1
  65. package/dist/runtime/deno.js +27 -1
  66. package/dist/runtime/deps.d.ts +38 -4
  67. package/dist/runtime/deps.d.ts.map +1 -1
  68. package/dist/runtime/fs.d.ts +24 -5
  69. package/dist/runtime/fs.d.ts.map +1 -1
  70. package/dist/runtime/fs.js +38 -7
  71. package/dist/runtime/mock.d.ts +7 -0
  72. package/dist/runtime/mock.d.ts.map +1 -1
  73. package/dist/runtime/mock.js +41 -1
  74. package/dist/runtime/node.d.ts.map +1 -1
  75. package/dist/runtime/node.js +7 -1
  76. package/dist/runtime/secure_file.d.ts +71 -0
  77. package/dist/runtime/secure_file.d.ts.map +1 -0
  78. package/dist/runtime/secure_file.js +125 -0
  79. package/dist/server/app_backend.d.ts +6 -5
  80. package/dist/server/app_backend.d.ts.map +1 -1
  81. package/dist/server/app_backend.js +2 -3
  82. package/dist/testing/CLAUDE.md +36 -1
  83. package/dist/testing/admin_integration.d.ts.map +1 -1
  84. package/dist/testing/admin_integration.js +10 -2
  85. package/dist/testing/app_server.d.ts.map +1 -1
  86. package/dist/testing/app_server.js +13 -8
  87. package/dist/testing/audit_completeness.d.ts.map +1 -1
  88. package/dist/testing/audit_completeness.js +10 -2
  89. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  90. package/dist/testing/cross_backend/cell_grant_role.js +34 -1
  91. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  92. package/dist/testing/cross_backend/cell_relations.js +53 -1
  93. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/conformance_table.js +2 -1
  95. package/dist/testing/cross_backend/query_shape.d.ts +10 -0
  96. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
  97. package/dist/testing/cross_backend/query_shape.js +128 -0
  98. package/dist/testing/cross_backend/setup.js +1 -1
  99. package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
  100. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/token_lifetime.js +99 -0
  102. package/dist/testing/cross_backend/token_scope_surface.js +1 -1
  103. package/dist/testing/daemon_token_rotation.d.ts +83 -0
  104. package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
  105. package/dist/testing/daemon_token_rotation.js +99 -0
  106. package/dist/testing/integration.d.ts.map +1 -1
  107. package/dist/testing/integration.js +15 -3
  108. package/dist/testing/stubs.d.ts.map +1 -1
  109. package/dist/testing/stubs.js +4 -4
  110. package/dist/ui/AccountSessions.svelte +0 -5
  111. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  112. package/dist/ui/AdminOverview.svelte +7 -4
  113. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  114. package/dist/ui/AdminSessions.svelte +0 -5
  115. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  116. package/dist/ui/CLAUDE.md +9 -4
  117. package/dist/ui/table_state.svelte.d.ts +18 -5
  118. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  119. package/dist/ui/table_state.svelte.js +22 -7
  120. package/dist/ui/ui_format.d.ts.map +1 -1
  121. package/dist/ui/ui_format.js +4 -0
  122. package/package.json +1 -1
@@ -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/dist/ui/CLAUDE.md CHANGED
@@ -244,9 +244,13 @@ destructive actions.
244
244
  - `table_state.svelte.ts` — `TableState`. Paginated DB browser state.
245
245
  Holds one `AsyncSlot` (`list`) + payload fields (`table_name`,
246
246
  `columns`, `rows`, `total`, `offset`, `limit` capped by
247
- `TABLE_LIMIT_MAX = 1000`, `primary_key`). Derived
248
- `showing_start`/`showing_end`/`has_prev`/`has_next`. Methods:
249
- `fetch`, `go_prev`/`go_next`, `delete_row`. `delete_row` uses
247
+ `http/db_routes.ts`'s `DB_TABLE_ROWS_LIMIT_MAX`, `primary_key`,
248
+ `deletable`). Derived
249
+ `showing_start`/`showing_end`/`has_prev`/`has_next`, plus
250
+ `can_delete` (`deletable && primary_key !== null`) — gate the
251
+ delete affordance on that, not on `primary_key`, or a
252
+ policy-excluded table shows a button that only ever refuses.
253
+ Methods: `fetch`, `go_prev`/`go_next`, `delete_row`. `delete_row` uses
250
254
  plain try/catch + scalar `deleting` / `delete_error` fields (no
251
255
  slot — error must survive past `list.run()` retries).
252
256
  - `form_state.svelte.ts` — `FormState`. Enter-advance between
@@ -421,4 +425,5 @@ format_scope, global_label)` helper — `global_label = null` renders no
421
425
  - `format_value(value)` — table-cell stringifier (NULL / undefined /
422
426
  JSON / primitive).
423
427
  - `format_audit_metadata(event_type, metadata)` — event-type-
424
- specific metadata summary (switch across every `AuditEventType`).
428
+ specific metadata summary (bespoke cases for the common event types,
429
+ JSON-stringified fallback for the rest).
@@ -40,9 +40,7 @@
40
40
  * @module
41
41
  */
42
42
  import { AsyncSlot } from './async_slot.svelte.js';
43
- import type { ColumnInfo } from '../http/db_routes.js';
44
- /** Maximum number of rows that can be fetched in a single page. */
45
- export declare const TABLE_LIMIT_MAX = 1000;
43
+ import { type ColumnInfo } from '../http/db_routes.js';
46
44
  export declare class TableState {
47
45
  readonly list: AsyncSlot<void, string>;
48
46
  table_name: string;
@@ -52,15 +50,30 @@ export declare class TableState {
52
50
  offset: number;
53
51
  limit: number;
54
52
  primary_key: string | null;
53
+ /**
54
+ * Whether the server will accept a row `DELETE` on this table. False for a
55
+ * table with no single-column primary key *and* for one the server excludes
56
+ * by policy (the audit trail, singleton bookkeeping rows), which `primary_key`
57
+ * alone can't distinguish — it reports the key shape, not the policy.
58
+ */
59
+ deletable: boolean;
55
60
  deleting: string | null;
56
61
  delete_error: string | null;
62
+ /**
63
+ * Whether to offer a delete affordance at all. Gate rendering on this rather
64
+ * than on `primary_key`, or a policy-excluded table shows a button whose only
65
+ * outcome is a `table_not_deletable` refusal.
66
+ */
67
+ readonly can_delete: boolean;
57
68
  readonly showing_start: number;
58
69
  readonly showing_end: number;
59
70
  readonly has_prev: boolean;
60
71
  readonly has_next: boolean;
61
72
  /**
62
73
  * Fetch a page of rows for `table_name` from `GET /api/db/tables/{table_name}`.
63
- * `limit` is clamped to `[1, TABLE_LIMIT_MAX]`.
74
+ * `limit` is clamped to `[1, DB_TABLE_ROWS_LIMIT_MAX]` — the same bounds
75
+ * the route 400s on, single-sourced so the client guard can't drift into
76
+ * sending a request the server refuses.
64
77
  *
65
78
  * @mutates `this`
66
79
  */
@@ -72,7 +85,7 @@ export declare class TableState {
72
85
  * Optimistically drops it from `rows` and decrements `total` on success;
73
86
  * surfaces server errors on `delete_error`.
74
87
  *
75
- * @returns `true` when the row was removed; `false` on missing primary key or server error
88
+ * @returns `true` when the row was removed; `false` when the table isn't deletable or the server refused
76
89
  * @mutates `this`
77
90
  */
78
91
  delete_row(row: Record<string, unknown>): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"table_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/table_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,mEAAmE;AACnE,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,qBAAa,UAAU;IACtB,QAAQ,CAAC,IAAI,0BAAyB;IAEtC,UAAU,EAAE,MAAM,CAAkB;IACpC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAkB;IAC5C,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAkB;IACtD,KAAK,SAAiB;IACtB,MAAM,SAAiB;IACvB,KAAK,SAAmB;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC9C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC3C,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAG/C,QAAQ,CAAC,aAAa,SAAoD;IAC1E,QAAQ,CAAC,WAAW,SAAkE;IACtF,QAAQ,CAAC,QAAQ,UAA6B;IAC9C,QAAQ,CAAC,QAAQ,UAAmD;IAEpE;;;;;OAKG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBvE,OAAO,IAAI,IAAI;IAIf,OAAO,IAAI,IAAI;IAIf;;;;;;;OAOG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CAoChE"}
1
+ {"version":3,"file":"table_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/table_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,EAGN,KAAK,UAAU,EACf,MAAM,sBAAsB,CAAC;AAE9B,qBAAa,UAAU;IACtB,QAAQ,CAAC,IAAI,0BAAyB;IAEtC,UAAU,EAAE,MAAM,CAAkB;IACpC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAkB;IAC5C,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAkB;IACtD,KAAK,SAAiB;IACtB,MAAM,SAAiB;IACvB,KAAK,SAA2C;IAChD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC9C;;;;;OAKG;IACH,SAAS,UAAqB;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAoB;IAC3C,YAAY,EAAE,MAAM,GAAG,IAAI,CAAoB;IAE/C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,UAAyD;IAG5E,QAAQ,CAAC,aAAa,SAAoD;IAC1E,QAAQ,CAAC,WAAW,SAAkE;IACtF,QAAQ,CAAC,QAAQ,UAA6B;IAC9C,QAAQ,CAAC,QAAQ,UAAmD;IAEpE;;;;;;;OAOG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBvE,OAAO,IAAI,IAAI;IAIf,OAAO,IAAI,IAAI;IAIf;;;;;;;OAOG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CAoChE"}
@@ -43,8 +43,7 @@ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
43
43
  import { AsyncSlot } from './async_slot.svelte.js';
44
44
  import { parse_response_error, ui_fetch } from './ui_fetch.js';
45
45
  import { format_value } from './ui_format.js';
46
- /** Maximum number of rows that can be fetched in a single page. */
47
- export const TABLE_LIMIT_MAX = 1000;
46
+ import { DB_TABLE_ROWS_DEFAULT_LIMIT, DB_TABLE_ROWS_LIMIT_MAX } from '../http/db_routes.js';
48
47
  export class TableState {
49
48
  list = new AsyncSlot();
50
49
  table_name = $state.raw('');
@@ -52,10 +51,23 @@ export class TableState {
52
51
  rows = $state.raw([]);
53
52
  total = $state.raw(0);
54
53
  offset = $state.raw(0);
55
- limit = $state.raw(100);
54
+ limit = $state.raw(DB_TABLE_ROWS_DEFAULT_LIMIT);
56
55
  primary_key = $state.raw(null);
56
+ /**
57
+ * Whether the server will accept a row `DELETE` on this table. False for a
58
+ * table with no single-column primary key *and* for one the server excludes
59
+ * by policy (the audit trail, singleton bookkeeping rows), which `primary_key`
60
+ * alone can't distinguish — it reports the key shape, not the policy.
61
+ */
62
+ deletable = $state.raw(false);
57
63
  deleting = $state.raw(null);
58
64
  delete_error = $state.raw(null);
65
+ /**
66
+ * Whether to offer a delete affordance at all. Gate rendering on this rather
67
+ * than on `primary_key`, or a policy-excluded table shows a button whose only
68
+ * outcome is a `table_not_deletable` refusal.
69
+ */
70
+ can_delete = $derived(this.deletable && this.primary_key !== null);
59
71
  // Pagination computed values
60
72
  showing_start = $derived(this.total === 0 ? 0 : this.offset + 1);
61
73
  showing_end = $derived(Math.min(this.offset + this.rows.length, this.total));
@@ -63,14 +75,16 @@ export class TableState {
63
75
  has_next = $derived(this.offset + this.limit < this.total);
64
76
  /**
65
77
  * Fetch a page of rows for `table_name` from `GET /api/db/tables/{table_name}`.
66
- * `limit` is clamped to `[1, TABLE_LIMIT_MAX]`.
78
+ * `limit` is clamped to `[1, DB_TABLE_ROWS_LIMIT_MAX]` — the same bounds
79
+ * the route 400s on, single-sourced so the client guard can't drift into
80
+ * sending a request the server refuses.
67
81
  *
68
82
  * @mutates `this`
69
83
  */
70
84
  async fetch(table_name, offset = 0, limit = 100) {
71
85
  this.table_name = table_name;
72
86
  this.offset = offset;
73
- this.limit = Math.max(1, Math.min(TABLE_LIMIT_MAX, limit));
87
+ this.limit = Math.max(1, Math.min(DB_TABLE_ROWS_LIMIT_MAX, limit));
74
88
  await this.list.run(async () => {
75
89
  const response = await ui_fetch(`/api/db/tables/${table_name}?offset=${this.offset}&limit=${this.limit}`);
76
90
  if (!response.ok) {
@@ -81,6 +95,7 @@ export class TableState {
81
95
  this.rows = data.rows ?? [];
82
96
  this.total = data.total ?? 0;
83
97
  this.primary_key = data.primary_key ?? null;
98
+ this.deletable = data.deletable ?? false;
84
99
  });
85
100
  }
86
101
  go_prev() {
@@ -94,11 +109,11 @@ export class TableState {
94
109
  * Optimistically drops it from `rows` and decrements `total` on success;
95
110
  * surfaces server errors on `delete_error`.
96
111
  *
97
- * @returns `true` when the row was removed; `false` on missing primary key or server error
112
+ * @returns `true` when the row was removed; `false` when the table isn't deletable or the server refused
98
113
  * @mutates `this`
99
114
  */
100
115
  async delete_row(row) {
101
- if (!this.primary_key)
116
+ if (!this.can_delete || !this.primary_key)
102
117
  return false;
103
118
  const pk_value = row[this.primary_key];
104
119
  if (pk_value === null || pk_value === undefined)
@@ -1 +1 @@
1
- {"version":3,"file":"ui_format.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/ui_format.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,MAAM,GAAG,MAAM,GAAG,IAAI,EACjC,MAAK,MAAmB,KACtB,MA2BF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,KAAG,MAY1C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,YAAY,MAAM,EAAE,kBAAe,KAAG,MAOlF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAmC,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAS7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,MAAM,GAAG,IAAI,KAAG,MAOzE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GACjC,YAAY,MAAM,EAClB,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KACtC,MAyDF,CAAC"}
1
+ {"version":3,"file":"ui_format.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/ui_format.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,MAAM,GAAG,MAAM,GAAG,IAAI,EACjC,MAAK,MAAmB,KACtB,MA2BF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,KAAG,MAY1C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,YAAY,MAAM,EAAE,kBAAe,KAAG,MAOlF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAmC,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAS7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,MAAM,GAAG,IAAI,KAAG,MAOzE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GACjC,YAAY,MAAM,EAClB,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KACtC,MAgEF,CAAC"}
@@ -178,6 +178,10 @@ export const format_audit_metadata = (event_type, metadata) => {
178
178
  return metadata.setting
179
179
  ? `${metadata.setting}: ${JSON.stringify(metadata.old_value)} → ${JSON.stringify(metadata.new_value)}`
180
180
  : '';
181
+ case 'db_admin_row_delete':
182
+ return metadata.table
183
+ ? `${metadata.table}.${metadata.pk_column}: ${truncate_middle(metadata.id, 12)}`
184
+ : '';
181
185
  default:
182
186
  return JSON.stringify(metadata);
183
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.108.0",
3
+ "version": "0.110.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",