@vellumai/vellum-gateway 0.12.2 → 0.12.3-staging.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 (65) hide show
  1. package/AGENTS.md +6 -4
  2. package/ARCHITECTURE.md +1 -1
  3. package/README.md +1 -1
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  6. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  7. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  10. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/package.json +1 -1
  14. package/src/__tests__/telegram-normalize.test.ts +123 -111
  15. package/src/__tests__/telegram-webhook-drop-visibility.test.ts +229 -0
  16. package/src/__tests__/telegram-webhook-handler.test.ts +160 -0
  17. package/src/attachments/download.ts +5 -0
  18. package/src/attachments/ingest.test.ts +167 -3
  19. package/src/attachments/ingest.ts +139 -6
  20. package/src/channels/admission-drop-log.test.ts +47 -0
  21. package/src/channels/admission-drop-log.ts +94 -0
  22. package/src/channels/inbound-event.ts +9 -0
  23. package/src/channels/room-admission.test.ts +95 -0
  24. package/src/channels/room-admission.ts +149 -0
  25. package/src/discord/admission-log.test.ts +15 -12
  26. package/src/discord/admission-log.ts +11 -95
  27. package/src/discord/admit.test.ts +8 -9
  28. package/src/discord/admit.ts +48 -93
  29. package/src/discord/forward.ts +1 -1
  30. package/src/discord/gateway-socket.ts +4 -2
  31. package/src/discord/normalize.test.ts +34 -1
  32. package/src/discord/normalize.ts +23 -3
  33. package/src/email/attachments.test.ts +40 -5
  34. package/src/email/attachments.ts +71 -67
  35. package/src/email/inbound-pipeline.test.ts +1 -0
  36. package/src/email/inbound-pipeline.ts +1 -1
  37. package/src/handlers/handle-inbound.ts +3 -0
  38. package/src/http/routes/email-webhook.ts +1 -1
  39. package/src/http/routes/telegram-webhook.ts +52 -13
  40. package/src/http/routes/whatsapp-webhook.ts +1 -3
  41. package/src/index.ts +1 -1
  42. package/src/risk/bash-risk-classifier.test.ts +79 -0
  43. package/src/risk/command-registry/commands/dig.ts +2 -1
  44. package/src/risk/command-registry/commands/host.ts +2 -1
  45. package/src/risk/command-registry/commands/mtr.ts +2 -1
  46. package/src/risk/command-registry/commands/nslookup.ts +2 -1
  47. package/src/risk/command-registry/commands/ping.ts +2 -1
  48. package/src/risk/command-registry/commands/tracepath.ts +2 -1
  49. package/src/risk/command-registry/commands/traceroute.ts +2 -1
  50. package/src/risk/risk-types.ts +1 -0
  51. package/src/risk/shell-parser.test.ts +105 -1
  52. package/src/risk/shell-parser.ts +111 -4
  53. package/src/slack/message-normalizer.ts +14 -1
  54. package/src/slack/source-metadata.test.ts +5 -2
  55. package/src/slack/source-metadata.ts +2 -6
  56. package/src/telegram/admit.test.ts +155 -0
  57. package/src/telegram/admit.ts +149 -0
  58. package/src/telegram/bot-identity.test.ts +124 -0
  59. package/src/telegram/bot-identity.ts +127 -0
  60. package/src/telegram/drop-log.ts +41 -0
  61. package/src/telegram/normalize.test.ts +343 -134
  62. package/src/telegram/normalize.ts +197 -197
  63. package/src/telegram/schemas.ts +217 -0
  64. package/src/telegram/send.ts +1 -1
  65. package/src/whatsapp/download.ts +1 -0
package/AGENTS.md CHANGED
@@ -51,7 +51,7 @@ Route-registration consequences (`gateway/src/index.ts`):
51
51
 
52
52
  - **Contacts** (`/v1/contacts...`, `/v1/contact-channels...`) are served on **flat routes only** — both boundaries deliver flat, so no assistant-scoped contact mirrors exist. The self-hosted flattening covers the **entire** `contacts`/`contact-channels` segment family, not just the CRUD writes: invites requests land on the gateway's flat edge-scoped invite routes (the same gateway invite engine the daemon relays to), and daemon-only subpaths (`contacts/search`, `contacts/prompt`) match no flat registration and fall through the runtime-proxy catch-all to the daemon verbatim.
53
53
  - **Other control-plane families** (channel-admission-policy, channel-permission-overrides, trust-rules, backups) register **flat + assistant-scoped variants**: the self-hosted rewrite forwards their paths verbatim, so a scoped mirror must catch that traffic. These stores are gateway-global — the scoped variant matches and discards the id.
54
- - **Invariant: never remove a flat gateway control-plane route.** Cloud's prefix strip means the flat family is the load-bearing production path even when repo-local clients appear to emit only scoped URLs. The flat channel-admission-policy routes were removed once on that mistaken theory (`2fb435314f`) and had to be restored before #35150 merged (`53fcfa7cc2` re-added the schema entries the removal took out).
54
+ - **Invariant: never remove a flat gateway control-plane route.** Cloud's prefix strip means the flat family is the load-bearing production path even when repo-local clients appear to emit only scoped URLs. The flat channel-admission-policy routes were removed once on that mistaken theory ([`2fb435314f`](https://github.com/vellum-ai/vellum-assistant/commit/2fb435314f)) and had to be restored before #35150 merged ([`53fcfa7cc2`](https://github.com/vellum-ai/vellum-assistant/commit/53fcfa7cc2) re-added the schema entries the removal took out); both commits are squashed out of `main`'s history, so they resolve on GitHub and not in a clone.
55
55
 
56
56
  ### Trust Management in Docker Mode
57
57
 
@@ -90,6 +90,8 @@ Gateway inbound events use a channel-discriminated union model (`GatewayInboundE
90
90
 
91
91
  Trust/guardian decisions must be keyed on `actorExternalId` only — never fall back to `conversationExternalId` for actor identity.
92
92
 
93
+ **Room admission is one verdict, two adapter halves.** Whether the gateway acts on an inbound message at all is decided by `channels/room-admission.ts` over a neutral candidate: self and bot authors drop first, a direct chat is admitted on its own lane without a mention, a room only when the message addresses the bot, a chat kind the product does not serve is refused, and an unknown bot identity drops rather than admits. Each channel builds the candidate from its own facts (`discord/admit.ts`, `telegram/admit.ts`): how the platform proves a chat is direct (guild absence, `chat.type`) and how it proves a message is addressed (a mentions array, text entities, a reply to the bot's post). Extend the verdict, never a copy of it; a platform-only rule rides a neutral fact (Discord's legacy allow-list is `roomAllowed`). A drop is never silent: every channel logs it through `channels/admission-drop-log.ts`, one promoted line per reason and conversation, with a drop that names no conversation promoted every time. Slack's socket filter predates this and still carries the same rules inline; converging it is queued with the Slack envelope work.
94
+
93
95
  Physical DB column names (`externalUserId`, `externalChatId`) are unchanged; the rename is at the API/type layer only.
94
96
 
95
97
  **Provider words that are not our words.** A Discord **guild** is what Discord's
@@ -193,7 +195,7 @@ Both the flat (`/v1/channel-admission-policy/...`) and assistant-scoped (`/v1/as
193
195
 
194
196
  - **Gateway kill switch** — `handle-inbound.ts` enforces the `no_one` floor before forwarding. Zero contact-table lookups, zero daemon I/O, true kill.
195
197
  - **Runtime floor** — every other policy flows through the gateway unchanged; the runtime evaluates rank-vs-floor inside `admission-policy.ts`. This keeps the canonical gateway classifier (`gateway/src/risk/trust-verdict-resolver.ts`) as the single source of `TrustClass` truth (no fork): the runtime consumes the stamped verdict; the daemon's `actor-trust-resolver.ts` is only a residual sync guardian-or-unknown view for the vellum reset-drift path.
196
- - **Gateway vs runtime reciprocity** — the gateway section in `gateway/CLAUDE.md` records _which channels the gateway enforces_; the assistant section records _how the runtime classifies_. Either side getting out of sync is a bug, not an over-defended boundary.
198
+ - **Gateway vs runtime reciprocity**: this section records _which channels the gateway enforces_; the runtime's evaluation lives in `assistant/src/runtime/routes/inbound-stages/admission-policy.ts`, which reads `TRUST_CLASS_RANK` and `ADMISSION_FLOOR` from the shared contract. Either side getting out of sync is a bug, not an over-defended boundary.
197
199
 
198
200
  **Adding a new policy**: extend the `AdmissionPolicy` union in `packages/gateway-client/src/admission-policy-contract.ts`, add its floor in `ADMISSION_FLOOR`, update the openapi schema, and update `gateway/src/__tests__/channel-admission-policy-routes.test.ts` + `assistant/src/runtime/routes/inbound-stages/admission-policy.test.ts`. Do not add a 6th floor without also bumping the `TRUST_CLASS_RANK` ceiling to match.
199
201
 
@@ -208,9 +210,9 @@ The gateway owns channel-permission matrix storage (`gateway/src/db/channel-perm
208
210
  - **Cascade, least → most specific:** `workspace` → `adapter` → `channel_type` (`dm | private | public`) → `channel` (external channel ID). `ChannelPermissionStore.resolve()` walks most-specific-first and returns the first cell set for the contact-type.
209
211
  - **Vocabulary contract:** `packages/gateway-client/src/channel-permission-contract.ts` (selectors, thresholds, scopes, resolve request). The contact-type axis is the canonical `TrustClass` — granularity intentionally stops there; do not add per-individual-contact cells.
210
212
  - **IPC surface:** `list_channel_permission_overrides`, `set_channel_permission_override`, `delete_channel_permission_override`, `resolve_channel_permission_threshold`. Writes validate the adapter against the gateway channel registry.
211
- - **HTTP surface (configuration clients):** `GET`/`PUT /v1/channel-permission-overrides` + `POST /v1/channel-permission-overrides/delete` + `POST /v1/channel-permission-overrides/resolve` (`gateway/src/http/routes/channel-permission-overrides.ts`), published in the gateway OpenAPI spec for the web SDK. Mirrors the IPC list/set/delete/resolve with the same contract schemas and adapter validation. Resolve over HTTP is read-only (`settings.read`) and exists so configuration clients can display the effective fall-through without re-implementing the cascade walk client-side (which drifts — the runtime resolves Slack rooms with no conversation type, so a client-side walk would apply `channel_type` cells the evaluator never matches); the runtime evaluator keeps using the IPC resolve. Flat + assistant-scoped variants, `settings.read`/`settings.write` scopes — same shape as channel-admission-policy.
213
+ - **HTTP surface (configuration clients):** `GET`/`PUT /v1/channel-permission-overrides` + `POST /v1/channel-permission-overrides/delete` + `POST /v1/channel-permission-overrides/resolve` (`gateway/src/http/routes/channel-permission-overrides.ts`), published in the gateway OpenAPI spec for the web SDK. Mirrors the IPC list/set/delete/resolve with the same contract schemas and adapter validation. Resolve over HTTP is read-only (`settings.read`) and exists so configuration clients can display the effective fall-through without re-implementing the cascade walk client-side (which drifts as the cascade evolves); the runtime evaluator keeps using the IPC resolve. Flat + assistant-scoped variants, `settings.read`/`settings.write` scopes: same shape as channel-admission-policy.
212
214
  - **Migration provenance:** `m0012-migrate-slack-channel-permissions` lifts Slack-skill `channelPermissions` profiles with `trustLevel: "restricted"` into channel-scoped Strict cells (non-guardian contact-types). Per-tool fields (`blockedTools` / `allowedToolCategories`) have no matrix representation — they stay in the Slack skill config, enforced by the legacy deterministic channel gate in `assistant/src/tools/tool-approval-handler.ts`.
213
- - **Runtime consumption (per-tool-call evaluation):** the assistant's permission checker (`assistant/src/permissions/checker.ts`) builds a resolve query from the turn's `PolicyContext` (adapter = source channel, conversation type, external channel ID, contact-type = trust class) and threads it into the threshold cascade in `assistant/src/permissions/gateway-threshold-reader.ts`. The cell sits between the per-conversation override (most specific) and the global defaults; the winning threshold feeds `DefaultApprovalPolicy.evaluate` as `autoApproveUpTo`, composing cell RiskThreshold × tool RiskLevel with the untouched capability floor (`resolveSensitiveToolDecision`). Fail-safe semantics: a cell transport failure falls through to global on the cached read, but the pre-prompt refresh keeps its prompt (returns null) rather than falling through to a possibly-looser global. Slack non-DMs resolve with no conversation type (the gateway forwards public and private alike as `"channel"`), so the `channel_type` tier only matches DMs/groups until the gateway forwards the distinction.
215
+ - **Runtime consumption (per-tool-call evaluation):** the assistant's permission checker (`assistant/src/permissions/checker.ts`) builds a resolve query from the turn's `PolicyContext` (adapter = source channel, conversation type, external channel ID, contact-type = trust class) and threads it into the threshold cascade in `assistant/src/permissions/gateway-threshold-reader.ts`. The cell sits between the per-conversation override (most specific) and the global defaults; the winning threshold feeds `DefaultApprovalPolicy.evaluate` as `autoApproveUpTo`, composing cell RiskThreshold × tool RiskLevel with the untouched capability floor (`resolveSensitiveToolDecision`). Fail-safe semantics: a cell transport failure falls through to global on the cached read, but the pre-prompt refresh keeps its prompt (returns null) rather than falling through to a possibly-looser global. Slack states `conversationType` for every room it can classify (`slackConversationVisibility` in `gateway/src/slack/message-normalizer.ts`: a public channel is `public`, a private channel or group DM is `private`, a DM is `dm`), so the `channel_type` tier matches Slack rooms as well as DMs.
214
216
 
215
217
  ### Trust Classes → Capabilities (what an actor may do)
216
218
 
package/ARCHITECTURE.md CHANGED
@@ -443,7 +443,7 @@ Runtime detects needs_confirmation
443
443
  | `assistant/src/channels/gateway-guardian-requests.ts` | Typed daemon client for the gateway-owned `guardian_requests` lifecycle (`guardian_requests_create` / `_decide` / `_list_expired_pending` / `_expire`) |
444
444
  | `assistant/src/runtime/gateway-client.ts` | `deliverApprovalPrompt()` — sends approval payload to gateway |
445
445
  | `gateway/src/telegram/send.ts` | `buildInlineKeyboard()` — renders approval actions as Telegram inline buttons |
446
- | `gateway/src/telegram/normalize.ts` | `callback_query` normalization into `GatewayInboundEvent` (DM-only, drops callbacks without data) |
446
+ | `gateway/src/telegram/normalize.ts` | `callback_query` normalization into `GatewayInboundEvent` (private chats, groups, and supergroups; drops callbacks without data) |
447
447
 
448
448
  ### Approval Message Composer
449
449
 
package/README.md CHANGED
@@ -106,7 +106,7 @@ The gateway normalizes Telegram `callback_query` updates (inline button clicks)
106
106
 
107
107
  These fields are forwarded to the runtime in the `/channels/inbound` payload alongside the standard `conversationExternalId`, `externalMessageId`, and actor metadata. The runtime uses `callbackData` to route the click to the appropriate approval handler.
108
108
 
109
- **Normalization constraints:** Only DM-only (`private` chat type) callback queries are processed. Group and channel callbacks are dropped and acknowledged with `answerCallbackQuery` so the Telegram button spinner clears. Callback queries with no `data` field or no associated `message` are also dropped.
109
+ **Normalization constraints:** Callback queries from private chats, groups, and supergroups are processed; a tap on a keyboard the bot posted is addressed to the bot by construction, and who may press it is the runtime's decision. Callbacks from broadcast channels are dropped and acknowledged with `answerCallbackQuery` so the Telegram button spinner clears. Callback queries with no `data` field or no associated `message` are also dropped. Every drop is logged with its reason before the update is acknowledged (`telegram/drop-log.ts`).
110
110
 
111
111
  **Stale callback blocking:** When the runtime receives `callbackData` that does not match any pending approval (e.g., a button from an old prompt), it returns `stale_ignored` and does not process the payload as a regular message. This is enforced regardless of whether the callback has non-empty content. The gateway sends a best-effort `answerCallbackQuery` acknowledgment for normalized callback updates (including stale, rejected, and forward-failure paths) so the button spinner clears promptly. Transient forwarding failures may still return `500` so Telegram retries update delivery.
112
112
 
@@ -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-|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
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, "");
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  buildShellInvocation,
5
+ buildShellSpawnFlags,
5
6
  pathListDelimiter,
6
7
  prependUniquePathEntries,
7
8
  } from "./shell.js";
@@ -38,6 +39,26 @@ describe("buildShellInvocation", () => {
38
39
  });
39
40
  });
40
41
 
42
+ describe("buildShellSpawnFlags", () => {
43
+ test("creates a POSIX process group and hides Windows consoles", () => {
44
+ expect(buildShellSpawnFlags("linux")).toEqual({
45
+ detached: true,
46
+ windowsHide: true,
47
+ });
48
+ expect(buildShellSpawnFlags("darwin")).toEqual({
49
+ detached: true,
50
+ windowsHide: true,
51
+ });
52
+ });
53
+
54
+ test("does not detach Windows children that use piped stdio", () => {
55
+ expect(buildShellSpawnFlags("win32")).toEqual({
56
+ detached: false,
57
+ windowsHide: true,
58
+ });
59
+ });
60
+ });
61
+
41
62
  describe("path list handling", () => {
42
63
  test("uses the platform delimiter", () => {
43
64
  expect(pathListDelimiter("win32")).toBe(";");
@@ -5,6 +5,11 @@ export interface ShellInvocation {
5
5
  args: string[];
6
6
  }
7
7
 
8
+ export interface ShellSpawnFlags {
9
+ detached: boolean;
10
+ windowsHide: true;
11
+ }
12
+
8
13
  const WINDOWS_UTF8_PREAMBLE =
9
14
  "try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}; " +
10
15
  "$OutputEncoding = [System.Text.Encoding]::UTF8; " +
@@ -43,6 +48,25 @@ export function buildShellInvocation(
43
48
  return { command: "bash", args: ["-c", "--", command] };
44
49
  }
45
50
 
51
+ /**
52
+ * Spawn flags for assistant-owned shell children (sandbox bash, local
53
+ * host_bash fallback, sanitized CLI bash, skill runners, scheduled scripts).
54
+ *
55
+ * POSIX uses a new process group so timeout/abort can SIGKILL the tree via
56
+ * `-pid`. Windows process trees are torn down with `taskkill /T`, which does
57
+ * not need a detached process. Combining `DETACHED_PROCESS`,
58
+ * `CREATE_NO_WINDOW`, and piped stdio on Windows can emit `close` with exit
59
+ * 0 and empty pipes without running the encoded command.
60
+ */
61
+ export function buildShellSpawnFlags(
62
+ hostPlatform: NodeJS.Platform = process.platform,
63
+ ): ShellSpawnFlags {
64
+ return {
65
+ detached: hostPlatform !== "win32",
66
+ windowsHide: true,
67
+ };
68
+ }
69
+
46
70
  export function pathListDelimiter(
47
71
  hostPlatform: NodeJS.Platform = process.platform,
48
72
  ): string {
@@ -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-|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
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, "");
@@ -83,8 +83,14 @@ export const SourceMetadataSchema = z
83
83
  clientTimezone: z.string().optional(),
84
84
  /** Channel command intent (e.g. Telegram /start). */
85
85
  commandIntent: CommandIntentSchema.optional(),
86
- /** Slack-specific: whether the bot was @-mentioned. */
87
- slackBotMentioned: z.boolean().optional(),
86
+ /**
87
+ * Whether the message addresses the assistant by name, an @-mention on
88
+ * the platforms that have one. Stated by the channel's normalizer only
89
+ * where it proved the answer; absent means "not established". Read to
90
+ * decide whether a reply is expected before any text exists, never to
91
+ * admit or deny: admission is the gate's job, upstream of this.
92
+ */
93
+ botMentioned: z.boolean().optional(),
88
94
  /**
89
95
  * Slack-specific: the `edited.ts` float-string from a `message_changed`
90
96
  * event. Used by the daemon to reject out-of-order edit deliveries whose
@@ -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-|-----BEGIN [A-Z ]*PRIVATE KEY-----)/,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.12.2",
3
+ "version": "0.12.3-staging.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {