@zackbart/connecta 0.15.0 → 0.15.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.15.1 — 2026-08-12
6
+
7
+ The Cloudflare connection now supports legacy user-scoped Global API Keys as
8
+ an explicit authentication mode. Scoped API tokens remain the default. The
9
+ three guarded raw tools still cover the full v4 path space without adding one
10
+ tool per endpoint, and ordinary JSON responses such as GraphQL results now
11
+ survive that path intact.
12
+
13
+ Nothing breaks for existing deployments. They keep their current API token,
14
+ credential form, verification tool, and 55-tool surface. A deployment that
15
+ needs the legacy scheme opts in and stores the Cloudflare user email and Global
16
+ API Key as separate encrypted fields.
17
+
18
+ ### Added
19
+
20
+ - **Legacy Cloudflare Global API Key authentication.** Set
21
+ `authentication: "globalApiKey"` to send operator-managed `X-Auth-Email` and
22
+ `X-Auth-Key` headers. `verify_global_api_key` checks the pair through
23
+ Cloudflare's authenticated user endpoint, and raw calls cannot replace either
24
+ connector-owned header.
25
+
26
+ ### Fixed
27
+
28
+ - **Raw Cloudflare calls preserve non-envelope JSON.** Endpoints such as
29
+ `/graphql` return their complete JSON document instead of an undefined
30
+ `result`.
31
+
5
32
  ## 0.15.0 — 2026-08-10
6
33
 
7
34
  Discovery now keeps strong action/object near-matches visible beside complete
@@ -1,6 +1,8 @@
1
1
  import type { Connector, ConnectorCredentialConfig } from "../types.js";
2
2
  /** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
3
3
  export declare const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
4
+ /** Authentication schemes accepted by Cloudflare's v4 API. */
5
+ export type CloudflareAuthentication = "apiToken" | "globalApiKey";
4
6
  /**
5
7
  * Every DNS record type the records API accepts, for filtering a list.
6
8
  * Enumerated in the schema so an agent picks a legal type without reading
@@ -40,7 +42,9 @@ export interface CloudflareOptions {
40
42
  zoneId?: string;
41
43
  /** API base override for a proxy or a test double. Defaults to the v4 API. */
42
44
  baseUrl?: string;
43
- /** Credential presentation override; the token is always operator-managed. */
45
+ /** Authentication scheme. Defaults to the recommended scoped API token. */
46
+ authentication?: CloudflareAuthentication;
47
+ /** Credential presentation override; credentials are always operator-managed. */
44
48
  credential?: ConnectorCredentialConfig;
45
49
  /** Account-specific conventions appended to the maintained provider guide. */
46
50
  instructions?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/providers/cloudflare.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,SAAS,EAGT,yBAAyB,EAE1B,MAAM,aAAa,CAAC;AAErB,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,kLAsB9B,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mCAAmC,yEAStC,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA0jHD,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,SAAS,CAiD5E"}
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/providers/cloudflare.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,SAAS,EAGT,yBAAyB,EAE1B,MAAM,aAAa,CAAC;AAErB,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,yCAAyC,CAAC;AAE1E,8DAA8D;AAC9D,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,cAAc,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,kLAsB9B,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mCAAmC,yEAStC,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,wBAAwB,CAAC;IAC1C,iFAAiF;IACjF,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAmsHD,mDAAmD;AACnD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,SAAS,CAuG5E"}
@@ -91,11 +91,52 @@ function admissionPolicy(maxConcurrency) {
91
91
  ],
92
92
  };
93
93
  }
94
- const DEFAULT_CREDENTIAL = {
94
+ const API_TOKEN_CREDENTIAL = {
95
95
  label: "Cloudflare API token",
96
96
  description: "A scoped API token (My Profile → API Tokens → Create Token), not a Global API Key. Grant only the permissions the deployment needs: zone-scoped \"Zone Read\", \"Zone Settings Write\", \"DNS Write\", \"Cache Purge\", and the phase-specific Rules product Read permissions as needed; account-scoped \"Workers Scripts Read/Write\", \"Workers KV Storage Read/Write\", \"Workers R2 Storage Read/Write\", or \"Cloudflare Pages Read/Write\" for the platform tools.",
97
97
  placeholder: "Paste API token",
98
98
  };
99
+ const GLOBAL_API_KEY_CREDENTIAL = {
100
+ label: "Cloudflare Global API Key",
101
+ description: "Legacy user-scoped authentication. The key has the same access as its Cloudflare user across every account and zone that user can reach. Prefer a scoped API token when possible.",
102
+ fields: [
103
+ {
104
+ name: "email",
105
+ label: "Account email",
106
+ description: "The verified email address for the Cloudflare user that owns the Global API Key.",
107
+ placeholder: "you@example.com",
108
+ inputType: "email",
109
+ },
110
+ {
111
+ name: "apiKey",
112
+ label: "Global API Key",
113
+ description: "The legacy Global API Key from My Profile → API Tokens.",
114
+ placeholder: "Paste Global API Key",
115
+ inputType: "password",
116
+ },
117
+ ],
118
+ };
119
+ function credentialConfig(authentication, override) {
120
+ if (authentication === "apiToken") {
121
+ const credential = override ?? API_TOKEN_CREDENTIAL;
122
+ if (credential.fields?.length) {
123
+ throw new Error("cloudflare() API token authentication requires a single-value credential.");
124
+ }
125
+ return credential;
126
+ }
127
+ const credential = override
128
+ ? {
129
+ ...GLOBAL_API_KEY_CREDENTIAL,
130
+ ...override,
131
+ fields: override.fields ?? GLOBAL_API_KEY_CREDENTIAL.fields,
132
+ }
133
+ : GLOBAL_API_KEY_CREDENTIAL;
134
+ const fields = credential.fields?.map((field) => field.name).sort();
135
+ if (fields?.join(",") !== "apiKey,email") {
136
+ throw new Error('cloudflare() Global API Key authentication requires credential fields named "email" and "apiKey".');
137
+ }
138
+ return credential;
139
+ }
99
140
  function asRecord(value) {
100
141
  return value && typeof value === "object" && !Array.isArray(value)
101
142
  ? value
@@ -188,7 +229,7 @@ function failureFor(status, headers, errors) {
188
229
  }
189
230
  const authCoded = [...codes].some((code) => AUTH_ERROR_CODES.has(code));
190
231
  if (status === 401 || status === 403 || authCoded) {
191
- return new ConnectorCallError("auth_required", `Cloudflare rejected the API token (HTTP ${status}). ${detail} Check that the token is valid and carries the permission this call needs.`);
232
+ return new ConnectorCallError("auth_required", `Cloudflare rejected the configured credential (HTTP ${status}). ${detail} Check that it is valid and has permission to access this resource.`);
192
233
  }
193
234
  if (status === 400 || status === 409 || status === 422) {
194
235
  return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
@@ -201,12 +242,25 @@ function failureFor(status, headers, errors) {
201
242
  }
202
243
  return new ConnectorCallError("connector_call_failed", `Cloudflare request failed (HTTP ${status}). ${detail}`);
203
244
  }
204
- async function readToken(ctx) {
245
+ const AUTHENTICATION_CONTEXT = Symbol("cloudflareAuthentication");
246
+ function withAuthentication(ctx, authentication) {
247
+ return { ...ctx, [AUTHENTICATION_CONTEXT]: authentication };
248
+ }
249
+ async function readAuthenticationHeaders(ctx) {
250
+ if (ctx[AUTHENTICATION_CONTEXT] === "globalApiKey") {
251
+ const values = await ctx.credential?.getAll();
252
+ const email = values?.["email"];
253
+ const apiKey = values?.["apiKey"];
254
+ if (!email || !apiKey) {
255
+ throw new ConnectorCallError("auth_required", "No Cloudflare Global API Key and account email are configured for this connector. An operator must add both before any call can run.");
256
+ }
257
+ return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
258
+ }
205
259
  const token = await ctx.credential?.get();
206
260
  if (!token) {
207
261
  throw new ConnectorCallError("auth_required", "No Cloudflare API token is configured for this connector. An operator must add one before any call can run.");
208
262
  }
209
- return token;
263
+ return { Authorization: `Bearer ${token}` };
210
264
  }
211
265
  function buildUrl(base, spec) {
212
266
  const url = new URL(`${base.replace(/\/+$/, "")}${spec.path}`);
@@ -218,7 +272,7 @@ function buildUrl(base, spec) {
218
272
  return url.toString();
219
273
  }
220
274
  async function fetchCloudflare(base, spec, ctx) {
221
- const token = await readToken(ctx);
275
+ const authenticationHeaders = await readAuthenticationHeaders(ctx);
222
276
  if (spec.body !== undefined && spec.rawBody !== undefined) {
223
277
  throw new Error("A Cloudflare request cannot have both JSON and raw bodies.");
224
278
  }
@@ -227,7 +281,7 @@ async function fetchCloudflare(base, spec, ctx) {
227
281
  response = await fetch(buildUrl(base, spec), {
228
282
  method: spec.method,
229
283
  headers: {
230
- Authorization: `Bearer ${token}`,
284
+ ...authenticationHeaders,
231
285
  Accept: "application/json",
232
286
  ...(spec.body !== undefined
233
287
  ? { "Content-Type": "application/json" }
@@ -263,9 +317,16 @@ async function callCloudflare(base, spec, ctx) {
263
317
  if (!response.ok || envelope.success === false) {
264
318
  throw failureFor(response.status, response.headers, errors);
265
319
  }
320
+ const isV4Envelope = "success" in envelope ||
321
+ "result" in envelope ||
322
+ "result_info" in envelope ||
323
+ "messages" in envelope;
266
324
  return {
267
- result: envelope.result,
268
- resultInfo: envelope.result_info,
325
+ // `/graphql` and a small number of product APIs return ordinary JSON
326
+ // instead of the standard v4 envelope. Preserve that document whole so
327
+ // the raw tools cover them too.
328
+ result: isV4Envelope ? envelope.result : envelope,
329
+ resultInfo: isV4Envelope ? envelope.result_info : undefined,
269
330
  };
270
331
  }
271
332
  function base64FromBytes(bytes) {
@@ -787,6 +848,8 @@ function headersFromArgs(value) {
787
848
  const headers = {};
788
849
  const forbidden = new Set([
789
850
  "authorization",
851
+ "x-auth-email",
852
+ "x-auth-key",
790
853
  "cookie",
791
854
  "host",
792
855
  "content-length",
@@ -924,53 +987,82 @@ const R2_OBJECT_SCHEMA = {
924
987
  },
925
988
  required: ["key"],
926
989
  };
927
- function buildTools(scope) {
990
+ function buildTools(scope, authentication) {
928
991
  const { base } = scope;
929
992
  const zoneArg = (args) => requireScope(args["zoneId"], scope.zoneId, "zoneId");
930
993
  const accountArg = (args) => requireScope(args["accountId"], scope.accountId, "accountId");
931
994
  const readOnly = { readOnlyHint: true, destructiveHint: false };
932
- return [
933
- {
934
- name: "verify_api_token",
935
- description: "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.",
936
- annotations: readOnly,
937
- inputSchema: {
938
- type: "object",
939
- properties: {},
940
- required: [],
941
- additionalProperties: false,
942
- },
943
- outputSchema: {
944
- type: "object",
945
- properties: {
946
- id: { type: "string" },
947
- status: {
948
- type: "string",
949
- description: "\"active\" for a usable token.",
995
+ const tools = [
996
+ authentication === "apiToken"
997
+ ? {
998
+ name: "verify_api_token",
999
+ description: "Verify the configured Cloudflare API token and report its status. Use this first when any other tool fails with an authentication error, to separate a bad token from a missing permission.",
1000
+ annotations: readOnly,
1001
+ inputSchema: {
1002
+ type: "object",
1003
+ properties: {},
1004
+ required: [],
1005
+ additionalProperties: false,
1006
+ },
1007
+ outputSchema: {
1008
+ type: "object",
1009
+ properties: {
1010
+ id: { type: "string" },
1011
+ status: {
1012
+ type: "string",
1013
+ description: "\"active\" for a usable token.",
1014
+ },
1015
+ notBefore: { type: "string" },
1016
+ expiresOn: { type: "string" },
1017
+ },
1018
+ required: ["status"],
1019
+ },
1020
+ handler: async (_args, ctx) => {
1021
+ const { result } = await callCloudflare(base, { method: "GET", path: "/user/tokens/verify" }, ctx);
1022
+ const token = asRecord(result);
1023
+ return {
1024
+ id: token["id"],
1025
+ status: token["status"],
1026
+ ...(token["not_before"] !== undefined
1027
+ ? { notBefore: token["not_before"] }
1028
+ : {}),
1029
+ ...(token["expires_on"] !== undefined
1030
+ ? { expiresOn: token["expires_on"] }
1031
+ : {}),
1032
+ };
1033
+ },
1034
+ }
1035
+ : {
1036
+ name: "verify_global_api_key",
1037
+ description: "Verify the configured Cloudflare Global API Key and account email by retrieving the authenticated user. Use this first when another tool fails with an authentication error.",
1038
+ annotations: readOnly,
1039
+ inputSchema: {
1040
+ type: "object",
1041
+ properties: {},
1042
+ required: [],
1043
+ additionalProperties: false,
1044
+ },
1045
+ outputSchema: {
1046
+ type: "object",
1047
+ properties: {
1048
+ id: { type: "string" },
1049
+ email: { type: "string" },
1050
+ status: {
1051
+ type: "string",
1052
+ description: "\"active\" when Cloudflare accepts the email and key.",
1053
+ },
950
1054
  },
951
- notBefore: { type: "string" },
952
- expiresOn: { type: "string" },
1055
+ required: ["email", "status"],
1056
+ },
1057
+ handler: async (_args, ctx) => {
1058
+ const { result } = await callCloudflare(base, { method: "GET", path: "/user" }, ctx);
1059
+ const user = asRecord(result);
1060
+ return { id: user["id"], email: user["email"], status: "active" };
953
1061
  },
954
- required: ["status"],
955
- },
956
- handler: async (_args, ctx) => {
957
- const { result } = await callCloudflare(base, { method: "GET", path: "/user/tokens/verify" }, ctx);
958
- const token = asRecord(result);
959
- return {
960
- id: token["id"],
961
- status: token["status"],
962
- ...(token["not_before"] !== undefined
963
- ? { notBefore: token["not_before"] }
964
- : {}),
965
- ...(token["expires_on"] !== undefined
966
- ? { expiresOn: token["expires_on"] }
967
- : {}),
968
- };
969
1062
  },
970
- },
971
1063
  {
972
1064
  name: "cloudflare_api_get",
973
- description: "Call any GET endpoint under Cloudflare's v4 API with this connector's token. Use a named tool when one exists; use this read-only escape hatch for Images, Stream, Email Routing, D1, Queues, Access, Tunnels, Analytics, and newer product endpoints the curated surface does not yet name.",
1065
+ description: "Call any GET endpoint under Cloudflare's v4 API with this connector's credential. Use a named tool when one exists; use this read-only escape hatch for Images, Stream, Email Routing, D1, Queues, Access, Tunnels, Analytics, and newer product endpoints the curated surface does not yet name.",
974
1066
  annotations: readOnly,
975
1067
  inputSchema: {
976
1068
  type: "object",
@@ -1030,7 +1122,7 @@ function buildTools(scope) {
1030
1122
  },
1031
1123
  {
1032
1124
  name: "cloudflare_api_mutate",
1033
- description: "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's token. This is the approval-gated escape hatch for managing Cloudflare products without waiting for a named tool. It does not support multipart or binary uploads.",
1125
+ description: "Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's credential. This is the approval-gated escape hatch for managing Cloudflare products without waiting for a named tool. It does not support multipart or binary uploads.",
1034
1126
  annotations: { readOnlyHint: false, destructiveHint: true },
1035
1127
  inputSchema: {
1036
1128
  type: "object",
@@ -3132,8 +3224,12 @@ function buildTools(scope) {
3132
3224
  },
3133
3225
  },
3134
3226
  ];
3227
+ return tools.map((tool) => ({
3228
+ ...tool,
3229
+ handler: (args, ctx) => tool.handler(args, withAuthentication(ctx, authentication)),
3230
+ }));
3135
3231
  }
3136
- function usageGuide(purpose, scope, instructions) {
3232
+ function usageGuide(purpose, scope, instructions, authentication) {
3137
3233
  const accountInstructions = instructions?.trim();
3138
3234
  const zoneLine = scope.zoneId
3139
3235
  ? `This connector defaults to zone \`${scope.zoneId}\`; omit \`zoneId\` unless the request names a different domain.`
@@ -3141,6 +3237,9 @@ function usageGuide(purpose, scope, instructions) {
3141
3237
  const accountLine = scope.accountId
3142
3238
  ? `It defaults to account \`${scope.accountId}\`; omit \`accountId\` unless the request names a different account.`
3143
3239
  : "It declares no default account. `list_accounts` supplies the `accountId` the Workers, KV, R2, and Pages tools need.";
3240
+ const authenticationLine = authentication === "apiToken"
3241
+ ? "The API token is operator-managed and scoped by permission. An `auth_required` failure means the token is missing, invalid, or lacks that call's permission. Call `verify_api_token` first."
3242
+ : "The Global API Key and account email are operator-managed. The key has the same access as its Cloudflare user. An `auth_required` failure means one field is missing, the pair is invalid, or the user lacks access. Call `verify_global_api_key` first.";
3144
3243
  return `# Cloudflare usage
3145
3244
 
3146
3245
  Account purpose: ${purpose}
@@ -3148,11 +3247,11 @@ Account purpose: ${purpose}
3148
3247
  - ${zoneLine}
3149
3248
  - ${accountLine}
3150
3249
  - Prefer a named tool: its schema is complete, projected, and enough to call it without provider documentation. For an operation without a named tool, use \`cloudflare_api_get\` for GET, \`cloudflare_api_mutate\` for JSON POST/PUT/PATCH/DELETE, or \`cloudflare_api_upload\` for raw and multipart content. Raw tools take a path below \`/client/v4\`; their argument schemas are complete, but endpoint-specific query, header, and body fields come from Cloudflare's API reference. Use \`headers\` for endpoint-specific controls such as \`cf-r2-jurisdiction\`, ETags, and object metadata; authentication, host, content type, and request framing remain connector-owned.
3151
- - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The API token remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3250
+ - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The configured Cloudflare credential remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3152
3251
  - Useful raw paths include \`/accounts/{accountId}/images/v1\` (Images), \`/accounts/{accountId}/stream\` (Stream), \`/zones/{zoneId}/email/routing/rules\` (Email Routing), \`/accounts/{accountId}/d1/database\` (D1), and \`/accounts/{accountId}/queues\` (Queues). On GET, use \`responseType: "text"\` or \`"base64"\` for non-JSON content. Direct-upload endpoints can issue upload URLs; \`cloudflare_api_upload\` can also send explicit text, base64 bytes, or multipart fields/files.
3153
3252
  - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. \`list_zone_rulesets\`, \`list_r2_buckets\`, \`list_r2_objects\`, and \`list_kv_keys\` instead return \`nextCursor\`; \`list_worker_scripts\` is unpaginated.
3154
3253
  - Results are projected to the fields that identify and describe a resource. Pass \`raw: true\` on a read when you genuinely need a field the projection drops.
3155
- - The API token is operator-managed and scoped by permission, not by role. An \`auth_required\` failure means the token is missing, invalid, or lacks that call's permission — it is never fixed by retrying. Call \`verify_api_token\` to tell a dead token from a missing permission, then report which permission is needed rather than trying other tools.
3254
+ - ${authenticationLine}
3156
3255
  - A \`rate_limited\` failure carries the wait window. Cloudflare's limit is 1,200 requests per five minutes per user, counted across the dashboard and every token, so do not fan out speculatively; filter server-side with \`name\`, \`type\`, and \`content\` instead of listing everything and filtering locally.
3157
3256
  - Named creates that only add a resource are write-routed without claiming destruction. Updates, overwrites, deletes, rollbacks, cache purges, \`cloudflare_api_mutate\`, and \`cloudflare_api_upload\` are destructive. Read current state before changing it, and prefer a targeted \`purge_cache\` over \`everything\`.
3158
3257
  ${accountInstructions
@@ -3169,6 +3268,10 @@ export function cloudflare(id, options) {
3169
3268
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
3170
3269
  throw new Error("cloudflare() maxConcurrency must be a positive integer.");
3171
3270
  }
3271
+ const authentication = options.authentication ?? "apiToken";
3272
+ if (authentication !== "apiToken" && authentication !== "globalApiKey") {
3273
+ throw new Error('cloudflare() authentication must be "apiToken" or "globalApiKey".');
3274
+ }
3172
3275
  const scope = {
3173
3276
  base: options.baseUrl?.trim() || CLOUDFLARE_API_BASE,
3174
3277
  accountId: options.accountId?.trim() || undefined,
@@ -3177,34 +3280,64 @@ export function cloudflare(id, options) {
3177
3280
  return api(id, {
3178
3281
  title: options.title ?? "Cloudflare",
3179
3282
  description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
3180
- credential: options.credential ?? DEFAULT_CREDENTIAL,
3283
+ credential: credentialConfig(authentication, options.credential),
3181
3284
  callAdmission: admissionPolicy(maxConcurrency),
3182
- usageGuide: usageGuide(purpose, scope, options.instructions),
3285
+ usageGuide: usageGuide(purpose, scope, options.instructions, authentication),
3183
3286
  // The schemas are hand-written and closed; a schema that cannot be
3184
3287
  // enforced is a bug in this file, not input to pass through.
3185
3288
  strictValidation: true,
3186
3289
  ...(options.maxResultBytes !== undefined
3187
3290
  ? { maxResultBytes: options.maxResultBytes }
3188
3291
  : {}),
3189
- tools: buildTools(scope),
3190
- async testCredential(value, ctx) {
3191
- try {
3192
- const { result } = await callCloudflare(scope.base, { method: "GET", path: "/user/tokens/verify" }, {
3193
- ...ctx,
3194
- credential: { get: async () => value, getAll: async () => ({ value }) },
3195
- });
3196
- const status = asRecord(result)["status"];
3197
- return status === "active"
3198
- ? { ok: true, message: "Token verified: active." }
3199
- : { ok: false, message: `Token status is "${String(status)}".` };
3200
- }
3201
- catch (error) {
3202
- return {
3203
- ok: false,
3204
- message: error instanceof Error ? error.message : String(error),
3205
- };
3292
+ tools: buildTools(scope, authentication),
3293
+ ...(authentication === "apiToken"
3294
+ ? {
3295
+ async testCredential(value, ctx) {
3296
+ try {
3297
+ const { result } = await callCloudflare(scope.base, { method: "GET", path: "/user/tokens/verify" }, withAuthentication({
3298
+ ...ctx,
3299
+ credential: {
3300
+ get: async () => value,
3301
+ getAll: async () => ({ value }),
3302
+ },
3303
+ }, authentication));
3304
+ const status = asRecord(result)["status"];
3305
+ return status === "active"
3306
+ ? { ok: true, message: "Token verified: active." }
3307
+ : { ok: false, message: `Token status is "${String(status)}".` };
3308
+ }
3309
+ catch (error) {
3310
+ return {
3311
+ ok: false,
3312
+ message: error instanceof Error ? error.message : String(error),
3313
+ };
3314
+ }
3315
+ },
3206
3316
  }
3207
- },
3317
+ : {
3318
+ async testCredentials(values, ctx) {
3319
+ try {
3320
+ const { result } = await callCloudflare(scope.base, { method: "GET", path: "/user" }, withAuthentication({
3321
+ ...ctx,
3322
+ credential: {
3323
+ get: async (field) => field ? values[field] ?? null : null,
3324
+ getAll: async () => values,
3325
+ },
3326
+ }, authentication));
3327
+ const email = asRecord(result)["email"];
3328
+ return {
3329
+ ok: true,
3330
+ message: `Global API Key verified for ${String(email)}.`,
3331
+ };
3332
+ }
3333
+ catch (error) {
3334
+ return {
3335
+ ok: false,
3336
+ message: error instanceof Error ? error.message : String(error),
3337
+ };
3338
+ }
3339
+ },
3340
+ }),
3208
3341
  });
3209
3342
  }
3210
3343
  //# sourceMappingURL=cloudflare.js.map