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.
- package/AGENT-CHEATSHEET.md +491 -414
- package/CHANGELOG.md +361 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +969 -210
- package/api-contract.json +385 -0
- package/dist/acl.d.ts +7 -2
- package/dist/acl.js +19 -2
- package/dist/admin.d.ts +7 -1
- package/dist/admin.js +16 -9
- package/dist/auth.d.ts +47 -11
- package/dist/auth.js +129 -15
- package/dist/chain.d.ts +52 -25
- package/dist/chain.js +445 -64
- package/dist/cli.js +72 -20
- package/dist/errors.d.ts +23 -6
- package/dist/errors.js +52 -13
- package/dist/index.d.ts +32 -15
- package/dist/index.js +209 -68
- package/dist/internal.d.ts +2 -0
- package/dist/load.d.ts +10 -6
- package/dist/load.js +30 -11
- package/dist/log.d.ts +121 -0
- package/dist/log.js +654 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +34 -16
- package/dist/messages/access.d.ts +10 -0
- package/dist/messages/access.js +13 -0
- package/dist/messages/api.d.ts +76 -0
- package/dist/messages/api.js +109 -7
- package/dist/messages/boot.d.ts +5 -0
- package/dist/messages/boot.js +11 -4
- package/dist/messages/codes.js +4 -4
- package/dist/messages/index.d.ts +5 -2
- package/dist/messages/index.js +5 -2
- package/dist/messages/log.d.ts +232 -0
- package/dist/messages/log.js +105 -0
- package/dist/messages/mcp.js +7 -7
- package/dist/messages/model.d.ts +40 -1
- package/dist/messages/model.js +52 -3
- package/dist/messages/query.d.ts +10 -0
- package/dist/messages/query.js +13 -0
- package/dist/messages/sql-core.d.ts +15 -1
- package/dist/messages/sql-core.js +20 -3
- package/dist/messages/sql-time.d.ts +15 -0
- package/dist/messages/sql-time.js +23 -0
- package/dist/messages/write.d.ts +52 -0
- package/dist/messages/write.js +77 -6
- package/dist/model.d.ts +10 -2
- package/dist/model.js +28 -6
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +6 -0
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +77 -25
- package/dist/sync.d.ts +4 -5
- package/dist/sync.js +53 -4
- package/dist/tx.d.ts +10 -1
- package/dist/tx.js +44 -2
- package/dist/typed.d.ts +18 -14
- package/dist/types.d.ts +91 -8
- package/dist/types.js +8 -3
- package/dist/up.d.ts +8 -5
- package/dist/up.js +71 -23
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +9 -1
- package/dist/validate.js +24 -7
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +13 -0
- package/dist/write.d.ts +84 -3
- package/dist/write.js +427 -40
- package/llms.txt +38 -17
- package/package.json +12 -6
- package/sql/20-context.sql +11 -3
- package/sql/30-validate.sql +51 -10
- package/sql/40-class.sql +21 -1
- package/sql/60-write.sql +18 -6
- package/sql/80-auth.sql +88 -17
- package/sql/90-time.sql +34 -6
- package/sql/99-revision.sql +5 -1
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- 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
|
-
/** Роль
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 {};
|