@hasna/connectors 1.3.44 → 1.3.46

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 (114) hide show
  1. package/README.md +25 -0
  2. package/bin/index.js +792 -130
  3. package/bin/mcp.js +438 -52
  4. package/bin/serve.js +438 -52
  5. package/connectors/cloudflare-workers/.env.example +5 -0
  6. package/connectors/cloudflare-workers/AGENTS.md +11 -0
  7. package/connectors/cloudflare-workers/CLAUDE.md +11 -0
  8. package/connectors/cloudflare-workers/README.md +45 -0
  9. package/connectors/cloudflare-workers/package.json +47 -0
  10. package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
  11. package/connectors/cloudflare-workers/src/api/client.ts +386 -0
  12. package/connectors/cloudflare-workers/src/api/index.ts +1 -0
  13. package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
  14. package/connectors/cloudflare-workers/src/index.ts +16 -0
  15. package/connectors/cloudflare-workers/src/types/index.ts +53 -0
  16. package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
  17. package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
  18. package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
  19. package/connectors/cloudflare-workers/tsconfig.json +14 -0
  20. package/connectors/tidio/.env.example +3 -2
  21. package/connectors/tidio/CLAUDE.md +21 -96
  22. package/connectors/tidio/README.md +22 -18
  23. package/connectors/tidio/package.json +4 -2
  24. package/connectors/tidio/src/api/client.ts +26 -15
  25. package/connectors/tidio/src/api/index.ts +185 -128
  26. package/connectors/tidio/src/api/tidio.test.ts +175 -44
  27. package/connectors/tidio/src/cli/index.ts +154 -388
  28. package/connectors/tidio/src/index.ts +5 -3
  29. package/connectors/tidio/src/types/index.ts +135 -82
  30. package/connectors/tidio/src/utils/config.ts +90 -44
  31. package/connectors/wandb/.env.example +7 -0
  32. package/connectors/wandb/AGENTS.md +43 -0
  33. package/connectors/wandb/CLAUDE.md +67 -0
  34. package/connectors/wandb/README.md +65 -0
  35. package/connectors/wandb/package.json +52 -0
  36. package/connectors/wandb/src/api/client.test.ts +116 -0
  37. package/connectors/wandb/src/api/client.ts +83 -0
  38. package/connectors/wandb/src/api/graphql.ts +9 -0
  39. package/connectors/wandb/src/api/index.ts +36 -0
  40. package/connectors/wandb/src/api/projects.ts +36 -0
  41. package/connectors/wandb/src/api/viewer.ts +20 -0
  42. package/connectors/wandb/src/cli/index.ts +265 -0
  43. package/connectors/wandb/src/index.ts +22 -0
  44. package/connectors/wandb/src/types/index.ts +75 -0
  45. package/connectors/wandb/src/utils/config.ts +164 -0
  46. package/connectors/wandb/src/utils/output.ts +119 -0
  47. package/connectors/wandb/tsconfig.json +16 -0
  48. package/connectors/weights-biases/.env.example +7 -0
  49. package/connectors/weights-biases/CLAUDE.md +65 -0
  50. package/connectors/weights-biases/README.md +62 -0
  51. package/connectors/weights-biases/package.json +53 -0
  52. package/connectors/weights-biases/src/api/client.test.ts +46 -0
  53. package/connectors/weights-biases/src/api/client.ts +119 -0
  54. package/connectors/weights-biases/src/api/events.ts +18 -0
  55. package/connectors/weights-biases/src/api/index.ts +56 -0
  56. package/connectors/weights-biases/src/api/runs.ts +28 -0
  57. package/connectors/weights-biases/src/api/search.ts +10 -0
  58. package/connectors/weights-biases/src/cli/index.ts +289 -0
  59. package/connectors/weights-biases/src/index.ts +19 -0
  60. package/connectors/weights-biases/src/types/index.ts +92 -0
  61. package/connectors/weights-biases/src/utils/config.ts +168 -0
  62. package/connectors/weights-biases/src/utils/output.ts +116 -0
  63. package/connectors/weights-biases/tsconfig.json +16 -0
  64. package/connectors/zoominfo/.env.example +4 -0
  65. package/connectors/zoominfo/AGENTS.md +26 -0
  66. package/connectors/zoominfo/CLAUDE.md +20 -0
  67. package/connectors/zoominfo/README.md +69 -0
  68. package/connectors/zoominfo/package.json +52 -0
  69. package/connectors/zoominfo/src/api/client.test.ts +140 -0
  70. package/connectors/zoominfo/src/api/client.ts +228 -0
  71. package/connectors/zoominfo/src/api/index.ts +1 -0
  72. package/connectors/zoominfo/src/cli/index.ts +179 -0
  73. package/connectors/zoominfo/src/index.ts +3 -0
  74. package/connectors/zoominfo/src/types/index.ts +39 -0
  75. package/connectors/zoominfo/src/utils/config.ts +61 -0
  76. package/connectors/zoominfo/src/utils/output.ts +27 -0
  77. package/connectors/zoominfo/tsconfig.json +20 -0
  78. package/connectors/zotero/.env.example +7 -0
  79. package/connectors/zotero/.npmrc.example +2 -0
  80. package/connectors/zotero/AGENTS.md +40 -0
  81. package/connectors/zotero/CLAUDE.md +69 -0
  82. package/connectors/zotero/README.md +91 -0
  83. package/connectors/zotero/package.json +43 -0
  84. package/connectors/zotero/src/api/attachments.ts +130 -0
  85. package/connectors/zotero/src/api/client.test.ts +166 -0
  86. package/connectors/zotero/src/api/client.ts +169 -0
  87. package/connectors/zotero/src/api/collections.ts +26 -0
  88. package/connectors/zotero/src/api/index.ts +45 -0
  89. package/connectors/zotero/src/api/items.ts +64 -0
  90. package/connectors/zotero/src/cli/index.ts +386 -0
  91. package/connectors/zotero/src/index.ts +28 -0
  92. package/connectors/zotero/src/types/index.ts +129 -0
  93. package/connectors/zotero/src/utils/config.ts +245 -0
  94. package/connectors/zotero/src/utils/output.ts +119 -0
  95. package/connectors/zotero/tsconfig.json +16 -0
  96. package/connectors/zymbly/.env.example +2 -0
  97. package/connectors/zymbly/AGENTS.md +11 -0
  98. package/connectors/zymbly/CLAUDE.md +5 -0
  99. package/connectors/zymbly/README.md +35 -0
  100. package/connectors/zymbly/package.json +52 -0
  101. package/connectors/zymbly/src/api/client.test.ts +110 -0
  102. package/connectors/zymbly/src/api/client.ts +166 -0
  103. package/connectors/zymbly/src/api/index.ts +1 -0
  104. package/connectors/zymbly/src/cli/index.ts +156 -0
  105. package/connectors/zymbly/src/index.ts +12 -0
  106. package/connectors/zymbly/src/types/index.ts +34 -0
  107. package/connectors/zymbly/src/utils/config.ts +52 -0
  108. package/connectors/zymbly/src/utils/output.ts +17 -0
  109. package/connectors/zymbly/tsconfig.json +13 -0
  110. package/dist/index.js +45 -2
  111. package/dist/lib/compact-output.d.ts +40 -0
  112. package/dist/mcp/tools/jobs.d.ts +32 -0
  113. package/dist/mcp/tools/jobs.test.d.ts +1 -0
  114. package/package.json +3 -2
@@ -0,0 +1,5 @@
1
+ CLOUDFLARE_WORKERS_API_TOKEN=your_cloudflare_api_token
2
+ CLOUDFLARE_WORKERS_ACCOUNT_ID=your_cloudflare_account_id
3
+
4
+ # Optional: override the Cloudflare API base URL for tests or private gateways.
5
+ # CLOUDFLARE_WORKERS_BASE_URL=https://api.cloudflare.com/client/v4
@@ -0,0 +1,11 @@
1
+ # Cloudflare Workers Connector
2
+
3
+ Follow the repository root agent instructions. This connector uses Cloudflare Workers REST API endpoints and must not add browser automation dependencies.
4
+
5
+ ## Checks
6
+
7
+ ```bash
8
+ bun run typecheck
9
+ bun run build
10
+ bun run ./src/cli/index.ts --help
11
+ ```
@@ -0,0 +1,11 @@
1
+ # Cloudflare Workers Connector
2
+
3
+ Follow the repository root agent instructions. This connector uses Cloudflare Workers REST API endpoints and must not add browser automation dependencies.
4
+
5
+ ## Checks
6
+
7
+ ```bash
8
+ bun run typecheck
9
+ bun run build
10
+ bun run ./src/cli/index.ts --help
11
+ ```
@@ -0,0 +1,45 @@
1
+ # @hasna/connect-cloudflare-workers
2
+
3
+ Cloudflare Workers REST API connector for scripts, script uploads, settings, routes, deployments, versions, secrets, schedules, tails, subdomains, beta Workers, and scoped raw requests.
4
+
5
+ ## Configure
6
+
7
+ ```bash
8
+ cp .env.example .env
9
+ export CLOUDFLARE_WORKERS_API_TOKEN=your_cloudflare_api_token
10
+ export CLOUDFLARE_WORKERS_ACCOUNT_ID=your_cloudflare_account_id
11
+ ```
12
+
13
+ `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are accepted as fallbacks.
14
+
15
+ ## CLI
16
+
17
+ ```bash
18
+ bun run ./src/cli/index.ts list-scripts
19
+ bun run ./src/cli/index.ts upload-script hello-worker --file ./worker.js --metadata '{"main_module":"worker.js"}'
20
+ bun run ./src/cli/index.ts list-routes ZONE_ID
21
+ bun run ./src/cli/index.ts create-beta-worker --body '{"name":"hello-worker"}'
22
+ bun run ./src/cli/index.ts raw /accounts/ACCOUNT_ID/workers/scripts
23
+ ```
24
+
25
+ ## Library
26
+
27
+ ```ts
28
+ import { CloudflareWorkers } from "@hasna/connect-cloudflare-workers";
29
+
30
+ const workers = new CloudflareWorkers({
31
+ apiToken: process.env.CLOUDFLARE_WORKERS_API_TOKEN,
32
+ accountId: process.env.CLOUDFLARE_WORKERS_ACCOUNT_ID,
33
+ });
34
+
35
+ const scripts = await workers.listScripts();
36
+ await workers.uploadScript("hello-worker", "export default { fetch() { return new Response('ok') } }");
37
+ ```
38
+
39
+ ## Checks
40
+
41
+ ```bash
42
+ bun run test
43
+ bun run typecheck
44
+ bun run build
45
+ ```
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@hasna/connect-cloudflare-workers",
3
+ "version": "0.0.1",
4
+ "description": "Cloudflare Workers REST API connector for scripts, uploads, routes, deployments, versions, secrets, tails, subdomains, beta workers, and scoped raw requests",
5
+ "type": "module",
6
+ "bin": {
7
+ "connect-cloudflare-workers": "./bin/index.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "scripts": {
18
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/cli/index.ts --outdir ./bin --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly",
19
+ "dev": "bun run ./src/cli/index.ts",
20
+ "test": "bun test",
21
+ "typecheck": "tsc --noEmit",
22
+ "prepublishOnly": "bun run build"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "bin",
27
+ "README.md"
28
+ ],
29
+ "keywords": ["cloudflare", "workers", "serverless", "edge", "scripts", "deployments", "routes", "connector", "cli", "typescript", "bun"],
30
+ "author": "Hasna",
31
+ "license": "Apache-2.0",
32
+ "devDependencies": {
33
+ "@types/bun": "latest",
34
+ "typescript": "^5"
35
+ },
36
+ "dependencies": {
37
+ "chalk": "^5.3.0",
38
+ "commander": "^12.1.0"
39
+ },
40
+ "engines": {
41
+ "bun": ">=1.0.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/hasna/open-connectors.git"
46
+ }
47
+ }
@@ -0,0 +1,144 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { CloudflareWorkers, CloudflareWorkersApiError } from "../index";
3
+
4
+ const originalFetch = globalThis.fetch;
5
+ let fetchMock: ReturnType<typeof mock>;
6
+
7
+ function mockJsonResponse(body: unknown, init: ResponseInit = {}) {
8
+ return new Response(JSON.stringify(body), {
9
+ status: init.status ?? 200,
10
+ headers: { "content-type": "application/json", ...init.headers },
11
+ });
12
+ }
13
+
14
+ function client() {
15
+ return new CloudflareWorkers({
16
+ apiToken: "token-123",
17
+ accountId: "account-123",
18
+ });
19
+ }
20
+
21
+ beforeEach(() => {
22
+ fetchMock = mock(async () =>
23
+ mockJsonResponse({ success: true, result: { ok: true } })
24
+ );
25
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
26
+ });
27
+
28
+ afterEach(() => {
29
+ globalThis.fetch = originalFetch;
30
+ });
31
+
32
+ describe("CloudflareWorkers", () => {
33
+ test("lists scripts with account path, auth header, and query params", async () => {
34
+ const api = client();
35
+
36
+ await api.listScripts({ per_page: 25, tag: ["edge", "prod"] });
37
+
38
+ expect(fetchMock).toHaveBeenCalledTimes(1);
39
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
40
+ expect(url).toBe(
41
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/scripts?per_page=25&tag=edge&tag=prod"
42
+ );
43
+ expect(init.method).toBe("GET");
44
+ expect((init.headers as Record<string, string>).Authorization).toBe(
45
+ "Bearer token-123"
46
+ );
47
+ });
48
+
49
+ test("uploads a script as multipart form data", async () => {
50
+ const api = client();
51
+
52
+ await api.uploadScript("hello-worker", "export default {}", {
53
+ metadata: { main_module: "worker.js", compatibility_date: "2026-01-01" },
54
+ filename: "worker.js",
55
+ });
56
+
57
+ const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
58
+ expect(url).toBe(
59
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/scripts/hello-worker"
60
+ );
61
+ expect(init.method).toBe("PUT");
62
+ expect(init.body).toBeInstanceOf(FormData);
63
+ const form = init.body as FormData;
64
+ expect(Array.from(form.keys())).toEqual(["metadata", "worker.js"]);
65
+ expect(form.get("worker.js")).toBeInstanceOf(Blob);
66
+ expect((init.headers as Record<string, string>)["Content-Type"]).toBeUndefined();
67
+ });
68
+
69
+ test("uses Workers resource endpoint for Worker resources and versions", async () => {
70
+ const api = client();
71
+
72
+ await api.createBetaWorker({ name: "payment-service" });
73
+ await api.createBetaVersion("worker-id", {
74
+ main_module: "worker.js",
75
+ compatibility_date: "2026-01-01",
76
+ });
77
+
78
+ const calls = fetchMock.mock.calls as [
79
+ string,
80
+ RequestInit,
81
+ ][];
82
+ expect(calls[0][0]).toBe(
83
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/workers"
84
+ );
85
+ expect(calls[1][0]).toBe(
86
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/workers/worker-id/versions"
87
+ );
88
+ });
89
+
90
+ test("allows scoped raw Workers API paths and rejects traversal", async () => {
91
+ const api = client();
92
+
93
+ await api.rawRequest(
94
+ "GET",
95
+ "/accounts/account-123/workers/scripts/hello-worker/deployments"
96
+ );
97
+ await api.rawRequest(
98
+ "GET",
99
+ "/accounts/account-123/workers/workers/worker-id/versions"
100
+ );
101
+
102
+ const [url] = fetchMock.mock.calls[0] as [
103
+ string,
104
+ RequestInit,
105
+ ];
106
+ expect(url).toBe(
107
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/scripts/hello-worker/deployments"
108
+ );
109
+ const [workerVersionsUrl] = fetchMock.mock.calls[1] as [
110
+ string,
111
+ RequestInit,
112
+ ];
113
+ expect(workerVersionsUrl).toBe(
114
+ "https://api.cloudflare.com/client/v4/accounts/account-123/workers/workers/worker-id/versions"
115
+ );
116
+
117
+ expect(() =>
118
+ api.rawRequest("GET", "/accounts/account-123/workers/scripts/../tokens")
119
+ ).toThrow(CloudflareWorkersApiError);
120
+ });
121
+
122
+ test("surfaces Cloudflare API error messages", async () => {
123
+ fetchMock = mock(async () =>
124
+ mockJsonResponse(
125
+ { success: false, errors: [{ message: "missing permission" }] },
126
+ { status: 403 }
127
+ )
128
+ );
129
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
130
+
131
+ await expect(client().listScripts()).rejects.toThrow(
132
+ "Cloudflare Workers: missing permission"
133
+ );
134
+ });
135
+
136
+ test("requires token and account ID before sending account-scoped requests", async () => {
137
+ const api = new CloudflareWorkers({ apiToken: "", accountId: "" });
138
+
139
+ expect(() => api.listScripts()).toThrow(
140
+ "Cloudflare Workers accountId is required"
141
+ );
142
+ expect(fetchMock).not.toHaveBeenCalled();
143
+ });
144
+ });
@@ -0,0 +1,386 @@
1
+ import type {
2
+ CloudflareWorkersConfig,
3
+ CloudflareWorkersRawResponse,
4
+ CloudflareWorkersRequestOptions,
5
+ HttpMethod,
6
+ JsonObject,
7
+ QueryParams,
8
+ QueryValue,
9
+ RequestBody,
10
+ } from "../types";
11
+ import { CloudflareWorkersApiError } from "../types";
12
+
13
+ const DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
14
+ const DEFAULT_TIMEOUT_MS = 30_000;
15
+
16
+ export class CloudflareWorkersClient {
17
+ private readonly apiToken?: string;
18
+ private readonly accountId?: string;
19
+ private readonly baseUrl: string;
20
+ private readonly timeoutMs: number;
21
+
22
+ constructor(config: CloudflareWorkersConfig = {}) {
23
+ this.apiToken = config.apiToken;
24
+ this.accountId = config.accountId;
25
+ this.baseUrl = this.cleanBaseUrl(config.baseUrl || DEFAULT_BASE_URL);
26
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27
+ }
28
+
29
+ getBaseUrl(): string {
30
+ return this.baseUrl;
31
+ }
32
+
33
+ request<T = unknown>(path: string, options: CloudflareWorkersRequestOptions = {}): Promise<T> {
34
+ return this.fetchPath<T>(this.resolvePath(path), options);
35
+ }
36
+
37
+ listScripts(query: QueryParams = {}) {
38
+ return this.fetchPath(this.accountPath("/workers/scripts"), { method: "GET", query });
39
+ }
40
+
41
+ getScript(scriptName: string) {
42
+ return this.fetchPath(this.scriptPath(scriptName), { method: "GET" });
43
+ }
44
+
45
+ getScriptContent(scriptName: string): Promise<string> {
46
+ return this.fetchPath(this.scriptPath(scriptName), { method: "GET", headers: { Accept: "application/javascript, text/plain, */*" }, responseType: "text" });
47
+ }
48
+
49
+ uploadScript(scriptName: string, script: string | Uint8Array | ArrayBuffer | Blob, options: { metadata?: JsonObject; filename?: string; contentType?: string } = {}) {
50
+ const filename = options.filename || "worker.js";
51
+ const metadata = options.metadata || { main_module: filename };
52
+ const mainModule = typeof metadata.main_module === "string" && metadata.main_module.trim()
53
+ ? metadata.main_module
54
+ : filename;
55
+ const form = new FormData();
56
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
57
+ form.append(mainModule, new Blob([this.blobPart(script)], { type: options.contentType || "application/javascript" }), filename);
58
+ return this.fetchPath(this.scriptPath(scriptName), { method: "PUT", body: form, rawBody: true });
59
+ }
60
+
61
+ deleteScript(scriptName: string) {
62
+ return this.fetchPath(this.scriptPath(scriptName), { method: "DELETE" });
63
+ }
64
+
65
+ getScriptSettings(scriptName: string) {
66
+ return this.fetchPath(`${this.scriptPath(scriptName)}/settings`, { method: "GET" });
67
+ }
68
+
69
+ updateScriptSettings(scriptName: string, body: JsonObject) {
70
+ return this.fetchPath(`${this.scriptPath(scriptName)}/settings`, { method: "PATCH", body: this.requireObject(body, "settings body") });
71
+ }
72
+
73
+ listDeployments(scriptName: string) {
74
+ return this.fetchPath(`${this.scriptPath(scriptName)}/deployments`, { method: "GET" });
75
+ }
76
+
77
+ createDeployment(scriptName: string, body: JsonObject) {
78
+ return this.fetchPath(`${this.scriptPath(scriptName)}/deployments`, { method: "POST", body: this.requireObject(body, "deployment body") });
79
+ }
80
+
81
+ listVersions(scriptName: string) {
82
+ return this.fetchPath(`${this.scriptPath(scriptName)}/versions`, { method: "GET" });
83
+ }
84
+
85
+ getVersion(scriptName: string, versionId: string) {
86
+ return this.fetchPath(`${this.scriptPath(scriptName)}/versions/${this.segment(versionId, "versionId")}`, { method: "GET" });
87
+ }
88
+
89
+ listSecrets(scriptName: string) {
90
+ return this.fetchPath(`${this.scriptPath(scriptName)}/secrets`, { method: "GET" });
91
+ }
92
+
93
+ putSecret(scriptName: string, body: JsonObject) {
94
+ return this.fetchPath(`${this.scriptPath(scriptName)}/secrets`, { method: "PUT", body: this.requireObject(body, "secret body") });
95
+ }
96
+
97
+ deleteSecret(scriptName: string, secretName: string) {
98
+ return this.fetchPath(`${this.scriptPath(scriptName)}/secrets/${this.segment(secretName, "secretName")}`, { method: "DELETE" });
99
+ }
100
+
101
+ getSchedules(scriptName: string) {
102
+ return this.fetchPath(`${this.scriptPath(scriptName)}/schedules`, { method: "GET" });
103
+ }
104
+
105
+ putSchedules(scriptName: string, body: JsonObject) {
106
+ return this.fetchPath(`${this.scriptPath(scriptName)}/schedules`, { method: "PUT", body: this.requireObject(body, "schedules body") });
107
+ }
108
+
109
+ createTail(scriptName: string) {
110
+ return this.fetchPath(`${this.scriptPath(scriptName)}/tails`, { method: "POST", body: {} });
111
+ }
112
+
113
+ deleteTail(scriptName: string, tailId: string) {
114
+ return this.fetchPath(`${this.scriptPath(scriptName)}/tails/${this.segment(tailId, "tailId")}`, { method: "DELETE" });
115
+ }
116
+
117
+ getAccountSubdomain() {
118
+ return this.fetchPath(this.accountPath("/workers/subdomain"), { method: "GET" });
119
+ }
120
+
121
+ updateAccountSubdomain(body: JsonObject) {
122
+ return this.fetchPath(this.accountPath("/workers/subdomain"), { method: "PUT", body: this.requireObject(body, "subdomain body") });
123
+ }
124
+
125
+ getScriptSubdomain(scriptName: string) {
126
+ return this.fetchPath(`${this.scriptPath(scriptName)}/subdomain`, { method: "GET" });
127
+ }
128
+
129
+ updateScriptSubdomain(scriptName: string, body: JsonObject) {
130
+ return this.fetchPath(`${this.scriptPath(scriptName)}/subdomain`, { method: "POST", body: this.requireObject(body, "script subdomain body") });
131
+ }
132
+
133
+ listRoutes(zoneId: string) {
134
+ return this.fetchPath(`/zones/${this.segment(zoneId, "zoneId")}/workers/routes`, { method: "GET" });
135
+ }
136
+
137
+ getRoute(zoneId: string, routeId: string) {
138
+ return this.fetchPath(`/zones/${this.segment(zoneId, "zoneId")}/workers/routes/${this.segment(routeId, "routeId")}`, { method: "GET" });
139
+ }
140
+
141
+ createRoute(zoneId: string, body: JsonObject) {
142
+ return this.fetchPath(`/zones/${this.segment(zoneId, "zoneId")}/workers/routes`, { method: "POST", body: this.requireObject(body, "route body") });
143
+ }
144
+
145
+ updateRoute(zoneId: string, routeId: string, body: JsonObject) {
146
+ return this.fetchPath(`/zones/${this.segment(zoneId, "zoneId")}/workers/routes/${this.segment(routeId, "routeId")}`, {
147
+ method: "PUT",
148
+ body: this.requireObject(body, "route body"),
149
+ });
150
+ }
151
+
152
+ deleteRoute(zoneId: string, routeId: string) {
153
+ return this.fetchPath(`/zones/${this.segment(zoneId, "zoneId")}/workers/routes/${this.segment(routeId, "routeId")}`, { method: "DELETE" });
154
+ }
155
+
156
+ getAccountSettings() {
157
+ return this.fetchPath(this.accountPath("/workers/account-settings"), { method: "GET" });
158
+ }
159
+
160
+ updateAccountSettings(body: JsonObject) {
161
+ return this.fetchPath(this.accountPath("/workers/account-settings"), { method: "PATCH", body: this.requireObject(body, "account settings body") });
162
+ }
163
+
164
+ listBetaWorkers(query: QueryParams = {}) {
165
+ return this.fetchPath(this.accountPath("/workers/workers"), { method: "GET", query });
166
+ }
167
+
168
+ createBetaWorker(body: JsonObject) {
169
+ return this.fetchPath(this.accountPath("/workers/workers"), { method: "POST", body: this.requireObject(body, "worker body") });
170
+ }
171
+
172
+ getBetaWorker(workerId: string) {
173
+ return this.fetchPath(this.accountPath(`/workers/workers/${this.segment(workerId, "workerId")}`), { method: "GET" });
174
+ }
175
+
176
+ deleteBetaWorker(workerId: string) {
177
+ return this.fetchPath(this.accountPath(`/workers/workers/${this.segment(workerId, "workerId")}`), { method: "DELETE" });
178
+ }
179
+
180
+ listBetaVersions(workerId: string) {
181
+ return this.fetchPath(this.accountPath(`/workers/workers/${this.segment(workerId, "workerId")}/versions`), { method: "GET" });
182
+ }
183
+
184
+ createBetaVersion(workerId: string, body: JsonObject) {
185
+ return this.fetchPath(this.accountPath(`/workers/workers/${this.segment(workerId, "workerId")}/versions`), {
186
+ method: "POST",
187
+ body: this.requireObject(body, "version body"),
188
+ });
189
+ }
190
+
191
+ getBetaVersion(workerId: string, versionId: string) {
192
+ return this.fetchPath(this.accountPath(`/workers/workers/${this.segment(workerId, "workerId")}/versions/${this.segment(versionId, "versionId")}`), {
193
+ method: "GET",
194
+ });
195
+ }
196
+
197
+ rawRequest<T = unknown>(method: HttpMethod, path: string, options: Omit<CloudflareWorkersRequestOptions, "method"> = {}) {
198
+ return this.fetchPath<T>(this.resolvePath(path), { ...options, method });
199
+ }
200
+
201
+ private async fetchPath<T>(path: string, options: CloudflareWorkersRequestOptions = {}): Promise<T> {
202
+ const { method = "GET", body, headers = {}, query, rawBody = false, responseType = "json" } = options;
203
+ const requestHeaders: Record<string, string> = {
204
+ Accept: responseType === "json" ? "application/json" : "*/*",
205
+ Authorization: `Bearer ${this.requireString(this.apiToken, "apiToken")}`,
206
+ ...headers,
207
+ };
208
+ const init: RequestInit = { method, headers: requestHeaders };
209
+ if (body !== undefined && method !== "GET") {
210
+ if (rawBody) {
211
+ init.body = this.toRawBody(body);
212
+ } else {
213
+ requestHeaders["Content-Type"] = requestHeaders["Content-Type"] || "application/json";
214
+ init.body = JSON.stringify(this.clean(body));
215
+ }
216
+ }
217
+
218
+ const controller = new AbortController();
219
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
220
+ try {
221
+ const response = await fetch(this.buildUrl(path, query), {
222
+ ...init,
223
+ signal: options.signal || controller.signal,
224
+ });
225
+ if (responseType === "arrayBuffer") {
226
+ if (!response.ok) throw new CloudflareWorkersApiError(await this.errorFromResponse(response), response.status);
227
+ return { status: response.status, headers: this.headersToObject(response.headers), body: await response.arrayBuffer() } as T;
228
+ }
229
+ const text = await response.text();
230
+ if (responseType === "text") {
231
+ if (!response.ok) throw new CloudflareWorkersApiError(this.errorMessage(this.parseBody(text, response.headers.get("content-type")), response.statusText, response.status), response.status);
232
+ return text as T;
233
+ }
234
+ const data = this.parseBody(text, response.headers.get("content-type"));
235
+ if (!response.ok) throw new CloudflareWorkersApiError(this.errorMessage(data, response.statusText, response.status), response.status, data);
236
+ if (data && typeof data === "object" && (data as Record<string, unknown>).success === false) {
237
+ throw new CloudflareWorkersApiError(this.errorMessage(data, "Cloudflare Workers request failed", response.status), response.status, data);
238
+ }
239
+ return data as T;
240
+ } catch (err) {
241
+ if (err instanceof CloudflareWorkersApiError) throw err;
242
+ if (err instanceof DOMException && err.name === "AbortError") throw new CloudflareWorkersApiError("Cloudflare Workers request timed out");
243
+ throw new CloudflareWorkersApiError(`Cloudflare Workers network error: ${err instanceof Error ? err.message : String(err)}`);
244
+ } finally {
245
+ clearTimeout(timeout);
246
+ }
247
+ }
248
+
249
+ private accountPath(path: string): string {
250
+ return `/accounts/${encodeURIComponent(this.requireString(this.accountId, "accountId"))}${path}`;
251
+ }
252
+
253
+ private scriptPath(scriptName: string): string {
254
+ return this.accountPath(`/workers/scripts/${this.segment(scriptName, "scriptName")}`);
255
+ }
256
+
257
+ private resolvePath(input: string): string {
258
+ const raw = input.trim();
259
+ if (!raw) throw new CloudflareWorkersApiError("Cloudflare Workers request path is required.");
260
+ if (/^https?:\/\//i.test(raw)) throw new CloudflareWorkersApiError("Cloudflare Workers paths must be relative.");
261
+ const path = raw.startsWith("/") ? raw : `/${raw}`;
262
+ if (/[?#]/.test(path) || /(^|\/)\.\.?(\/|$)|%2e|%2f/i.test(path)) {
263
+ throw new CloudflareWorkersApiError("Cloudflare Workers path must not contain query strings, fragments, or traversal.");
264
+ }
265
+ const accountWorkers = /^\/accounts\/[^/]+\/workers(?:\/(?:subdomain|account-settings|domains(?:\/[^/]+)?|scripts(?:\/[^/]+(?:\/(?:assets(?:\/upload)?|subdomain|schedules|tails(?:\/[^/]+)?|content|settings|deployments(?:\/[^/]+)?|versions(?:\/[^/]+)?|secrets(?:\/[^/]+)?|script-settings|version-settings))?)?|workers(?:\/[^/]+(?:\/versions(?:\/[^/]+)?)?)?|observability(?:\/[^/]+)*))?\/?$/;
266
+ const zoneRoutes = /^\/zones\/[^/]+\/workers\/routes(?:\/[^/]+)?\/?$/;
267
+ if (!accountWorkers.test(path) && !zoneRoutes.test(path)) throw new CloudflareWorkersApiError(`Raw Cloudflare Workers path is not allowed: ${path}`);
268
+ return path.replace(/\/$/, "");
269
+ }
270
+
271
+ private buildUrl(path: string, query?: QueryParams): string {
272
+ const url = new URL(`${this.baseUrl}${path}`);
273
+ if (query) {
274
+ for (const [key, value] of Object.entries(query)) this.appendQuery(url, key, value);
275
+ }
276
+ return url.toString();
277
+ }
278
+
279
+ private appendQuery(url: URL, key: string, value: QueryValue | QueryValue[]): void {
280
+ if (Array.isArray(value)) {
281
+ for (const item of value) this.appendQueryValue(url, key, item);
282
+ return;
283
+ }
284
+ this.appendQueryValue(url, key, value);
285
+ }
286
+
287
+ private appendQueryValue(url: URL, key: string, value: QueryValue): void {
288
+ if (value === undefined || value === null || value === "") return;
289
+ url.searchParams.append(key, value instanceof Date ? value.toISOString() : String(value));
290
+ }
291
+
292
+ private toRawBody(body: RequestBody): BodyInit {
293
+ if (body instanceof FormData) return body;
294
+ if (body instanceof Uint8Array) return new Uint8Array(body).buffer as ArrayBuffer;
295
+ if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer) return body;
296
+ return JSON.stringify(this.clean(body));
297
+ }
298
+
299
+ private blobPart(value: string | Uint8Array | ArrayBuffer | Blob): BlobPart {
300
+ if (value instanceof Uint8Array) return new Uint8Array(value).buffer as ArrayBuffer;
301
+ return value;
302
+ }
303
+
304
+ private parseBody(text: string, contentType: string | null): unknown {
305
+ if (!text) return {};
306
+ if (contentType?.includes("json") || text.trim().startsWith("{") || text.trim().startsWith("[")) {
307
+ try {
308
+ return JSON.parse(text) as unknown;
309
+ } catch {
310
+ return { raw: text };
311
+ }
312
+ }
313
+ return { raw: text };
314
+ }
315
+
316
+ private async errorFromResponse(response: Response): Promise<string> {
317
+ const text = await response.text();
318
+ return this.errorMessage(this.parseBody(text, response.headers.get("content-type")), response.statusText, response.status);
319
+ }
320
+
321
+ private errorMessage(data: unknown, statusText: string, status: number): string {
322
+ if (data && typeof data === "object") {
323
+ const record = data as Record<string, unknown>;
324
+ const errors = record.errors;
325
+ if (Array.isArray(errors) && errors.length > 0) {
326
+ const messages = errors.map((error) => typeof error === "object" && error ? (error as Record<string, unknown>).message : error).filter(Boolean);
327
+ if (messages.length > 0) return `Cloudflare Workers: ${messages.join(", ")}`;
328
+ }
329
+ for (const key of ["message", "error", "detail", "title"]) {
330
+ const value = record[key];
331
+ if (typeof value === "string" && value.trim()) return `Cloudflare Workers: ${value}`;
332
+ }
333
+ }
334
+ return `Cloudflare Workers request failed (${status}${statusText ? ` ${statusText}` : ""})`;
335
+ }
336
+
337
+ private headersToObject(headers: Headers): Record<string, string> {
338
+ const output: Record<string, string> = {};
339
+ headers.forEach((value, key) => {
340
+ output[key] = value;
341
+ });
342
+ return output;
343
+ }
344
+
345
+ private clean(value: unknown): unknown {
346
+ if (value instanceof Date) return value.toISOString();
347
+ if (Array.isArray(value)) return value.map((item) => this.clean(item));
348
+ if (value && typeof value === "object" && !(value instanceof Blob) && !(value instanceof ArrayBuffer) && !(value instanceof Uint8Array) && !(value instanceof FormData)) {
349
+ const output: Record<string, unknown> = {};
350
+ for (const [key, item] of Object.entries(value)) {
351
+ if (item !== undefined) output[key] = this.clean(item);
352
+ }
353
+ return output;
354
+ }
355
+ return value;
356
+ }
357
+
358
+ private cleanBaseUrl(input: string): string {
359
+ const trimmed = input.trim();
360
+ if (!trimmed) throw new CloudflareWorkersApiError("Cloudflare base URL is required.");
361
+ const url = new URL(trimmed);
362
+ if (url.protocol !== "https:") throw new CloudflareWorkersApiError("Cloudflare base URL must use https.");
363
+ url.hash = "";
364
+ url.search = "";
365
+ return url.toString().replace(/\/$/, "");
366
+ }
367
+
368
+ private segment(value: string, label: string): string {
369
+ return encodeURIComponent(this.requireString(value, label));
370
+ }
371
+
372
+ private requireObject(body: JsonObject, label: string): JsonObject {
373
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length === 0) {
374
+ throw new CloudflareWorkersApiError(`Cloudflare Workers ${label} must be a non-empty JSON object.`);
375
+ }
376
+ return body;
377
+ }
378
+
379
+ private requireString(value: string | undefined, label: string): string {
380
+ const trimmed = value?.trim();
381
+ if (!trimmed) throw new CloudflareWorkersApiError(`Cloudflare Workers ${label} is required.`);
382
+ return trimmed;
383
+ }
384
+ }
385
+
386
+ export { CloudflareWorkersClient as CloudflareWorkers };
@@ -0,0 +1 @@
1
+ export { CloudflareWorkers, CloudflareWorkersClient } from "./client";