letopis 1.3.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 (56) hide show
  1. package/AGENT-CHEATSHEET.en.md +14 -6
  2. package/AGENT-CHEATSHEET.md +18 -10
  3. package/CHANGELOG.md +247 -0
  4. package/README.en.md +146 -58
  5. package/README.md +129 -51
  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 +18 -4
  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 +44 -12
  28. package/dist/project.js +90 -8
  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/sync.js +11 -5
  34. package/dist/tx.js +10 -1
  35. package/dist/types.d.ts +14 -7
  36. package/dist/types.js +13 -6
  37. package/dist/up.d.ts +65 -25
  38. package/dist/up.js +152 -40
  39. package/dist/validate.d.ts +9 -2
  40. package/dist/validate.js +19 -3
  41. package/dist/watch.d.ts +17 -5
  42. package/dist/watch.js +40 -8
  43. package/dist/write.d.ts +3 -2
  44. package/dist/write.js +29 -10
  45. package/llms.txt +1 -1
  46. package/package.json +1 -1
  47. package/sql/10-core.sql +6 -1
  48. package/sql/50-acl.sql +5 -1
  49. package/sql/60-write.sql +77 -22
  50. package/sql/80-auth.sql +39 -2
  51. package/sql/88-derived.sql +82 -1
  52. package/sql/90-time.sql +75 -38
  53. package/sql/95-seed.system.sql +6 -3
  54. package/sql/99-revision.sql +3 -2
  55. package/sql/README.md +2 -0
  56. package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/sql/90-time.sql CHANGED
@@ -80,13 +80,22 @@ begin
80
80
  return false;
81
81
  end $$;
82
82
 
83
+ -- Действующая роль сеанса, под которой вызывающий работает вне функций владельца: роль SET ROLE (и параметра запуска
84
+ -- role), иначе роль входа. Внутри функции владельца current_user — владелец, а session_user — роль входа, даже если
85
+ -- поверх неё поставлена роль приложения: привилегированный вход с ролью приложения выглядел бы администратором
86
+ -- (автоматическое ревью коммита 1734d21). Подменить её нельзя: SET ROLE проверяет членство.
87
+ create or replace function @SCHEMA@.session_role() returns name
88
+ language sql stable
89
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
90
+ return coalesce(nullif(current_setting('role'), 'none'), session_user)::name;
91
+
83
92
  -- Роль подключения — администраторская (суперпользователь или может стать владельцем): verify,
84
93
  -- reset и maintain доступны ей и без сессии (CLI, обслуживание по расписанию).
85
94
  create or replace function @SCHEMA@.admin_role() returns boolean
86
95
  language sql stable
87
96
  set search_path = pg_catalog, @SCHEMA@, pg_temp
88
- return exists (select 1 from pg_roles where rolname = session_user and rolsuper)
89
- or pg_has_role(session_user, '@OWNER_NAME@', 'SET');
97
+ return exists (select 1 from pg_roles where rolname = @SCHEMA@.session_role() and rolsuper)
98
+ or pg_has_role(@SCHEMA@.session_role(), '@OWNER_NAME@', 'SET');
90
99
 
91
100
  -- Право letopis.* актора сессии по правилам System или администраторская роль подключения.
92
101
  create or replace function @SCHEMA@.letopis_allowed(p_endpoint text) returns boolean
@@ -209,6 +218,9 @@ begin
209
218
  end if;
210
219
  perform @SCHEMA@.family_lock(cls, false);
211
220
  cls := @SCHEMA@.class_row(l.class, l.tenant);
221
+ if cls.id is null then
222
+ perform @SCHEMA@.lt_fail('invalid_class', format('класс %s удалён', l.class));
223
+ end if;
212
224
  res := cls.data->'resolved';
213
225
  if coalesce((res->>'abstract')::boolean, false) then
214
226
  perform @SCHEMA@.lt_fail('invalid_class', format('класс %s абстрактный', l.class));
@@ -256,12 +268,16 @@ end $$;
256
268
  -- все версии удаляются, а на место версии 1 встаёт запись purge с числом стёртых версий и хэшем
257
269
  -- последней. Право DELETE на класс по последнему снимку. История системных классов — аудит прав и
258
270
  -- схемы: её не стирают.
259
- -- Проектор источника ещё не прочёл версию (p_tx, p_seq) строки арендатора p_tenant класса из p_classes (ревью
260
- -- безопасности №3, №5 — purge; хвост ревью №3 — обрезка истории). Проектор касается арендатора, у которого есть его
261
- -- производный класс (derivedBy — имя проектора): брошенный проектор с удалёнными производными классами не держит.
262
- -- Курсор «tx:seq~снимок»: транзакции, видимые в снимке, учла перестройка. Чекпоинт без from (до 1.2.0) касается всех
263
- -- классов.
264
- create or replace function @SCHEMA@.projector_behind(p_tenant uuid, p_classes text[], p_tx xid8, p_seq bigint) returns boolean
271
+ -- Проектор источника ещё не прочёл одну из версий p_id арендатора p_tenant с номером до p_rev включительно (null — все
272
+ -- версии): курсор не прошёл её позицию (tx, seq), и снимок перестройки её не учёл (ревью безопасности №3, №5 — purge;
273
+ -- хвост ревью №3 — обрезка истории). Проверяется каждая версия (хвост ревью доработок после 1.2.0): у более поздней
274
+ -- версии бывает меньший xid, а снимок перестройки учитывает транзакции, зафиксированные до него, — непрочитанной может
275
+ -- оказаться и версия между самой поздней по (tx, seq) и последней. Проектор касается арендатора, у которого есть его
276
+ -- производный класс (derivedBy — имя проектора): брошенный проектор с удалёнными производными классами не держит, — и
277
+ -- строки, у которой хоть одна из этих версий — в классах его источника: уход из семейства источников сменой класса он
278
+ -- тоже читает. Курсор «tx:seq~снимок»; чекпоинт без from (до 1.2.0) касается всех классов. Записи purge, trim и reset —
279
+ -- не версии.
280
+ create or replace function @SCHEMA@.projector_unread(p_tenant uuid, p_id uuid, p_rev int) returns boolean
265
281
  language sql stable
266
282
  set search_path = pg_catalog, @SCHEMA@, pg_temp
267
283
  return exists (select 1 from @SCHEMA@.secret s
@@ -272,11 +288,18 @@ return exists (select 1 from @SCHEMA@.secret s
272
288
  and exists (select 1 from @SCHEMA@.entity x where x.class = 'Class' and x.tenant = p_tenant
273
289
  and x.data->'resolved'->>'derivedBy' = s.ident)
274
290
  and (not s.meta ? 'from'
275
- or exists (select 1 from unnest(p_classes) c
276
- where coalesce((@SCHEMA@.class_row(c, p_tenant)).data->'resolved'->'ancestors', jsonb_build_array(c))
291
+ or exists (select 1 from (select distinct v.class from @SCHEMA@.log v
292
+ where v.id = p_id and (p_rev is null or v.rev <= p_rev)
293
+ and v.op not in ('purge', 'trim', 'reset')) c
294
+ where coalesce((@SCHEMA@.class_row(c.class, p_tenant)).data->'resolved'->'ancestors', jsonb_build_array(c.class))
277
295
  ?| array(select jsonb_array_elements_text(s.meta->'from'))))
278
- and (p_tx, p_seq) > (split_part(k.head, ':', 1)::xid8, split_part(k.head, ':', 2)::bigint)
279
- and not coalesce(pg_visible_in_snapshot(p_tx, k.snap::pg_snapshot), false));
296
+ and exists (select 1 from @SCHEMA@.log v
297
+ where v.id = p_id and (p_rev is null or v.rev <= p_rev) and v.op not in ('purge', 'trim', 'reset')
298
+ and (v.tx, v.seq) > (split_part(k.head, ':', 1)::xid8, split_part(k.head, ':', 2)::bigint)
299
+ and not coalesce(pg_visible_in_snapshot(v.tx, k.snap::pg_snapshot), false)));
300
+
301
+ -- до 1.4 проверялась одна позиция: надгробие у purge, самая поздняя по (tx, seq) версия у стыка обрезки
302
+ drop function if exists @SCHEMA@.projector_behind(uuid, text[], xid8, bigint);
280
303
 
281
304
  create or replace function @SCHEMA@.purge(p_id uuid, p_confirm boolean default false) returns jsonb
282
305
  language plpgsql volatile security definer
@@ -301,9 +324,9 @@ begin
301
324
  if not @SCHEMA@.acl_row_ok(@SCHEMA@.acl_plan('DELETE'), l.class, l.id, l.owner, l.tenant, l.tags, l.data, l.links) then
302
325
  perform @SCHEMA@.lt_fail('acl_denied', format('нет права DELETE на класс %s', l.class));
303
326
  end if;
304
- -- проектор источника, чей курсор ещё не прошёл надгробие (в том числе надгробие этой транзакции), об удалении не
305
- -- узнал бы — вклад строки остался бы в итогах навсегда (ревью безопасности №3, №5)
306
- if @SCHEMA@.projector_behind(l.tenant, array[l.class], l.tx, l.seq) then
327
+ -- проектор источника, который ещё не прочёл какую-то версию строки (в том числе надгробие этой транзакции), об
328
+ -- удалении не узнал бы — вклад строки остался бы в итогах навсегда (ревью безопасности №3, №5)
329
+ if @SCHEMA@.projector_unread(l.tenant, p_id, null) then
307
330
  perform @SCHEMA@.lt_fail('projector_pending', 'проектор источника ещё не прочёл удаление: purge — после его run()');
308
331
  end if;
309
332
  select count(*)::int into n from @SCHEMA@.log x where x.id = p_id;
@@ -340,7 +363,6 @@ declare
340
363
  r @SCHEMA@.log;
341
364
  first @SCHEMA@.log;
342
365
  nxt record;
343
- late record;
344
366
  j int;
345
367
  h bytea;
346
368
  k int;
@@ -385,15 +407,11 @@ begin
385
407
  select x.* into first from @SCHEMA@.log x where x.id = r.id order by x.rev limit 1;
386
408
  -- до момента — только сама запись trim: стирать нечего
387
409
  continue when j = 1 and first.op = 'trim';
388
- -- проектор источника, чей курсор ещё не прошёл первую оставшуюся версию: читая её, он возьмёт прошлую версию для
389
- -- before, а её уже не будет — свёртка прибавила бы новую, не вычитая прежнюю (хвост ревью безопасности №3). Класс —
390
- -- обеих версий у стыка: строка могла уйти из семейства источников сменой класса
391
- select x.rev, x.class into nxt from @SCHEMA@.log x where x.id = r.id and x.rev > j order by x.rev limit 1;
392
- -- позиция — самая поздняя по (tx, seq) среди стираемых версий и первой оставшейся: у более поздней версии бывает
393
- -- меньший xid (автоматическое ревью)
394
- select x.tx, x.seq into late from @SCHEMA@.log x where x.id = r.id and x.rev <= nxt.rev order by x.tx desc, x.seq desc limit 1;
395
- if @SCHEMA@.projector_behind(r.tenant, array[(select x.class from @SCHEMA@.log x where x.id = r.id and x.rev = j), nxt.class],
396
- late.tx, late.seq) then
410
+ -- проектор источника, который ещё не прочёл стираемые версии или первую оставшуюся: читая её, он возьмёт прошлую
411
+ -- версию для before, а её уже не будет — свёртка прибавила бы новую, не вычитая прежнюю (хвост ревью безопасности
412
+ -- №3); каждая версия до первой оставшейся — у более поздней версии бывает меньший xid
413
+ select x.rev into nxt from @SCHEMA@.log x where x.id = r.id and x.rev > j order by x.rev limit 1;
414
+ if @SCHEMA@.projector_unread(r.tenant, r.id, nxt.rev) then
397
415
  perform @SCHEMA@.lt_fail('projector_pending', 'проектор источника ещё не прочёл версии: trimHistory — после его run()');
398
416
  end if;
399
417
  select x.hash into h from @SCHEMA@.log x where x.id = r.id and x.rev = j;
@@ -456,6 +474,9 @@ begin
456
474
  cls := @SCHEMA@.class_row(o.class, o.tenant);
457
475
  perform @SCHEMA@.family_lock(cls, false);
458
476
  cls := @SCHEMA@.class_row(o.class, o.tenant);
477
+ if cls.id is null then
478
+ perform @SCHEMA@.lt_fail('invalid_class', format('класс %s удалён', o.class));
479
+ end if;
459
480
  res := cls.data->'resolved';
460
481
  if res ? 'derivedBy' then
461
482
  perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', o.class, res->>'derivedBy'));
@@ -601,7 +622,7 @@ end $$;
601
622
  -- последнего включённого владельца. Данные и журнал арендатора-цели стираются физически; в чужих
602
623
  -- арендаторах членства снимаются (надгробия), владение строками переходит арендаторам (§2.10);
603
624
  -- остались ссылки на аккаунт — delete_restricted. Строка Account удаляется с надгробием (история
604
- -- аккаунтов — аудит), сессии гаснут, креды удаляются.
625
+ -- аккаунтов — аудит), сессии гаснут с сигналом, строки secret аккаунта стирает каскад внешнего ключа.
605
626
  create or replace function @SCHEMA@.account_purge(p_account uuid) returns jsonb
606
627
  language plpgsql volatile security definer
607
628
  set search_path = pg_catalog, @SCHEMA@, pg_temp
@@ -637,6 +658,14 @@ begin
637
658
  if a.id is null or p_account = '@SYSTEM@'::uuid then
638
659
  perform @SCHEMA@.lt_fail('target_not_found', 'аккаунт не найден');
639
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;
640
669
  -- последний владелец: в каждом чужом арендаторе, где цель — владелец, должен остаться другой
641
670
  for t in select x.tenant from @SCHEMA@.entity x
642
671
  where x.class = 'member' and x.links->>'User' = p_account::text and x.tenant <> p_account
@@ -658,7 +687,7 @@ begin
658
687
  perform @SCHEMA@.tombstone(m);
659
688
  n_mem := n_mem + 1;
660
689
  end loop;
661
- 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
662
691
  n_own := n_own + @SCHEMA@.owner_to_tenant(t.tenant, p_account);
663
692
  end loop;
664
693
  select coalesce(jsonb_agg(jsonb_build_object('id', x.id, 'class', x.class, 'tenant', x.tenant)), '[]'::jsonb) into blockers
@@ -666,10 +695,10 @@ begin
666
695
  if blockers <> '[]'::jsonb then
667
696
  perform @SCHEMA@.lt_fail('delete_restricted', 'на аккаунт ссылаются другие строки', @SCHEMA@.visible_detail(blockers, c.tenant));
668
697
  end if;
698
+ -- сессии аккаунта гасит с сигналом триггер до удаления (trg_entity_bd_account, срабатывает и для владельца), а его
699
+ -- строки secret — сессии и креды — стирает каскад внешнего ключа secret.account (ревизия 7)
669
700
  delete from @SCHEMA@.entity x where x.id = p_account;
670
701
  perform @SCHEMA@.tombstone(a);
671
- perform @SCHEMA@.auth_kill(p_account, null);
672
- delete from @SCHEMA@.secret s where s.account = p_account;
673
702
  -- данные арендатора-цели — его парами; Account и member — классы прав, их сигнал сбрасывает всё
674
703
  perform @SCHEMA@.signal(classes || array['Account', 'member'],
675
704
  array(select p_account from unnest(classes)) || array['@SYSTEM@'::uuid, '@SYSTEM@'::uuid]);
@@ -876,7 +905,9 @@ begin
876
905
  with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
877
906
  and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
878
907
  select count(*)::int into s from d;
879
- 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})?$'
880
911
  and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
881
912
  select count(*)::int into o from d;
882
913
  return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
@@ -1061,7 +1092,10 @@ end $$;
1061
1092
  -- окна p_upto — последняя строка полной пачки watch_read), а строки — синтетические надгробия прежней версии: класс,
1062
1093
  -- концы и данные прежние; номер, время, автор, tx и seq — версии смены класса; вид delete; агента и причины нет. Только
1063
1094
  -- если нового класса нет в p_classes, а прежнюю версию актор видит (row_visible с его планом READ): о строке, которой он
1064
- -- не читал, подписчик не узнает. Функция владельца: версию с новым классом актору видеть не нужно.
1095
+ -- не читал, подписчик не узнает. Функция владельца: версию с новым классом актору видеть не нужно. Подключение в обход
1096
+ -- RLS (суперпользователь, BYPASSRLS, член роли-владельца с наследованием — connect() пускает его только с
1097
+ -- allowBypassRls) видит в watch_read журнал всех арендаторов, поэтому и события ухода получает все, с автором (хвост
1098
+ -- ревью доработок после 1.2.0). Роль — действующая роль сеанса (session_role): та, к которой watch_read применяет RLS.
1065
1099
  create or replace function @SCHEMA@.watch_leaving(p_tx xid8, p_seq bigint, p_classes text[], p_limit int,
1066
1100
  p_snap pg_snapshot default null, p_upto_tx xid8 default null, p_upto_seq bigint default null)
1067
1101
  returns setof @SCHEMA@.log
@@ -1071,12 +1105,15 @@ as $$
1071
1105
  declare
1072
1106
  horizon xid8 := pg_snapshot_xmin(pg_current_snapshot());
1073
1107
  a jsonb;
1108
+ bypass boolean;
1074
1109
  begin
1075
1110
  if p_classes is null then
1076
1111
  return;
1077
1112
  end if;
1078
- a := @SCHEMA@.acl_actor('READ');
1079
- if a is null then
1113
+ bypass := exists (select 1 from pg_roles r where r.rolname = @SCHEMA@.session_role() and (r.rolsuper or r.rolbypassrls))
1114
+ or pg_has_role(@SCHEMA@.session_role(), '@OWNER_NAME@', 'USAGE');
1115
+ a := case when not bypass then @SCHEMA@.acl_actor('READ') end;
1116
+ if a is null and not bypass then
1080
1117
  return;
1081
1118
  end if;
1082
1119
  -- новую версию актор не видит — надгробие при любом p_classes и без автора: перебором классов подписки он узнал бы её
@@ -1086,15 +1123,15 @@ begin
1086
1123
  from @SCHEMA@.log l
1087
1124
  cross join lateral (select x.* from @SCHEMA@.log x where x.id = l.id and x.rev < l.rev and x.op not in ('purge', 'trim', 'reset')
1088
1125
  order by x.rev desc limit 1) p
1089
- cross join lateral (select @SCHEMA@.row_visible(l.tenant, l.class, l.id, l.owner, l.tags, l.data, l.links,
1126
+ cross join lateral (select bypass or @SCHEMA@.row_visible(l.tenant, l.class, l.id, l.owner, l.tags, l.data, l.links,
1090
1127
  (a->>'tenant')::uuid, (a->>'account')::uuid, a->'plan') as seen) v
1091
1128
  where (l.tx, l.seq) > (p_tx, p_seq) and l.tx < horizon
1092
1129
  and (p_upto_tx is null or (l.tx, l.seq) <= (p_upto_tx, p_upto_seq))
1093
1130
  and l.op = 'reclass' and l.moved is null and (not (l.class = any (p_classes)) or not v.seen)
1094
1131
  and p.class = any (p_classes) and p.op <> 'delete'
1095
1132
  and (p_snap is null or not pg_visible_in_snapshot(l.tx, p_snap))
1096
- and @SCHEMA@.row_visible(p.tenant, p.class, p.id, p.owner, p.tags, p.data, p.links,
1097
- (a->>'tenant')::uuid, (a->>'account')::uuid, a->'plan')
1133
+ and (bypass or @SCHEMA@.row_visible(p.tenant, p.class, p.id, p.owner, p.tags, p.data, p.links,
1134
+ (a->>'tenant')::uuid, (a->>'account')::uuid, a->'plan'))
1098
1135
  order by l.tx, l.seq limit p_limit;
1099
1136
  end $$;
1100
1137
 
@@ -1116,8 +1153,8 @@ declare f text;
1116
1153
  begin
1117
1154
  foreach f in array array[
1118
1155
  'history_default()', 'history_policy(jsonb)', 'history_iv(text)', 'history_horizon(jsonb)',
1119
- 'history_keep(jsonb, timestamp, timestamp, timestamp)', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
1120
- 'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_behind(uuid, text[], xid8, bigint)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
1156
+ 'history_keep(jsonb, timestamp, timestamp, timestamp)', 'session_role()', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
1157
+ 'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_unread(uuid, uuid, int)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
1121
1158
  'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
1122
1159
  'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
1123
1160
  'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint)', 'watch_head()'] loop
@@ -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,3 +1,4 @@
1
1
  -- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
2
- -- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs).
3
- comment on schema @SCHEMA@ is 'letopis ddl_revision=6';
2
+ -- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs). С 1.4 в штампе ещё версия пакета и хэш
3
+ -- файлов ядра (engine, sql): connect() предупреждает, если функции схемы ставила другая версия движка той же ревизии.
4
+ comment on schema @SCHEMA@ is 'letopis ddl_revision=7 engine=@ENGINE_VERSION@ sql=@ENGINE_SQL@';
package/sql/README.md CHANGED
@@ -17,6 +17,8 @@
17
17
  | `@HISTORY_DEFAULT@` | политика хранения по умолчанию `up({ history })` литералом JSON или `'null'` — константа установки |
18
18
  | `@RESERVED_NAMES@` | методы цепочки — `RESERVED_CLASS_NAMES` из `lib/src/types.ts`, литералами через запятую |
19
19
  | `@SIGNALS@` | режим сигналов `class`, `tenant`, `opaque` или `none` (план 1.1, §2.2): опция `up({ signals })`, иначе сохранённый в `meta.signals` строки ключа `secret`, иначе `class` — константа установки |
20
+ | `@ENGINE_VERSION@` | версия пакета (`lib/package.json`) — штамп движка в комментарии схемы (`99-revision.sql`) |
21
+ | `@ENGINE_SQL@` | 16 знаков sha256 исходного текста файлов `NN-*.sql` до подстановок (переводы строк — LF): `connect()` сверяет его с файлами своего пакета |
20
22
 
21
23
  ## Колонки снимка версии
22
24
 
@@ -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 $$;