@oomol-lab/open-connector 1.6.3 → 1.6.5

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 (108) hide show
  1. package/assets/open-connector/catalog/apps/aisa.json +568 -0
  2. package/assets/open-connector/catalog/apps/bilibili.json +2586 -0
  3. package/assets/open-connector/catalog/apps/bitwarden.json +1423 -0
  4. package/assets/open-connector/catalog/apps/cronfree.json +303 -0
  5. package/assets/open-connector/catalog/apps/cufinder.json +368 -0
  6. package/assets/open-connector/catalog/apps/feishu_app_bot.json +2 -2
  7. package/assets/open-connector/catalog/apps/gmail.json +73 -0
  8. package/assets/open-connector/catalog/apps/investoday_mcp.json +22228 -197
  9. package/assets/open-connector/catalog/apps/ixspy.json +2274 -0
  10. package/assets/open-connector/catalog/apps/jin10.json +1 -1
  11. package/assets/open-connector/catalog/apps/osv.json +663 -0
  12. package/assets/open-connector/catalog/apps/pdf_vector.json +329 -0
  13. package/assets/open-connector/catalog/apps/pulsetic.json +406 -0
  14. package/assets/open-connector/catalog/apps/shopee.json +1785 -0
  15. package/assets/open-connector/catalog/apps/slack.json +81 -0
  16. package/assets/open-connector/catalog/apps/slackbot.json +81 -0
  17. package/assets/open-connector/catalog/apps/speak_ai.json +360 -0
  18. package/assets/open-connector/catalog/apps/tongdaxin.json +11 -440
  19. package/assets/open-connector/catalog/apps/zyhub.json +1585 -0
  20. package/assets/open-connector/catalog/apps-index.json +1 -1
  21. package/package.json +1 -1
  22. package/src/core/json-schema.d.ts +5 -1
  23. package/src/core/json-schema.js +14 -2
  24. package/src/core/types.d.ts +9 -0
  25. package/src/oauth/oauth-credential-refresh-service.js +3 -1
  26. package/src/oauth/oauth-flow-service.js +17 -2
  27. package/src/oauth/oauth-token.d.ts +13 -0
  28. package/src/providers/action-contracts.generated.d.ts +16 -4
  29. package/src/providers/aisa/executors.d.ts +4 -0
  30. package/src/providers/aisa/executors.js +176 -0
  31. package/src/providers/bilibili/articles.d.ts +3 -0
  32. package/src/providers/bilibili/articles.js +369 -0
  33. package/src/providers/bilibili/executors.d.ts +8 -0
  34. package/src/providers/bilibili/executors.js +262 -0
  35. package/src/providers/bilibili/runtime.d.ts +95 -0
  36. package/src/providers/bilibili/runtime.js +238 -0
  37. package/src/providers/bilibili/upload.d.ts +14 -0
  38. package/src/providers/bilibili/upload.js +105 -0
  39. package/src/providers/bitwarden/executors.d.ts +3 -0
  40. package/src/providers/bitwarden/executors.js +146 -0
  41. package/src/providers/cronfree/executors.d.ts +4 -0
  42. package/src/providers/cronfree/executors.js +89 -0
  43. package/src/providers/cufinder/executors.d.ts +4 -0
  44. package/src/providers/cufinder/executors.js +105 -0
  45. package/src/providers/dropbox/executors.js +21 -3
  46. package/src/providers/elevenreader/runtime.js +6 -5
  47. package/src/providers/feishu_app_bot/scopes.js +1 -1
  48. package/src/providers/gmail/attachment-stream.d.ts +2 -0
  49. package/src/providers/gmail/attachment-stream.js +162 -0
  50. package/src/providers/gmail/executors.d.ts +3 -1
  51. package/src/providers/gmail/executors.js +53 -25
  52. package/src/providers/investoday_mcp/official-actions.js +19 -372
  53. package/src/providers/investoday_mcp/official-tool-types.d.ts +10 -0
  54. package/src/providers/investoday_mcp/official-tool-types.js +4 -0
  55. package/src/providers/investoday_mcp/official-tools/charts.d.ts +2 -0
  56. package/src/providers/investoday_mcp/official-tools/charts.js +32 -0
  57. package/src/providers/investoday_mcp/official-tools/fixed-income.d.ts +2 -0
  58. package/src/providers/investoday_mcp/official-tools/fixed-income.js +326 -0
  59. package/src/providers/investoday_mcp/official-tools/funds.d.ts +2 -0
  60. package/src/providers/investoday_mcp/official-tools/funds.js +1085 -0
  61. package/src/providers/investoday_mcp/official-tools/hong-kong.d.ts +2 -0
  62. package/src/providers/investoday_mcp/official-tools/hong-kong.js +275 -0
  63. package/src/providers/investoday_mcp/official-tools/indices-industries.d.ts +2 -0
  64. package/src/providers/investoday_mcp/official-tools/indices-industries.js +642 -0
  65. package/src/providers/investoday_mcp/official-tools/macro-calendar.d.ts +2 -0
  66. package/src/providers/investoday_mcp/official-tools/macro-calendar.js +398 -0
  67. package/src/providers/investoday_mcp/official-tools/other.d.ts +2 -0
  68. package/src/providers/investoday_mcp/official-tools/other.js +131 -0
  69. package/src/providers/investoday_mcp/official-tools/research-news.d.ts +2 -0
  70. package/src/providers/investoday_mcp/official-tools/research-news.js +302 -0
  71. package/src/providers/investoday_mcp/official-tools/stocks.d.ts +2 -0
  72. package/src/providers/investoday_mcp/official-tools/stocks.js +2829 -0
  73. package/src/providers/ixspy/executors.d.ts +3 -0
  74. package/src/providers/ixspy/executors.js +220 -0
  75. package/src/providers/mcp-client.d.ts +1 -0
  76. package/src/providers/mcp-client.js +41 -27
  77. package/src/providers/osv/executors.d.ts +2 -0
  78. package/src/providers/osv/executors.js +11 -0
  79. package/src/providers/osv/runtime.d.ts +9 -0
  80. package/src/providers/osv/runtime.js +66 -0
  81. package/src/providers/pdf_vector/executors.d.ts +4 -0
  82. package/src/providers/pdf_vector/executors.js +108 -0
  83. package/src/providers/provider-runtime.d.ts +3 -0
  84. package/src/providers/provider-runtime.js +4 -1
  85. package/src/providers/pulsetic/executors.d.ts +4 -0
  86. package/src/providers/pulsetic/executors.js +126 -0
  87. package/src/providers/registry.generated.js +12 -0
  88. package/src/providers/sellersprite_mcp/executors.js +2 -1
  89. package/src/providers/shopee/actions.d.ts +2 -0
  90. package/src/providers/shopee/actions.js +474 -0
  91. package/src/providers/shopee/executors.d.ts +3 -0
  92. package/src/providers/shopee/executors.js +85 -0
  93. package/src/providers/shopee/runtime.d.ts +79 -0
  94. package/src/providers/shopee/runtime.js +693 -0
  95. package/src/providers/slack/actions.js +17 -0
  96. package/src/providers/slack/runtime.js +62 -4
  97. package/src/providers/speak_ai/executors.d.ts +3 -0
  98. package/src/providers/speak_ai/executors.js +187 -0
  99. package/src/providers/tongdaxin/executors.js +5 -4
  100. package/src/providers/tongdaxin/named-action-output.js +3 -9
  101. package/src/providers/tongdaxin/named-action-routing.js +1 -1
  102. package/src/providers/tongdaxin/named-actions.js +1 -6
  103. package/src/providers/tushare/runtime.js +22 -9
  104. package/src/providers/twenty_crm/runtime.js +4 -2
  105. package/src/providers/zyhub/executors.d.ts +3 -0
  106. package/src/providers/zyhub/executors.js +240 -0
  107. package/src/server/files/transit-files.d.ts +3 -2
  108. package/src/server/files/transit-files.js +31 -35
@@ -0,0 +1,89 @@
1
+ import { looseArray, optionalString } from "../../core/cast.js";
2
+ import { defineApiKeyProviderExecutors, defineProviderProxy, providerInputError, ProviderRequestError, providerUserAgent, readProviderJsonBody, requiredInputString, runProviderRequest, } from "../provider-runtime.js";
3
+ const service = "cronfree";
4
+ const cronfreeApiBaseUrl = "https://login.cronfree.com/zapier";
5
+ const handlers = {
6
+ async create_schedule(input, context) {
7
+ const result = await requestCronfree({
8
+ path: "/schedule",
9
+ body: {
10
+ hookUrl: requiredInputString(input.hookUrl, "hookUrl"),
11
+ wdays: readStringArray(input.weekdays),
12
+ months: readStringArray(input.months),
13
+ mdays: readStringArray(input.monthDays),
14
+ hours: readStringArray(input.hours),
15
+ minutes: readStringArray(input.minutes),
16
+ timezone: [requiredInputString(input.timezone, "timezone")],
17
+ },
18
+ context,
19
+ phase: "execute",
20
+ });
21
+ return { result };
22
+ },
23
+ async delete_schedule(input, context) {
24
+ const result = await requestCronfree({
25
+ path: "/unschedule",
26
+ body: { hookUrl: requiredInputString(input.hookUrl, "hookUrl") },
27
+ context,
28
+ phase: "execute",
29
+ });
30
+ return { result };
31
+ },
32
+ };
33
+ export const executors = defineApiKeyProviderExecutors(service, handlers, {
34
+ skipDnsValidation: true,
35
+ });
36
+ export const proxy = defineProviderProxy({
37
+ service,
38
+ baseUrl: cronfreeApiBaseUrl,
39
+ auth: { type: "api_key_json_body", name: "license_key" },
40
+ skipDnsValidation: true,
41
+ customizeRequest({ headers }) {
42
+ headers.set("accept", "application/json");
43
+ headers.set("content-type", "application/json");
44
+ },
45
+ });
46
+ export const credentialValidators = {
47
+ async apiKey(input, { fetcher, signal }) {
48
+ await requestCronfree({
49
+ path: "/unschedule",
50
+ body: { hookUrl: `https://connector.oomol.com/cronfree-validation/${crypto.randomUUID()}` },
51
+ context: { apiKey: input.apiKey, fetcher, signal },
52
+ phase: "validate",
53
+ });
54
+ return {
55
+ profile: { accountId: "cronfree-license-key", displayName: "Cronfree License Key" },
56
+ grantedScopes: [],
57
+ metadata: { apiBaseUrl: cronfreeApiBaseUrl, validationEndpoint: "/unschedule" },
58
+ };
59
+ },
60
+ };
61
+ function readStringArray(value) {
62
+ return looseArray(value)
63
+ .map((item) => optionalString(item))
64
+ .filter((item) => item != null);
65
+ }
66
+ async function requestCronfree(input) {
67
+ return runProviderRequest({ signal: input.context.signal, label: "Cronfree" }, async (signal) => {
68
+ const response = await input.context.fetcher(`${cronfreeApiBaseUrl}${input.path}`, {
69
+ method: "POST",
70
+ headers: {
71
+ accept: "application/json",
72
+ "content-type": "application/json",
73
+ "user-agent": providerUserAgent,
74
+ },
75
+ body: JSON.stringify({ ...input.body, license_key: input.context.apiKey }),
76
+ signal,
77
+ });
78
+ if (!response.ok) {
79
+ if (response.status === 401 && input.phase === "validate") {
80
+ throw providerInputError("Cronfree rejected the license key");
81
+ }
82
+ throw new ProviderRequestError(response.status, `Cronfree request failed with HTTP ${response.status}`);
83
+ }
84
+ return readProviderJsonBody(response, {
85
+ emptyBody: null,
86
+ invalidJsonMessage: "Cronfree returned invalid JSON",
87
+ });
88
+ });
89
+ }
@@ -0,0 +1,4 @@
1
+ import type { CredentialValidators, ProviderExecutors, ProviderProxyExecutor } from "../../core/types.ts";
2
+ export declare const executors: ProviderExecutors;
3
+ export declare const proxy: ProviderProxyExecutor;
4
+ export declare const credentialValidators: CredentialValidators;
@@ -0,0 +1,105 @@
1
+ import { compactObject, optionalRecord, optionalString } from "../../core/cast.js";
2
+ import { defineApiKeyProviderExecutors, defineProviderProxy, providerInputError, ProviderRequestError, providerResponseError, providerUserAgent, readProviderJsonBody, requiredResponseRecord, runProviderRequest, } from "../provider-runtime.js";
3
+ const service = "cufinder";
4
+ const baseUrl = "https://api.cufinder.io/v3";
5
+ const handlers = {
6
+ find_company_domain(input, context) {
7
+ return execute("/companies/name-to-domain", { name: trim(input.name), country_code: upper(input.countryCode), address: trim(input.address) }, context, (data, meta) => ({ domain: requireString(data.domain, "domain"), meta }));
8
+ },
9
+ find_company_name(input, context) {
10
+ return execute("/companies/domain-to-name", { domain: trim(input.domain) }, context, (data, meta) => ({
11
+ name: requireString(data.name, "name"),
12
+ meta,
13
+ }));
14
+ },
15
+ enrich_company(input, context) {
16
+ return execute("/companies/enrich", { query: trim(input.query) }, context, (data, meta) => ({
17
+ company: requiredResponseRecord(data.company, "cufinder company"),
18
+ meta,
19
+ }));
20
+ },
21
+ enrich_person(input, context) {
22
+ return execute("/people/enrich-by-name", { full_name: trim(input.fullName), company: trim(input.company) }, context, (data, meta) => ({ person: requiredResponseRecord(data.person, "cufinder person"), meta }));
23
+ },
24
+ enrich_person_by_email(input, context) {
25
+ return execute("/people/enrich-by-email", { email: trim(input.email) }, context, (data, meta) => ({
26
+ person: requiredResponseRecord(data.person, "cufinder person"),
27
+ meta,
28
+ }));
29
+ },
30
+ };
31
+ export const executors = defineApiKeyProviderExecutors(service, handlers, {
32
+ skipDnsValidation: true,
33
+ });
34
+ export const proxy = defineProviderProxy({
35
+ service,
36
+ baseUrl,
37
+ auth: { type: "api_key_header", name: "x-api-key" },
38
+ skipDnsValidation: true,
39
+ });
40
+ export const credentialValidators = {
41
+ async apiKey(input, { fetcher, signal }) {
42
+ await request("/normalize/url", { value: "https://cufinder.io" }, { apiKey: input.apiKey, fetcher, signal }, "validate");
43
+ return {
44
+ profile: { accountId: "cufinder-api-key", displayName: "CUFinder API Key" },
45
+ grantedScopes: [],
46
+ metadata: { apiBaseUrl: baseUrl, validationEndpoint: "/normalize/url" },
47
+ };
48
+ },
49
+ };
50
+ async function execute(path, body, context, normalize) {
51
+ const payload = await request(path, body, context, "execute");
52
+ return normalize(requiredResponseRecord(payload.data, "cufinder data"), requiredResponseRecord(payload.meta, "cufinder metadata"));
53
+ }
54
+ async function request(path, body, context, phase) {
55
+ return runProviderRequest({ signal: context.signal, label: "cufinder" }, async (signal) => {
56
+ const response = await context.fetcher(`${baseUrl}${path}`, {
57
+ method: "POST",
58
+ headers: {
59
+ accept: "application/json",
60
+ "content-type": "application/json",
61
+ "user-agent": providerUserAgent,
62
+ "x-api-key": context.apiKey,
63
+ },
64
+ body: JSON.stringify(compactObject(body)),
65
+ signal,
66
+ });
67
+ const decoded = await readProviderJsonBody(response, {
68
+ emptyBody: {},
69
+ invalidJsonMessage: "cufinder returned malformed JSON",
70
+ invalidJsonFallback: response.ok ? undefined : () => ({}),
71
+ });
72
+ const payload = optionalRecord(decoded) ?? {};
73
+ if (!response.ok)
74
+ throw makeError(response.status, payload, phase);
75
+ if (payload.success !== true)
76
+ throw makeError(response.status, payload, phase);
77
+ return payload;
78
+ });
79
+ }
80
+ function makeError(status, payload, phase) {
81
+ const error = optionalRecord(payload.error);
82
+ const code = optionalString(error?.code);
83
+ const message = optionalString(error?.message) ?? `cufinder request failed with status ${status}`;
84
+ if (code === "unauthorized")
85
+ return phase === "validate" ? providerInputError(message) : new ProviderRequestError(409, message, payload);
86
+ if (code === "rate_limited")
87
+ return new ProviderRequestError(429, message, payload);
88
+ if (["invalid_request", "validation_failed", "not_found"].includes(code ?? ""))
89
+ return providerInputError(message);
90
+ if (code === "request_timeout")
91
+ return new ProviderRequestError(504, message, payload);
92
+ return new ProviderRequestError(status >= 400 ? status : 502, message, payload);
93
+ }
94
+ function trim(value) {
95
+ return typeof value === "string" ? value.trim() : value;
96
+ }
97
+ function upper(value) {
98
+ return typeof value === "string" ? value.trim().toUpperCase() : value;
99
+ }
100
+ function requireString(value, field) {
101
+ const result = optionalString(value);
102
+ if (!result)
103
+ throw providerResponseError(`cufinder data.${field} must be a string`);
104
+ return result;
105
+ }
@@ -209,6 +209,24 @@ async function getMetadata(input, accessToken, fetcher) {
209
209
  metadata: mapDropboxMetadata(payload),
210
210
  };
211
211
  }
212
+ /**
213
+ * Serialize a `Dropbox-API-Arg` header value.
214
+ *
215
+ * Dropbox requires this header to be ASCII: its own docs say `0x7F` and every
216
+ * non-ASCII character must be escaped as `\uXXXX`, which is also the range the
217
+ * official JS SDK escapes. `JSON.stringify` does not escape, and Node's `Headers`
218
+ * refuses a value outside ByteString, so a path containing any non-ASCII
219
+ * character threw `TypeError: Cannot convert argument to a ByteString` inside
220
+ * the runtime BEFORE the request was made. `provider-runtime.ts`'s fallback
221
+ * then reported it as a bare `internal_error`, discarding the cause.
222
+ *
223
+ * Measured 2026-09-18 against a real account: `/kkndme 天涯.pdf` failed in 1ms
224
+ * with no network call, while ASCII-path siblings downloaded in ~400ms. Dropbox
225
+ * itself accepts either spelling on the wire; only the header build was broken.
226
+ */
227
+ function dropboxApiArg(arg) {
228
+ return JSON.stringify(arg).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
229
+ }
212
230
  async function downloadFile(input, context) {
213
231
  const { accessToken, fetcher, transitFiles } = context;
214
232
  if (!transitFiles) {
@@ -218,7 +236,7 @@ async function downloadFile(input, context) {
218
236
  method: "POST",
219
237
  headers: {
220
238
  ...dropboxAuthHeaders(accessToken),
221
- "Dropbox-API-Arg": JSON.stringify({
239
+ "Dropbox-API-Arg": dropboxApiArg({
222
240
  path: requireString(input.path, "dropbox download path"),
223
241
  }),
224
242
  },
@@ -251,7 +269,7 @@ async function uploadFile(input, accessToken, fetcher) {
251
269
  headers: {
252
270
  ...dropboxAuthHeaders(accessToken),
253
271
  "Content-Type": source.mimeType || "application/octet-stream",
254
- "Dropbox-API-Arg": JSON.stringify(arg),
272
+ "Dropbox-API-Arg": dropboxApiArg(arg),
255
273
  },
256
274
  body: Buffer.from(source.bytes),
257
275
  });
@@ -491,7 +509,7 @@ async function getSharedLinkFile(input, context) {
491
509
  method: "POST",
492
510
  headers: {
493
511
  ...dropboxAuthHeaders(accessToken),
494
- "Dropbox-API-Arg": JSON.stringify(arg),
512
+ "Dropbox-API-Arg": dropboxApiArg(arg),
495
513
  },
496
514
  signal: context.signal,
497
515
  });
@@ -1,7 +1,7 @@
1
1
  import { booleanString, compactObject, optionalBoolean, optionalInteger, optionalRecord, optionalString, requiredString, stringRecord, } from "../../core/cast.js";
2
2
  import { readBoundedResponseBytes } from "../../core/request.js";
3
3
  import { providerInputError, providerUserAgent, ProviderRequestError } from "../provider-runtime.js";
4
- const elevenreaderApiBaseUrl = "https://api.elevenlabs.io/v1";
4
+ const elevenreaderApiOrigin = "https://api.elevenlabs.io";
5
5
  export const elevenreaderActionHandlers = {
6
6
  get_user_info(_input, context) {
7
7
  return getElevenreaderUserInfo(context);
@@ -74,7 +74,8 @@ async function getElevenreaderModels(context) {
74
74
  }
75
75
  async function searchElevenreaderVoices(input, context) {
76
76
  const payload = await requestElevenreaderJson({
77
- path: "/voices/search",
77
+ path: "/voices",
78
+ version: "v2",
78
79
  query: compactObject({
79
80
  search: optionalString(input.search),
80
81
  category: optionalString(input.category),
@@ -163,7 +164,7 @@ async function readElevenreaderText(input, context) {
163
164
  }
164
165
  async function requestElevenreaderJson(input) {
165
166
  const hasBody = input.body !== undefined;
166
- const response = await input.fetcher(buildElevenreaderUrl(input.path, input.query), {
167
+ const response = await input.fetcher(buildElevenreaderUrl(input.path, input.query, input.version), {
167
168
  method: hasBody ? "POST" : "GET",
168
169
  headers: hasBody ? elevenreaderJsonHeaders(input.apiKey) : elevenreaderHeaders(input.apiKey),
169
170
  body: hasBody ? JSON.stringify(input.body) : undefined,
@@ -174,8 +175,8 @@ async function requestElevenreaderJson(input) {
174
175
  }
175
176
  return readElevenreaderJson(response);
176
177
  }
177
- function buildElevenreaderUrl(path, query) {
178
- const url = new URL(`${elevenreaderApiBaseUrl}${path}`);
178
+ function buildElevenreaderUrl(path, query, version = "v1") {
179
+ const url = new URL(`${elevenreaderApiOrigin}/${version}${path}`);
179
180
  for (const [key, value] of Object.entries(query ?? {})) {
180
181
  if (value !== undefined) {
181
182
  url.searchParams.set(key, value);
@@ -15,6 +15,6 @@ export const feishuAppBotScopes = {
15
15
  groupAtReadonly: "im:message.group_at_msg:readonly",
16
16
  groupAtWithBotReadonly: "im:message.group_at_msg.include_bot:readonly",
17
17
  groupReadonly: "im:message.group_msg:readonly",
18
- applicationRead: "application:application:readonly",
18
+ applicationRead: "application:application:self_manage",
19
19
  applicationVersionRead: "application:application.app_version:readonly",
20
20
  };
@@ -0,0 +1,2 @@
1
+ /** Decode MessagePartBody.data while retaining only a small JSON envelope and one base64 block. */
2
+ export declare function decodeGmailAttachment(body: ReadableStream<Uint8Array>, maxBytes: number): ReadableStream<Uint8Array>;
@@ -0,0 +1,162 @@
1
+ import { providerResponseError, ProviderRequestError } from "../provider-runtime.js";
2
+ const chunkSize = 16 * 1024;
3
+ const maxEnvelopeCharacters = 16 * 1024;
4
+ /** Decode MessagePartBody.data while retaining only a small JSON envelope and one base64 block. */
5
+ export function decodeGmailAttachment(body, maxBytes) {
6
+ const decoder = new TextDecoder("utf-8", { fatal: true });
7
+ const parser = new AttachmentParser(maxBytes);
8
+ return body.pipeThrough(new TransformStream({
9
+ transform(chunk, controller) {
10
+ for (let offset = 0; offset < chunk.length; offset += chunkSize) {
11
+ parser.push(decoder.decode(chunk.subarray(offset, offset + chunkSize), { stream: true }), controller);
12
+ }
13
+ },
14
+ flush(controller) {
15
+ parser.push(decoder.decode(), controller);
16
+ parser.finish();
17
+ },
18
+ }));
19
+ }
20
+ /** Strip only the top-level data string; JSON.parse validates the remaining bounded envelope at EOF. */
21
+ class AttachmentParser {
22
+ maxBytes;
23
+ envelope = "";
24
+ depth = 0;
25
+ previous = "";
26
+ stringKind;
27
+ escaped = false;
28
+ keyToken = "";
29
+ key = "";
30
+ keys = new Set();
31
+ sawData = false;
32
+ dataEscape = "";
33
+ base64 = "";
34
+ padding = false;
35
+ sizeBytes = 0;
36
+ constructor(maxBytes) {
37
+ this.maxBytes = maxBytes;
38
+ }
39
+ push(text, controller) {
40
+ for (const character of text) {
41
+ if (this.stringKind === "data") {
42
+ this.pushData(character, controller);
43
+ continue;
44
+ }
45
+ this.envelope += character;
46
+ if (this.envelope.length > maxEnvelopeCharacters) {
47
+ throw providerResponseError("Gmail attachment JSON envelope is too large");
48
+ }
49
+ if (this.stringKind) {
50
+ if (this.stringKind === "key")
51
+ this.keyToken += character;
52
+ if (this.escaped) {
53
+ this.escaped = false;
54
+ }
55
+ else if (character === "\\") {
56
+ this.escaped = true;
57
+ }
58
+ else if (character === '"') {
59
+ if (this.stringKind === "key") {
60
+ this.key = JSON.parse(this.keyToken);
61
+ if (this.keys.has(this.key))
62
+ throw providerResponseError("Duplicate Gmail attachment field");
63
+ this.keys.add(this.key);
64
+ }
65
+ this.stringKind = undefined;
66
+ this.previous = '"';
67
+ }
68
+ continue;
69
+ }
70
+ if (character === '"') {
71
+ if (this.depth === 1 && (this.previous === "{" || this.previous === ",")) {
72
+ this.stringKind = "key";
73
+ this.keyToken = '"';
74
+ }
75
+ else if (this.depth === 1 && this.previous === ":" && this.key === "data") {
76
+ this.stringKind = "data";
77
+ this.sawData = true;
78
+ }
79
+ else {
80
+ this.stringKind = "other";
81
+ }
82
+ }
83
+ else if (character === "{" || character === "[") {
84
+ this.depth++;
85
+ }
86
+ else if (character === "}" || character === "]") {
87
+ this.depth--;
88
+ }
89
+ if (!/\s/.test(character))
90
+ this.previous = character;
91
+ }
92
+ }
93
+ finish() {
94
+ let envelope;
95
+ try {
96
+ envelope = JSON.parse(this.envelope);
97
+ }
98
+ catch {
99
+ throw providerResponseError("Invalid Gmail attachment JSON");
100
+ }
101
+ if (!this.sawData ||
102
+ this.stringKind ||
103
+ envelope?.data !== "" ||
104
+ !Number.isSafeInteger(envelope.size) ||
105
+ envelope.size !== this.sizeBytes) {
106
+ throw providerResponseError("Gmail attachment is incomplete or its size does not match");
107
+ }
108
+ }
109
+ pushData(character, controller) {
110
+ if (this.dataEscape) {
111
+ this.dataEscape += character;
112
+ if (this.dataEscape === "\\u" || (this.dataEscape.startsWith("\\u") && this.dataEscape.length < 6))
113
+ return;
114
+ try {
115
+ character = JSON.parse(`"${this.dataEscape}"`);
116
+ }
117
+ catch {
118
+ throw providerResponseError("Invalid Gmail attachment JSON escape");
119
+ }
120
+ this.dataEscape = "";
121
+ }
122
+ else if (character === "\\") {
123
+ this.dataEscape = character;
124
+ return;
125
+ }
126
+ else if (character === '"') {
127
+ // Buffer's decoder is permissive. Round-tripping the tail rejects bad padding and unused bits.
128
+ const tail = Buffer.from(this.base64, "base64url");
129
+ if (!/^[A-Za-z0-9_-]*={0,2}$/.test(this.base64) ||
130
+ (this.padding && this.base64.length % 4 !== 0) ||
131
+ tail.toString("base64url") !== this.base64.replace(/=+$/, "")) {
132
+ throw providerResponseError("Invalid Gmail attachment base64url data");
133
+ }
134
+ this.emit(tail, controller);
135
+ this.base64 = "";
136
+ this.stringKind = undefined;
137
+ this.previous = '"';
138
+ this.envelope += '"';
139
+ return;
140
+ }
141
+ if (!/^[A-Za-z0-9_=-]$/.test(character) || (this.padding && character !== "=")) {
142
+ throw providerResponseError("Invalid Gmail attachment base64url data");
143
+ }
144
+ this.padding ||= character === "=";
145
+ this.base64 += character;
146
+ if (this.padding && this.base64.length > chunkSize + 2) {
147
+ throw providerResponseError("Invalid Gmail attachment base64url padding");
148
+ }
149
+ if (!this.padding && this.base64.length === chunkSize) {
150
+ this.emit(Buffer.from(this.base64, "base64url"), controller);
151
+ this.base64 = "";
152
+ }
153
+ }
154
+ emit(bytes, controller) {
155
+ this.sizeBytes += bytes.length;
156
+ if (this.sizeBytes > this.maxBytes) {
157
+ throw new ProviderRequestError(413, `Gmail attachment exceeds transit file limit of ${this.maxBytes} bytes`);
158
+ }
159
+ if (bytes.length)
160
+ controller.enqueue(bytes);
161
+ }
162
+ }
@@ -1,10 +1,12 @@
1
- import type { CredentialValidators, ProviderExecutors, ProviderProxyExecutor } from "../../core/types.ts";
1
+ import type { CredentialValidators, ExecutionContext, ProviderExecutors, ProviderProxyExecutor } from "../../core/types.ts";
2
2
  import type { ProviderActionHandlers } from "../provider-runtime.ts";
3
3
  declare const service = "gmail";
4
4
  interface ActionContext {
5
5
  userId: string;
6
6
  accessToken: string;
7
7
  fetcher: typeof fetch;
8
+ transitFiles?: ExecutionContext["transitFiles"];
9
+ signal?: AbortSignal;
8
10
  }
9
11
  type ActionHandler = (input: Record<string, unknown>, context: ActionContext) => Promise<unknown>;
10
12
  export declare const gmailActionHandlers: ProviderActionHandlers<typeof service, ActionHandler>;
@@ -1,12 +1,40 @@
1
+ import { looseArray, optionalRecord, optionalString } from "../../core/cast.js";
2
+ import { encodePathSegment } from "../../core/request.js";
1
3
  import { googleBearerProxyAuth, googleServiceAccountValidator, resolveGoogleAccessToken } from "../google-auth.js";
2
- import { defineProviderExecutors, defineProviderProxy, ProviderRequestError, readProviderJsonBody, } from "../provider-runtime.js";
4
+ import { defineProviderExecutors, defineProviderProxy, ProviderRequestError, readProviderErrorTextBody, readProviderJsonBody, requiredInputString, runProviderRequest, } from "../provider-runtime.js";
5
+ import { decodeGmailAttachment } from "./attachment-stream.js";
3
6
  import { buildRecipients, encodeMimeMessage, extractBodyContent, firstAddress, normalizeGmailMessage, normalizeMessageId, normalizeThreadId, parseAddressList, readHeader, resolveReplyHeaders, summarizeGmailMessage, } from "./message.js";
4
7
  import { gmailOAuthScopes } from "./scopes.js";
5
8
  const service = "gmail";
6
9
  const gmailApiBaseUrl = "https://gmail.googleapis.com/gmail/v1";
7
10
  const detailHydrationBatchSize = 10;
11
+ // Attachments may reach Gmail's 25 MB cap, and the base64 JSON envelope is a third larger again;
12
+ // the default 30 s request budget covers fetch, decode, and disk write, so give this transfer longer.
13
+ const attachmentDownloadTimeoutMs = 120_000;
8
14
  const defaultFetchEmailsMaxResults = 20;
9
15
  export const gmailActionHandlers = {
16
+ async download_attachment(input, context) {
17
+ const { transitFiles, fetcher, accessToken } = context;
18
+ if (!transitFiles?.createFromStream) {
19
+ throw new ProviderRequestError(400, "Gmail attachment downloads require a streaming transit file backend (filesystem).");
20
+ }
21
+ const messageId = requiredInputString(input.messageId, "messageId");
22
+ const attachmentId = requiredInputString(input.attachmentId, "attachmentId");
23
+ const userId = optionalString(input.userId) ?? context.userId;
24
+ const url = `${gmailUserUrl(userId, "messages")}/${encodePathSegment(messageId)}/attachments/${encodePathSegment(attachmentId)}?fields=data,size`;
25
+ return runProviderRequest({ signal: context.signal, label: "Gmail attachment", timeoutMs: attachmentDownloadTimeoutMs }, async (signal) => {
26
+ const response = await fetcher(url, { headers: { authorization: `Bearer ${accessToken}` }, signal });
27
+ await assertGmailResponse(response);
28
+ if (!response.body)
29
+ throw new ProviderRequestError(502, "Gmail attachment response has no body");
30
+ return transitFiles.createFromStream({
31
+ body: decodeGmailAttachment(response.body, transitFiles.maxBytes),
32
+ name: optionalString(input.fileName) ?? "attachment",
33
+ mimeType: optionalString(input.mimeType) ?? "application/octet-stream",
34
+ signal,
35
+ });
36
+ });
37
+ },
10
38
  async search_threads(input, { userId, accessToken, fetcher }) {
11
39
  const output = await listThreads(input, userId, accessToken, fetcher);
12
40
  return {
@@ -175,13 +203,20 @@ export const executors = defineProviderExecutors({
175
203
  fetcher,
176
204
  signal: context.signal,
177
205
  });
178
- return { userId: "me", accessToken: resolved.accessToken, fetcher };
206
+ return {
207
+ userId: "me",
208
+ accessToken: resolved.accessToken,
209
+ fetcher,
210
+ transitFiles: context.transitFiles,
211
+ signal: context.signal,
212
+ };
179
213
  },
180
214
  });
181
215
  export const proxy = defineProviderProxy({
182
216
  service,
183
217
  baseUrl: gmailApiBaseUrl,
184
218
  auth: googleBearerProxyAuth(gmailOAuthScopes),
219
+ readError: readGmailError,
185
220
  });
186
221
  export const credentialValidators = {
187
222
  async oauth2(input, { fetcher }) {
@@ -733,31 +768,24 @@ async function assertGmailResponse(response) {
733
768
  if (response.ok) {
734
769
  return;
735
770
  }
736
- const text = await response.text().catch(() => "");
737
- const message = readGmailErrorMessage(text) || `gmail request failed with ${response.status}`;
738
- if (response.status === 400) {
739
- throw new ProviderRequestError(400, message);
740
- }
741
- if (response.status === 401 || response.status === 403) {
742
- throw new ProviderRequestError(response.status, message);
743
- }
744
- if (response.status === 429) {
745
- throw new ProviderRequestError(429, message);
746
- }
747
- throw new ProviderRequestError(response.status, message);
748
- }
749
- function readGmailErrorMessage(text) {
750
- if (!text) {
751
- return "";
752
- }
771
+ throw await readGmailError(response);
772
+ }
773
+ const gmailQuotaReasons = new Set([
774
+ "rateLimitExceeded",
775
+ "userRateLimitExceeded",
776
+ "dailyLimitExceeded",
777
+ "quotaExceeded",
778
+ ]);
779
+ async function readGmailError(response) {
780
+ const text = await readProviderErrorTextBody(response, "gmail error response");
781
+ let error;
753
782
  try {
754
- const payload = JSON.parse(text);
755
- if (typeof payload.error === "string") {
756
- return payload.error;
757
- }
758
- return payload.error?.message ?? text;
783
+ error = optionalRecord(optionalRecord(JSON.parse(text))?.error);
759
784
  }
760
785
  catch {
761
- return text;
786
+ // A malformed response must not expose its raw body or change status classification.
762
787
  }
788
+ const rateLimited = response.status === 403 &&
789
+ looseArray(error?.errors).some((entry) => gmailQuotaReasons.has(optionalString(optionalRecord(entry)?.reason) ?? ""));
790
+ return new ProviderRequestError(response.status, optionalString(error?.message) ?? `gmail request failed with ${response.status}`, undefined, rateLimited ? "rate_limited" : undefined);
763
791
  }