letopis 1.4.0 → 1.4.2

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 (62) hide show
  1. package/AGENT-CHEATSHEET.en.md +15 -7
  2. package/AGENT-CHEATSHEET.md +19 -11
  3. package/CHANGELOG.md +299 -1
  4. package/README.en.md +180 -62
  5. package/README.md +165 -55
  6. package/dist/acl.d.ts +11 -5
  7. package/dist/admin.d.ts +30 -14
  8. package/dist/admin.js +53 -18
  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/cache.js +3 -2
  13. package/dist/chain.d.ts +92 -15
  14. package/dist/chain.js +56 -7
  15. package/dist/cli.js +16 -2
  16. package/dist/errors.d.ts +2 -2
  17. package/dist/errors.js +5 -5
  18. package/dist/import.js +7 -0
  19. package/dist/index.d.ts +73 -45
  20. package/dist/index.js +69 -14
  21. package/dist/internal.d.ts +7 -0
  22. package/dist/internal.js +5 -0
  23. package/dist/load.d.ts +10 -3
  24. package/dist/load.js +39 -11
  25. package/dist/mcp.d.ts +20 -11
  26. package/dist/mcp.js +8 -6
  27. package/dist/model.d.ts +4 -1
  28. package/dist/model.js +7 -1
  29. package/dist/ops.d.ts +58 -18
  30. package/dist/ops.js +58 -18
  31. package/dist/project.d.ts +39 -11
  32. package/dist/project.js +75 -15
  33. package/dist/registry.d.ts +4 -1
  34. package/dist/registry.js +4 -1
  35. package/dist/signals.d.ts +4 -4
  36. package/dist/signals.js +4 -4
  37. package/dist/sql.d.ts +8 -4
  38. package/dist/sql.js +110 -25
  39. package/dist/sync.js +11 -4
  40. package/dist/tx.d.ts +15 -0
  41. package/dist/tx.js +22 -4
  42. package/dist/types.d.ts +21 -7
  43. package/dist/types.js +13 -6
  44. package/dist/up.d.ts +48 -25
  45. package/dist/up.js +94 -41
  46. package/dist/validate.d.ts +9 -2
  47. package/dist/validate.js +19 -3
  48. package/dist/watch.d.ts +29 -5
  49. package/dist/watch.js +62 -13
  50. package/dist/write.d.ts +3 -2
  51. package/dist/write.js +52 -19
  52. package/package.json +8 -4
  53. package/sql/10-core.sql +6 -1
  54. package/sql/60-write.sql +47 -18
  55. package/sql/70-read.sql +13 -7
  56. package/sql/75-history.sql +106 -28
  57. package/sql/80-auth.sql +41 -3
  58. package/sql/88-derived.sql +3 -1
  59. package/sql/90-time.sql +118 -30
  60. package/sql/95-seed.system.sql +6 -3
  61. package/sql/99-revision.sql +1 -1
  62. package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/sql/90-time.sql CHANGED
@@ -389,9 +389,12 @@ begin
389
389
  end if;
390
390
  end loop;
391
391
  end if;
392
+ -- по классам: последняя версия id — любого класса, класс проверяется у неё (план 2.0, 7.1 №1)
392
393
  for r in select distinct on (l.id) l.* from @SCHEMA@.log l
393
- where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids) else l.class = any (p_classes) end)
394
+ where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids)
395
+ else l.id in (select x.id from @SCHEMA@.log x where x.tenant = c.tenant and x.class = any (p_classes)) end)
394
396
  order by l.id, l.rev desc loop
397
+ continue when p_ids is null and not (r.class = any (p_classes));
395
398
  continue when r.op in ('purge', 'reset')
396
399
  or not @SCHEMA@.row_visible(r.tenant, r.class, r.id, r.owner, r.tags, r.data, r.links, c.tenant, c.account, rd->'plan');
397
400
  if r.class in ('Class', 'Account', 'member', 'Resource', 'rule') then
@@ -622,7 +625,7 @@ end $$;
622
625
  -- последнего включённого владельца. Данные и журнал арендатора-цели стираются физически; в чужих
623
626
  -- арендаторах членства снимаются (надгробия), владение строками переходит арендаторам (§2.10);
624
627
  -- остались ссылки на аккаунт — delete_restricted. Строка Account удаляется с надгробием (история
625
- -- аккаунтов — аудит), сессии гаснут, креды удаляются.
628
+ -- аккаунтов — аудит), сессии гаснут с сигналом, строки secret аккаунта стирает каскад внешнего ключа.
626
629
  create or replace function @SCHEMA@.account_purge(p_account uuid) returns jsonb
627
630
  language plpgsql volatile security definer
628
631
  set search_path = pg_catalog, @SCHEMA@, pg_temp
@@ -658,6 +661,14 @@ begin
658
661
  if a.id is null or p_account = '@SYSTEM@'::uuid then
659
662
  perform @SCHEMA@.lt_fail('target_not_found', 'аккаунт не найден');
660
663
  end if;
664
+ -- владелец стирает арендатора, в котором работает: его членство уйдёт вместе с данными, а надгробия и
665
+ -- передача владения ниже перечитывают ctx() — остаток операции идёт в личном арендаторе вызывающего,
666
+ -- и его сессия больше не указывает на стёртого арендатора (иначе tenant_denied и откат всего вызова)
667
+ if c.tenant = p_account then
668
+ perform set_config('letopis.tenant', c.account::text, true);
669
+ update @SCHEMA@.secret s set meta = s.meta - 'tenant'
670
+ where s.id = c.session and s.kind = 'SESSION' and s.meta->>'tenant' = p_account::text;
671
+ end if;
661
672
  -- последний владелец: в каждом чужом арендаторе, где цель — владелец, должен остаться другой
662
673
  for t in select x.tenant from @SCHEMA@.entity x
663
674
  where x.class = 'member' and x.links->>'User' = p_account::text and x.tenant <> p_account
@@ -679,7 +690,7 @@ begin
679
690
  perform @SCHEMA@.tombstone(m);
680
691
  n_mem := n_mem + 1;
681
692
  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
693
+ 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
694
  n_own := n_own + @SCHEMA@.owner_to_tenant(t.tenant, p_account);
684
695
  end loop;
685
696
  select coalesce(jsonb_agg(jsonb_build_object('id', x.id, 'class', x.class, 'tenant', x.tenant)), '[]'::jsonb) into blockers
@@ -687,10 +698,10 @@ begin
687
698
  if blockers <> '[]'::jsonb then
688
699
  perform @SCHEMA@.lt_fail('delete_restricted', 'на аккаунт ссылаются другие строки', @SCHEMA@.visible_detail(blockers, c.tenant));
689
700
  end if;
701
+ -- сессии аккаунта гасит с сигналом триггер до удаления (trg_entity_bd_account, срабатывает и для владельца), а его
702
+ -- строки secret — сессии и креды — стирает каскад внешнего ключа secret.account (ревизия 7)
690
703
  delete from @SCHEMA@.entity x where x.id = p_account;
691
704
  perform @SCHEMA@.tombstone(a);
692
- perform @SCHEMA@.auth_kill(p_account, null);
693
- delete from @SCHEMA@.secret s where s.account = p_account;
694
705
  -- данные арендатора-цели — его парами; Account и member — классы прав, их сигнал сбрасывает всё
695
706
  perform @SCHEMA@.signal(classes || array['Account', 'member'],
696
707
  array(select p_account from unnest(classes)) || array['@SYSTEM@'::uuid, '@SYSTEM@'::uuid]);
@@ -789,7 +800,9 @@ end $$;
789
800
  -- ========================= обслуживание =========================
790
801
 
791
802
  -- Одна пачка прореживания журнала (§2.11): версии, которые не нужны действующей политике хранения
792
- -- своего класса. Не трогаются версия 1, текущая и версии особых видов (create, delete, reclass,
803
+ -- своего класса — как его резолвит class_row: свой класс арендатора, иначе System; у арендатора со своим одноимённым
804
+ -- классом политика класса System не действует (план 2.0, 7.3 №1: класс System, заведённый позже, дублировал строки
805
+ -- журнала в отборе и стирал промежуточные версии). Не трогаются версия 1, текущая и версии особых видов (create, delete, reclass,
793
806
  -- rekey, restore, purge, reset, trim); история удалённого объекта, надгробие которого старше
794
807
  -- последнего яруса со сроком, стирается целиком. Пачка — до 100 объектов после курсора (meta
795
808
  -- строки ключа: участки по порядку id), не больше p_limit версий; объект, не вошедший целиком,
@@ -819,32 +832,60 @@ begin
819
832
  )
820
833
  select array_agg(q.id order by q.id) into ids from (
821
834
  select distinct l.id from @SCHEMA@.log l
822
- join pol on pol.p is not null and pol.cls = l.class and (pol.ct = l.tenant or pol.ct = '@SYSTEM@'::uuid)
835
+ join pol on pol.p is not null and pol.cls = l.class
836
+ and (pol.ct = l.tenant
837
+ or pol.ct = '@SYSTEM@'::uuid
838
+ and not exists (select 1 from @SCHEMA@.entity y
839
+ where y.class = 'Class' and y.tenant = l.tenant and y.data->>'name' = l.class))
823
840
  where l.at < p_now - @SCHEMA@.history_iv(pol.p->>'all') and (cur is null or l.id > cur)
824
841
  order by l.id limit 100) q;
825
842
  if ids is null then
826
843
  update @SCHEMA@.secret s set meta = s.meta - 'thin' where s.id = '@SYSTEM@'::uuid;
827
844
  return jsonb_build_object('thinned', 0, 'more', false);
828
845
  end if;
846
+ -- последняя запись каждого id пачки — for update: restore(), purge() и новая версия того же id берут её же (инвариант
847
+ -- M-TIME); отбор ниже видит то, что они успели зафиксировать, и не стирает историю объекта, который только что
848
+ -- восстановили (план 2.0, 7.3 №3)
849
+ perform 1 from @SCHEMA@.log l
850
+ where l.id = any (ids) and not exists (select 1 from @SCHEMA@.log z where z.id = l.id and z.rev > l.rev)
851
+ order by l.id
852
+ for update of l;
829
853
  with pol as (
830
854
  select x.data->>'name' as cls, x.tenant as ct, @SCHEMA@.history_policy(x.data->'resolved') as p
831
855
  from @SCHEMA@.entity x where x.class = 'Class'
832
856
  ), v as (
833
- select l.id, l.rev, l.at, l.op, pol.p, coalesce(pol.p->>'tz', 'UTC') as tz,
857
+ -- все версии id, любого класса: последняя версия и следующая по времени — по всей цепочке (план 2.0, 7.3 №2: после
858
+ -- reclass версии прежнего класса без политики выпадали из отбора и переживали стирание истории удалённого)
859
+ select l.id, l.rev, l.at, l.op, l.class, l.tenant,
834
860
  max(l.rev) over (partition by l.id) as last_rev,
835
861
  first_value(l.op) over (partition by l.id order by l.rev desc) as last_op,
836
862
  first_value(l.at) over (partition by l.id order by l.rev desc) as last_at,
863
+ first_value(l.class) over (partition by l.id order by l.rev desc) as last_class,
837
864
  lead(l.at) over (partition by l.id order by l.rev) as next_at
838
865
  from @SCHEMA@.log l
839
- join pol on pol.p is not null and pol.cls = l.class and (pol.ct = l.tenant or pol.ct = '@SYSTEM@'::uuid)
840
866
  where l.id = any (ids)
867
+ ), vp as (
868
+ -- прореживание версии — по политике её класса; стирание удалённого целиком — по политике класса надгробия
869
+ select v.*, pv.p, coalesce(pv.p->>'tz', 'UTC') as tz, pl.p as lp, coalesce(pl.p->>'tz', 'UTC') as ltz
870
+ from v
871
+ left join pol pv on pv.cls = v.class
872
+ and (pv.ct = v.tenant
873
+ or pv.ct = '@SYSTEM@'::uuid
874
+ and not exists (select 1 from @SCHEMA@.entity y
875
+ where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.class))
876
+ left join pol pl on pl.cls = v.last_class
877
+ and (pl.ct = v.tenant
878
+ or pl.ct = '@SYSTEM@'::uuid
879
+ and not exists (select 1 from @SCHEMA@.entity y
880
+ where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.last_class))
841
881
  )
842
- select array_agg(v.id order by v.id, v.rev), array_agg(v.rev order by v.id, v.rev) into vid, vrev
843
- from v
844
- where (v.last_op = 'delete' and timezone(v.tz, v.last_at) <= timezone(v.tz, p_now) - @SCHEMA@.history_horizon(v.p))
845
- or (v.rev <> 1 and v.rev <> v.last_rev
846
- and v.op not in ('create', 'delete', 'reclass', 'rekey', 'restore', 'purge', 'reset', 'trim')
847
- and not @SCHEMA@.history_keep(v.p, timezone(v.tz, v.at), timezone(v.tz, p_now), timezone(v.tz, v.next_at)));
882
+ select array_agg(vp.id order by vp.id, vp.rev), array_agg(vp.rev order by vp.id, vp.rev) into vid, vrev
883
+ from vp
884
+ where (vp.last_op = 'delete' and vp.lp is not null
885
+ and timezone(vp.ltz, vp.last_at) <= timezone(vp.ltz, p_now) - @SCHEMA@.history_horizon(vp.lp))
886
+ or (vp.p is not null and vp.rev <> 1 and vp.rev <> vp.last_rev
887
+ and vp.op not in ('create', 'delete', 'reclass', 'rekey', 'restore', 'purge', 'reset', 'trim')
888
+ and not @SCHEMA@.history_keep(vp.p, timezone(vp.tz, vp.at), timezone(vp.tz, p_now), timezone(vp.tz, vp.next_at)));
848
889
  total := coalesce(cardinality(vid), 0);
849
890
  k := least(total, p_limit);
850
891
  if k > 0 then
@@ -867,11 +908,15 @@ begin
867
908
  return jsonb_build_object('thinned', k, 'more', more);
868
909
  end $$;
869
910
 
870
- -- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, одна пачка
871
- -- прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
911
+ -- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, строки-замки удалённых семейств, одна
912
+ -- пачка прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
872
913
  -- правом letopis.maintain или администраторское подключение. p_now — момент «сейчас» для
873
- -- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю.
874
- create or replace function @SCHEMA@.maintain(p_now timestamptz default null, p_limit int default 10000) returns jsonb
914
+ -- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю. p_cleanup —
915
+ -- удалять ли истёкшие сессии, коды и строки-замки: библиотека передаёт true только в первой пачке вызова, иначе каждая
916
+ -- из ≤100 пачек заново просматривала бы secret (план 2.0, 7.3 №4); прежний вызов без третьего аргумента чистит, как раньше.
917
+ drop function if exists @SCHEMA@.maintain(timestamptz, int);
918
+ create or replace function @SCHEMA@.maintain(p_now timestamptz default null, p_limit int default 10000,
919
+ p_cleanup boolean default true) returns jsonb
875
920
  language plpgsql volatile security definer
876
921
  set search_path = pg_catalog, @SCHEMA@, pg_temp
877
922
  as $$
@@ -879,6 +924,7 @@ declare
879
924
  ts timestamptz := clock_timestamp();
880
925
  s int;
881
926
  o int;
927
+ fam text := '@SCHEMA_NAME@:family:';
882
928
  begin
883
929
  if not @SCHEMA@.letopis_allowed('letopis.maintain') then
884
930
  perform @SCHEMA@.lt_fail('acl_denied', 'нет права letopis.maintain');
@@ -894,12 +940,41 @@ begin
894
940
  end if;
895
941
  ts := p_now;
896
942
  end if;
897
- with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
898
- and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
899
- select count(*)::int into s from d;
900
- with d as (delete from @SCHEMA@.secret x where x.kind = 'OTP'
901
- and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
902
- select count(*)::int into o from d;
943
+ s := 0;
944
+ o := 0;
945
+ if coalesce(p_cleanup, true) then
946
+ with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
947
+ and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
948
+ select count(*)::int into s from d;
949
+ -- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
950
+ -- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
951
+ with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
952
+ and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
953
+ select count(*)::int into o from d;
954
+ -- строки-замки семейств, чьего корня больше нет — удалён класс или его арендатор (план 2.0, 7.7 №8, долгий
955
+ -- прогон): их заранее заводит запись строки Class (family_rows), а сами они не уходили — заведение и удаление
956
+ -- классов копило их без предела. Корень жив — строка Class с этим именем у арендатора семейства (family_name;
957
+ -- в имени класса двоеточия нет). Занятую строку не трогаем (skip locked), а строку-замок семейства — только вместе
958
+ -- с её шлюзом или без шлюза: исключительная блокировка берёт шлюз первым, и строка-замок, до которой она ещё не
959
+ -- дошла, у неё не пропадёт. Пропавшую строку lock_row и family_lock заводят заново, если класс с этим именем
960
+ -- появится снова.
961
+ with stale as (
962
+ select l.id, l.meta->>'name' as n
963
+ from @SCHEMA@.secret l
964
+ where l.kind = 'LOCK' and l.account = '@SYSTEM@'::uuid and l.deleted_at is null
965
+ and left(l.meta->>'name', length(fam)) = fam
966
+ and not exists (
967
+ select 1 from @SCHEMA@.entity c
968
+ where c.class = 'Class'
969
+ and c.data->>'name' = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 2)
970
+ and c.tenant::text = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 1))
971
+ for update of l skip locked)
972
+ delete from @SCHEMA@.secret x using stale st
973
+ where x.id = st.id
974
+ and (st.n like '%:gate'
975
+ or exists (select 1 from stale g where g.n = st.n || ':gate')
976
+ or not exists (select 1 from @SCHEMA@.secret y where y.id = @SCHEMA@.lock_id(st.n || ':gate')));
977
+ end if;
903
978
  return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
904
979
  || @SCHEMA@.thin_batch(ts, greatest(coalesce(p_limit, 10000), 1));
905
980
  end $$;
@@ -959,8 +1034,12 @@ begin
959
1034
  select jsonb_build_object('id', id, 'rev', want_rev, 'problem', 'разрыв: нет версий ' || want_rev || '…' || (rev - 1))
960
1035
  from exp
961
1036
  where rev > want_rev
962
- and not exists (select 1 from pol where pol.cls = exp.class and (pol.ct = exp.tenant or pol.ct = '@SYSTEM@'::uuid)
963
- and exp.prev_at < ts - pol.all_iv)
1037
+ and not exists (select 1 from pol where pol.cls = exp.class and exp.prev_at < ts - pol.all_iv
1038
+ and (pol.ct = exp.tenant
1039
+ or pol.ct = '@SYSTEM@'::uuid
1040
+ and not exists (select 1 from @SCHEMA@.entity y
1041
+ where y.class = 'Class' and y.tenant = exp.tenant
1042
+ and y.data->>'name' = exp.class)))
964
1043
  union all
965
1044
  select jsonb_build_object('id', id, 'rev', rev, 'problem', 'номер версии меньше ожидаемого после обрезки')
966
1045
  from exp where rev < want_rev
@@ -1054,11 +1133,20 @@ as $$
1054
1133
  declare
1055
1134
  horizon xid8 := pg_snapshot_xmin(pg_current_snapshot());
1056
1135
  lim interval;
1136
+ t uuid := @SCHEMA@.ctx_tenant();
1057
1137
  begin
1058
1138
  if p_at is not null then
1139
+ -- ярус all классов, которые видит подписка: арендатора сессии и System (кроме одноимённых своих); политика класса
1140
+ -- чужого арендатора с тем же именем курсор не просрочивает (план 2.0, 7.3 №1). Без арендатора в контексте
1141
+ -- (администраторское подключение видит всех) — все классы
1059
1142
  select min(@SCHEMA@.history_iv(p.p->>'all')) into lim
1060
1143
  from @SCHEMA@.entity x cross join lateral (select @SCHEMA@.history_policy(x.data->'resolved') as p) p
1061
- where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes));
1144
+ where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes))
1145
+ and (t is null
1146
+ or x.tenant = t
1147
+ or x.tenant = '@SYSTEM@'::uuid
1148
+ and not exists (select 1 from @SCHEMA@.entity y
1149
+ where y.class = 'Class' and y.tenant = t and y.data->>'name' = x.data->>'name'));
1062
1150
  if lim is not null and p_at < clock_timestamp() - lim then
1063
1151
  raise exception using errcode = 'LT016',
1064
1152
  message = 'letopis: cursor_expired: курсор подписки старше яруса all — события могли проредиться, перечитайте текущее состояние';
@@ -1146,7 +1234,7 @@ begin
1146
1234
  'history_keep(jsonb, timestamp, timestamp, timestamp)', 'session_role()', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
1147
1235
  'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_unread(uuid, uuid, int)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
1148
1236
  'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
1149
- 'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
1237
+ 'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int, boolean)', 'verify(boolean)',
1150
1238
  'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint)', 'watch_head()'] loop
1151
1239
  execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
1152
1240
  execute format('revoke all on function @SCHEMA@.%s from public', f);
@@ -1155,7 +1243,7 @@ end $$;
1155
1243
  -- приложению: функции владельца этапа 6 (проверяют права сами), подписка (под RLS) и чистые помощники политики
1156
1244
  grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean), @SCHEMA@.trim_history(uuid[], text[], timestamptz),
1157
1245
  @SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
1158
- @SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
1246
+ @SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int, boolean), @SCHEMA@.verify(boolean),
1159
1247
  @SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint), @SCHEMA@.watch_head(),
1160
1248
  @SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
1161
1249
  @SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;
@@ -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 $$;