letopis 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENT-CHEATSHEET.en.md +10 -5
  2. package/AGENT-CHEATSHEET.md +14 -9
  3. package/CHANGELOG.md +159 -0
  4. package/README.en.md +119 -52
  5. package/README.md +107 -47
  6. package/dist/acl.d.ts +11 -5
  7. package/dist/admin.d.ts +30 -14
  8. package/dist/admin.js +42 -16
  9. package/dist/auth.d.ts +61 -17
  10. package/dist/auth.js +16 -2
  11. package/dist/cache.d.ts +5 -1
  12. package/dist/chain.d.ts +91 -15
  13. package/dist/chain.js +47 -4
  14. package/dist/cli.js +6 -0
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +5 -5
  17. package/dist/import.js +7 -0
  18. package/dist/index.d.ts +74 -45
  19. package/dist/index.js +12 -5
  20. package/dist/load.d.ts +10 -3
  21. package/dist/load.js +19 -4
  22. package/dist/mcp.d.ts +20 -11
  23. package/dist/mcp.js +4 -4
  24. package/dist/model.d.ts +4 -1
  25. package/dist/ops.d.ts +58 -18
  26. package/dist/ops.js +58 -18
  27. package/dist/project.d.ts +30 -11
  28. package/dist/project.js +30 -4
  29. package/dist/registry.d.ts +4 -1
  30. package/dist/registry.js +4 -1
  31. package/dist/sql.d.ts +4 -1
  32. package/dist/sql.js +12 -4
  33. package/dist/tx.js +10 -1
  34. package/dist/types.d.ts +14 -7
  35. package/dist/types.js +13 -6
  36. package/dist/up.d.ts +48 -25
  37. package/dist/up.js +89 -41
  38. package/dist/validate.d.ts +9 -2
  39. package/dist/validate.js +19 -3
  40. package/dist/watch.d.ts +17 -5
  41. package/dist/watch.js +40 -8
  42. package/dist/write.d.ts +3 -2
  43. package/dist/write.js +29 -10
  44. package/package.json +1 -1
  45. package/sql/10-core.sql +6 -1
  46. package/sql/60-write.sql +38 -18
  47. package/sql/80-auth.sql +39 -2
  48. package/sql/88-derived.sql +1 -0
  49. package/sql/90-time.sql +15 -5
  50. package/sql/95-seed.system.sql +6 -3
  51. package/sql/99-revision.sql +1 -1
  52. package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/sql/60-write.sql CHANGED
@@ -1217,7 +1217,8 @@ begin
1217
1217
  select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason, n.moved, n.prev_hash
1218
1218
  from added n
1219
1219
  join (select k.class, k.tenant from (select distinct a.class, a.tenant from added a) k
1220
- where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant;
1220
+ where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
1221
+ order by n.at, n.id; -- порядок seq — порядок входа, а не плана соединения
1221
1222
  -- сигнал — по паре «класс, арендатор»
1222
1223
  select array_agg(k.class), array_agg(k.tenant) into sc, st from (select distinct a.class, a.tenant from added a) k;
1223
1224
  perform @SCHEMA@.signal(sc, st);
@@ -1273,7 +1274,8 @@ begin
1273
1274
  from new_rows n
1274
1275
  join old_rows o on o.id = n.id and o.rev <> n.rev
1275
1276
  join (select k.class, k.tenant from (select distinct a.class, a.tenant from new_rows a) k
1276
- where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant;
1277
+ where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
1278
+ order by n.id; -- порядок seq не зависит от плана: правка многих строк пишет версии в порядке id
1277
1279
  -- сигнал — по парам нового и прежнего класса: при смене класса без ключа ответы прежнего класса в
1278
1280
  -- кэше других процессов устарели бы (в 1.0.1 сигнал шёл только по новому)
1279
1281
  select array_agg(k.class), array_agg(k.tenant) into sc, st
@@ -1385,6 +1387,24 @@ begin
1385
1387
  return null;
1386
1388
  end $$;
1387
1389
 
1390
+ -- Перед удалением строки Account (ревизия 7, решение владельца). Строки secret аккаунта — сессии, хэши паролей и ключей,
1391
+ -- секреты TOTP, внешние учётные записи — уносит каскад внешнего ключа secret.account в конце оператора, раньше триггера
1392
+ -- после него, поэтому сессии гаснут с сигналом session:<хэш> здесь: процессы сбрасывают кэш ответов этих сессий.
1393
+ -- Срабатывает для всех ролей, в том числе владельца: аккаунты удаляют и функции владельца — каскад trg_entity_ad и
1394
+ -- account_purge. Строка Account у System не удаляется: ей принадлежат служебные строки secret (KEY — ключ прав и
1395
+ -- сигналов, LOCK — строки-замки, PROJECTOR — чекпоинты проекторов), которые каскад стёр бы, а без них установка не пишет.
1396
+ create or replace function @SCHEMA@.trg_entity_bd_account() returns trigger
1397
+ language plpgsql security definer
1398
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
1399
+ as $$
1400
+ begin
1401
+ if OLD.id = '@SYSTEM@'::uuid then
1402
+ perform @SCHEMA@.lt_fail('acl_denied', 'аккаунт System не удаляется: ему принадлежат служебные строки установки');
1403
+ end if;
1404
+ perform @SCHEMA@.auth_kill(OLD.id, null);
1405
+ return OLD;
1406
+ end $$;
1407
+
1388
1408
  -- Действие со строкой, которая ссылается на удаляемые id (p_ids), по правилам её концов (p_ends):
1389
1409
  -- cascade сильнее restrict, restrict сильнее unset; null — строка на них не ссылается. Общее правило
1390
1410
  -- для триггера удаления и превью delete_preview() (этап 4, п. 6): превью не расходится с удалением.
@@ -1456,9 +1476,7 @@ declare
1456
1476
  dplan jsonb;
1457
1477
  wplan jsonb;
1458
1478
  denied text;
1459
- accs uuid[];
1460
1479
  mems jsonb;
1461
- lvl_accs uuid[];
1462
1480
  lvl_mems jsonb;
1463
1481
  drows jsonb;
1464
1482
  own uuid[];
@@ -1526,19 +1544,21 @@ begin
1526
1544
  perform @SCHEMA@.agg_mark(g.tenant, @SCHEMA@.agg_of(g.data->'resolved'))
1527
1545
  from gone g where g.class = 'Class' and g.data->'resolved' ? 'aggregate';
1528
1546
  v_author := coalesce(c.author, '@SYSTEM@'::uuid);
1529
- -- надгробия: последний снимок следующей версией с видом delete — одной вставкой на уровень
1547
+ -- надгробия: последний снимок следующей версией с видом delete — одной вставкой на уровень, в порядке id: без
1548
+ -- order by порядок seq (а с ним итог delete() и время at) зависел от плана соединения и менялся от прогона к
1549
+ -- прогону; время now_ts() (volatile) планировщик вычисляет уже после сортировки
1530
1550
  insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, changed)
1531
1551
  select g.id, g.rev + 1, g.class, g.cv, g.tenant, g.owner, g.links, g.data, g.tags,
1532
1552
  greatest(@SCHEMA@.now_ts(), g.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason, null, g.hash, '[]'::jsonb
1533
1553
  from gone g
1534
1554
  join (select d.class, d.tenant from (select distinct x.class, x.tenant from gone x) d
1535
- where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant;
1555
+ where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
1556
+ order by g.id;
1536
1557
  -- пары «класс, арендатор» для сигнала — строкой «класс арендатор» (пробела нет ни в имени, ни в uuid)
1537
1558
  select coalesce(array_agg(g.id), '{}'), coalesce(array_agg(distinct g.class), '{}'),
1538
1559
  coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}'),
1539
- coalesce(array_agg(g.id) filter (where g.class = 'Account'), '{}'),
1540
1560
  coalesce(jsonb_agg(jsonb_build_object('tenant', g.tenant, 'user', g.links->>'User')) filter (where g.class = 'member'), '[]'::jsonb)
1541
- into lvl, classes, pairs, accs, mems from gone g;
1561
+ into lvl, classes, pairs, mems from gone g;
1542
1562
  all_ids := lvl;
1543
1563
  while cardinality(lvl) > 0 loop
1544
1564
  -- кто ссылается на уровень: действие по правилам концов (общее с превью)
@@ -1566,20 +1586,19 @@ begin
1566
1586
  from d
1567
1587
  join (select dd.class, dd.tenant from (select distinct x.class, x.tenant from d x) dd
1568
1588
  where @SCHEMA@.history_on(dd.class, dd.tenant)) h on h.class = d.class and h.tenant = d.tenant
1589
+ order by d.id -- надгробия уровня в порядке id, как и на уровне 0
1569
1590
  )
1570
1591
  select coalesce(array_agg(d.id), '{}'), coalesce(array_agg(distinct d.class), '{}'),
1571
1592
  coalesce(array_agg(distinct d.class || ' ' || d.tenant::text), '{}'),
1572
1593
  jsonb_agg(to_jsonb(d)) filter (where not @SCHEMA@.acl_row_ok(dplan, d.class, d.id, d.owner, d.tenant, d.tags, d.data, d.links)),
1573
- coalesce(array_agg(d.id) filter (where d.class = 'Account'), '{}'),
1574
1594
  coalesce(jsonb_agg(jsonb_build_object('tenant', d.tenant, 'user', d.links->>'User')) filter (where d.class = 'member'), '[]'::jsonb)
1575
- into lvl, lvl_classes, lvl_pairs, drows, lvl_accs, lvl_mems from d;
1595
+ into lvl, lvl_classes, lvl_pairs, drows, lvl_mems from d;
1576
1596
  -- класс отказавшей строки называется, только если актор её видит (ревью безопасности №3)
1577
1597
  if drows is not null then
1578
1598
  denied := @SCHEMA@.visible_class(drows);
1579
1599
  perform @SCHEMA@.lt_fail('acl_denied', case when denied is not null then format('нет права DELETE на класс %s в каскаде удаления', denied)
1580
1600
  else 'нет права DELETE на строки в каскаде удаления' end);
1581
1601
  end if;
1582
- accs := accs || lvl_accs;
1583
1602
  mems := mems || lvl_mems;
1584
1603
  classes := classes || lvl_classes;
1585
1604
  pairs := pairs || lvl_pairs;
@@ -1605,14 +1624,11 @@ begin
1605
1624
  end loop;
1606
1625
  -- выход из членства (и удаление аккаунта, которое снимает его членства): строки арендатора, которыми
1607
1626
  -- владел участник, переходят арендатору (§2.10) — новой версией
1608
- for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m loop
1627
+ for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m order by 1, 2 loop
1609
1628
  perform @SCHEMA@.owner_to_tenant(r.tenant, r.usr);
1610
1629
  end loop;
1611
- -- удалённый аккаунт: сессии гаснут, креды удаляются (идентификаторы освобождаются)
1612
- for r in select distinct u as acc from unnest(accs) u loop
1613
- perform @SCHEMA@.auth_kill(r.acc, null);
1614
- update @SCHEMA@.secret x set deleted_at = clock_timestamp() where x.account = r.acc and x.deleted_at is null;
1615
- end loop;
1630
+ -- удалённый аккаунт (ревизия 7): сессии погасил с сигналом триггер до удаления (trg_entity_bd_account), его строки
1631
+ -- secret стёр каскад внешнего ключа secret.account — идентификаторы входа свободны
1616
1632
  perform @SCHEMA@.signal(array_agg(split_part(x.p, ' ', 1)), array_agg(split_part(x.p, ' ', 2)::uuid))
1617
1633
  from (select distinct p from unnest(pairs) p) x;
1618
1634
  if classes && array['rule', 'Resource', 'member', 'Account', 'Class'] then
@@ -1700,6 +1716,9 @@ create or replace trigger entity_au after update on @SCHEMA@.entity referencing
1700
1716
  for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_au();
1701
1717
  create or replace trigger entity_bd before delete on @SCHEMA@.entity
1702
1718
  for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_bd();
1719
+ -- удаление аккаунта — для всех ролей, в том числе владельца (каскад trg_entity_ad, account_purge): ревизия 7
1720
+ create or replace trigger entity_bd_account before delete on @SCHEMA@.entity
1721
+ for each row when (OLD.class = 'Account') execute function @SCHEMA@.trg_entity_bd_account();
1703
1722
  create or replace trigger entity_ad after delete on @SCHEMA@.entity referencing old table as gone
1704
1723
  for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_ad();
1705
1724
  -- в режиме репликации (session_replication_role = replica) триггеры тоже работают
@@ -1708,6 +1727,7 @@ alter table @SCHEMA@.entity enable always trigger entity_bu;
1708
1727
  alter table @SCHEMA@.entity enable always trigger entity_ai;
1709
1728
  alter table @SCHEMA@.entity enable always trigger entity_au;
1710
1729
  alter table @SCHEMA@.entity enable always trigger entity_bd;
1730
+ alter table @SCHEMA@.entity enable always trigger entity_bd_account;
1711
1731
  alter table @SCHEMA@.entity enable always trigger entity_ad;
1712
1732
 
1713
1733
  -- ========================= смена класса с ключом =========================
@@ -2030,7 +2050,7 @@ begin
2030
2050
  'incoming_conflicts(uuid, text, uuid)', 'visible_detail(jsonb, uuid)', 'visible_class(jsonb)', 'class_change_safe(jsonb, jsonb)',
2031
2051
  'family_conflicts(text[], uuid, boolean)', 'reserved_names()', 'class_acl_admit(text, uuid)', 'class_rows_admit(text, uuid, text[], text)', 'class_has_history(text, uuid)', 'class_orphan_history(text, uuid)', 'class_admit(jsonb, uuid)',
2032
2052
  'trg_entity_bi()', 'trg_entity_bu()', 'trg_entity_ai()', 'trg_entity_au()', 'history_on(text, uuid)', 'signal_token(text, text, bytea)', 'signal_raw(text[])', 'signal(text[], uuid[])', 'class_evolve(@SCHEMA@.entity, @SCHEMA@.entity)',
2033
- 'trg_entity_bd()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
2053
+ 'trg_entity_bd()', 'trg_entity_bd_account()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
2034
2054
  execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
2035
2055
  execute format('revoke all on function @SCHEMA@.%s from public', f);
2036
2056
  end loop;
package/sql/80-auth.sql CHANGED
@@ -795,6 +795,43 @@ begin
795
795
  where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant);
796
796
  end $$;
797
797
 
798
+ -- Сведения о сессии по токену (db.auth.session): сервис, у которого есть токен пользователя (например, из
799
+ -- cookie), узнаёт, чья это сессия, жива ли она и в каком арендаторе работает, не подключаясь по этому токену.
800
+ -- Право auth.session, как у issue_session. Неверный, погашенный или истёкший токен и отключённый аккаунт — null:
801
+ -- причина не различается, как в ctx(). Сессия ищется только среди SESSION: ключ API или код с тем же sha256 не
802
+ -- подойдут. member — проверка членства из ctx() без исключения: членство в рабочем арендаторе цело и арендатор
803
+ -- включён; собственный арендатор — всегда true.
804
+ create or replace function @SCHEMA@.auth_session_info(p_token text) returns jsonb
805
+ language plpgsql stable security definer
806
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
807
+ as $$
808
+ declare
809
+ c record;
810
+ s record;
811
+ t uuid;
812
+ begin
813
+ select * into c from @SCHEMA@.ctx();
814
+ perform @SCHEMA@.auth_session_required(c.account);
815
+ perform @SCHEMA@.acl_require('auth.session');
816
+ select x.account, x.meta, x.expires_at into s from @SCHEMA@.secret x
817
+ where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(p_token, ''), 'UTF8'))
818
+ and (x.expires_at is null or x.expires_at > clock_timestamp());
819
+ if s.account is null or not @SCHEMA@.auth_enabled(s.account) then
820
+ return null;
821
+ end if;
822
+ t := coalesce((s.meta->>'tenant')::uuid, s.account);
823
+ return jsonb_build_object(
824
+ 'account', s.account,
825
+ 'tenant', t,
826
+ 'member', t = s.account or (
827
+ exists (select 1 from @SCHEMA@.entity x
828
+ where x.id = @SCHEMA@.v5_id(t, 'member', array[s.account::text, t::text]) and x.class = 'member' and x.tenant = t)
829
+ and @SCHEMA@.auth_enabled(t)),
830
+ 'expiresAt', s.expires_at,
831
+ 'loginAt', s.meta->'login_at',
832
+ 'methods', coalesce(s.meta->'methods', '[]'::jsonb));
833
+ end $$;
834
+
798
835
  do $$
799
836
  declare f text;
800
837
  begin
@@ -807,7 +844,7 @@ begin
807
844
  'login_otp(text, text, text)', 'totp_code(bytea, bigint)', 'auth_enroll_totp(uuid)', 'auth_verify_totp(uuid, text, int)',
808
845
  'auth_totp_enabled(uuid)', 'auth_link(uuid, text, text, jsonb, boolean)', 'auth_lookup(text, text, boolean)',
809
846
  'auth_credentials(uuid)', 'auth_revoke_credential(uuid, boolean)', 'auth_set_flags(uuid, boolean, boolean)',
810
- 'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()'] loop
847
+ 'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()', 'auth_session_info(text)'] loop
811
848
  execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
812
849
  execute format('revoke all on function @SCHEMA@.%s from public', f);
813
850
  end loop;
@@ -824,4 +861,4 @@ grant execute on function
824
861
  @SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
825
862
  @SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
826
863
  @SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_all(uuid), @SCHEMA@.auth_switch(uuid),
827
- @SCHEMA@.auth_tenants() to @APP@;
864
+ @SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
@@ -272,6 +272,7 @@ begin
272
272
  from g
273
273
  join (select d.class, d.tenant from (select distinct y.class, y.tenant from g y) d
274
274
  where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
275
+ order by g.id -- надгробия в порядке id, как в trg_entity_ad
275
276
  )
276
277
  select count(*)::int, coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}') into n, pairs from g;
277
278
  if n > 0 then
package/sql/90-time.sql CHANGED
@@ -622,7 +622,7 @@ end $$;
622
622
  -- последнего включённого владельца. Данные и журнал арендатора-цели стираются физически; в чужих
623
623
  -- арендаторах членства снимаются (надгробия), владение строками переходит арендаторам (§2.10);
624
624
  -- остались ссылки на аккаунт — delete_restricted. Строка Account удаляется с надгробием (история
625
- -- аккаунтов — аудит), сессии гаснут, креды удаляются.
625
+ -- аккаунтов — аудит), сессии гаснут с сигналом, строки secret аккаунта стирает каскад внешнего ключа.
626
626
  create or replace function @SCHEMA@.account_purge(p_account uuid) returns jsonb
627
627
  language plpgsql volatile security definer
628
628
  set search_path = pg_catalog, @SCHEMA@, pg_temp
@@ -658,6 +658,14 @@ begin
658
658
  if a.id is null or p_account = '@SYSTEM@'::uuid then
659
659
  perform @SCHEMA@.lt_fail('target_not_found', 'аккаунт не найден');
660
660
  end if;
661
+ -- владелец стирает арендатора, в котором работает: его членство уйдёт вместе с данными, а надгробия и
662
+ -- передача владения ниже перечитывают ctx() — остаток операции идёт в личном арендаторе вызывающего,
663
+ -- и его сессия больше не указывает на стёртого арендатора (иначе tenant_denied и откат всего вызова)
664
+ if c.tenant = p_account then
665
+ perform set_config('letopis.tenant', c.account::text, true);
666
+ update @SCHEMA@.secret s set meta = s.meta - 'tenant'
667
+ where s.id = c.session and s.kind = 'SESSION' and s.meta->>'tenant' = p_account::text;
668
+ end if;
661
669
  -- последний владелец: в каждом чужом арендаторе, где цель — владелец, должен остаться другой
662
670
  for t in select x.tenant from @SCHEMA@.entity x
663
671
  where x.class = 'member' and x.links->>'User' = p_account::text and x.tenant <> p_account
@@ -679,7 +687,7 @@ begin
679
687
  perform @SCHEMA@.tombstone(m);
680
688
  n_mem := n_mem + 1;
681
689
  end loop;
682
- for t in select distinct x.tenant from @SCHEMA@.entity x where x.owner = p_account and x.owner <> x.tenant loop
690
+ for t in select distinct x.tenant from @SCHEMA@.entity x where x.owner = p_account and x.owner <> x.tenant order by x.tenant loop
683
691
  n_own := n_own + @SCHEMA@.owner_to_tenant(t.tenant, p_account);
684
692
  end loop;
685
693
  select coalesce(jsonb_agg(jsonb_build_object('id', x.id, 'class', x.class, 'tenant', x.tenant)), '[]'::jsonb) into blockers
@@ -687,10 +695,10 @@ begin
687
695
  if blockers <> '[]'::jsonb then
688
696
  perform @SCHEMA@.lt_fail('delete_restricted', 'на аккаунт ссылаются другие строки', @SCHEMA@.visible_detail(blockers, c.tenant));
689
697
  end if;
698
+ -- сессии аккаунта гасит с сигналом триггер до удаления (trg_entity_bd_account, срабатывает и для владельца), а его
699
+ -- строки secret — сессии и креды — стирает каскад внешнего ключа secret.account (ревизия 7)
690
700
  delete from @SCHEMA@.entity x where x.id = p_account;
691
701
  perform @SCHEMA@.tombstone(a);
692
- perform @SCHEMA@.auth_kill(p_account, null);
693
- delete from @SCHEMA@.secret s where s.account = p_account;
694
702
  -- данные арендатора-цели — его парами; Account и member — классы прав, их сигнал сбрасывает всё
695
703
  perform @SCHEMA@.signal(classes || array['Account', 'member'],
696
704
  array(select p_account from unnest(classes)) || array['@SYSTEM@'::uuid, '@SYSTEM@'::uuid]);
@@ -897,7 +905,9 @@ begin
897
905
  with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
898
906
  and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
899
907
  select count(*)::int into s from d;
900
- with d as (delete from @SCHEMA@.secret x where x.kind = 'OTP'
908
+ -- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
909
+ -- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
910
+ with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
901
911
  and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
902
912
  select count(*)::int into o from d;
903
913
  return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
@@ -78,6 +78,12 @@ declare
78
78
  o_signals uuid;
79
79
  op text;
80
80
  begin
81
+ -- аккаунт System — раньше строки ключа: служебные строки secret (KEY, LOCK, PROJECTOR) принадлежат ему, а внешний
82
+ -- ключ secret.account (ревизия 7) требует строку аккаунта в entity. Счётчик прав sys_create повышает, только когда
83
+ -- строка ключа уже есть, — на свежей установке и после reset({ all }) ей неоткуда взяться, и начальный v — 0
84
+ perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
85
+ '{"name": "System", "categories": ["System"], "enabled": true}');
86
+
81
87
  -- Ключ подписи пропуска прав и счётчик изменений прав (50-acl.sql: acl_token, acl_bump); от ключа же
82
88
  -- выводится ключ сигналов. Режим сигналов (up({ signals })) хранится в meta этой строки: установщик
83
89
  -- читает его перед накатом, поэтому повторный up() без опции режим не меняет, а reset({ all }) кладёт
@@ -87,9 +93,6 @@ begin
87
93
  on conflict (id) do update set meta = k.meta || '{"signals": "@SIGNALS@"}'
88
94
  where k.meta->>'signals' is distinct from '@SIGNALS@';
89
95
 
90
- perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
91
- '{"name": "System", "categories": ["System"], "enabled": true}');
92
-
93
96
  -- Строка Class, описывающая саму себя, и системные классы. Полная метасхема и развёрнутое
94
97
  -- описание resolved — в 40-class.sql (этап 1, п. 6).
95
98
  perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Class']), 'Class', '@SYSTEM@'::uuid, '{}',
@@ -1,4 +1,4 @@
1
1
  -- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
2
2
  -- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs). С 1.4 в штампе ещё версия пакета и хэш
3
3
  -- файлов ядра (engine, sql): connect() предупреждает, если функции схемы ставила другая версия движка той же ревизии.
4
- comment on schema @SCHEMA@ is 'letopis ddl_revision=6 engine=@ENGINE_VERSION@ sql=@ENGINE_SQL@';
4
+ comment on schema @SCHEMA@ is 'letopis ddl_revision=7 engine=@ENGINE_VERSION@ sql=@ENGINE_SQL@';
@@ -0,0 +1,12 @@
1
+ -- Ревизия 7 (1.4.1-alpha.0, решение владельца): строки secret принадлежат аккаунту — строке entity. Удалённый аккаунт
2
+ -- уносит свои строки каскадом внешнего ключа: сессии, хэши паролей и ключей, секреты TOTP, внешние учётные записи. До
3
+ -- ревизии 7 удаление аккаунта лишь помечало их deleted_at, а maintain() убирает только сессии и коды OTP — остальное
4
+ -- хранилось вечно. Сначала стираются строки, чьих аккаунтов в entity уже нет, затем ставится ключ (как в 10-core.sql).
5
+ delete from @SCHEMA@.secret s where not exists (select 1 from @SCHEMA@.entity e where e.id = s.account);
6
+ do $$
7
+ begin
8
+ if not exists (select 1 from pg_constraint where conrelid = '@SCHEMA@.secret'::regclass and conname = 'secret_account_fkey') then
9
+ alter table @SCHEMA@.secret add constraint secret_account_fkey
10
+ foreign key (account) references @SCHEMA@.entity (id) on delete cascade;
11
+ end if;
12
+ end $$;