@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.
- package/dist/auth/CLAUDE.md +99 -5
- package/dist/auth/account_queries.d.ts +87 -4
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +107 -17
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +8 -0
- package/dist/auth/admin_action_specs.d.ts +168 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +146 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +218 -4
- package/dist/auth/audit_log_ddl.d.ts +10 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +13 -4
- package/dist/auth/audit_log_schema.d.ts +34 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +73 -0
- package/dist/auth/auth_ddl.d.ts +2 -2
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +10 -2
- package/dist/auth/cell_action_specs.d.ts +1295 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_action_specs.js +397 -0
- package/dist/auth/cell_actions.d.ts +63 -0
- package/dist/auth/cell_actions.d.ts.map +1 -0
- package/dist/auth/cell_actions.js +546 -0
- package/dist/auth/cell_audit_action_specs.d.ts +131 -0
- package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_audit_action_specs.js +70 -0
- package/dist/auth/cell_audit_actions.d.ts +18 -0
- package/dist/auth/cell_audit_actions.d.ts.map +1 -0
- package/dist/auth/cell_audit_actions.js +59 -0
- package/dist/auth/cell_audit_events.d.ts +28 -0
- package/dist/auth/cell_audit_events.d.ts.map +1 -0
- package/dist/auth/cell_audit_events.js +42 -0
- package/dist/auth/cell_audit_metadata.d.ts +48 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_audit_metadata.js +46 -0
- package/dist/auth/cell_authorize.d.ts +88 -0
- package/dist/auth/cell_authorize.d.ts.map +1 -0
- package/dist/auth/cell_authorize.js +172 -0
- package/dist/auth/cell_data_schema.d.ts +44 -0
- package/dist/auth/cell_data_schema.d.ts.map +1 -0
- package/dist/auth/cell_data_schema.js +42 -0
- package/dist/auth/cell_field_action_specs.d.ts +244 -0
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_field_action_specs.js +136 -0
- package/dist/auth/cell_field_actions.d.ts +34 -0
- package/dist/auth/cell_field_actions.d.ts.map +1 -0
- package/dist/auth/cell_field_actions.js +153 -0
- package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
- package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_field_audit_metadata.js +28 -0
- package/dist/auth/cell_grant_action_specs.d.ts +333 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_grant_action_specs.js +148 -0
- package/dist/auth/cell_grant_actions.d.ts +50 -0
- package/dist/auth/cell_grant_actions.d.ts.map +1 -0
- package/dist/auth/cell_grant_actions.js +208 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_grant_audit_metadata.js +54 -0
- package/dist/auth/cell_item_action_specs.d.ts +331 -0
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_item_action_specs.js +182 -0
- package/dist/auth/cell_item_actions.d.ts +37 -0
- package/dist/auth/cell_item_actions.d.ts.map +1 -0
- package/dist/auth/cell_item_actions.js +204 -0
- package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
- package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_item_audit_metadata.js +32 -0
- package/dist/auth/cell_relation_visibility.d.ts +32 -0
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
- package/dist/auth/cell_relation_visibility.js +57 -0
- package/dist/auth/deps.d.ts +9 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +30 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +54 -0
- package/dist/db/CLAUDE.md +118 -0
- package/dist/db/cell_audit_queries.d.ts +26 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -0
- package/dist/db/cell_audit_queries.js +53 -0
- package/dist/db/cell_ddl.d.ts +151 -0
- package/dist/db/cell_ddl.d.ts.map +1 -0
- package/dist/db/cell_ddl.js +247 -0
- package/dist/db/cell_field_queries.d.ts +105 -0
- package/dist/db/cell_field_queries.d.ts.map +1 -0
- package/dist/db/cell_field_queries.js +113 -0
- package/dist/db/cell_grant_queries.d.ts +132 -0
- package/dist/db/cell_grant_queries.d.ts.map +1 -0
- package/dist/db/cell_grant_queries.js +145 -0
- package/dist/db/cell_history_ddl.d.ts +38 -0
- package/dist/db/cell_history_ddl.d.ts.map +1 -0
- package/dist/db/cell_history_ddl.js +61 -0
- package/dist/db/cell_item_queries.d.ts +107 -0
- package/dist/db/cell_item_queries.d.ts.map +1 -0
- package/dist/db/cell_item_queries.js +119 -0
- package/dist/db/cell_queries.d.ts +327 -0
- package/dist/db/cell_queries.d.ts.map +1 -0
- package/dist/db/cell_queries.js +431 -0
- package/dist/db/fact_ddl.d.ts +38 -0
- package/dist/db/fact_ddl.d.ts.map +1 -0
- package/dist/db/fact_ddl.js +71 -0
- package/dist/db/fact_queries.d.ts +140 -0
- package/dist/db/fact_queries.d.ts.map +1 -0
- package/dist/db/fact_queries.js +161 -0
- package/dist/db/fact_store.d.ts +112 -0
- package/dist/db/fact_store.d.ts.map +1 -0
- package/dist/db/fact_store.js +225 -0
- package/dist/server/env.d.ts +2 -0
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +6 -0
- package/dist/server/fact_write.d.ts +32 -0
- package/dist/server/fact_write.d.ts.map +1 -0
- package/dist/server/fact_write.js +56 -0
- package/dist/server/file_fact_fetcher.d.ts +42 -0
- package/dist/server/file_fact_fetcher.d.ts.map +1 -0
- package/dist/server/file_fact_fetcher.js +60 -0
- package/dist/server/file_fact_url.d.ts +53 -0
- package/dist/server/file_fact_url.d.ts.map +1 -0
- package/dist/server/file_fact_url.js +52 -0
- package/dist/server/serve_fact_route.d.ts +78 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -0
- package/dist/server/serve_fact_route.js +205 -0
- package/dist/testing/CLAUDE.md +58 -5
- package/dist/testing/app_server.d.ts +12 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +36 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +67 -1
- package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/account_lifecycle.js +76 -0
- package/dist/testing/cross_backend/capabilities.d.ts +31 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
- package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_crud.js +168 -0
- package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_relations.js +229 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +6 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +5 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +31 -3
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_bun.js +29 -2
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -0
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +4 -0
- package/dist/ui/AdminAccounts.svelte +58 -0
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +45 -1
- package/dist/ui/admin_rpc_adapters.d.ts +6 -2
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +5 -1
- 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
|
|
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
|
|
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
|
|
29
|
-
account_id UUID
|
|
30
|
-
target_account_id UUID
|
|
31
|
-
target_actor_id UUID
|
|
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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAoB5C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,
|
|
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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"}
|
|
@@ -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.' }),
|
package/dist/auth/auth_ddl.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/auth/auth_ddl.js
CHANGED
|
@@ -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)`;
|