letopis 1.4.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/AGENT-CHEATSHEET.md +516 -398
  2. package/CHANGELOG.md +739 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +1312 -1190
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +81 -1
  8. package/dist/acl.js +281 -13
  9. package/dist/admin.d.ts +11 -141
  10. package/dist/admin.js +28 -186
  11. package/dist/auth.d.ts +20 -5
  12. package/dist/auth.js +80 -21
  13. package/dist/cache.d.ts +8 -1
  14. package/dist/cache.js +30 -8
  15. package/dist/chain.d.ts +46 -48
  16. package/dist/chain.js +556 -223
  17. package/dist/cli.js +73 -96
  18. package/dist/demo/booking.d.ts +352 -36
  19. package/dist/describe.d.ts +47 -0
  20. package/dist/describe.js +195 -0
  21. package/dist/errors.d.ts +77 -13
  22. package/dist/errors.js +231 -30
  23. package/dist/index.d.ts +62 -74
  24. package/dist/index.js +312 -155
  25. package/dist/internal.d.ts +7 -0
  26. package/dist/jsonschema.js +18 -17
  27. package/dist/load.d.ts +29 -17
  28. package/dist/load.js +235 -190
  29. package/dist/log.d.ts +121 -0
  30. package/dist/log.js +633 -0
  31. package/dist/mcp.d.ts +5 -7
  32. package/dist/mcp.js +194 -225
  33. package/dist/messages/access.d.ts +90 -0
  34. package/dist/messages/access.js +113 -0
  35. package/dist/messages/api.d.ts +237 -0
  36. package/dist/messages/api.js +304 -0
  37. package/dist/messages/boot.d.ts +201 -0
  38. package/dist/messages/boot.js +256 -0
  39. package/dist/messages/codes.d.ts +172 -0
  40. package/dist/messages/codes.js +173 -0
  41. package/dist/messages/index.d.ts +98 -0
  42. package/dist/messages/index.js +121 -0
  43. package/dist/messages/log.d.ts +229 -0
  44. package/dist/messages/log.js +102 -0
  45. package/dist/messages/mcp.d.ts +306 -0
  46. package/dist/messages/mcp.js +387 -0
  47. package/dist/messages/model.d.ts +256 -0
  48. package/dist/messages/model.js +314 -0
  49. package/dist/messages/query.d.ts +141 -0
  50. package/dist/messages/query.js +171 -0
  51. package/dist/messages/sql-access.d.ts +70 -0
  52. package/dist/messages/sql-access.js +87 -0
  53. package/dist/messages/sql-core.d.ts +164 -0
  54. package/dist/messages/sql-core.js +198 -0
  55. package/dist/messages/sql-time.d.ts +86 -0
  56. package/dist/messages/sql-time.js +111 -0
  57. package/dist/messages/sql-write.d.ts +246 -0
  58. package/dist/messages/sql-write.js +270 -0
  59. package/dist/messages/time.d.ts +23 -0
  60. package/dist/messages/time.js +29 -0
  61. package/dist/messages/types.d.ts +27 -0
  62. package/dist/messages/types.js +5 -0
  63. package/dist/messages/write.d.ts +452 -0
  64. package/dist/messages/write.js +572 -0
  65. package/dist/model.d.ts +77 -60
  66. package/dist/model.js +43 -34
  67. package/dist/ops.d.ts +35 -0
  68. package/dist/ops.js +101 -0
  69. package/dist/pglite.d.ts +0 -9
  70. package/dist/pglite.js +3 -1
  71. package/dist/registry.d.ts +3 -19
  72. package/dist/registry.js +3 -16
  73. package/dist/signals.d.ts +1 -5
  74. package/dist/signals.js +1 -5
  75. package/dist/sql.d.ts +9 -1
  76. package/dist/sql.js +175 -107
  77. package/dist/sync.d.ts +10 -4
  78. package/dist/sync.js +90 -17
  79. package/dist/tx.d.ts +21 -2
  80. package/dist/tx.js +76 -13
  81. package/dist/typed.d.ts +252 -53
  82. package/dist/types.d.ts +93 -90
  83. package/dist/types.js +17 -14
  84. package/dist/up.d.ts +56 -35
  85. package/dist/up.js +229 -83
  86. package/dist/uuid.d.ts +8 -0
  87. package/dist/uuid.js +14 -6
  88. package/dist/validate.d.ts +13 -1
  89. package/dist/validate.js +82 -33
  90. package/dist/watch.d.ts +10 -16
  91. package/dist/watch.js +36 -77
  92. package/dist/write.d.ts +87 -17
  93. package/dist/write.js +441 -206
  94. package/llms.txt +48 -31
  95. package/package.json +13 -14
  96. package/sql/10-core.sql +7 -50
  97. package/sql/15-errors.sql +64 -10
  98. package/sql/20-context.sql +18 -9
  99. package/sql/30-validate.sql +79 -30
  100. package/sql/40-class.sql +41 -115
  101. package/sql/50-acl.sql +8 -13
  102. package/sql/60-write.sql +220 -676
  103. package/sql/70-read.sql +11 -13
  104. package/sql/80-auth.sql +109 -42
  105. package/sql/90-time.sql +108 -947
  106. package/sql/95-seed.system.sql +7 -14
  107. package/sql/99-revision.sql +6 -1
  108. package/sql/README.md +32 -24
  109. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  110. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  111. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  112. package/sql/migrate/README.md +18 -0
  113. package/AGENT-CHEATSHEET.en.md +0 -488
  114. package/MIGRATION.md +0 -190
  115. package/README.en.md +0 -2910
  116. package/dist/import.d.ts +0 -185
  117. package/dist/import.js +0 -820
  118. package/dist/project.d.ts +0 -359
  119. package/dist/project.js +0 -1258
  120. package/sql/75-history.sql +0 -436
  121. package/sql/88-derived.sql +0 -1236
  122. package/sql/migrate/0006-changed-paths.sql +0 -3
  123. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/sql/70-read.sql CHANGED
@@ -17,8 +17,7 @@
17
17
  --
18
18
  -- Источник — текущее состояние (entity) или журнал: версии, действовавшие на момент p_at (срез
19
19
  -- asOf), либо надгробия удалённых (p_dead, для withDeleted). Возвращается rev версии: строки
20
- -- журнала читаются по паре (id, rev) через уникальный индекс log_id_rev. Записи purge, trim и reset
21
- -- (этап 6) — не версии объекта: на момент после них объекта в срезе нет.
20
+ -- журнала читаются по паре (id, rev) через уникальный индекс log_id_rev.
22
21
 
23
22
  -- Цели концов строки: всех (p_roles is null) или только перечисленных ролей.
24
23
  create or replace function @SCHEMA@.role_ids(p_links jsonb, p_roles text[]) returns uuid[]
@@ -55,8 +54,7 @@ as $$
55
54
  select y.* from (select x.* from @SCHEMA@.log x where x.id = c.id offset 0) y
56
55
  where y.at <= coalesce(p_at, 'infinity'::timestamptz)
57
56
  order by y.rev desc limit 1) w
58
- -- класс — у последней версии на момент, а не среди версий классов шага: после смены класса без смены id объект
59
- -- не числится в прежнем классе (план 2.0, 7.1 №1)
57
+ -- класс — у последней версии на момент, а не среди версий классов шага (план 2.0, 7.1 №1)
60
58
  where w.class = any (p_classes) and @SCHEMA@.link_ids(w.links) @> array[p_src]
61
59
  $$;
62
60
 
@@ -123,21 +121,21 @@ begin
123
121
  from @SCHEMA@.log l
124
122
  where l.id = any (p_ids) and l.at <= coalesce(p_at, 'infinity'::timestamptz)
125
123
  order by l.id, l.rev desc) v
126
- where v.class = any (p_classes) and (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
124
+ where v.class = any (p_classes) and (v.op = 'delete') = p_dead
127
125
  and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
128
126
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
129
127
  elsif p_ends is not null then
130
128
  -- по целям концов — от версий, ссылавшихся на первую цель (GIN log_ends); арендатор — после пробы
131
129
  return query select v.id, v.rev from @SCHEMA@.log_refs_at(p_classes, p_ends[1], p_at) v
132
130
  where (v.tenant = any (v_tenants) or v.class = 'member')
133
- and (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
131
+ and (v.op = 'delete') = p_dead
134
132
  and (p_data is null or v.data @> p_data) and @SCHEMA@.link_ids(v.links) @> p_ends
135
133
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
136
134
  else
137
- -- арендатор id неизменен, записи purge и trim несут арендатора объекта: условие до distinct on не меняет, какая
135
+ -- арендатор id неизменен: условие до distinct on не меняет, какая
138
136
  -- версия id последняя (индекс log_class_tenant_at)
139
137
  -- кандидаты — по классу (индекс log_class_tenant_at), последняя версия id на момент — любого класса, и класс
140
- -- проверяется у неё: иначе после смены класса без смены id объект числился бы в прежнем классе (план 2.0, 7.1 №1)
138
+ -- проверяется у неё (план 2.0, 7.1 №1)
141
139
  return query select v.id, v.rev from (
142
140
  select distinct l.id from @SCHEMA@.log l
143
141
  where l.class = any (p_classes) and l.tenant = any (v_tenants) and l.at <= coalesce(p_at, 'infinity'::timestamptz)) c
@@ -145,14 +143,14 @@ begin
145
143
  select y.id, y.rev, y.op, y.class, y.tenant, y.owner, y.tags, y.data, y.links
146
144
  from @SCHEMA@.log y where y.id = c.id and y.at <= coalesce(p_at, 'infinity'::timestamptz)
147
145
  order by y.rev desc limit 1) v
148
- where v.class = any (p_classes) and (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
146
+ where v.class = any (p_classes) and (v.op = 'delete') = p_dead
149
147
  and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
150
148
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
151
149
  -- свои членства в других арендаторах: версии, ссылавшиеся на аккаунт (GIN log_ends)
152
150
  if v_member then
153
151
  return query select v.id, v.rev from @SCHEMA@.log_refs_at(array['member'], c_account, p_at) v
154
152
  where not (v.tenant = any (v_tenants))
155
- and (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
153
+ and (v.op = 'delete') = p_dead
156
154
  and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
157
155
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
158
156
  end if;
@@ -193,7 +191,7 @@ begin
193
191
  return query select s.src, v.id, v.rev
194
192
  from (select distinct u.x as src from unnest(p_src) u(x)) s
195
193
  cross join lateral @SCHEMA@.log_refs_at(p_classes, s.src, p_at) v
196
- where (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
194
+ where (v.op = 'delete') = p_dead
197
195
  and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
198
196
  and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, s.src))
199
197
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
@@ -243,14 +241,14 @@ begin
243
241
  select s.src, v.id, v.rev, 1, array[s.src, v.id]
244
242
  from (select distinct u.x as src from unnest(p_src) u(x)) s
245
243
  cross join lateral @SCHEMA@.log_refs_at(p_classes, s.src, p_at) v
246
- where v.op not in ('delete', 'purge', 'trim', 'reset')
244
+ where v.op <> 'delete'
247
245
  and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, s.src))
248
246
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan)
249
247
  union all
250
248
  select d.src, v.id, v.rev, d.depth + 1, d.seen || v.id
251
249
  from d
252
250
  cross join lateral @SCHEMA@.log_refs_at(p_classes, d.id, p_at) v
253
- where d.depth < p_max and not (v.id = any (d.seen)) and v.op not in ('delete', 'purge', 'trim', 'reset')
251
+ where d.depth < p_max and not (v.id = any (d.seen)) and v.op <> 'delete'
254
252
  and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, d.id))
255
253
  and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan))
256
254
  select distinct on (d.src, d.id) d.src, d.id, d.rev, d.depth from d order by d.src, d.id, d.depth;
package/sql/80-auth.sql CHANGED
@@ -17,7 +17,7 @@ return interval '10 minutes';
17
17
 
18
18
  create or replace function @SCHEMA@.auth_kind_ok(p_kind text) returns boolean
19
19
  language sql immutable parallel safe
20
- return p_kind ~ '^[A-Z][A-Z0-9_]{1,31}$' and p_kind not in ('SESSION', 'APIKEY', 'KEYSECRET', 'OTP', 'TOTP', 'KEY', 'PROJECTOR', 'LOCK');
20
+ return p_kind ~ '^[A-Z][A-Z0-9_]{1,31}$' and p_kind not in ('SESSION', 'APIKEY', 'KEYSECRET', 'OTP', 'TOTP', 'KEY', 'LOCK');
21
21
 
22
22
  -- Действующая сессия обязательна (иначе no_session).
23
23
  create or replace function @SCHEMA@.auth_session_required(p_account uuid) returns void
@@ -25,7 +25,7 @@ language plpgsql immutable
25
25
  as $$
26
26
  begin
27
27
  if p_account is null then
28
- perform @SCHEMA@.lt_fail('no_session', 'нет действующей сессии');
28
+ perform @SCHEMA@.lt_fail('sql-access.no_session');
29
29
  end if;
30
30
  end $$;
31
31
 
@@ -42,7 +42,7 @@ declare
42
42
  begin
43
43
  select * into c from @SCHEMA@.ctx();
44
44
  if c.account is null then
45
- perform @SCHEMA@.lt_fail('no_session', 'нет действующей сессии');
45
+ perform @SCHEMA@.lt_fail('sql-access.no_session');
46
46
  end if;
47
47
  if c.account = p_account and c.service is null then
48
48
  select (s.meta->>'login_at')::timestamptz, coalesce(s.meta->'methods', '[]'::jsonb) into login_at, methods
@@ -52,17 +52,18 @@ begin
52
52
  if login_at > clock_timestamp() - @SCHEMA@.auth_recent()
53
53
  and exists (select 1 from jsonb_array_elements_text(methods) m where m not in ('APIKEY', 'KEYSECRET', 'SERVICE'))
54
54
  and (methods ? 'TOTP' or not exists (select 1 from @SCHEMA@.secret t where t.account = p_account and t.kind = 'TOTP'
55
- and t.deleted_at is null and coalesce((t.meta->>'confirmed')::boolean, false))) then
55
+ and t.deleted_at is null and coalesce((t.meta->>'confirmed')::boolean, false)
56
+ and coalesce((t.meta->>'enabled')::boolean, true))) then
56
57
  return;
57
58
  end if;
58
59
  end if;
59
60
  if not coalesce((@SCHEMA@.acl_api_for(c.tenant, c.categories, c.roles, 'auth.credential', true)->>'allow')::boolean, false) then
60
- perform @SCHEMA@.lt_fail('acl_denied', 'привязка отклонена: нужен недавний вход или право auth.credential');
61
+ perform @SCHEMA@.lt_fail('sql-access.bind_denied');
61
62
  end if;
62
63
  -- креды System, сервисов и системных администраторов выдаёт только системный администратор: иначе
63
64
  -- сервис привязал бы себе вход под ними (ревью этапа 5)
64
65
  if not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
65
- perform @SCHEMA@.lt_fail('acl_denied', 'привязка отклонена: нужен недавний вход или право auth.credential');
66
+ perform @SCHEMA@.lt_fail('sql-access.bind_denied');
66
67
  end if;
67
68
  end $$;
68
69
 
@@ -77,7 +78,7 @@ declare
77
78
  begin
78
79
  select * into c from @SCHEMA@.ctx();
79
80
  if p_account is distinct from c.account and not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
80
- perform @SCHEMA@.lt_fail('acl_denied', 'креды и сессии этого аккаунта меняет только он сам или системный администратор');
81
+ perform @SCHEMA@.lt_fail('sql-access.self_or_sysadmin');
81
82
  end if;
82
83
  end $$;
83
84
 
@@ -203,7 +204,7 @@ begin
203
204
  perform @SCHEMA@.auth_session_required(c.account);
204
205
  perform @SCHEMA@.acl_require('auth.session');
205
206
  if not @SCHEMA@.auth_target_ok(p_account) then
206
- perform @SCHEMA@.lt_fail('acl_denied', 'сессии для этого аккаунта не выпускаются');
207
+ perform @SCHEMA@.lt_fail('sql-access.no_sessions_for_account');
207
208
  end if;
208
209
  return @SCHEMA@.new_session(p_account, p_ttl, jsonb_build_object('methods', to_jsonb(p_methods), 'service', c.account));
209
210
  end $$;
@@ -248,14 +249,14 @@ begin
248
249
  p_confirmed := null;
249
250
  end if;
250
251
  if not @SCHEMA@.auth_kind_ok(p_kind) or coalesce(p_ident, '') = '' or p_hash !~ '^scrypt\$' then
251
- perform @SCHEMA@.lt_fail('invalid_data', 'пароль: вид, идентификатор и хэш scrypt$… обязательны');
252
+ perform @SCHEMA@.lt_fail('sql-access.password_args');
252
253
  end if;
253
254
  select s.id, s.account into cur from @SCHEMA@.secret s
254
255
  where s.kind = p_kind and s.deleted_at is null
255
256
  and casefold(s.ident) collate pg_unicode_fast = casefold(p_ident) collate pg_unicode_fast
256
257
  for update;
257
258
  if cur.id is not null and cur.account <> p_account then
258
- perform @SCHEMA@.lt_fail('acl_denied', 'привязка отклонена: нужен недавний вход или право auth.credential');
259
+ perform @SCHEMA@.lt_fail('sql-access.bind_denied');
259
260
  end if;
260
261
  if cur.id is not null then
261
262
  update @SCHEMA@.secret s
@@ -270,7 +271,7 @@ begin
270
271
  values (p_account, p_kind, p_ident, convert_to(p_hash, 'UTF8'), jsonb_build_object('confirmed', coalesce(p_confirmed, false)))
271
272
  returning id into cid;
272
273
  exception when unique_violation then
273
- perform @SCHEMA@.lt_fail('acl_denied', 'привязка отклонена: нужен недавний вход или право auth.credential');
274
+ perform @SCHEMA@.lt_fail('sql-access.bind_denied');
274
275
  end;
275
276
  end if;
276
277
  return cid;
@@ -308,7 +309,7 @@ begin
308
309
  null; -- совпал префикс: новый ключ
309
310
  end;
310
311
  end loop;
311
- perform @SCHEMA@.lt_raise('40001', 'conflict', 'не удалось выпустить ключ с уникальным префиксом');
312
+ perform @SCHEMA@.lt_raise('40001','sql-access.apikey_prefix_conflict', '{}'::jsonb);
312
313
  end $$;
313
314
 
314
315
  -- Вход по ключу API для db.auth.verifyApiKey: { account, token } или null (login_apikey — для
@@ -326,7 +327,9 @@ begin
326
327
  and s.material = sha256(convert_to(coalesce(p_key, ''), 'UTF8'))
327
328
  and (s.expires_at is null or s.expires_at > clock_timestamp())
328
329
  and coalesce((s.meta->>'enabled')::boolean, true)
329
- and @SCHEMA@.auth_enabled(s.account);
330
+ and @SCHEMA@.auth_enabled(s.account)
331
+ -- строка ключа — под блокировкой до фиксации входа: отзыв ждёт её и гасит открытую здесь сессию
332
+ for share of s;
330
333
  if k.account is null then
331
334
  return null;
332
335
  end if;
@@ -370,7 +373,9 @@ begin
370
373
  and casefold(s.ident) collate pg_unicode_fast = casefold(p_key) collate pg_unicode_fast
371
374
  and s.material = sha256(convert_to(coalesce(p_secret, ''), 'UTF8'))
372
375
  and coalesce((s.meta->>'enabled')::boolean, true)
373
- and @SCHEMA@.auth_enabled(s.account);
376
+ and @SCHEMA@.auth_enabled(s.account)
377
+ -- строка ключа — под блокировкой до фиксации входа: отзыв ждёт её и гасит открытую здесь сессию
378
+ for share of s;
374
379
  if k.account is null then
375
380
  return null;
376
381
  end if;
@@ -390,13 +395,13 @@ declare
390
395
  begin
391
396
  perform @SCHEMA@.acl_require('auth.otp');
392
397
  if p_kind !~ '^OTP(_[A-Z0-9]{1,27})?$' or coalesce(p_ident, '') = '' or p_digits not between 4 and 10 then
393
- perform @SCHEMA@.lt_fail('invalid_data', 'код: вид OTP или OTP_<имя>, идентификатор, 4–10 знаков');
398
+ perform @SCHEMA@.lt_fail('sql-access.otp_args');
394
399
  end if;
395
400
  if not @SCHEMA@.auth_enabled(p_account) then
396
- perform @SCHEMA@.lt_fail('target_not_found', 'аккаунт не найден');
401
+ perform @SCHEMA@.lt_fail('sql-access.account_not_found');
397
402
  end if;
398
403
  if not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
399
- perform @SCHEMA@.lt_fail('acl_denied', 'коды входа для System, сервисов и системных администраторов выпускает только системный администратор');
404
+ perform @SCHEMA@.lt_fail('sql-access.otp_sysadmin_only');
400
405
  end if;
401
406
  code := lpad((('x' || encode(@PGCRYPTO@.gen_random_bytes(7), 'hex'))::bit(56)::bigint % (10::bigint ^ p_digits)::bigint)::text, p_digits, '0');
402
407
  update @SCHEMA@.secret s set deleted_at = clock_timestamp()
@@ -428,6 +433,7 @@ begin
428
433
  from @SCHEMA@.secret x
429
434
  where x.kind = p_kind and x.kind ~ '^OTP' and x.deleted_at is null
430
435
  and casefold(x.ident) collate pg_unicode_fast = casefold(p_ident) collate pg_unicode_fast
436
+ and coalesce((x.meta->>'enabled')::boolean, true) -- отключённый код (setFlags) не принимается
431
437
  for update;
432
438
  if s.id is null then
433
439
  return null;
@@ -507,6 +513,7 @@ begin
507
513
  coalesce((x.meta->>'fails')::int, 0) as fails, (x.meta->>'locked_until')::timestamptz as locked_until into s
508
514
  from @SCHEMA@.secret x
509
515
  where x.account = p_account and x.kind = 'TOTP' and x.deleted_at is null
516
+ and coalesce((x.meta->>'enabled')::boolean, true) -- отключённый фактор (setFlags) не принимается
510
517
  for update;
511
518
  if s.id is null or s.locked_until > clock_timestamp() then
512
519
  return false;
@@ -547,7 +554,7 @@ begin
547
554
  perform @SCHEMA@.auth_guard(p_account);
548
555
  end if;
549
556
  return exists (select 1 from @SCHEMA@.secret s where s.account = p_account and s.kind = 'TOTP' and s.deleted_at is null
550
- and coalesce((s.meta->>'confirmed')::boolean, false));
557
+ and coalesce((s.meta->>'confirmed')::boolean, false) and coalesce((s.meta->>'enabled')::boolean, true));
551
558
  end $$;
552
559
 
553
560
  -- Внешняя identity (TELEGRAM, GOOGLE, SSO-провайдер): токен проверяет сервис, здесь — связка.
@@ -564,7 +571,7 @@ begin
564
571
  p_confirmed := false;
565
572
  end if;
566
573
  if not @SCHEMA@.auth_kind_ok(p_kind) or p_kind = 'PASSWORD' or coalesce(p_ident, '') = '' then
567
- perform @SCHEMA@.lt_fail('invalid_data', 'identity: вид (TELEGRAM, GOOGLE, …) и идентификатор обязательны');
574
+ perform @SCHEMA@.lt_fail('sql-access.identity_args');
568
575
  end if;
569
576
  begin
570
577
  insert into @SCHEMA@.secret (account, kind, ident, meta)
@@ -572,7 +579,7 @@ begin
572
579
  || jsonb_build_object('confirmed', coalesce(p_confirmed, false)))
573
580
  returning id into cid;
574
581
  exception when unique_violation then
575
- perform @SCHEMA@.lt_fail('acl_denied', 'привязка отклонена: нужен недавний вход или право auth.credential');
582
+ perform @SCHEMA@.lt_fail('sql-access.bind_denied');
576
583
  end;
577
584
  return cid;
578
585
  end $$;
@@ -617,7 +624,7 @@ begin
617
624
  coalesce((s.meta->>'confirmed')::boolean, false), coalesce((s.meta->>'enabled')::boolean, true),
618
625
  s.meta - 'confirmed' - 'enabled' - 'attempts' - 'last_step', s.created_at, s.expires_at
619
626
  from @SCHEMA@.secret s
620
- where s.account = p_account and s.kind not in ('SESSION', 'KEY', 'PROJECTOR', 'LOCK') and s.deleted_at is null
627
+ where s.account = p_account and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
621
628
  and (s.expires_at is null or s.expires_at > clock_timestamp())
622
629
  order by s.created_at, s.id;
623
630
  end $$;
@@ -639,7 +646,10 @@ begin
639
646
  perform @SCHEMA@.auth_session_required(c.account);
640
647
  select s.account, s.kind,
641
648
  (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true) as live
642
- into cr from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'PROJECTOR', 'LOCK') and s.deleted_at is null;
649
+ into cr from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
650
+ -- строка креда — под блокировкой: вход по этому ключу, начатый раньше, отзыв дожидается (его сессию гасит
651
+ -- auth_kill_cred ниже), начатый позже — ждёт отзыва и ключа уже не находит (login_apikey, login_keysecret)
652
+ for update;
643
653
  if cr.account is null then
644
654
  return false;
645
655
  end if;
@@ -655,7 +665,7 @@ begin
655
665
  if not exists (select 1 from @SCHEMA@.secret s
656
666
  where s.account = cr.account and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null and s.id <> p_id
657
667
  and (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)) then
658
- perform @SCHEMA@.lt_fail('acl_denied', 'последний действующий ключ сервиса не отзывается: сначала выпустите новый');
668
+ perform @SCHEMA@.lt_fail('sql-access.last_service_key');
659
669
  end if;
660
670
  end if;
661
671
  update @SCHEMA@.secret s set deleted_at = clock_timestamp() where s.id = p_id and s.deleted_at is null;
@@ -677,17 +687,47 @@ set search_path = pg_catalog, @SCHEMA@, pg_temp
677
687
  as $$
678
688
  declare
679
689
  acc uuid;
690
+ knd text;
691
+ live boolean;
680
692
  begin
681
693
  perform @SCHEMA@.acl_require('auth.credential');
682
- select s.account into acc from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'PROJECTOR', 'LOCK') and s.deleted_at is null;
694
+ -- строка креда — под блокировкой, как при отзыве (auth_revoke_credential): вход по этому ключу, начатый раньше,
695
+ -- отключение дожидается и гасит его сессию ниже; начатый позже — ждёт и видит enabled = false (3.0.0, ревизия 11)
696
+ select s.account, s.kind,
697
+ (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)
698
+ into acc, knd, live
699
+ from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
700
+ for update;
683
701
  if acc is null then
684
702
  return false;
685
703
  end if;
686
704
  perform @SCHEMA@.auth_guard(acc);
705
+ if p_enabled is false and live and knd in ('APIKEY', 'KEYSECRET') and @SCHEMA@.auth_is_service(acc) then
706
+ -- последний действующий ключ сервиса не отключается, как и не отзывается
707
+ perform 1 from @SCHEMA@.secret s
708
+ where s.account = acc and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null
709
+ for update;
710
+ if not exists (select 1 from @SCHEMA@.secret s
711
+ where s.account = acc and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null and s.id <> p_id
712
+ and (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)) then
713
+ perform @SCHEMA@.lt_fail('sql-access.last_service_key');
714
+ end if;
715
+ end if;
687
716
  update @SCHEMA@.secret s
688
717
  set meta = s.meta || jsonb_strip_nulls(jsonb_build_object('confirmed', p_confirmed, 'enabled', p_enabled))
689
718
  where s.id = p_id;
690
- return found;
719
+ if not found then
720
+ return false;
721
+ end if;
722
+ -- отключённый кред не должен оставлять действующих сессий: ключ — свои (auth_kill_cred), прочие — сессии аккаунта
723
+ if p_enabled is false then
724
+ if knd in ('APIKEY', 'KEYSECRET') then
725
+ perform @SCHEMA@.auth_kill_cred(acc, p_id, knd, null);
726
+ else
727
+ perform @SCHEMA@.auth_kill(acc, null);
728
+ end if;
729
+ end if;
730
+ return true;
691
731
  end $$;
692
732
 
693
733
  -- Продление текущей сессии (библиотека — не чаще раза в минуту, §2.9). Новый срок — от сейчас.
@@ -725,6 +765,31 @@ begin
725
765
  return m is not null;
726
766
  end $$;
727
767
 
768
+ -- Выход пользователя по токену его сессии (db.auth.revoke(token), план 2.1, A3): сервис, у которого есть токен
769
+ -- пользователя (например, из cookie), гасит эту сессию, не подключаясь по ней. Право auth.session, как у
770
+ -- auth_session_info. Сессия ищется только среди SESSION по sha256 токена; погашенная — сигнал session:… кэшу, как у
771
+ -- auth_logout. Неверный, погашенный или истёкший токен — false: причина не различается.
772
+ create or replace function @SCHEMA@.auth_logout_token(p_token text) returns boolean
773
+ language plpgsql volatile security definer
774
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
775
+ as $$
776
+ declare
777
+ c record;
778
+ m bytea;
779
+ begin
780
+ select * into c from @SCHEMA@.ctx();
781
+ perform @SCHEMA@.auth_session_required(c.account);
782
+ perform @SCHEMA@.acl_require('auth.session');
783
+ update @SCHEMA@.secret s set deleted_at = clock_timestamp()
784
+ where s.kind = 'SESSION' and s.deleted_at is null and s.material = sha256(convert_to(coalesce(p_token, ''), 'UTF8'))
785
+ and (s.expires_at is null or s.expires_at > clock_timestamp())
786
+ returning s.material into m;
787
+ if m is not null then
788
+ perform @SCHEMA@.signal_raw(array['session:' || left(encode(m, 'hex'), 16)]);
789
+ end if;
790
+ return m is not null;
791
+ end $$;
792
+
728
793
  -- Выход везде: все сессии аккаунта (своего — сам; чужого — с правом auth.session).
729
794
  create or replace function @SCHEMA@.auth_logout_all(p_account uuid default null) returns int
730
795
  language plpgsql volatile security definer
@@ -756,20 +821,20 @@ declare
756
821
  begin
757
822
  if nullif(current_setting('letopis.account', true), '') is not null then
758
823
  -- код из каталога базы (invalid_query в нём нет — lt_fail дал бы LT000 и сырую ошибку; план 2.0, 7.5 №3)
759
- perform @SCHEMA@.lt_fail('acl_denied', 'auth.switch — для собственной сессии, не при имперсонации');
824
+ perform @SCHEMA@.lt_fail('sql-access.switch_impersonating');
760
825
  end if;
761
826
  select x.id, x.account into s from @SCHEMA@.secret x
762
827
  where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(tok, ''), 'UTF8'))
763
828
  and (x.expires_at is null or x.expires_at > clock_timestamp())
764
829
  for update;
765
830
  if s.id is null or not @SCHEMA@.auth_enabled(s.account) then
766
- perform @SCHEMA@.lt_fail('no_session', 'нет действующей сессии');
831
+ perform @SCHEMA@.lt_fail('sql-access.no_session');
767
832
  end if;
768
833
  if p_tenant <> s.account and not (
769
834
  exists (select 1 from @SCHEMA@.entity x
770
835
  where x.id = @SCHEMA@.v5_id(p_tenant, 'member', array[s.account::text, p_tenant::text]) and x.class = 'member' and x.tenant = p_tenant)
771
836
  and @SCHEMA@.auth_enabled(p_tenant)) then
772
- perform @SCHEMA@.lt_fail('tenant_denied', 'нет членства в арендаторе');
837
+ perform @SCHEMA@.lt_fail('sql-access.no_membership');
773
838
  end if;
774
839
  update @SCHEMA@.secret x
775
840
  set meta = case when p_tenant = s.account then x.meta - 'tenant' else x.meta || jsonb_build_object('tenant', p_tenant) end
@@ -777,8 +842,10 @@ begin
777
842
  return p_tenant;
778
843
  end $$;
779
844
 
780
- -- Арендаторы, доступные аккаунту сессии: собственный и по членству (с ролями).
781
- create or replace function @SCHEMA@.auth_tenants() returns table (tenant uuid, roles jsonb)
845
+ -- Арендаторы, доступные аккаунту сессии: собственный и по членству (с ролями). С ревизии 9 (план 2.1, A5) — ещё имя
846
+ -- аккаунта арендатора (строка Account в System: createTenant({ name }), у личного — имя пользователя) и признак
847
+ -- собственного арендатора personal; прежняя форма (tenant, roles) снимается миграцией 0009.
848
+ create or replace function @SCHEMA@.auth_tenants() returns table (tenant uuid, roles jsonb, name text, personal boolean)
782
849
  language plpgsql stable security definer
783
850
  set search_path = pg_catalog, @SCHEMA@, pg_temp
784
851
  as $$
@@ -789,11 +856,14 @@ begin
789
856
  select * into c from @SCHEMA@.ctx();
790
857
  perform @SCHEMA@.auth_session_required(c.account);
791
858
  return query
792
- select c.account, '["owner"]'::jsonb
793
- union all
794
- select m.tenant, coalesce(m.data->'roles', '[]'::jsonb)
795
- from @SCHEMA@.entity m
796
- where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant);
859
+ select t.tenant, t.roles, a.data->>'name', t.personal
860
+ from (select c.account as tenant, '["owner"]'::jsonb as roles, true as personal, 0 as ord
861
+ union all
862
+ select m.tenant, coalesce(m.data->'roles', '[]'::jsonb), false, 1
863
+ from @SCHEMA@.entity m
864
+ where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant)) t
865
+ left join @SCHEMA@.entity a on a.id = t.tenant and a.class = 'Account' and a.tenant = '@SYSTEM@'::uuid
866
+ order by t.ord, t.tenant;
797
867
  end $$;
798
868
 
799
869
  -- Сведения о сессии по токену (db.auth.session): сервис, у которого есть токен пользователя (например, из
@@ -845,15 +915,12 @@ begin
845
915
  'login_otp(text, text, text)', 'totp_code(bytea, bigint)', 'auth_enroll_totp(uuid)', 'auth_verify_totp(uuid, text, int)',
846
916
  'auth_totp_enabled(uuid)', 'auth_link(uuid, text, text, jsonb, boolean)', 'auth_lookup(text, text, boolean)',
847
917
  'auth_credentials(uuid)', 'auth_revoke_credential(uuid, boolean)', 'auth_set_flags(uuid, boolean, boolean)',
848
- 'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()', 'auth_session_info(text)'] loop
918
+ 'auth_refresh(interval)', 'auth_logout()', 'auth_logout_token(text)', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()',
919
+ 'auth_session_info(text)'] loop
849
920
  execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
850
921
  execute format('revoke all on function @SCHEMA@.%s from public', f);
851
922
  end loop;
852
923
  end $$;
853
- -- до этапа 5 выпуск сессии сервисом принимал два аргумента; ранние сигнатуры OTP этапа 5
854
- drop function if exists @SCHEMA@.issue_session(uuid, interval);
855
- drop function if exists @SCHEMA@.auth_issue_otp(uuid, text, text, interval, int);
856
- drop function if exists @SCHEMA@.login_otp(text, text, text, int);
857
924
  grant execute on function
858
925
  @SCHEMA@.issue_session(uuid, interval, text[]), @SCHEMA@.auth_password_get(text, text),
859
926
  @SCHEMA@.auth_set_password(uuid, text, text, text, boolean, boolean), @SCHEMA@.auth_issue_apikey(uuid, text, interval),
@@ -861,5 +928,5 @@ grant execute on function
861
928
  @SCHEMA@.login_otp(text, text, text), @SCHEMA@.auth_enroll_totp(uuid), @SCHEMA@.auth_verify_totp(uuid, text, int),
862
929
  @SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
863
930
  @SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
864
- @SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_all(uuid), @SCHEMA@.auth_switch(uuid),
865
- @SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
931
+ @SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_token(text), @SCHEMA@.auth_logout_all(uuid),
932
+ @SCHEMA@.auth_switch(uuid), @SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;