@wix/pathgrade 1.0.22 → 1.0.23

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.
@@ -15,6 +15,7 @@ import { buildLiveMcpProtocolErrorEvent, buildPolicyDeniedMcpToolEvent, CodexMcp
15
15
  import { projectItemIntoTurn } from './item-projection.js';
16
16
  import { CodexItemLifecycle, } from './item-lifecycle.js';
17
17
  import { createCodexManagedAuth } from './managed-auth.js';
18
+ import { clearTurnAuthorityDeadline, closeUncorrelatedTurnTransport, establishAuthoritativeTurn, isAbortError, isAuthoritativeTurnCompletion, rememberAuthoritativeTurnId, shouldRetireAuthoritativeTurnTransport, } from './turn-authority.js';
18
19
  const TURN_COMPLETED_METHOD = 'turn/completed';
19
20
  function projectMcpStartupStatusIntoTurn(params, turn) {
20
21
  const name = params.name ?? 'unknown';
@@ -64,18 +65,6 @@ function projectCompletedItem(item, timing, params, turn, correlator, sensitiveV
64
65
  }
65
66
  turn.nonAskToolEvents.push(buildPolicyDeniedMcpToolEvent(turn.turnNumber, pendingDenial, mcpItem));
66
67
  }
67
- function extractTurnCompletionIdentity(params) {
68
- if (!params || typeof params !== 'object' || Array.isArray(params))
69
- return {};
70
- const record = params;
71
- const turnId = typeof record.turn?.id === 'string'
72
- ? record.turn.id
73
- : typeof record.turnId === 'string' ? record.turnId : undefined;
74
- return {
75
- ...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
76
- ...(turnId ? { turnId } : {}),
77
- };
78
- }
79
68
  function isLiveMcpSafetyMode(options) {
80
69
  const runMode = options?.runMode ?? 'mock';
81
70
  return runMode === 'live-readonly' || runMode === 'live-sandbox' || runMode === 'live';
@@ -102,9 +91,11 @@ export class CodexAppServerAgent extends BaseAgent {
102
91
  let handle = null;
103
92
  let threadId = null;
104
93
  let turnCounter = 0;
94
+ const authoritativeTurnIds = new Set();
105
95
  let closeInfo = null;
106
96
  let activeTurn = null;
107
97
  let disposed = false;
98
+ let sessionRetired = false;
108
99
  let codexUserAgent = 'unknown';
109
100
  const managedAuth = createCodexManagedAuth({
110
101
  enabled: !runtimeEnv.OPENAI_API_KEY && !runtimeEnv.OPENAI_BASE_URL
@@ -137,7 +128,8 @@ export class CodexAppServerAgent extends BaseAgent {
137
128
  managedAuth,
138
129
  }));
139
130
  transport.onClose((info) => {
140
- closeInfo = info;
131
+ if (handle === spawnedHandle)
132
+ closeInfo = info;
141
133
  });
142
134
  transport.onNotification((n) => {
143
135
  if (process.env.PATHGRADE_CODEX_DEBUG) {
@@ -173,9 +165,6 @@ export class CodexAppServerAgent extends BaseAgent {
173
165
  if (typeof initialized.userAgent === 'string') {
174
166
  codexUserAgent = initialized.userAgent.slice(0, 200);
175
167
  }
176
- // Upstream ClientNotification = { method: "initialized" }: send it
177
- // before any thread/start so the handshake matches the v0.149
178
- // contract and is forward-compatible with servers that enforce it.
179
168
  transport.sendNotification('initialized', null);
180
169
  await managedAuth.login(transport);
181
170
  return transport;
@@ -189,6 +178,7 @@ export class CodexAppServerAgent extends BaseAgent {
189
178
  };
190
179
  const runTurn = async (message) => {
191
180
  const t = await ensureTransport();
181
+ const turnHandle = handle;
192
182
  turnCounter += 1;
193
183
  const turn = {
194
184
  turnNumber: turnCounter,
@@ -199,6 +189,8 @@ export class CodexAppServerAgent extends BaseAgent {
199
189
  turnFailed: false,
200
190
  pendingTurnCompletions: [],
201
191
  };
192
+ const turnStartController = new AbortController();
193
+ let turnStartCanReuseTransport = false;
202
194
  activeTurn = turn;
203
195
  correlator?.beginTurn();
204
196
  try {
@@ -251,7 +243,18 @@ export class CodexAppServerAgent extends BaseAgent {
251
243
  }
252
244
  },
253
245
  });
254
- // Wait for TurnCompleted OR subprocess crash OR dispatcher failure.
246
+ let resolveAuthoritativeTurn;
247
+ const authoritativeTurnReady = new Promise((resolve) => {
248
+ resolveAuthoritativeTurn = resolve;
249
+ });
250
+ const markAuthoritativeTurn = (candidateTurnId) => establishAuthoritativeTurn({
251
+ turn,
252
+ candidateTurnId,
253
+ authoritativeThreadId,
254
+ authoritativeTurnIds,
255
+ correlator,
256
+ onReady: () => resolveAuthoritativeTurn?.(),
257
+ });
255
258
  const turnCompletion = new Promise((resolve, reject) => {
256
259
  let settled = false;
257
260
  const off = t.onNotification((n) => {
@@ -264,14 +267,13 @@ export class CodexAppServerAgent extends BaseAgent {
264
267
  const acceptTurnCompletion = (params) => {
265
268
  if (settled)
266
269
  return;
267
- const identity = extractTurnCompletionIdentity(params);
268
- if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
269
- return;
270
- if (identity.turnId && !turn.authoritativeTurnId) {
271
- turn.pendingTurnCompletions.push(params);
272
- return;
273
- }
274
- if (identity.turnId && identity.turnId !== turn.authoritativeTurnId)
270
+ if (!isAuthoritativeTurnCompletion({
271
+ params,
272
+ turn,
273
+ authoritativeTurnIds,
274
+ markAuthoritativeTurn,
275
+ getRemainingMs: () => options?.getRemainingMs?.() ?? Number.POSITIVE_INFINITY,
276
+ }))
275
277
  return;
276
278
  settled = true;
277
279
  off();
@@ -295,13 +297,14 @@ export class CodexAppServerAgent extends BaseAgent {
295
297
  }));
296
298
  });
297
299
  turn.signalFailure = (msg) => {
300
+ turn.turnFailed = true;
301
+ turn.failureMessage = msg;
302
+ resolveAuthoritativeTurn?.();
298
303
  if (settled)
299
304
  return;
300
305
  settled = true;
301
306
  off();
302
307
  closeOff();
303
- turn.turnFailed = true;
304
- turn.failureMessage = msg;
305
308
  resolve();
306
309
  };
307
310
  if (turn.turnFailed) {
@@ -319,31 +322,31 @@ export class CodexAppServerAgent extends BaseAgent {
319
322
  }
320
323
  }
321
324
  });
322
- const authoritativeTurn = t.sendRequest('turn/start', {
325
+ void t.sendRequest('turn/start', {
323
326
  threadId,
324
327
  input: [{ type: 'text', text: message, text_elements: [] }],
325
- })
328
+ }, { signal: turnStartController.signal })
326
329
  .then((started) => {
327
- const authoritativeTurnId = started.turn?.id ?? started.turnId;
328
- if (!authoritativeTurnId) {
329
- scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
330
- turn.signalFailure?.('turn/start did not return an authoritative turn id');
330
+ const authoritativeTurnId = started?.turn?.id ?? started?.turnId;
331
+ if (activeTurn !== turn) {
332
+ if (authoritativeTurnId)
333
+ rememberAuthoritativeTurnId(authoritativeTurnIds, authoritativeTurnId);
331
334
  return;
332
335
  }
333
- turn.itemLifecycle?.setAuthoritativeTurn(authoritativeTurnId);
334
- turn.authoritativeTurnId = authoritativeTurnId;
335
- if (correlator?.setAuthoritativeTurn(authoritativeThreadId, authoritativeTurnId) === false) {
336
- turn.signalFailure?.('MCP lifecycle did not match the authoritative turn');
337
- }
338
- for (const pending of turn.pendingTurnCompletions.splice(0)) {
339
- turn.acceptTurnCompletion?.(pending);
336
+ if (!authoritativeTurnId) {
337
+ if (!turn.authoritativeTurnId) {
338
+ scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
339
+ turn.signalFailure?.('turn/start did not return an authoritative turn id');
340
+ }
341
+ return;
340
342
  }
343
+ turnStartCanReuseTransport = true;
344
+ markAuthoritativeTurn(authoritativeTurnId);
341
345
  })
342
346
  .catch((err) => {
343
- // A JSON-RPC error on turn/start means the server will
344
- // never send turn/completed — short-circuit the turn
345
- // through the existing signalFailure path instead of
346
- // waiting for an outer timeout.
347
+ if (isAbortError(err) || activeTurn !== turn)
348
+ return;
349
+ turnStartCanReuseTransport = true;
347
350
  const msg = err instanceof Error
348
351
  ? err.message
349
352
  : typeof err === 'string'
@@ -375,9 +378,7 @@ export class CodexAppServerAgent extends BaseAgent {
375
378
  sensitiveValues,
376
379
  });
377
380
  }
378
- // Ensure pre-response lifecycle notifications have been correlated
379
- // and projected before exposing the successful turn result.
380
- await authoritativeTurn;
381
+ await authoritativeTurnReady;
381
382
  if (turn.turnFailed) {
382
383
  return assembleTurnResult({
383
384
  askBus, activeTurn: turn, exitCode: 1,
@@ -393,8 +394,25 @@ export class CodexAppServerAgent extends BaseAgent {
393
394
  });
394
395
  }
395
396
  finally {
397
+ turnStartController.abort();
398
+ clearTurnAuthorityDeadline(turn);
396
399
  activeTurn = null;
397
400
  correlator?.endTurn();
401
+ const retireTransport = shouldRetireAuthoritativeTurnTransport(authoritativeTurnIds);
402
+ await closeUncorrelatedTurnTransport({
403
+ turnStartCanReuseTransport,
404
+ authoritativeTurnId: turn.authoritativeTurnId,
405
+ retireTransport,
406
+ currentHandle: handle,
407
+ turnHandle,
408
+ reset: () => {
409
+ sessionRetired ||= retireTransport;
410
+ handle = null;
411
+ threadId = null;
412
+ authoritativeTurnIds.clear();
413
+ closeInfo = null;
414
+ },
415
+ });
398
416
  }
399
417
  };
400
418
  const dispose = async () => {
@@ -419,6 +437,9 @@ export class CodexAppServerAgent extends BaseAgent {
419
437
  if (disposed) {
420
438
  throw new Error('CodexAppServerAgent session disposed');
421
439
  }
440
+ if (sessionRetired) {
441
+ throw new Error('CodexAppServerAgent session reached its safe turn-history limit; create a new session');
442
+ }
422
443
  };
423
444
  return {
424
445
  start: async ({ message }) => {
@@ -15,7 +15,9 @@ export interface TransportCloseInfo {
15
15
  readonly pid?: number;
16
16
  }
17
17
  export interface AppServerTransport {
18
- sendRequest<T = unknown>(method: string, params: unknown): Promise<T>;
18
+ sendRequest<T = unknown>(method: string, params: unknown, options?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<T>;
19
21
  sendNotification(method: string, params: unknown): void;
20
22
  sendResponse(id: number | string, result: unknown): void;
21
23
  sendErrorResponse(id: number | string, code: number, message: string): void;
@@ -80,18 +80,31 @@ function createNdjsonTransportInternal(cfg) {
80
80
  pendingRequests.clear();
81
81
  };
82
82
  return {
83
- sendRequest(method, params) {
83
+ sendRequest(method, params, options) {
84
84
  if (closed)
85
85
  return Promise.reject(new Error('AppServerTransport closed'));
86
86
  const id = nextId++;
87
87
  return new Promise((resolve, reject) => {
88
+ const abort = () => {
89
+ if (!pendingRequests.delete(id))
90
+ return;
91
+ const error = new Error('AppServerTransport request aborted');
92
+ error.name = 'AbortError';
93
+ reject(error);
94
+ };
88
95
  pendingRequests.set(id, (env) => {
96
+ options?.signal?.removeEventListener('abort', abort);
89
97
  if (env.error) {
90
98
  reject(new Error(`codex app-server '${method}' failed (${env.error.code}): ${env.error.message}`));
91
99
  return;
92
100
  }
93
101
  resolve(env.result);
94
102
  });
103
+ options?.signal?.addEventListener('abort', abort, { once: true });
104
+ if (options?.signal?.aborted) {
105
+ abort();
106
+ return;
107
+ }
95
108
  writeLine({ jsonrpc: '2.0', id, method, params });
96
109
  });
97
110
  },
@@ -0,0 +1,42 @@
1
+ import type { CodexMcpApprovalCorrelator } from './mcp-approval-correlator.js';
2
+ import type { CodexItemLifecycle } from './item-lifecycle.js';
3
+ import type { AppServerSessionHandle } from './transport.js';
4
+ interface TurnAuthorityState {
5
+ authoritativeThreadId?: string;
6
+ authoritativeTurnId?: string;
7
+ pendingTurnCompletions: unknown[];
8
+ acceptTurnCompletion?: (params: unknown) => void;
9
+ itemLifecycle?: CodexItemLifecycle;
10
+ turnFailed: boolean;
11
+ failureMessage?: string;
12
+ signalFailure?: (message: string) => void;
13
+ authorityDeadlineTimer?: ReturnType<typeof setTimeout>;
14
+ }
15
+ export declare function isAbortError(error: unknown): boolean;
16
+ export declare function closeUncorrelatedTurnTransport(args: {
17
+ turnStartCanReuseTransport: boolean;
18
+ authoritativeTurnId?: string;
19
+ retireTransport: boolean;
20
+ currentHandle: AppServerSessionHandle | null;
21
+ turnHandle: AppServerSessionHandle | null;
22
+ reset(): void;
23
+ }): Promise<void>;
24
+ export declare function rememberAuthoritativeTurnId(turnIds: Set<string>, turnId: string): void;
25
+ export declare function shouldRetireAuthoritativeTurnTransport(turnIds: ReadonlySet<string>): boolean;
26
+ export declare function establishAuthoritativeTurn(args: {
27
+ turn: TurnAuthorityState;
28
+ candidateTurnId: string;
29
+ authoritativeThreadId: string;
30
+ authoritativeTurnIds: Set<string>;
31
+ correlator?: CodexMcpApprovalCorrelator;
32
+ onReady(): void;
33
+ }): boolean;
34
+ export declare function isAuthoritativeTurnCompletion(args: {
35
+ params: unknown;
36
+ turn: TurnAuthorityState;
37
+ authoritativeTurnIds: ReadonlySet<string>;
38
+ markAuthoritativeTurn(turnId: string): boolean;
39
+ getRemainingMs(): number;
40
+ }): boolean;
41
+ export declare function clearTurnAuthorityDeadline(turn: TurnAuthorityState): void;
42
+ export {};
@@ -0,0 +1,96 @@
1
+ const MAX_RECENT_AUTHORITATIVE_TURN_IDS = 256;
2
+ export function isAbortError(error) {
3
+ return error instanceof Error && error.name === 'AbortError';
4
+ }
5
+ export async function closeUncorrelatedTurnTransport(args) {
6
+ const { turnStartCanReuseTransport, authoritativeTurnId, retireTransport, currentHandle, turnHandle, reset } = args;
7
+ if ((!retireTransport && (turnStartCanReuseTransport || authoritativeTurnId))
8
+ || currentHandle !== turnHandle || !turnHandle)
9
+ return;
10
+ reset();
11
+ await turnHandle.close().catch(() => undefined);
12
+ }
13
+ export function rememberAuthoritativeTurnId(turnIds, turnId) {
14
+ turnIds.add(turnId);
15
+ }
16
+ export function shouldRetireAuthoritativeTurnTransport(turnIds) {
17
+ return turnIds.size >= MAX_RECENT_AUTHORITATIVE_TURN_IDS;
18
+ }
19
+ export function establishAuthoritativeTurn(args) {
20
+ const { turn, candidateTurnId, authoritativeThreadId, authoritativeTurnIds, correlator, onReady } = args;
21
+ if (turn.authoritativeTurnId) {
22
+ if (turn.authoritativeTurnId === candidateTurnId)
23
+ return true;
24
+ failTurn(turn, 'turn lifecycle reported conflicting authoritative turn ids');
25
+ return false;
26
+ }
27
+ if (turn.itemLifecycle?.setAuthoritativeTurn(candidateTurnId) === false)
28
+ return false;
29
+ turn.authoritativeTurnId = candidateTurnId;
30
+ clearTurnAuthorityDeadline(turn);
31
+ rememberAuthoritativeTurnId(authoritativeTurnIds, candidateTurnId);
32
+ if (correlator?.setAuthoritativeTurn(authoritativeThreadId, candidateTurnId) === false) {
33
+ failTurn(turn, 'MCP lifecycle did not match the authoritative turn');
34
+ return false;
35
+ }
36
+ onReady();
37
+ for (const pending of turn.pendingTurnCompletions.splice(0)) {
38
+ turn.acceptTurnCompletion?.(pending);
39
+ }
40
+ return true;
41
+ }
42
+ export function isAuthoritativeTurnCompletion(args) {
43
+ const { params, turn, authoritativeTurnIds, markAuthoritativeTurn, getRemainingMs } = args;
44
+ const identity = extractTurnCompletionIdentity(params);
45
+ if (identity.threadId && identity.threadId !== turn.authoritativeThreadId)
46
+ return false;
47
+ if (!identity.turnId && !turn.authoritativeTurnId) {
48
+ if (turn.pendingTurnCompletions.length === 0) {
49
+ const remainingMs = getRemainingMs();
50
+ if (Number.isFinite(remainingMs))
51
+ turn.authorityDeadlineTimer = setTimeout(() => {
52
+ if (!turn.authoritativeTurnId && !turn.turnFailed) {
53
+ failTurn(turn, 'turn/completed did not identify the authoritative turn');
54
+ }
55
+ }, Math.max(0, remainingMs));
56
+ }
57
+ turn.pendingTurnCompletions.push(params);
58
+ return false;
59
+ }
60
+ if (identity.turnId && !turn.authoritativeTurnId) {
61
+ if (authoritativeTurnIds.has(identity.turnId))
62
+ return false;
63
+ if (identity.threadId === turn.authoritativeThreadId) {
64
+ if (!markAuthoritativeTurn(identity.turnId))
65
+ return false;
66
+ }
67
+ else {
68
+ turn.pendingTurnCompletions.push(params);
69
+ return false;
70
+ }
71
+ }
72
+ return !identity.turnId || identity.turnId === turn.authoritativeTurnId;
73
+ }
74
+ function extractTurnCompletionIdentity(params) {
75
+ if (!params || typeof params !== 'object' || Array.isArray(params))
76
+ return {};
77
+ const record = params;
78
+ const turnId = typeof record.turn?.id === 'string'
79
+ ? record.turn.id
80
+ : typeof record.turnId === 'string' ? record.turnId : undefined;
81
+ return {
82
+ ...(typeof record.threadId === 'string' ? { threadId: record.threadId } : {}),
83
+ ...(turnId ? { turnId } : {}),
84
+ };
85
+ }
86
+ function failTurn(turn, message) {
87
+ clearTurnAuthorityDeadline(turn);
88
+ turn.turnFailed = true;
89
+ turn.failureMessage = message;
90
+ turn.signalFailure?.(message);
91
+ }
92
+ export function clearTurnAuthorityDeadline(turn) {
93
+ if (turn.authorityDeadlineTimer)
94
+ clearTimeout(turn.authorityDeadlineTimer);
95
+ turn.authorityDeadlineTimer = undefined;
96
+ }
@@ -130,11 +130,14 @@ export class OpenCodeRuntimePolicy {
130
130
  if (this.authWatcher)
131
131
  throw new Error('OpenCode auth monitor is already active');
132
132
  this.authMutationObserved = false;
133
- const expectedName = path.basename(this.authPath);
134
- this.authWatcher = watch(path.dirname(this.authPath), { persistent: false }, (_event, filename) => {
135
- if (filename === null || filename.toString() === expectedName)
133
+ try {
134
+ this.authWatcher = watch(this.authPath, { persistent: false }, () => {
136
135
  this.authMutationObserved = true;
137
- });
136
+ });
137
+ }
138
+ catch {
139
+ throw poisonedAuthError();
140
+ }
138
141
  this.authWatcher.on('error', () => {
139
142
  this.authMutationObserved = true;
140
143
  });
@@ -24,10 +24,24 @@ const SECRET_KEY_NAMES = [
24
24
  'connectionstring',
25
25
  'credentials',
26
26
  'credential',
27
+ 'svsession',
28
+ 'smsession',
29
+ 'wixsession',
30
+ 'wixsession2',
27
31
  ];
32
+ const EXACT_ONLY_SECRET_KEY_NAMES = new Set([
33
+ 'auth',
34
+ 'token',
35
+ 'svsession',
36
+ 'smsession',
37
+ 'wixsession',
38
+ 'wixsession2',
39
+ ]);
40
+ const NON_SECRET_ENVIRONMENT_KEY_NAMES = new Set(['tokencount', 'tokenizersparallelism', 'tokenusage']);
41
+ const BOUNDARY_ONLY_SENSITIVE_VALUE_MAX_LENGTH = 1;
28
42
  export function collectSensitiveEnvValues(env) {
29
43
  return [...new Set(Object.entries(env ?? {})
30
- .filter(([key, value]) => isSecretKey(key) && value.length > 0)
44
+ .filter(([key, value]) => isSecretEnvironmentKey(key) && value.length > 0)
31
45
  .map(([, value]) => value))]
32
46
  .sort((a, b) => b.length - a.length);
33
47
  }
@@ -37,11 +51,13 @@ export function collectSensitiveEnvValues(env) {
37
51
  * is important for summaries, snippets, traces, and provider error text.
38
52
  */
39
53
  export function sanitizePersistenceValue(source, explicitSensitiveValues = []) {
54
+ const explicitValues = [...new Set(explicitSensitiveValues.filter((value) => value.length > 0))]
55
+ .sort((a, b) => b.length - a.length);
40
56
  const sensitiveValues = [...new Set([
41
- ...explicitSensitiveValues.filter((value) => value.length > 0),
57
+ ...explicitValues,
42
58
  ...collectStructuredSensitiveValues(source),
43
59
  ])].sort((a, b) => b.length - a.length);
44
- return sanitizeValue(source, '', sensitiveValues);
60
+ return sanitizeValue(source, '', sensitiveValues, new Set(explicitValues));
45
61
  }
46
62
  export function sanitizeToolEventResult(source, sensitiveValues) {
47
63
  const result = {};
@@ -63,18 +79,27 @@ export function sanitizeToolEventResult(source, sensitiveValues) {
63
79
  result.truncated = true;
64
80
  return result;
65
81
  }
66
- function redactSensitiveValues(value, sensitiveValues) {
82
+ function redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues) {
67
83
  let redacted = value;
68
84
  for (const secret of sensitiveValues) {
69
- if (secret.length > 0)
85
+ if (secret.length === 0)
86
+ continue;
87
+ if (explicitSensitiveValues.has(secret) || secret.length > BOUNDARY_ONLY_SENSITIVE_VALUE_MAX_LENGTH) {
70
88
  redacted = redacted.split(secret).join('<redacted>');
89
+ continue;
90
+ }
91
+ const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ const leftBoundary = /^[a-z0-9]/i.test(secret) ? '(^|[^a-z0-9])' : '()';
93
+ const rightBoundary = /[a-z0-9]$/i.test(secret) ? '(?=$|[^a-z0-9])' : '';
94
+ redacted = redacted.replace(new RegExp(`${leftBoundary}${escaped}${rightBoundary}`, 'g'), '$1<redacted>');
71
95
  }
72
96
  return redacted;
73
97
  }
74
98
  function collectStructuredSensitiveValues(source) {
75
99
  const values = new Set();
76
100
  const visit = (value, key) => {
77
- if (typeof value === 'string' && isSecretKey(key) && value.length > 0) {
101
+ if (typeof value === 'string' && !isNumericTokenTelemetry(key, value)
102
+ && isSecretKey(key) && value.length > 0) {
78
103
  values.add(value);
79
104
  return;
80
105
  }
@@ -91,26 +116,32 @@ function collectStructuredSensitiveValues(source) {
91
116
  visit(source, '');
92
117
  return [...values];
93
118
  }
94
- function sanitizeValue(value, key, sensitiveValues) {
95
- if (isSecretKey(key))
119
+ function sanitizeValue(value, key, sensitiveValues, explicitSensitiveValues) {
120
+ if (!isNumericTokenTelemetry(key, value) && isSecretKey(key))
96
121
  return '<redacted>';
97
- if (typeof value === 'string')
98
- return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues));
99
- if (Array.isArray(value))
100
- return value.map((entry) => sanitizeValue(entry, key, sensitiveValues));
122
+ if (typeof value === 'string') {
123
+ return redactCredentialShapes(redactSensitiveValues(value, sensitiveValues, explicitSensitiveValues), sensitiveValues, explicitSensitiveValues);
124
+ }
125
+ if (Array.isArray(value)) {
126
+ return value.map((entry) => sanitizeValue(entry, key, sensitiveValues, explicitSensitiveValues));
127
+ }
101
128
  if (isRecord(value)) {
102
129
  return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
103
130
  entryKey,
104
- sanitizeValue(entryValue, entryKey, sensitiveValues),
131
+ sanitizeValue(entryValue, entryKey, sensitiveValues, explicitSensitiveValues),
105
132
  ]));
106
133
  }
107
134
  return value;
108
135
  }
109
- function redactCredentialShapes(value) {
136
+ function redactCredentialShapes(value, sensitiveValues, explicitSensitiveValues) {
110
137
  let redacted = value;
111
138
  const structured = parseStructuredJson(redacted);
112
139
  if (structured !== undefined) {
113
- redacted = JSON.stringify(sanitizePersistenceValue(structured));
140
+ const nestedSensitiveValues = [...new Set([
141
+ ...sensitiveValues,
142
+ ...collectStructuredSensitiveValues(structured),
143
+ ])].sort((a, b) => b.length - a.length);
144
+ redacted = JSON.stringify(sanitizeValue(structured, '', nestedSensitiveValues, explicitSensitiveValues));
114
145
  }
115
146
  if (redacted.includes('PRIVATE KEY-----')) {
116
147
  redacted = redacted.replace(/-----BEGIN [^-\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\n]*PRIVATE KEY-----/g, '<redacted>');
@@ -128,12 +159,20 @@ function redactCredentialShapes(value) {
128
159
  }
129
160
  function isSecretKey(key) {
130
161
  const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
162
+ if (normalized === 'tokens')
163
+ return true;
131
164
  if (SECRET_KEY_NAMES.some((name) => normalized === name))
132
165
  return true;
133
- const compoundNames = SECRET_KEY_NAMES.filter((name) => name !== 'auth');
166
+ if (normalized !== 'tokens'
167
+ && (normalized.endsWith('token') || /token(?:value|id|key|payload|secret)$/.test(normalized)))
168
+ return true;
169
+ if (/^(?:svsession|smsession|wixsession2?)(?:id|token|cookie|value)$/.test(normalized))
170
+ return true;
171
+ const compoundNames = SECRET_KEY_NAMES.filter((name) => !EXACT_ONLY_SECRET_KEY_NAMES.has(name));
134
172
  if (compoundNames.some((name) => normalized.startsWith(name) || normalized.endsWith(name)))
135
173
  return true;
136
- const segments = key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
174
+ const segments = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
175
+ .toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
137
176
  for (let start = 0; start < segments.length; start++) {
138
177
  let candidate = '';
139
178
  for (let end = start; end < segments.length; end++) {
@@ -144,6 +183,36 @@ function isSecretKey(key) {
144
183
  }
145
184
  return false;
146
185
  }
186
+ function isNumericTokenTelemetry(key, value) {
187
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
188
+ if (normalized === 'tokencount') {
189
+ return (typeof value === 'number' && Number.isFinite(value))
190
+ || (typeof value === 'string' && /^\d+$/.test(value));
191
+ }
192
+ if (!isRecord(value))
193
+ return false;
194
+ const entries = Object.entries(value);
195
+ if (normalized === 'tokenusage') {
196
+ return entries.length > 0 && entries.every(([entryKey, entryValue]) => (entryKey === 'inputTokens' || entryKey === 'outputTokens')
197
+ && typeof entryValue === 'number' && Number.isFinite(entryValue));
198
+ }
199
+ if (normalized !== 'tokens' || entries.length === 0)
200
+ return false;
201
+ return entries.every(([entryKey, entryValue]) => {
202
+ if (entryKey === 'input' || entryKey === 'output' || entryKey === 'reasoning') {
203
+ return typeof entryValue === 'number' && Number.isFinite(entryValue);
204
+ }
205
+ return entryKey === 'cache' && isRecord(entryValue)
206
+ && Object.entries(entryValue).every(([cacheKey, cacheValue]) => (cacheKey === 'write' || cacheKey === 'read')
207
+ && typeof cacheValue === 'number' && Number.isFinite(cacheValue));
208
+ });
209
+ }
210
+ function isSecretEnvironmentKey(key) {
211
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
212
+ if (NON_SECRET_ENVIRONMENT_KEY_NAMES.has(normalized))
213
+ return false;
214
+ return isSecretKey(key);
215
+ }
147
216
  function parseStructuredJson(value) {
148
217
  const trimmed = value.trim();
149
218
  if (!(trimmed.startsWith('{') && trimmed.endsWith('}'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -140,5 +140,5 @@
140
140
  "typescript": "^5.9.3",
141
141
  "zod": "4.3.6"
142
142
  },
143
- "falconPackageHash": "9c3889eefa996053dbb4d3dca4d3ef2dbfe9f7776d7e69e82b0d721f"
143
+ "falconPackageHash": "0debc4519da71de54bc8ca58c57f1ddbf0899cc2b06cbf908371e1e8"
144
144
  }