@zackbart/connecta 0.14.2 → 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 +89 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +104 -56
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +8 -2
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +58 -9
- package/dist/catalog.js.map +1 -1
- package/dist/connectors/api.d.ts +4 -4
- package/dist/connectors/api.js +4 -4
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +8 -7
- package/dist/execute.js.map +1 -1
- package/dist/meta-tools.js +1 -1
- package/dist/meta-tools.js.map +1 -1
- package/dist/providers/cloudflare.d.ts +5 -1
- package/dist/providers/cloudflare.d.ts.map +1 -1
- package/dist/providers/cloudflare.js +203 -70
- package/dist/providers/cloudflare.js.map +1 -1
- package/dist/providers/linear.d.ts.map +1 -1
- package/dist/providers/linear.js +4 -3
- package/dist/providers/linear.js.map +1 -1
- package/dist/providers/mixpanel.d.ts.map +1 -1
- package/dist/providers/mixpanel.js +4 -3
- package/dist/providers/mixpanel.js.map +1 -1
- package/dist/providers/stripe.d.ts.map +1 -1
- package/dist/providers/stripe.js +4 -3
- package/dist/providers/stripe.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +97 -17
- package/dist/validate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/cloudflare.md +37 -19
- package/documentation/code-mode.md +9 -9
- package/documentation/connectors.md +9 -7
- package/documentation/linear.md +6 -7
- package/documentation/meta-tools.md +56 -13
- package/documentation/mixpanel.md +6 -7
- package/documentation/stripe.md +12 -12
- package/ethos.md +2 -1
- package/package.json +1 -1
- package/src/catalog-service.ts +93 -29
- package/src/catalog.ts +71 -8
- package/src/connectors/api.ts +4 -4
- package/src/execute.ts +8 -7
- package/src/meta-tools.ts +1 -1
- package/src/providers/cloudflare.ts +275 -79
- package/src/providers/linear.ts +4 -3
- package/src/providers/mixpanel.ts +4 -3
- package/src/providers/stripe.ts +4 -3
- package/src/validate.ts +125 -20
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
|
@@ -28,6 +28,9 @@ import type {
|
|
|
28
28
|
/** Cloudflare's v4 REST base. Override only for a proxy or a test double. */
|
|
29
29
|
export const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
30
30
|
|
|
31
|
+
/** Authentication schemes accepted by Cloudflare's v4 API. */
|
|
32
|
+
export type CloudflareAuthentication = "apiToken" | "globalApiKey";
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
35
|
* Every DNS record type the records API accepts, for filtering a list.
|
|
33
36
|
* Enumerated in the schema so an agent picks a legal type without reading
|
|
@@ -100,7 +103,9 @@ export interface CloudflareOptions {
|
|
|
100
103
|
zoneId?: string;
|
|
101
104
|
/** API base override for a proxy or a test double. Defaults to the v4 API. */
|
|
102
105
|
baseUrl?: string;
|
|
103
|
-
/**
|
|
106
|
+
/** Authentication scheme. Defaults to the recommended scoped API token. */
|
|
107
|
+
authentication?: CloudflareAuthentication;
|
|
108
|
+
/** Credential presentation override; credentials are always operator-managed. */
|
|
104
109
|
credential?: ConnectorCredentialConfig;
|
|
105
110
|
/** Account-specific conventions appended to the maintained provider guide. */
|
|
106
111
|
instructions?: string;
|
|
@@ -132,13 +137,65 @@ function admissionPolicy(maxConcurrency: number): ConnectorCallAdmissionPolicy {
|
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
const
|
|
140
|
+
const API_TOKEN_CREDENTIAL: ConnectorCredentialConfig = {
|
|
136
141
|
label: "Cloudflare API token",
|
|
137
142
|
description:
|
|
138
143
|
"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.",
|
|
139
144
|
placeholder: "Paste API token",
|
|
140
145
|
};
|
|
141
146
|
|
|
147
|
+
const GLOBAL_API_KEY_CREDENTIAL: ConnectorCredentialConfig = {
|
|
148
|
+
label: "Cloudflare Global API Key",
|
|
149
|
+
description:
|
|
150
|
+
"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.",
|
|
151
|
+
fields: [
|
|
152
|
+
{
|
|
153
|
+
name: "email",
|
|
154
|
+
label: "Account email",
|
|
155
|
+
description: "The verified email address for the Cloudflare user that owns the Global API Key.",
|
|
156
|
+
placeholder: "you@example.com",
|
|
157
|
+
inputType: "email",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "apiKey",
|
|
161
|
+
label: "Global API Key",
|
|
162
|
+
description: "The legacy Global API Key from My Profile → API Tokens.",
|
|
163
|
+
placeholder: "Paste Global API Key",
|
|
164
|
+
inputType: "password",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function credentialConfig(
|
|
170
|
+
authentication: CloudflareAuthentication,
|
|
171
|
+
override: ConnectorCredentialConfig | undefined,
|
|
172
|
+
): ConnectorCredentialConfig {
|
|
173
|
+
if (authentication === "apiToken") {
|
|
174
|
+
const credential = override ?? API_TOKEN_CREDENTIAL;
|
|
175
|
+
if (credential.fields?.length) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"cloudflare() API token authentication requires a single-value credential.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return credential;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const credential = override
|
|
184
|
+
? {
|
|
185
|
+
...GLOBAL_API_KEY_CREDENTIAL,
|
|
186
|
+
...override,
|
|
187
|
+
fields: override.fields ?? GLOBAL_API_KEY_CREDENTIAL.fields!,
|
|
188
|
+
}
|
|
189
|
+
: GLOBAL_API_KEY_CREDENTIAL;
|
|
190
|
+
const fields = credential.fields?.map((field) => field.name).sort();
|
|
191
|
+
if (fields?.join(",") !== "apiKey,email") {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'cloudflare() Global API Key authentication requires credential fields named "email" and "apiKey".',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return credential;
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
// --- Cloudflare's response envelope -----------------------------------------
|
|
143
200
|
|
|
144
201
|
interface CloudflareEnvelopeError {
|
|
@@ -273,7 +330,7 @@ function failureFor(
|
|
|
273
330
|
if (status === 401 || status === 403 || authCoded) {
|
|
274
331
|
return new ConnectorCallError(
|
|
275
332
|
"auth_required",
|
|
276
|
-
`Cloudflare rejected the
|
|
333
|
+
`Cloudflare rejected the configured credential (HTTP ${status}). ${detail} Check that it is valid and has permission to access this resource.`,
|
|
277
334
|
);
|
|
278
335
|
}
|
|
279
336
|
if (status === 400 || status === 409 || status === 422) {
|
|
@@ -316,7 +373,35 @@ interface CloudflareResponse {
|
|
|
316
373
|
resultInfo: CloudflareResultInfo | undefined;
|
|
317
374
|
}
|
|
318
375
|
|
|
319
|
-
|
|
376
|
+
const AUTHENTICATION_CONTEXT = Symbol("cloudflareAuthentication");
|
|
377
|
+
|
|
378
|
+
type CloudflareContext = ConnectorContext & {
|
|
379
|
+
[AUTHENTICATION_CONTEXT]?: CloudflareAuthentication;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function withAuthentication(
|
|
383
|
+
ctx: ConnectorContext,
|
|
384
|
+
authentication: CloudflareAuthentication,
|
|
385
|
+
): CloudflareContext {
|
|
386
|
+
return { ...ctx, [AUTHENTICATION_CONTEXT]: authentication };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function readAuthenticationHeaders(
|
|
390
|
+
ctx: CloudflareContext,
|
|
391
|
+
): Promise<Record<string, string>> {
|
|
392
|
+
if (ctx[AUTHENTICATION_CONTEXT] === "globalApiKey") {
|
|
393
|
+
const values = await ctx.credential?.getAll();
|
|
394
|
+
const email = values?.["email"];
|
|
395
|
+
const apiKey = values?.["apiKey"];
|
|
396
|
+
if (!email || !apiKey) {
|
|
397
|
+
throw new ConnectorCallError(
|
|
398
|
+
"auth_required",
|
|
399
|
+
"No Cloudflare Global API Key and account email are configured for this connector. An operator must add both before any call can run.",
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return { "X-Auth-Email": email, "X-Auth-Key": apiKey };
|
|
403
|
+
}
|
|
404
|
+
|
|
320
405
|
const token = await ctx.credential?.get();
|
|
321
406
|
if (!token) {
|
|
322
407
|
throw new ConnectorCallError(
|
|
@@ -324,7 +409,7 @@ async function readToken(ctx: ConnectorContext): Promise<string> {
|
|
|
324
409
|
"No Cloudflare API token is configured for this connector. An operator must add one before any call can run.",
|
|
325
410
|
);
|
|
326
411
|
}
|
|
327
|
-
return token;
|
|
412
|
+
return { Authorization: `Bearer ${token}` };
|
|
328
413
|
}
|
|
329
414
|
|
|
330
415
|
function buildUrl(base: string, spec: RequestSpec): string {
|
|
@@ -341,7 +426,7 @@ async function fetchCloudflare(
|
|
|
341
426
|
spec: RequestSpec,
|
|
342
427
|
ctx: ConnectorContext,
|
|
343
428
|
): Promise<Response> {
|
|
344
|
-
const
|
|
429
|
+
const authenticationHeaders = await readAuthenticationHeaders(ctx);
|
|
345
430
|
if (spec.body !== undefined && spec.rawBody !== undefined) {
|
|
346
431
|
throw new Error("A Cloudflare request cannot have both JSON and raw bodies.");
|
|
347
432
|
}
|
|
@@ -350,7 +435,7 @@ async function fetchCloudflare(
|
|
|
350
435
|
response = await fetch(buildUrl(base, spec), {
|
|
351
436
|
method: spec.method,
|
|
352
437
|
headers: {
|
|
353
|
-
|
|
438
|
+
...authenticationHeaders,
|
|
354
439
|
Accept: "application/json",
|
|
355
440
|
...(spec.body !== undefined
|
|
356
441
|
? { "Content-Type": "application/json" }
|
|
@@ -405,9 +490,17 @@ async function callCloudflare(
|
|
|
405
490
|
if (!response.ok || envelope.success === false) {
|
|
406
491
|
throw failureFor(response.status, response.headers, errors);
|
|
407
492
|
}
|
|
493
|
+
const isV4Envelope =
|
|
494
|
+
"success" in envelope ||
|
|
495
|
+
"result" in envelope ||
|
|
496
|
+
"result_info" in envelope ||
|
|
497
|
+
"messages" in envelope;
|
|
408
498
|
return {
|
|
409
|
-
|
|
410
|
-
|
|
499
|
+
// `/graphql` and a small number of product APIs return ordinary JSON
|
|
500
|
+
// instead of the standard v4 envelope. Preserve that document whole so
|
|
501
|
+
// the raw tools cover them too.
|
|
502
|
+
result: isV4Envelope ? envelope.result : envelope,
|
|
503
|
+
resultInfo: isV4Envelope ? envelope.result_info : undefined,
|
|
411
504
|
};
|
|
412
505
|
}
|
|
413
506
|
|
|
@@ -1031,6 +1124,8 @@ function headersFromArgs(value: unknown): Record<string, string> | undefined {
|
|
|
1031
1124
|
const headers: Record<string, string> = {};
|
|
1032
1125
|
const forbidden = new Set([
|
|
1033
1126
|
"authorization",
|
|
1127
|
+
"x-auth-email",
|
|
1128
|
+
"x-auth-key",
|
|
1034
1129
|
"cookie",
|
|
1035
1130
|
"host",
|
|
1036
1131
|
"content-length",
|
|
@@ -1206,7 +1301,10 @@ const R2_OBJECT_SCHEMA: JsonSchema = {
|
|
|
1206
1301
|
required: ["key"],
|
|
1207
1302
|
};
|
|
1208
1303
|
|
|
1209
|
-
function buildTools(
|
|
1304
|
+
function buildTools(
|
|
1305
|
+
scope: Scoping,
|
|
1306
|
+
authentication: CloudflareAuthentication,
|
|
1307
|
+
): ApiTool[] {
|
|
1210
1308
|
const { base } = scope;
|
|
1211
1309
|
const zoneArg = (args: JsonRecord): string =>
|
|
1212
1310
|
requireScope(args["zoneId"], scope.zoneId, "zoneId");
|
|
@@ -1215,54 +1313,88 @@ function buildTools(scope: Scoping): ApiTool[] {
|
|
|
1215
1313
|
|
|
1216
1314
|
const readOnly = { readOnlyHint: true, destructiveHint: false } as const;
|
|
1217
1315
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1316
|
+
const tools: ApiTool[] = [
|
|
1317
|
+
authentication === "apiToken"
|
|
1318
|
+
? {
|
|
1319
|
+
name: "verify_api_token",
|
|
1320
|
+
description:
|
|
1321
|
+
"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.",
|
|
1322
|
+
annotations: readOnly,
|
|
1323
|
+
inputSchema: {
|
|
1324
|
+
type: "object",
|
|
1325
|
+
properties: {},
|
|
1326
|
+
required: [],
|
|
1327
|
+
additionalProperties: false,
|
|
1328
|
+
},
|
|
1329
|
+
outputSchema: {
|
|
1330
|
+
type: "object",
|
|
1331
|
+
properties: {
|
|
1332
|
+
id: { type: "string" },
|
|
1333
|
+
status: {
|
|
1334
|
+
type: "string",
|
|
1335
|
+
description: "\"active\" for a usable token.",
|
|
1336
|
+
},
|
|
1337
|
+
notBefore: { type: "string" },
|
|
1338
|
+
expiresOn: { type: "string" },
|
|
1339
|
+
},
|
|
1340
|
+
required: ["status"],
|
|
1341
|
+
},
|
|
1342
|
+
handler: async (_args, ctx) => {
|
|
1343
|
+
const { result } = await callCloudflare(
|
|
1344
|
+
base,
|
|
1345
|
+
{ method: "GET", path: "/user/tokens/verify" },
|
|
1346
|
+
ctx,
|
|
1347
|
+
);
|
|
1348
|
+
const token = asRecord(result);
|
|
1349
|
+
return {
|
|
1350
|
+
id: token["id"],
|
|
1351
|
+
status: token["status"],
|
|
1352
|
+
...(token["not_before"] !== undefined
|
|
1353
|
+
? { notBefore: token["not_before"] }
|
|
1354
|
+
: {}),
|
|
1355
|
+
...(token["expires_on"] !== undefined
|
|
1356
|
+
? { expiresOn: token["expires_on"] }
|
|
1357
|
+
: {}),
|
|
1358
|
+
};
|
|
1359
|
+
},
|
|
1360
|
+
}
|
|
1361
|
+
: {
|
|
1362
|
+
name: "verify_global_api_key",
|
|
1363
|
+
description:
|
|
1364
|
+
"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.",
|
|
1365
|
+
annotations: readOnly,
|
|
1366
|
+
inputSchema: {
|
|
1367
|
+
type: "object",
|
|
1368
|
+
properties: {},
|
|
1369
|
+
required: [],
|
|
1370
|
+
additionalProperties: false,
|
|
1371
|
+
},
|
|
1372
|
+
outputSchema: {
|
|
1373
|
+
type: "object",
|
|
1374
|
+
properties: {
|
|
1375
|
+
id: { type: "string" },
|
|
1376
|
+
email: { type: "string" },
|
|
1377
|
+
status: {
|
|
1378
|
+
type: "string",
|
|
1379
|
+
description: "\"active\" when Cloudflare accepts the email and key.",
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
required: ["email", "status"],
|
|
1383
|
+
},
|
|
1384
|
+
handler: async (_args, ctx) => {
|
|
1385
|
+
const { result } = await callCloudflare(
|
|
1386
|
+
base,
|
|
1387
|
+
{ method: "GET", path: "/user" },
|
|
1388
|
+
ctx,
|
|
1389
|
+
);
|
|
1390
|
+
const user = asRecord(result);
|
|
1391
|
+
return { id: user["id"], email: user["email"], status: "active" };
|
|
1237
1392
|
},
|
|
1238
|
-
notBefore: { type: "string" },
|
|
1239
|
-
expiresOn: { type: "string" },
|
|
1240
1393
|
},
|
|
1241
|
-
required: ["status"],
|
|
1242
|
-
},
|
|
1243
|
-
handler: async (_args, ctx) => {
|
|
1244
|
-
const { result } = await callCloudflare(
|
|
1245
|
-
base,
|
|
1246
|
-
{ method: "GET", path: "/user/tokens/verify" },
|
|
1247
|
-
ctx,
|
|
1248
|
-
);
|
|
1249
|
-
const token = asRecord(result);
|
|
1250
|
-
return {
|
|
1251
|
-
id: token["id"],
|
|
1252
|
-
status: token["status"],
|
|
1253
|
-
...(token["not_before"] !== undefined
|
|
1254
|
-
? { notBefore: token["not_before"] }
|
|
1255
|
-
: {}),
|
|
1256
|
-
...(token["expires_on"] !== undefined
|
|
1257
|
-
? { expiresOn: token["expires_on"] }
|
|
1258
|
-
: {}),
|
|
1259
|
-
};
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
1394
|
{
|
|
1263
1395
|
name: "cloudflare_api_get",
|
|
1264
1396
|
description:
|
|
1265
|
-
"Call any GET endpoint under Cloudflare's v4 API with this connector's
|
|
1397
|
+
"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.",
|
|
1266
1398
|
annotations: readOnly,
|
|
1267
1399
|
inputSchema: {
|
|
1268
1400
|
type: "object",
|
|
@@ -1330,7 +1462,7 @@ function buildTools(scope: Scoping): ApiTool[] {
|
|
|
1330
1462
|
{
|
|
1331
1463
|
name: "cloudflare_api_mutate",
|
|
1332
1464
|
description:
|
|
1333
|
-
"Call any JSON POST, PUT, PATCH, or DELETE endpoint under Cloudflare's v4 API with this connector's
|
|
1465
|
+
"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.",
|
|
1334
1466
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
1335
1467
|
inputSchema: {
|
|
1336
1468
|
type: "object",
|
|
@@ -3715,12 +3847,18 @@ function buildTools(scope: Scoping): ApiTool[] {
|
|
|
3715
3847
|
},
|
|
3716
3848
|
},
|
|
3717
3849
|
];
|
|
3850
|
+
return tools.map((tool) => ({
|
|
3851
|
+
...tool,
|
|
3852
|
+
handler: (args, ctx) =>
|
|
3853
|
+
tool.handler(args, withAuthentication(ctx, authentication)),
|
|
3854
|
+
}));
|
|
3718
3855
|
}
|
|
3719
3856
|
|
|
3720
3857
|
function usageGuide(
|
|
3721
3858
|
purpose: string,
|
|
3722
3859
|
scope: Scoping,
|
|
3723
3860
|
instructions: string | undefined,
|
|
3861
|
+
authentication: CloudflareAuthentication,
|
|
3724
3862
|
): string {
|
|
3725
3863
|
const accountInstructions = instructions?.trim();
|
|
3726
3864
|
const zoneLine = scope.zoneId
|
|
@@ -3729,6 +3867,10 @@ function usageGuide(
|
|
|
3729
3867
|
const accountLine = scope.accountId
|
|
3730
3868
|
? `It defaults to account \`${scope.accountId}\`; omit \`accountId\` unless the request names a different account.`
|
|
3731
3869
|
: "It declares no default account. `list_accounts` supplies the `accountId` the Workers, KV, R2, and Pages tools need.";
|
|
3870
|
+
const authenticationLine =
|
|
3871
|
+
authentication === "apiToken"
|
|
3872
|
+
? "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."
|
|
3873
|
+
: "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.";
|
|
3732
3874
|
return `# Cloudflare usage
|
|
3733
3875
|
|
|
3734
3876
|
Account purpose: ${purpose}
|
|
@@ -3736,11 +3878,11 @@ Account purpose: ${purpose}
|
|
|
3736
3878
|
- ${zoneLine}
|
|
3737
3879
|
- ${accountLine}
|
|
3738
3880
|
- 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.
|
|
3739
|
-
- 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
|
|
3881
|
+
- 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.
|
|
3740
3882
|
- 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.
|
|
3741
3883
|
- 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.
|
|
3742
3884
|
- 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.
|
|
3743
|
-
-
|
|
3885
|
+
- ${authenticationLine}
|
|
3744
3886
|
- 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.
|
|
3745
3887
|
- 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\`.
|
|
3746
3888
|
${
|
|
@@ -3760,6 +3902,12 @@ export function cloudflare(id: string, options: CloudflareOptions): Connector {
|
|
|
3760
3902
|
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
|
3761
3903
|
throw new Error("cloudflare() maxConcurrency must be a positive integer.");
|
|
3762
3904
|
}
|
|
3905
|
+
const authentication = options.authentication ?? "apiToken";
|
|
3906
|
+
if (authentication !== "apiToken" && authentication !== "globalApiKey") {
|
|
3907
|
+
throw new Error(
|
|
3908
|
+
'cloudflare() authentication must be "apiToken" or "globalApiKey".',
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3763
3911
|
const scope: Scoping = {
|
|
3764
3912
|
base: options.baseUrl?.trim() || CLOUDFLARE_API_BASE,
|
|
3765
3913
|
accountId: options.accountId?.trim() || undefined,
|
|
@@ -3768,36 +3916,84 @@ export function cloudflare(id: string, options: CloudflareOptions): Connector {
|
|
|
3768
3916
|
return api(id, {
|
|
3769
3917
|
title: options.title ?? "Cloudflare",
|
|
3770
3918
|
description: `Cloudflare control-plane access for zones, DNS, Workers, KV, R2, Pages, media, email, and other v4 APIs — ${purpose}`,
|
|
3771
|
-
credential: options.credential
|
|
3919
|
+
credential: credentialConfig(authentication, options.credential),
|
|
3772
3920
|
callAdmission: admissionPolicy(maxConcurrency),
|
|
3773
|
-
usageGuide: usageGuide(
|
|
3921
|
+
usageGuide: usageGuide(
|
|
3922
|
+
purpose,
|
|
3923
|
+
scope,
|
|
3924
|
+
options.instructions,
|
|
3925
|
+
authentication,
|
|
3926
|
+
),
|
|
3774
3927
|
// The schemas are hand-written and closed; a schema that cannot be
|
|
3775
3928
|
// enforced is a bug in this file, not input to pass through.
|
|
3776
3929
|
strictValidation: true,
|
|
3777
3930
|
...(options.maxResultBytes !== undefined
|
|
3778
3931
|
? { maxResultBytes: options.maxResultBytes }
|
|
3779
3932
|
: {}),
|
|
3780
|
-
tools: buildTools(scope),
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3933
|
+
tools: buildTools(scope, authentication),
|
|
3934
|
+
...(authentication === "apiToken"
|
|
3935
|
+
? {
|
|
3936
|
+
async testCredential(value: string, ctx: ConnectorContext) {
|
|
3937
|
+
try {
|
|
3938
|
+
const { result } = await callCloudflare(
|
|
3939
|
+
scope.base,
|
|
3940
|
+
{ method: "GET", path: "/user/tokens/verify" },
|
|
3941
|
+
withAuthentication(
|
|
3942
|
+
{
|
|
3943
|
+
...ctx,
|
|
3944
|
+
credential: {
|
|
3945
|
+
get: async () => value,
|
|
3946
|
+
getAll: async () => ({ value }),
|
|
3947
|
+
},
|
|
3948
|
+
},
|
|
3949
|
+
authentication,
|
|
3950
|
+
),
|
|
3951
|
+
);
|
|
3952
|
+
const status = asRecord(result)["status"];
|
|
3953
|
+
return status === "active"
|
|
3954
|
+
? { ok: true, message: "Token verified: active." }
|
|
3955
|
+
: { ok: false, message: `Token status is "${String(status)}".` };
|
|
3956
|
+
} catch (error) {
|
|
3957
|
+
return {
|
|
3958
|
+
ok: false,
|
|
3959
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3789
3962
|
},
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3963
|
+
}
|
|
3964
|
+
: {
|
|
3965
|
+
async testCredentials(
|
|
3966
|
+
values: Record<string, string>,
|
|
3967
|
+
ctx: ConnectorContext,
|
|
3968
|
+
) {
|
|
3969
|
+
try {
|
|
3970
|
+
const { result } = await callCloudflare(
|
|
3971
|
+
scope.base,
|
|
3972
|
+
{ method: "GET", path: "/user" },
|
|
3973
|
+
withAuthentication(
|
|
3974
|
+
{
|
|
3975
|
+
...ctx,
|
|
3976
|
+
credential: {
|
|
3977
|
+
get: async (field?: string) =>
|
|
3978
|
+
field ? values[field] ?? null : null,
|
|
3979
|
+
getAll: async () => values,
|
|
3980
|
+
},
|
|
3981
|
+
},
|
|
3982
|
+
authentication,
|
|
3983
|
+
),
|
|
3984
|
+
);
|
|
3985
|
+
const email = asRecord(result)["email"];
|
|
3986
|
+
return {
|
|
3987
|
+
ok: true,
|
|
3988
|
+
message: `Global API Key verified for ${String(email)}.`,
|
|
3989
|
+
};
|
|
3990
|
+
} catch (error) {
|
|
3991
|
+
return {
|
|
3992
|
+
ok: false,
|
|
3993
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
},
|
|
3997
|
+
}),
|
|
3802
3998
|
});
|
|
3803
3999
|
}
|
package/src/providers/linear.ts
CHANGED
|
@@ -173,17 +173,18 @@ const WRITE_TOOLS: ReadonlyMap<string, "additive" | "destructive"> = new Map([
|
|
|
173
173
|
]);
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
* Fill in
|
|
176
|
+
* Fill in downstream silence; keep reviewed destructive tools fail-closed.
|
|
177
177
|
*
|
|
178
178
|
* Silence is what a vetted classification is for, and an explicit downstream
|
|
179
|
-
* annotation wins in both directions. `destructiveHint: true` or
|
|
179
|
+
* annotation otherwise wins in both directions. `destructiveHint: true` or
|
|
180
180
|
* `readOnlyHint: false` on an allowlisted read name is the downstream telling
|
|
181
181
|
* us this release's allowlist is stale; `readOnlyHint: true` on a name no
|
|
182
182
|
* release has classified says the same thing from the other side. The single
|
|
183
183
|
* place a vetted verdict still overrides the downstream is a name this release
|
|
184
184
|
* reviewed and filed destructive: there connecta knows what the tool does, and
|
|
185
185
|
* a claim to the contrary is a downstream bug rather than news
|
|
186
|
-
* ([#310](https://github.com/zackbart/connecta/issues/310)
|
|
186
|
+
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
187
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
187
188
|
*/
|
|
188
189
|
function vettedSafety(definition: ToolDef): ToolDef {
|
|
189
190
|
const downstream = definition.annotations ?? {};
|
|
@@ -125,17 +125,18 @@ const WRITE_TOOLS: ReadonlyMap<string, "additive" | "destructive"> = new Map([
|
|
|
125
125
|
]);
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Fill in
|
|
128
|
+
* Fill in downstream silence; keep reviewed destructive tools fail-closed.
|
|
129
129
|
*
|
|
130
130
|
* Silence is what a vetted classification is for, and an explicit downstream
|
|
131
|
-
* annotation wins in both directions. `destructiveHint: true` or
|
|
131
|
+
* annotation otherwise wins in both directions. `destructiveHint: true` or
|
|
132
132
|
* `readOnlyHint: false` on an allowlisted read name is the downstream telling
|
|
133
133
|
* us this release's allowlist is stale; `readOnlyHint: true` on a name no
|
|
134
134
|
* release has classified says the same thing from the other side. The single
|
|
135
135
|
* place a vetted verdict still overrides the downstream is a name this release
|
|
136
136
|
* reviewed and filed destructive: there connecta knows what the tool does, and
|
|
137
137
|
* a claim to the contrary is a downstream bug rather than news
|
|
138
|
-
* ([#310](https://github.com/zackbart/connecta/issues/310)
|
|
138
|
+
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
139
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
139
140
|
*/
|
|
140
141
|
function vettedSafety(definition: ToolDef): ToolDef {
|
|
141
142
|
const downstream = definition.annotations ?? {};
|
package/src/providers/stripe.ts
CHANGED
|
@@ -113,17 +113,18 @@ const WRITE_TOOLS: ReadonlyMap<string, "additive" | "destructive"> = new Map([
|
|
|
113
113
|
]);
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Fill in
|
|
116
|
+
* Fill in downstream silence; keep reviewed destructive tools fail-closed.
|
|
117
117
|
*
|
|
118
118
|
* Silence is what a vetted classification is for, and an explicit downstream
|
|
119
|
-
* annotation wins in both directions. `destructiveHint: true` or
|
|
119
|
+
* annotation otherwise wins in both directions. `destructiveHint: true` or
|
|
120
120
|
* `readOnlyHint: false` on an allowlisted read name is the downstream telling
|
|
121
121
|
* us this release's allowlist is stale; `readOnlyHint: true` on a name no
|
|
122
122
|
* release has classified says the same thing from the other side. The single
|
|
123
123
|
* place a vetted verdict still overrides the downstream is a name this release
|
|
124
124
|
* reviewed and filed destructive: there connecta knows what the tool does, and
|
|
125
125
|
* a claim to the contrary is a downstream bug rather than news
|
|
126
|
-
* ([#310](https://github.com/zackbart/connecta/issues/310)
|
|
126
|
+
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
127
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
127
128
|
*/
|
|
128
129
|
function vettedSafety(definition: ToolDef): ToolDef {
|
|
129
130
|
const downstream = definition.annotations ?? {};
|