@cortexkit/common-auth 0.3.0 → 0.4.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/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 +237 -45
- 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 +14 -2
- package/dist/store/errors.d.ts +6 -3
- package/dist/store/identity.d.ts +13 -4
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +27 -4
- package/dist/store/mutate.js +43 -26
- package/dist/store/pool.d.ts +16 -3
- package/dist/store/pool.js +7 -2
- package/dist/store/rows.d.ts +20 -6
- package/dist/store/rows.js +141 -46
- package/dist/store/schema.d.ts +74 -5
- package/dist/store/schema.js +112 -10
- package/dist/store/torn.d.ts +29 -0
- package/dist/store/torn.js +113 -0
- package/package.json +1 -1
package/dist/commands/menu.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// caller's context.
|
|
5
5
|
import { createLogger } from '../logger/index.js';
|
|
6
6
|
import { builtinSections, } from './builtins.js';
|
|
7
|
-
import { applyResult, confirmationOf, dialogPayload, } from './seam.js';
|
|
7
|
+
import { applyResult, confirmationOf, createTextRedactor, dialogPayload, projectFailure, } from './seam.js';
|
|
8
8
|
const BUILTIN_IDS = new Set([
|
|
9
9
|
'accounts',
|
|
10
10
|
'quota',
|
|
@@ -19,12 +19,16 @@ const BUILTIN_IDS = new Set([
|
|
|
19
19
|
* reuses and rebinds one context object for the next session cannot pull an
|
|
20
20
|
* earlier invocation's feedback over to it.
|
|
21
21
|
*/
|
|
22
|
-
function ownInvocation(invocation) {
|
|
22
|
+
function ownInvocation(invocation, redact) {
|
|
23
23
|
const { sessionId } = invocation;
|
|
24
24
|
const notify = invocation.notify.bind(invocation);
|
|
25
|
+
// A notification leaves the process just as a payload does, so its text
|
|
26
|
+
// goes through the same redactor.
|
|
25
27
|
return Object.freeze({
|
|
26
28
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
27
|
-
notify,
|
|
29
|
+
notify: (message, kind) => kind === undefined
|
|
30
|
+
? notify(redact(String(message)))
|
|
31
|
+
: notify(redact(String(message)), kind),
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
function coerceOne(knob, raw) {
|
|
@@ -94,6 +98,8 @@ function findAction(sections, request) {
|
|
|
94
98
|
}
|
|
95
99
|
export function createCommandMenu(options) {
|
|
96
100
|
const logger = options.logger ?? createLogger('commands');
|
|
101
|
+
const redact = createTextRedactor(options.redaction);
|
|
102
|
+
const seam = { logger, redact };
|
|
97
103
|
const now = options.now ?? Date.now;
|
|
98
104
|
const extras = options.extras ?? [];
|
|
99
105
|
const seen = new Set();
|
|
@@ -116,7 +122,7 @@ export function createCommandMenu(options) {
|
|
|
116
122
|
logger.warn('command menu section failed to build', {
|
|
117
123
|
command: options.command,
|
|
118
124
|
section: id,
|
|
119
|
-
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
error: redact(error instanceof Error ? error.message : String(error)),
|
|
120
126
|
});
|
|
121
127
|
return {
|
|
122
128
|
id,
|
|
@@ -148,30 +154,36 @@ export function createCommandMenu(options) {
|
|
|
148
154
|
return {
|
|
149
155
|
command: options.command,
|
|
150
156
|
async open(invocation) {
|
|
151
|
-
const own = ownInvocation(invocation);
|
|
152
|
-
return dialogPayload(options.command, options.title, await sections(own),
|
|
157
|
+
const own = ownInvocation(invocation, redact);
|
|
158
|
+
return dialogPayload(options.command, options.title, await sections(own), seam);
|
|
153
159
|
},
|
|
154
160
|
async apply(request, invocation) {
|
|
155
|
-
const own = ownInvocation(invocation);
|
|
156
|
-
const finish = async (outcome) => applyResult(options.command, options.title, await sections(own), outcome,
|
|
161
|
+
const own = ownInvocation(invocation, redact);
|
|
162
|
+
const finish = async (outcome) => applyResult(options.command, options.title, await sections(own), outcome, seam);
|
|
157
163
|
const action = request.command === options.command
|
|
158
164
|
? findAction(await sections(own), request)
|
|
159
165
|
: undefined;
|
|
160
166
|
if (!action)
|
|
161
167
|
return finish({
|
|
162
168
|
ok: false,
|
|
169
|
+
code: 'unavailable',
|
|
163
170
|
text: 'That action is no longer available.',
|
|
164
171
|
});
|
|
165
172
|
const confirmation = confirmationOf(action);
|
|
166
173
|
if (confirmation && request.confirmed !== true)
|
|
167
174
|
return finish({
|
|
168
175
|
ok: false,
|
|
176
|
+
code: 'needs-confirmation',
|
|
169
177
|
text: confirmation.message,
|
|
170
178
|
needsConfirmation: true,
|
|
171
179
|
});
|
|
172
180
|
const coerced = coerceValues(action.knobs ?? [], request.values);
|
|
173
181
|
if ('problem' in coerced)
|
|
174
|
-
return finish({
|
|
182
|
+
return finish({
|
|
183
|
+
ok: false,
|
|
184
|
+
code: 'invalid-input',
|
|
185
|
+
text: coerced.problem,
|
|
186
|
+
});
|
|
175
187
|
let outcome;
|
|
176
188
|
try {
|
|
177
189
|
const result = await action.run({
|
|
@@ -180,17 +192,26 @@ export function createCommandMenu(options) {
|
|
|
180
192
|
invocation: own,
|
|
181
193
|
});
|
|
182
194
|
outcome =
|
|
183
|
-
typeof result === 'string'
|
|
195
|
+
typeof result === 'string'
|
|
196
|
+
? { ok: true, text: result }
|
|
197
|
+
: {
|
|
198
|
+
ok: result.ok,
|
|
199
|
+
text: result.text,
|
|
200
|
+
...(result.ok ? {} : { code: result.code ?? 'refused' }),
|
|
201
|
+
};
|
|
184
202
|
}
|
|
185
203
|
catch (error) {
|
|
186
|
-
|
|
204
|
+
// The raw text goes only to the log (redacted); the user sees the
|
|
205
|
+
// projected code and message, never the exception's own words.
|
|
206
|
+
const failure = projectFailure(error);
|
|
187
207
|
logger.warn('command menu action failed', {
|
|
188
208
|
command: options.command,
|
|
189
209
|
section: request.sectionId,
|
|
190
210
|
action: request.actionId,
|
|
191
|
-
|
|
211
|
+
code: failure.code,
|
|
212
|
+
error: redact(error instanceof Error ? error.message : String(error)),
|
|
192
213
|
});
|
|
193
|
-
outcome = { ok: false,
|
|
214
|
+
outcome = { ok: false, ...failure };
|
|
194
215
|
}
|
|
195
216
|
return finish(outcome);
|
|
196
217
|
},
|
package/dist/commands/model.d.ts
CHANGED
|
@@ -120,6 +120,13 @@ export interface CommandApplyResult {
|
|
|
120
120
|
command: string;
|
|
121
121
|
ok: boolean;
|
|
122
122
|
text: string;
|
|
123
|
+
/**
|
|
124
|
+
* A stable code naming why the apply failed; present on every failure.
|
|
125
|
+
* The library's own codes are `unavailable`, `needs-confirmation`,
|
|
126
|
+
* `invalid-input`, `refused`, `action-failed` and `pool-<store failure
|
|
127
|
+
* kind>`; a plugin's `CommandError` or `ActionOutcome` names its own.
|
|
128
|
+
*/
|
|
129
|
+
code?: string;
|
|
123
130
|
/** True when the action was refused only for want of a confirmation. */
|
|
124
131
|
needsConfirmation?: boolean;
|
|
125
132
|
menu: CommandMenuModel;
|
|
@@ -138,6 +145,8 @@ export interface CommandInvocation {
|
|
|
138
145
|
export interface ActionOutcome {
|
|
139
146
|
ok: boolean;
|
|
140
147
|
text: string;
|
|
148
|
+
/** The failure's stable code; a failure without one is `refused`. */
|
|
149
|
+
code?: string;
|
|
141
150
|
}
|
|
142
151
|
export interface ActionInput {
|
|
143
152
|
values: KnobValues;
|
package/dist/commands/seam.d.ts
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
import { type RedactionOptions } from '../logger/index.js';
|
|
1
2
|
import type { ActionDefinition, CommandApplyResult, CommandDialogPayload, ItemDefinition, MenuAccount, MenuConfirmation, SectionContent, SectionSlot } from './model.js';
|
|
2
3
|
/** Where a dropped field is reported. Names only, never values. */
|
|
3
4
|
export interface SeamLogger {
|
|
4
5
|
warn(message: string, data?: unknown): void;
|
|
5
6
|
}
|
|
7
|
+
/** Masks the secret-shaped parts of one string. */
|
|
8
|
+
export type TextRedactor = (text: string) => string;
|
|
9
|
+
/** The string redactor every value crossing the seam goes through. */
|
|
10
|
+
export declare function createTextRedactor(options?: RedactionOptions): TextRedactor;
|
|
11
|
+
/**
|
|
12
|
+
* A failure whose message was written for the user. An action throws it to
|
|
13
|
+
* show `message` with the stable `code`; any other thrown value is shown only
|
|
14
|
+
* as a generic message, because its text may quote a request, a response or
|
|
15
|
+
* a credential. `code` is lowercase letters, digits and dashes; anything else
|
|
16
|
+
* is reported as `action-failed`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class CommandError extends Error {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
constructor(code: string, message: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** What a failed action shows: a stable code and a message safe to display. */
|
|
25
|
+
export interface ProjectedFailure {
|
|
26
|
+
code: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/** The code and message shown for an exception the seam cannot vouch for. */
|
|
30
|
+
export declare const ACTION_FAILED: ProjectedFailure;
|
|
31
|
+
/** The code and display text for a thrown value; never its raw text. */
|
|
32
|
+
export declare function projectFailure(error: unknown): ProjectedFailure;
|
|
6
33
|
/** A built-in item may carry the account it shows; plugin items cannot. */
|
|
7
34
|
export interface ResolvedItem extends ItemDefinition {
|
|
8
35
|
account?: MenuAccount;
|
|
@@ -18,6 +45,11 @@ export interface ResolvedSection {
|
|
|
18
45
|
}
|
|
19
46
|
/** The confirmation shown when an irreversible action names none. */
|
|
20
47
|
export declare const DEFAULT_IRREVERSIBLE_CONFIRMATION = "This cannot be undone. Continue?";
|
|
48
|
+
/** What every payload builder needs besides the payload itself. */
|
|
49
|
+
export interface SeamContext {
|
|
50
|
+
logger: SeamLogger;
|
|
51
|
+
redact: TextRedactor;
|
|
52
|
+
}
|
|
21
53
|
/**
|
|
22
54
|
* The confirmation an action must pass before it runs, or undefined. An
|
|
23
55
|
* irreversible action always has one, even when its definition (built
|
|
@@ -27,10 +59,14 @@ export declare function confirmationOf(action: ActionDefinition): MenuConfirmati
|
|
|
27
59
|
/** The account fields a renderer may show; see `MenuAccount`. */
|
|
28
60
|
export declare function projectAccount(account: MenuAccount): MenuAccount;
|
|
29
61
|
/** The payload a host's TUI receives when the slash command opens. */
|
|
30
|
-
export declare function dialogPayload(command: string, title: string, sections: readonly ResolvedSection[],
|
|
31
|
-
/**
|
|
32
|
-
export
|
|
62
|
+
export declare function dialogPayload(command: string, title: string, sections: readonly ResolvedSection[], seam: SeamContext): CommandDialogPayload;
|
|
63
|
+
/** How an apply ended, before the seam turns it into a payload. */
|
|
64
|
+
export interface ApplyOutcome {
|
|
33
65
|
ok: boolean;
|
|
34
66
|
text: string;
|
|
67
|
+
/** Present on every failure. */
|
|
68
|
+
code?: string;
|
|
35
69
|
needsConfirmation?: boolean;
|
|
36
|
-
}
|
|
70
|
+
}
|
|
71
|
+
/** An apply's result: the message and the refreshed menu. */
|
|
72
|
+
export declare function applyResult(command: string, title: string, sections: readonly ResolvedSection[], outcome: ApplyOutcome, seam: SeamContext): CommandApplyResult;
|
package/dist/commands/seam.js
CHANGED
|
@@ -2,9 +2,97 @@
|
|
|
2
2
|
// payload a renderer receives (the dialog payload and every apply result) is
|
|
3
3
|
// produced here: each part is projected field by field from its definition,
|
|
4
4
|
// so action bodies and stray properties never travel, and the whole payload
|
|
5
|
-
// is then scrubbed
|
|
5
|
+
// is then scrubbed as a backstop: credential-shaped property names are
|
|
6
|
+
// dropped and every string value is passed through the redactor. Nothing
|
|
6
7
|
// outside this module can build a payload without passing through
|
|
7
|
-
// `dialogPayload` or `applyResult`.
|
|
8
|
+
// `dialogPayload` or `applyResult`. A failed action never shows its raw
|
|
9
|
+
// exception text: `projectFailure` turns it into a stable code and a message
|
|
10
|
+
// the library or the plugin wrote for the user.
|
|
11
|
+
import { createRedactor } from '../logger/index.js';
|
|
12
|
+
import { PoolOperationError } from '../store/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Secret shapes the seam masks on top of the logger's (bearer tokens, `sk-`
|
|
15
|
+
* keys, JWTs, vault host tokens): a value assigned to a secret-named field
|
|
16
|
+
* (`client_secret=…`, `"api_key": "…"`, `Authorization: …`), and a run of 32
|
|
17
|
+
* or more hex digits, the shape of a minted request secret. A plugin adds its
|
|
18
|
+
* provider's key shapes through `RedactionOptions.extraValuePatterns`.
|
|
19
|
+
*/
|
|
20
|
+
const SEAM_VALUE_PATTERNS = [
|
|
21
|
+
/\b[\w-]*(?:secret|token|password|passwd|api[_-]?key|authorization|verifier)["']?\s*[:=]\s*["']?(?:(?:Bearer|Basic)\s+)?[^\s"'&,;}]+/gi,
|
|
22
|
+
/\b[0-9a-fA-F]{32,}\b/g,
|
|
23
|
+
];
|
|
24
|
+
/** The string redactor every value crossing the seam goes through. */
|
|
25
|
+
export function createTextRedactor(options = {}) {
|
|
26
|
+
const { redactStrings } = createRedactor({
|
|
27
|
+
...options,
|
|
28
|
+
extraValuePatterns: [
|
|
29
|
+
...SEAM_VALUE_PATTERNS,
|
|
30
|
+
...(options.extraValuePatterns ?? []),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
return (text) => redactStrings(text);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A failure whose message was written for the user. An action throws it to
|
|
37
|
+
* show `message` with the stable `code`; any other thrown value is shown only
|
|
38
|
+
* as a generic message, because its text may quote a request, a response or
|
|
39
|
+
* a credential. `code` is lowercase letters, digits and dashes; anything else
|
|
40
|
+
* is reported as `action-failed`.
|
|
41
|
+
*/
|
|
42
|
+
export class CommandError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = 'CommandError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const CODE_SHAPE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
51
|
+
/** The code and message shown for an exception the seam cannot vouch for. */
|
|
52
|
+
export const ACTION_FAILED = {
|
|
53
|
+
code: 'action-failed',
|
|
54
|
+
text: 'That action failed.',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Store failure kinds whose message the store composes itself from row ids
|
|
58
|
+
* and the plugin's own refusal reasons (`protect`). Other kinds can carry a
|
|
59
|
+
* lock or filesystem error's text, or an arbitrary exception from a hook, so
|
|
60
|
+
* they show a generic message with the kind as the code.
|
|
61
|
+
*/
|
|
62
|
+
const STORE_KINDS_WITH_OWN_MESSAGE = new Set([
|
|
63
|
+
'unknown-row',
|
|
64
|
+
'invalid-row',
|
|
65
|
+
'invalid-input',
|
|
66
|
+
'id-exists',
|
|
67
|
+
'id-removed',
|
|
68
|
+
'type-mismatch',
|
|
69
|
+
'no-credential',
|
|
70
|
+
'row-disabled',
|
|
71
|
+
'row-protected',
|
|
72
|
+
'duplicate-identity',
|
|
73
|
+
'row-key-changed',
|
|
74
|
+
'invalid-order',
|
|
75
|
+
'pending-migration',
|
|
76
|
+
'provider',
|
|
77
|
+
'pull',
|
|
78
|
+
'attribution',
|
|
79
|
+
]);
|
|
80
|
+
/** The code and display text for a thrown value; never its raw text. */
|
|
81
|
+
export function projectFailure(error) {
|
|
82
|
+
if (error instanceof CommandError)
|
|
83
|
+
return {
|
|
84
|
+
code: CODE_SHAPE.test(error.code) ? error.code : ACTION_FAILED.code,
|
|
85
|
+
text: error.message,
|
|
86
|
+
};
|
|
87
|
+
if (error instanceof PoolOperationError)
|
|
88
|
+
return {
|
|
89
|
+
code: `pool-${error.kind}`,
|
|
90
|
+
text: STORE_KINDS_WITH_OWN_MESSAGE.has(error.kind)
|
|
91
|
+
? error.message
|
|
92
|
+
: `The account pool could not do that (${error.kind}).`,
|
|
93
|
+
};
|
|
94
|
+
return ACTION_FAILED;
|
|
95
|
+
}
|
|
8
96
|
/** The confirmation shown when an irreversible action names none. */
|
|
9
97
|
export const DEFAULT_IRREVERSIBLE_CONFIRMATION = 'This cannot be undone. Continue?';
|
|
10
98
|
const CREDENTIAL_NAMES = new Set([
|
|
@@ -32,20 +120,29 @@ function isCredentialName(name) {
|
|
|
32
120
|
function isPlainRecord(value) {
|
|
33
121
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
122
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
123
|
+
/**
|
|
124
|
+
* A copy of `value` without credential-shaped properties and with every
|
|
125
|
+
* string redacted; the paths of both go to `report`.
|
|
126
|
+
*/
|
|
127
|
+
function scrub(value, path, report) {
|
|
128
|
+
if (typeof value === 'string') {
|
|
129
|
+
const clean = report.redact(value);
|
|
130
|
+
if (clean !== value)
|
|
131
|
+
report.redacted.push(path);
|
|
132
|
+
return clean;
|
|
133
|
+
}
|
|
37
134
|
if (Array.isArray(value))
|
|
38
|
-
return value.map((entry, index) => scrub(entry, `${path}[${index}]`,
|
|
135
|
+
return value.map((entry, index) => scrub(entry, `${path}[${index}]`, report));
|
|
39
136
|
if (!isPlainRecord(value))
|
|
40
137
|
return value;
|
|
41
138
|
const out = {};
|
|
42
139
|
for (const [name, entry] of Object.entries(value)) {
|
|
43
140
|
if (isCredentialName(name)) {
|
|
44
|
-
|
|
141
|
+
report.dropped.push(`${path}.${name}`);
|
|
45
142
|
continue;
|
|
46
143
|
}
|
|
47
144
|
Object.defineProperty(out, name, {
|
|
48
|
-
value: scrub(entry, `${path}.${name}`,
|
|
145
|
+
value: scrub(entry, `${path}.${name}`, report),
|
|
49
146
|
enumerable: true,
|
|
50
147
|
writable: true,
|
|
51
148
|
configurable: true,
|
|
@@ -53,13 +150,22 @@ function scrub(value, path, found) {
|
|
|
53
150
|
}
|
|
54
151
|
return out;
|
|
55
152
|
}
|
|
56
|
-
function sealed(payload, command,
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
153
|
+
function sealed(payload, command, seam) {
|
|
154
|
+
const report = {
|
|
155
|
+
redact: seam.redact,
|
|
156
|
+
dropped: [],
|
|
157
|
+
redacted: [],
|
|
158
|
+
};
|
|
159
|
+
const clean = scrub(payload, 'payload', report);
|
|
160
|
+
if (report.dropped.length > 0)
|
|
161
|
+
seam.logger.warn('credential-shaped field dropped from a command payload', {
|
|
162
|
+
command,
|
|
163
|
+
fields: report.dropped,
|
|
164
|
+
});
|
|
165
|
+
if (report.redacted.length > 0)
|
|
166
|
+
seam.logger.warn('secret-shaped text masked in a command payload', {
|
|
61
167
|
command,
|
|
62
|
-
fields:
|
|
168
|
+
fields: report.redacted,
|
|
63
169
|
});
|
|
64
170
|
return clean;
|
|
65
171
|
}
|
|
@@ -94,11 +200,15 @@ function projectKnob(knob) {
|
|
|
94
200
|
...(knob.required ? { required: true } : {}),
|
|
95
201
|
};
|
|
96
202
|
case 'text':
|
|
203
|
+
// A masked input holds a secret, so its current value is never shown;
|
|
204
|
+
// the action still receives it when the user leaves the input alone.
|
|
97
205
|
return {
|
|
98
206
|
kind: 'text',
|
|
99
207
|
id: knob.id,
|
|
100
208
|
label: knob.label,
|
|
101
|
-
...(knob.value !== undefined
|
|
209
|
+
...(knob.value !== undefined && !knob.masked
|
|
210
|
+
? { value: knob.value }
|
|
211
|
+
: {}),
|
|
102
212
|
...(knob.placeholder !== undefined
|
|
103
213
|
? { placeholder: knob.placeholder }
|
|
104
214
|
: {}),
|
|
@@ -170,16 +280,19 @@ function projectMenu(command, title, sections) {
|
|
|
170
280
|
return { command, title, sections: sections.map(projectSection) };
|
|
171
281
|
}
|
|
172
282
|
/** The payload a host's TUI receives when the slash command opens. */
|
|
173
|
-
export function dialogPayload(command, title, sections,
|
|
174
|
-
return sealed({ command, menu: projectMenu(command, title, sections) }, command,
|
|
283
|
+
export function dialogPayload(command, title, sections, seam) {
|
|
284
|
+
return sealed({ command, menu: projectMenu(command, title, sections) }, command, seam);
|
|
175
285
|
}
|
|
176
286
|
/** An apply's result: the message and the refreshed menu. */
|
|
177
|
-
export function applyResult(command, title, sections, outcome,
|
|
287
|
+
export function applyResult(command, title, sections, outcome, seam) {
|
|
178
288
|
return sealed({
|
|
179
289
|
command,
|
|
180
290
|
ok: outcome.ok,
|
|
181
|
-
text: outcome.text,
|
|
291
|
+
text: String(outcome.text),
|
|
292
|
+
...(!outcome.ok && outcome.code !== undefined
|
|
293
|
+
? { code: CODE_SHAPE.test(outcome.code) ? outcome.code : 'refused' }
|
|
294
|
+
: {}),
|
|
182
295
|
...(outcome.needsConfirmation ? { needsConfirmation: true } : {}),
|
|
183
296
|
menu: projectMenu(command, title, sections),
|
|
184
|
-
}, command,
|
|
297
|
+
}, command, seam);
|
|
185
298
|
}
|
package/dist/dump/index.d.ts
CHANGED
|
@@ -32,6 +32,33 @@ export interface DumpOptions {
|
|
|
32
32
|
* It receives the redacted body.
|
|
33
33
|
*/
|
|
34
34
|
summarize?: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Byte cap for the dump artifacts in `dir`. When it is above zero, a sweep
|
|
37
|
+
* runs after a successful dump, at most once per `sweepIntervalMs`, and
|
|
38
|
+
* evicts whole dumps, oldest first, until the artifacts fit. Default off:
|
|
39
|
+
* the directory grows without bound.
|
|
40
|
+
*/
|
|
41
|
+
maxBytes?: number;
|
|
42
|
+
/** Least time between two automatic sweeps. Default five minutes. */
|
|
43
|
+
sweepIntervalMs?: number;
|
|
44
|
+
/**
|
|
45
|
+
* A dump whose newest file is younger than this is never evicted, so a
|
|
46
|
+
* sweep cannot remove a dump whose response is still being attached.
|
|
47
|
+
* Default one minute.
|
|
48
|
+
*/
|
|
49
|
+
sweepMinAgeMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* A response staging file older than this is left over from a crashed
|
|
52
|
+
* write and is removed by any sweep, even under the cap. Default ten
|
|
53
|
+
* minutes.
|
|
54
|
+
*/
|
|
55
|
+
partialStaleMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Remove the files a dump did write when another file of the same dump
|
|
58
|
+
* failed, so a failed dump leaves no half group behind. Default false: the
|
|
59
|
+
* files that were written stay.
|
|
60
|
+
*/
|
|
61
|
+
cleanupFailedDumps?: boolean;
|
|
35
62
|
}
|
|
36
63
|
export interface DumpInput {
|
|
37
64
|
/** The session the request belongs to; missing means `session-unknown`. */
|
|
@@ -70,6 +97,35 @@ export interface DumpResult {
|
|
|
70
97
|
metadata: string;
|
|
71
98
|
request: string;
|
|
72
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Where `dumpResponse` writes this dump's response artifact. Nothing is
|
|
102
|
+
* there until a response is attached.
|
|
103
|
+
*/
|
|
104
|
+
responseFile: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What a plugin knows about the upstream response to a dumped request. Every
|
|
108
|
+
* field is optional; the artifact holds only what was given, redacted like the
|
|
109
|
+
* request. Pass no response content: the artifact is evidence about the
|
|
110
|
+
* response (who answered, what it cost), not a copy of it.
|
|
111
|
+
*/
|
|
112
|
+
export interface DumpResponseInput {
|
|
113
|
+
status?: number;
|
|
114
|
+
/** The provider's id for the request or response, for support lookups. */
|
|
115
|
+
requestId?: string;
|
|
116
|
+
/** The usage block the provider reported. */
|
|
117
|
+
usage?: unknown;
|
|
118
|
+
/**
|
|
119
|
+
* False while a stream is still open or when it ended without its final
|
|
120
|
+
* frame. Default true.
|
|
121
|
+
*/
|
|
122
|
+
complete?: boolean;
|
|
123
|
+
/** Further provider fields, such as the model or the stop reason. */
|
|
124
|
+
fields?: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
export interface DumpSweepResult {
|
|
127
|
+
removed: number;
|
|
128
|
+
freedBytes: number;
|
|
73
129
|
}
|
|
74
130
|
export interface Dumper {
|
|
75
131
|
isEnabled(): boolean;
|
|
@@ -80,7 +136,45 @@ export interface Dumper {
|
|
|
80
136
|
* and never throws into the request path.
|
|
81
137
|
*/
|
|
82
138
|
dump(input: DumpInput): Promise<DumpResult | undefined>;
|
|
139
|
+
/**
|
|
140
|
+
* Write the response artifact of an earlier dump as
|
|
141
|
+
* `<id>.response.json`, replacing any earlier one, so a stream can record
|
|
142
|
+
* its opening usage and later its final usage. Does nothing for an
|
|
143
|
+
* undefined dump. Returns the path, or undefined when the write failed
|
|
144
|
+
* (logged, never thrown).
|
|
145
|
+
*/
|
|
146
|
+
dumpResponse(dump: DumpResult | undefined, input: DumpResponseInput): Promise<string | undefined>;
|
|
147
|
+
/**
|
|
148
|
+
* Apply the byte cap now, whatever the sweep interval. Does nothing when
|
|
149
|
+
* `maxBytes` is not above zero.
|
|
150
|
+
*/
|
|
151
|
+
sweep(protectedPaths?: readonly string[]): Promise<DumpSweepResult>;
|
|
152
|
+
}
|
|
153
|
+
export interface SweepDumpDirectoryOptions {
|
|
154
|
+
dir: string;
|
|
155
|
+
/** Byte cap for the dump artifacts; zero or less disables the sweep. */
|
|
156
|
+
maxBytes: number;
|
|
157
|
+
/** Files never removed, such as the dump just written. */
|
|
158
|
+
protectedPaths?: readonly string[];
|
|
159
|
+
/** Milliseconds since the epoch. Default `Date.now()`. */
|
|
160
|
+
now?: number;
|
|
161
|
+
/** A dump whose newest file is younger than this is kept. Default one minute. */
|
|
162
|
+
minAgeMs?: number;
|
|
163
|
+
/** A response staging file older than this is removed. Default ten minutes. */
|
|
164
|
+
partialStaleMs?: number;
|
|
165
|
+
logger?: DumpLogger;
|
|
83
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Hold the dump artifacts in `dir` to `maxBytes`. Only file names a dumper
|
|
169
|
+
* writes are counted or removed, so unrelated files in a directory the user
|
|
170
|
+
* chose survive. A dump's files are evicted together, oldest dump first by
|
|
171
|
+
* its newest file, because a body without its metadata (or the reverse) is no
|
|
172
|
+
* use to anyone. Dumps younger than `minAgeMs`, protected paths and symlinks
|
|
173
|
+
* are kept, and a symlinked directory is refused outright. Response staging
|
|
174
|
+
* files left by a crash are reclaimed once stale, even under the cap.
|
|
175
|
+
* Best-effort: a failure removes less, and never throws.
|
|
176
|
+
*/
|
|
177
|
+
export declare function sweepDumpDirectory(options: SweepDumpDirectoryOptions): Promise<DumpSweepResult>;
|
|
84
178
|
/**
|
|
85
179
|
* Where two bodies first and last differ, in string offsets into the dumped
|
|
86
180
|
* (redacted) text. Offsets count UTF-16 units, as the plugins' cache analysis
|