@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.
Files changed (60) hide show
  1. package/dist/cachekeep/manager.d.ts +18 -6
  2. package/dist/cachekeep/manager.js +40 -10
  3. package/dist/claustrum/consumer.d.ts +13 -4
  4. package/dist/claustrum/consumer.js +11 -3
  5. package/dist/claustrum/custody.d.ts +47 -6
  6. package/dist/claustrum/custody.js +37 -7
  7. package/dist/claustrum/errors.d.ts +1 -1
  8. package/dist/claustrum/index.d.ts +3 -3
  9. package/dist/claustrum/index.js +2 -2
  10. package/dist/claustrum/interlock.d.ts +15 -17
  11. package/dist/claustrum/interlock.js +19 -26
  12. package/dist/claustrum/roster.d.ts +96 -6
  13. package/dist/claustrum/roster.js +237 -45
  14. package/dist/commands/builtins.d.ts +1 -1
  15. package/dist/commands/builtins.js +6 -1
  16. package/dist/commands/index.d.ts +2 -2
  17. package/dist/commands/index.js +1 -1
  18. package/dist/commands/menu.d.ts +8 -0
  19. package/dist/commands/menu.js +34 -13
  20. package/dist/commands/model.d.ts +9 -0
  21. package/dist/commands/seam.d.ts +40 -4
  22. package/dist/commands/seam.js +132 -19
  23. package/dist/dump/index.d.ts +94 -0
  24. package/dist/dump/index.js +236 -9
  25. package/dist/logger/engine.d.ts +52 -17
  26. package/dist/logger/engine.js +178 -135
  27. package/dist/logger/index.d.ts +2 -2
  28. package/dist/logger/index.js +1 -1
  29. package/dist/opencode2/install.d.ts +8 -3
  30. package/dist/opencode2/install.js +18 -9
  31. package/dist/opencode2/types.d.ts +22 -3
  32. package/dist/quota/projection.d.ts +11 -4
  33. package/dist/quota/projection.js +11 -4
  34. package/dist/routing/admission.js +3 -1
  35. package/dist/routing/index.d.ts +2 -2
  36. package/dist/routing/index.js +1 -1
  37. package/dist/routing/sticky.d.ts +19 -6
  38. package/dist/routing/sticky.js +34 -23
  39. package/dist/rpc/notifications.d.ts +20 -0
  40. package/dist/rpc/notifications.js +21 -0
  41. package/dist/rpc/rpc-server.d.ts +9 -1
  42. package/dist/rpc/rpc-server.js +8 -1
  43. package/dist/sidebar-file/index.d.ts +1 -1
  44. package/dist/sidebar-file/sidebar-file.d.ts +50 -2
  45. package/dist/sidebar-file/sidebar-file.js +92 -21
  46. package/dist/store/attribution.js +14 -2
  47. package/dist/store/errors.d.ts +6 -3
  48. package/dist/store/identity.d.ts +13 -4
  49. package/dist/store/index.d.ts +1 -1
  50. package/dist/store/mutate.d.ts +27 -4
  51. package/dist/store/mutate.js +43 -26
  52. package/dist/store/pool.d.ts +16 -3
  53. package/dist/store/pool.js +7 -2
  54. package/dist/store/rows.d.ts +20 -6
  55. package/dist/store/rows.js +141 -46
  56. package/dist/store/schema.d.ts +74 -5
  57. package/dist/store/schema.js +112 -10
  58. package/dist/store/torn.d.ts +29 -0
  59. package/dist/store/torn.js +113 -0
  60. package/package.json +1 -1
@@ -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), logger);
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, logger);
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({ ok: false, text: coerced.problem });
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' ? { ok: true, text: result } : result;
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
- const message = error instanceof Error ? error.message : String(error);
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
- error: message,
211
+ code: failure.code,
212
+ error: redact(error instanceof Error ? error.message : String(error)),
192
213
  });
193
- outcome = { ok: false, text: message };
214
+ outcome = { ok: false, ...failure };
194
215
  }
195
216
  return finish(outcome);
196
217
  },
@@ -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;
@@ -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[], logger: SeamLogger): CommandDialogPayload;
31
- /** An apply's result: the message and the refreshed menu. */
32
- export declare function applyResult(command: string, title: string, sections: readonly ResolvedSection[], outcome: {
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
- }, logger: SeamLogger): CommandApplyResult;
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;
@@ -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 of credential-shaped property names as a backstop. Nothing
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
- /** A copy of `value` without credential-shaped properties; their paths go to `found`. */
36
- function scrub(value, path, found) {
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}]`, found));
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
- found.push(`${path}.${name}`);
141
+ report.dropped.push(`${path}.${name}`);
45
142
  continue;
46
143
  }
47
144
  Object.defineProperty(out, name, {
48
- value: scrub(entry, `${path}.${name}`, found),
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, logger) {
57
- const found = [];
58
- const clean = scrub(payload, 'payload', found);
59
- if (found.length > 0)
60
- logger.warn('credential-shaped field dropped from a command payload', {
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: found,
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 ? { value: knob.value } : {}),
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, logger) {
174
- return sealed({ command, menu: projectMenu(command, title, sections) }, command, logger);
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, logger) {
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, logger);
297
+ }, command, seam);
185
298
  }
@@ -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