@cortexkit/common-auth 0.3.0 → 0.4.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/dist/cachekeep/manager.d.ts +18 -6
- package/dist/cachekeep/manager.js +40 -10
- package/dist/claustrum/consumer.d.ts +13 -4
- package/dist/claustrum/consumer.js +11 -3
- package/dist/claustrum/custody.d.ts +47 -6
- package/dist/claustrum/custody.js +37 -7
- package/dist/claustrum/errors.d.ts +1 -1
- package/dist/claustrum/index.d.ts +3 -3
- package/dist/claustrum/index.js +2 -2
- package/dist/claustrum/interlock.d.ts +15 -17
- package/dist/claustrum/interlock.js +19 -26
- package/dist/claustrum/roster.d.ts +96 -6
- package/dist/claustrum/roster.js +209 -44
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +6 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/menu.d.ts +8 -0
- package/dist/commands/menu.js +34 -13
- package/dist/commands/model.d.ts +9 -0
- package/dist/commands/seam.d.ts +40 -4
- package/dist/commands/seam.js +132 -19
- package/dist/dump/index.d.ts +94 -0
- package/dist/dump/index.js +236 -9
- package/dist/logger/engine.d.ts +52 -17
- package/dist/logger/engine.js +178 -135
- package/dist/logger/index.d.ts +2 -2
- package/dist/logger/index.js +1 -1
- package/dist/opencode2/install.d.ts +8 -3
- package/dist/opencode2/install.js +18 -9
- package/dist/opencode2/types.d.ts +22 -3
- package/dist/quota/projection.d.ts +11 -4
- package/dist/quota/projection.js +11 -4
- package/dist/routing/admission.js +3 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/routing/sticky.d.ts +19 -6
- package/dist/routing/sticky.js +34 -23
- package/dist/rpc/notifications.d.ts +20 -0
- package/dist/rpc/notifications.js +21 -0
- package/dist/rpc/rpc-server.d.ts +9 -1
- package/dist/rpc/rpc-server.js +8 -1
- package/dist/sidebar-file/index.d.ts +1 -1
- package/dist/sidebar-file/sidebar-file.d.ts +50 -2
- package/dist/sidebar-file/sidebar-file.js +92 -21
- package/dist/store/attribution.js +11 -2
- package/dist/store/errors.d.ts +6 -3
- package/dist/store/identity.d.ts +13 -4
- package/dist/store/mutate.d.ts +23 -3
- package/dist/store/mutate.js +43 -26
- package/dist/store/pool.d.ts +8 -1
- package/dist/store/pool.js +7 -2
- package/dist/store/rows.d.ts +17 -4
- package/dist/store/rows.js +82 -33
- package/dist/store/schema.d.ts +57 -4
- package/dist/store/schema.js +100 -7
- package/dist/store/torn.d.ts +29 -0
- package/dist/store/torn.js +113 -0
- package/package.json +1 -1
package/dist/dump/index.js
CHANGED
|
@@ -1,9 +1,161 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { chmod, lstat, mkdir, readdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
import { createRedactor } from '../logger/redact.js';
|
|
5
5
|
const PREVIOUS_BODY_LIMIT = 100;
|
|
6
6
|
const UNKNOWN_SESSION = 'session-unknown';
|
|
7
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
8
|
+
const DEFAULT_SWEEP_MIN_AGE_MS = 60 * 1000;
|
|
9
|
+
const DEFAULT_PARTIAL_STALE_MS = 10 * 60 * 1000;
|
|
10
|
+
/**
|
|
11
|
+
* The suffixes of the files one dump writes. A dump's files share the name
|
|
12
|
+
* before the suffix, which is how the sweep groups them.
|
|
13
|
+
*/
|
|
14
|
+
const ARTIFACT_SUFFIX = /\.(body|meta|request|response)\.json$/;
|
|
15
|
+
/**
|
|
16
|
+
* A dump id: the ISO time with `:` and `.` replaced, the pid, a counter of
|
|
17
|
+
* at least six digits, then the sanitised session, channel and phase. Only
|
|
18
|
+
* names of this shape are swept, so a plugin may point dumps at a directory
|
|
19
|
+
* that also holds other files.
|
|
20
|
+
*/
|
|
21
|
+
const ARTIFACT_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-\d+-\d{6,}-[a-zA-Z0-9._-]+$/;
|
|
22
|
+
/** A response staging file: the final name, a random nonce, `.partial`. */
|
|
23
|
+
const PARTIAL_SUFFIX = /\.[a-f0-9]{24}\.partial$/;
|
|
24
|
+
function artifactGroup(name) {
|
|
25
|
+
const stem = name.replace(ARTIFACT_SUFFIX, '');
|
|
26
|
+
if (stem === name || !ARTIFACT_ID.test(stem))
|
|
27
|
+
return undefined;
|
|
28
|
+
return stem;
|
|
29
|
+
}
|
|
30
|
+
function isPartialName(name) {
|
|
31
|
+
const finalName = name.replace(PARTIAL_SUFFIX, '');
|
|
32
|
+
return finalName !== name && artifactGroup(finalName) !== undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hold the dump artifacts in `dir` to `maxBytes`. Only file names a dumper
|
|
36
|
+
* writes are counted or removed, so unrelated files in a directory the user
|
|
37
|
+
* chose survive. A dump's files are evicted together, oldest dump first by
|
|
38
|
+
* its newest file, because a body without its metadata (or the reverse) is no
|
|
39
|
+
* use to anyone. Dumps younger than `minAgeMs`, protected paths and symlinks
|
|
40
|
+
* are kept, and a symlinked directory is refused outright. Response staging
|
|
41
|
+
* files left by a crash are reclaimed once stale, even under the cap.
|
|
42
|
+
* Best-effort: a failure removes less, and never throws.
|
|
43
|
+
*/
|
|
44
|
+
export async function sweepDumpDirectory(options) {
|
|
45
|
+
const { dir, maxBytes } = options;
|
|
46
|
+
const now = options.now ?? Date.now();
|
|
47
|
+
const minAgeMs = options.minAgeMs ?? DEFAULT_SWEEP_MIN_AGE_MS;
|
|
48
|
+
const partialStaleMs = options.partialStaleMs ?? DEFAULT_PARTIAL_STALE_MS;
|
|
49
|
+
const empty = { removed: 0, freedBytes: 0 };
|
|
50
|
+
if (!(maxBytes > 0))
|
|
51
|
+
return empty;
|
|
52
|
+
try {
|
|
53
|
+
if ((await lstat(dir)).isSymbolicLink())
|
|
54
|
+
return empty;
|
|
55
|
+
const protectedPaths = new Set((options.protectedPaths ?? []).map((path) => resolve(path)));
|
|
56
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
57
|
+
const files = [];
|
|
58
|
+
await Promise.all(entries.map(async (entry) => {
|
|
59
|
+
if (!entry.isFile())
|
|
60
|
+
return;
|
|
61
|
+
const partial = isPartialName(entry.name);
|
|
62
|
+
const group = partial ? undefined : artifactGroup(entry.name);
|
|
63
|
+
if (!partial && group === undefined)
|
|
64
|
+
return;
|
|
65
|
+
const path = join(dir, entry.name);
|
|
66
|
+
try {
|
|
67
|
+
const stats = await lstat(path);
|
|
68
|
+
if (!stats.isFile())
|
|
69
|
+
return;
|
|
70
|
+
files.push({ path, size: stats.size, mtimeMs: stats.mtimeMs, group });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Another process removed it between the listing and the stat.
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
let total = files.reduce((sum, file) => sum + file.size, 0);
|
|
77
|
+
let removed = 0;
|
|
78
|
+
let freedBytes = 0;
|
|
79
|
+
const remove = async (file) => {
|
|
80
|
+
try {
|
|
81
|
+
await unlink(file.path);
|
|
82
|
+
total -= file.size;
|
|
83
|
+
freedBytes += file.size;
|
|
84
|
+
removed += 1;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Best-effort: what cannot be removed now is tried again next sweep.
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// A staging file is never a usable dump, so a stale one goes whatever
|
|
91
|
+
// the total; a fresh one may still be renamed into place.
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (file.group !== undefined)
|
|
94
|
+
continue;
|
|
95
|
+
if (protectedPaths.has(resolve(file.path)))
|
|
96
|
+
continue;
|
|
97
|
+
if (now - file.mtimeMs < partialStaleMs)
|
|
98
|
+
continue;
|
|
99
|
+
await remove(file);
|
|
100
|
+
}
|
|
101
|
+
const groups = new Map();
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
if (file.group === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
const group = groups.get(file.group) ?? { files: [], newest: 0 };
|
|
106
|
+
group.files.push(file);
|
|
107
|
+
group.newest = Math.max(group.newest, file.mtimeMs);
|
|
108
|
+
groups.set(file.group, group);
|
|
109
|
+
}
|
|
110
|
+
const oldestFirst = [...groups.entries()].sort(([leftName, left], [rightName, right]) => left.newest - right.newest || leftName.localeCompare(rightName));
|
|
111
|
+
for (const [, group] of oldestFirst) {
|
|
112
|
+
if (total <= maxBytes)
|
|
113
|
+
break;
|
|
114
|
+
if (now - group.newest < minAgeMs)
|
|
115
|
+
continue;
|
|
116
|
+
if (group.files.some((file) => protectedPaths.has(resolve(file.path))))
|
|
117
|
+
continue;
|
|
118
|
+
for (const file of group.files)
|
|
119
|
+
await remove(file);
|
|
120
|
+
}
|
|
121
|
+
if (removed > 0) {
|
|
122
|
+
options.logger?.debug('removed old dump files', { removed, freedBytes });
|
|
123
|
+
}
|
|
124
|
+
return { removed, freedBytes };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return empty;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Replace `path` through a staging file created exclusively next to it, so a
|
|
132
|
+
* reader never sees half a file and a symlink planted at a predictable
|
|
133
|
+
* staging name is never followed.
|
|
134
|
+
*/
|
|
135
|
+
async function replaceFile(path, text) {
|
|
136
|
+
for (let attempt = 0;; attempt += 1) {
|
|
137
|
+
const staging = `${path}.${randomBytes(12).toString('hex')}.partial`;
|
|
138
|
+
let created = false;
|
|
139
|
+
try {
|
|
140
|
+
await writeFile(staging, text, {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
mode: 0o600,
|
|
143
|
+
flag: 'wx',
|
|
144
|
+
});
|
|
145
|
+
created = true;
|
|
146
|
+
await rename(staging, path);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (created)
|
|
151
|
+
await unlink(staging).catch(() => { });
|
|
152
|
+
const code = error.code;
|
|
153
|
+
if (code === 'EEXIST' && attempt === 0)
|
|
154
|
+
continue;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
7
159
|
function shortSession(session) {
|
|
8
160
|
return session.length <= 16 ? session : `${session.slice(0, 12)}…`;
|
|
9
161
|
}
|
|
@@ -265,18 +417,28 @@ export function createDumper(options) {
|
|
|
265
417
|
});
|
|
266
418
|
// `wx`: a name collision fails loudly instead of overwriting a dump.
|
|
267
419
|
const write = (path, text) => writeFile(path, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
]
|
|
420
|
+
const writes = [
|
|
421
|
+
[files.body, body.text],
|
|
422
|
+
[files.metadata, `${JSON.stringify(metadata, null, 2)}\n`],
|
|
423
|
+
[files.request, `${JSON.stringify(request, null, 2)}\n`],
|
|
424
|
+
];
|
|
425
|
+
// Settle every write before judging the group, so a cleanup never
|
|
426
|
+
// races a write that is still in flight.
|
|
427
|
+
const settled = await Promise.allSettled(writes.map(([path, text]) => write(path, text)));
|
|
428
|
+
const failed = settled.find((result) => result.status === 'rejected');
|
|
429
|
+
if (failed) {
|
|
430
|
+
if (options.cleanupFailedDumps)
|
|
431
|
+
await removeGroup(writes, settled);
|
|
432
|
+
throw failed.reason;
|
|
433
|
+
}
|
|
273
434
|
remember(baselineKey, body.text);
|
|
274
435
|
log?.debug('dumped request', {
|
|
275
436
|
id,
|
|
276
437
|
session: shortSession(session),
|
|
277
438
|
body: files.body,
|
|
278
439
|
});
|
|
279
|
-
|
|
440
|
+
scheduleSweep(dir, Object.values(files));
|
|
441
|
+
return { id, files, responseFile: `${prefix}.response.json` };
|
|
280
442
|
}
|
|
281
443
|
catch (error) {
|
|
282
444
|
log?.warn('request dump failed', {
|
|
@@ -286,11 +448,76 @@ export function createDumper(options) {
|
|
|
286
448
|
return undefined;
|
|
287
449
|
}
|
|
288
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Remove what a failed dump wrote. A write that failed with EEXIST found
|
|
453
|
+
* someone else's file at that name and must leave it; any other failure
|
|
454
|
+
* either created nothing or left a partial file of this dump.
|
|
455
|
+
*/
|
|
456
|
+
async function removeGroup(writes, settled) {
|
|
457
|
+
await Promise.all(writes.map(async ([path], index) => {
|
|
458
|
+
const result = settled[index];
|
|
459
|
+
if (result?.status === 'rejected' &&
|
|
460
|
+
result.reason?.code === 'EEXIST') {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
await unlink(path).catch(() => { });
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
const maxBytes = options.maxBytes ?? 0;
|
|
467
|
+
const sweepIntervalMs = options.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
|
|
468
|
+
// Zero, so the first dump of a process sweeps whatever earlier processes
|
|
469
|
+
// left behind.
|
|
470
|
+
let lastSweepAt = 0;
|
|
471
|
+
function sweepOptions(dir, protectedPaths) {
|
|
472
|
+
return {
|
|
473
|
+
dir,
|
|
474
|
+
maxBytes,
|
|
475
|
+
protectedPaths,
|
|
476
|
+
now: now(),
|
|
477
|
+
minAgeMs: options.sweepMinAgeMs,
|
|
478
|
+
partialStaleMs: options.partialStaleMs,
|
|
479
|
+
logger: log,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/** Sweep in the background, off the request path, at most once per interval. */
|
|
483
|
+
function scheduleSweep(dir, protectedPaths) {
|
|
484
|
+
if (!(maxBytes > 0))
|
|
485
|
+
return;
|
|
486
|
+
const at = now();
|
|
487
|
+
if (at - lastSweepAt < sweepIntervalMs)
|
|
488
|
+
return;
|
|
489
|
+
lastSweepAt = at;
|
|
490
|
+
void sweepDumpDirectory(sweepOptions(dir, protectedPaths));
|
|
491
|
+
}
|
|
492
|
+
async function dumpResponse(result, input) {
|
|
493
|
+
if (!result)
|
|
494
|
+
return undefined;
|
|
495
|
+
const artifact = scrub({
|
|
496
|
+
status: input.status,
|
|
497
|
+
requestId: input.requestId,
|
|
498
|
+
usage: input.usage,
|
|
499
|
+
...input.fields,
|
|
500
|
+
complete: input.complete ?? true,
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
await replaceFile(result.responseFile, `${JSON.stringify(artifact, null, 2)}\n`);
|
|
504
|
+
return result.responseFile;
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
log?.warn('response dump failed', {
|
|
508
|
+
id: result.id,
|
|
509
|
+
error: error instanceof Error ? error.message : String(error),
|
|
510
|
+
});
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
289
514
|
return {
|
|
290
515
|
isEnabled: () => enabled,
|
|
291
516
|
setEnabled: (value) => {
|
|
292
517
|
enabled = value;
|
|
293
518
|
},
|
|
294
519
|
dump,
|
|
520
|
+
dumpResponse,
|
|
521
|
+
sweep: (protectedPaths) => sweepDumpDirectory(sweepOptions(resolveDir(), protectedPaths)),
|
|
295
522
|
};
|
|
296
523
|
}
|
package/dist/logger/engine.d.ts
CHANGED
|
@@ -15,30 +15,65 @@ export interface InitLoggerOptions extends RedactionOptions {
|
|
|
15
15
|
/** Level floor applied when no `setLogLevel` call has overridden it. */
|
|
16
16
|
level?: Level | (() => Level | undefined);
|
|
17
17
|
}
|
|
18
|
+
export interface ChannelLogger {
|
|
19
|
+
error(message: string, data?: unknown): void;
|
|
20
|
+
warn(message: string, data?: unknown): void;
|
|
21
|
+
info(message: string, data?: unknown): void;
|
|
22
|
+
debug(message: string, data?: unknown): void;
|
|
23
|
+
trace(message: string, data?: unknown): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A logger with its own file, level, redaction, capture sink and buffer.
|
|
27
|
+
* Two instances in one process never touch each other's settings or lines,
|
|
28
|
+
* so two plugins that load one shared copy of this module each keep their own
|
|
29
|
+
* log by holding their own instance.
|
|
30
|
+
*/
|
|
31
|
+
export interface LoggerInstance {
|
|
32
|
+
/** A logger whose lines carry `channel` and go to this instance's file. */
|
|
33
|
+
createLogger(channel: string): ChannelLogger;
|
|
34
|
+
/**
|
|
35
|
+
* Point this instance at a host's file and level. Idempotent: calling it
|
|
36
|
+
* again replaces both. A runtime level installed by `setLogLevel` is left
|
|
37
|
+
* alone, because it is the operator's explicit choice and outranks the
|
|
38
|
+
* floor a host computed at start-up.
|
|
39
|
+
*/
|
|
40
|
+
configure(options: InitLoggerOptions): void;
|
|
41
|
+
/** Operator override of the level floor; undefined removes it. */
|
|
42
|
+
setLogLevel(level: Level | undefined): void;
|
|
43
|
+
/**
|
|
44
|
+
* Write whatever is buffered. Safe to call synchronously from a
|
|
45
|
+
* process-exit handler, which is how each host drains the buffer on
|
|
46
|
+
* shutdown.
|
|
47
|
+
*/
|
|
48
|
+
flushLogs(): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A logger instance of its own, configured with `options`. A plugin whose
|
|
52
|
+
* copy of this module may be shared with another plugin in the same process
|
|
53
|
+
* uses this instead of `initLogger`, so neither replaces the other's file,
|
|
54
|
+
* level, redaction or capture sink.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createLoggerInstance(options: InitLoggerOptions): LoggerInstance;
|
|
18
57
|
/**
|
|
19
|
-
* Point the logger at a host's file and level
|
|
20
|
-
* replaces both. A runtime level installed
|
|
21
|
-
* left alone, because it is the operator's
|
|
22
|
-
* floor a host computed at start-up.
|
|
58
|
+
* Point the module's default logger at a host's file and level, and return
|
|
59
|
+
* it. Idempotent: calling it again replaces both. A runtime level installed
|
|
60
|
+
* by `setLogLevel` is deliberately left alone, because it is the operator's
|
|
61
|
+
* explicit choice and outranks the floor a host computed at start-up.
|
|
23
62
|
*/
|
|
24
|
-
export declare function initLogger(options: InitLoggerOptions):
|
|
63
|
+
export declare function initLogger(options: InitLoggerOptions): LoggerInstance;
|
|
25
64
|
export declare function setLogLevel(l: Level | undefined): void;
|
|
26
65
|
/**
|
|
27
|
-
* Write whatever
|
|
28
|
-
* handler, which is how each host drains the buffer on
|
|
66
|
+
* Write whatever the default logger has buffered. Safe to call synchronously
|
|
67
|
+
* from a process-exit handler, which is how each host drains the buffer on
|
|
68
|
+
* shutdown.
|
|
29
69
|
*/
|
|
30
70
|
export declare function flushLogs(): void;
|
|
31
|
-
export declare function createLogger(channel: string):
|
|
32
|
-
error: (m: string, d?: unknown) => void;
|
|
33
|
-
warn: (m: string, d?: unknown) => void;
|
|
34
|
-
info: (m: string, d?: unknown) => void;
|
|
35
|
-
debug: (m: string, d?: unknown) => void;
|
|
36
|
-
trace: (m: string, d?: unknown) => void;
|
|
37
|
-
};
|
|
71
|
+
export declare function createLogger(channel: string): ChannelLogger;
|
|
38
72
|
export declare function flushForTest(): Promise<void>;
|
|
39
73
|
/**
|
|
40
|
-
* Return the logger to its uninitialised state. Only a test needs
|
|
41
|
-
* single process runs every test file, so a file path left over from
|
|
42
|
-
* would keep a later "logger was never initialised" case writing
|
|
74
|
+
* Return the default logger to its uninitialised state. Only a test needs
|
|
75
|
+
* this: a single process runs every test file, so a file path left over from
|
|
76
|
+
* one test would keep a later "logger was never initialised" case writing
|
|
77
|
+
* lines.
|
|
43
78
|
*/
|
|
44
79
|
export declare function resetLoggerForTest(): void;
|