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/dist/acl.js CHANGED
@@ -1,11 +1,180 @@
1
1
  /**
2
- * db.acl — решения прав для приложения (план, §2.8, этап 5, п. 7). Сами права применяет база
3
- * (политики RLS по плану прав, 50-acl.sql); эти вызовы отвечают «можно ли» заранее — для интерфейса
4
- * и эндпоинтов приложения. Правила — строки Resource и rule (группа → объект, allow | deny, вес);
5
- * решение — ровно одно правило с наибольшим весом, при равном весе запрет сильнее, нет правил — запрет.
2
+ * db.acl — права приложения (план, §2.8, этап 5, п. 7; план 2.0, этап 3, п. 1). Сами права применяет база
3
+ * (политики RLS по плану прав, 50-acl.sql); check и checkData отвечают «можно ли» заранее — для интерфейса и
4
+ * эндпоинтов приложения. Правила — строки Resource и rule (группа → объект, allow | deny, вес); решение — ровно одно
5
+ * правило с наибольшим весом, при равном весе запрет сильнее, нет правил — запрет.
6
+ *
7
+ * Запись правил (план 2.0, 7.9 №1): grant, deny, revoke, roles и list — обёртка над теми же строками Resource, rule и
8
+ * member текущего арендатора, цепочками фасада: право на саму запись, триггеры и RLS проверяет база, как у любой
9
+ * записи. Alias ресурсов обёртки детерминированные — группа `role:<роль>:ACCOUNT`, объект `class:<класс>[условие]:<ОП>`
10
+ * или `api:<эндпоинт>:API`, — а ключ правила — пара [Group, Object]: повтор вызова строк и версий не заводит, а grant
11
+ * после deny той же роли и цели заменяет запрет разрешением. Ресурсы обёртки живут в арендаторе: ссылку правила
12
+ * арендатора на ресурс System база не пропустит (цель должна быть видна).
13
+ *
14
+ * Все аккаунты арендатора (`everyone: true` вместо `role`): группа обёртки `everyone:ACCOUNT` с маской группы
15
+ * `authenticated:ACCOUNT` System — её правила «можно всё» (вес 10) заводит createTenant. revoke для всех аккаунтов
16
+ * снимает правила обеих групп, а цель `'*'` — и объект обёртки `class:*:<ОП>`, и объект System `all:<ОП>`: так
17
+ * `revoke({ everyone: true, read: '*', write: '*', delete: '*' })` снимает «можно всё». Строк System арендатор не видит,
18
+ * их id — `v5Id(System, 'Resource', [alias])`.
6
19
  */
7
- import { removed } from './errors.js';
8
- export function makeAcl(S, exec) {
20
+ import { lerr } from './errors.js';
21
+ import { internalsOf } from './internal.js';
22
+ import { v5Id } from './uuid.js';
23
+ const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
24
+ /** Имя класса — как в метасхеме Class (40-class.sql). */
25
+ const NAME = /^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$/;
26
+ /** Колонки, на которые ставят условие правила (шаблон acl_row_ok, 50-acl.sql). */
27
+ const COLUMNS = ['id', 'owner', 'tenant', 'tags', 'data', 'links'];
28
+ const OPS = { read: 'READ', write: 'WRITE', delete: 'DELETE' };
29
+ /** Ресурсы сида System (95-seed.system.sql): на них ссылаются правила grantAll арендатора, а самих строк арендатор не видит. */
30
+ const SYSTEM_RESOURCES = ['anonymous:ACCOUNT', 'authenticated:ACCOUNT', 'any:ACCOUNT', 'service:ACCOUNT', 'admin:ACCOUNT',
31
+ 'any:API', 'auth:API', 'letopis:API', 'signals:API', 'all:READ', 'all:WRITE', 'all:DELETE'];
32
+ /** JSON с ключами объектов по порядку: одно условие — один alias. */
33
+ function canon(v) {
34
+ if (Array.isArray(v))
35
+ return `[${v.map(canon).join(',')}]`;
36
+ if (v && typeof v === 'object') {
37
+ return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
38
+ }
39
+ return JSON.stringify(v);
40
+ }
41
+ /**
42
+ * Имя роли — по тому же правилу, что роли createTenant: маска группы делит роли по запятой и берёт их в скобки. what —
43
+ * метод db.acl (grant, deny, revoke, roles): им начинается текст ошибки, как и у classItem, apiItem и parse.
44
+ */
45
+ function roleName(v, what) {
46
+ if (typeof v !== 'string')
47
+ throw lerr('access.role_not_string', { what, type: v === null ? 'null' : typeof v });
48
+ if (!v || v !== v.trim() || /[,{}]/.test(v) || v.startsWith('!')) {
49
+ throw lerr('access.bad_role_name', { what, role: JSON.stringify(v) });
50
+ }
51
+ return v;
52
+ }
53
+ const many = (v) => (Array.isArray(v) ? v : [v]);
54
+ function groupOf(role) {
55
+ return { alias: `role:${role}:ACCOUNT`, category: 'ACCOUNT', pattern: { roles: `{${role}}` } };
56
+ }
57
+ /** Все аккаунты арендатора: маска как у группы authenticated:ACCOUNT System (95-seed.system.sql). */
58
+ const EVERYONE = { alias: 'everyone:ACCOUNT', category: 'ACCOUNT', pattern: { categories: '!{Anonymous,Shadow}' } };
59
+ function classItem(op, t, what) {
60
+ let cls = t;
61
+ let cond = {};
62
+ if (t && typeof t === 'object' && !Array.isArray(t)) {
63
+ ({ class: cls, ...cond } = t);
64
+ for (const k of Object.keys(cond)) {
65
+ if (!COLUMNS.includes(k))
66
+ throw lerr('access.bad_condition_column', { what, column: k, columns: COLUMNS.join(', ') });
67
+ }
68
+ }
69
+ if (typeof cls !== 'string' || (cls !== '*' && !NAME.test(cls))) {
70
+ throw lerr('access.bad_rule_target', { what, target: JSON.stringify(t) });
71
+ }
72
+ const condition = Object.keys(cond).length ? cond : undefined;
73
+ return {
74
+ op, category: op, target: cls,
75
+ alias: `class:${cls}${condition ? canon(condition) : ''}:${op}`,
76
+ pattern: { ...(cls === '*' ? {} : { class: cls }), ...cond },
77
+ ...(condition ? { condition } : {}),
78
+ };
79
+ }
80
+ function apiItem(ep, what) {
81
+ if (typeof ep !== 'string' || !/^[\p{L}\p{N}_.*{},-]+$/u.test(ep)) {
82
+ throw lerr('access.bad_endpoint_mask', { what, endpoint: JSON.stringify(ep) });
83
+ }
84
+ return { op: 'API', category: 'API', target: ep, alias: `api:${ep}:API`, pattern: { endpoint: ep } };
85
+ }
86
+ /** Спецификация правила; role — null, когда правило для всех аккаунтов арендатора (everyone: true). */
87
+ function parse(spec, what, withWeight) {
88
+ if (!spec || typeof spec !== 'object' || Array.isArray(spec))
89
+ throw lerr('access.rule_spec_not_object', { what });
90
+ const o = spec;
91
+ const keys = ['role', 'everyone', 'read', 'write', 'delete', 'api', ...(withWeight ? ['weight'] : [])];
92
+ for (const k of Object.keys(o))
93
+ if (!keys.includes(k))
94
+ throw lerr('access.unknown_rule_field', { what, field: k, fields: keys.join(', ') });
95
+ // кому: ровно одно из role и everyone: true
96
+ if (o.everyone === undefined ? o.role === undefined : o.everyone !== true || o.role !== undefined) {
97
+ throw lerr('access.rule_who', { what });
98
+ }
99
+ const role = o.everyone === true ? null : roleName(o.role, what);
100
+ let weight = 50;
101
+ if (o.weight !== undefined) {
102
+ if (typeof o.weight !== 'number' || !Number.isInteger(o.weight) || o.weight < 1 || o.weight > 999) {
103
+ throw lerr('access.bad_rule_weight', { what });
104
+ }
105
+ weight = o.weight;
106
+ }
107
+ const items = [];
108
+ for (const [key, op] of Object.entries(OPS)) {
109
+ if (o[key] !== undefined)
110
+ for (const t of many(o[key]))
111
+ items.push(classItem(op, t, what));
112
+ }
113
+ if (o.api !== undefined)
114
+ for (const ep of many(o.api))
115
+ items.push(apiItem(ep, what));
116
+ if (!items.length)
117
+ throw lerr('access.no_rule_operations', { what });
118
+ return { role, weight, items };
119
+ }
120
+ /** Роль группы обёртки и групп вида `{роль}`; иначе null. */
121
+ function roleOf(pattern) {
122
+ const p = pattern;
123
+ if (!p || p.categories !== undefined || typeof p.roles !== 'string')
124
+ return null;
125
+ const m = /^\{([^,{}!]+)\}$/.exec(p.roles);
126
+ return m ? m[1] : null;
127
+ }
128
+ /**
129
+ * db.acl фасада. db — сам фасад (корень, db.as(), db.begin()): запись правил идёт его цепочками, внутри db.begin() — в её
130
+ * транзакции.
131
+ */
132
+ export function makeAcl(S, exec, db) {
133
+ const facade = () => {
134
+ const d = db?.();
135
+ if (!d)
136
+ throw lerr('access.no_facade');
137
+ return d;
138
+ };
139
+ async function inTx(what, fn) {
140
+ const d = facade();
141
+ if (internalsOf(d)?.explicit)
142
+ return fn(d);
143
+ const tr = await d.begin({ reason: `db.acl.${what}` });
144
+ try {
145
+ const r = await fn(tr);
146
+ await tr.commit();
147
+ return r;
148
+ }
149
+ catch (e) {
150
+ await tr.rollback().catch(() => { });
151
+ throw e;
152
+ }
153
+ }
154
+ /** Ресурс целиком (upsert по alias): та же строка — пустая правка, без новой версии. */
155
+ async function resource(tr, r) {
156
+ const [row] = await tr.Resource().upsert({ alias: r.alias, category: r.category, pattern: r.pattern }).rows();
157
+ return row.id;
158
+ }
159
+ async function put(spec, permission, what) {
160
+ const p = parse(spec, what, true);
161
+ return inTx(what, async (tr) => {
162
+ const group = p.role === null ? EVERYONE : groupOf(p.role);
163
+ const g = await resource(tr, group);
164
+ const out = [];
165
+ for (const it of p.items) {
166
+ const o = await resource(tr, it);
167
+ const [r] = await tr.rule().upsert({ permission, weight: p.weight }).Group.set(g).Object.set(o).rows();
168
+ out.push({ id: r.id, role: p.role, everyone: p.role === null, group: group.alias, op: it.op, target: it.target,
169
+ ...(it.condition ? { condition: it.condition } : {}), permission, weight: p.weight, enabled: true });
170
+ }
171
+ return out;
172
+ });
173
+ }
174
+ /** id ресурса сида System: строку арендатор не видит, ссылаться на неё правилом обёртки не может. */
175
+ const sysId = (alias) => v5Id(SYSTEM_ID, 'Resource', [alias]);
176
+ const sys = new Map(SYSTEM_RESOURCES.map((a) => [sysId(a), a]));
177
+ const AUTHENTICATED = sysId('authenticated:ACCOUNT');
9
178
  const api = {
10
179
  async check(endpoint) {
11
180
  const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check($1) as d`, [endpoint]), { read: true });
@@ -15,12 +184,111 @@ export function makeAcl(S, exec) {
15
184
  const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check_data($1, $2) as d`, [className, op]), { read: true });
16
185
  return r.d;
17
186
  },
18
- };
19
- return new Proxy(api, {
20
- get(t, p, r) {
21
- if (p === 'reload')
22
- throw removed('db.acl.reload()', 'права обновляются сами: база применяет действующие правила');
23
- return Reflect.get(t, p, r);
187
+ grant: (spec) => put(spec, 'allow', 'grant'),
188
+ deny: (spec) => put(spec, 'deny', 'deny'),
189
+ async revoke(spec) {
190
+ const p = parse(spec, 'revoke', false);
191
+ return inTx('revoke', async (tr) => {
192
+ const find = async (alias) => (await tr.Resource({ alias }).rows())[0]?.id;
193
+ // все аккаунты — группа обёртки и группа «можно всё» System
194
+ const groups = p.role === null ? [await find(EVERYONE.alias), AUTHENTICATED] : [await find(groupOf(p.role).alias)];
195
+ let n = 0;
196
+ for (const it of p.items) {
197
+ const objects = [await find(it.alias)];
198
+ // все классы — объект обёртки class:*:<ОП> и объект «можно всё» System all:<ОП>
199
+ if (it.op !== 'API' && it.target === '*' && !it.condition)
200
+ objects.push(sysId(`all:${it.op}`));
201
+ for (const g of groups) {
202
+ for (const o of objects) {
203
+ // снять правило — замысел вызова, превью некому показывать: { all: true } (3.0) — без сверки, цели сужает фильтр
204
+ if (g && o)
205
+ n += (await tr.rule({ Group: g, Object: o }).delete({ confirm: true, all: true }).rows()).length;
206
+ }
207
+ }
208
+ }
209
+ return n;
210
+ });
211
+ },
212
+ async roles(account, roles) {
213
+ const acc = typeof account === 'object' && account !== null ? account.id : account;
214
+ if (typeof acc !== 'string' || !acc)
215
+ throw lerr('access.bad_account');
216
+ const d = facade();
217
+ const tenant = d.tenant;
218
+ if (!tenant)
219
+ throw lerr('access.no_session_tenant', { what: 'roles' });
220
+ if (roles === undefined) {
221
+ const [m] = await d.member({ User: acc, Tenant: tenant }).rows();
222
+ return m ? [...(m.data.roles ?? [])] : null;
223
+ }
224
+ if (roles === null) {
225
+ // roles(account, null) — снять членство: замысел вызова, без сверки с превью (3.0)
226
+ await inTx('roles', (tr) => tr.member({ User: acc, Tenant: tenant }).delete({ confirm: true, all: true }).rows());
227
+ return null;
228
+ }
229
+ if (!Array.isArray(roles))
230
+ throw lerr('access.roles_not_array');
231
+ const list = roles.map((r) => roleName(r, 'roles'));
232
+ if (new Set(list).size !== list.length)
233
+ throw lerr('access.duplicate_roles');
234
+ await inTx('roles', (tr) => tr.member().upsert({ roles: list }).User.set(acc).Tenant.set(tenant).rows());
235
+ return list;
24
236
  },
25
- });
237
+ async list() {
238
+ const d = facade();
239
+ const tenant = d.tenant?.toLowerCase();
240
+ if (!tenant)
241
+ throw lerr('access.no_session_tenant', { what: 'list' });
242
+ // под RLS строки rule — только арендатора сессии; роль без RLS видит правила всех арендаторов и System
243
+ const rules = (await d.rule().rows()).filter((r) => String(r.tenant).toLowerCase() === tenant);
244
+ const res = new Map((await d.Resource().rows()).map((r) => [r.id, r.data]));
245
+ const out = rules.map((r) => {
246
+ const g = res.get(r.links.Group);
247
+ const o = res.get(r.links.Object);
248
+ let op;
249
+ let target;
250
+ let condition;
251
+ if (o) {
252
+ op = o.category === 'ACCOUNT' ? 'API' : o.category;
253
+ const { class: cls, endpoint, ...rest } = (o.pattern ?? {});
254
+ target = String((op === 'API' ? endpoint : cls) ?? '*');
255
+ if (op !== 'API' && Object.keys(rest).length)
256
+ condition = rest;
257
+ }
258
+ else {
259
+ // ресурс System (grantAll от createTenant): арендатор строку не видит — по id сида
260
+ const a = sys.get(r.links.Object) ?? r.links.Object;
261
+ op = a.split(':').pop() ?? 'READ';
262
+ target = /^(all|any):/.test(a) ? '*' : a.split(':')[0];
263
+ }
264
+ const group = g?.alias ?? sys.get(r.links.Group) ?? r.links.Group;
265
+ return {
266
+ id: r.id, role: g ? roleOf(g.pattern) : null,
267
+ everyone: r.links.Group === AUTHENTICATED || (group === EVERYONE.alias && !sys.has(r.links.Group)), group, op, target,
268
+ ...(condition ? { condition } : {}),
269
+ permission: r.data.permission === 'allow' ? 'allow' : 'deny', weight: Number(r.data.weight ?? 0),
270
+ // как acl_plan_for (50-acl.sql): нет поля — правило действует
271
+ enabled: r.data.enabled !== false,
272
+ };
273
+ });
274
+ const key = (x) => `${x.op}\u0000${x.target}\u0000${x.role ?? x.group}\u0000${x.permission}`;
275
+ return out.sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
276
+ },
277
+ };
278
+ return api;
26
279
  }
280
+ // ---------------------------------------------------------------------------
281
+ // Логирование (3.0): события db.acl — обёртка instrument (log.ts)
282
+ // ---------------------------------------------------------------------------
283
+ const accountOf = (a) => (a !== null && typeof a === 'object' ? a.id : a);
284
+ /** Что пишет в лог каждый метод db.acl: правка прав — acl.change, проверки и список — acl.check. */
285
+ export const ACL_LOG = {
286
+ check: (args, r) => ({ event: 'acl.check', detail: { endpoint: args[0], decision: r } }),
287
+ checkData: (args, r) => ({ event: 'acl.check', detail: { class: args[0], access: args[1], decision: r } }),
288
+ grant: (args, r) => ({ event: 'acl.change', rows: Array.isArray(r) ? r.length : undefined, detail: { action: 'grant', spec: args[0] } }),
289
+ deny: (args, r) => ({ event: 'acl.change', rows: Array.isArray(r) ? r.length : undefined, detail: { action: 'deny', spec: args[0] } }),
290
+ revoke: (args, n) => ({ event: 'acl.change', rows: typeof n === 'number' ? n : undefined, detail: { action: 'revoke', spec: args[0] } }),
291
+ roles: (args, r) => ({ event: args.length > 1 && args[1] !== undefined ? 'acl.change' : 'acl.check',
292
+ detail: { action: 'roles', account: accountOf(args[0]), roles: r } }),
293
+ list: (_args, r) => ({ event: 'acl.check', rows: Array.isArray(r) ? r.length : undefined, detail: { action: 'list' } }),
294
+ };
package/dist/admin.d.ts CHANGED
@@ -1,35 +1,5 @@
1
- /**
2
- * Эксплуатация (план, этап 6, п. 3, 4, 6–8, 12): verify, maintain и его запуск по расписанию, reset,
3
- * обрезка истории, новый ключ класса, describe. Всё проверяет база: функции владельца 90-time.sql
4
- * сами сверяют права (letopis.* — системный администратор или администраторское подключение).
5
- */
6
- import type postgres from 'postgres';
7
- import type { AggregateInfo, Registry } from './registry.js';
1
+ import { type Log } from './log.js';
8
2
  import { type Executor, type TxContext } from './tx.js';
9
- type Sql = postgres.Sql<Record<string, unknown>>;
10
- export interface VerifyIssue {
11
- id: string;
12
- rev?: number;
13
- class?: string;
14
- problem: string;
15
- [k: string]: unknown;
16
- }
17
- export interface VerifyResult {
18
- ok: boolean;
19
- issues: VerifyIssue[];
20
- /** Один хэш текущих версий всех объектов — храните его вне базы: подмену самых свежих версий выдаст его расхождение. */
21
- anchor: string;
22
- }
23
- export interface MaintainOptions {
24
- /** Момент «сейчас» для прореживания — только в установке с `allowReset` (тесты). */
25
- now?: string | Date;
26
- /** Сколько пачек прореживания журнала самое большее сделать за один вызов; каждая пачка — своя транзакция. */
27
- batches?: number;
28
- /** Сколько версий самое большее удалить в одной пачке прореживания. */
29
- limit?: number;
30
- }
31
- /** Внутренняя опция maintain(): остановка запуска по расписанию — текущая пачка доделывается, следующая не начинается. */
32
- export declare const MAINTAIN_STOP: unique symbol;
33
3
  export interface MaintainReport {
34
4
  /** Другой процесс уже обслуживает установку (держит строку-замок обслуживания): этот вызов ничего не делал. */
35
5
  skipped: boolean;
@@ -40,92 +10,24 @@ export interface MaintainReport {
40
10
  * заменённых новым и исчерпавших попытки.
41
11
  */
42
12
  codes: number;
43
- /** Прорежено версий журнала. */
44
- thinned: number;
45
- batches: number;
46
- /** Прореживание не дошло до конца журнала — следующий вызов продолжит. */
47
- more: boolean;
48
13
  /** Заполнение очереди сигналов PostgreSQL (0…1); больше половины — предупреждение. */
49
14
  queue: number;
50
15
  }
51
- export interface ResetOptions {
52
- level: 'sessions' | 'tenant' | 'all';
53
- /** Подтверждение — имя схемы установки. */
54
- confirm: string;
55
- /** Какого арендатора сбросить при `level: 'tenant'` — его id; на других уровнях не учитывается. */
56
- tenant?: string;
57
- }
58
- export interface ResetResult {
59
- level: string;
60
- sessions?: number;
61
- tenant?: string;
62
- rows?: number;
63
- versions?: number;
64
- /** `level: 'all'` — новый ключ сервиса: прежние сессии и ключи больше не действуют. */
65
- serviceKey?: string;
66
- }
67
- /** Машинное описание модели (`db.describe()`). */
68
- export interface ModelDescription {
69
- schema: string;
70
- tenant: string | null;
71
- classes: {
72
- name: string;
73
- kind: 'hub' | 'link';
74
- alias?: string;
75
- abstract: boolean;
76
- parent?: string;
77
- ancestors: string[];
78
- system: boolean;
79
- fields: Record<string, unknown>;
80
- required: string[];
81
- /** Условия по нескольким полям (`fields.anyOf` описания с учётом наследования): хотя бы одно должно выполняться. */
82
- anyOf?: unknown[];
83
- /** Условия по нескольким полям (`fields.oneOf`): ровно одно должно выполняться. */
84
- oneOf?: unknown[];
85
- ends: Record<string, unknown>;
86
- key: string[];
87
- history: unknown;
88
- ownerDefault: string;
89
- cache?: {
90
- ttl?: number;
91
- };
92
- meta: Record<string, unknown>;
93
- /** Производный класс: строки пишет только проектор с этим именем (план 1.1, §2.4). */
94
- derivedBy?: string;
95
- /** Агрегатные поля: сумма или число живых строк класса-источника, их считает база (план 1.1, §2.4). */
96
- aggregate?: Record<string, AggregateInfo>;
97
- }[];
98
- methods: {
99
- chain: readonly string[];
100
- db: readonly string[];
101
- };
102
- }
103
16
  export interface AdminCtx {
104
- sql: Sql;
105
- schema: string;
106
17
  /** Схема в кавычках. */
107
18
  S: string;
108
19
  txc: TxContext;
109
20
  exec: Executor;
110
- registry: () => Registry;
111
- tenant: () => string | null;
112
21
  }
113
- export declare function verify(a: AdminCtx, opts?: {
114
- data?: boolean;
115
- }): Promise<VerifyResult>;
116
22
  /**
117
- * Обслуживание: истёкшие сессии и коды, прореживание журнала пачками (каждая — своя транзакция,
118
- * долгих блокировок нет), заполнение очереди сигналов. Из нескольких процессов работает один: пачка
119
- * первым делом берёт строку-замок обслуживания (lock_try в maintain() базы: for update skip locked строки
120
- * secret, которую посторонняя роль не займёт), и та снимается с фиксацией пачки. Сеансовая блокировка
121
- * за пулом в режиме транзакций (PgBouncer) оставалась бы на
122
- * серверном соединении, которое пул отдаёт другим клиентам, — и обслуживание молча вставало бы
123
- * (skipped у всех). Блокировку держит другой процесс: до первой пачки — skipped; между пачками —
124
- * вызов останавливается (продолжит тот процесс), отчёт — о сделанном, more: true.
23
+ * Обслуживание: истёкшие и погашенные сессии, одноразовые коды, строки-замки удалённых семейств и заполнение очереди
24
+ * сигналов — одной транзакцией. Из нескольких процессов работает один: maintain() базы первым делом берёт строку-замок
25
+ * обслуживания (lock_try: for update skip locked строки secret, которую посторонняя роль не займёт), и та снимается с
26
+ * фиксацией. Сеансовая блокировка за пулом в режиме транзакций (PgBouncer) оставалась бы на серверном соединении,
27
+ * которое пул отдаёт другим клиентам, — и обслуживание молча вставало бы (skipped у всех). Строку-замок держит другой
28
+ * процесс — skipped.
125
29
  */
126
- export declare function maintain(a: AdminCtx, opts?: MaintainOptions & {
127
- [MAINTAIN_STOP]?: () => boolean;
128
- }): Promise<MaintainReport>;
30
+ export declare function maintain(a: AdminCtx): Promise<MaintainReport>;
129
31
  /** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
130
32
  export declare function queueWarning(queue: number): string | null;
131
33
  /**
@@ -136,39 +38,7 @@ export declare function queueWarning(queue: number): string | null;
136
38
  export declare function parseInterval(v: unknown): number;
137
39
  /**
138
40
  * Запуск maintain() по расписанию (connect({ maintain: '1h' }); ms — уже проверенный интервал): ошибки — предупреждением,
139
- * следующий запуск не начинается, пока идёт прежний. Возвращает остановку: она снимает таймер, просит идущий запуск не
140
- * начинать новых пачек и ждёт его конца — close() закрывает пул после неё, а не посреди пачки.
41
+ * следующий запуск не начинается, пока идёт прежний. Возвращает остановку: она снимает таймер и ждёт конца идущего
42
+ * запуска — close() закрывает пул после него, а не посреди транзакции.
141
43
  */
142
- export declare function scheduleMaintain(run: (stopped: () => boolean) => Promise<unknown>, ms: number): () => Promise<void>;
143
- /**
144
- * Сброс (этап 6, п. 6). Предохранители — в базе: флаг allowReset установки, право letopis.reset,
145
- * подтверждение именем схемы. tenant — пачками (длинная операция не держит горизонт подписок).
146
- */
147
- export declare function reset(a: AdminCtx, opts: ResetOptions): Promise<ResetResult>;
148
- /**
149
- * Обрезка истории (этап 6, п. 3): версии строже момента before удаляются, текущая — никогда; на место
150
- * версии 1 встаёт запись trim. target — имя класса (с потомками), id или список id, строка ответа.
151
- */
152
- export declare function trimHistory(a: AdminCtx, target: string | string[] | {
153
- id: string;
154
- } | {
155
- id: string;
156
- }[], before: string | Date): Promise<number>;
157
- /** Новый ключ класса (этап 6, п. 7): строки семейства переносятся на новые id. Возвращает их число. */
158
- export declare function rekeyClass(a: AdminCtx, cls: string, key: string[]): Promise<number>;
159
- /**
160
- * Ремонт агрегатов (план 1.1, §2.4): поля класса и его потомков во всех арендаторах — заново из живых источников,
161
- * под исключительными блокировками семейств целей и источников (запись в них ждёт). Версия пишется только там, где
162
- * значение изменилось; возвращает число исправленных строк. Функция владельца — от его роли (set local role), как
163
- * загрузчик; подключение проверил вызывающий (admin_required).
164
- */
165
- export declare function rebuildAggregates(env: {
166
- sql: Sql;
167
- S: string;
168
- ownerRole: string;
169
- }, cls: string): Promise<number>;
170
- /** Машинное описание модели арендатора вместе с метаданными классов (этап 6, п. 12). */
171
- export declare function describe(a: AdminCtx): ModelDescription;
172
- /** Описание модели текстом для языковых моделей. */
173
- export declare function describeText(d: ModelDescription): string;
174
- export {};
44
+ export declare function scheduleMaintain(run: () => Promise<unknown>, ms: number, log?: Log): () => Promise<void>;