letopis 2.0.0 → 3.0.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 (83) hide show
  1. package/AGENT-CHEATSHEET.md +491 -414
  2. package/CHANGELOG.md +361 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +969 -210
  6. package/api-contract.json +385 -0
  7. package/dist/acl.d.ts +7 -2
  8. package/dist/acl.js +19 -2
  9. package/dist/admin.d.ts +7 -1
  10. package/dist/admin.js +16 -9
  11. package/dist/auth.d.ts +47 -11
  12. package/dist/auth.js +129 -15
  13. package/dist/chain.d.ts +52 -25
  14. package/dist/chain.js +445 -64
  15. package/dist/cli.js +72 -20
  16. package/dist/errors.d.ts +23 -6
  17. package/dist/errors.js +52 -13
  18. package/dist/index.d.ts +32 -15
  19. package/dist/index.js +209 -68
  20. package/dist/internal.d.ts +2 -0
  21. package/dist/load.d.ts +10 -6
  22. package/dist/load.js +30 -11
  23. package/dist/log.d.ts +121 -0
  24. package/dist/log.js +654 -0
  25. package/dist/mcp.d.ts +1 -1
  26. package/dist/mcp.js +34 -16
  27. package/dist/messages/access.d.ts +10 -0
  28. package/dist/messages/access.js +13 -0
  29. package/dist/messages/api.d.ts +76 -0
  30. package/dist/messages/api.js +109 -7
  31. package/dist/messages/boot.d.ts +5 -0
  32. package/dist/messages/boot.js +11 -4
  33. package/dist/messages/codes.js +4 -4
  34. package/dist/messages/index.d.ts +5 -2
  35. package/dist/messages/index.js +5 -2
  36. package/dist/messages/log.d.ts +232 -0
  37. package/dist/messages/log.js +105 -0
  38. package/dist/messages/mcp.js +7 -7
  39. package/dist/messages/model.d.ts +40 -1
  40. package/dist/messages/model.js +52 -3
  41. package/dist/messages/query.d.ts +10 -0
  42. package/dist/messages/query.js +13 -0
  43. package/dist/messages/sql-core.d.ts +15 -1
  44. package/dist/messages/sql-core.js +20 -3
  45. package/dist/messages/sql-time.d.ts +15 -0
  46. package/dist/messages/sql-time.js +23 -0
  47. package/dist/messages/write.d.ts +52 -0
  48. package/dist/messages/write.js +77 -6
  49. package/dist/model.d.ts +10 -2
  50. package/dist/model.js +28 -6
  51. package/dist/registry.d.ts +6 -0
  52. package/dist/registry.js +6 -0
  53. package/dist/sql.d.ts +2 -0
  54. package/dist/sql.js +77 -25
  55. package/dist/sync.d.ts +4 -5
  56. package/dist/sync.js +53 -4
  57. package/dist/tx.d.ts +10 -1
  58. package/dist/tx.js +44 -2
  59. package/dist/typed.d.ts +18 -14
  60. package/dist/types.d.ts +91 -8
  61. package/dist/types.js +8 -3
  62. package/dist/up.d.ts +8 -5
  63. package/dist/up.js +71 -23
  64. package/dist/uuid.d.ts +8 -0
  65. package/dist/uuid.js +9 -1
  66. package/dist/validate.js +24 -7
  67. package/dist/watch.d.ts +5 -0
  68. package/dist/watch.js +13 -0
  69. package/dist/write.d.ts +84 -3
  70. package/dist/write.js +427 -40
  71. package/llms.txt +38 -17
  72. package/package.json +12 -6
  73. package/sql/20-context.sql +11 -3
  74. package/sql/30-validate.sql +51 -10
  75. package/sql/40-class.sql +21 -1
  76. package/sql/60-write.sql +18 -6
  77. package/sql/80-auth.sql +88 -17
  78. package/sql/90-time.sql +34 -6
  79. package/sql/99-revision.sql +5 -1
  80. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  81. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  82. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  83. package/sql/migrate/README.md +5 -1
package/dist/load.js CHANGED
@@ -22,13 +22,16 @@ import { LetopisError, checkOptions, fromDbError, issuesFromDb, lerr, lmsg } fro
22
22
  import { isOp } from './ops.js';
23
23
  import { Registry } from './registry.js';
24
24
  import { accepts } from './sql.js';
25
- import { v5Id } from './uuid.js';
25
+ import { UUID_RE, v5Id } from './uuid.js';
26
26
  import { applyDefaults, leavesToDb } from './validate.js';
27
27
  import { isInc } from './write.js';
28
28
  import { ACL_CLASS_NAMES } from './types.js';
29
29
  import { isConnectionError, release, rollbackAfter } from './tx.js';
30
30
  const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
31
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
31
+ const UUID = UUID_RE;
32
+ function sameList(a, b) {
33
+ return a.length === b.length && a.every((x, k) => x === b[k]);
34
+ }
32
35
  /**
33
36
  * Все опции load(): неизвестная — invalid_data с похожими (checkOptions; план 2.0, 7.10 №4). Фасад db.load() сверяет их
34
37
  * до пула администратора (index.ts), load() — ещё раз до проверки прав.
@@ -523,6 +526,14 @@ export async function load(env, input, opts = {}) {
523
526
  for (const r of roots)
524
527
  await conn.unsafe(`select ${S}.family_lock(${S}.class_row($1, $2::uuid), false)`, [r, tenant]);
525
528
  const reg = await readRegistry(conn);
529
+ // класс строки, удалённый между первым чтением реестра и блокировкой семейства, — отказ, а не прежнее описание
530
+ for (const i of portion) {
531
+ const n = p1[i].cls.name;
532
+ if (!reg.find(n))
533
+ issues.add({ line: i + 1, code: 'invalid_class', message: lmsg('write.load_class_not_found', { name: n }), id: p1[i].id, class: n });
534
+ }
535
+ if (issues.any)
536
+ issues.fail();
526
537
  const [{ ts, xid }] = await conn.unsafe(`select clock_timestamp()::text as ts, pg_current_xact_id()::text as xid`);
527
538
  // от имени пользователя: арендатор доступен ему, планы прав READ и WRITE; класс без права — отказ
528
539
  if (asAcc) {
@@ -618,10 +629,11 @@ export async function load(env, input, opts = {}) {
618
629
  const found = new Map((await conn.unsafe(`select e.id, e.tenant, e.class from ${S}.entity e where e.id = any ($1::uuid[]) order by e.id for key share`, [ext]))
619
630
  .map((r) => [r.id, r]));
620
631
  for (const id of ext) {
621
- const ref = external.get(id);
622
632
  const f = found.get(id);
623
- if (!f || (f.tenant !== tenant && f.tenant !== SYSTEM_ID) || !accepts(reg, ref.allowed, f.class)) {
624
- issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
633
+ for (const ref of external.get(id)) {
634
+ if (!f || (f.tenant !== tenant && f.tenant !== SYSTEM_ID) || !accepts(reg, ref.allowed, f.class)) {
635
+ issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
636
+ }
625
637
  }
626
638
  }
627
639
  }
@@ -713,7 +725,7 @@ export async function load(env, input, opts = {}) {
713
725
  /** Проверки строки и ссылок; null — строка не пишется (нарушение уже в отчёте). */
714
726
  function check(reg, i, inLoad, external, lastVersion) {
715
727
  const raw = rows[i];
716
- const cls = reg.find(p1[i].cls.name) ?? p1[i].cls;
728
+ const cls = reg.find(p1[i].cls.name); // наличие в свежем реестре проверено после блокировки (loadPortion)
717
729
  const sh = shape(reg, tenant, cls, raw, true);
718
730
  if (sh.error) {
719
731
  issues.add({ line: i + 1, code: sh.error.code, message: sh.error.message, id: p1[i].id, class: cls.name, ...(sh.error.issues ? { issues: sh.error.issues } : {}) });
@@ -758,8 +770,14 @@ export async function load(env, input, opts = {}) {
758
770
  if (!t.alive || !accepts(reg, allowed, t.cls))
759
771
  issues.add({ line: i + 1, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role }), id: p1[i].id, class: cls.name });
760
772
  }
761
- else if (!external.has(id)) {
762
- external.set(id, { line: i + 1, role, allowed });
773
+ else {
774
+ // первая ссылка на id с каждым набором допустимых классов: тот же id под другой ролью может требовать
775
+ // другой класс (сверяются все наборы), повторы того же набора — не копятся
776
+ const refs = external.get(id);
777
+ if (!refs)
778
+ external.set(id, [{ line: i + 1, role, allowed }]);
779
+ else if (!refs.some((r) => sameList(r.allowed, allowed)))
780
+ refs.push({ line: i + 1, role, allowed });
763
781
  }
764
782
  }
765
783
  }
@@ -1181,10 +1199,11 @@ export async function initialSession(env, fn, tune = {}) {
1181
1199
  const found = new Map((await q(`select e.id, e.tenant, e.class from ${S}.entity e where e.id = any ($1::uuid[])`, [ext]))
1182
1200
  .map((r) => [r.id, r]));
1183
1201
  for (const id of ext) {
1184
- const ref = p.external.get(id);
1185
1202
  const f = found.get(id);
1186
- if (!f || (f.tenant !== p.tenant && f.tenant !== SYSTEM_ID) || !accepts(p.reg, ref.allowed, f.class)) {
1187
- issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
1203
+ for (const ref of p.external.get(id)) {
1204
+ if (!f || (f.tenant !== p.tenant && f.tenant !== SYSTEM_ID) || !accepts(p.reg, ref.allowed, f.class)) {
1205
+ issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
1206
+ }
1188
1207
  }
1189
1208
  }
1190
1209
  }
package/dist/log.d.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { type LogEventLevel, type LogEventName } from './messages/index.js';
2
+ import type { LogEvent, LogFn, LogOptions } from './types.js';
3
+ export type { LogEventLevel, LogEventName } from './messages/index.js';
4
+ export type { LogEvent, LogFn, LogLevel, LogObject, LogOptions, Logger } from './types.js';
5
+ /** Поля события, которые задаёт место вызова. */
6
+ export type LogFields = Partial<Omit<LogEvent, 'ts' | 'event' | 'level'>> & {
7
+ level?: LogEventLevel;
8
+ };
9
+ /** Строка без секретов: учётные данные в строке подключения, пароль параметром и в тексте SQL. */
10
+ export declare function maskText(s: string): string;
11
+ type Sink = (level: LogEventLevel, ev: LogEvent) => void;
12
+ /** Кто действует: арендатор и аккаунт фасада — считаются в момент события. */
13
+ export interface LogBound {
14
+ who?: () => {
15
+ tenant?: string | null;
16
+ account?: string | null;
17
+ };
18
+ tx?: string;
19
+ /** Счётчик операций записи явной транзакции (db.begin()): по нему уровень tx.commit — info, если запись была. */
20
+ writes?: {
21
+ n: number;
22
+ };
23
+ }
24
+ /**
25
+ * Текст ошибки для предупреждения или события: у ошибки PostgreSQL — только «PostgreSQL <SQLSTATE>» (без её текста),
26
+ * у прочих — текст без паролей.
27
+ */
28
+ export declare function safeErrorText(e: unknown): string;
29
+ /** Уровень события error: warn — штатный отказ (EXPECTED_CODES, нарушения данных), иначе error. */
30
+ export declare function errorLevel(e: unknown): LogEventLevel;
31
+ /**
32
+ * Поля события error по ошибке: код, ключ каталога, текст, подробности, причина. Ошибка PostgreSQL (сама или причиной)
33
+ * — только SQLSTATE и имена объектов схемы: её текст, DETAIL, WHERE и HINT в событие не идут.
34
+ */
35
+ export declare function errorFields(e: unknown): LogFields;
36
+ /**
37
+ * Логгер подключения: приёмник, порог, имена полей для маскировки и привязка (кто действует, метка транзакции).
38
+ * Производные фасады (db.as(), db.begin(), батч) получают child() — тот же приёмник и порог.
39
+ */
40
+ export declare class Log {
41
+ private readonly sink;
42
+ private readonly min;
43
+ private readonly redactKeys;
44
+ private readonly bound;
45
+ constructor(sink: Sink | null, min: number, redactKeys: ReadonlySet<string>, bound?: LogBound);
46
+ /** Хоть одно событие может уйти (есть приёмник и порог не off). */
47
+ get enabled(): boolean;
48
+ /** Логгер задан (log или LETOPIS_LOG): предупреждения идут в него, а не в console.warn. */
49
+ get active(): boolean;
50
+ /** Метка транзакции привязки. */
51
+ get tx(): string | undefined;
52
+ /** Операций записи в явной транзакции привязки (undefined — привязки к db.begin() нет). */
53
+ get writes(): number | undefined;
54
+ /** Учесть удавшуюся операцию в счётчике записи транзакции — до проверки порога, событие может и не уйти. */
55
+ mark(event: string): void;
56
+ /** Уровень проходит порог. */
57
+ level(l: LogEventLevel): boolean;
58
+ /** Событие (его уровень по каталогу) проходит порог — проверка до сборки полей. */
59
+ wants(event: LogEventName): boolean;
60
+ child(b: LogBound): Log;
61
+ /** Событие: уровень — из каталога (или fields.level); ниже порога — ничего. */
62
+ emit(event: LogEventName, f?: LogFields): void;
63
+ /**
64
+ * Событие error по ошибке; одна ошибка — одно событие. Уровень — errorLevel: штатный отказ (exists, conflict,
65
+ * target_not_found, нарушения данных и т. п.) — warn, прочее — error.
66
+ */
67
+ error(e: unknown, f?: LogFields): void;
68
+ /** Предупреждение: событие warning (ключ каталога и текст). */
69
+ warning(key: string | undefined, msg: string, detail?: Record<string, unknown>): void;
70
+ /** Имя поля — секрет: всегда или по redact. */
71
+ secret(key: string): boolean;
72
+ /** Значение для события: секреты — '***', строки без паролей, массивы и вложенность ограничены. */
73
+ clean(v: unknown, depth?: number, key?: string): unknown;
74
+ private build;
75
+ }
76
+ /** Логирование выключено: ни приёмника, ни событий. */
77
+ export declare const NOLOG: Log;
78
+ /** Логгер-функция: события JSON-строками в stderr (LETOPIS_LOG, флаг CLI --log). */
79
+ export declare const stderrLog: LogFn;
80
+ /**
81
+ * Уровень из LETOPIS_LOG: `debug|info|warn|error`; `1`, `true`, `yes`, `on` и прочее непустое — `info`; не задана, пусто,
82
+ * `0`, `false`, `no`, `off` — логирования по переменной нет.
83
+ */
84
+ export declare function envLevel(v?: string | undefined): LogEventLevel | undefined;
85
+ /**
86
+ * Логгер по опциям: log (или stderr по LETOPIS_LOG), порог logLevel (или из LETOPIS_LOG, иначе 'info'), redact. Неверные
87
+ * опции — invalid_data (log.bad_level, log.bad_logger, log.bad_redact) до подключения.
88
+ */
89
+ export declare function makeLog(o?: LogOptions): Log;
90
+ /** Логгер по одной LETOPIS_LOG — для мест без подключения (предупреждения, createTenant). */
91
+ export declare function envLog(): Log;
92
+ /** Метка транзакции: b — db.begin(), t — db.transaction(); номер в процессе. */
93
+ export declare const nextTx: (kind: "b" | "t") => string;
94
+ /** Строка подключения без пароля. */
95
+ export declare const maskDsn: (dsn: string | undefined) => string | undefined;
96
+ /**
97
+ * Операция с событием: время, событие успеха (done может дополнить поля, сменить событие или вернуть null — без
98
+ * события) и error при отказе. Выключенный логгер — сам вызов без обёртки.
99
+ */
100
+ export declare function traced<T>(log: Log, event: LogEventName | null, f: LogFields, fn: () => Promise<T>, done?: (r: T) => (LogFields & {
101
+ event?: LogEventName;
102
+ }) | null | undefined): Promise<T>;
103
+ /** Синхронный вызов: ошибка — событие error и проброс (шаги и модификаторы цепочки, Proxy фасада). */
104
+ export declare function guarded<T>(log: Log, f: LogFields | (() => LogFields), fn: () => T): T;
105
+ /** Что записать о вызове метода API: событие и поля; null — без события (ошибка пишется всё равно). */
106
+ export type CallSpec = (args: unknown[], result: unknown, before: unknown) => (LogFields & {
107
+ event: LogEventName;
108
+ }) | null;
109
+ /**
110
+ * Обёртка API-объекта (db.auth, db.acl): каждый метод — время, событие по spec и error при отказе. pre — снимок до вызова
111
+ * (например, прежний арендатор у auth.switch). Выключенный логгер — тот же объект.
112
+ */
113
+ export declare function instrument<T extends object>(api: T, log: Log, area: string, specs: Readonly<Record<string, CallSpec>>, pre?: Readonly<Record<string, () => unknown>>): T;
114
+ /** Текст запроса для события: одной строкой, без паролей, не длиннее 2000 символов. */
115
+ export declare function sqlText(text: string): string;
116
+ /**
117
+ * Пул postgres.js (или зарезервированное соединение, транзакция), каждый запрос которого даёт событие sql: шаблон
118
+ * sql`…`, unsafe(), а также соединения reserve() и транзакции begin()/savepoint(). Только при пороге debug — иначе тот
119
+ * же объект. Значения параметров в событие не попадают.
120
+ */
121
+ export declare function traceSql<S extends object>(sql: S, log: Log): S;