@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.
- package/ARCHITECTURE.md +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- 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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 (
|
|
126
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
77
127
|
if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
|
|
78
|
-
const retryAfter =
|
|
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
|
|
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
|
-
|
|
89
|
-
`Gmail API ${resp.status}: ${
|
|
142
|
+
"",
|
|
143
|
+
`Gmail API ${resp.status}: ${bodyStr}`,
|
|
90
144
|
);
|
|
91
145
|
}
|
|
92
146
|
|
|
93
|
-
//
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>(
|
|
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
|
-
|
|
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>(
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
235
|
-
|
|
285
|
+
const contentType = resp.headers.get("content-type") ?? "";
|
|
286
|
+
const responseText = await resp.text();
|
|
236
287
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
326
|
+
throw new Error(
|
|
327
|
+
"Unreachable: batch retry loop exited without returning or throwing",
|
|
328
|
+
);
|
|
329
|
+
};
|
|
266
330
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
|
349
|
+
// Single message -- just use getMessage directly
|
|
287
350
|
if (messageIds.length === 1) {
|
|
288
351
|
return [
|
|
289
|
-
await getMessage(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
417
|
+
connection: OAuthConnection,
|
|
343
418
|
messageId: string,
|
|
344
419
|
modifications: GmailModifyRequest,
|
|
345
420
|
): Promise<GmailMessage> {
|
|
346
|
-
return request<GmailMessage>(
|
|
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
|
-
|
|
430
|
+
connection: OAuthConnection,
|
|
356
431
|
messageIds: string[],
|
|
357
432
|
modifications: GmailModifyRequest,
|
|
358
433
|
): Promise<void> {
|
|
359
|
-
await request<void>(
|
|
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
|
-
|
|
443
|
+
connection: OAuthConnection,
|
|
369
444
|
messageId: string,
|
|
370
445
|
): Promise<GmailMessage> {
|
|
371
|
-
return request<GmailMessage>(
|
|
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(
|
|
379
|
-
|
|
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
|
-
|
|
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>(
|
|
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
|
-
|
|
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>(
|
|
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
|
-
|
|
511
|
+
connection: OAuthConnection,
|
|
435
512
|
draftId: string,
|
|
436
513
|
): Promise<GmailMessage> {
|
|
437
|
-
return request<GmailMessage>(
|
|
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
|
-
|
|
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>(
|
|
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(
|
|
476
|
-
|
|
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
|
-
|
|
560
|
+
connection: OAuthConnection,
|
|
482
561
|
messageId: string,
|
|
483
562
|
attachmentId: string,
|
|
484
563
|
): Promise<GmailAttachment> {
|
|
485
564
|
return request<GmailAttachment>(
|
|
486
|
-
|
|
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
|
-
|
|
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>(
|
|
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
|
-
|
|
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>(
|
|
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(
|
|
600
|
+
export async function listFilters(
|
|
601
|
+
connection: OAuthConnection,
|
|
602
|
+
): Promise<GmailFilter[]> {
|
|
522
603
|
const resp = await request<GmailFiltersListResponse>(
|
|
523
|
-
|
|
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
|
-
|
|
612
|
+
connection: OAuthConnection,
|
|
532
613
|
criteria: GmailFilterCriteria,
|
|
533
614
|
action: GmailFilterAction,
|
|
534
615
|
): Promise<GmailFilter> {
|
|
535
|
-
return request<GmailFilter>(
|
|
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
|
-
|
|
624
|
+
connection: OAuthConnection,
|
|
544
625
|
filterId: string,
|
|
545
626
|
): Promise<void> {
|
|
546
|
-
await request<void>(
|
|
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
|
-
|
|
634
|
+
connection: OAuthConnection,
|
|
554
635
|
): Promise<GmailVacationSettings> {
|
|
555
|
-
return request<GmailVacationSettings>(
|
|
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
|
-
|
|
641
|
+
connection: OAuthConnection,
|
|
561
642
|
settings: GmailVacationSettings,
|
|
562
643
|
): Promise<GmailVacationSettings> {
|
|
563
|
-
return request<GmailVacationSettings>(
|
|
644
|
+
return request<GmailVacationSettings>(connection, "/settings/vacation", {
|
|
564
645
|
method: "PUT",
|
|
565
646
|
body: JSON.stringify(settings),
|
|
566
647
|
});
|