@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,95 @@
|
|
|
1
|
+
import type { ExecutionContext, TransitFileWriter } from "../../core/types.ts";
|
|
2
|
+
import type { OAuthTokenResult } from "../../oauth/oauth-token.ts";
|
|
3
|
+
import type { ProviderFetch, ProviderRuntimeHandler } from "../provider-runtime.ts";
|
|
4
|
+
export declare const bilibiliUposBaseUrl = "https://openupos.bilivideo.com";
|
|
5
|
+
export declare const bilibiliOAuthRefreshTokenUrl = "https://api.bilibili.com/x/account-oauth2/v1/refresh_token";
|
|
6
|
+
export interface BilibiliActionContext {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
appSecret: string;
|
|
10
|
+
fetcher: ProviderFetch;
|
|
11
|
+
transitFiles?: TransitFileWriter;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
/** Provider-native handler shape for Bilibili actions. */
|
|
15
|
+
export type BilibiliActionHandler = ProviderRuntimeHandler<BilibiliActionContext>;
|
|
16
|
+
/**
|
|
17
|
+
* Reject an edit call that carries no editable field: it would resubmit
|
|
18
|
+
* unchanged content and still cost a provider-side review round.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertAnyInputField(input: Record<string, unknown>, fields: readonly string[]): void;
|
|
21
|
+
/** Read the signing material stored on the connection metadata by the OAuth client config. */
|
|
22
|
+
export declare function readBilibiliSigningMaterial(metadata: Record<string, unknown>): {
|
|
23
|
+
clientId: string;
|
|
24
|
+
appSecret: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the access token plus the signing material of the configured OAuth
|
|
28
|
+
* client. The Bilibili signature signs with the application App Secret, which
|
|
29
|
+
* the OAuth client config stores as the `appSecret` secretExtra field.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createBilibiliContext(context: ExecutionContext, fetcher: ProviderFetch): Promise<BilibiliActionContext>;
|
|
32
|
+
export interface BilibiliSignedHeadersInput {
|
|
33
|
+
clientId: string;
|
|
34
|
+
appSecret: string;
|
|
35
|
+
accessToken: string;
|
|
36
|
+
/** Exact request body string; GET and empty-body requests sign the MD5 of "". */
|
|
37
|
+
body?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create the signed common header set (signature version 2.0) for one arcopen
|
|
41
|
+
* request. The HMAC-SHA256 signature covers the sorted x-bili-* header lines
|
|
42
|
+
* joined by "\n"; per Bilibili's official demos the trailing newline of the
|
|
43
|
+
* last line is trimmed before signing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createBilibiliSignedHeaders(input: BilibiliSignedHeadersInput): Record<string, string>;
|
|
46
|
+
export interface BilibiliApiRequestOptions {
|
|
47
|
+
method?: "GET" | "POST";
|
|
48
|
+
/** Endpoint path under https://member.bilibili.com/arcopen/fn, for example /archive/viewlist. */
|
|
49
|
+
path: string;
|
|
50
|
+
query?: Record<string, string | undefined>;
|
|
51
|
+
body?: Record<string, unknown>;
|
|
52
|
+
/** Short request label used in error messages. */
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
/** Call one signed member.bilibili.com arcopen endpoint and unwrap its data envelope. */
|
|
56
|
+
export declare function bilibiliApiRequest(context: BilibiliActionContext, options: BilibiliApiRequestOptions): Promise<unknown>;
|
|
57
|
+
export interface BilibiliFormRequestOptions {
|
|
58
|
+
/** Endpoint path under https://member.bilibili.com/arcopen/fn. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** Form fields; undefined values are skipped. */
|
|
61
|
+
fields?: Record<string, string | undefined>;
|
|
62
|
+
/** Optional file part, sent as the `file` form field. */
|
|
63
|
+
file?: File;
|
|
64
|
+
/** Short request label used in error messages. */
|
|
65
|
+
label: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* POST one arcopen endpoint as multipart/form-data. Bilibili article
|
|
69
|
+
* endpoints take form fields instead of JSON, and per the signing rules the
|
|
70
|
+
* content MD5 of a form body is computed over an empty string.
|
|
71
|
+
*/
|
|
72
|
+
export declare function bilibiliFormRequest(context: BilibiliActionContext, options: BilibiliFormRequestOptions): Promise<unknown>;
|
|
73
|
+
/** Read one arcopen/openupos response into its envelope data, mapping HTTP and envelope failures. */
|
|
74
|
+
export declare function readBilibiliApiResponse(response: Response, label: string): Promise<unknown>;
|
|
75
|
+
/** Unwrap the shared Bilibili { code, message, data } envelope of arcopen and openupos responses. */
|
|
76
|
+
export declare function readBilibiliEnvelopeData(payload: unknown, label: string): unknown;
|
|
77
|
+
/**
|
|
78
|
+
* Read the comment increment of an inc-stats response. The field table of the
|
|
79
|
+
* official document spells this counter icn_reply; the sample payload uses inc_reply.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readBilibiliReplyIncrement(data: Record<string, unknown>): number | undefined;
|
|
82
|
+
export interface BilibiliOAuthTokenRequest {
|
|
83
|
+
url: string;
|
|
84
|
+
fields: Record<string, string>;
|
|
85
|
+
fetcher: ProviderFetch;
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
createError: (message: string) => Error;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Exchange or refresh a Bilibili OAuth token. Two Bilibili quirks force this
|
|
91
|
+
* provider-local request: the parameters travel in the URL query (the
|
|
92
|
+
* documents list them as url 参数), and `expires_in` is an absolute UTC Unix
|
|
93
|
+
* timestamp rather than a lifetime in seconds.
|
|
94
|
+
*/
|
|
95
|
+
export declare function requestBilibiliOAuthToken(input: BilibiliOAuthTokenRequest): Promise<OAuthTokenResult>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash, createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
import { compactObject, optionalNumber, optionalRecord, optionalString, requiredString } from "../../core/cast.js";
|
|
3
|
+
import { ProviderRequestError, createProviderTimeout, isAbortLikeError, providerInputError, providerResponseError, providerUserAgent, readProviderErrorTextBody, readProviderJsonBody, requireOAuthCredential, requiredResponseRecord, runProviderRequest, setSearchParams, } from "../provider-runtime.js";
|
|
4
|
+
const bilibiliMemberBaseUrl = "https://member.bilibili.com";
|
|
5
|
+
export const bilibiliUposBaseUrl = "https://openupos.bilivideo.com";
|
|
6
|
+
export const bilibiliOAuthRefreshTokenUrl = "https://api.bilibili.com/x/account-oauth2/v1/refresh_token";
|
|
7
|
+
const bilibiliApiBasePath = "/arcopen/fn";
|
|
8
|
+
const bilibiliSignatureMethod = "HMAC-SHA256";
|
|
9
|
+
const bilibiliSignatureVersion = "2.0";
|
|
10
|
+
// Business codes from the 接口签名实现标准和状态码 document. -101 is the
|
|
11
|
+
// main-site "not logged in" code that arcopen also returns for a dead token.
|
|
12
|
+
const bilibiliCredentialErrorCodes = new Set([-101, 127000, 127001, 127002, 127004]);
|
|
13
|
+
const bilibiliPermissionErrorCodes = new Set([123001, 127005, 127006, 127007, 127011, 127304, 127305]);
|
|
14
|
+
const bilibiliRateLimitErrorCodes = new Set([127009, 127306]);
|
|
15
|
+
const bilibiliInvalidInputErrorCodes = new Set([
|
|
16
|
+
4000, 123003, 123004, 123005, 123008, 123009, 123010, 123012, 123013, 123014, 123015, 123016, 123017, 123018, 123024,
|
|
17
|
+
123029, 123030, 123038, 123040, 123041, 129000, 129001, 129002, 129003, 129004, 129005, 129006, 129009, 129010,
|
|
18
|
+
129012, 129015, 129018,
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Reject an edit call that carries no editable field: it would resubmit
|
|
22
|
+
* unchanged content and still cost a provider-side review round.
|
|
23
|
+
*/
|
|
24
|
+
export function assertAnyInputField(input, fields) {
|
|
25
|
+
if (!fields.some((field) => input[field] !== undefined)) {
|
|
26
|
+
throw providerInputError(`Provide at least one field to edit: ${fields.join(", ")}.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Read the signing material stored on the connection metadata by the OAuth client config. */
|
|
30
|
+
export function readBilibiliSigningMaterial(metadata) {
|
|
31
|
+
const clientId = optionalString(metadata.oauthClientId);
|
|
32
|
+
const appSecret = optionalString(optionalRecord(metadata.oauthClientSecretExtra)?.appSecret);
|
|
33
|
+
return clientId && appSecret ? { clientId, appSecret } : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the access token plus the signing material of the configured OAuth
|
|
37
|
+
* client. The Bilibili signature signs with the application App Secret, which
|
|
38
|
+
* the OAuth client config stores as the `appSecret` secretExtra field.
|
|
39
|
+
*/
|
|
40
|
+
export async function createBilibiliContext(context, fetcher) {
|
|
41
|
+
const credential = await requireOAuthCredential(context, "bilibili");
|
|
42
|
+
const signing = readBilibiliSigningMaterial(credential.metadata);
|
|
43
|
+
if (!signing) {
|
|
44
|
+
throw new ProviderRequestError(401, "Bilibili OAuth client is missing clientId or appSecret. Reconfigure the Bilibili OAuth client first.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
accessToken: credential.accessToken,
|
|
48
|
+
clientId: signing.clientId,
|
|
49
|
+
appSecret: signing.appSecret,
|
|
50
|
+
fetcher,
|
|
51
|
+
transitFiles: context.transitFiles,
|
|
52
|
+
signal: context.signal,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create the signed common header set (signature version 2.0) for one arcopen
|
|
57
|
+
* request. The HMAC-SHA256 signature covers the sorted x-bili-* header lines
|
|
58
|
+
* joined by "\n"; per Bilibili's official demos the trailing newline of the
|
|
59
|
+
* last line is trimmed before signing.
|
|
60
|
+
*/
|
|
61
|
+
export function createBilibiliSignedHeaders(input) {
|
|
62
|
+
const contentMd5 = createHash("md5")
|
|
63
|
+
.update(input.body ?? "")
|
|
64
|
+
.digest("hex");
|
|
65
|
+
const nonce = randomUUID();
|
|
66
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
67
|
+
const signedLines = [
|
|
68
|
+
`x-bili-accesskeyid:${input.clientId}`,
|
|
69
|
+
`x-bili-content-md5:${contentMd5}`,
|
|
70
|
+
`x-bili-signature-method:${bilibiliSignatureMethod}`,
|
|
71
|
+
`x-bili-signature-nonce:${nonce}`,
|
|
72
|
+
`x-bili-signature-version:${bilibiliSignatureVersion}`,
|
|
73
|
+
`x-bili-timestamp:${timestamp}`,
|
|
74
|
+
];
|
|
75
|
+
const authorization = createHmac("sha256", input.appSecret).update(signedLines.join("\n")).digest("hex");
|
|
76
|
+
return {
|
|
77
|
+
accept: "application/json",
|
|
78
|
+
"access-token": input.accessToken,
|
|
79
|
+
authorization,
|
|
80
|
+
"x-bili-accesskeyid": input.clientId,
|
|
81
|
+
"x-bili-content-md5": contentMd5,
|
|
82
|
+
"x-bili-signature-method": bilibiliSignatureMethod,
|
|
83
|
+
"x-bili-signature-nonce": nonce,
|
|
84
|
+
"x-bili-signature-version": bilibiliSignatureVersion,
|
|
85
|
+
"x-bili-timestamp": timestamp,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Call one signed member.bilibili.com arcopen endpoint and unwrap its data envelope. */
|
|
89
|
+
export async function bilibiliApiRequest(context, options) {
|
|
90
|
+
const method = options.method ?? "GET";
|
|
91
|
+
const url = new URL(`${bilibiliMemberBaseUrl}${bilibiliApiBasePath}${options.path}`);
|
|
92
|
+
setSearchParams(url, options.query ?? {});
|
|
93
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
94
|
+
const headers = createBilibiliSignedHeaders({
|
|
95
|
+
clientId: context.clientId,
|
|
96
|
+
appSecret: context.appSecret,
|
|
97
|
+
accessToken: context.accessToken,
|
|
98
|
+
body,
|
|
99
|
+
});
|
|
100
|
+
// The document's samples send Content-Type: application/json on every
|
|
101
|
+
// JSON-channel request, including GETs and body-less POSTs.
|
|
102
|
+
headers["content-type"] = "application/json";
|
|
103
|
+
const response = await runProviderRequest({ signal: context.signal, label: options.label }, (signal) => context.fetcher(url.toString(), { method, headers, body, signal }));
|
|
104
|
+
return readBilibiliApiResponse(response, options.label);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* POST one arcopen endpoint as multipart/form-data. Bilibili article
|
|
108
|
+
* endpoints take form fields instead of JSON, and per the signing rules the
|
|
109
|
+
* content MD5 of a form body is computed over an empty string.
|
|
110
|
+
*/
|
|
111
|
+
export async function bilibiliFormRequest(context, options) {
|
|
112
|
+
const url = new URL(`${bilibiliMemberBaseUrl}${bilibiliApiBasePath}${options.path}`);
|
|
113
|
+
const form = new FormData();
|
|
114
|
+
for (const [key, value] of Object.entries(options.fields ?? {})) {
|
|
115
|
+
if (value !== undefined) {
|
|
116
|
+
form.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (options.file) {
|
|
120
|
+
form.set("file", options.file, options.file.name);
|
|
121
|
+
}
|
|
122
|
+
const headers = createBilibiliSignedHeaders({
|
|
123
|
+
clientId: context.clientId,
|
|
124
|
+
appSecret: context.appSecret,
|
|
125
|
+
accessToken: context.accessToken,
|
|
126
|
+
});
|
|
127
|
+
const response = await runProviderRequest({ signal: context.signal, label: options.label }, (signal) => context.fetcher(url.toString(), { method: "POST", headers, body: form, signal }));
|
|
128
|
+
return readBilibiliApiResponse(response, options.label);
|
|
129
|
+
}
|
|
130
|
+
/** Read one arcopen/openupos response into its envelope data, mapping HTTP and envelope failures. */
|
|
131
|
+
export async function readBilibiliApiResponse(response, label) {
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const text = await readProviderErrorTextBody(response, `${label} error response`);
|
|
134
|
+
throw new ProviderRequestError(response.status >= 500 ? 502 : response.status, text || `${label} failed with HTTP ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
const payload = await readProviderJsonBody(response, {
|
|
137
|
+
emptyBody: {},
|
|
138
|
+
invalidJsonMessage: `${label} returned invalid JSON`,
|
|
139
|
+
});
|
|
140
|
+
return readBilibiliEnvelopeData(payload, label);
|
|
141
|
+
}
|
|
142
|
+
/** Unwrap the shared Bilibili { code, message, data } envelope of arcopen and openupos responses. */
|
|
143
|
+
export function readBilibiliEnvelopeData(payload, label) {
|
|
144
|
+
const envelope = requiredResponseRecord(payload, label);
|
|
145
|
+
const code = optionalNumber(envelope.code);
|
|
146
|
+
if (code === 0) {
|
|
147
|
+
return envelope.data;
|
|
148
|
+
}
|
|
149
|
+
const message = optionalString(envelope.message) ?? "unknown error";
|
|
150
|
+
if (code !== undefined && bilibiliCredentialErrorCodes.has(code)) {
|
|
151
|
+
throw new ProviderRequestError(401, `Bilibili credential expired or invalid (code ${code}): ${message}`, envelope);
|
|
152
|
+
}
|
|
153
|
+
const status = code === undefined ? 502 : mapBilibiliErrorStatus(code);
|
|
154
|
+
if (status === 502) {
|
|
155
|
+
throw providerResponseError(`${label} failed (code ${code ?? "unknown"}): ${message}`);
|
|
156
|
+
}
|
|
157
|
+
throw new ProviderRequestError(status, `${label} failed (code ${code}): ${message}`, envelope);
|
|
158
|
+
}
|
|
159
|
+
/** Map a documented non-zero business code to the runtime status it means; unknown codes stay upstream failures. */
|
|
160
|
+
function mapBilibiliErrorStatus(code) {
|
|
161
|
+
if (bilibiliPermissionErrorCodes.has(code)) {
|
|
162
|
+
return 403;
|
|
163
|
+
}
|
|
164
|
+
if (bilibiliRateLimitErrorCodes.has(code)) {
|
|
165
|
+
return 429;
|
|
166
|
+
}
|
|
167
|
+
if (bilibiliInvalidInputErrorCodes.has(code)) {
|
|
168
|
+
return 400;
|
|
169
|
+
}
|
|
170
|
+
return 502;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Read the comment increment of an inc-stats response. The field table of the
|
|
174
|
+
* official document spells this counter icn_reply; the sample payload uses inc_reply.
|
|
175
|
+
*/
|
|
176
|
+
export function readBilibiliReplyIncrement(data) {
|
|
177
|
+
return optionalNumber(data.inc_reply) ?? optionalNumber(data.icn_reply);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Exchange or refresh a Bilibili OAuth token. Two Bilibili quirks force this
|
|
181
|
+
* provider-local request: the parameters travel in the URL query (the
|
|
182
|
+
* documents list them as url 参数), and `expires_in` is an absolute UTC Unix
|
|
183
|
+
* timestamp rather than a lifetime in seconds.
|
|
184
|
+
*/
|
|
185
|
+
export async function requestBilibiliOAuthToken(input) {
|
|
186
|
+
const url = new URL(input.url);
|
|
187
|
+
setSearchParams(url, input.fields);
|
|
188
|
+
const timeout = createProviderTimeout(input.signal);
|
|
189
|
+
let response;
|
|
190
|
+
try {
|
|
191
|
+
response = await input.fetcher(url.toString(), {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: {
|
|
194
|
+
accept: "application/json",
|
|
195
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
196
|
+
"user-agent": providerUserAgent,
|
|
197
|
+
},
|
|
198
|
+
redirect: "manual",
|
|
199
|
+
signal: timeout.signal,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
timeout.cleanup();
|
|
204
|
+
if (input.signal?.aborted) {
|
|
205
|
+
throw input.createError("Bilibili OAuth token request was cancelled.");
|
|
206
|
+
}
|
|
207
|
+
if (timeout.didTimeout() || isAbortLikeError(error)) {
|
|
208
|
+
throw input.createError("Bilibili OAuth token request timed out.");
|
|
209
|
+
}
|
|
210
|
+
throw input.createError("Bilibili OAuth token request failed without an HTTP response.");
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const payload = optionalRecord(await readProviderJsonBody(response, {
|
|
214
|
+
emptyBody: {},
|
|
215
|
+
invalidJsonMessage: "Bilibili OAuth token request returned invalid JSON",
|
|
216
|
+
}));
|
|
217
|
+
const code = optionalNumber(payload?.code);
|
|
218
|
+
if (!response.ok || code !== 0) {
|
|
219
|
+
throw input.createError(optionalString(payload?.message) ?? `Bilibili OAuth token request failed (HTTP ${response.status}).`);
|
|
220
|
+
}
|
|
221
|
+
const data = optionalRecord(payload?.data);
|
|
222
|
+
const accessToken = requiredString(data?.access_token, "access_token", input.createError);
|
|
223
|
+
return {
|
|
224
|
+
accessToken,
|
|
225
|
+
refreshToken: optionalString(data?.refresh_token),
|
|
226
|
+
tokenType: "Bearer",
|
|
227
|
+
expiresAt: readBilibiliExpiresAt(optionalNumber(data?.expires_in)),
|
|
228
|
+
metadata: compactObject({ scopes: data?.scopes }),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
timeout.cleanup();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function readBilibiliExpiresAt(expiresAtUnixSeconds) {
|
|
236
|
+
const millis = (expiresAtUnixSeconds ?? 0) * 1000;
|
|
237
|
+
return millis > 0 && millis <= 8.64e15 ? new Date(millis).toISOString() : undefined;
|
|
238
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BilibiliActionContext } from "./runtime.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Upload one video through Bilibili's upload-token flow: initialize, then a
|
|
4
|
+
* single openupos upload (files up to 100MB) or equal-sized 10MB parts plus
|
|
5
|
+
* the merge call. Returns the upload token used by archive submission.
|
|
6
|
+
*/
|
|
7
|
+
export declare function uploadBilibiliVideo(context: BilibiliActionContext, file: File): Promise<string>;
|
|
8
|
+
/** Upload one cover image (jpeg/png, up to 5MB) and return its Bilibili-hosted URL. */
|
|
9
|
+
export declare function uploadBilibiliCover(context: BilibiliActionContext, file: File): Promise<string>;
|
|
10
|
+
/** Upload one article image (jpg/png, up to 5MB) and return its Bilibili-hosted URL. */
|
|
11
|
+
export declare function uploadBilibiliArticleImage(context: BilibiliActionContext, file: File, watermark?: boolean): Promise<{
|
|
12
|
+
url: string;
|
|
13
|
+
size?: number;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { optionalNumber, requiredString } from "../../core/cast.js";
|
|
2
|
+
import { providerInputError, providerResponseError, requiredResponseRecord, runProviderRequest, setSearchParams, } from "../provider-runtime.js";
|
|
3
|
+
import { bilibiliApiRequest, bilibiliFormRequest, bilibiliUposBaseUrl, readBilibiliApiResponse } from "./runtime.js";
|
|
4
|
+
const smallVideoUploadMaxBytes = 100 * 1024 * 1024;
|
|
5
|
+
const videoUploadPartBytes = 10 * 1024 * 1024;
|
|
6
|
+
const videoUploadMaxBytes = 4 * 1024 * 1024 * 1024;
|
|
7
|
+
const imageUploadMaxBytes = 5 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* Upload one video through Bilibili's upload-token flow: initialize, then a
|
|
10
|
+
* single openupos upload (files up to 100MB) or equal-sized 10MB parts plus
|
|
11
|
+
* the merge call. Returns the upload token used by archive submission.
|
|
12
|
+
*/
|
|
13
|
+
export async function uploadBilibiliVideo(context, file) {
|
|
14
|
+
if (file.size === 0) {
|
|
15
|
+
throw providerInputError("The video file is empty.");
|
|
16
|
+
}
|
|
17
|
+
if (file.size > videoUploadMaxBytes) {
|
|
18
|
+
throw providerInputError("The video file exceeds the Bilibili 4GB upload limit.");
|
|
19
|
+
}
|
|
20
|
+
const initData = requiredResponseRecord(await bilibiliApiRequest(context, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
path: "/archive/video/init",
|
|
23
|
+
body: {
|
|
24
|
+
name: normalizeUploadFileName(file.name),
|
|
25
|
+
utype: file.size <= smallVideoUploadMaxBytes ? "1" : "0",
|
|
26
|
+
},
|
|
27
|
+
label: "Bilibili video upload init",
|
|
28
|
+
}), "Bilibili video upload init");
|
|
29
|
+
const uploadToken = requiredString(initData.upload_token, "upload_token", providerResponseError);
|
|
30
|
+
if (file.size <= smallVideoUploadMaxBytes) {
|
|
31
|
+
await postBilibiliUposBytes(context, "/video/v2/upload", { upload_token: uploadToken }, file, "Bilibili video upload");
|
|
32
|
+
return uploadToken;
|
|
33
|
+
}
|
|
34
|
+
let offset = 0;
|
|
35
|
+
let partNumber = 1;
|
|
36
|
+
while (offset < file.size) {
|
|
37
|
+
const part = file.slice(offset, offset + videoUploadPartBytes);
|
|
38
|
+
await postBilibiliUposBytes(context, "/video/v2/part/upload", { upload_token: uploadToken, part_number: String(partNumber) }, part, `Bilibili video part ${partNumber} upload`);
|
|
39
|
+
offset += videoUploadPartBytes;
|
|
40
|
+
partNumber += 1;
|
|
41
|
+
}
|
|
42
|
+
await bilibiliApiRequest(context, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
path: "/archive/video/complete",
|
|
45
|
+
query: { upload_token: uploadToken },
|
|
46
|
+
label: "Bilibili video upload complete",
|
|
47
|
+
});
|
|
48
|
+
return uploadToken;
|
|
49
|
+
}
|
|
50
|
+
/** Upload one cover image (jpeg/png, up to 5MB) and return its Bilibili-hosted URL. */
|
|
51
|
+
export async function uploadBilibiliCover(context, file) {
|
|
52
|
+
if (file.size === 0) {
|
|
53
|
+
throw providerInputError("The cover image is empty.");
|
|
54
|
+
}
|
|
55
|
+
if (file.size > imageUploadMaxBytes) {
|
|
56
|
+
throw providerInputError("The cover image exceeds the Bilibili 5MB limit.");
|
|
57
|
+
}
|
|
58
|
+
const data = requiredResponseRecord(await bilibiliFormRequest(context, {
|
|
59
|
+
path: "/archive/cover/upload",
|
|
60
|
+
file,
|
|
61
|
+
label: "Bilibili cover upload",
|
|
62
|
+
}), "Bilibili cover upload");
|
|
63
|
+
return requiredString(data.url, "url", providerResponseError);
|
|
64
|
+
}
|
|
65
|
+
/** Upload one article image (jpg/png, up to 5MB) and return its Bilibili-hosted URL. */
|
|
66
|
+
export async function uploadBilibiliArticleImage(context, file, watermark) {
|
|
67
|
+
if (file.size === 0) {
|
|
68
|
+
throw providerInputError("The article image is empty.");
|
|
69
|
+
}
|
|
70
|
+
if (file.size > imageUploadMaxBytes) {
|
|
71
|
+
throw providerInputError("The article image exceeds the Bilibili 5MB limit.");
|
|
72
|
+
}
|
|
73
|
+
const data = requiredResponseRecord(await bilibiliFormRequest(context, {
|
|
74
|
+
path: "/article/upload/image",
|
|
75
|
+
fields: { watermark: watermark === undefined ? undefined : String(watermark) },
|
|
76
|
+
file,
|
|
77
|
+
label: "Bilibili article image upload",
|
|
78
|
+
}), "Bilibili article image upload");
|
|
79
|
+
const url = requiredString(data.url, "url", providerResponseError);
|
|
80
|
+
const size = optionalNumber(data.size);
|
|
81
|
+
return size === undefined ? { url } : { url, size };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Post raw file bytes to the openupos upload host. These endpoints
|
|
85
|
+
* authenticate through the upload_token query parameter and must not carry
|
|
86
|
+
* multipart packaging around the bare binary body.
|
|
87
|
+
*/
|
|
88
|
+
async function postBilibiliUposBytes(context, path, query, body, label) {
|
|
89
|
+
const url = new URL(`${bilibiliUposBaseUrl}${path}`);
|
|
90
|
+
setSearchParams(url, query);
|
|
91
|
+
const response = await runProviderRequest({ signal: context.signal, label }, (signal) => context.fetcher(url.toString(), {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/octet-stream" },
|
|
94
|
+
body,
|
|
95
|
+
signal,
|
|
96
|
+
}));
|
|
97
|
+
await readBilibiliApiResponse(response, label);
|
|
98
|
+
}
|
|
99
|
+
function normalizeUploadFileName(name) {
|
|
100
|
+
const trimmed = name.trim();
|
|
101
|
+
if (!trimmed) {
|
|
102
|
+
return "video.mp4";
|
|
103
|
+
}
|
|
104
|
+
return trimmed.includes(".") ? trimmed : `${trimmed}.mp4`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { compactObject, looseArray, optionalRecord, optionalString } from "../../core/cast.js";
|
|
2
|
+
import { defineProviderExecutors, providerInputError, ProviderRequestError, providerResponseError, providerUserAgent, readProviderJsonBody, requireCustomCredential, requiredInputString, requiredResponseRecord, runProviderRequest, } from "../provider-runtime.js";
|
|
3
|
+
const service = "bitwarden";
|
|
4
|
+
const regions = {
|
|
5
|
+
us: { api: "https://api.bitwarden.com", identity: "https://identity.bitwarden.com" },
|
|
6
|
+
eu: { api: "https://api.bitwarden.eu", identity: "https://identity.bitwarden.eu" },
|
|
7
|
+
};
|
|
8
|
+
const handlers = {
|
|
9
|
+
list_collections: list("/public/collections", "collections"),
|
|
10
|
+
get_collection: get("/public/collections", "collection"),
|
|
11
|
+
async list_events(input, context) {
|
|
12
|
+
const payload = requiredResponseRecord(await request("/public/events", "list events", context, compactObject({
|
|
13
|
+
start: optionalString(input.start),
|
|
14
|
+
end: optionalString(input.end),
|
|
15
|
+
actingUserId: optionalString(input.actingUserId),
|
|
16
|
+
itemId: optionalString(input.itemId),
|
|
17
|
+
secretId: optionalString(input.secretId),
|
|
18
|
+
projectId: optionalString(input.projectId),
|
|
19
|
+
continuationToken: optionalString(input.continuationToken),
|
|
20
|
+
})), "Bitwarden event list");
|
|
21
|
+
return { events: looseArray(payload.data), continuationToken: optionalString(payload.continuationToken) ?? null };
|
|
22
|
+
},
|
|
23
|
+
list_groups: list("/public/groups", "groups"),
|
|
24
|
+
get_group: get("/public/groups", "group"),
|
|
25
|
+
get_group_member_ids: ids("/public/groups", "member-ids", "memberIds"),
|
|
26
|
+
list_members: list("/public/members", "members"),
|
|
27
|
+
get_member: get("/public/members", "member"),
|
|
28
|
+
get_member_group_ids: ids("/public/members", "group-ids", "groupIds"),
|
|
29
|
+
list_policies: list("/public/policies", "policies"),
|
|
30
|
+
async get_policy(input, context) {
|
|
31
|
+
if (!Number.isInteger(input.type))
|
|
32
|
+
throw providerInputError("type is required");
|
|
33
|
+
return {
|
|
34
|
+
policy: requiredResponseRecord(await request(`/public/policies/${input.type}`, "get policy", context), "Bitwarden policy"),
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
async get_subscription(_input, context) {
|
|
38
|
+
return requiredResponseRecord(await request("/public/organization/subscription", "get subscription", context), "Bitwarden subscription");
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
export const executors = defineProviderExecutors({
|
|
42
|
+
service,
|
|
43
|
+
handlers,
|
|
44
|
+
async createContext(context, fetcher) {
|
|
45
|
+
const stored = await requireCustomCredential(context, service);
|
|
46
|
+
return { ...credential(stored.values), fetcher, signal: context.signal };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
export const credentialValidators = {
|
|
50
|
+
async customCredential(input, { fetcher, signal }) {
|
|
51
|
+
const value = credential(input.values);
|
|
52
|
+
await token(value, fetcher, signal, true);
|
|
53
|
+
return {
|
|
54
|
+
profile: { accountId: value.clientId, displayName: `Bitwarden Organization (${value.region.toUpperCase()})` },
|
|
55
|
+
grantedScopes: ["api.organization"],
|
|
56
|
+
metadata: { region: value.region, apiBaseUrl: regions[value.region].api },
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
function credential(values) {
|
|
61
|
+
const region = requiredInputString(values.region, "region").toLowerCase();
|
|
62
|
+
if (region !== "us" && region !== "eu")
|
|
63
|
+
throw providerInputError("region must be us or eu");
|
|
64
|
+
return {
|
|
65
|
+
clientId: requiredInputString(values.clientId, "clientId"),
|
|
66
|
+
clientSecret: requiredInputString(values.clientSecret, "clientSecret"),
|
|
67
|
+
region,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function list(path, field) {
|
|
71
|
+
return async (input, context) => {
|
|
72
|
+
const payload = requiredResponseRecord(await request(path, `list ${field}`, context, compactObject({ continuationToken: optionalString(input.continuationToken) })), `Bitwarden ${field}`);
|
|
73
|
+
return { [field]: looseArray(payload.data), continuationToken: optionalString(payload.continuationToken) ?? null };
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function get(path, field) {
|
|
77
|
+
return async (input, context) => ({
|
|
78
|
+
[field]: requiredResponseRecord(await request(`${path}/${encodeURIComponent(requiredInputString(input.id, "id"))}`, `get ${field}`, context), `Bitwarden ${field}`),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function ids(path, suffix, field) {
|
|
82
|
+
return async (input, context) => ({
|
|
83
|
+
[field]: looseArray(await request(`${path}/${encodeURIComponent(requiredInputString(input.id, "id"))}/${suffix}`, `get ${field}`, context)),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function token(value, fetcher, parentSignal, validating) {
|
|
87
|
+
const response = await runProviderRequest({ signal: parentSignal, label: "Bitwarden token exchange" }, (signal) => fetcher(`${regions[value.region].identity}/connect/token`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
accept: "application/json",
|
|
91
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
92
|
+
"user-agent": providerUserAgent,
|
|
93
|
+
},
|
|
94
|
+
body: new URLSearchParams({
|
|
95
|
+
grant_type: "client_credentials",
|
|
96
|
+
scope: "api.organization",
|
|
97
|
+
client_id: value.clientId,
|
|
98
|
+
client_secret: value.clientSecret,
|
|
99
|
+
}).toString(),
|
|
100
|
+
signal,
|
|
101
|
+
}));
|
|
102
|
+
const payload = await readProviderJsonBody(response, {
|
|
103
|
+
emptyBody: null,
|
|
104
|
+
invalidJsonMessage: "Bitwarden token exchange returned invalid JSON",
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok)
|
|
107
|
+
throw error(response.status, payload, validating);
|
|
108
|
+
const accessToken = optionalString(optionalRecord(payload)?.access_token)?.trim();
|
|
109
|
+
if (!accessToken)
|
|
110
|
+
throw providerResponseError("Bitwarden access token is missing");
|
|
111
|
+
return accessToken;
|
|
112
|
+
}
|
|
113
|
+
async function request(path, operation, context, query = {}) {
|
|
114
|
+
const accessToken = await token(context, context.fetcher, context.signal, false);
|
|
115
|
+
const url = new URL(path, `${regions[context.region].api}/`);
|
|
116
|
+
for (const [key, value] of Object.entries(query))
|
|
117
|
+
if (value != null)
|
|
118
|
+
url.searchParams.set(key, String(value));
|
|
119
|
+
const response = await runProviderRequest({ signal: context.signal, label: `Bitwarden ${operation}` }, (signal) => context.fetcher(url, {
|
|
120
|
+
headers: { accept: "application/json", authorization: `Bearer ${accessToken}`, "user-agent": providerUserAgent },
|
|
121
|
+
signal,
|
|
122
|
+
}));
|
|
123
|
+
const payload = await readProviderJsonBody(response, {
|
|
124
|
+
emptyBody: null,
|
|
125
|
+
invalidJsonMessage: `Bitwarden ${operation} returned invalid JSON`,
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok)
|
|
128
|
+
throw error(response.status, payload, false);
|
|
129
|
+
if (payload == null)
|
|
130
|
+
throw providerResponseError(`Bitwarden ${operation} returned an empty response`);
|
|
131
|
+
return payload;
|
|
132
|
+
}
|
|
133
|
+
function error(status, payload, validating) {
|
|
134
|
+
const body = optionalRecord(payload);
|
|
135
|
+
const code = optionalString(body?.error);
|
|
136
|
+
const message = optionalString(body?.error_description) ??
|
|
137
|
+
optionalString(body?.message) ??
|
|
138
|
+
`Bitwarden request failed with ${status}`;
|
|
139
|
+
if (status === 429)
|
|
140
|
+
return new ProviderRequestError(429, message, payload);
|
|
141
|
+
if (code === "invalid_client")
|
|
142
|
+
return validating ? providerInputError(message) : new ProviderRequestError(409, message, payload);
|
|
143
|
+
if (status === 400)
|
|
144
|
+
return providerInputError(message);
|
|
145
|
+
return new ProviderRequestError(status, message, payload);
|
|
146
|
+
}
|
|
@@ -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;
|