@zackbart/connecta 0.29.0 → 0.30.0

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 (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +123 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/catalog-cache.js +2 -2
  9. package/dist/catalog-service.js +103 -63
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +12 -7
  12. package/dist/connectors/guarded-fetch.js +3 -3
  13. package/dist/connectors/negotiation-cache.js +1 -1
  14. package/dist/downstream-input-context.js +9 -0
  15. package/dist/downstream-input.js +13 -11
  16. package/dist/errors.d.ts +36 -12
  17. package/dist/errors.js +19 -9
  18. package/dist/execute.js +375 -43
  19. package/dist/executor-result.js +11 -9
  20. package/dist/executors/quickjs-child.js +12 -5
  21. package/dist/executors/quickjs-protocol.js +5 -6
  22. package/dist/executors/quickjs-runtime.js +6 -1
  23. package/dist/executors/quickjs.js +20 -4
  24. package/dist/guest-runtime.js +52 -2
  25. package/dist/guest-types.d.ts +47 -5
  26. package/dist/index.d.ts +1 -1
  27. package/dist/invocation.js +40 -14
  28. package/dist/meta-output.js +0 -15
  29. package/dist/meta-tools.js +166 -95
  30. package/dist/oauth-handoff.js +88 -11
  31. package/dist/program-result.js +5 -0
  32. package/dist/program-writes.js +5 -1
  33. package/dist/provider.d.ts +1 -1
  34. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  35. package/dist/providers/_shared/request-token.js +83 -0
  36. package/dist/providers/infisical/index.d.ts +7 -0
  37. package/dist/providers/infisical/index.js +713 -0
  38. package/dist/providers/infisical/skill.generated.js +8 -0
  39. package/dist/registry.d.ts +8 -6
  40. package/dist/registry.js +134 -61
  41. package/dist/resource-read.js +4 -1
  42. package/dist/routes/mcp.js +11 -2
  43. package/dist/sent-secrets.d.ts +54 -13
  44. package/dist/sent-secrets.js +770 -231
  45. package/dist/skills.js +26 -16
  46. package/dist/sqlite.js +21 -6
  47. package/dist/storage/keys.js +20 -5
  48. package/dist/storage/memory.js +9 -4
  49. package/dist/storage/sql.js +18 -9
  50. package/dist/types.d.ts +17 -3
  51. package/dist/usage-guide.js +49 -40
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/worker.js +2 -1
  55. package/documentation/README.md +63 -0
  56. package/documentation/architecture.md +121 -89
  57. package/documentation/auth.md +138 -5
  58. package/documentation/code-mode.md +224 -107
  59. package/documentation/deploying.md +146 -0
  60. package/documentation/integrating.md +159 -0
  61. package/documentation/meta-tools.md +135 -30
  62. package/documentation/operating.md +135 -0
  63. package/documentation/operator-tests.md +4 -0
  64. package/documentation/operator-ui.md +132 -0
  65. package/documentation/provider-migration-0.29.md +5 -0
  66. package/examples/worker/README.md +7 -0
  67. package/package.json +7 -1
  68. package/templates/node/README.md +7 -0
  69. package/templates/node/package.json +1 -1
@@ -0,0 +1,713 @@
1
+ /** Infisical Universal Auth and a finite, reviewed REST secret-manager surface. */
2
+ import { apiConnector as api, defined as compact } from "../../connectors/api-connector.js";
3
+ import { guardedFetch, retryAfterMs } from "../../connectors/guarded-fetch.js";
4
+ import { ConnectorCallError } from "../../errors.js";
5
+ import { keys, optionsOf } from "../../config-schema.js";
6
+ import { defineProvider, PROVIDER_COMMON } from "../../provider.js";
7
+ import { sentSecretsFor } from "../../sent-secrets.js";
8
+ import { requestTokenCache } from "../_shared/request-token.js";
9
+ import { skill } from "./skill.generated.js";
10
+ export const INFISICAL_API_BASE_URL = "https://app.infisical.com/api";
11
+ /** Connecta's response ceiling, before JSON parsing; not an Infisical limit. */
12
+ const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
13
+ function asRecord(value) {
14
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
15
+ }
16
+ function asArray(value) {
17
+ return Array.isArray(value) ? value : [];
18
+ }
19
+ /** Fixed prose only: downstream failures may contain submitted values or credentials. */
20
+ function failure(status, headers, login = false) {
21
+ const detail = `Infisical returned HTTP ${status}.`;
22
+ const wait = retryAfterMs(headers);
23
+ const retry = wait === undefined ? {} : { retryAfterMs: wait };
24
+ if (status === 429)
25
+ return new ConnectorCallError("rate_limited", `${detail} Wait before retrying.`, retry);
26
+ if (login && status >= 400 && status < 500)
27
+ return new ConnectorCallError("auth_required", `${detail} Infisical refused the machine identity login. An operator must check the client ID, secret, and trusted IPs in this connection.`);
28
+ if (status === 401)
29
+ return new ConnectorCallError("auth_required", `${detail} An operator must replace invalid or revoked machine identity credentials in this connection.`);
30
+ if (status === 403)
31
+ return new ConnectorCallError("auth_required", `${detail} An operator must add the machine identity to the project or widen its role for this environment and path.`);
32
+ if (status === 404)
33
+ return new ConnectorCallError("not_found", `${detail} Confirm the project ID with list_projects, the environment slug, and the path with list_folders.`);
34
+ if ([400, 409, 422].includes(status))
35
+ return new ConnectorCallError("invalid_args", `${detail} Check the named tool's arguments.`);
36
+ if (status >= 500)
37
+ return new ConnectorCallError("unavailable", `${detail} Infisical is failing upstream.`, retry);
38
+ return new ConnectorCallError("connector_call_failed", detail, { retryable: false });
39
+ }
40
+ export const infisical = defineProvider({
41
+ name: "infisical",
42
+ title: "Infisical",
43
+ readme: "Infisical",
44
+ kind: "api",
45
+ skill,
46
+ options: optionsOf()({ ...PROVIDER_COMMON, ...keys("baseUrl") }),
47
+ bundle: {
48
+ baselineGzip: 27673,
49
+ maxGzip: 87673,
50
+ note: "./providers/infisical starts at 27,673 B gzip (#677): guarded transport, Universal Auth request-local flights and eight named tools, with no MCP client or Effect runtime. The cap uses the measured baseline plus the existing 60,000 B headroom policy.",
51
+ },
52
+ create(id, options, provider) {
53
+ if (options.baseUrl !== undefined && (typeof options.baseUrl !== "string" || !options.baseUrl.trim()))
54
+ throw new Error("baseUrl must be a non-empty absolute HTTPS URL including /api.");
55
+ const baseUrl = options.baseUrl?.trim() ?? INFISICAL_API_BASE_URL;
56
+ const send = guardedFetch({
57
+ provider: "Infisical",
58
+ baseUrl,
59
+ headers: { Accept: "application/json" },
60
+ maxResponseBytes: MAX_RESPONSE_BYTES,
61
+ authenticate: () => ({}),
62
+ });
63
+ async function exchange(clientId, clientSecret, ctx) {
64
+ sentSecretsFor(ctx).add(clientId);
65
+ sentSecretsFor(ctx).secret(clientSecret);
66
+ const issuedAt = Date.now();
67
+ return await send({ method: "POST", path: "/v1/auth/universal-auth/login", body: { clientId, clientSecret } }, ctx, async (response) => {
68
+ if (!response.ok) {
69
+ await response.bytes();
70
+ throw failure(response.status, response.headers, true);
71
+ }
72
+ const parsed = await response.jsonResult();
73
+ const body = asRecord("value" in parsed ? parsed.value : undefined);
74
+ const accessToken = body["accessToken"], expiresIn = body["expiresIn"];
75
+ if (typeof accessToken !== "string" ||
76
+ !/^[\x21-\x7e]+$/.test(accessToken) ||
77
+ typeof expiresIn !== "number" ||
78
+ !Number.isFinite(expiresIn) ||
79
+ expiresIn < 0 ||
80
+ !Number.isSafeInteger(Math.ceil(expiresIn * 1000)))
81
+ throw new ConnectorCallError("connector_call_failed", "Infisical returned a malformed login response.", {
82
+ retryable: false,
83
+ });
84
+ const ttl = expiresIn === 0 ? 3_600_000 : expiresIn * 1000;
85
+ return { accessToken, expiresAt: issuedAt + (expiresIn === 0 ? ttl : ttl - Math.min(60_000, ttl / 10)) };
86
+ });
87
+ }
88
+ const tokens = requestTokenCache(async (ctx) => {
89
+ const values = await ctx.credential?.getAll();
90
+ const clientId = values?.["clientId"]?.trim(), clientSecret = values?.["clientSecret"]?.trim();
91
+ if (!clientId || !clientSecret)
92
+ throw new ConnectorCallError("auth_required", "No Infisical machine identity is configured. Call authorize_connector for recovery options; an operator adds the Universal Auth client ID and secret in this connection.");
93
+ // Stored values may be padded; register the exact login fields even
94
+ // when this request reuses a token and sends no login body.
95
+ sentSecretsFor(ctx).add(clientId);
96
+ sentSecretsFor(ctx).secret(clientSecret);
97
+ return {
98
+ key: JSON.stringify([clientId, clientSecret]),
99
+ mint: (loginCtx) => exchange(clientId, clientSecret, loginCtx),
100
+ };
101
+ });
102
+ const call = async (request, ctx) => {
103
+ try {
104
+ const attempt = (token) => send({ ...request, headers: { Authorization: `Bearer ${token}` } }, ctx, async (response) => {
105
+ // Even failures are drained through the bound; their text is never rendered.
106
+ if (!response.ok) {
107
+ await response.bytes();
108
+ return { rejected: response.status === 401, error: failure(response.status, response.headers) };
109
+ }
110
+ const parsed = await response.jsonResult();
111
+ if (!("value" in parsed) && request.method !== "GET")
112
+ return { rejected: false, value: undefined };
113
+ if (!("value" in parsed))
114
+ throw new ConnectorCallError("connector_call_failed", "Infisical returned malformed JSON.", {
115
+ retryable: false,
116
+ });
117
+ return { rejected: false, value: parsed.value };
118
+ });
119
+ const token = await tokens.token(ctx);
120
+ let result = await attempt(token);
121
+ if (result.rejected) {
122
+ const refreshed = await tokens.token(ctx, token);
123
+ // A 401 can arrive after a write reached downstream code. Prepare a
124
+ // fresh token for an explicit invocation, but never replay this write.
125
+ if (request.method === "GET")
126
+ result = await attempt(refreshed);
127
+ }
128
+ if (result.error)
129
+ throw result.error;
130
+ return result.value;
131
+ }
132
+ catch (error) {
133
+ if (error instanceof ConnectorCallError && error.message.includes("response ceiling"))
134
+ throw new ConnectorCallError("connector_call_failed", "Infisical's response exceeded 8 MiB. Narrow secretPath, drop recursive, or omit includeValues.", { retryable: false });
135
+ throw error;
136
+ }
137
+ };
138
+ return api(id, {
139
+ ...provider.connectorOptions,
140
+ title: options.title ?? "Infisical",
141
+ description: `Infisical secrets management — ${options.purpose}`,
142
+ credential: {
143
+ label: "Machine identity",
144
+ description: "Universal Auth credentials. Project membership and roles bound this connection's access.",
145
+ fields: [
146
+ { name: "clientId", label: "Client ID", inputType: "text", placeholder: "Universal Auth client ID" },
147
+ { name: "clientSecret", label: "Client secret", inputType: "password", placeholder: "Paste client secret" },
148
+ ],
149
+ },
150
+ async testCredentials(values, ctx) {
151
+ const clientId = values["clientId"]?.trim(), clientSecret = values["clientSecret"]?.trim();
152
+ if (!clientId || !clientSecret)
153
+ return { ok: false, message: "Client ID and client secret are both required." };
154
+ try {
155
+ await exchange(clientId, clientSecret, ctx);
156
+ return { ok: true };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ ok: false,
161
+ message: error instanceof ConnectorCallError ? error.message : "Infisical login failed.",
162
+ };
163
+ }
164
+ },
165
+ usageGuide: provider.usageGuide({
166
+ context: [`Use this Infisical machine identity at ${baseUrl} for ${options.purpose}.`],
167
+ summary: "Find projects and environments first; secret values are omitted from lists unless requested.",
168
+ }),
169
+ tools: tools(call),
170
+ });
171
+ },
172
+ });
173
+ function segment(value) {
174
+ if (value === "." || value === "..")
175
+ throw new ConnectorCallError("invalid_args", "A secret key cannot be a URL dot segment.");
176
+ return encodeURIComponent(value);
177
+ }
178
+ // These grammars constrain identifiers, not arbitrary human-authored metadata.
179
+ // UUIDs and legacy 24-hex IDs are accepted; unknown shapes are withheld.
180
+ const ID_PATTERN = /^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{24})$/;
181
+ const KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,255}$/;
182
+ const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
183
+ const PATH_PATTERN = /^\/(?:[A-Za-z0-9_-][A-Za-z0-9_.-]{0,254}(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]{0,254})*\/?)?$/;
184
+ const ISO_PATTERN = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
185
+ const SECRET_TYPES = ["shared", "personal"];
186
+ // Infisical RequestState, not individual reviewers' ApprovalStatus.
187
+ const APPROVAL_STATES = ["open", "close"];
188
+ const PROJECT_TYPES = ["secret-manager"];
189
+ /** One checked projection for every value-free result. Never match secret values. */
190
+ class IdentifierProjection {
191
+ omitted = false;
192
+ record(value, fields, ignored = []) {
193
+ if (!value || typeof value !== "object" || Array.isArray(value))
194
+ this.omitted = true;
195
+ const record = asRecord(value);
196
+ if (Object.keys(record).some((key) => !fields.includes(key) && !ignored.includes(key)))
197
+ this.omitted = true;
198
+ return record;
199
+ }
200
+ array(value) {
201
+ if (value !== undefined && !Array.isArray(value))
202
+ this.omitted = true;
203
+ return asArray(value);
204
+ }
205
+ text(value, pattern) {
206
+ if (value === undefined)
207
+ return undefined;
208
+ if (typeof value === "string" && pattern.exec(value)?.[0] === value)
209
+ return value;
210
+ this.omitted = true;
211
+ return undefined;
212
+ }
213
+ choice(value, allowed) {
214
+ if (value === undefined)
215
+ return undefined;
216
+ if (typeof value === "string" && allowed.includes(value))
217
+ return value;
218
+ this.omitted = true;
219
+ return undefined;
220
+ }
221
+ version(value) {
222
+ if (value === undefined)
223
+ return undefined;
224
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1)
225
+ return value;
226
+ this.omitted = true;
227
+ return undefined;
228
+ }
229
+ finish(value) {
230
+ return this.omitted ? { ...value, metadataOmitted: true } : value;
231
+ }
232
+ }
233
+ function projectProject(value, projection) {
234
+ // Infisical derives default slugs from free-text project names.
235
+ const project = projection.record(value, ["id", "type", "environments", "createdAt", "updatedAt"]);
236
+ return compact({
237
+ id: projection.text(project["id"], ID_PATTERN),
238
+ type: projection.choice(project["type"], PROJECT_TYPES),
239
+ environments: projection.array(project["environments"]).map((env) => {
240
+ const record = projection.record(env, ["slug", "id"]);
241
+ return compact({
242
+ slug: projection.text(record["slug"], SLUG_PATTERN),
243
+ id: projection.text(record["id"], ID_PATTERN),
244
+ });
245
+ }),
246
+ createdAt: projection.text(project["createdAt"], ISO_PATTERN),
247
+ updatedAt: projection.text(project["updatedAt"], ISO_PATTERN),
248
+ });
249
+ }
250
+ function projectSecretIdentifiers(value, projection) {
251
+ const secret = projection.record(value, ["id", "secretKey", "environment", "secretPath", "type", "version", "tags", "createdAt", "updatedAt"], ["secretValue", "secretValueHidden"]);
252
+ return compact({
253
+ id: projection.text(secret["id"], ID_PATTERN),
254
+ key: projection.text(secret["secretKey"], KEY_PATTERN),
255
+ environment: projection.text(secret["environment"], SLUG_PATTERN),
256
+ path: projection.text(secret["secretPath"], PATH_PATTERN),
257
+ type: projection.choice(secret["type"], SECRET_TYPES),
258
+ version: projection.version(secret["version"]),
259
+ tags: projection.array(secret["tags"]).flatMap((tag) => {
260
+ const slug = projection.text(projection.record(tag, ["slug"])["slug"], SLUG_PATTERN);
261
+ return slug === undefined ? [] : [slug];
262
+ }),
263
+ createdAt: projection.text(secret["createdAt"], ISO_PATTERN),
264
+ updatedAt: projection.text(secret["updatedAt"], ISO_PATTERN),
265
+ });
266
+ }
267
+ function projectSecret(value) {
268
+ const secret = asRecord(value);
269
+ return compact({
270
+ id: secret["id"],
271
+ key: secret["secretKey"],
272
+ value: secret["secretValueHidden"] === true ? undefined : secret["secretValue"],
273
+ valueHidden: secret["secretValueHidden"] === true ? true : undefined,
274
+ comment: secret["secretComment"] || undefined,
275
+ environment: secret["environment"],
276
+ path: secret["secretPath"],
277
+ type: secret["type"],
278
+ version: secret["version"],
279
+ tags: asArray(secret["tags"])
280
+ .map((tag) => asRecord(tag)["slug"])
281
+ .filter(Boolean),
282
+ createdAt: secret["createdAt"],
283
+ updatedAt: secret["updatedAt"],
284
+ });
285
+ }
286
+ /** Join a folder path and a child name or walk-relative path into an absolute path. */
287
+ function joinPath(base, rest) {
288
+ const head = base.replace(/\/+$/, "");
289
+ const tail = rest.replace(/^\/+/, "");
290
+ return tail ? `${head}/${tail}` : head || "/";
291
+ }
292
+ /**
293
+ * Infisical omits a path from flat listings and gives recursive ones a path
294
+ * relative to the listed folder; both become absolute here.
295
+ */
296
+ function projectFolder(value, projection, parent) {
297
+ const folder = projection.record(value, ["id", "path", "version", "createdAt", "updatedAt"]);
298
+ const name = typeof folder["name"] === "string" ? folder["name"] : undefined;
299
+ const relative = typeof folder["relativePath"] === "string" ? folder["relativePath"] : undefined;
300
+ const path = parent === undefined
301
+ ? folder["path"]
302
+ : relative !== undefined
303
+ ? joinPath(parent, relative)
304
+ : name !== undefined
305
+ ? joinPath(parent, name)
306
+ : undefined;
307
+ return compact({
308
+ id: projection.text(folder["id"], ID_PATTERN),
309
+ path: projection.text(path, PATH_PATTERN),
310
+ version: projection.version(folder["version"]),
311
+ createdAt: projection.text(folder["createdAt"], ISO_PATTERN),
312
+ updatedAt: projection.text(folder["updatedAt"], ISO_PATTERN),
313
+ });
314
+ }
315
+ const projectId = { type: "string", minLength: 1, description: "Project ID from list_projects." };
316
+ const environment = {
317
+ type: "string",
318
+ minLength: 1,
319
+ description: "Environment slug, e.g. dev, staging, prod. list_projects shows each project's environments.",
320
+ };
321
+ const secretPath = {
322
+ type: "string",
323
+ pattern: "^/",
324
+ description: "Absolute folder path beginning with /. Defaults to /.",
325
+ default: "/",
326
+ };
327
+ const secretName = { type: "string", minLength: 1, description: "Secret key, e.g. DATABASE_URL." };
328
+ const secretType = {
329
+ type: "string",
330
+ enum: ["shared", "personal"],
331
+ description: "shared (default) or the caller's personal override.",
332
+ };
333
+ function result(properties) {
334
+ return { type: "object", properties, additionalProperties: false };
335
+ }
336
+ const string = { type: "string" };
337
+ const number = { type: "number" };
338
+ const strings = { type: "array", items: string };
339
+ const identifier = (pattern) => ({ type: "string", pattern: pattern.source });
340
+ // Keep the compact schema within its discovery budget; runtime projection
341
+ // still checks ISO_PATTERN before any timestamp leaves the provider.
342
+ const timestamp = { type: "string", format: "date-time" };
343
+ const metadataOmitted = {
344
+ type: "boolean",
345
+ description: "Free text, invalid identifiers or unavailable upstream metadata was withheld.",
346
+ };
347
+ const PROJECT = result({
348
+ id: identifier(ID_PATTERN),
349
+ type: { type: "string", enum: PROJECT_TYPES },
350
+ environments: { type: "array", items: result({ slug: identifier(SLUG_PATTERN), id: identifier(ID_PATTERN) }) },
351
+ createdAt: timestamp,
352
+ updatedAt: timestamp,
353
+ });
354
+ const FOLDER = result({
355
+ id: identifier(ID_PATTERN),
356
+ path: identifier(PATH_PATTERN),
357
+ version: { type: "integer", minimum: 1 },
358
+ createdAt: timestamp,
359
+ updatedAt: timestamp,
360
+ });
361
+ const SECRET_IDENTIFIERS = {
362
+ id: identifier(ID_PATTERN),
363
+ key: identifier(KEY_PATTERN),
364
+ environment: identifier(SLUG_PATTERN),
365
+ path: identifier(PATH_PATTERN),
366
+ type: { type: "string", enum: SECRET_TYPES },
367
+ version: { type: "integer", minimum: 1 },
368
+ tags: { type: "array", items: identifier(SLUG_PATTERN) },
369
+ createdAt: timestamp,
370
+ updatedAt: timestamp,
371
+ };
372
+ // Explicit value reads retain values and comments, including keys outside the
373
+ // conservative value-free identifier grammar.
374
+ const SECRET = result({
375
+ id: string,
376
+ key: string,
377
+ comment: string,
378
+ environment: string,
379
+ path: string,
380
+ type: string,
381
+ version: number,
382
+ tags: strings,
383
+ createdAt: string,
384
+ updatedAt: string,
385
+ value: string,
386
+ valueHidden: { type: "boolean" },
387
+ });
388
+ const SECRET_LIST = { type: "array", items: SECRET };
389
+ const APPROVAL = result({ id: identifier(ID_PATTERN), status: { type: "string", enum: APPROVAL_STATES } });
390
+ const PROJECTS_RESULT = result({ projects: { type: "array", items: PROJECT }, metadataOmitted });
391
+ const FOLDERS_RESULT = result({ folders: { type: "array", items: FOLDER }, metadataOmitted });
392
+ const SECRETS_RESULT = result({
393
+ secrets: SECRET_LIST,
394
+ imports: {
395
+ type: "array",
396
+ items: result({ environment: identifier(SLUG_PATTERN), path: identifier(PATH_PATTERN), secrets: SECRET_LIST }),
397
+ },
398
+ metadataOmitted,
399
+ });
400
+ const SECRET_RESULT = result({ secret: SECRET });
401
+ const WRITE_RESULT = result({
402
+ secret: result(SECRET_IDENTIFIERS),
403
+ pendingApproval: APPROVAL,
404
+ ok: { type: "boolean" },
405
+ metadataOmitted,
406
+ });
407
+ const FOLDER_RESULT = result({ folder: FOLDER, pendingApproval: APPROVAL, ok: { type: "boolean" }, metadataOmitted });
408
+ function tools(call) {
409
+ return [
410
+ {
411
+ name: "list_projects",
412
+ description: "List Infisical secret-manager projects with their IDs and environment slugs.",
413
+ inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false },
414
+ outputSchema: PROJECTS_RESULT,
415
+ annotations: { readOnlyHint: true },
416
+ handler: async (_args, ctx) => {
417
+ const payload = await call({ method: "GET", path: "/v1/projects", query: { type: "secret-manager" } }, ctx);
418
+ const projection = new IdentifierProjection();
419
+ const body = projection.record(payload, ["projects"]);
420
+ return projection.finish({
421
+ projects: projection.array(body["projects"]).map((project) => projectProject(project, projection)),
422
+ });
423
+ },
424
+ },
425
+ {
426
+ name: "list_folders",
427
+ description: "List folders in an Infisical project environment at a path.",
428
+ inputSchema: {
429
+ type: "object",
430
+ properties: {
431
+ projectId,
432
+ environment,
433
+ path: secretPath,
434
+ recursive: { type: "boolean", description: "Include every nested folder below path." },
435
+ },
436
+ required: ["projectId", "environment"],
437
+ additionalProperties: false,
438
+ },
439
+ outputSchema: FOLDERS_RESULT,
440
+ annotations: { readOnlyHint: true },
441
+ handler: async (args, ctx) => {
442
+ const payload = await call({
443
+ method: "GET",
444
+ path: "/v2/folders",
445
+ query: {
446
+ projectId: args.projectId,
447
+ environment: args.environment,
448
+ path: args.path ?? "/",
449
+ recursive: args.recursive,
450
+ },
451
+ }, ctx);
452
+ const parent = args.path ?? "/";
453
+ const projection = new IdentifierProjection();
454
+ const body = projection.record(payload, ["folders"]);
455
+ return projection.finish({
456
+ folders: projection.array(body["folders"]).map((folder) => projectFolder(folder, projection, parent)),
457
+ });
458
+ },
459
+ },
460
+ {
461
+ name: "list_secrets",
462
+ description: "List Infisical secret identifiers. includeValues explicitly returns secret values and comments.",
463
+ inputSchema: {
464
+ type: "object",
465
+ properties: {
466
+ projectId,
467
+ environment,
468
+ secretPath,
469
+ recursive: { type: "boolean", description: "Include secrets in every nested folder." },
470
+ includeValues: { type: "boolean", description: "Return secret values and comments. Defaults to false." },
471
+ includeImports: {
472
+ type: "boolean",
473
+ description: "Include secrets imported from other paths. Defaults to true.",
474
+ },
475
+ expandReferences: {
476
+ type: "boolean",
477
+ description: "Expand ${env.KEY} references in values. Defaults to true.",
478
+ },
479
+ tagSlugs: { type: "array", items: { type: "string" }, description: "Only secrets with these tag slugs." },
480
+ },
481
+ required: ["projectId", "environment"],
482
+ additionalProperties: false,
483
+ },
484
+ outputSchema: SECRETS_RESULT,
485
+ annotations: { readOnlyHint: true },
486
+ handler: async (args, ctx) => {
487
+ const includeValues = args.includeValues === true;
488
+ const payload = await call({
489
+ method: "GET",
490
+ path: "/v4/secrets",
491
+ query: {
492
+ projectId: args.projectId,
493
+ environment: args.environment,
494
+ secretPath: args.secretPath ?? "/",
495
+ recursive: args.recursive,
496
+ viewSecretValue: includeValues,
497
+ expandSecretReferences: includeValues ? (args.expandReferences ?? true) : false,
498
+ includeImports: args.includeImports ?? true,
499
+ tagSlugs: args.tagSlugs?.length ? args.tagSlugs.join(",") : undefined,
500
+ },
501
+ }, ctx);
502
+ const projection = new IdentifierProjection();
503
+ const body = projection.record(payload, ["secrets", "imports"]);
504
+ const shape = (secret) => includeValues ? projectSecret(secret) : projectSecretIdentifiers(secret, projection);
505
+ const imports = projection.array(body["imports"]).map((entry) => {
506
+ const record = projection.record(entry, ["environment", "secretPath", "secrets"]);
507
+ return compact({
508
+ environment: projection.text(record["environment"], SLUG_PATTERN),
509
+ path: projection.text(record["secretPath"], PATH_PATTERN),
510
+ secrets: projection.array(record["secrets"]).map(shape),
511
+ });
512
+ });
513
+ return projection.finish(compact({
514
+ secrets: projection.array(body["secrets"]).map(shape),
515
+ imports: imports.length ? imports : undefined,
516
+ }));
517
+ },
518
+ },
519
+ {
520
+ name: "get_secret",
521
+ description: "Read one Infisical secret, including its value and comment, by key.",
522
+ inputSchema: {
523
+ type: "object",
524
+ properties: {
525
+ projectId,
526
+ environment,
527
+ secretName,
528
+ secretPath,
529
+ version: { type: "integer", minimum: 1, description: "A past version to read. Omit for the current one." },
530
+ type: secretType,
531
+ expandReferences: { type: "boolean", description: "Expand ${env.KEY} references. Defaults to true." },
532
+ },
533
+ required: ["projectId", "environment", "secretName"],
534
+ additionalProperties: false,
535
+ },
536
+ outputSchema: SECRET_RESULT,
537
+ annotations: { readOnlyHint: true },
538
+ handler: async (args, ctx) => {
539
+ const payload = await call({
540
+ method: "GET",
541
+ path: `/v4/secrets/${segment(args.secretName)}`,
542
+ query: {
543
+ projectId: args.projectId,
544
+ environment: args.environment,
545
+ secretPath: args.secretPath ?? "/",
546
+ version: args.version,
547
+ type: args.type,
548
+ expandSecretReferences: args.expandReferences ?? true,
549
+ },
550
+ }, ctx);
551
+ return { secret: projectSecret(asRecord(payload)["secret"]) };
552
+ },
553
+ },
554
+ {
555
+ name: "create_secret",
556
+ description: "Create a secret in an Infisical project environment and path.",
557
+ inputSchema: {
558
+ type: "object",
559
+ properties: {
560
+ projectId,
561
+ environment,
562
+ secretName,
563
+ secretValue: {
564
+ type: "string",
565
+ writeOnly: true,
566
+ description: "The value. May reference others as ${env.KEY}.",
567
+ },
568
+ secretPath,
569
+ secretComment: { type: "string", writeOnly: true, description: "Optional note shown beside the secret." },
570
+ type: secretType,
571
+ },
572
+ required: ["projectId", "environment", "secretName", "secretValue"],
573
+ additionalProperties: false,
574
+ },
575
+ outputSchema: WRITE_RESULT,
576
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
577
+ handler: async (args, ctx) => {
578
+ const payload = asRecord(await call({
579
+ method: "POST",
580
+ path: `/v4/secrets/${segment(args.secretName)}`,
581
+ body: {
582
+ projectId: args.projectId,
583
+ environment: args.environment,
584
+ secretValue: args.secretValue,
585
+ secretPath: args.secretPath ?? "/",
586
+ secretComment: args.secretComment,
587
+ type: args.type,
588
+ },
589
+ }, ctx));
590
+ return writeResult(payload);
591
+ },
592
+ },
593
+ {
594
+ name: "update_secret",
595
+ description: "Update an Infisical secret's value, comment, or name. Only the fields passed change.",
596
+ inputSchema: {
597
+ type: "object",
598
+ properties: {
599
+ projectId,
600
+ environment,
601
+ secretName,
602
+ secretPath,
603
+ secretValue: { type: "string", writeOnly: true, description: "New value." },
604
+ secretComment: { type: "string", writeOnly: true, description: "New comment." },
605
+ newSecretName: { type: "string", minLength: 1, description: "Rename the secret to this key." },
606
+ type: secretType,
607
+ },
608
+ required: ["projectId", "environment", "secretName"],
609
+ additionalProperties: false,
610
+ },
611
+ outputSchema: WRITE_RESULT,
612
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
613
+ handler: async (args, ctx) => {
614
+ // Checked here rather than with a schema anyOf, which would hide the
615
+ // argument list from discovery.
616
+ if (args.secretValue === undefined && args.secretComment === undefined && args.newSecretName === undefined) {
617
+ throw new ConnectorCallError("invalid_args", "Pass secretValue, secretComment, or newSecretName.");
618
+ }
619
+ const payload = asRecord(await call({
620
+ method: "PATCH",
621
+ path: `/v4/secrets/${segment(args.secretName)}`,
622
+ body: {
623
+ projectId: args.projectId,
624
+ environment: args.environment,
625
+ secretPath: args.secretPath ?? "/",
626
+ secretValue: args.secretValue,
627
+ secretComment: args.secretComment,
628
+ newSecretName: args.newSecretName,
629
+ type: args.type,
630
+ },
631
+ }, ctx));
632
+ return writeResult(payload);
633
+ },
634
+ },
635
+ {
636
+ name: "delete_secret",
637
+ description: "Delete an Infisical secret from a project environment and path.",
638
+ inputSchema: {
639
+ type: "object",
640
+ properties: { projectId, environment, secretName, secretPath, type: secretType },
641
+ required: ["projectId", "environment", "secretName"],
642
+ additionalProperties: false,
643
+ },
644
+ outputSchema: WRITE_RESULT,
645
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
646
+ handler: async (args, ctx) => {
647
+ const payload = asRecord(await call({
648
+ method: "DELETE",
649
+ path: `/v4/secrets/${segment(args.secretName)}`,
650
+ body: {
651
+ projectId: args.projectId,
652
+ environment: args.environment,
653
+ secretPath: args.secretPath ?? "/",
654
+ type: args.type,
655
+ },
656
+ }, ctx));
657
+ return writeResult(payload);
658
+ },
659
+ },
660
+ {
661
+ name: "create_folder",
662
+ description: "Create a folder in an Infisical project environment.",
663
+ inputSchema: {
664
+ type: "object",
665
+ properties: {
666
+ projectId,
667
+ environment,
668
+ name: { type: "string", minLength: 1, description: "Folder name." },
669
+ path: { type: "string", pattern: "^/", description: "Absolute parent path beginning with /. Defaults to /." },
670
+ description: { type: "string", writeOnly: true, description: "Optional folder description." },
671
+ },
672
+ required: ["projectId", "environment", "name"],
673
+ additionalProperties: false,
674
+ },
675
+ outputSchema: FOLDER_RESULT,
676
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
677
+ handler: async (args, ctx) => {
678
+ const payload = await call({
679
+ method: "POST",
680
+ path: "/v2/folders",
681
+ body: {
682
+ projectId: args.projectId,
683
+ environment: args.environment,
684
+ name: args.name,
685
+ path: args.path ?? "/",
686
+ description: args.description,
687
+ },
688
+ }, ctx);
689
+ return writeResult(asRecord(payload), args.path ?? "/");
690
+ },
691
+ },
692
+ ];
693
+ }
694
+ /** Writes and pending approvals expose validated identifiers, never free text. */
695
+ function writeResult(payload, folderParent) {
696
+ const projection = new IdentifierProjection();
697
+ const body = projection.record(payload, ["approval", "secret", "folder"]);
698
+ if (body["approval"] !== undefined) {
699
+ const approval = projection.record(body["approval"], ["id", "status"]);
700
+ return projection.finish({
701
+ pendingApproval: compact({
702
+ id: projection.text(approval["id"], ID_PATTERN),
703
+ status: projection.choice(approval["status"], APPROVAL_STATES),
704
+ }),
705
+ });
706
+ }
707
+ if (folderParent !== undefined && body["folder"] !== undefined)
708
+ return projection.finish({ folder: projectFolder(body["folder"], projection, folderParent) });
709
+ if (folderParent === undefined && body["secret"] !== undefined)
710
+ return projection.finish({ secret: projectSecretIdentifiers(body["secret"], projection) });
711
+ // A successful unrecognized response must never be forwarded.
712
+ return { ok: true, metadataOmitted: true };
713
+ }