letopis 0.20.3 → 1.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.
- package/AGENT-CHEATSHEET.en.md +368 -0
- package/AGENT-CHEATSHEET.md +354 -0
- package/CHANGELOG.md +348 -0
- package/MIGRATION.md +190 -0
- package/README.en.md +1937 -0
- package/README.md +1493 -3466
- package/dist/acl.d.ts +26 -50
- package/dist/acl.js +22 -267
- package/dist/admin.d.ts +138 -0
- package/dist/admin.js +170 -0
- package/dist/auth.d.ts +120 -73
- package/dist/auth.js +121 -306
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +148 -0
- package/dist/chain.d.ts +124 -191
- package/dist/chain.js +369 -551
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/demo/booking.d.ts +289 -0
- package/dist/demo/booking.js +159 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +70 -0
- package/dist/import.d.ts +179 -0
- package/dist/import.js +792 -0
- package/dist/index.d.ts +172 -26
- package/dist/index.js +304 -178
- package/dist/jsonschema.d.ts +22 -0
- package/dist/jsonschema.js +167 -0
- package/dist/load.d.ts +76 -0
- package/dist/load.js +884 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.js +224 -0
- package/dist/ops.d.ts +7 -6
- package/dist/ops.js +7 -51
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.js +45 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +82 -0
- package/dist/sql.d.ts +59 -142
- package/dist/sql.js +568 -654
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +108 -0
- package/dist/tx.d.ts +129 -8
- package/dist/tx.js +300 -73
- package/dist/typed.d.ts +97 -0
- package/dist/typed.js +1 -0
- package/dist/types.d.ts +71 -250
- package/dist/types.js +27 -108
- package/dist/up.d.ts +140 -47
- package/dist/up.js +339 -267
- package/dist/uuid.d.ts +21 -6
- package/dist/uuid.js +48 -64
- package/dist/validate.d.ts +24 -0
- package/dist/validate.js +251 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +168 -0
- package/dist/write.d.ts +117 -74
- package/dist/write.js +658 -720
- package/llms.txt +26 -0
- package/package.json +49 -19
- package/sql/10-core.sql +136 -0
- package/sql/15-errors.sql +60 -0
- package/sql/20-context.sql +153 -0
- package/sql/30-validate.sql +423 -0
- package/sql/40-class.sql +259 -0
- package/sql/50-acl.sql +539 -0
- package/sql/60-write.sql +1369 -0
- package/sql/70-read.sql +245 -0
- package/sql/80-auth.sql +827 -0
- package/sql/90-time.sql +957 -0
- package/sql/95-seed.system.sql +178 -0
- package/sql/99-revision.sql +3 -0
- package/sql/README.md +56 -0
- package/sql/seed.booking.sql +39 -112
- package/dist/schema.d.ts +0 -15
- package/dist/schema.js +0 -351
- package/dist/sessions.d.ts +0 -32
- package/dist/sessions.js +0 -114
- package/dist/tables.d.ts +0 -105
- package/dist/tables.js +0 -248
- package/docker/Dockerfile +0 -40
- package/docker/start.sh +0 -18
- package/scripts/check-docs.mjs +0 -375
- package/scripts/gen-api-contract.mjs +0 -226
- package/scripts/gen-types.mjs +0 -350
- package/scripts/release-notes.mjs +0 -76
- package/scripts/schema-sync.mjs +0 -185
- package/sql/ddl.sql +0 -600
- package/sql/seed.auth.sql +0 -73
package/dist/acl.d.ts
CHANGED
|
@@ -1,53 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* category='READ' | 'WRITE' | 'DELETE' — ШАБЛОН СТРОКИ Entity (операция = категория):
|
|
9
|
-
* ключи — реальные колонки (class, owner, account, tags, data, links, id, partition),
|
|
10
|
-
* значения — литералы или "$account" (id текущего аккаунта, подставляется в запрос).
|
|
11
|
-
* "class" — маска с lineage (класс или любой его предок); нет ключа/NULL — все строки.
|
|
12
|
-
* Остальные ключи победившего allow-правила вливаются в SQL ДО сортировки и лимита —
|
|
13
|
-
* пагинация/count/keyset работают по уже суженному множеству.
|
|
14
|
-
*
|
|
15
|
-
* Rule(субъект-alias → объект-alias, permission allow|deny, weight): правила никогда не
|
|
16
|
-
* указывают на конкретный аккаунт — принадлежность группе вычисляется из categories.
|
|
17
|
-
* Побеждает РОВНО ОДНО правило: max weight; при равном весе deny бьёт allow (равные
|
|
18
|
-
* allow — детерминированно по alias ресурса); НЕТ правил — deny (deny-by-default).
|
|
19
|
-
*/
|
|
20
|
-
import type { AclOp, AclDecision, ClassDef, Resource, Rule } from './types.js';
|
|
21
|
-
import type { Registry } from './schema.js';
|
|
22
|
-
import type { Tables } from './tables.js';
|
|
23
|
-
export declare const ACL_OPS: readonly AclOp[];
|
|
24
|
-
/** pattern.categories: "{A,B}" | "!{A,B}" | отсутствие/NULL (матч всех). */
|
|
25
|
-
export declare function matchCategories(pattern: Record<string, unknown> | null, cats: string[]): boolean;
|
|
26
|
-
export declare function matchMask(mask: string, value: string): boolean;
|
|
27
|
-
export interface AclApi {
|
|
28
|
-
/** Может ли аккаунт дёрнуть эндпоинт (Resource category='API'). */
|
|
29
|
-
check(account: string | {
|
|
30
|
-
id: string;
|
|
31
|
-
}, endpoint: string): Promise<AclDecision>;
|
|
32
|
-
/** Решение операции над классом: allow + остаточный шаблон строк (filter) | deny. */
|
|
33
|
-
checkData(account: string | {
|
|
1
|
+
import type { Executor } from './tx.js';
|
|
2
|
+
export type AclOp = 'READ' | 'WRITE' | 'DELETE';
|
|
3
|
+
/** Решение по эндпоинту API. */
|
|
4
|
+
export interface AclDecision {
|
|
5
|
+
allow: boolean;
|
|
6
|
+
/** Победившее правило: id, группа и объект (alias ресурсов), вес, разрешение. */
|
|
7
|
+
rule?: {
|
|
34
8
|
id: string;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
9
|
+
group: string;
|
|
10
|
+
object: string;
|
|
11
|
+
weight: number;
|
|
12
|
+
permission: 'allow' | 'deny';
|
|
13
|
+
};
|
|
14
|
+
/** Код и сообщение запрещающего правила (поля code, message строки rule). */
|
|
15
|
+
code?: number;
|
|
16
|
+
message?: string;
|
|
42
17
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
18
|
+
/** Решение по операции над классом: разрешено и, если есть, условие на строки (шаблон колонок). */
|
|
19
|
+
export interface AclDataDecision {
|
|
20
|
+
allow: boolean;
|
|
21
|
+
filter?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface AclApi {
|
|
24
|
+
/** Может ли актор вызвать эндпоинт (ресурсы категории API; правила System и арендатора). */
|
|
25
|
+
check(endpoint: string): Promise<AclDecision>;
|
|
26
|
+
/** Решение операции над классом для актора: allow и условие на строки (filter). */
|
|
27
|
+
checkData(className: string, op: AclOp): Promise<AclDataDecision>;
|
|
46
28
|
}
|
|
47
|
-
export declare function makeAcl(
|
|
48
|
-
/** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
|
|
49
|
-
* компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
|
|
50
|
-
export declare function compileEnforcer(src: AclSource, account: string, cats: string[], registry: Registry): (cls: ClassDef, op: AclOp) => AclDecision;
|
|
51
|
-
/** Читаемая ошибка отказа для цепочек. */
|
|
52
|
-
export declare function aclDenied(op: AclOp, clsId: string, d: AclDecision): Error;
|
|
53
|
-
export {};
|
|
29
|
+
export declare function makeAcl(S: string, exec: Executor): AclApi;
|
package/dist/acl.js
CHANGED
|
@@ -1,271 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (typeof spec !== 'string')
|
|
14
|
-
return true;
|
|
15
|
-
const neg = spec.startsWith('!');
|
|
16
|
-
const list = spec
|
|
17
|
-
.replace(/^!/, '')
|
|
18
|
-
.replace(/^\{/, '')
|
|
19
|
-
.replace(/\}$/, '')
|
|
20
|
-
.split(',')
|
|
21
|
-
.map((s) => s.trim())
|
|
22
|
-
.filter(Boolean);
|
|
23
|
-
const hit = list.some((c) => cats.includes(c));
|
|
24
|
-
return neg ? !hit : hit;
|
|
25
|
-
}
|
|
26
|
-
// START_CONTRACT: maskBody
|
|
27
|
-
// PURPOSE: Скомпилировать тело маски в regex-фрагмент: '{a,b}' → альтернативы (рекурсивно), '*' → хвост из 1+ сегментов.
|
|
28
|
-
// INPUTS: { mask: string - маска с '.'-сегментами }
|
|
29
|
-
// OUTPUTS: { string - regex-исходник без якорей }
|
|
30
|
-
// SIDE_EFFECTS: none
|
|
31
|
-
// LINKS: M-ACL, V-M-ACL
|
|
32
|
-
// END_CONTRACT: maskBody
|
|
33
|
-
/** Маска → regex: '{a,b}' — альтернативы (рекурсивно), '*' — хвост из 1+ сегментов. */
|
|
34
|
-
function maskBody(mask) {
|
|
35
|
-
// START_BLOCK_MASK_COMPILE
|
|
36
|
-
let out = '';
|
|
37
|
-
for (let i = 0; i < mask.length; i++) {
|
|
38
|
-
const ch = mask[i];
|
|
39
|
-
if (ch === '{') {
|
|
40
|
-
let depth = 1;
|
|
41
|
-
let j = i + 1;
|
|
42
|
-
for (; j < mask.length && depth; j++) {
|
|
43
|
-
if (mask[j] === '{')
|
|
44
|
-
depth++;
|
|
45
|
-
else if (mask[j] === '}')
|
|
46
|
-
depth--;
|
|
47
|
-
}
|
|
48
|
-
const inner = mask.slice(i + 1, j - 1);
|
|
49
|
-
const parts = [];
|
|
50
|
-
let d = 0;
|
|
51
|
-
let start = 0;
|
|
52
|
-
for (let k = 0; k <= inner.length; k++) {
|
|
53
|
-
const c = inner[k];
|
|
54
|
-
if (k === inner.length || (c === ',' && d === 0)) {
|
|
55
|
-
parts.push(inner.slice(start, k));
|
|
56
|
-
start = k + 1;
|
|
57
|
-
}
|
|
58
|
-
else if (c === '{')
|
|
59
|
-
d++;
|
|
60
|
-
else if (c === '}')
|
|
61
|
-
d--;
|
|
62
|
-
}
|
|
63
|
-
out += '(?:' + parts.map(maskBody).join('|') + ')';
|
|
64
|
-
i = j - 1;
|
|
65
|
-
}
|
|
66
|
-
else if (ch === '*') {
|
|
67
|
-
out += '[^.]+(?:\\.[^.]+)*';
|
|
68
|
-
}
|
|
69
|
-
else if ('.\\+?()[]^$|'.includes(ch)) {
|
|
70
|
-
out += '\\' + ch;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
out += ch;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// END_BLOCK_MASK_COMPILE
|
|
77
|
-
return out;
|
|
78
|
-
}
|
|
79
|
-
const regexCache = new Map();
|
|
80
|
-
// START_CONTRACT: matchMask
|
|
81
|
-
// PURPOSE: Сопоставить значение с маской, кэшируя скомпилированный RegExp.
|
|
82
|
-
// INPUTS: { mask: string; value: string }
|
|
83
|
-
// OUTPUTS: { boolean }
|
|
84
|
-
// SIDE_EFFECTS: заполняет regexCache
|
|
85
|
-
// LINKS: M-ACL, V-M-ACL
|
|
86
|
-
// END_CONTRACT: matchMask
|
|
87
|
-
export function matchMask(mask, value) {
|
|
88
|
-
let re = regexCache.get(mask);
|
|
89
|
-
if (!re) {
|
|
90
|
-
re = new RegExp(`^${maskBody(mask)}$`);
|
|
91
|
-
regexCache.set(mask, re);
|
|
92
|
-
}
|
|
93
|
-
return re.test(value);
|
|
94
|
-
}
|
|
95
|
-
// --- резолюция ----------------------------------------------------------------
|
|
96
|
-
/** Все имена, под которыми класс виден правилам: id+алиас его самого и каждого предка. */
|
|
97
|
-
function classNames(cls, registry) {
|
|
98
|
-
const names = [];
|
|
99
|
-
for (const id of cls.ancestors) {
|
|
100
|
-
names.push(id);
|
|
101
|
-
try {
|
|
102
|
-
const a = registry.resolve(id).alias;
|
|
103
|
-
if (a && a !== id)
|
|
104
|
-
names.push(a);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* предок вне реестра — пропуск */
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return names;
|
|
111
|
-
}
|
|
112
|
-
const matchesApi = (r, endpoint) => {
|
|
113
|
-
if (r.category !== 'API')
|
|
114
|
-
return false;
|
|
115
|
-
if (r.pattern == null)
|
|
116
|
-
return true;
|
|
117
|
-
const m = r.pattern.endpoint;
|
|
118
|
-
return typeof m === 'string' && matchMask(m, endpoint);
|
|
119
|
-
};
|
|
120
|
-
/** Ресурс операции op применим к классу? (class-ключ — маска с lineage; нет ключа — все). */
|
|
121
|
-
const matchesRow = (r, op, names) => {
|
|
122
|
-
if (r.category !== op)
|
|
123
|
-
return false;
|
|
124
|
-
if (r.pattern == null)
|
|
125
|
-
return true;
|
|
126
|
-
const clsMask = r.pattern.class;
|
|
127
|
-
return typeof clsMask !== 'string' || names.some((n) => matchMask(clsMask, n));
|
|
128
|
-
};
|
|
129
|
-
/** "$account" → id субъекта, рекурсивно по значениям шаблона. */
|
|
130
|
-
function substitute(v, account) {
|
|
131
|
-
if (v === '$account')
|
|
132
|
-
return account;
|
|
133
|
-
if (Array.isArray(v))
|
|
134
|
-
return v.map((x) => substitute(x, account));
|
|
135
|
-
if (v && typeof v === 'object') {
|
|
136
|
-
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, substitute(x, account)]));
|
|
137
|
-
}
|
|
138
|
-
return v;
|
|
139
|
-
}
|
|
140
|
-
/** Остаточный шаблон строки победившего allow: все ключи, кроме class, с подстановкой. */
|
|
141
|
-
function rowFilter(r, account) {
|
|
142
|
-
if (r.pattern == null)
|
|
143
|
-
return undefined;
|
|
144
|
-
const rest = Object.entries(r.pattern).filter(([k]) => k !== 'class');
|
|
145
|
-
if (!rest.length)
|
|
146
|
-
return undefined;
|
|
147
|
-
return Object.fromEntries(rest.map(([k, v]) => [k, substitute(v, account)]));
|
|
148
|
-
}
|
|
149
|
-
// START_CONTRACT: decide
|
|
150
|
-
// PURPOSE: Выбрать РОВНО ОДНО победившее правило: max weight; при равенстве deny бьёт allow, равные allow — по alias ресурса.
|
|
151
|
-
// INPUTS: { rules: Rule[]; subjects: Set<string> - alias'ы субъекта; objects: Map<string,Resource> - применимые ресурсы }
|
|
152
|
-
// OUTPUTS: { { winner?: Rule } }
|
|
153
|
-
// SIDE_EFFECTS: none
|
|
154
|
-
// LINKS: M-ACL, V-M-ACL
|
|
155
|
-
// END_CONTRACT: decide
|
|
156
|
-
/** Победитель: max weight; равный вес — deny бьёт allow, равные allow — по alias объекта. */
|
|
157
|
-
function decide(rules, subjects, objects) {
|
|
158
|
-
// START_BLOCK_DECIDE_WINNER
|
|
159
|
-
let winner;
|
|
160
|
-
for (const rule of rules) {
|
|
161
|
-
if (!rule.enabled || !subjects.has(rule.account) || !objects.has(rule.resource))
|
|
162
|
-
continue;
|
|
163
|
-
if (!winner) {
|
|
164
|
-
winner = rule;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const w = rule.weight ?? 0;
|
|
168
|
-
const ww = winner.weight ?? 0;
|
|
169
|
-
if (w > ww ||
|
|
170
|
-
(w === ww && winner.permission === 'allow' && rule.permission !== 'allow') ||
|
|
171
|
-
(w === ww && winner.permission === rule.permission && rule.resource < winner.resource)) {
|
|
172
|
-
winner = rule;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// END_BLOCK_DECIDE_WINNER
|
|
176
|
-
return { winner };
|
|
177
|
-
}
|
|
178
|
-
// START_CONTRACT: toDecision
|
|
179
|
-
// PURPOSE: Превратить победителя в AclDecision (allow + остаточный row-фильтр | deny + code/message | deny-by-default).
|
|
180
|
-
// INPUTS: { winner?: Rule; objects: Map<string,Resource>; account: string }
|
|
181
|
-
// OUTPUTS: { AclDecision }
|
|
182
|
-
// SIDE_EFFECTS: none
|
|
183
|
-
// LINKS: M-ACL, V-M-ACL, type-AclDecision
|
|
184
|
-
// END_CONTRACT: toDecision
|
|
185
|
-
function toDecision(winner, objects, account) {
|
|
186
|
-
if (!winner)
|
|
187
|
-
return { allow: false, message: 'no matching rule (deny by default)' };
|
|
188
|
-
if (winner.permission !== 'allow') {
|
|
189
|
-
const meta = (winner.meta ?? {});
|
|
190
|
-
return { allow: false, rule: winner, code: meta.code, message: meta.message };
|
|
191
|
-
}
|
|
192
|
-
const res = objects.get(winner.resource);
|
|
193
|
-
// остаточный шаблон строк — только у ресурсов-операций (у API pattern — не строки Entity)
|
|
194
|
-
const filter = ACL_OPS.includes(res.category) ? rowFilter(res, account) : undefined;
|
|
195
|
-
return filter ? { allow: true, rule: winner, filter } : { allow: true, rule: winner };
|
|
196
|
-
}
|
|
197
|
-
const subjectAliases = (resources, cats) => new Set(resources.filter((r) => r.category === 'ACCOUNT' && matchCategories(r.pattern, cats)).map((r) => r.alias));
|
|
198
|
-
const toMap = (rs) => new Map(rs.map((r) => [r.alias, r]));
|
|
199
|
-
const accId = (a) => (typeof a === 'object' ? a.id : a);
|
|
200
|
-
// START_CONTRACT: makeAcl
|
|
201
|
-
// PURPOSE: Построить рантайм-фасад AclApi (check по эндпоинту / checkData по классу+op / reload) с кэшом Resource/Rule.
|
|
202
|
-
// INPUTS: { tables: Tables; registry: Registry }
|
|
203
|
-
// OUTPUTS: { AclApi }
|
|
204
|
-
// SIDE_EFFECTS: лениво читает resources/rules через tables (кэш до reload())
|
|
205
|
-
// LINKS: M-ACL, V-M-ACL
|
|
206
|
-
// END_CONTRACT: makeAcl
|
|
207
|
-
export function makeAcl(tables, registry) {
|
|
208
|
-
let cache = null;
|
|
209
|
-
const load = () => (cache ??= (async () => ({
|
|
210
|
-
resources: await tables.resources.find(),
|
|
211
|
-
rules: await tables.rules.find({ enabled: true }),
|
|
212
|
-
}))());
|
|
213
|
-
const catsOf = async (account) => (await tables.accounts.get(accId(account)))?.categories ?? [];
|
|
214
|
-
return {
|
|
215
|
-
async check(account, endpoint) {
|
|
216
|
-
const [{ resources, rules }, cats] = await Promise.all([load(), catsOf(account)]);
|
|
217
|
-
const objects = toMap(resources.filter((r) => matchesApi(r, endpoint)));
|
|
218
|
-
const { winner } = decide(rules, subjectAliases(resources, cats), objects);
|
|
219
|
-
return toDecision(winner, objects, accId(account));
|
|
220
|
-
},
|
|
221
|
-
async checkData(account, className, op) {
|
|
222
|
-
const [{ resources, rules }, cats] = await Promise.all([load(), catsOf(account)]);
|
|
223
|
-
const names = classNames(registry.resolve(className), registry);
|
|
224
|
-
const objects = toMap(resources.filter((r) => matchesRow(r, op, names)));
|
|
225
|
-
const { winner } = decide(rules, subjectAliases(resources, cats), objects);
|
|
226
|
-
return toDecision(winner, objects, accId(account));
|
|
1
|
+
/**
|
|
2
|
+
* db.acl — решения прав для приложения (план, §2.8, этап 5, п. 7). Сами права применяет база
|
|
3
|
+
* (политики RLS по плану прав, 50-acl.sql); эти вызовы отвечают «можно ли» заранее — для интерфейса
|
|
4
|
+
* и эндпоинтов приложения. Правила — строки Resource и rule (группа → объект, allow | deny, вес);
|
|
5
|
+
* решение — ровно одно правило с наибольшим весом, при равном весе запрет сильнее, нет правил — запрет.
|
|
6
|
+
*/
|
|
7
|
+
import { removed } from './errors.js';
|
|
8
|
+
export function makeAcl(S, exec) {
|
|
9
|
+
const api = {
|
|
10
|
+
async check(endpoint) {
|
|
11
|
+
const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check($1) as d`, [endpoint]), { read: true });
|
|
12
|
+
return r.d;
|
|
227
13
|
},
|
|
228
|
-
|
|
229
|
-
|
|
14
|
+
async checkData(className, op) {
|
|
15
|
+
const [r] = await exec.run((q) => q.unsafe(`select ${S}.acl_check_data($1, $2) as d`, [className, op]), { read: true });
|
|
16
|
+
return r.d;
|
|
230
17
|
},
|
|
231
18
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// LINKS: M-ACL, V-M-ACL, M-SQL, M-WRITE
|
|
240
|
-
// END_CONTRACT: compileEnforcer
|
|
241
|
-
/** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
|
|
242
|
-
* компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
|
|
243
|
-
export function compileEnforcer(src, account, cats, registry) {
|
|
244
|
-
// START_BLOCK_ENFORCER_MEMO
|
|
245
|
-
const subjects = subjectAliases(src.resources, cats);
|
|
246
|
-
const memo = new Map();
|
|
247
|
-
return (cls, op) => {
|
|
248
|
-
const key = `${cls.id} ${op}`;
|
|
249
|
-
let d = memo.get(key);
|
|
250
|
-
if (!d) {
|
|
251
|
-
const names = classNames(cls, registry);
|
|
252
|
-
const objects = toMap(src.resources.filter((r) => matchesRow(r, op, names)));
|
|
253
|
-
const { winner } = decide(src.rules, subjects, objects);
|
|
254
|
-
d = toDecision(winner, objects, account);
|
|
255
|
-
memo.set(key, d);
|
|
256
|
-
}
|
|
257
|
-
return d;
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
// END_BLOCK_ENFORCER_MEMO
|
|
261
|
-
// START_CONTRACT: aclDenied
|
|
262
|
-
// PURPOSE: Собрать читаемую Error отказа ACL для цепочек (её бросают вызывающие sql/write).
|
|
263
|
-
// INPUTS: { op: AclOp; clsId: string; d: AclDecision }
|
|
264
|
-
// OUTPUTS: { Error }
|
|
265
|
-
// SIDE_EFFECTS: none (возвращает Error, не бросает)
|
|
266
|
-
// LINKS: M-ACL, V-M-ACL
|
|
267
|
-
// END_CONTRACT: aclDenied
|
|
268
|
-
/** Читаемая ошибка отказа для цепочек. */
|
|
269
|
-
export function aclDenied(op, clsId, d) {
|
|
270
|
-
return new Error(`letopis: acl denies ${op} on ${clsId}${d.message ? ` — ${d.message}` : ''}`);
|
|
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);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
271
26
|
}
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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 { Registry } from './registry.js';
|
|
8
|
+
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
|
+
/** Пачек прореживания за вызов (по умолчанию 100); каждая — своя транзакция. */
|
|
27
|
+
batches?: number;
|
|
28
|
+
/** Версий в пачке (по умолчанию 10 000). */
|
|
29
|
+
limit?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MaintainReport {
|
|
32
|
+
/** Другой процесс уже обслуживает установку (advisory-блокировка): этот вызов ничего не делал. */
|
|
33
|
+
skipped: boolean;
|
|
34
|
+
/** Удалено истёкших и погашенных сессий и одноразовых кодов. */
|
|
35
|
+
sessions: number;
|
|
36
|
+
codes: number;
|
|
37
|
+
/** Прорежено версий журнала. */
|
|
38
|
+
thinned: number;
|
|
39
|
+
batches: number;
|
|
40
|
+
/** Прореживание не дошло до конца журнала — следующий вызов продолжит. */
|
|
41
|
+
more: boolean;
|
|
42
|
+
/** Заполнение очереди сигналов PostgreSQL (0…1); больше половины — предупреждение. */
|
|
43
|
+
queue: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ResetOptions {
|
|
46
|
+
level: 'sessions' | 'tenant' | 'all';
|
|
47
|
+
/** Подтверждение — имя схемы установки. */
|
|
48
|
+
confirm: string;
|
|
49
|
+
/** level: 'tenant' — арендатор (по умолчанию — арендатор сессии). */
|
|
50
|
+
tenant?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ResetResult {
|
|
53
|
+
level: string;
|
|
54
|
+
sessions?: number;
|
|
55
|
+
tenant?: string;
|
|
56
|
+
rows?: number;
|
|
57
|
+
versions?: number;
|
|
58
|
+
/** level: 'all' — новый ключ сервиса: прежние сессии и ключи больше не действуют. */
|
|
59
|
+
serviceKey?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Машинное описание модели (db.describe()). */
|
|
62
|
+
export interface ModelDescription {
|
|
63
|
+
schema: string;
|
|
64
|
+
tenant: string | null;
|
|
65
|
+
classes: {
|
|
66
|
+
name: string;
|
|
67
|
+
kind: 'hub' | 'link';
|
|
68
|
+
alias?: string;
|
|
69
|
+
abstract: boolean;
|
|
70
|
+
parent?: string;
|
|
71
|
+
ancestors: string[];
|
|
72
|
+
system: boolean;
|
|
73
|
+
fields: Record<string, unknown>;
|
|
74
|
+
required: string[];
|
|
75
|
+
ends: Record<string, unknown>;
|
|
76
|
+
key: string[];
|
|
77
|
+
history: unknown;
|
|
78
|
+
ownerDefault: string;
|
|
79
|
+
cache?: {
|
|
80
|
+
ttl?: number;
|
|
81
|
+
};
|
|
82
|
+
meta: Record<string, unknown>;
|
|
83
|
+
}[];
|
|
84
|
+
methods: {
|
|
85
|
+
chain: readonly string[];
|
|
86
|
+
db: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export interface AdminCtx {
|
|
90
|
+
sql: Sql;
|
|
91
|
+
schema: string;
|
|
92
|
+
/** Схема в кавычках. */
|
|
93
|
+
S: string;
|
|
94
|
+
txc: TxContext;
|
|
95
|
+
exec: Executor;
|
|
96
|
+
registry: () => Registry;
|
|
97
|
+
tenant: () => string | null;
|
|
98
|
+
}
|
|
99
|
+
export declare function verify(a: AdminCtx, opts?: {
|
|
100
|
+
data?: boolean;
|
|
101
|
+
}): Promise<VerifyResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Обслуживание: истёкшие сессии и коды, прореживание журнала пачками (каждая — своя транзакция,
|
|
104
|
+
* долгих блокировок нет), заполнение очереди сигналов. Из нескольких процессов работает один: пачка
|
|
105
|
+
* первым делом берёт транзакционную advisory-блокировку (pg_try_advisory_xact_lock), и та снимается
|
|
106
|
+
* с фиксацией пачки. Сеансовая блокировка за пулом в режиме транзакций (PgBouncer) оставалась бы на
|
|
107
|
+
* серверном соединении, которое пул отдаёт другим клиентам, — и обслуживание молча вставало бы
|
|
108
|
+
* (skipped у всех). Блокировку держит другой процесс: до первой пачки — skipped; между пачками —
|
|
109
|
+
* вызов останавливается (продолжит тот процесс), отчёт — о сделанном, more: true.
|
|
110
|
+
*/
|
|
111
|
+
export declare function maintain(a: AdminCtx, opts?: MaintainOptions): Promise<MaintainReport>;
|
|
112
|
+
/** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
|
|
113
|
+
export declare function queueWarning(queue: number): string | null;
|
|
114
|
+
/** Интервал расписания: число мс или '30s', '15m', '1h', '1d'. */
|
|
115
|
+
export declare function parseInterval(v: string | number): number;
|
|
116
|
+
/** Запуск maintain() по расписанию (connect({ maintain: '1h' })): ошибки — предупреждением. */
|
|
117
|
+
export declare function scheduleMaintain(run: () => Promise<unknown>, every: string | number): () => void;
|
|
118
|
+
/**
|
|
119
|
+
* Сброс (этап 6, п. 6). Предохранители — в базе: флаг allowReset установки, право letopis.reset,
|
|
120
|
+
* подтверждение именем схемы. tenant — пачками (длинная операция не держит горизонт подписок).
|
|
121
|
+
*/
|
|
122
|
+
export declare function reset(a: AdminCtx, opts: ResetOptions): Promise<ResetResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Обрезка истории (этап 6, п. 3): версии строже момента before удаляются, текущая — никогда; на место
|
|
125
|
+
* версии 1 встаёт запись trim. target — имя класса (с потомками), id или список id, строка ответа.
|
|
126
|
+
*/
|
|
127
|
+
export declare function trimHistory(a: AdminCtx, target: string | string[] | {
|
|
128
|
+
id: string;
|
|
129
|
+
} | {
|
|
130
|
+
id: string;
|
|
131
|
+
}[], before: string | Date): Promise<number>;
|
|
132
|
+
/** Новый ключ класса (этап 6, п. 7): строки семейства переносятся на новые id. Возвращает их число. */
|
|
133
|
+
export declare function rekeyClass(a: AdminCtx, cls: string, key: string[]): Promise<number>;
|
|
134
|
+
/** Машинное описание модели арендатора вместе с метаданными классов (этап 6, п. 12). */
|
|
135
|
+
export declare function describe(a: AdminCtx): ModelDescription;
|
|
136
|
+
/** Описание модели текстом для языковых моделей (llms.txt). */
|
|
137
|
+
export declare function describeText(d: ModelDescription): string;
|
|
138
|
+
export {};
|