@vellumai/assistant 0.12.2-dev.202609162314.7778bc8 → 0.12.2-dev.202609170127.2eef54c
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/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/package.json +1 -1
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/desktop/desktop-wallpaper.test.ts +12 -1
|
@@ -63,6 +63,7 @@ const FIXTURES: Record<string, string> = {
|
|
|
63
63
|
"Perplexity API Key": `pplx-${filler(40)}`,
|
|
64
64
|
"Tavily API Key": `tvly-${filler(20)}`,
|
|
65
65
|
"Firecrawl API Key": `fc-${filler(20)}`,
|
|
66
|
+
"Resend API Key": `re_${filler(8)}_${filler(24)}`,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
describe("subpath export", () => {
|
|
@@ -230,6 +230,9 @@ export const PREFIX_PATTERNS: SecretPrefixPattern[] = [
|
|
|
230
230
|
|
|
231
231
|
// -- Firecrawl --
|
|
232
232
|
{ label: "Firecrawl API Key", regex: /fc-[A-Za-z0-9]{20,}/ },
|
|
233
|
+
// Resend documents only the `re_` prefix; the segment shape is the observed
|
|
234
|
+
// key format. Strict on purpose: bare `re_` prefixes ordinary identifiers.
|
|
235
|
+
{ label: "Resend API Key", regex: /re_[A-Za-z0-9]{8}_[A-Za-z0-9]{24,}/ },
|
|
233
236
|
];
|
|
234
237
|
|
|
235
238
|
/**
|
|
@@ -362,7 +365,7 @@ export function isPlaceholderValue(value: string): boolean {
|
|
|
362
365
|
// Strip known prefixes to isolate the variable part
|
|
363
366
|
const variablePart = value
|
|
364
367
|
.replace(
|
|
365
|
-
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx
|
|
368
|
+
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx-|re_|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
|
|
366
369
|
"",
|
|
367
370
|
)
|
|
368
371
|
.replace(/[^A-Za-z0-9]/g, "");
|
|
@@ -63,6 +63,7 @@ const FIXTURES: Record<string, string> = {
|
|
|
63
63
|
"Perplexity API Key": `pplx-${filler(40)}`,
|
|
64
64
|
"Tavily API Key": `tvly-${filler(20)}`,
|
|
65
65
|
"Firecrawl API Key": `fc-${filler(20)}`,
|
|
66
|
+
"Resend API Key": `re_${filler(8)}_${filler(24)}`,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
describe("subpath export", () => {
|
|
@@ -230,6 +230,9 @@ export const PREFIX_PATTERNS: SecretPrefixPattern[] = [
|
|
|
230
230
|
|
|
231
231
|
// -- Firecrawl --
|
|
232
232
|
{ label: "Firecrawl API Key", regex: /fc-[A-Za-z0-9]{20,}/ },
|
|
233
|
+
// Resend documents only the `re_` prefix; the segment shape is the observed
|
|
234
|
+
// key format. Strict on purpose: bare `re_` prefixes ordinary identifiers.
|
|
235
|
+
{ label: "Resend API Key", regex: /re_[A-Za-z0-9]{8}_[A-Za-z0-9]{24,}/ },
|
|
233
236
|
];
|
|
234
237
|
|
|
235
238
|
/**
|
|
@@ -362,7 +365,7 @@ export function isPlaceholderValue(value: string): boolean {
|
|
|
362
365
|
// Strip known prefixes to isolate the variable part
|
|
363
366
|
const variablePart = value
|
|
364
367
|
.replace(
|
|
365
|
-
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx
|
|
368
|
+
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx-|re_|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
|
|
366
369
|
"",
|
|
367
370
|
)
|
|
368
371
|
.replace(/[^A-Za-z0-9]/g, "");
|
|
@@ -63,6 +63,7 @@ const FIXTURES: Record<string, string> = {
|
|
|
63
63
|
"Perplexity API Key": `pplx-${filler(40)}`,
|
|
64
64
|
"Tavily API Key": `tvly-${filler(20)}`,
|
|
65
65
|
"Firecrawl API Key": `fc-${filler(20)}`,
|
|
66
|
+
"Resend API Key": `re_${filler(8)}_${filler(24)}`,
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
describe("subpath export", () => {
|
|
@@ -230,6 +230,9 @@ export const PREFIX_PATTERNS: SecretPrefixPattern[] = [
|
|
|
230
230
|
|
|
231
231
|
// -- Firecrawl --
|
|
232
232
|
{ label: "Firecrawl API Key", regex: /fc-[A-Za-z0-9]{20,}/ },
|
|
233
|
+
// Resend documents only the `re_` prefix; the segment shape is the observed
|
|
234
|
+
// key format. Strict on purpose: bare `re_` prefixes ordinary identifiers.
|
|
235
|
+
{ label: "Resend API Key", regex: /re_[A-Za-z0-9]{8}_[A-Za-z0-9]{24,}/ },
|
|
233
236
|
];
|
|
234
237
|
|
|
235
238
|
/**
|
|
@@ -362,7 +365,7 @@ export function isPlaceholderValue(value: string): boolean {
|
|
|
362
365
|
// Strip known prefixes to isolate the variable part
|
|
363
366
|
const variablePart = value
|
|
364
367
|
.replace(
|
|
365
|
-
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx
|
|
368
|
+
/^(?:AKIA|gh[pousr]_|github_pat_|glpat-|sk_live_|rk_live_|xoxb-|xoxp-|xapp-|sk-ant-|sk-proj-|sk-or-v1-|AIza|GOCSPX-|SK|SG\.|npm_|pypi-|key-|lin_api_|ntn_|fw_|pplx-|re_|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
|
|
366
369
|
"",
|
|
367
370
|
)
|
|
368
371
|
.replace(/[^A-Za-z0-9]/g, "");
|
package/package.json
CHANGED
|
@@ -204,6 +204,22 @@ describe("collectRevealRefsFromCommand", () => {
|
|
|
204
204
|
);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
test("parses a legacy service/field path", () => {
|
|
208
|
+
expect(
|
|
209
|
+
collectRevealRefsFromCommand(
|
|
210
|
+
"assistant credentials reveal resend/api_key",
|
|
211
|
+
),
|
|
212
|
+
).toEqual([{ service: "resend", field: "api_key" }]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("legacy path survives malformed extra arguments", () => {
|
|
216
|
+
expect(
|
|
217
|
+
collectRevealRefsFromCommand(
|
|
218
|
+
"assistant credentials reveal resend/api_key --json extra",
|
|
219
|
+
),
|
|
220
|
+
).toEqual([{ service: "resend", field: "api_key" }]);
|
|
221
|
+
});
|
|
222
|
+
|
|
207
223
|
test("unparseable invocation yields no ref (fails safe)", () => {
|
|
208
224
|
expect(
|
|
209
225
|
collectRevealRefsFromCommand("assistant credentials reveal --service x"),
|
|
@@ -437,6 +453,13 @@ describe("redactSecretsForChat", () => {
|
|
|
437
453
|
expect(out).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
438
454
|
});
|
|
439
455
|
|
|
456
|
+
test("Resend key in persisted assistant text is redacted without a candidate", () => {
|
|
457
|
+
const key = `re_${"a".repeat(8)}_${"b".repeat(24)}`;
|
|
458
|
+
const out = redactSecretsForChat(`the key is ${key} and it failed`, []);
|
|
459
|
+
expect(out).not.toContain(key);
|
|
460
|
+
expect(out).toContain("\u3014redacted:Resend API Key\u3015");
|
|
461
|
+
});
|
|
462
|
+
|
|
440
463
|
test("no candidate match produces the plain sentinel — never a guess", () => {
|
|
441
464
|
const out = redactSecretsForChat(`key: ${SYNTHETIC_OPENAI_PROJECT_KEY}`, [
|
|
442
465
|
{ service: "openai", field: "api_key", value: "different-value" },
|
|
@@ -187,6 +187,26 @@ describe("Anthropic keys", () => {
|
|
|
187
187
|
});
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Resend
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
describe("Resend keys", () => {
|
|
194
|
+
const key = `re_${"a".repeat(8)}_${"b".repeat(24)}`;
|
|
195
|
+
|
|
196
|
+
test("detects re_ key", () => {
|
|
197
|
+
expectMatch(key, "Resend API Key");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("redacts a key echoed from shell output", () => {
|
|
201
|
+
const out = redactSecrets(`$ cat .env\nRESEND=${key}\n`);
|
|
202
|
+
expect(out).not.toContain(key);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("ignores re_ identifiers that are not key-shaped", () => {
|
|
206
|
+
expectNoMatch("re_0_of_1_is_deprecated_6387");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
190
210
|
// ---------------------------------------------------------------------------
|
|
191
211
|
// OpenAI
|
|
192
212
|
// ---------------------------------------------------------------------------
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 1. While a turn runs, every shell-style tool command is scanned for
|
|
12
12
|
* `credentials reveal` invocations and the referenced credentials
|
|
13
|
-
* (`--service`/`--field` flags
|
|
13
|
+
* (`--service`/`--field` flags, a positional UUID, or a legacy
|
|
14
|
+
* `service/field` path) are recorded as
|
|
14
15
|
* *candidates* ({@link collectRevealRefsFromCommand}).
|
|
15
16
|
* 2. At persist time, only those candidates' plaintexts are fetched from
|
|
16
17
|
* the credential store — a scoped read of secrets that were already in
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
} from "../security/secret-scanner.js";
|
|
56
57
|
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
57
58
|
import { getCredentialMetadataById } from "../tools/credentials/metadata-store.js";
|
|
59
|
+
import { parseServiceFieldRef } from "../tools/credentials/ref-parse.js";
|
|
58
60
|
import { getLogger } from "../util/logger.js";
|
|
59
61
|
|
|
60
62
|
const log = getLogger("chat-credential-redaction");
|
|
@@ -106,6 +108,12 @@ const SERVICE_FLAG_RE = new RegExp(String.raw`--service(?:=|\s+)${FLAG_VALUE}`);
|
|
|
106
108
|
const FIELD_FLAG_RE = new RegExp(String.raw`--field(?:=|\s+)${FLAG_VALUE}`);
|
|
107
109
|
const UUID_RE =
|
|
108
110
|
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
111
|
+
// The CLI's single positional argument, directly after `reveal`. A UUID is
|
|
112
|
+
// handled above; anything else is tried as a legacy `service/field` path via
|
|
113
|
+
// the shared grammar in `parseServiceFieldRef`. The route rejects that form
|
|
114
|
+
// as an id today, so no plaintext prints; staging the ref is defense in depth
|
|
115
|
+
// so a future lookup that does succeed is already proof-gated.
|
|
116
|
+
const POSITIONAL_RE = /\breveal\s+([^\s"'\\()<>`-][^\s"'\\()<>`]*)/;
|
|
109
117
|
|
|
110
118
|
/**
|
|
111
119
|
* Extract a flag's value from a segment, undoing the shell quoting the
|
|
@@ -265,6 +273,13 @@ export function collectRevealRefsFromCommand(
|
|
|
265
273
|
const id = UUID_RE.exec(invocation)?.[0];
|
|
266
274
|
if (id) {
|
|
267
275
|
refs.push({ id });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const positional = POSITIONAL_RE.exec(invocation)?.[1];
|
|
279
|
+
const pathRef =
|
|
280
|
+
positional === undefined ? undefined : parseServiceFieldRef(positional);
|
|
281
|
+
if (pathRef) {
|
|
282
|
+
refs.push(pathRef);
|
|
268
283
|
}
|
|
269
284
|
// No parseable identity → no ref. The span still gets redacted at
|
|
270
285
|
// persist; it just won't be revealable.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
expect,
|
|
8
|
+
setDefaultTimeout,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
5
11
|
|
|
6
12
|
import sharp from "sharp";
|
|
7
13
|
|
|
@@ -14,6 +20,11 @@ import {
|
|
|
14
20
|
renderDesktopWallpaper,
|
|
15
21
|
} from "./desktop-wallpaper.js";
|
|
16
22
|
|
|
23
|
+
// Native resvg renders scan the system font database per instance, and on a
|
|
24
|
+
// loaded CI runner the file's first render runs past bun's 5s default. Each
|
|
25
|
+
// test file runs in its own process, so this doesn't leak.
|
|
26
|
+
setDefaultTimeout(15_000);
|
|
27
|
+
|
|
17
28
|
let workspace: string;
|
|
18
29
|
let previousWorkspace: string | undefined;
|
|
19
30
|
beforeEach(() => {
|