@fuzdev/fuz_app 0.67.0 → 0.68.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 (168) hide show
  1. package/dist/auth/CLAUDE.md +99 -5
  2. package/dist/auth/account_queries.d.ts +87 -4
  3. package/dist/auth/account_queries.d.ts.map +1 -1
  4. package/dist/auth/account_queries.js +107 -17
  5. package/dist/auth/account_schema.d.ts +19 -0
  6. package/dist/auth/account_schema.d.ts.map +1 -1
  7. package/dist/auth/account_schema.js +8 -0
  8. package/dist/auth/admin_action_specs.d.ts +168 -0
  9. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  10. package/dist/auth/admin_action_specs.js +146 -1
  11. package/dist/auth/admin_actions.d.ts.map +1 -1
  12. package/dist/auth/admin_actions.js +218 -4
  13. package/dist/auth/audit_log_ddl.d.ts +10 -1
  14. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  15. package/dist/auth/audit_log_ddl.js +13 -4
  16. package/dist/auth/audit_log_schema.d.ts +34 -1
  17. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  18. package/dist/auth/audit_log_schema.js +73 -0
  19. package/dist/auth/auth_ddl.d.ts +2 -2
  20. package/dist/auth/auth_ddl.d.ts.map +1 -1
  21. package/dist/auth/auth_ddl.js +10 -2
  22. package/dist/auth/cell_action_specs.d.ts +1295 -0
  23. package/dist/auth/cell_action_specs.d.ts.map +1 -0
  24. package/dist/auth/cell_action_specs.js +397 -0
  25. package/dist/auth/cell_actions.d.ts +63 -0
  26. package/dist/auth/cell_actions.d.ts.map +1 -0
  27. package/dist/auth/cell_actions.js +546 -0
  28. package/dist/auth/cell_audit_action_specs.d.ts +131 -0
  29. package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
  30. package/dist/auth/cell_audit_action_specs.js +70 -0
  31. package/dist/auth/cell_audit_actions.d.ts +18 -0
  32. package/dist/auth/cell_audit_actions.d.ts.map +1 -0
  33. package/dist/auth/cell_audit_actions.js +59 -0
  34. package/dist/auth/cell_audit_events.d.ts +28 -0
  35. package/dist/auth/cell_audit_events.d.ts.map +1 -0
  36. package/dist/auth/cell_audit_events.js +42 -0
  37. package/dist/auth/cell_audit_metadata.d.ts +48 -0
  38. package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
  39. package/dist/auth/cell_audit_metadata.js +46 -0
  40. package/dist/auth/cell_authorize.d.ts +88 -0
  41. package/dist/auth/cell_authorize.d.ts.map +1 -0
  42. package/dist/auth/cell_authorize.js +172 -0
  43. package/dist/auth/cell_data_schema.d.ts +44 -0
  44. package/dist/auth/cell_data_schema.d.ts.map +1 -0
  45. package/dist/auth/cell_data_schema.js +42 -0
  46. package/dist/auth/cell_field_action_specs.d.ts +244 -0
  47. package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
  48. package/dist/auth/cell_field_action_specs.js +136 -0
  49. package/dist/auth/cell_field_actions.d.ts +34 -0
  50. package/dist/auth/cell_field_actions.d.ts.map +1 -0
  51. package/dist/auth/cell_field_actions.js +153 -0
  52. package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
  53. package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
  54. package/dist/auth/cell_field_audit_metadata.js +28 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts +333 -0
  56. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
  57. package/dist/auth/cell_grant_action_specs.js +148 -0
  58. package/dist/auth/cell_grant_actions.d.ts +50 -0
  59. package/dist/auth/cell_grant_actions.d.ts.map +1 -0
  60. package/dist/auth/cell_grant_actions.js +208 -0
  61. package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
  62. package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
  63. package/dist/auth/cell_grant_audit_metadata.js +54 -0
  64. package/dist/auth/cell_item_action_specs.d.ts +331 -0
  65. package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
  66. package/dist/auth/cell_item_action_specs.js +182 -0
  67. package/dist/auth/cell_item_actions.d.ts +37 -0
  68. package/dist/auth/cell_item_actions.d.ts.map +1 -0
  69. package/dist/auth/cell_item_actions.js +204 -0
  70. package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
  71. package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
  72. package/dist/auth/cell_item_audit_metadata.js +32 -0
  73. package/dist/auth/cell_relation_visibility.d.ts +32 -0
  74. package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
  75. package/dist/auth/cell_relation_visibility.js +57 -0
  76. package/dist/auth/deps.d.ts +9 -0
  77. package/dist/auth/deps.d.ts.map +1 -1
  78. package/dist/auth/role_grant_queries.d.ts +30 -0
  79. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  80. package/dist/auth/role_grant_queries.js +54 -0
  81. package/dist/db/CLAUDE.md +118 -0
  82. package/dist/db/cell_audit_queries.d.ts +26 -0
  83. package/dist/db/cell_audit_queries.d.ts.map +1 -0
  84. package/dist/db/cell_audit_queries.js +53 -0
  85. package/dist/db/cell_ddl.d.ts +151 -0
  86. package/dist/db/cell_ddl.d.ts.map +1 -0
  87. package/dist/db/cell_ddl.js +247 -0
  88. package/dist/db/cell_field_queries.d.ts +105 -0
  89. package/dist/db/cell_field_queries.d.ts.map +1 -0
  90. package/dist/db/cell_field_queries.js +113 -0
  91. package/dist/db/cell_grant_queries.d.ts +132 -0
  92. package/dist/db/cell_grant_queries.d.ts.map +1 -0
  93. package/dist/db/cell_grant_queries.js +145 -0
  94. package/dist/db/cell_history_ddl.d.ts +38 -0
  95. package/dist/db/cell_history_ddl.d.ts.map +1 -0
  96. package/dist/db/cell_history_ddl.js +61 -0
  97. package/dist/db/cell_item_queries.d.ts +107 -0
  98. package/dist/db/cell_item_queries.d.ts.map +1 -0
  99. package/dist/db/cell_item_queries.js +119 -0
  100. package/dist/db/cell_queries.d.ts +327 -0
  101. package/dist/db/cell_queries.d.ts.map +1 -0
  102. package/dist/db/cell_queries.js +431 -0
  103. package/dist/db/fact_ddl.d.ts +38 -0
  104. package/dist/db/fact_ddl.d.ts.map +1 -0
  105. package/dist/db/fact_ddl.js +71 -0
  106. package/dist/db/fact_queries.d.ts +140 -0
  107. package/dist/db/fact_queries.d.ts.map +1 -0
  108. package/dist/db/fact_queries.js +161 -0
  109. package/dist/db/fact_store.d.ts +112 -0
  110. package/dist/db/fact_store.d.ts.map +1 -0
  111. package/dist/db/fact_store.js +225 -0
  112. package/dist/server/env.d.ts +2 -0
  113. package/dist/server/env.d.ts.map +1 -1
  114. package/dist/server/env.js +6 -0
  115. package/dist/server/fact_write.d.ts +32 -0
  116. package/dist/server/fact_write.d.ts.map +1 -0
  117. package/dist/server/fact_write.js +56 -0
  118. package/dist/server/file_fact_fetcher.d.ts +42 -0
  119. package/dist/server/file_fact_fetcher.d.ts.map +1 -0
  120. package/dist/server/file_fact_fetcher.js +60 -0
  121. package/dist/server/file_fact_url.d.ts +53 -0
  122. package/dist/server/file_fact_url.d.ts.map +1 -0
  123. package/dist/server/file_fact_url.js +52 -0
  124. package/dist/server/serve_fact_route.d.ts +78 -0
  125. package/dist/server/serve_fact_route.d.ts.map +1 -0
  126. package/dist/server/serve_fact_route.js +205 -0
  127. package/dist/testing/CLAUDE.md +58 -5
  128. package/dist/testing/app_server.d.ts +12 -0
  129. package/dist/testing/app_server.d.ts.map +1 -1
  130. package/dist/testing/app_server.js +36 -2
  131. package/dist/testing/audit_completeness.d.ts.map +1 -1
  132. package/dist/testing/audit_completeness.js +67 -1
  133. package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
  134. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
  135. package/dist/testing/cross_backend/account_lifecycle.js +76 -0
  136. package/dist/testing/cross_backend/capabilities.d.ts +31 -0
  137. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/capabilities.js +3 -0
  139. package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
  140. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
  141. package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
  142. package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
  144. package/dist/testing/cross_backend/cell_crud.js +168 -0
  145. package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
  146. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/cell_relations.js +229 -0
  148. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/default_backend_configs.js +6 -0
  150. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/setup.js +5 -0
  152. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
  153. package/dist/testing/cross_backend/spawn_backend.js +31 -3
  154. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
  155. package/dist/testing/cross_backend/testing_server_bun.js +29 -2
  156. package/dist/testing/entities.d.ts.map +1 -1
  157. package/dist/testing/entities.js +4 -0
  158. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  159. package/dist/testing/ws_round_trip.js +4 -0
  160. package/dist/ui/AdminAccounts.svelte +58 -0
  161. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  162. package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
  163. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  164. package/dist/ui/admin_accounts_state.svelte.js +45 -1
  165. package/dist/ui/admin_rpc_adapters.d.ts +6 -2
  166. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  167. package/dist/ui/admin_rpc_adapters.js +5 -1
  168. package/package.json +4 -2
@@ -29,9 +29,11 @@
29
29
  */
30
30
  import { rpc_action } from '../actions/action_rpc.js';
31
31
  import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
32
- import { builtin_role_specs_by_name, list_roles_with_grant_path, } from './role_schema.js';
32
+ import { builtin_role_specs_by_name, list_roles_with_grant_path, ROLE_ADMIN, ROLE_KEEPER, } from './role_schema.js';
33
+ import { has_role } from './request_context.js';
34
+ import { query_account_has_global_role, query_account_has_active_global_role, query_count_active_accounts_with_global_role, } from './role_grant_queries.js';
33
35
  import { GRANT_PATH_ADMIN } from './grant_path_schema.js';
34
- import { query_account_by_email, query_account_by_id, query_account_by_username, query_admin_account_list, } from './account_queries.js';
36
+ import { query_account_by_email, query_account_by_id, query_account_by_username, query_account_soft_delete, query_account_undelete, query_actor_soft_delete, query_actor_undelete, query_actors_by_account, query_admin_account_list, query_purge_account, } from './account_queries.js';
35
37
  import { query_session_list_all_active, query_session_revoke_all_for_account, } from './session_queries.js';
36
38
  import { query_revoke_all_api_tokens_for_account } from './api_token_queries.js';
37
39
  import { query_audit_log_list_role_grant_history, query_audit_log_list_with_usernames, } from './audit_log_queries.js';
@@ -40,8 +42,8 @@ import { query_create_invite, query_invite_delete_unclaimed, query_invite_list_a
40
42
  import {} from './app_settings_schema.js';
41
43
  import { query_app_settings_load_with_username, query_app_settings_update, } from './app_settings_queries.js';
42
44
  import { is_pg_unique_violation } from '../db/pg_error.js';
43
- import { ERROR_ACCOUNT_NOT_FOUND, ERROR_INVITE_ACCOUNT_EXISTS_EMAIL, ERROR_INVITE_ACCOUNT_EXISTS_USERNAME, ERROR_INVITE_DUPLICATE, ERROR_INVITE_NOT_FOUND, } from '../http/error_schemas.js';
44
- import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_role_grant_history_action_spec, invite_create_action_spec, invite_list_action_spec, invite_delete_action_spec, app_settings_get_action_spec, app_settings_update_action_spec, } from './admin_action_specs.js';
45
+ import { ERROR_ACCOUNT_NOT_FOUND, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_INVITE_ACCOUNT_EXISTS_EMAIL, ERROR_INVITE_ACCOUNT_EXISTS_USERNAME, ERROR_INVITE_DUPLICATE, ERROR_INVITE_NOT_FOUND, } from '../http/error_schemas.js';
46
+ import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_role_grant_history_action_spec, invite_create_action_spec, invite_list_action_spec, invite_delete_action_spec, account_delete_action_spec, account_purge_action_spec, account_undelete_action_spec, app_settings_get_action_spec, app_settings_update_action_spec, ERROR_PURGE_NOT_CONFIRMED, ERROR_CANNOT_DELETE_KEEPER, ERROR_CANNOT_DELETE_LAST_ADMIN, } from './admin_action_specs.js';
45
47
  /**
46
48
  * Create the admin-only RPC actions.
47
49
  *
@@ -61,6 +63,7 @@ export const create_admin_actions = (deps, options = {}) => {
61
63
  const accounts = await query_admin_account_list(ctx, {
62
64
  limit: input.limit,
63
65
  offset: input.offset,
66
+ include_deleted: input.include_deleted,
64
67
  });
65
68
  return { accounts, grantable_roles };
66
69
  };
@@ -228,8 +231,219 @@ export const create_admin_actions = (deps, options = {}) => {
228
231
  });
229
232
  return { ok: true };
230
233
  };
234
+ // Shared removability guard for `account_delete` / `account_purge`,
235
+ // checked after auth + (for purge) confirm, before any mutation. Two
236
+ // protections, each emitting a forensic `outcome: 'failure'` audit row
237
+ // (fail-loud) before throwing 403:
238
+ // 1. **Keeper** — the keeper account is never API-removable: auth +
239
+ // daemon-token resolution both pivot on it, so removing it bricks
240
+ // keeper/daemon auth with no recovery (keeper role is non-web-
241
+ // revocable and purge itself needs keeper auth). Out-of-band only.
242
+ // 2. **Last admin** — the sole remaining *active* admin is protected
243
+ // (soft-deleted admins can't log in, so they don't count). Unlike
244
+ // the keeper guard this is keeper-recoverable, but it stops an admin
245
+ // tombstoning the last admin in one call.
246
+ // A missing account holds neither role here and falls through to the
247
+ // handler's own not-found path. `event_type` selects the audit event so
248
+ // the failure row matches the operation in flight.
249
+ const assert_account_removable = async (ctx, target_account_id, event_type) => {
250
+ const auth = ctx.auth;
251
+ const deny = (reason, message) => {
252
+ deps.audit.emit(ctx, {
253
+ event_type,
254
+ outcome: 'failure',
255
+ actor_id: auth.actor.id,
256
+ account_id: auth.account.id,
257
+ target_account_id,
258
+ ip: ctx.client_ip,
259
+ metadata: { reason },
260
+ });
261
+ throw jsonrpc_errors.forbidden(message, { reason });
262
+ };
263
+ const verb = event_type === 'account_purge' ? 'purge' : 'delete';
264
+ if (await query_account_has_global_role(ctx, target_account_id, ROLE_KEEPER)) {
265
+ deny(ERROR_CANNOT_DELETE_KEEPER, `cannot ${verb} the keeper account`);
266
+ }
267
+ // `_active_` variant: a tombstoned admin is already excluded from the
268
+ // active count, so removing it can't drop the tally — guarding it would
269
+ // falsely block (cannot_delete_last_admin) the removal of a soft-deleted
270
+ // admin while another active admin exists. Keeper branch above stays
271
+ // unconditional; the last-admin branch fires only for an *active* target.
272
+ if ((await query_account_has_active_global_role(ctx, target_account_id, ROLE_ADMIN)) &&
273
+ (await query_count_active_accounts_with_global_role(ctx, ROLE_ADMIN)) <= 1) {
274
+ deny(ERROR_CANNOT_DELETE_LAST_ADMIN, `cannot ${verb} the last admin account`);
275
+ }
276
+ };
277
+ // Soft-delete an account (reversible tombstone). Self-or-admin:
278
+ // deleting another account requires the admin role. Tombstones the
279
+ // account + its actor(s), revokes sessions/tokens, closes sockets, and
280
+ // emits `account_delete` + one `actor_delete` per soft-deleted actor —
281
+ // each carrying its identity snapshot. `delete` = soft.
282
+ const account_delete_handler = async (input, ctx) => {
283
+ const auth = ctx.auth;
284
+ const target_account_id = input.account_id ?? auth.account.id;
285
+ // Self-or-admin elevation: deleting someone else needs admin.
286
+ if (target_account_id !== auth.account.id && !has_role(auth, ROLE_ADMIN)) {
287
+ throw jsonrpc_errors.forbidden('cannot delete another account', {
288
+ reason: ERROR_INSUFFICIENT_PERMISSIONS,
289
+ });
290
+ }
291
+ // Keeper + last-admin guards (fail-loud, before the tombstone).
292
+ await assert_account_removable(ctx, target_account_id, 'account_delete');
293
+ // Snapshot actor names before the tombstone.
294
+ const actors = await query_actors_by_account(ctx, target_account_id);
295
+ const snapshot = await query_account_soft_delete(ctx, target_account_id, auth.actor.id);
296
+ if (!snapshot) {
297
+ // Missing or already soft-deleted — UUID probe key, failure audit is safe.
298
+ deps.audit.emit(ctx, {
299
+ event_type: 'account_delete',
300
+ outcome: 'failure',
301
+ actor_id: auth.actor.id,
302
+ account_id: auth.account.id,
303
+ ip: ctx.client_ip,
304
+ metadata: { reason: ERROR_ACCOUNT_NOT_FOUND, attempted_account_id: target_account_id },
305
+ });
306
+ throw jsonrpc_errors.not_found('account', { reason: ERROR_ACCOUNT_NOT_FOUND });
307
+ }
308
+ const soft_deleted_actors = [];
309
+ for (const actor of actors) {
310
+ if (await query_actor_soft_delete(ctx, actor.id, auth.actor.id)) {
311
+ soft_deleted_actors.push(actor);
312
+ }
313
+ }
314
+ await query_session_revoke_all_for_account(ctx, target_account_id);
315
+ await query_revoke_all_api_tokens_for_account(ctx, target_account_id);
316
+ if (connection_closer)
317
+ connection_closer.close_sockets_for_account(target_account_id);
318
+ deps.audit.emit(ctx, {
319
+ event_type: 'account_delete',
320
+ actor_id: auth.actor.id,
321
+ account_id: auth.account.id,
322
+ target_account_id,
323
+ ip: ctx.client_ip,
324
+ metadata: { username: snapshot.username, email: snapshot.email },
325
+ });
326
+ for (const actor of soft_deleted_actors) {
327
+ deps.audit.emit(ctx, {
328
+ event_type: 'actor_delete',
329
+ actor_id: auth.actor.id,
330
+ account_id: auth.account.id,
331
+ target_account_id,
332
+ target_actor_id: actor.id,
333
+ ip: ctx.client_ip,
334
+ metadata: { name: actor.name },
335
+ });
336
+ }
337
+ return { ok: true, deleted: true };
338
+ };
339
+ // Hard-purge an account (keeper-only, irreversible). Fail-loud:
340
+ // requires `confirm: true` + emits a WARN. Snapshots identity into
341
+ // `account_purge` + per-actor `actor_purge` before the cascading delete
342
+ // removes the rows — the `audit_log` id columns carry no FK, so the
343
+ // purged ids survive on historical rows and the snapshots name them.
344
+ const account_purge_handler = async (input, ctx) => {
345
+ const auth = ctx.auth;
346
+ // Fail-loud: refuse the irreversible purge without explicit confirm.
347
+ if (input.confirm !== true) {
348
+ throw jsonrpc_errors.invalid_params('purge requires confirm: true', {
349
+ reason: ERROR_PURGE_NOT_CONFIRMED,
350
+ });
351
+ }
352
+ // Keeper + last-admin guards (fail-loud, before the cascade).
353
+ await assert_account_removable(ctx, input.account_id, 'account_purge');
354
+ // Snapshot actors before the cascade removes them.
355
+ const actors = await query_actors_by_account(ctx, input.account_id);
356
+ const snapshot = await query_purge_account(ctx, input.account_id);
357
+ if (!snapshot) {
358
+ deps.audit.emit(ctx, {
359
+ event_type: 'account_purge',
360
+ outcome: 'failure',
361
+ actor_id: auth.actor.id,
362
+ account_id: auth.account.id,
363
+ ip: ctx.client_ip,
364
+ metadata: { reason: ERROR_ACCOUNT_NOT_FOUND, attempted_account_id: input.account_id },
365
+ });
366
+ throw jsonrpc_errors.not_found('account', { reason: ERROR_ACCOUNT_NOT_FOUND });
367
+ }
368
+ if (connection_closer)
369
+ connection_closer.close_sockets_for_account(input.account_id);
370
+ deps.log.warn(`account hard-purged (irreversible cascading delete): ${input.account_id} by actor ${auth.actor.id}`);
371
+ deps.audit.emit(ctx, {
372
+ event_type: 'account_purge',
373
+ actor_id: auth.actor.id,
374
+ account_id: auth.account.id,
375
+ target_account_id: input.account_id,
376
+ ip: ctx.client_ip,
377
+ metadata: { username: snapshot.username, email: snapshot.email },
378
+ });
379
+ for (const actor of actors) {
380
+ deps.audit.emit(ctx, {
381
+ event_type: 'actor_purge',
382
+ actor_id: auth.actor.id,
383
+ account_id: auth.account.id,
384
+ target_account_id: input.account_id,
385
+ target_actor_id: actor.id,
386
+ ip: ctx.client_ip,
387
+ metadata: { name: actor.name },
388
+ });
389
+ }
390
+ return { ok: true, purged: true };
391
+ };
392
+ // Reactivate a soft-deleted account (clears the tombstone). Admin-only —
393
+ // the self path is unreachable (a tombstoned account can't authenticate).
394
+ // Clears `deleted_at` on the account + its soft-deleted actor(s) and
395
+ // emits `account_undelete` + one `actor_undelete` per reactivated actor.
396
+ // Does NOT restore revoked sessions/tokens. The inverse of `delete`.
397
+ const account_undelete_handler = async (input, ctx) => {
398
+ const auth = ctx.auth;
399
+ // Snapshot actors (the listing includes soft-deleted rows) before
400
+ // clearing the tombstones so names land in the per-actor events.
401
+ const actors = await query_actors_by_account(ctx, input.account_id);
402
+ const snapshot = await query_account_undelete(ctx, input.account_id);
403
+ if (!snapshot) {
404
+ // Missing or not soft-deleted — UUID probe key, failure audit is safe.
405
+ deps.audit.emit(ctx, {
406
+ event_type: 'account_undelete',
407
+ outcome: 'failure',
408
+ actor_id: auth.actor.id,
409
+ account_id: auth.account.id,
410
+ ip: ctx.client_ip,
411
+ metadata: { reason: ERROR_ACCOUNT_NOT_FOUND, attempted_account_id: input.account_id },
412
+ });
413
+ throw jsonrpc_errors.not_found('account', { reason: ERROR_ACCOUNT_NOT_FOUND });
414
+ }
415
+ const undeleted_actors = [];
416
+ for (const actor of actors) {
417
+ if (await query_actor_undelete(ctx, actor.id)) {
418
+ undeleted_actors.push(actor);
419
+ }
420
+ }
421
+ deps.audit.emit(ctx, {
422
+ event_type: 'account_undelete',
423
+ actor_id: auth.actor.id,
424
+ account_id: auth.account.id,
425
+ target_account_id: input.account_id,
426
+ ip: ctx.client_ip,
427
+ metadata: { username: snapshot.username, email: snapshot.email },
428
+ });
429
+ for (const actor of undeleted_actors) {
430
+ deps.audit.emit(ctx, {
431
+ event_type: 'actor_undelete',
432
+ actor_id: auth.actor.id,
433
+ account_id: auth.account.id,
434
+ target_account_id: input.account_id,
435
+ target_actor_id: actor.id,
436
+ ip: ctx.client_ip,
437
+ metadata: { name: actor.name },
438
+ });
439
+ }
440
+ return { ok: true, undeleted: true };
441
+ };
231
442
  const actions = [
232
443
  rpc_action(admin_account_list_action_spec, account_list_handler),
444
+ rpc_action(account_delete_action_spec, account_delete_handler),
445
+ rpc_action(account_purge_action_spec, account_purge_handler),
446
+ rpc_action(account_undelete_action_spec, account_undelete_handler),
233
447
  rpc_action(admin_session_list_action_spec, session_list_handler),
234
448
  rpc_action(admin_session_revoke_all_action_spec, session_revoke_all_handler),
235
449
  rpc_action(admin_token_revoke_all_action_spec, token_revoke_all_handler),
@@ -17,8 +17,17 @@
17
17
  * - `target_actor_id` is populated iff the event subject is actor-bound
18
18
  * (see `AuditLogEvent.target_actor_id` doc-comment for the rule).
19
19
  *
20
+ * **No FK on the four identity columns.** They are plain `UUID`, not
21
+ * `REFERENCES … ON DELETE SET NULL`. An audit log is an append-only
22
+ * historical record, not a live relational entity; `SET NULL` erased the
23
+ * very attribution the log exists to preserve. With soft-delete as the
24
+ * default (`account.deleted_at`) the rows stay and the ids JOIN-resolve;
25
+ * on a hard purge the raw id survives instead of nulling, and the purge
26
+ * audit event snapshots the identity into metadata (delete = soft,
27
+ * purge = hard).
28
+ *
20
29
  * @module
21
30
  */
22
- export declare const AUDIT_LOG_SCHEMA = "\nCREATE TABLE IF NOT EXISTS audit_log (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n seq BIGSERIAL NOT NULL,\n event_type TEXT NOT NULL,\n outcome TEXT NOT NULL DEFAULT 'success',\n actor_id UUID REFERENCES actor(id) ON DELETE SET NULL,\n account_id UUID REFERENCES account(id) ON DELETE SET NULL,\n target_account_id UUID REFERENCES account(id) ON DELETE SET NULL,\n target_actor_id UUID REFERENCES actor(id) ON DELETE SET NULL,\n ip TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB\n)";
31
+ export declare const AUDIT_LOG_SCHEMA = "\nCREATE TABLE IF NOT EXISTS audit_log (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n seq BIGSERIAL NOT NULL,\n event_type TEXT NOT NULL,\n outcome TEXT NOT NULL DEFAULT 'success',\n actor_id UUID,\n account_id UUID,\n target_account_id UUID,\n target_actor_id UUID,\n ip TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB\n)";
23
32
  export declare const AUDIT_LOG_INDEXES: string[];
24
33
  //# sourceMappingURL=audit_log_ddl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,eAAO,MAAM,gBAAgB,ohBAa3B,CAAC;AAEH,eAAO,MAAM,iBAAiB,UAM7B,CAAC"}
1
+ {"version":3,"file":"audit_log_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,gBAAgB,gXAa3B,CAAC;AAEH,eAAO,MAAM,iBAAiB,UAM7B,CAAC"}
@@ -17,6 +17,15 @@
17
17
  * - `target_actor_id` is populated iff the event subject is actor-bound
18
18
  * (see `AuditLogEvent.target_actor_id` doc-comment for the rule).
19
19
  *
20
+ * **No FK on the four identity columns.** They are plain `UUID`, not
21
+ * `REFERENCES … ON DELETE SET NULL`. An audit log is an append-only
22
+ * historical record, not a live relational entity; `SET NULL` erased the
23
+ * very attribution the log exists to preserve. With soft-delete as the
24
+ * default (`account.deleted_at`) the rows stay and the ids JOIN-resolve;
25
+ * on a hard purge the raw id survives instead of nulling, and the purge
26
+ * audit event snapshots the identity into metadata (delete = soft,
27
+ * purge = hard).
28
+ *
20
29
  * @module
21
30
  */
22
31
  export const AUDIT_LOG_SCHEMA = `
@@ -25,10 +34,10 @@ CREATE TABLE IF NOT EXISTS audit_log (
25
34
  seq BIGSERIAL NOT NULL,
26
35
  event_type TEXT NOT NULL,
27
36
  outcome TEXT NOT NULL DEFAULT 'success',
28
- actor_id UUID REFERENCES actor(id) ON DELETE SET NULL,
29
- account_id UUID REFERENCES account(id) ON DELETE SET NULL,
30
- target_account_id UUID REFERENCES account(id) ON DELETE SET NULL,
31
- target_actor_id UUID REFERENCES actor(id) ON DELETE SET NULL,
37
+ actor_id UUID,
38
+ account_id UUID,
39
+ target_account_id UUID,
40
+ target_actor_id UUID,
32
41
  ip TEXT,
33
42
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
34
43
  metadata JSONB
@@ -16,7 +16,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.js';
16
16
  * Not a security boundary — in-process code has many other paths to subvert
17
17
  * audit logging.
18
18
  */
19
- export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "app_settings_update"];
19
+ export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "account_delete", "account_purge", "account_undelete", "actor_delete", "actor_purge", "actor_undelete", "app_settings_update"];
20
20
  /** Zod schema for audit event types. */
21
21
  export declare const AuditEventType: z.ZodEnum<{
22
22
  bootstrap: "bootstrap";
@@ -39,6 +39,12 @@ export declare const AuditEventType: z.ZodEnum<{
39
39
  role_grant_offer_supersede: "role_grant_offer_supersede";
40
40
  invite_create: "invite_create";
41
41
  invite_delete: "invite_delete";
42
+ account_delete: "account_delete";
43
+ account_purge: "account_purge";
44
+ account_undelete: "account_undelete";
45
+ actor_delete: "actor_delete";
46
+ actor_purge: "actor_purge";
47
+ actor_undelete: "actor_undelete";
42
48
  app_settings_update: "app_settings_update";
43
49
  }>;
44
50
  export type AuditEventType = z.infer<typeof AuditEventType>;
@@ -192,6 +198,33 @@ export declare const audit_metadata_schemas: Readonly<{
192
198
  invite_delete: z.ZodObject<{
193
199
  invite_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
194
200
  }, z.core.$loose>;
201
+ account_delete: z.ZodObject<{
202
+ username: z.ZodOptional<z.ZodString>;
203
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
204
+ reason: z.ZodOptional<z.ZodString>;
205
+ attempted_account_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
206
+ }, z.core.$loose>;
207
+ account_purge: z.ZodObject<{
208
+ username: z.ZodOptional<z.ZodString>;
209
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
210
+ reason: z.ZodOptional<z.ZodString>;
211
+ attempted_account_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
212
+ }, z.core.$loose>;
213
+ account_undelete: z.ZodObject<{
214
+ username: z.ZodOptional<z.ZodString>;
215
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
216
+ reason: z.ZodOptional<z.ZodString>;
217
+ attempted_account_id: z.ZodOptional<z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">>;
218
+ }, z.core.$loose>;
219
+ actor_delete: z.ZodObject<{
220
+ name: z.ZodOptional<z.ZodString>;
221
+ }, z.core.$loose>;
222
+ actor_purge: z.ZodObject<{
223
+ name: z.ZodOptional<z.ZodString>;
224
+ }, z.core.$loose>;
225
+ actor_undelete: z.ZodObject<{
226
+ name: z.ZodOptional<z.ZodString>;
227
+ }, z.core.$loose>;
195
228
  app_settings_update: z.ZodObject<{
196
229
  setting: z.ZodString;
197
230
  old_value: z.ZodUnknown;
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAoB5C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,shBA4BnB,CAAC;AAEZ,wCAAwC;AACxC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAA4B,CAAC;AACxD,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,QAA+B,CAAC;AAExE,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,aAE7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,2CAA2C;AAC3C,eAAO,MAAM,YAAY;;;EAAiC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqRW,CAAC;AAE/C,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GAAG;KAC7B,CAAC,IAAI,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;CAClE,CAAC;AAEF,oGAAoG;AACpG,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,kBAAkB,CAAC;IAC/B,OAAO,EAAE,YAAY,CAAC;IACtB;;;;;;;;;;;;;OAaG;IACH,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,cAAc,EAC1D,OAAO,aAAa,GAAG;IAAC,UAAU,EAAE,CAAC,CAAA;CAAC,KACpC,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAExB,CAAC;AAEF,6CAA6C;AAC7C,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,cAAc;IAC/D,UAAU,EAAE,CAAC,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,iBAAiB,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAChC,eAAe,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,SAAS,cAAc,GAChC,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,GACtD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,cAAc;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CAC/D;AAED,4FAA4F;AAC5F,eAAO,MAAM,wBAAwB,EAAE,cAGrC,CAAC;AAEH,6CAA6C;AAC7C,MAAM,WAAW,2BAA2B;IAC3C;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU,2BAA2B,KAAG,cA2B/E,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;kBAY5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,+DAA+D;AAC/D,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;kBAGzC,CAAC;AACH,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAE5F,wEAAwE;AACxE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;kBAGpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,iEAAiE;AACjE,eAAO,MAAM,gBAAgB;;;;;;;kBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
@@ -52,6 +52,12 @@ export const AUDIT_EVENT_TYPES = Object.freeze([
52
52
  'role_grant_offer_supersede',
53
53
  'invite_create',
54
54
  'invite_delete',
55
+ 'account_delete',
56
+ 'account_purge',
57
+ 'account_undelete',
58
+ 'actor_delete',
59
+ 'actor_purge',
60
+ 'actor_undelete',
55
61
  'app_settings_update',
56
62
  ]);
57
63
  /** Zod schema for audit event types. */
@@ -266,6 +272,73 @@ export const audit_metadata_schemas = Object.freeze({
266
272
  invite_delete: z.looseObject({
267
273
  invite_id: Uuid.meta({ description: 'Id of the deleted invite.' }),
268
274
  }),
275
+ // Account/actor deletion snapshots identity into metadata so the
276
+ // identity behind a now-orphaned audit id survives a purge (the
277
+ // `audit_log` id columns carry no FK). `delete` = soft (reversible
278
+ // tombstone), `purge` = hard (irreversible cascading removal). Known
279
+ // fields are optional so the same schema validates the success
280
+ // snapshot and the not-found failure shape (`reason` /
281
+ // `attempted_account_id`). See `auth/CLAUDE.md` §Account/actor deletion
282
+ // (delete = soft, purge = hard).
283
+ account_delete: z.looseObject({
284
+ username: z.string().optional().meta({ description: 'Username at soft-delete time.' }),
285
+ email: z
286
+ .string()
287
+ .nullable()
288
+ .optional()
289
+ .meta({ description: 'Email at soft-delete time; null when unset.' }),
290
+ reason: z
291
+ .string()
292
+ .optional()
293
+ .meta({ description: 'Failure category. Set only on `outcome=failure`.' }),
294
+ attempted_account_id: Uuid.optional().meta({
295
+ description: 'Probed account id when the target was missing (`outcome=failure`).',
296
+ }),
297
+ }),
298
+ account_purge: z.looseObject({
299
+ username: z.string().optional().meta({ description: 'Username at purge time.' }),
300
+ email: z
301
+ .string()
302
+ .nullable()
303
+ .optional()
304
+ .meta({ description: 'Email at purge time; null when unset.' }),
305
+ reason: z
306
+ .string()
307
+ .optional()
308
+ .meta({ description: 'Failure category. Set only on `outcome=failure`.' }),
309
+ attempted_account_id: Uuid.optional().meta({
310
+ description: 'Probed account id when the target was missing (`outcome=failure`).',
311
+ }),
312
+ }),
313
+ // `account_undelete` / `actor_undelete` are the reactivation events
314
+ // (clearing the soft-delete tombstone). Same identity-snapshot shape as
315
+ // the delete events so reviewers see who was reactivated; `reason` /
316
+ // `attempted_account_id` carry the not-found failure shape. Admin-only —
317
+ // the self path is unreachable (a tombstoned account can't authenticate).
318
+ account_undelete: z.looseObject({
319
+ username: z.string().optional().meta({ description: 'Username at reactivation time.' }),
320
+ email: z
321
+ .string()
322
+ .nullable()
323
+ .optional()
324
+ .meta({ description: 'Email at reactivation time; null when unset.' }),
325
+ reason: z
326
+ .string()
327
+ .optional()
328
+ .meta({ description: 'Failure category. Set only on `outcome=failure`.' }),
329
+ attempted_account_id: Uuid.optional().meta({
330
+ description: 'Probed account id when the target was missing/not-deleted (`outcome=failure`).',
331
+ }),
332
+ }),
333
+ actor_delete: z.looseObject({
334
+ name: z.string().optional().meta({ description: 'Actor display name at soft-delete time.' }),
335
+ }),
336
+ actor_purge: z.looseObject({
337
+ name: z.string().optional().meta({ description: 'Actor display name at purge time.' }),
338
+ }),
339
+ actor_undelete: z.looseObject({
340
+ name: z.string().optional().meta({ description: 'Actor display name at reactivation time.' }),
341
+ }),
269
342
  app_settings_update: z.looseObject({
270
343
  setting: z.string().meta({ description: 'Name of the setting that changed.' }),
271
344
  old_value: z.unknown().meta({ description: 'Setting value before the update.' }),
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- export declare const ACCOUNT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS account (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n username TEXT UNIQUE NOT NULL,\n email TEXT,\n email_verified BOOLEAN NOT NULL DEFAULT false,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n created_by UUID,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_by UUID\n)";
13
- export declare const ACTOR_SCHEMA = "\nCREATE TABLE IF NOT EXISTS actor (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n account_id UUID NOT NULL REFERENCES account(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ,\n updated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
12
+ export declare const ACCOUNT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS account (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n username TEXT UNIQUE NOT NULL,\n email TEXT,\n email_verified BOOLEAN NOT NULL DEFAULT false,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n created_by UUID,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_by UUID,\n deleted_at TIMESTAMPTZ,\n deleted_by UUID\n)";
13
+ export declare const ACTOR_SCHEMA = "\nCREATE TABLE IF NOT EXISTS actor (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n account_id UUID NOT NULL REFERENCES account(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ,\n updated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n deleted_at TIMESTAMPTZ,\n deleted_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
14
14
  export declare const ACTOR_INDEX = "\nCREATE INDEX IF NOT EXISTS idx_actor_account ON actor(account_id)";
15
15
  /**
16
16
  * Functional index on `LOWER(actor.name)` supporting case-insensitive
@@ -1 +1 @@
1
- {"version":3,"file":"auth_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,cAAc,8WAWzB,CAAC;AAEH,eAAO,MAAM,YAAY,mUAQvB,CAAC;AAEH,eAAO,MAAM,WAAW,wEAC0C,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,8FACqD,CAAC;AAEzF,eAAO,MAAM,iBAAiB,2ZAU5B,CAAC;AAEH,eAAO,MAAM,kBAAkB,UAI9B,CAAC;AAEF,eAAO,MAAM,mBAAmB,0RAO9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,UAGhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,iUAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB,4GACsE,CAAC;AAEvG,eAAO,MAAM,yBAAyB,6FACiD,CAAC;AAExF,eAAO,MAAM,eAAe,gFAC8C,CAAC;AAE3E,eAAO,MAAM,qBAAqB,wJAIhC,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,yHAGP,CAAC;AAE1B,eAAO,MAAM,aAAa,6ZAUxB,CAAC;AAEH,eAAO,MAAM,cAAc,UAI1B,CAAC;AAEF,eAAO,MAAM,mBAAmB,oMAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB,sEACkC,CAAC"}
1
+ {"version":3,"file":"auth_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,eAAO,MAAM,cAAc,6ZAazB,CAAC;AAEH,eAAO,MAAM,YAAY,0ZAUvB,CAAC;AAEH,eAAO,MAAM,WAAW,wEAC0C,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,8FACqD,CAAC;AAEzF,eAAO,MAAM,iBAAiB,2ZAU5B,CAAC;AAEH,eAAO,MAAM,kBAAkB,UAI9B,CAAC;AAEF,eAAO,MAAM,mBAAmB,0RAO9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,UAGhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,iUAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB,4GACsE,CAAC;AAEvG,eAAO,MAAM,yBAAyB,6FACiD,CAAC;AAExF,eAAO,MAAM,eAAe,gFAC8C,CAAC;AAE3E,eAAO,MAAM,qBAAqB,wJAIhC,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,yHAGP,CAAC;AAE1B,eAAO,MAAM,aAAa,6ZAUxB,CAAC;AAEH,eAAO,MAAM,cAAc,UAI1B,CAAC;AAEF,eAAO,MAAM,mBAAmB,oMAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB,sEACkC,CAAC"}
@@ -9,6 +9,10 @@
9
9
  *
10
10
  * @module
11
11
  */
12
+ // `deleted_at` is the soft-delete tombstone (delete = soft, purge = hard).
13
+ // Auth resolution treats a non-null `deleted_at` as absent; the username /
14
+ // email unique indexes stay unconditional so a soft-deleted identity stays
15
+ // reserved (no reuse): delete is soft, purge is hard.
12
16
  export const ACCOUNT_SCHEMA = `
13
17
  CREATE TABLE IF NOT EXISTS account (
14
18
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -19,7 +23,9 @@ CREATE TABLE IF NOT EXISTS account (
19
23
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
20
24
  created_by UUID,
21
25
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
22
- updated_by UUID
26
+ updated_by UUID,
27
+ deleted_at TIMESTAMPTZ,
28
+ deleted_by UUID
23
29
  )`;
24
30
  export const ACTOR_SCHEMA = `
25
31
  CREATE TABLE IF NOT EXISTS actor (
@@ -28,7 +34,9 @@ CREATE TABLE IF NOT EXISTS actor (
28
34
  name TEXT NOT NULL,
29
35
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
30
36
  updated_at TIMESTAMPTZ,
31
- updated_by UUID REFERENCES actor(id) ON DELETE SET NULL
37
+ updated_by UUID REFERENCES actor(id) ON DELETE SET NULL,
38
+ deleted_at TIMESTAMPTZ,
39
+ deleted_by UUID REFERENCES actor(id) ON DELETE SET NULL
32
40
  )`;
33
41
  export const ACTOR_INDEX = `
34
42
  CREATE INDEX IF NOT EXISTS idx_actor_account ON actor(account_id)`;