@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.
- package/assets/open-connector/catalog/apps/aisa.json +568 -0
- package/assets/open-connector/catalog/apps/bilibili.json +2586 -0
- package/assets/open-connector/catalog/apps/bitwarden.json +1423 -0
- package/assets/open-connector/catalog/apps/cronfree.json +303 -0
- package/assets/open-connector/catalog/apps/cufinder.json +368 -0
- package/assets/open-connector/catalog/apps/feishu_app_bot.json +2 -2
- package/assets/open-connector/catalog/apps/gmail.json +73 -0
- package/assets/open-connector/catalog/apps/investoday_mcp.json +22228 -197
- package/assets/open-connector/catalog/apps/ixspy.json +2274 -0
- package/assets/open-connector/catalog/apps/jin10.json +1 -1
- package/assets/open-connector/catalog/apps/osv.json +663 -0
- package/assets/open-connector/catalog/apps/pdf_vector.json +329 -0
- package/assets/open-connector/catalog/apps/pulsetic.json +406 -0
- package/assets/open-connector/catalog/apps/shopee.json +1785 -0
- package/assets/open-connector/catalog/apps/slack.json +81 -0
- package/assets/open-connector/catalog/apps/slackbot.json +81 -0
- package/assets/open-connector/catalog/apps/speak_ai.json +360 -0
- package/assets/open-connector/catalog/apps/tongdaxin.json +11 -440
- package/assets/open-connector/catalog/apps/zyhub.json +1585 -0
- package/assets/open-connector/catalog/apps-index.json +1 -1
- package/package.json +1 -1
- package/src/core/json-schema.d.ts +5 -1
- package/src/core/json-schema.js +14 -2
- package/src/core/types.d.ts +9 -0
- package/src/oauth/oauth-credential-refresh-service.js +3 -1
- package/src/oauth/oauth-flow-service.js +17 -2
- package/src/oauth/oauth-token.d.ts +13 -0
- package/src/providers/action-contracts.generated.d.ts +16 -4
- package/src/providers/aisa/executors.d.ts +4 -0
- package/src/providers/aisa/executors.js +176 -0
- package/src/providers/bilibili/articles.d.ts +3 -0
- package/src/providers/bilibili/articles.js +369 -0
- package/src/providers/bilibili/executors.d.ts +8 -0
- package/src/providers/bilibili/executors.js +262 -0
- package/src/providers/bilibili/runtime.d.ts +95 -0
- package/src/providers/bilibili/runtime.js +238 -0
- package/src/providers/bilibili/upload.d.ts +14 -0
- package/src/providers/bilibili/upload.js +105 -0
- package/src/providers/bitwarden/executors.d.ts +3 -0
- package/src/providers/bitwarden/executors.js +146 -0
- package/src/providers/cronfree/executors.d.ts +4 -0
- package/src/providers/cronfree/executors.js +89 -0
- package/src/providers/cufinder/executors.d.ts +4 -0
- package/src/providers/cufinder/executors.js +105 -0
- package/src/providers/dropbox/executors.js +21 -3
- package/src/providers/elevenreader/runtime.js +6 -5
- package/src/providers/feishu_app_bot/scopes.js +1 -1
- package/src/providers/gmail/attachment-stream.d.ts +2 -0
- package/src/providers/gmail/attachment-stream.js +162 -0
- package/src/providers/gmail/executors.d.ts +3 -1
- package/src/providers/gmail/executors.js +53 -25
- package/src/providers/investoday_mcp/official-actions.js +19 -372
- package/src/providers/investoday_mcp/official-tool-types.d.ts +10 -0
- package/src/providers/investoday_mcp/official-tool-types.js +4 -0
- package/src/providers/investoday_mcp/official-tools/charts.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/charts.js +32 -0
- package/src/providers/investoday_mcp/official-tools/fixed-income.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/fixed-income.js +326 -0
- package/src/providers/investoday_mcp/official-tools/funds.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/funds.js +1085 -0
- package/src/providers/investoday_mcp/official-tools/hong-kong.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/hong-kong.js +275 -0
- package/src/providers/investoday_mcp/official-tools/indices-industries.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/indices-industries.js +642 -0
- package/src/providers/investoday_mcp/official-tools/macro-calendar.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/macro-calendar.js +398 -0
- package/src/providers/investoday_mcp/official-tools/other.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/other.js +131 -0
- package/src/providers/investoday_mcp/official-tools/research-news.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/research-news.js +302 -0
- package/src/providers/investoday_mcp/official-tools/stocks.d.ts +2 -0
- package/src/providers/investoday_mcp/official-tools/stocks.js +2829 -0
- package/src/providers/ixspy/executors.d.ts +3 -0
- package/src/providers/ixspy/executors.js +220 -0
- package/src/providers/mcp-client.d.ts +1 -0
- package/src/providers/mcp-client.js +41 -27
- package/src/providers/osv/executors.d.ts +2 -0
- package/src/providers/osv/executors.js +11 -0
- package/src/providers/osv/runtime.d.ts +9 -0
- package/src/providers/osv/runtime.js +66 -0
- package/src/providers/pdf_vector/executors.d.ts +4 -0
- package/src/providers/pdf_vector/executors.js +108 -0
- package/src/providers/provider-runtime.d.ts +3 -0
- package/src/providers/provider-runtime.js +4 -1
- package/src/providers/pulsetic/executors.d.ts +4 -0
- package/src/providers/pulsetic/executors.js +126 -0
- package/src/providers/registry.generated.js +12 -0
- package/src/providers/sellersprite_mcp/executors.js +2 -1
- package/src/providers/shopee/actions.d.ts +2 -0
- package/src/providers/shopee/actions.js +474 -0
- package/src/providers/shopee/executors.d.ts +3 -0
- package/src/providers/shopee/executors.js +85 -0
- package/src/providers/shopee/runtime.d.ts +79 -0
- package/src/providers/shopee/runtime.js +693 -0
- package/src/providers/slack/actions.js +17 -0
- package/src/providers/slack/runtime.js +62 -4
- package/src/providers/speak_ai/executors.d.ts +3 -0
- package/src/providers/speak_ai/executors.js +187 -0
- package/src/providers/tongdaxin/executors.js +5 -4
- package/src/providers/tongdaxin/named-action-output.js +3 -9
- package/src/providers/tongdaxin/named-action-routing.js +1 -1
- package/src/providers/tongdaxin/named-actions.js +1 -6
- package/src/providers/tushare/runtime.js +22 -9
- package/src/providers/twenty_crm/runtime.js +4 -2
- package/src/providers/zyhub/executors.d.ts +3 -0
- package/src/providers/zyhub/executors.js +240 -0
- package/src/server/files/transit-files.d.ts +3 -2
- 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":
|
|
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":
|
|
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":
|
|
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
|
|
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
|
|
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(`${
|
|
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:
|
|
18
|
+
applicationRead: "application:application:self_manage",
|
|
19
19
|
applicationVersionRead: "application:application.app_version:readonly",
|
|
20
20
|
};
|
|
@@ -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 {
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|