@cortexkit/common-auth 0.2.3 → 0.2.5
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/auth-menu/accounts.d.ts +88 -0
- package/dist/auth-menu/accounts.js +401 -0
- package/dist/auth-menu/ansi.d.ts +24 -0
- package/dist/auth-menu/ansi.js +67 -0
- package/dist/auth-menu/confirm.d.ts +7 -0
- package/dist/auth-menu/confirm.js +21 -0
- package/dist/auth-menu/doctor.d.ts +52 -0
- package/dist/auth-menu/doctor.js +81 -0
- package/dist/auth-menu/index.d.ts +17 -0
- package/dist/auth-menu/index.js +12 -0
- package/dist/auth-menu/login.d.ts +47 -0
- package/dist/auth-menu/login.js +63 -0
- package/dist/auth-menu/menu.d.ts +59 -0
- package/dist/auth-menu/menu.js +73 -0
- package/dist/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/auth-menu/opencode-v1.js +47 -0
- package/dist/auth-menu/select.d.ts +21 -0
- package/dist/auth-menu/select.js +153 -0
- package/dist/auth-menu/terminal.d.ts +43 -0
- package/dist/auth-menu/terminal.js +19 -0
- package/dist/cachekeep/index.d.ts +4 -0
- package/dist/cachekeep/index.js +2 -0
- package/dist/cachekeep/manager.d.ts +214 -0
- package/dist/cachekeep/manager.js +424 -0
- package/dist/cachekeep/window.d.ts +13 -0
- package/dist/cachekeep/window.js +33 -0
- package/dist/claustrum/index.d.ts +1 -0
- package/dist/claustrum/index.js +2 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +2 -0
- package/dist/dump/index.d.ts +90 -0
- package/dist/dump/index.js +292 -0
- package/dist/opencode2/index.d.ts +1 -0
- package/dist/opencode2/index.js +2 -0
- package/dist/store/errors.d.ts +2 -2
- package/dist/store/hooks.d.ts +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +3 -2
- package/dist/store/mutate.js +2 -1
- package/dist/store/pool.d.ts +8 -1
- package/dist/store/pool.js +2 -1
- package/dist/store/rows.d.ts +28 -1
- package/dist/store/rows.js +93 -0
- package/package.json +37 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a persisted `{ startHour, endHour }` pair. Anything other than two
|
|
3
|
+
* distinct whole hours in 0-23 is no window at all, which callers treat as
|
|
4
|
+
* "always warm".
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeCacheKeepWindow(value) {
|
|
7
|
+
if (!value || typeof value !== 'object')
|
|
8
|
+
return undefined;
|
|
9
|
+
const record = value;
|
|
10
|
+
const startHour = Number(record.startHour);
|
|
11
|
+
const endHour = Number(record.endHour);
|
|
12
|
+
if (!Number.isInteger(startHour) ||
|
|
13
|
+
!Number.isInteger(endHour) ||
|
|
14
|
+
startHour < 0 ||
|
|
15
|
+
startHour > 23 ||
|
|
16
|
+
endHour < 0 ||
|
|
17
|
+
endHour > 23 ||
|
|
18
|
+
startHour === endHour) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return { startHour, endHour };
|
|
22
|
+
}
|
|
23
|
+
/** Whether the local hour of `now` falls inside the window; no window is never inside. */
|
|
24
|
+
export function isWithinCacheKeepWindow(window, now = new Date()) {
|
|
25
|
+
if (!window)
|
|
26
|
+
return false;
|
|
27
|
+
const hour = now.getHours();
|
|
28
|
+
if (window.startHour < window.endHour) {
|
|
29
|
+
return hour >= window.startHour && hour < window.endHour;
|
|
30
|
+
}
|
|
31
|
+
// Overnight wrap (e.g. 22-6): in window at or after the start, or before the end.
|
|
32
|
+
return hour >= window.startHour || hour < window.endHour;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export interface DumpLogger {
|
|
2
|
+
debug(message: string, data?: unknown): void;
|
|
3
|
+
warn(message: string, data?: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
export interface DumpOptions {
|
|
6
|
+
/** Directory dumps are written to; a function is read on every dump. */
|
|
7
|
+
dir: string | (() => string);
|
|
8
|
+
/** Initial state; `setEnabled` switches it at runtime. Default off. */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/** Written into every filename. Default the current process id. */
|
|
11
|
+
pid?: number;
|
|
12
|
+
/** Clock for filenames and `createdAt`. Default `Date.now`. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
logger?: DumpLogger;
|
|
15
|
+
/**
|
|
16
|
+
* Key names redacted wherever they appear in headers, request metadata and
|
|
17
|
+
* the body, on top of the common credential set. Compared lower-cased with
|
|
18
|
+
* `-` and `_` removed, so `ChatGPT-Account-Id` and `chatgptAccountId` match
|
|
19
|
+
* `chatgptaccountid`.
|
|
20
|
+
*/
|
|
21
|
+
secretKeys?: readonly string[];
|
|
22
|
+
/** Value patterns scrubbed from every string, on top of the common token patterns. */
|
|
23
|
+
secretPatterns?: readonly RegExp[];
|
|
24
|
+
/**
|
|
25
|
+
* Keys, at any depth of the body, whose values are declarations rather than
|
|
26
|
+
* data: their strings are scrubbed but their key names are never redacted.
|
|
27
|
+
* Default `['tools']`.
|
|
28
|
+
*/
|
|
29
|
+
schemaKeys?: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Provider summary of the parsed body, stored in the metadata as `body`.
|
|
32
|
+
* It receives the redacted body.
|
|
33
|
+
*/
|
|
34
|
+
summarize?: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface DumpInput {
|
|
37
|
+
/** The session the request belongs to; missing means `session-unknown`. */
|
|
38
|
+
session?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Diff baseline group: a dump is diffed against the previous dump of the
|
|
41
|
+
* same session and channel (for example, one per transport).
|
|
42
|
+
*/
|
|
43
|
+
channel: string;
|
|
44
|
+
/** Finer label for the filename, e.g. prewarm or main; it does not split the baseline. */
|
|
45
|
+
phase?: string;
|
|
46
|
+
bodyText: string;
|
|
47
|
+
accountId?: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
method?: string;
|
|
50
|
+
headers?: ConstructorParameters<typeof Headers>[0];
|
|
51
|
+
status?: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
/** Extra metadata, redacted like the body. */
|
|
54
|
+
meta?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
export interface DumpDiff {
|
|
57
|
+
changed: boolean;
|
|
58
|
+
firstByte: number;
|
|
59
|
+
lastPreviousByte: number;
|
|
60
|
+
lastCurrentByte: number;
|
|
61
|
+
changedPreviousBytes?: number;
|
|
62
|
+
changedCurrentBytes?: number;
|
|
63
|
+
previousBytes: number;
|
|
64
|
+
currentBytes: number;
|
|
65
|
+
}
|
|
66
|
+
export interface DumpResult {
|
|
67
|
+
id: string;
|
|
68
|
+
files: {
|
|
69
|
+
body: string;
|
|
70
|
+
metadata: string;
|
|
71
|
+
request: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface Dumper {
|
|
75
|
+
isEnabled(): boolean;
|
|
76
|
+
setEnabled(enabled: boolean): void;
|
|
77
|
+
/**
|
|
78
|
+
* Write `<id>.body.json`, `<id>.meta.json` and `<id>.request.json`. Returns
|
|
79
|
+
* undefined when dumps are off or the write failed; dumping is diagnostic
|
|
80
|
+
* and never throws into the request path.
|
|
81
|
+
*/
|
|
82
|
+
dump(input: DumpInput): Promise<DumpResult | undefined>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Where two bodies first and last differ, in string offsets into the dumped
|
|
86
|
+
* (redacted) text. Offsets count UTF-16 units, as the plugins' cache analysis
|
|
87
|
+
* always has.
|
|
88
|
+
*/
|
|
89
|
+
export declare function diffDumpBodies(previous: string | undefined, current: string): DumpDiff | null;
|
|
90
|
+
export declare function createDumper(options: DumpOptions): Dumper;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createRedactor } from '../logger/redact.js';
|
|
5
|
+
const PREVIOUS_BODY_LIMIT = 100;
|
|
6
|
+
const UNKNOWN_SESSION = 'session-unknown';
|
|
7
|
+
function shortSession(session) {
|
|
8
|
+
return session.length <= 16 ? session : `${session.slice(0, 12)}…`;
|
|
9
|
+
}
|
|
10
|
+
function fileSegment(value, fallback) {
|
|
11
|
+
const normalized = value
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '');
|
|
15
|
+
if (!normalized)
|
|
16
|
+
return fallback;
|
|
17
|
+
return normalized.slice(0, 80);
|
|
18
|
+
}
|
|
19
|
+
function hashText(value) {
|
|
20
|
+
return createHash('sha256').update(value).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
function normalizeKey(key) {
|
|
23
|
+
return key.toLowerCase().replace(/[-_]/g, '');
|
|
24
|
+
}
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Where two bodies first and last differ, in string offsets into the dumped
|
|
30
|
+
* (redacted) text. Offsets count UTF-16 units, as the plugins' cache analysis
|
|
31
|
+
* always has.
|
|
32
|
+
*/
|
|
33
|
+
export function diffDumpBodies(previous, current) {
|
|
34
|
+
if (previous === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
if (previous === current) {
|
|
37
|
+
return {
|
|
38
|
+
changed: false,
|
|
39
|
+
firstByte: -1,
|
|
40
|
+
lastPreviousByte: -1,
|
|
41
|
+
lastCurrentByte: -1,
|
|
42
|
+
previousBytes: previous.length,
|
|
43
|
+
currentBytes: current.length,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let firstByte = 0;
|
|
47
|
+
while (firstByte < previous.length &&
|
|
48
|
+
firstByte < current.length &&
|
|
49
|
+
previous[firstByte] === current[firstByte]) {
|
|
50
|
+
firstByte++;
|
|
51
|
+
}
|
|
52
|
+
let previousTail = previous.length - 1;
|
|
53
|
+
let currentTail = current.length - 1;
|
|
54
|
+
while (previousTail >= firstByte &&
|
|
55
|
+
currentTail >= firstByte &&
|
|
56
|
+
previous[previousTail] === current[currentTail]) {
|
|
57
|
+
previousTail--;
|
|
58
|
+
currentTail--;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
changed: true,
|
|
62
|
+
firstByte,
|
|
63
|
+
lastPreviousByte: previousTail,
|
|
64
|
+
lastCurrentByte: currentTail,
|
|
65
|
+
changedPreviousBytes: previousTail - firstByte + 1,
|
|
66
|
+
changedCurrentBytes: currentTail - firstByte + 1,
|
|
67
|
+
previousBytes: previous.length,
|
|
68
|
+
currentBytes: current.length,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createDumper(options) {
|
|
72
|
+
let enabled = options.enabled === true;
|
|
73
|
+
let counter = 0;
|
|
74
|
+
const pid = options.pid ?? process.pid;
|
|
75
|
+
const now = options.now ?? Date.now;
|
|
76
|
+
const log = options.logger;
|
|
77
|
+
const extraKeys = new Set((options.secretKeys ?? []).map(normalizeKey));
|
|
78
|
+
const redactor = createRedactor({
|
|
79
|
+
extraSecretKeys: (normalized) => extraKeys.has(normalized),
|
|
80
|
+
extraValuePatterns: [...(options.secretPatterns ?? [])],
|
|
81
|
+
});
|
|
82
|
+
const schemaKeys = new Set(options.schemaKeys ?? ['tools']);
|
|
83
|
+
/**
|
|
84
|
+
* Last dumped (redacted) body per baseline key, in memory for this process.
|
|
85
|
+
* A key missing here, on its first dump or after eviction, is looked up on
|
|
86
|
+
* disk.
|
|
87
|
+
*/
|
|
88
|
+
const previousBodies = new Map();
|
|
89
|
+
const resolveDir = () => typeof options.dir === 'function' ? options.dir() : options.dir;
|
|
90
|
+
/** The mask for a secret key name, or undefined for an ordinary key. */
|
|
91
|
+
function maskFor(key) {
|
|
92
|
+
// The shared redactor masks a secret key whatever its value, so probing it
|
|
93
|
+
// with null answers "is this key a secret" without duplicating its rules.
|
|
94
|
+
const masked = redactor.redact({ [key]: null })[key];
|
|
95
|
+
return masked === null ? undefined : masked;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Redact by key name and scrub token-shaped strings, except below a schema
|
|
99
|
+
* key, where only strings are scrubbed: a tool parameter named `api_key`
|
|
100
|
+
* declares what the tool accepts, and masking that node would leave a
|
|
101
|
+
* schema that no longer parses. Key order is kept because dumps are diffed
|
|
102
|
+
* against each other.
|
|
103
|
+
*/
|
|
104
|
+
function scrub(value) {
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return value.map(scrub);
|
|
107
|
+
if (isRecord(value)) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
110
|
+
const mask = maskFor(key);
|
|
111
|
+
if (schemaKeys.has(key))
|
|
112
|
+
out[key] = redactor.redactStrings(entry);
|
|
113
|
+
else if (mask !== undefined)
|
|
114
|
+
out[key] = mask;
|
|
115
|
+
else
|
|
116
|
+
out[key] = scrub(entry);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
return redactor.redactStrings(value);
|
|
121
|
+
}
|
|
122
|
+
function scrubBody(bodyText) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(bodyText);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Not JSON: no keys to judge, but token-shaped strings still go.
|
|
129
|
+
return {
|
|
130
|
+
text: redactor.redactStrings(bodyText),
|
|
131
|
+
parsed: undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const scrubbed = scrub(parsed);
|
|
135
|
+
const scrubbedText = JSON.stringify(scrubbed);
|
|
136
|
+
// Nothing redacted: keep the original bytes, whitespace included, so the
|
|
137
|
+
// dump is exactly what was sent.
|
|
138
|
+
const text = JSON.stringify(parsed) === scrubbedText ? bodyText : scrubbedText;
|
|
139
|
+
return { text, parsed: isRecord(scrubbed) ? scrubbed : undefined };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The newest earlier dump of this baseline key, read back from the dump
|
|
143
|
+
* directory. The in-memory baseline dies with the process, and the first
|
|
144
|
+
* request after a restart is the one most likely to break the prompt cache,
|
|
145
|
+
* so it is the one whose diff matters most. Reading the dumps themselves
|
|
146
|
+
* needs no extra state, and clearing the directory clears the baseline with
|
|
147
|
+
* it. Any failure is no baseline.
|
|
148
|
+
*/
|
|
149
|
+
async function recoverBaseline(dir, session, channel, baselineKey) {
|
|
150
|
+
const marker = `-${fileSegment(session, UNKNOWN_SESSION)}-${fileSegment(channel, 'channel')}`;
|
|
151
|
+
let names;
|
|
152
|
+
try {
|
|
153
|
+
names = await readdir(dir);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
// Names start with an ISO timestamp, so reverse lexicographic order is
|
|
159
|
+
// newest first. The metadata's key decides the match: a filename segment
|
|
160
|
+
// is truncated and sanitised, so two sessions can share one.
|
|
161
|
+
const candidates = names
|
|
162
|
+
.filter((name) => name.endsWith('.meta.json') && name.includes(marker))
|
|
163
|
+
.sort()
|
|
164
|
+
.reverse();
|
|
165
|
+
for (const name of candidates) {
|
|
166
|
+
try {
|
|
167
|
+
const metadata = JSON.parse(await readFile(join(dir, name), 'utf8'));
|
|
168
|
+
if (metadata?.baselineKey !== baselineKey)
|
|
169
|
+
continue;
|
|
170
|
+
return await readFile(join(dir, name.replace(/\.meta\.json$/, '.body.json')), 'utf8');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// A file removed or half-written by another process: try the next.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
function remember(key, text) {
|
|
179
|
+
if (!previousBodies.has(key)) {
|
|
180
|
+
while (previousBodies.size >= PREVIOUS_BODY_LIMIT) {
|
|
181
|
+
const oldest = previousBodies.keys().next().value;
|
|
182
|
+
if (oldest === undefined)
|
|
183
|
+
break;
|
|
184
|
+
previousBodies.delete(oldest);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
previousBodies.set(key, text);
|
|
188
|
+
}
|
|
189
|
+
async function dump(input) {
|
|
190
|
+
if (!enabled)
|
|
191
|
+
return undefined;
|
|
192
|
+
counter++;
|
|
193
|
+
const session = input.session?.trim() || UNKNOWN_SESSION;
|
|
194
|
+
const dir = resolveDir();
|
|
195
|
+
const createdAt = new Date(now()).toISOString();
|
|
196
|
+
// The pid makes names unique across processes: each process starts its
|
|
197
|
+
// counter at zero, so two processes dumping the same session in the same
|
|
198
|
+
// millisecond into a shared directory would otherwise pick the same name.
|
|
199
|
+
const id = [
|
|
200
|
+
createdAt.replace(/[:.]/g, '-'),
|
|
201
|
+
String(pid),
|
|
202
|
+
String(counter).padStart(6, '0'),
|
|
203
|
+
fileSegment(session, UNKNOWN_SESSION),
|
|
204
|
+
fileSegment(input.channel, 'channel'),
|
|
205
|
+
...(input.phase ? [fileSegment(input.phase, 'phase')] : []),
|
|
206
|
+
].join('-');
|
|
207
|
+
const prefix = join(dir, id);
|
|
208
|
+
const files = {
|
|
209
|
+
body: `${prefix}.body.json`,
|
|
210
|
+
metadata: `${prefix}.meta.json`,
|
|
211
|
+
request: `${prefix}.request.json`,
|
|
212
|
+
};
|
|
213
|
+
// Hashed so the metadata identifies the baseline without spelling out the
|
|
214
|
+
// full session id.
|
|
215
|
+
const baselineKey = hashText(`${input.channel}\u0000${session}`);
|
|
216
|
+
try {
|
|
217
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
218
|
+
// mkdir's mode does not apply to a directory that already exists.
|
|
219
|
+
await chmod(dir, 0o700);
|
|
220
|
+
let previous = previousBodies.get(baselineKey);
|
|
221
|
+
let baselineSource = previous === undefined ? undefined : 'memory';
|
|
222
|
+
if (previous === undefined) {
|
|
223
|
+
previous = await recoverBaseline(dir, session, input.channel, baselineKey);
|
|
224
|
+
if (previous !== undefined)
|
|
225
|
+
baselineSource = 'disk';
|
|
226
|
+
}
|
|
227
|
+
const body = scrubBody(input.bodyText);
|
|
228
|
+
const metadata = {
|
|
229
|
+
id,
|
|
230
|
+
createdAt,
|
|
231
|
+
pid,
|
|
232
|
+
session: shortSession(session),
|
|
233
|
+
channel: input.channel,
|
|
234
|
+
...(input.phase ? { phase: input.phase } : {}),
|
|
235
|
+
accountId: input.accountId,
|
|
236
|
+
status: input.status,
|
|
237
|
+
error: input.error === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: redactor.redactStrings(input.error),
|
|
240
|
+
baselineKey,
|
|
241
|
+
bodyBytes: Buffer.byteLength(input.bodyText, 'utf8'),
|
|
242
|
+
bodyHash: hashText(input.bodyText),
|
|
243
|
+
// Diffed on the redacted text that lands in the body file, so offsets
|
|
244
|
+
// index the file an operator can open, and a baseline read back from
|
|
245
|
+
// disk (also redacted) compares like with like.
|
|
246
|
+
diff: diffDumpBodies(previous, body.text),
|
|
247
|
+
baselineSource,
|
|
248
|
+
body: body.parsed
|
|
249
|
+
? { parseable: true, ...options.summarize?.(body.parsed) }
|
|
250
|
+
: { parseable: false },
|
|
251
|
+
...(input.meta ? { extra: scrub(input.meta) } : {}),
|
|
252
|
+
files,
|
|
253
|
+
};
|
|
254
|
+
const request = scrub({
|
|
255
|
+
url: input.url,
|
|
256
|
+
method: input.method,
|
|
257
|
+
accountId: input.accountId,
|
|
258
|
+
headers: input.headers === undefined
|
|
259
|
+
? undefined
|
|
260
|
+
: Object.fromEntries(new Headers(input.headers).entries()),
|
|
261
|
+
});
|
|
262
|
+
// `wx`: a name collision fails loudly instead of overwriting a dump.
|
|
263
|
+
const write = (path, text) => writeFile(path, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
264
|
+
await Promise.all([
|
|
265
|
+
write(files.body, body.text),
|
|
266
|
+
write(files.metadata, `${JSON.stringify(metadata, null, 2)}\n`),
|
|
267
|
+
write(files.request, `${JSON.stringify(request, null, 2)}\n`),
|
|
268
|
+
]);
|
|
269
|
+
remember(baselineKey, body.text);
|
|
270
|
+
log?.debug('dumped request', {
|
|
271
|
+
id,
|
|
272
|
+
session: shortSession(session),
|
|
273
|
+
body: files.body,
|
|
274
|
+
});
|
|
275
|
+
return { id, files };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
log?.warn('request dump failed', {
|
|
279
|
+
session: shortSession(session),
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
isEnabled: () => enabled,
|
|
287
|
+
setEnabled: (value) => {
|
|
288
|
+
enabled = value;
|
|
289
|
+
},
|
|
290
|
+
dump,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/store/errors.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StoredCredential } from './schema.js';
|
|
2
2
|
/** Every library operation that can fail, as named in the failure value. */
|
|
3
|
-
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'recordIdentity' | 'refresh' | 'pull';
|
|
3
|
+
export type PoolOperation = 'initialize' | 'add' | 'replace' | 'rotate' | 'disable' | 'enable' | 'remove' | 'reorder' | 'recordIdentity' | 'refresh' | 'pull';
|
|
4
4
|
/**
|
|
5
5
|
* How far an operation got before it failed.
|
|
6
6
|
*
|
|
@@ -16,7 +16,7 @@ export type PoolFailurePhase = 'before-first-write' | 'after-first-write' | 'pul
|
|
|
16
16
|
* lock outcomes (a wait that ran out, and a lease found lost); the rest are
|
|
17
17
|
* refusals and failures of the operation itself.
|
|
18
18
|
*/
|
|
19
|
-
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'row-key-changed' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
19
|
+
export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
|
|
20
20
|
/**
|
|
21
21
|
* The single failure value of every store operation. `committed` is present
|
|
22
22
|
* only when the operation had already written a credential to the state file
|
package/dist/store/hooks.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ export interface PoolLogger {
|
|
|
9
9
|
* Runs a failure hook. A hook that throws never replaces the failure it was
|
|
10
10
|
* handed: its exception is logged and discarded.
|
|
11
11
|
*/
|
|
12
|
-
export declare function callFailureHook<E>(operation: PoolOperation, hook: ((rowId:
|
|
12
|
+
export declare function callFailureHook<E, R extends string | undefined>(operation: PoolOperation, hook: ((rowId: R, error: E) => void | Promise<void>) | undefined, rowId: R, error: E, logger: PoolLogger | undefined): Promise<void>;
|
package/dist/store/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { PullHook, PullRequest } from './pull.js';
|
|
|
10
10
|
export type { ProviderRefresh, ProviderRefreshResult, RefreshOptions, RefreshOutcome, } from './refresh.js';
|
|
11
11
|
export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.js';
|
|
12
12
|
export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
|
|
13
|
-
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
13
|
+
export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
|
|
14
14
|
export type { PullReason } from './runtime.js';
|
|
15
15
|
export type { ApiKeyCredential, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, StoredCredential, } from './schema.js';
|
|
16
16
|
export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
|
package/dist/store/mutate.d.ts
CHANGED
|
@@ -127,6 +127,7 @@ export declare function toFailure(error: unknown, operation: PoolOperation, rowI
|
|
|
127
127
|
* The frame every lock-holding operation runs in: failures are mapped onto
|
|
128
128
|
* the failure value, handed to the failure hook while the outer locks are
|
|
129
129
|
* still held (the store locks are already released), and rethrown; every
|
|
130
|
-
* lock is released afterwards.
|
|
130
|
+
* lock is released afterwards. `rowId` is undefined for an operation that
|
|
131
|
+
* names no row (`reorder`).
|
|
131
132
|
*/
|
|
132
|
-
export declare function runOperation<T>(ctx: StoreContext, operation: PoolOperation, rowId:
|
|
133
|
+
export declare function runOperation<T, R extends string | undefined = string>(ctx: StoreContext, operation: PoolOperation, rowId: R, onFailure: ((rowId: R, error: PoolOperationError) => void | Promise<void>) | undefined, body: (locks: LockStack, progress: Progress) => Promise<T>): Promise<T>;
|
package/dist/store/mutate.js
CHANGED
|
@@ -335,7 +335,8 @@ export function toFailure(error, operation, rowId, progress) {
|
|
|
335
335
|
* The frame every lock-holding operation runs in: failures are mapped onto
|
|
336
336
|
* the failure value, handed to the failure hook while the outer locks are
|
|
337
337
|
* still held (the store locks are already released), and rethrown; every
|
|
338
|
-
* lock is released afterwards.
|
|
338
|
+
* lock is released afterwards. `rowId` is undefined for an operation that
|
|
339
|
+
* names no row (`reorder`).
|
|
339
340
|
*/
|
|
340
341
|
export async function runOperation(ctx, operation, rowId, onFailure, body) {
|
|
341
342
|
const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
|
package/dist/store/pool.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type HoldPoint, type InitializeOutcome, type StoreContext } from './mut
|
|
|
5
5
|
import { type PullHook } from './pull.js';
|
|
6
6
|
import { type ProviderRefresh, type RefreshOptions, type RefreshOutcome } from './refresh.js';
|
|
7
7
|
import { type LockEnvironment, type PoolLockOptions, type PoolLockSpec } from './refresh-lock.js';
|
|
8
|
-
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
8
|
+
import { type AddInput, type AddResult, type RemoveOptions, type RemoveResult, type ReorderOptions, type ReorderResult, type RowOperationOptions, type RowToggleOptions } from './rows.js';
|
|
9
9
|
import { type PoolCredential, type PoolRow, type QuotaCodec, type StoredCredential } from './schema.js';
|
|
10
10
|
export interface OpenPoolStoreOptions {
|
|
11
11
|
/** The provider every row of this pool belongs to; keys the provider-wide lock. */
|
|
@@ -101,6 +101,13 @@ export interface PoolStore {
|
|
|
101
101
|
* (since 0.2.3). Locks as `disable`; `protect` can refuse the id.
|
|
102
102
|
*/
|
|
103
103
|
remove(id: string, options?: RemoveOptions): Promise<RemoveResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Sets the roster order (since 0.2.4) in one config write. `ids` must name
|
|
106
|
+
* every roster id exactly once; anything else refuses with `invalid-order`
|
|
107
|
+
* and writes nothing. Takes `extraLocks`, then the store locks; no row or
|
|
108
|
+
* provider-wide lock. Roster rows and their entries are left unchanged.
|
|
109
|
+
*/
|
|
110
|
+
reorder(ids: readonly string[], options?: ReorderOptions): Promise<ReorderResult>;
|
|
104
111
|
recordIdentity(id: string, identity: string, options?: RowOperationOptions): Promise<{
|
|
105
112
|
id: string;
|
|
106
113
|
disabled: string[];
|
package/dist/store/pool.js
CHANGED
|
@@ -3,7 +3,7 @@ import { initializePool, readPool, } from './mutate.js';
|
|
|
3
3
|
import { PullScheduler } from './pull.js';
|
|
4
4
|
import { refreshRow, } from './refresh.js';
|
|
5
5
|
import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
|
|
6
|
-
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, replaceRow, rotateRow, } from './rows.js';
|
|
6
|
+
import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, reorderRows, replaceRow, rotateRow, } from './rows.js';
|
|
7
7
|
import { POOL_SCHEMA_VERSION, } from './schema.js';
|
|
8
8
|
/**
|
|
9
9
|
* Process-wide memory per config file: ids whose per-row entry a library
|
|
@@ -94,6 +94,7 @@ export function openPoolStore(options) {
|
|
|
94
94
|
disable: (id, reason, callOptions) => disableRow(rt, id, reason, callOptions),
|
|
95
95
|
enable: (id, callOptions) => enableRow(rt, id, callOptions),
|
|
96
96
|
remove: (id, callOptions) => removeRow(rt, id, callOptions),
|
|
97
|
+
reorder: (ids, callOptions) => reorderRows(rt, ids, callOptions),
|
|
97
98
|
recordIdentity: (id, identity, callOptions) => recordRowIdentity(rt, id, identity, callOptions),
|
|
98
99
|
refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
|
|
99
100
|
recordQuota: (id, attribution, observation) => recordQuota(rt, id, attribution, observation),
|
package/dist/store/rows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PoolOperationError } from './errors.js';
|
|
2
2
|
import { type Transaction } from './mutate.js';
|
|
3
3
|
import type { PoolLockSpec } from './refresh-lock.js';
|
|
4
4
|
import { type StoreRuntime } from './runtime.js';
|
|
@@ -57,6 +57,23 @@ export type RemoveResult = {
|
|
|
57
57
|
*/
|
|
58
58
|
outcome: 'removed' | 'completed';
|
|
59
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Options of `reorder`. It names no row, so its failure hook is handed only
|
|
62
|
+
* the failure; it takes no row lock and no provider-wide lock, so the extra
|
|
63
|
+
* locks are taken first, then the store locks.
|
|
64
|
+
*/
|
|
65
|
+
export interface ReorderOptions {
|
|
66
|
+
/** Called once, awaited, on every non-success path, before the extra locks release. */
|
|
67
|
+
onFailure?: (error: PoolOperationError) => void | Promise<void>;
|
|
68
|
+
/** Locks taken, in this order, before the store locks. */
|
|
69
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
70
|
+
}
|
|
71
|
+
export type ReorderResult = {
|
|
72
|
+
/** The roster order now on disk. */
|
|
73
|
+
ids: string[];
|
|
74
|
+
/** `unchanged` when the roster was already in this order; nothing was written. */
|
|
75
|
+
outcome: 'reordered' | 'unchanged';
|
|
76
|
+
};
|
|
60
77
|
export interface AddInput {
|
|
61
78
|
id: string;
|
|
62
79
|
credential: PoolCredential;
|
|
@@ -119,6 +136,16 @@ export declare function enableRow(rt: StoreRuntime, id: string, options?: RowTog
|
|
|
119
136
|
* not reused by `add` in this process.
|
|
120
137
|
*/
|
|
121
138
|
export declare function removeRow(rt: StoreRuntime, id: string, options?: RemoveOptions): Promise<RemoveResult>;
|
|
139
|
+
/**
|
|
140
|
+
* Sets the roster order in one config write. `ids` must name every roster id
|
|
141
|
+
* exactly once; anything else refuses (`invalid-order`) before writing. The
|
|
142
|
+
* roster rows, the per-row entries and the state file are left as they are:
|
|
143
|
+
* only the order of the legacy `accounts` array changes, which older readers
|
|
144
|
+
* load as is. It takes the extra locks, then the store locks, and no row or
|
|
145
|
+
* provider-wide lock, since no row's credential, identity or quota changes.
|
|
146
|
+
* An order equal to the current one writes nothing.
|
|
147
|
+
*/
|
|
148
|
+
export declare function reorderRows(rt: StoreRuntime, ids: readonly string[], options?: ReorderOptions): Promise<ReorderResult>;
|
|
122
149
|
export declare function recordRowIdentity(rt: StoreRuntime, id: string, identity: string, options?: RowOperationOptions): Promise<{
|
|
123
150
|
id: string;
|
|
124
151
|
disabled: string[];
|