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
@@ -0,0 +1,385 @@
1
+ {
2
+ "$comment": "СГЕНЕРИРОВАНО lib/scripts/gen-api-contract.mjs из исходников — не редактировать руками. Регенерация: cd lib && npm run api:contract. CI сверяет через npm run check:docs.",
3
+ "package": {
4
+ "name": "letopis",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "exports": [
10
+ ".",
11
+ "./model",
12
+ "./mcp"
13
+ ]
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "values": [
18
+ "CHAIN_METHODS",
19
+ "DB_METHODS",
20
+ "DDL_REVISION",
21
+ "LOG_EVENTS",
22
+ "LetopisError",
23
+ "RESERVED_CLASS_NAMES",
24
+ "Registry",
25
+ "ValidationError",
26
+ "applyDefaults",
27
+ "between",
28
+ "compileValidator",
29
+ "connect",
30
+ "createTenant",
31
+ "cursorOf",
32
+ "ends",
33
+ "exists",
34
+ "fromDbError",
35
+ "generateTypes",
36
+ "gt",
37
+ "gte",
38
+ "has",
39
+ "hasAll",
40
+ "hasAny",
41
+ "hashPassword",
42
+ "idOf",
43
+ "ilike",
44
+ "inList",
45
+ "inc",
46
+ "isInc",
47
+ "isNull",
48
+ "isOp",
49
+ "like",
50
+ "loose",
51
+ "lt",
52
+ "lte",
53
+ "ne",
54
+ "not",
55
+ "or",
56
+ "schemaToTs",
57
+ "starts",
58
+ "syncModels",
59
+ "totpCode",
60
+ "up",
61
+ "uuidv5",
62
+ "v5Id",
63
+ "v5Name",
64
+ "verifyHash"
65
+ ],
66
+ "types": [
67
+ "AclApi",
68
+ "AclDataDecision",
69
+ "AclDecision",
70
+ "AclOp",
71
+ "AclRule",
72
+ "AclRuleSpec",
73
+ "AclTarget",
74
+ "AnyModelLike",
75
+ "AuthApi",
76
+ "Batch",
77
+ "BatchEntry",
78
+ "CacheOptions",
79
+ "CacheStats",
80
+ "Chain",
81
+ "ChainCore",
82
+ "ChainMods",
83
+ "ChainRef",
84
+ "ClassInfo",
85
+ "ConnectOptions",
86
+ "Credential",
87
+ "Cursor",
88
+ "Db",
89
+ "DbCore",
90
+ "DbTx",
91
+ "DeleteOptions",
92
+ "EndInfo",
93
+ "Expected",
94
+ "FieldFilter",
95
+ "FieldFilterOf",
96
+ "Filter",
97
+ "FilterOf",
98
+ "IncMarker",
99
+ "Issue",
100
+ "KeyedClass",
101
+ "LeafType",
102
+ "LoadInput",
103
+ "LoadIssue",
104
+ "LoadOptions",
105
+ "LoadReport",
106
+ "LogEvent",
107
+ "LogEventName",
108
+ "LogFn",
109
+ "LogLevel",
110
+ "LogObject",
111
+ "LogOptions",
112
+ "Logger",
113
+ "LoginOptions",
114
+ "LoginResult",
115
+ "MaintainReport",
116
+ "ModelLike",
117
+ "Op",
118
+ "OrOp",
119
+ "PatchOf",
120
+ "Path",
121
+ "QueryEvent",
122
+ "RestoreOptions",
123
+ "Row",
124
+ "RowOf",
125
+ "Scalar",
126
+ "SessionInfo",
127
+ "SignalMode",
128
+ "StepFn",
129
+ "StepsOf",
130
+ "Subscription",
131
+ "SyncResult",
132
+ "SystemModels",
133
+ "TenantOptions",
134
+ "TenantResult",
135
+ "TypedChain",
136
+ "TypedDb",
137
+ "UpOptions",
138
+ "UpResult",
139
+ "Validator",
140
+ "WatchEvent",
141
+ "WatchOptions",
142
+ "WriteRow"
143
+ ]
144
+ },
145
+ "./model": {
146
+ "values": [
147
+ "End",
148
+ "hub",
149
+ "link",
150
+ "many",
151
+ "normalizeSchema",
152
+ "one",
153
+ "translatePattern"
154
+ ],
155
+ "types": [
156
+ "AnyModel",
157
+ "ClassDef",
158
+ "ClassDescription",
159
+ "ClassMeta",
160
+ "Infer",
161
+ "InferLinks",
162
+ "JsonSchema",
163
+ "LostRule",
164
+ "Model",
165
+ "OnDelete"
166
+ ]
167
+ },
168
+ "./mcp": {
169
+ "values": [
170
+ "createMcpServer"
171
+ ],
172
+ "types": [
173
+ "McpOptions"
174
+ ]
175
+ }
176
+ },
177
+ "chain": [
178
+ "add",
179
+ "after",
180
+ "alias",
181
+ "asOf",
182
+ "avg",
183
+ "cache",
184
+ "count",
185
+ "countBy",
186
+ "create",
187
+ "deep",
188
+ "delete",
189
+ "entity",
190
+ "exact",
191
+ "first",
192
+ "forUpdate",
193
+ "ids",
194
+ "limit",
195
+ "max",
196
+ "min",
197
+ "offset",
198
+ "owner",
199
+ "paths",
200
+ "remove",
201
+ "restore",
202
+ "rows",
203
+ "set",
204
+ "sort",
205
+ "sum",
206
+ "tags",
207
+ "then",
208
+ "unset",
209
+ "update",
210
+ "upsert",
211
+ "versions",
212
+ "withDeleted"
213
+ ],
214
+ "db": [
215
+ "acl",
216
+ "as",
217
+ "auth",
218
+ "batch",
219
+ "begin",
220
+ "cache",
221
+ "close",
222
+ "commit",
223
+ "flushSignals",
224
+ "idOf",
225
+ "load",
226
+ "lock",
227
+ "maintain",
228
+ "refresh",
229
+ "registry",
230
+ "rollback",
231
+ "schema",
232
+ "sql",
233
+ "tenant",
234
+ "token",
235
+ "transaction",
236
+ "watch"
237
+ ],
238
+ "batch": [
239
+ "discard",
240
+ "run",
241
+ "size"
242
+ ],
243
+ "codes": [
244
+ "acl_denied",
245
+ "admin_required",
246
+ "bypass_rls",
247
+ "class_not_loadable",
248
+ "commit_unknown",
249
+ "confirmation_required",
250
+ "conflict",
251
+ "delete_restricted",
252
+ "deleted",
253
+ "duplicate",
254
+ "exists",
255
+ "id_from_db",
256
+ "id_mismatch",
257
+ "immutable_key",
258
+ "internal",
259
+ "invalid_class",
260
+ "invalid_data",
261
+ "invalid_query",
262
+ "isolation_level",
263
+ "lock_unsupported",
264
+ "no_key",
265
+ "no_path",
266
+ "no_session",
267
+ "not_deleted",
268
+ "not_empty",
269
+ "recreated_in_statement",
270
+ "rev_ambiguous",
271
+ "target_not_found",
272
+ "tenant_denied",
273
+ "tightening_conflict",
274
+ "tx_required",
275
+ "tx_rolled_back",
276
+ "undeclared_end",
277
+ "user_session_required"
278
+ ],
279
+ "errors": [
280
+ {
281
+ "module": "15-errors",
282
+ "message": "letopis: acl_denied: отказ по правилам доступа"
283
+ },
284
+ {
285
+ "module": "15-errors",
286
+ "message": "letopis: delete_restricted: удалению мешают ссылающиеся строки"
287
+ },
288
+ {
289
+ "module": "15-errors",
290
+ "message": "letopis: id_from_db: id класса без ключа выдаёт база"
291
+ },
292
+ {
293
+ "module": "15-errors",
294
+ "message": "letopis: id_mismatch: id не равен v5 от ключа"
295
+ },
296
+ {
297
+ "module": "15-errors",
298
+ "message": "letopis: immutable_key: ключевое поле менять нельзя"
299
+ },
300
+ {
301
+ "module": "15-errors",
302
+ "message": "letopis: invalid_class: описание класса отклонено"
303
+ },
304
+ {
305
+ "module": "15-errors",
306
+ "message": "letopis: invalid_data: данные не проходят описание класса"
307
+ },
308
+ {
309
+ "module": "15-errors",
310
+ "message": "letopis: isolation_level: операция разрешена только в read committed"
311
+ },
312
+ {
313
+ "module": "15-errors",
314
+ "message": "letopis: no_session: нет действующей сессии"
315
+ },
316
+ {
317
+ "module": "15-errors",
318
+ "message": "letopis: not_deleted: объект не удалён — restore только для удалённых"
319
+ },
320
+ {
321
+ "module": "15-errors",
322
+ "message": "letopis: recreated_in_statement: id удалён и создан заново одним оператором"
323
+ },
324
+ {
325
+ "module": "15-errors",
326
+ "message": "letopis: target_not_found: цель не найдена"
327
+ },
328
+ {
329
+ "module": "15-errors",
330
+ "message": "letopis: tenant_denied: чужой арендатор"
331
+ },
332
+ {
333
+ "module": "15-errors",
334
+ "message": "letopis: tightening_conflict: ужесточению описания мешают строки"
335
+ },
336
+ {
337
+ "module": "15-errors",
338
+ "message": "letopis: undeclared_end: конец не объявлен в классе"
339
+ },
340
+ {
341
+ "module": "60-write",
342
+ "message": "letopis: invalid_data: "
343
+ },
344
+ {
345
+ "module": "cli",
346
+ "message": "letopis: <…>"
347
+ },
348
+ {
349
+ "module": "errors",
350
+ "message": "letopis: <…>"
351
+ },
352
+ {
353
+ "module": "errors",
354
+ "message": "letopis: <…>: <…>"
355
+ },
356
+ {
357
+ "module": "errors",
358
+ "message": "letopis: invalid_data: <…><…>"
359
+ },
360
+ {
361
+ "module": "messages/index",
362
+ "message": "letopis: ключ каталога <…> повторяется"
363
+ },
364
+ {
365
+ "module": "messages/index",
366
+ "message": "letopis: ключ каталога <…> — не из раздела <…>"
367
+ },
368
+ {
369
+ "module": "up",
370
+ "message": "letopis.up: ${own ? seed : "
371
+ },
372
+ {
373
+ "module": "up",
374
+ "message": "letopis.up: <…>"
375
+ },
376
+ {
377
+ "module": "up",
378
+ "message": "letopis.up: <…>: <…>"
379
+ },
380
+ {
381
+ "module": "up",
382
+ "message": "letopis.up: migrate/<…>: <…>"
383
+ }
384
+ ]
385
+ }
package/dist/acl.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Executor } from './tx.js';
2
+ import type { CallSpec } from './log.js';
2
3
  export type AclOp = 'READ' | 'WRITE' | 'DELETE';
3
4
  /** Решение по ресурсу приложения (эндпоинту): можно ли к нему обратиться. */
4
5
  export interface AclDecision {
@@ -50,7 +51,7 @@ export interface AclRuleSpec {
50
51
  /** Правило арендатора в виде обёртки. */
51
52
  export interface AclRule {
52
53
  id: string;
53
- /** Роль группы `role:<роль>:ACCOUNT`; null — группа не одной роли (все аккаунты или своя группа). */
54
+ /** Роль правила; null — правило не одной роли (все аккаунты или своя группа). */
54
55
  role: string | null;
55
56
  /** Группа всех аккаунтов арендатора: `authenticated:ACCOUNT` от createTenant или `everyone:ACCOUNT` обёртки. */
56
57
  everyone: boolean;
@@ -86,7 +87,9 @@ export interface AclApi {
86
87
  /** Запретить — те же правила с запретом: при равном весе запрет сильнее разрешения. */
87
88
  deny(spec: AclRuleSpec): Promise<AclRule[]>;
88
89
  /**
89
- * Снять правила роли или всех аккаунтов — разрешения и запреты — для этих операций и целей; число снятых.
90
+ * Снять правила роли или всех аккаунтов — разрешения и запреты — для этих операций и целей; возвращает число снятых
91
+ * правил. Операции снимаются по очереди, и правило на несколько операций считается один раз: «можно всё» на чтение,
92
+ * запись и удаление от createTenant даёт 1.
90
93
  * `revoke({ everyone: true, read: '*', write: '*', delete: '*' })` снимает «можно всё» от createTenant: дальше права —
91
94
  * только по ролям.
92
95
  */
@@ -110,3 +113,5 @@ export interface AclApi {
110
113
  * транзакции.
111
114
  */
112
115
  export declare function makeAcl(S: string, exec: Executor, db?: () => unknown): AclApi;
116
+ /** Что пишет в лог каждый метод db.acl: правка прав — acl.change, проверки и список — acl.check. */
117
+ export declare const ACL_LOG: Readonly<Record<string, CallSpec>>;
package/dist/acl.js CHANGED
@@ -200,8 +200,9 @@ export function makeAcl(S, exec, db) {
200
200
  objects.push(sysId(`all:${it.op}`));
201
201
  for (const g of groups) {
202
202
  for (const o of objects) {
203
+ // снять правило — замысел вызова, превью некому показывать: { all: true } (3.0) — без сверки, цели сужает фильтр
203
204
  if (g && o)
204
- n += (await tr.rule({ Group: g, Object: o }).delete({ confirm: true }).rows()).length;
205
+ n += (await tr.rule({ Group: g, Object: o }).delete({ confirm: true, all: true }).rows()).length;
205
206
  }
206
207
  }
207
208
  }
@@ -221,7 +222,8 @@ export function makeAcl(S, exec, db) {
221
222
  return m ? [...(m.data.roles ?? [])] : null;
222
223
  }
223
224
  if (roles === null) {
224
- await inTx('roles', (tr) => tr.member({ User: acc, Tenant: tenant }).delete({ confirm: true }).rows());
225
+ // roles(account, null) — снять членство: замысел вызова, без сверки с превью (3.0)
226
+ await inTx('roles', (tr) => tr.member({ User: acc, Tenant: tenant }).delete({ confirm: true, all: true }).rows());
225
227
  return null;
226
228
  }
227
229
  if (!Array.isArray(roles))
@@ -275,3 +277,18 @@ export function makeAcl(S, exec, db) {
275
277
  };
276
278
  return api;
277
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,3 +1,4 @@
1
+ import { type Log } from './log.js';
1
2
  import { type Executor, type TxContext } from './tx.js';
2
3
  export interface MaintainReport {
3
4
  /** Другой процесс уже обслуживает установку (держит строку-замок обслуживания): этот вызов ничего не делал. */
@@ -29,6 +30,11 @@ export interface AdminCtx {
29
30
  export declare function maintain(a: AdminCtx): Promise<MaintainReport>;
30
31
  /** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
31
32
  export declare function queueWarning(queue: number): string | null;
33
+ /**
34
+ * Короткая форма интервала — '500ms', '30s', '15m', '1h', '1d' (дробное число тоже) — в миллисекундах; другая строка —
35
+ * null. Общая для connect({ maintain }) и срока ttl у auth (3.0.1).
36
+ */
37
+ export declare function shortIntervalMs(v: string): number | null;
32
38
  /**
33
39
  * Интервал расписания: число мс или строка '500ms', '30s', '15m', '1h', '1d' — конечный, от 1 мс до 2³¹−1 мс (около 24
34
40
  * суток); иначе invalid_data. Ноль, отрицательное, NaN и слишком длинный интервал таймер Node.js превращает в 1 мс —
@@ -40,4 +46,4 @@ export declare function parseInterval(v: unknown): number;
40
46
  * следующий запуск не начинается, пока идёт прежний. Возвращает остановку: она снимает таймер и ждёт конца идущего
41
47
  * запуска — close() закрывает пул после него, а не посреди транзакции.
42
48
  */
43
- export declare function scheduleMaintain(run: () => Promise<unknown>, ms: number): () => Promise<void>;
49
+ export declare function scheduleMaintain(run: () => Promise<unknown>, ms: number, log?: Log): () => Promise<void>;
package/dist/admin.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * Эксплуатация (план, этап 6, п. 7, 8): maintain и его запуск по расписанию. Всё проверяет база: функция владельца
3
3
  * 90-time.sql сама сверяет права (letopis.maintain — системный администратор или администраторское подключение).
4
4
  */
5
- import { lerr, lmsg, lwarn } from './errors.js';
5
+ import { lerr, lmsg, lwarn, warnText } from './errors.js';
6
+ import { safeErrorText } from './log.js';
6
7
  import { runTx } from './tx.js';
7
8
  /**
8
9
  * Обслуживание: истёкшие и погашенные сессии, одноразовые коды, строки-замки удалённых семейств и заполнение очереди
@@ -21,8 +22,9 @@ export async function maintain(a) {
21
22
  const b = r;
22
23
  const report = { skipped: false, sessions: b.sessions, codes: b.codes, queue: Number(b.queue) };
23
24
  const warn = queueWarning(report.queue);
25
+ // логгер задан — событие warning (time.signal_queue), иначе console.warn, как прежде
24
26
  if (warn)
25
- console.warn(warn);
27
+ warnText(warn, 'time.signal_queue', a.txc.log, { queue: report.queue });
26
28
  return report;
27
29
  }
28
30
  /** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
@@ -31,6 +33,14 @@ export function queueWarning(queue) {
31
33
  }
32
34
  /** Самый длинный интервал таймера Node.js — 2³¹−1 мс (около 24,8 суток): дольше таймер не ждёт и срабатывает сразу. */
33
35
  const MAX_TIMER_MS = 2 ** 31 - 1;
36
+ /**
37
+ * Короткая форма интервала — '500ms', '30s', '15m', '1h', '1d' (дробное число тоже) — в миллисекундах; другая строка —
38
+ * null. Общая для connect({ maintain }) и срока ttl у auth (3.0.1).
39
+ */
40
+ export function shortIntervalMs(v) {
41
+ const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(v.trim());
42
+ return m ? Number(m[1]) * { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]] : null;
43
+ }
34
44
  /**
35
45
  * Интервал расписания: число мс или строка '500ms', '30s', '15m', '1h', '1d' — конечный, от 1 мс до 2³¹−1 мс (около 24
36
46
  * суток); иначе invalid_data. Ноль, отрицательное, NaN и слишком длинный интервал таймер Node.js превращает в 1 мс —
@@ -40,11 +50,8 @@ export function parseInterval(v) {
40
50
  let ms = Number.NaN;
41
51
  if (typeof v === 'number')
42
52
  ms = v;
43
- else if (typeof v === 'string') {
44
- const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(v.trim());
45
- if (m)
46
- ms = Number(m[1]) * { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
47
- }
53
+ else if (typeof v === 'string')
54
+ ms = shortIntervalMs(v) ?? Number.NaN;
48
55
  if (!Number.isFinite(ms) || ms < 1 || ms > MAX_TIMER_MS) {
49
56
  const shown = typeof v === 'string' ? `'${v}'` : typeof v === 'number' || typeof v === 'boolean' || v == null ? String(v) : typeof v;
50
57
  throw lerr('time.bad_interval', { value: shown });
@@ -56,14 +63,14 @@ export function parseInterval(v) {
56
63
  * следующий запуск не начинается, пока идёт прежний. Возвращает остановку: она снимает таймер и ждёт конца идущего
57
64
  * запуска — close() закрывает пул после него, а не посреди транзакции.
58
65
  */
59
- export function scheduleMaintain(run, ms) {
66
+ export function scheduleMaintain(run, ms, log) {
60
67
  let stopped = false;
61
68
  let current = null;
62
69
  const timer = setInterval(() => {
63
70
  if (current || stopped)
64
71
  return;
65
72
  current = run()
66
- .then(() => { }, (e) => lwarn('time.maintain_failed', { message: e.message }))
73
+ .then(() => { }, (e) => lwarn('time.maintain_failed', { message: safeErrorText(e) }, { log }))
67
74
  .finally(() => (current = null));
68
75
  }, ms);
69
76
  timer.unref?.();
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Executor } from './tx.js';
2
+ import type { CallSpec } from './log.js';
2
3
  /** Хэш пароля scrypt: `scrypt$N$r$p$соль$хэш`. */
3
4
  export declare function hashPassword(password: string): Promise<string>;
4
5
  /** Проверка пароля по хэшу scrypt; другие форматы не проверяются. */
@@ -10,6 +11,17 @@ export declare function totpCode(secretBase32: string, atMs?: number): string;
10
11
  type AccountRef = string | {
11
12
  id: string;
12
13
  };
14
+ /**
15
+ * Срок ttl (3.0.1) — интервал для базы: число секунд, короткая форма как у connect({ maintain }) ('30s', '15m', '2h',
16
+ * '1d') или строка-интервал PostgreSQL ('15 minutes') — её разбирает база, ошибку разбора переводит ttlGuard. Не задан —
17
+ * null (срок по умолчанию). Не число больше нуля, пустая строка, другой тип — invalid_data (access.bad_ttl).
18
+ */
19
+ export declare function ttlOf(v: unknown, where: string): string | null;
20
+ /**
21
+ * Строка ttl, которую не принял разбор интервала PostgreSQL (22007 — не интервал, 22008 и 22015 — вне диапазона), —
22
+ * invalid_data access.bad_ttl вместо сырой ошибки базы; прочие ошибки — как есть.
23
+ */
24
+ export declare function ttlGuard<T>(run: Promise<T>, ttl: unknown, where: string): Promise<T>;
13
25
  /** Успешный вход: аккаунт и токен новой сессии. */
14
26
  export interface LoginResult {
15
27
  account: string;
@@ -45,6 +57,10 @@ export interface Credential {
45
57
  created_at: string;
46
58
  expires_at: string | null;
47
59
  }
60
+ /**
61
+ * Вход, креды и сессии (`db.auth`). Срок `ttl` у входа, ключей, кодов и сессий — число секунд или строка: короткая форма
62
+ * `'15m'`/`'2h'`/`'30s'`/`'1d'` либо интервал PostgreSQL `'15 minutes'`; иное — `invalid_data`.
63
+ */
48
64
  export interface AuthApi {
49
65
  /** Пароль (и любой «логин/пароль»: `kind` — `PASSWORD`, `EMAIL`, …): создать или заменить. Смена гасит остальные сессии аккаунта. */
50
66
  setPassword(a: {
@@ -65,14 +81,14 @@ export interface AuthApi {
65
81
  password: string;
66
82
  kind?: string;
67
83
  requireConfirmed?: boolean;
68
- ttl?: number;
84
+ ttl?: number | string;
69
85
  totp?: string;
70
86
  }): Promise<LoginResult | null>;
71
87
  /** Ключ API `lts_…`: отдаётся один раз. Себе сервис выпускает ключ сам по сессии своего ключа. */
72
88
  issueApiKey(a: {
73
89
  account: AccountRef;
74
90
  name?: string;
75
- ttl?: number;
91
+ ttl?: number | string;
76
92
  }): Promise<{
77
93
  key: string;
78
94
  id: string;
@@ -111,7 +127,7 @@ export interface AuthApi {
111
127
  account: AccountRef;
112
128
  identifier: string;
113
129
  kind?: string;
114
- ttl?: number;
130
+ ttl?: number | string;
115
131
  digits?: number;
116
132
  maxAttempts?: number;
117
133
  }): Promise<{
@@ -146,7 +162,7 @@ export interface AuthApi {
146
162
  kind: string;
147
163
  identifier: string;
148
164
  requireConfirmed?: boolean;
149
- ttl?: number;
165
+ ttl?: number | string;
150
166
  totp?: string;
151
167
  }): Promise<LoginResult | null>;
152
168
  /** Способы входа (креды) аккаунта — без хэшей и секретов. */
@@ -164,9 +180,12 @@ export interface AuthApi {
164
180
  confirmed?: boolean;
165
181
  enabled?: boolean;
166
182
  }): Promise<boolean>;
167
- /** Сессия пользователя для другого процесса (право `auth.session`); срок — `ttl` в секундах, по умолчанию 7 суток. */
183
+ /**
184
+ * Сессия пользователя для другого процесса (право `auth.session`). Срок `ttl` — число секунд или строка: короткая форма
185
+ * `'15m'`/`'2h'`/`'30s'`/`'1d'` либо интервал PostgreSQL `'15 minutes'`; по умолчанию 7 суток.
186
+ */
168
187
  sessionFor(account: AccountRef, opts?: {
169
- ttl?: number;
188
+ ttl?: number | string;
170
189
  }): Promise<string>;
171
190
  /**
172
191
  * Сведения о сессии по её токену — для сервиса, который держит токен пользователя (например, в cookie): чья это
@@ -180,20 +199,35 @@ export interface AuthApi {
180
199
  * `no_session` — у подключения нет действующей сессии.
181
200
  */
182
201
  session(token: string): Promise<SessionInfo | null>;
183
- /** Продлить текущую сессию; новый срок — от сейчас. */
202
+ /**
203
+ * Продлить текущую сессию; новый срок — от сейчас. `ttl` — число секунд или строка: короткая форма `'15m'`/`'2h'`/`'30s'`/`'1d'`
204
+ * либо интервал PostgreSQL `'15 minutes'`; по умолчанию 7 суток.
205
+ */
184
206
  refresh(opts?: {
185
- ttl?: number;
207
+ ttl?: number | string;
186
208
  }): Promise<string>;
187
- /** Выход: погасить текущую сессию. */
188
- revoke(): Promise<boolean>;
209
+ /**
210
+ * Выход. Без аргумента — погасить сессию этого подключения. С токеном — погасить сессию пользователя по её токену
211
+ * (например, из cookie веб-сервиса): так сервис выводит пользователя, не подключаясь по его токену; нужно право
212
+ * `auth.session`, как у `session(token)`.
213
+ * @returns true — сессия погашена; false — токен неверный, сессия уже погашена или истекла (причина не различается).
214
+ * @throws LetopisError `acl_denied` — с токеном, а у вызывающего нет права `auth.session`; `no_session` — у подключения
215
+ * нет действующей сессии.
216
+ */
217
+ revoke(token?: string): Promise<boolean>;
189
218
  /** Выход везде: все сессии аккаунта (чужого — с правом `auth.session`). */
190
219
  revokeAll(account?: AccountRef): Promise<number>;
191
220
  /** Переключить арендатора сессии: собственный или по членству. */
192
221
  switch(tenant: string): Promise<void>;
193
- /** Арендаторы, доступные аккаунту сессии: собственный (`owner`) и по членству. */
222
+ /**
223
+ * Арендаторы, доступные аккаунту сессии: собственный (`roles: ['owner']`, `personal: true`) и по членству. `name` — имя
224
+ * аккаунта арендатора (`createTenant({ name })`, у личного — имя пользователя).
225
+ */
194
226
  tenants(): Promise<{
195
227
  tenant: string;
196
228
  roles: string[];
229
+ name: string | null;
230
+ personal: boolean;
197
231
  }[]>;
198
232
  /**
199
233
  * Удалить аккаунт: системный администратор или владелец его
@@ -211,4 +245,6 @@ export interface AuthApi {
211
245
  }
212
246
  /** Фасад db.auth. onSwitch — подключение перечитывает арендатора, реестр и пропуска прав. */
213
247
  export declare function makeAuth(S: string, exec: Executor, onSwitch: () => Promise<void>, canSwitch: boolean): AuthApi;
248
+ /** Что пишет в лог каждый метод db.auth. */
249
+ export declare const AUTH_LOG: Readonly<Record<string, CallSpec>>;
214
250
  export {};