@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -1,3 +1,7 @@
1
+ import type {
2
+ OAuthConnection,
3
+ OAuthConnectionResponse,
4
+ } from "../../../oauth/connection.js";
1
5
  import type {
2
6
  GmailAttachment,
3
7
  GmailDraft,
@@ -15,7 +19,6 @@ import type {
15
19
  GmailVacationSettings,
16
20
  } from "./types.js";
17
21
 
18
- const GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
19
22
  const GMAIL_BATCH_URL = "https://www.googleapis.com/batch/gmail/v1";
20
23
 
21
24
  /** Max sub-requests per batch HTTP call (Gmail API limit) */
@@ -36,6 +39,7 @@ export class GmailApiError extends Error {
36
39
 
37
40
  const MAX_RETRIES = 3;
38
41
  const INITIAL_BACKOFF_MS = 1000;
42
+ /** Timeout for batch API calls that bypass OAuthConnection.request() (which has its own 30s timeout). */
39
43
  const REQUEST_TIMEOUT_MS = 30_000;
40
44
 
41
45
  function isRetryable(status: number): boolean {
@@ -54,53 +58,99 @@ interface GmailRequestOptions extends RequestInit {
54
58
  retryable?: boolean;
55
59
  }
56
60
 
61
+ /**
62
+ * Extract non-Authorization headers from request options for use with OAuthConnection.
63
+ */
64
+ function extractNonAuthHeaders(
65
+ options?: GmailRequestOptions,
66
+ ): Record<string, string> | undefined {
67
+ if (!options?.headers) return undefined;
68
+ const raw = options.headers;
69
+ const result: Record<string, string> = {};
70
+ if (raw instanceof Headers) {
71
+ raw.forEach((v, k) => {
72
+ if (k.toLowerCase() !== "authorization") result[k] = v;
73
+ });
74
+ } else if (Array.isArray(raw)) {
75
+ for (const [k, v] of raw) {
76
+ if (k.toLowerCase() !== "authorization") result[k] = v;
77
+ }
78
+ } else {
79
+ for (const [k, v] of Object.entries(raw)) {
80
+ if (k.toLowerCase() !== "authorization" && v !== undefined) result[k] = v;
81
+ }
82
+ }
83
+ return Object.keys(result).length > 0 ? result : undefined;
84
+ }
85
+
86
+ /**
87
+ * Extract the JSON body from request options for use with OAuthConnection.
88
+ */
89
+ function extractBody(options?: GmailRequestOptions): unknown | undefined {
90
+ if (!options?.body) return undefined;
91
+ if (typeof options.body === "string") {
92
+ try {
93
+ return JSON.parse(options.body);
94
+ } catch {
95
+ return options.body;
96
+ }
97
+ }
98
+ return options.body;
99
+ }
100
+
57
101
  async function request<T>(
58
- token: string,
102
+ connection: OAuthConnection,
59
103
  path: string,
60
104
  options?: GmailRequestOptions,
61
105
  ): Promise<T> {
62
- const url = `${GMAIL_API_BASE}${path}`;
63
106
  const canRetry = options?.retryable ?? isIdempotent(options);
107
+ const method = (options?.method ?? "GET").toUpperCase();
64
108
 
65
109
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
66
- const resp = await fetch(url, {
67
- ...options,
68
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
69
- headers: {
70
- Authorization: `Bearer ${token}`,
71
- "Content-Type": "application/json",
72
- ...options?.headers,
73
- },
74
- });
110
+ let resp: OAuthConnectionResponse;
111
+ try {
112
+ resp = await connection.request({
113
+ method,
114
+ path,
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ ...extractNonAuthHeaders(options),
118
+ },
119
+ body: extractBody(options),
120
+ });
121
+ } catch (err) {
122
+ // Network-level errors from connection.request() are not retryable
123
+ throw err;
124
+ }
75
125
 
76
- if (!resp.ok) {
126
+ if (resp.status < 200 || resp.status >= 300) {
77
127
  if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
78
- const retryAfter = resp.headers.get("retry-after");
128
+ const retryAfter =
129
+ resp.headers["retry-after"] ?? resp.headers["Retry-After"];
79
130
  const delayMs = retryAfter
80
131
  ? parseInt(retryAfter, 10) * 1000
81
132
  : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
82
133
  await new Promise((resolve) => setTimeout(resolve, delayMs));
83
134
  continue;
84
135
  }
85
- const body = await resp.text().catch(() => "");
136
+ const bodyStr =
137
+ typeof resp.body === "string"
138
+ ? resp.body
139
+ : JSON.stringify(resp.body ?? "");
86
140
  throw new GmailApiError(
87
141
  resp.status,
88
- resp.statusText,
89
- `Gmail API ${resp.status}: ${body}`,
142
+ "",
143
+ `Gmail API ${resp.status}: ${bodyStr}`,
90
144
  );
91
145
  }
92
146
 
93
- // Some endpoints (e.g. batchModify) return empty success responses
94
- const contentLength = resp.headers.get("content-length");
95
- if (resp.status === 204 || contentLength === "0") {
147
+ // Success body is already parsed by connection.request()
148
+ if (resp.status === 204 || resp.body === undefined) {
96
149
  return undefined as T;
97
150
  }
98
- const text = await resp.text();
99
- if (!text) return undefined as T;
100
- return JSON.parse(text) as T;
151
+ return resp.body as T;
101
152
  }
102
153
 
103
- // Unreachable — the loop always returns or throws — but TypeScript needs this
104
154
  throw new Error(
105
155
  "Unreachable: retry loop exited without returning or throwing",
106
156
  );
@@ -108,7 +158,7 @@ async function request<T>(
108
158
 
109
159
  /** List messages matching a query. */
110
160
  export async function listMessages(
111
- token: string,
161
+ connection: OAuthConnection,
112
162
  query?: string,
113
163
  maxResults = 20,
114
164
  pageToken?: string,
@@ -121,12 +171,12 @@ export async function listMessages(
121
171
  if (labelIds) {
122
172
  for (const id of labelIds) params.append("labelIds", id);
123
173
  }
124
- return request<GmailMessageListResponse>(token, `/messages?${params}`);
174
+ return request<GmailMessageListResponse>(connection, `/messages?${params}`);
125
175
  }
126
176
 
127
177
  /** Get a single message by ID. */
128
178
  export async function getMessage(
129
- token: string,
179
+ connection: OAuthConnection,
130
180
  messageId: string,
131
181
  format: GmailMessageFormat = "full",
132
182
  metadataHeaders?: string[],
@@ -137,7 +187,7 @@ export async function getMessage(
137
187
  for (const h of metadataHeaders) params.append("metadataHeaders", h);
138
188
  }
139
189
  if (fields) params.set("fields", fields);
140
- return request<GmailMessage>(token, `/messages/${messageId}?${params}`);
190
+ return request<GmailMessage>(connection, `/messages/${messageId}?${params}`);
141
191
  }
142
192
 
143
193
  /**
@@ -175,7 +225,7 @@ function parseSubResponse(
175
225
  * Returns successfully parsed messages and a list of IDs that failed (for individual retry).
176
226
  */
177
227
  async function executeBatchCall(
178
- token: string,
228
+ connection: OAuthConnection,
179
229
  messageIds: string[],
180
230
  format: GmailMessageFormat,
181
231
  metadataHeaders: string[] | undefined,
@@ -203,70 +253,83 @@ async function executeBatchCall(
203
253
  );
204
254
  const body = parts.join("") + `--${boundary}--\r\n`;
205
255
 
206
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
207
- const resp = await fetch(GMAIL_BATCH_URL, {
208
- method: "POST",
209
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS * 2),
210
- headers: {
211
- Authorization: `Bearer ${token}`,
212
- "Content-Type": `multipart/mixed; boundary=${boundary}`,
213
- },
214
- body,
215
- });
216
-
217
- if (!resp.ok) {
218
- if (isRetryable(resp.status) && attempt < MAX_RETRIES) {
219
- const retryAfter = resp.headers.get("retry-after");
220
- const delayMs = retryAfter
221
- ? parseInt(retryAfter, 10) * 1000
222
- : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
223
- await new Promise((r) => setTimeout(r, delayMs));
224
- continue;
256
+ const doBatchFetch = async (token: string) => {
257
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
258
+ const resp = await fetch(GMAIL_BATCH_URL, {
259
+ method: "POST",
260
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS * 2),
261
+ headers: {
262
+ Authorization: `Bearer ${token}`,
263
+ "Content-Type": `multipart/mixed; boundary=${boundary}`,
264
+ },
265
+ body,
266
+ });
267
+
268
+ if (!resp.ok) {
269
+ if (isRetryable(resp.status) && attempt < MAX_RETRIES) {
270
+ const retryAfter = resp.headers.get("retry-after");
271
+ const delayMs = retryAfter
272
+ ? parseInt(retryAfter, 10) * 1000
273
+ : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
274
+ await new Promise((r) => setTimeout(r, delayMs));
275
+ continue;
276
+ }
277
+ const errBody = await resp.text().catch(() => "");
278
+ throw new GmailApiError(
279
+ resp.status,
280
+ resp.statusText,
281
+ `Gmail batch API ${resp.status}: ${errBody}`,
282
+ );
225
283
  }
226
- const errBody = await resp.text().catch(() => "");
227
- throw new GmailApiError(
228
- resp.status,
229
- resp.statusText,
230
- `Gmail batch API ${resp.status}: ${errBody}`,
231
- );
232
- }
233
284
 
234
- const contentType = resp.headers.get("content-type") ?? "";
235
- const responseText = await resp.text();
285
+ const contentType = resp.headers.get("content-type") ?? "";
286
+ const responseText = await resp.text();
236
287
 
237
- const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
238
- const respBoundary = boundaryMatch?.[1] ?? boundaryMatch?.[2];
239
- if (!respBoundary)
240
- throw new Error("Missing boundary in Gmail batch response");
241
-
242
- const respParts = responseText.split(`--${respBoundary}`);
243
- const messages: Array<{ index: number; msg: GmailMessage }> = [];
244
- const failedIds: Array<{ index: number; id: string }> = [];
245
-
246
- for (const rp of respParts) {
247
- const parsed = parseSubResponse(rp);
248
- if (!parsed) continue;
249
-
250
- if (parsed.status >= 200 && parsed.status < 300 && parsed.json) {
251
- try {
252
- messages.push({
288
+ const boundaryMatch = contentType.match(
289
+ /boundary=(?:"([^"]+)"|([^\s;]+))/,
290
+ );
291
+ const respBoundary = boundaryMatch?.[1] ?? boundaryMatch?.[2];
292
+ if (!respBoundary)
293
+ throw new Error("Missing boundary in Gmail batch response");
294
+
295
+ const respParts = responseText.split(`--${respBoundary}`);
296
+ const messages: Array<{ index: number; msg: GmailMessage }> = [];
297
+ const failedIds: Array<{ index: number; id: string }> = [];
298
+
299
+ for (const rp of respParts) {
300
+ const parsed = parseSubResponse(rp);
301
+ if (!parsed) continue;
302
+
303
+ if (parsed.status >= 200 && parsed.status < 300 && parsed.json) {
304
+ try {
305
+ messages.push({
306
+ index: parsed.index,
307
+ msg: JSON.parse(parsed.json) as GmailMessage,
308
+ });
309
+ } catch {
310
+ failedIds.push({
311
+ index: parsed.index,
312
+ id: messageIds[parsed.index],
313
+ });
314
+ }
315
+ } else {
316
+ failedIds.push({
253
317
  index: parsed.index,
254
- msg: JSON.parse(parsed.json) as GmailMessage,
318
+ id: messageIds[parsed.index],
255
319
  });
256
- } catch {
257
- failedIds.push({ index: parsed.index, id: messageIds[parsed.index] });
258
320
  }
259
- } else {
260
- failedIds.push({ index: parsed.index, id: messageIds[parsed.index] });
261
321
  }
322
+
323
+ return { messages, failedIds };
262
324
  }
263
325
 
264
- return { messages, failedIds };
265
- }
326
+ throw new Error(
327
+ "Unreachable: batch retry loop exited without returning or throwing",
328
+ );
329
+ };
266
330
 
267
- throw new Error(
268
- "Unreachable: batch retry loop exited without returning or throwing",
269
- );
331
+ // Use withToken to get raw token for batch endpoint
332
+ return connection.withToken(doBatchFetch);
270
333
  }
271
334
 
272
335
  /**
@@ -275,7 +338,7 @@ async function executeBatchCall(
275
338
  * Falls back to individual getMessage for any sub-requests that fail within a batch.
276
339
  */
277
340
  export async function batchGetMessages(
278
- token: string,
341
+ connection: OAuthConnection,
279
342
  messageIds: string[],
280
343
  format: GmailMessageFormat = "full",
281
344
  metadataHeaders?: string[],
@@ -283,10 +346,16 @@ export async function batchGetMessages(
283
346
  ): Promise<GmailMessage[]> {
284
347
  if (messageIds.length === 0) return [];
285
348
 
286
- // Single message just use getMessage directly
349
+ // Single message -- just use getMessage directly
287
350
  if (messageIds.length === 1) {
288
351
  return [
289
- await getMessage(token, messageIds[0], format, metadataHeaders, fields),
352
+ await getMessage(
353
+ connection,
354
+ messageIds[0],
355
+ format,
356
+ metadataHeaders,
357
+ fields,
358
+ ),
290
359
  ];
291
360
  }
292
361
 
@@ -307,7 +376,13 @@ export async function batchGetMessages(
307
376
  const wave = chunks.slice(i, i + BATCH_CONCURRENCY);
308
377
  const waveResults = await Promise.all(
309
378
  wave.map((chunk) =>
310
- executeBatchCall(token, chunk.ids, format, metadataHeaders, fields),
379
+ executeBatchCall(
380
+ connection,
381
+ chunk.ids,
382
+ format,
383
+ metadataHeaders,
384
+ fields,
385
+ ),
311
386
  ),
312
387
  );
313
388
 
@@ -324,7 +399,7 @@ export async function batchGetMessages(
324
399
  if (failedIds.length > 0) {
325
400
  const retried = await Promise.all(
326
401
  failedIds.map(({ id }) =>
327
- getMessage(token, id, format, metadataHeaders, fields),
402
+ getMessage(connection, id, format, metadataHeaders, fields),
328
403
  ),
329
404
  );
330
405
  for (let r = 0; r < failedIds.length; r++) {
@@ -339,11 +414,11 @@ export async function batchGetMessages(
339
414
 
340
415
  /** Modify labels on a single message. */
341
416
  export async function modifyMessage(
342
- token: string,
417
+ connection: OAuthConnection,
343
418
  messageId: string,
344
419
  modifications: GmailModifyRequest,
345
420
  ): Promise<GmailMessage> {
346
- return request<GmailMessage>(token, `/messages/${messageId}/modify`, {
421
+ return request<GmailMessage>(connection, `/messages/${messageId}/modify`, {
347
422
  method: "POST",
348
423
  body: JSON.stringify(modifications),
349
424
  retryable: true,
@@ -352,11 +427,11 @@ export async function modifyMessage(
352
427
 
353
428
  /** Batch modify labels on multiple messages. */
354
429
  export async function batchModifyMessages(
355
- token: string,
430
+ connection: OAuthConnection,
356
431
  messageIds: string[],
357
432
  modifications: GmailModifyRequest,
358
433
  ): Promise<void> {
359
- await request<void>(token, "/messages/batchModify", {
434
+ await request<void>(connection, "/messages/batchModify", {
360
435
  method: "POST",
361
436
  body: JSON.stringify({ ids: messageIds, ...modifications }),
362
437
  retryable: true,
@@ -365,24 +440,26 @@ export async function batchModifyMessages(
365
440
 
366
441
  /** Move a message to trash. */
367
442
  export async function trashMessage(
368
- token: string,
443
+ connection: OAuthConnection,
369
444
  messageId: string,
370
445
  ): Promise<GmailMessage> {
371
- return request<GmailMessage>(token, `/messages/${messageId}/trash`, {
446
+ return request<GmailMessage>(connection, `/messages/${messageId}/trash`, {
372
447
  method: "POST",
373
448
  retryable: true,
374
449
  });
375
450
  }
376
451
 
377
452
  /** List all labels. */
378
- export async function listLabels(token: string): Promise<GmailLabel[]> {
379
- const resp = await request<GmailLabelsListResponse>(token, "/labels");
453
+ export async function listLabels(
454
+ connection: OAuthConnection,
455
+ ): Promise<GmailLabel[]> {
456
+ const resp = await request<GmailLabelsListResponse>(connection, "/labels");
380
457
  return resp.labels ?? [];
381
458
  }
382
459
 
383
460
  /** Create a draft. */
384
461
  export async function createDraft(
385
- token: string,
462
+ connection: OAuthConnection,
386
463
  to: string,
387
464
  subject: string,
388
465
  body: string,
@@ -409,7 +486,7 @@ export async function createDraft(
409
486
  .replace(/=+$/, "");
410
487
  const message: Record<string, unknown> = { raw };
411
488
  if (threadId) message.threadId = threadId;
412
- return request<GmailDraft>(token, "/drafts", {
489
+ return request<GmailDraft>(connection, "/drafts", {
413
490
  method: "POST",
414
491
  body: JSON.stringify({ message }),
415
492
  });
@@ -417,13 +494,13 @@ export async function createDraft(
417
494
 
418
495
  /** Create a draft from a pre-built base64url MIME payload. */
419
496
  export async function createDraftRaw(
420
- token: string,
497
+ connection: OAuthConnection,
421
498
  raw: string,
422
499
  threadId?: string,
423
500
  ): Promise<GmailDraft> {
424
501
  const message: Record<string, unknown> = { raw };
425
502
  if (threadId) message.threadId = threadId;
426
- return request<GmailDraft>(token, "/drafts", {
503
+ return request<GmailDraft>(connection, "/drafts", {
427
504
  method: "POST",
428
505
  body: JSON.stringify({ message }),
429
506
  });
@@ -431,10 +508,10 @@ export async function createDraftRaw(
431
508
 
432
509
  /** Send an existing draft by ID. */
433
510
  export async function sendDraft(
434
- token: string,
511
+ connection: OAuthConnection,
435
512
  draftId: string,
436
513
  ): Promise<GmailMessage> {
437
- return request<GmailMessage>(token, "/drafts/send", {
514
+ return request<GmailMessage>(connection, "/drafts/send", {
438
515
  method: "POST",
439
516
  body: JSON.stringify({ id: draftId }),
440
517
  });
@@ -442,7 +519,7 @@ export async function sendDraft(
442
519
 
443
520
  /** Send an email. */
444
521
  export async function sendMessage(
445
- token: string,
522
+ connection: OAuthConnection,
446
523
  to: string,
447
524
  subject: string,
448
525
  body: string,
@@ -465,38 +542,40 @@ export async function sendMessage(
465
542
  .replace(/=+$/, "");
466
543
  const payload: Record<string, unknown> = { raw };
467
544
  if (threadId) payload.threadId = threadId;
468
- return request<GmailMessage>(token, "/messages/send", {
545
+ return request<GmailMessage>(connection, "/messages/send", {
469
546
  method: "POST",
470
547
  body: JSON.stringify(payload),
471
548
  });
472
549
  }
473
550
 
474
551
  /** Get the authenticated user's profile (email address). */
475
- export async function getProfile(token: string): Promise<GmailProfile> {
476
- return request<GmailProfile>(token, "/profile");
552
+ export async function getProfile(
553
+ connection: OAuthConnection,
554
+ ): Promise<GmailProfile> {
555
+ return request<GmailProfile>(connection, "/profile");
477
556
  }
478
557
 
479
558
  /** Get attachment data for a message. */
480
559
  export async function getAttachment(
481
- token: string,
560
+ connection: OAuthConnection,
482
561
  messageId: string,
483
562
  attachmentId: string,
484
563
  ): Promise<GmailAttachment> {
485
564
  return request<GmailAttachment>(
486
- token,
565
+ connection,
487
566
  `/messages/${messageId}/attachments/${attachmentId}`,
488
567
  );
489
568
  }
490
569
 
491
570
  /** Send an email with a pre-built raw MIME payload (for multipart/attachments). */
492
571
  export async function sendMessageRaw(
493
- token: string,
572
+ connection: OAuthConnection,
494
573
  raw: string,
495
574
  threadId?: string,
496
575
  ): Promise<GmailMessage> {
497
576
  const payload: Record<string, unknown> = { raw };
498
577
  if (threadId) payload.threadId = threadId;
499
- return request<GmailMessage>(token, "/messages/send", {
578
+ return request<GmailMessage>(connection, "/messages/send", {
500
579
  method: "POST",
501
580
  body: JSON.stringify(payload),
502
581
  });
@@ -504,23 +583,25 @@ export async function sendMessageRaw(
504
583
 
505
584
  /** Create a user label. */
506
585
  export async function createLabel(
507
- token: string,
586
+ connection: OAuthConnection,
508
587
  name: string,
509
588
  opts?: {
510
589
  messageListVisibility?: "show" | "hide";
511
590
  labelListVisibility?: "labelShow" | "labelShowIfUnread" | "labelHide";
512
591
  },
513
592
  ): Promise<GmailLabel> {
514
- return request<GmailLabel>(token, "/labels", {
593
+ return request<GmailLabel>(connection, "/labels", {
515
594
  method: "POST",
516
595
  body: JSON.stringify({ name, ...opts }),
517
596
  });
518
597
  }
519
598
 
520
599
  /** List all Gmail filters. */
521
- export async function listFilters(token: string): Promise<GmailFilter[]> {
600
+ export async function listFilters(
601
+ connection: OAuthConnection,
602
+ ): Promise<GmailFilter[]> {
522
603
  const resp = await request<GmailFiltersListResponse>(
523
- token,
604
+ connection,
524
605
  "/settings/filters",
525
606
  );
526
607
  return resp.filter ?? [];
@@ -528,11 +609,11 @@ export async function listFilters(token: string): Promise<GmailFilter[]> {
528
609
 
529
610
  /** Create a Gmail filter. */
530
611
  export async function createFilter(
531
- token: string,
612
+ connection: OAuthConnection,
532
613
  criteria: GmailFilterCriteria,
533
614
  action: GmailFilterAction,
534
615
  ): Promise<GmailFilter> {
535
- return request<GmailFilter>(token, "/settings/filters", {
616
+ return request<GmailFilter>(connection, "/settings/filters", {
536
617
  method: "POST",
537
618
  body: JSON.stringify({ criteria, action }),
538
619
  });
@@ -540,27 +621,27 @@ export async function createFilter(
540
621
 
541
622
  /** Delete a Gmail filter. */
542
623
  export async function deleteFilter(
543
- token: string,
624
+ connection: OAuthConnection,
544
625
  filterId: string,
545
626
  ): Promise<void> {
546
- await request<void>(token, `/settings/filters/${filterId}`, {
627
+ await request<void>(connection, `/settings/filters/${filterId}`, {
547
628
  method: "DELETE",
548
629
  });
549
630
  }
550
631
 
551
632
  /** Get vacation auto-reply settings. */
552
633
  export async function getVacation(
553
- token: string,
634
+ connection: OAuthConnection,
554
635
  ): Promise<GmailVacationSettings> {
555
- return request<GmailVacationSettings>(token, "/settings/vacation");
636
+ return request<GmailVacationSettings>(connection, "/settings/vacation");
556
637
  }
557
638
 
558
639
  /** Update vacation auto-reply settings. */
559
640
  export async function updateVacation(
560
- token: string,
641
+ connection: OAuthConnection,
561
642
  settings: GmailVacationSettings,
562
643
  ): Promise<GmailVacationSettings> {
563
- return request<GmailVacationSettings>(token, "/settings/vacation", {
644
+ return request<GmailVacationSettings>(connection, "/settings/vacation", {
564
645
  method: "PUT",
565
646
  body: JSON.stringify(settings),
566
647
  });