@ferrvault/mcp 10.13.0 → 11.0.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.
- package/dist/api.d.ts +19 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +32 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +14 -0
- package/dist/register.js +12 -12
- package/dist/results.d.ts +13 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +33 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +40 -0
- package/dist/secret.d.ts +18 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +4 -0
- package/dist/tools/secret-copy.d.ts +3 -0
- package/dist/tools/secret-copy.d.ts.map +1 -0
- package/dist/tools/secret-copy.js +33 -0
- package/dist/tools/secret-requests.d.ts +3 -0
- package/dist/tools/secret-requests.d.ts.map +1 -0
- package/dist/tools/secret-requests.js +58 -0
- package/dist/tools/secret-writes.d.ts +3 -0
- package/dist/tools/secret-writes.d.ts.map +1 -0
- package/dist/tools/secret-writes.js +52 -0
- package/dist/tools/secrets.d.ts +1 -1
- package/dist/tools/secrets.d.ts.map +1 -1
- package/dist/tools/secrets.js +21 -29
- package/dist/tools/service-tokens.d.ts +3 -0
- package/dist/tools/service-tokens.d.ts.map +1 -0
- package/dist/tools/service-tokens.js +82 -0
- package/dist/tools/vaults.d.ts +1 -1
- package/dist/tools/vaults.d.ts.map +1 -1
- package/dist/tools/vaults.js +21 -50
- package/dist/upsert.d.ts +9 -0
- package/dist/upsert.d.ts.map +1 -0
- package/dist/upsert.js +28 -0
- package/package.json +4 -4
- package/LICENSE +0 -373
- package/dist/resources/vaults.d.ts +0 -3
- package/dist/resources/vaults.d.ts.map +0 -1
- package/dist/resources/vaults.js +0 -16
- package/dist/tools/__tests__/mutations.test.d.ts +0 -2
- package/dist/tools/__tests__/mutations.test.d.ts.map +0 -1
- package/dist/tools/__tests__/mutations.test.js +0 -120
- package/dist/tools/audit.d.ts +0 -3
- package/dist/tools/audit.d.ts.map +0 -1
- package/dist/tools/audit.js +0 -39
- package/dist/tools/secret-mutations.d.ts +0 -3
- package/dist/tools/secret-mutations.d.ts.map +0 -1
- package/dist/tools/secret-mutations.js +0 -86
- package/dist/tools/vault-details.d.ts +0 -13
- package/dist/tools/vault-details.d.ts.map +0 -1
- package/dist/tools/vault-details.js +0 -18
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface VaultRequestOptions {
|
|
2
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
3
|
+
body?: unknown;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function vaultRequest<T>(path: string, options: VaultRequestOptions): Promise<T>;
|
|
7
|
+
export interface SecretTarget {
|
|
8
|
+
vault: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function vaultPath(vault: string): string;
|
|
12
|
+
export declare function secretsPath({ vault, environment }: SecretTarget): string;
|
|
13
|
+
export declare function secretPath(target: SecretTarget, name: string): string;
|
|
14
|
+
export declare function secretRequestsPath({ vault, environment }: SecretTarget): string;
|
|
15
|
+
export declare function archiveSecretRequestPath(target: SecretTarget, id: string): string;
|
|
16
|
+
export declare function serviceTokensPath({ vault, environment }: SecretTarget): string;
|
|
17
|
+
export declare function serviceTokenPath(target: SecretTarget, id: string): string;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAWA,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAMtF;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,YAAY,GAAG,MAAM,CAExE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,YAAY,GAAG,MAAM,CAE/E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,YAAY,GAAG,MAAM,CAE9E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzE"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { apiRequest } from '@ferrlabs/mcp-core';
|
|
2
|
+
const API_VERSION_HEADER = 'x-ferrvault-api-version';
|
|
3
|
+
const FERRVAULT_API_URL = (process.env.FERRVAULT_API_URL ?? 'https://api.ferrvault.com').replace(/\/+$/, '');
|
|
4
|
+
const FERRVAULT_API_VERSION = process.env.FERRVAULT_API_VERSION ?? '2026-08-04';
|
|
5
|
+
export function vaultRequest(path, options) {
|
|
6
|
+
return apiRequest(path, {
|
|
7
|
+
...options,
|
|
8
|
+
baseUrl: FERRVAULT_API_URL,
|
|
9
|
+
headers: { [API_VERSION_HEADER]: FERRVAULT_API_VERSION },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function vaultPath(vault) {
|
|
13
|
+
return `/vaults/${encodeURIComponent(vault)}`;
|
|
14
|
+
}
|
|
15
|
+
export function secretsPath({ vault, environment }) {
|
|
16
|
+
return `${vaultPath(vault)}/environments/${encodeURIComponent(environment)}/secrets`;
|
|
17
|
+
}
|
|
18
|
+
export function secretPath(target, name) {
|
|
19
|
+
return `${secretsPath(target)}/${encodeURIComponent(name)}`;
|
|
20
|
+
}
|
|
21
|
+
export function secretRequestsPath({ vault, environment }) {
|
|
22
|
+
return `${vaultPath(vault)}/environments/${encodeURIComponent(environment)}/secret-requests`;
|
|
23
|
+
}
|
|
24
|
+
export function archiveSecretRequestPath(target, id) {
|
|
25
|
+
return `${secretRequestsPath(target)}/${encodeURIComponent(id)}/archive`;
|
|
26
|
+
}
|
|
27
|
+
export function serviceTokensPath({ vault, environment }) {
|
|
28
|
+
return `${vaultPath(vault)}/environments/${encodeURIComponent(environment)}/operator/tokens`;
|
|
29
|
+
}
|
|
30
|
+
export function serviceTokenPath(target, id) {
|
|
31
|
+
return `${serviceTokensPath(target)}/${encodeURIComponent(id)}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const CHARSETS: {
|
|
2
|
+
readonly alphanumeric: string;
|
|
3
|
+
readonly hex: string;
|
|
4
|
+
readonly base64url: string;
|
|
5
|
+
readonly ascii: string;
|
|
6
|
+
};
|
|
7
|
+
export type Charset = keyof typeof CHARSETS;
|
|
8
|
+
export declare function generateValue(length: number, charset: Charset): string;
|
|
9
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,QAAQ;aACnB,YAAY;aACZ,GAAG;aACH,SAAS;aACT,KAAK;CACG,CAAC;AAEX,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC;AAE5C,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAGtE"}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
const LOWER = 'abcdefghijklmnopqrstuvwxyz';
|
|
3
|
+
const UPPER = LOWER.toUpperCase();
|
|
4
|
+
const DIGITS = '0123456789';
|
|
5
|
+
export const CHARSETS = {
|
|
6
|
+
alphanumeric: LOWER + UPPER + DIGITS,
|
|
7
|
+
hex: DIGITS + 'abcdef',
|
|
8
|
+
base64url: LOWER + UPPER + DIGITS + '-_',
|
|
9
|
+
ascii: LOWER + UPPER + DIGITS + '!#$%&()*+,-./:;<=>?@[]^_{|}~',
|
|
10
|
+
};
|
|
11
|
+
export function generateValue(length, charset) {
|
|
12
|
+
const alphabet = CHARSETS[charset];
|
|
13
|
+
return Array.from({ length }, () => alphabet[randomInt(alphabet.length)]).join('');
|
|
14
|
+
}
|
package/dist/register.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { registerVaultTools } from './tools/vaults.js';
|
|
2
|
+
import { registerSecretReadTools } from './tools/secrets.js';
|
|
3
|
+
import { registerSecretWriteTools } from './tools/secret-writes.js';
|
|
4
|
+
import { registerSecretRequestTools } from './tools/secret-requests.js';
|
|
5
|
+
import { registerSecretCopyTools } from './tools/secret-copy.js';
|
|
6
|
+
import { registerServiceTokenTools } from './tools/service-tokens.js';
|
|
7
7
|
export function register(server) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
registerVaultTools(server);
|
|
9
|
+
registerSecretReadTools(server);
|
|
10
|
+
registerSecretWriteTools(server);
|
|
11
|
+
registerSecretRequestTools(server);
|
|
12
|
+
registerSecretCopyTools(server);
|
|
13
|
+
registerServiceTokenTools(server);
|
|
14
14
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ToolResult {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
content: [{
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}];
|
|
7
|
+
isError?: true;
|
|
8
|
+
}
|
|
9
|
+
export type Redact = (secret: string) => void;
|
|
10
|
+
export declare function textResult(value: unknown): ToolResult;
|
|
11
|
+
export declare function describeError(err: unknown, secrets?: readonly string[]): string;
|
|
12
|
+
export declare function guarded(run: (redactLater: Redact) => Promise<ToolResult>, secret?: string): Promise<ToolResult>;
|
|
13
|
+
//# sourceMappingURL=results.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,IAAI,CAAC;CAChB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE9C,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAErD;AAMD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAOnF;AASD,wBAAsB,OAAO,CAC3B,GAAG,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,EACjD,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,CAAC,CASrB"}
|
package/dist/results.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ApiRequestError, toToolText } from '@ferrlabs/mcp-core';
|
|
2
|
+
export function textResult(value) {
|
|
3
|
+
return { content: [{ type: 'text', text: toToolText(value) }] };
|
|
4
|
+
}
|
|
5
|
+
function redact(message, secrets) {
|
|
6
|
+
return secrets.reduce((text, secret) => text.split(secret).join('[redacted]'), message);
|
|
7
|
+
}
|
|
8
|
+
export function describeError(err, secrets = []) {
|
|
9
|
+
const message = redact(err instanceof Error ? err.message : String(err), secrets);
|
|
10
|
+
if (err instanceof ApiRequestError) {
|
|
11
|
+
const code = err.code ? ` ${err.code}` : '';
|
|
12
|
+
return `FerrVault API error (HTTP ${err.status}${code}): ${message}`;
|
|
13
|
+
}
|
|
14
|
+
return message;
|
|
15
|
+
}
|
|
16
|
+
function errorResult(err, secrets) {
|
|
17
|
+
return {
|
|
18
|
+
isError: true,
|
|
19
|
+
content: [{ type: 'text', text: describeError(err, secrets) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function guarded(run, secret) {
|
|
23
|
+
const secrets = secret ? [secret] : [];
|
|
24
|
+
try {
|
|
25
|
+
return await run((later) => {
|
|
26
|
+
if (later)
|
|
27
|
+
secrets.push(later);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return errorResult(err, secrets);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const vaultSlug: z.ZodString;
|
|
3
|
+
export declare const environmentSlug: z.ZodString;
|
|
4
|
+
export declare const newVaultSlug: z.ZodString;
|
|
5
|
+
export declare const newEnvironmentSlug: z.ZodString;
|
|
6
|
+
export declare const displayName: z.ZodString;
|
|
7
|
+
export declare const secretName: z.ZodString;
|
|
8
|
+
export declare const secretRequestId: z.ZodUUID;
|
|
9
|
+
export declare const requestedName: z.ZodString;
|
|
10
|
+
export declare const serviceTokenId: z.ZodUUID;
|
|
11
|
+
export declare const serviceTokenName: z.ZodString;
|
|
12
|
+
export declare const vaultRole: z.ZodEnum<{
|
|
13
|
+
admin: "admin";
|
|
14
|
+
viewer: "viewer";
|
|
15
|
+
writer: "writer";
|
|
16
|
+
}>;
|
|
17
|
+
export declare const expiresAt: z.ZodISODateTime;
|
|
18
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,SAAS,aAAkE,CAAC;AAEzF,eAAO,MAAM,eAAe,aAE3B,CAAC;AAIF,eAAO,MAAM,YAAY,aAExB,CAAC;AAEF,eAAO,MAAM,kBAAkB,aAE9B,CAAC;AAEF,eAAO,MAAM,WAAW,aAAqE,CAAC;AAE9F,eAAO,MAAM,UAAU,aAQyB,CAAC;AAEjD,eAAO,MAAM,eAAe,WAEiD,CAAC;AAE9E,eAAO,MAAM,aAAa,aAIgE,CAAC;AAE3F,eAAO,MAAM,cAAc,WAEgD,CAAC;AAE5E,eAAO,MAAM,gBAAgB,aAIiD,CAAC;AAE/E,eAAO,MAAM,SAAS;;;;EAE0C,CAAC;AAEjE,eAAO,MAAM,SAAS,kBAEiE,CAAC"}
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const slug = z
|
|
3
|
+
.string()
|
|
4
|
+
.min(1)
|
|
5
|
+
.max(40)
|
|
6
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'must be a lowercase slug (letters, digits, hyphens)');
|
|
7
|
+
export const vaultSlug = slug.describe('Vault slug, as listed by list_ferrvault_vaults');
|
|
8
|
+
export const environmentSlug = slug.describe('Environment slug inside the vault, as listed by list_ferrvault_environments (e.g. prod)');
|
|
9
|
+
const newSlug = slug.min(2);
|
|
10
|
+
export const newVaultSlug = newSlug.describe('Slug of the new vault, unique in the organization (e.g. ferrlabs-infra)');
|
|
11
|
+
export const newEnvironmentSlug = newSlug.describe('Slug of the new environment, unique in the vault (e.g. staging)');
|
|
12
|
+
export const displayName = z.string().min(1).max(100).describe('Display name (e.g. Staging)');
|
|
13
|
+
export const secretName = z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.max(200)
|
|
17
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_.]*$/, 'must start with a letter or underscore, then letters, digits, underscores or dots')
|
|
18
|
+
.describe('Secret name (e.g. STRIPE_API_KEY)');
|
|
19
|
+
export const secretRequestId = z
|
|
20
|
+
.uuid()
|
|
21
|
+
.describe('Secret request id, as listed by list_ferrvault_secret_requests');
|
|
22
|
+
export const requestedName = z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(200)
|
|
26
|
+
.describe('Secret name a client requested, as listed by list_ferrvault_secret_requests');
|
|
27
|
+
export const serviceTokenId = z
|
|
28
|
+
.uuid()
|
|
29
|
+
.describe('Service token id, as listed by list_ferrvault_service_tokens');
|
|
30
|
+
export const serviceTokenName = z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(100)
|
|
34
|
+
.describe('Label shown for the token in FerrVault (e.g. k8s-operator-prod)');
|
|
35
|
+
export const vaultRole = z
|
|
36
|
+
.enum(['viewer', 'writer', 'admin'])
|
|
37
|
+
.describe('Role the token acts with on its vault environment');
|
|
38
|
+
export const expiresAt = z.iso
|
|
39
|
+
.datetime({ offset: true })
|
|
40
|
+
.describe('ISO-8601 instant after which the token stops working; omit for no expiry');
|
package/dist/secret.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SecretMetadata {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
current_version: number;
|
|
5
|
+
tags: string[];
|
|
6
|
+
expires_at: string | null;
|
|
7
|
+
expiry_action: 'notify' | 'disable';
|
|
8
|
+
expired: boolean;
|
|
9
|
+
created_at: string;
|
|
10
|
+
updated_at: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RevealedSecret extends SecretMetadata {
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function withoutValue(secret: SecretMetadata & {
|
|
16
|
+
value?: unknown;
|
|
17
|
+
}): SecretMetadata;
|
|
18
|
+
//# sourceMappingURL=secret.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAGzF"}
|
package/dist/secret.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-copy.d.ts","sourceRoot":"","sources":["../../src/tools/secret-copy.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAqB9D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA+B/D"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getToken } from '@ferrlabs/mcp-core';
|
|
3
|
+
import { secretPath, vaultRequest } from '../api.js';
|
|
4
|
+
import { guarded, textResult } from '../results.js';
|
|
5
|
+
import { environmentSlug, secretName, vaultSlug } from '../schemas.js';
|
|
6
|
+
import { upsertSecret, outcome } from '../upsert.js';
|
|
7
|
+
const source = z
|
|
8
|
+
.object({ vault: vaultSlug, environment: environmentSlug, name: secretName })
|
|
9
|
+
.describe('Secret to copy from');
|
|
10
|
+
const target = z
|
|
11
|
+
.object({ vault: vaultSlug, environment: environmentSlug, name: secretName.optional() })
|
|
12
|
+
.describe('Where to store the copy; name defaults to the source name');
|
|
13
|
+
function sameSecret(a, b) {
|
|
14
|
+
return a.vault === b.vault && a.environment === b.environment && a.name === b.name;
|
|
15
|
+
}
|
|
16
|
+
export function registerSecretCopyTools(server) {
|
|
17
|
+
server.tool('copy_ferrvault_secret', 'Copy the current value of a secret to another vault, environment or name, creating the target or storing a new version of it. The value is read and written inside the MCP server and never returned, so it never enters the conversation. The read is audit-logged by FerrVault.', { source, target }, ({ source: from, target: to }) => guarded(async (redactLater) => {
|
|
18
|
+
const dest = { ...to, name: to.name ?? from.name };
|
|
19
|
+
if (sameSecret(from, dest)) {
|
|
20
|
+
throw new Error('source and target are the same secret');
|
|
21
|
+
}
|
|
22
|
+
const token = await getToken();
|
|
23
|
+
const revealed = await vaultRequest(secretPath({ vault: from.vault, environment: from.environment }, from.name), { token });
|
|
24
|
+
redactLater(revealed.value);
|
|
25
|
+
const written = await upsertSecret(token, { vault: dest.vault, environment: dest.environment }, dest.name, revealed.value);
|
|
26
|
+
return textResult({
|
|
27
|
+
outcome: outcome(written, 'versioned'),
|
|
28
|
+
target: dest,
|
|
29
|
+
source_version: revealed.current_version,
|
|
30
|
+
target_version: written.secret.current_version,
|
|
31
|
+
});
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-requests.d.ts","sourceRoot":"","sources":["../../src/tools/secret-requests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA2E9D,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAkClE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getToken } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { archiveSecretRequestPath, secretRequestsPath, vaultRequest, } from '../api.js';
|
|
3
|
+
import { guarded, textResult } from '../results.js';
|
|
4
|
+
import { environmentSlug, requestedName, secretRequestId, vaultSlug } from '../schemas.js';
|
|
5
|
+
async function listRequests(token, target) {
|
|
6
|
+
const { requests } = await vaultRequest(secretRequestsPath(target), { token });
|
|
7
|
+
return requests;
|
|
8
|
+
}
|
|
9
|
+
function summary(request) {
|
|
10
|
+
const { id, name, state, request_count, first_requested_at, last_requested_at, fulfilled_at, last_requester_kind, } = request;
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
name,
|
|
14
|
+
state,
|
|
15
|
+
request_count,
|
|
16
|
+
first_requested_at,
|
|
17
|
+
last_requested_at,
|
|
18
|
+
fulfilled_at,
|
|
19
|
+
last_requester_kind,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function requestRef(id, name) {
|
|
23
|
+
if (id !== undefined && name === undefined)
|
|
24
|
+
return { id };
|
|
25
|
+
if (name !== undefined && id === undefined)
|
|
26
|
+
return { name };
|
|
27
|
+
throw new Error('pass exactly one of id or name');
|
|
28
|
+
}
|
|
29
|
+
async function resolvePendingId(token, target, name) {
|
|
30
|
+
const requests = await listRequests(token, target);
|
|
31
|
+
const pending = requests.find((r) => r.name === name && r.state === 'pending');
|
|
32
|
+
if (!pending) {
|
|
33
|
+
throw new Error(`no pending secret request named ${name} in ${target.vault}/${target.environment}`);
|
|
34
|
+
}
|
|
35
|
+
return pending.id;
|
|
36
|
+
}
|
|
37
|
+
export function registerSecretRequestTools(server) {
|
|
38
|
+
server.tool('list_ferrvault_secret_requests', 'List the pending secret requests of one vault environment: names a client (operator, CLI) asked for that have no value yet, with request counts, timestamps and requester kind. The API returns pending requests only; fulfilled and archived ones are not listed.', { vault: vaultSlug, environment: environmentSlug }, ({ vault, environment }) => guarded(async () => {
|
|
39
|
+
const token = await getToken();
|
|
40
|
+
return textResult((await listRequests(token, { vault, environment })).map(summary));
|
|
41
|
+
}));
|
|
42
|
+
server.tool('archive_ferrvault_secret_request', 'Archive a pending secret request (a false positive, e.g. a typo), by id or by secret name. An archived request stays archived even if clients keep asking for the name. Pass exactly one of id or name.', {
|
|
43
|
+
vault: vaultSlug,
|
|
44
|
+
environment: environmentSlug,
|
|
45
|
+
id: secretRequestId.optional(),
|
|
46
|
+
name: requestedName.optional(),
|
|
47
|
+
}, ({ vault, environment, id, name }) => guarded(async () => {
|
|
48
|
+
const ref = requestRef(id, name);
|
|
49
|
+
const token = await getToken();
|
|
50
|
+
const target = { vault, environment };
|
|
51
|
+
const requestId = 'id' in ref ? ref.id : await resolvePendingId(token, target, ref.name);
|
|
52
|
+
await vaultRequest(archiveSecretRequestPath(target, requestId), {
|
|
53
|
+
token,
|
|
54
|
+
method: 'POST',
|
|
55
|
+
});
|
|
56
|
+
return textResult({ archived: requestId, name, vault, environment, state: 'archived' });
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-writes.d.ts","sourceRoot":"","sources":["../../src/tools/secret-writes.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAmB9D,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAiDhE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getToken } from '@ferrlabs/mcp-core';
|
|
3
|
+
import { CHARSETS, generateValue } from '../generate.js';
|
|
4
|
+
import { guarded, textResult } from '../results.js';
|
|
5
|
+
import { environmentSlug, secretName, vaultSlug } from '../schemas.js';
|
|
6
|
+
import { withoutValue } from '../secret.js';
|
|
7
|
+
import { outcome, upsertSecret } from '../upsert.js';
|
|
8
|
+
function summary(written, target, extra = {}) {
|
|
9
|
+
return {
|
|
10
|
+
outcome: outcome(written, 'rotated'),
|
|
11
|
+
...target,
|
|
12
|
+
...extra,
|
|
13
|
+
secret: withoutValue(written.secret),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const charsetNames = Object.keys(CHARSETS);
|
|
17
|
+
export function registerSecretWriteTools(server) {
|
|
18
|
+
server.tool('set_ferrvault_secret', 'Create a secret, or store a new version of it when it already exists (the previous version stays restorable in FerrVault). The value you pass is part of this conversation; to avoid that, use generate_ferrvault_secret.', {
|
|
19
|
+
vault: vaultSlug,
|
|
20
|
+
environment: environmentSlug,
|
|
21
|
+
name: secretName,
|
|
22
|
+
value: z.string().min(1).max(65_536).describe('Plaintext value, encrypted by FerrVault'),
|
|
23
|
+
}, ({ vault, environment, name, value }) => guarded(async () => {
|
|
24
|
+
const token = await getToken();
|
|
25
|
+
const target = { vault, environment };
|
|
26
|
+
return textResult(summary(await upsertSecret(token, target, name, value), target));
|
|
27
|
+
}));
|
|
28
|
+
server.tool('generate_ferrvault_secret', 'Generate a cryptographically random value inside the MCP server and store it as a secret, creating it or rotating it to a new version. The value is never returned, so it never enters the conversation; reveal_ferrvault_secret reads it back if the user asks.', {
|
|
29
|
+
vault: vaultSlug,
|
|
30
|
+
environment: environmentSlug,
|
|
31
|
+
name: secretName,
|
|
32
|
+
length: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(8)
|
|
36
|
+
.max(4096)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Number of characters (default 48)'),
|
|
39
|
+
charset: z
|
|
40
|
+
.enum(charsetNames)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('alphanumeric (default), hex, base64url, or ascii (alphanumeric plus punctuation, no quotes, backslash or space)'),
|
|
43
|
+
}, ({ vault, environment, name, length = 48, charset = 'alphanumeric' }) => {
|
|
44
|
+
const value = generateValue(length, charset);
|
|
45
|
+
return guarded(async () => {
|
|
46
|
+
const token = await getToken();
|
|
47
|
+
const target = { vault, environment };
|
|
48
|
+
const written = await upsertSecret(token, target, name, value);
|
|
49
|
+
return textResult(summary(written, target, { length, charset }));
|
|
50
|
+
}, value);
|
|
51
|
+
});
|
|
52
|
+
}
|
package/dist/tools/secrets.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/tools/secrets.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/tools/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAM9D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2C/D"}
|
package/dist/tools/secrets.js
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}, async ({ org_slug, project_slug, vault_id }) => {
|
|
1
|
+
import { getToken } from '@ferrlabs/mcp-core';
|
|
2
|
+
import { secretPath, secretsPath, vaultRequest } from '../api.js';
|
|
3
|
+
import { guarded, textResult } from '../results.js';
|
|
4
|
+
import { environmentSlug, secretName, vaultSlug } from '../schemas.js';
|
|
5
|
+
import { withoutValue } from '../secret.js';
|
|
6
|
+
export function registerSecretReadTools(server) {
|
|
7
|
+
server.tool('list_ferrvault_secrets', 'List the secrets of one vault environment: names, versions, tags and expiry. Never includes values.', { vault: vaultSlug, environment: environmentSlug }, ({ vault, environment }) => guarded(async () => {
|
|
9
8
|
const token = await getToken();
|
|
10
|
-
const secrets = await
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
server.tool('get_secret', 'Get one secret. By default returns metadata only; pass reveal=true to also include the decrypted value (audit-logged on the server).', {
|
|
16
|
-
org_slug: z.string().min(1).describe('Organization slug'),
|
|
17
|
-
project_slug: z.string().min(1).describe('Project slug'),
|
|
18
|
-
vault_id: z.string().min(1).describe('Vault id'),
|
|
19
|
-
secret_id: z.string().min(1).describe('Secret id'),
|
|
20
|
-
reveal: z
|
|
21
|
-
.boolean()
|
|
22
|
-
.optional()
|
|
23
|
-
.describe('Include the decrypted value in the response (default false).'),
|
|
24
|
-
}, async ({ org_slug, project_slug, vault_id, secret_id, reveal }) => {
|
|
9
|
+
const secrets = await vaultRequest(secretsPath({ vault, environment }), { token });
|
|
10
|
+
return textResult(secrets.map(withoutValue));
|
|
11
|
+
}));
|
|
12
|
+
server.tool('reveal_ferrvault_secret', 'Reveal the current plaintext value of one secret. The read is audit-logged by FerrVault and the value lands in this conversation transcript: only call it when the user explicitly asks to see the value. To create or rotate a value without exposing it, use generate_ferrvault_secret.', { vault: vaultSlug, environment: environmentSlug, name: secretName }, ({ vault, environment, name }) => guarded(async () => {
|
|
25
13
|
const token = await getToken();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
14
|
+
return textResult(await vaultRequest(secretPath({ vault, environment }, name), { token }));
|
|
15
|
+
}));
|
|
16
|
+
server.tool('delete_ferrvault_secret', 'Delete a secret from one vault environment. Workloads reading it lose access.', { vault: vaultSlug, environment: environmentSlug, name: secretName }, ({ vault, environment, name }) => guarded(async () => {
|
|
17
|
+
const token = await getToken();
|
|
18
|
+
await vaultRequest(secretPath({ vault, environment }, name), {
|
|
19
|
+
token,
|
|
20
|
+
method: 'DELETE',
|
|
21
|
+
});
|
|
22
|
+
return textResult({ deleted: name, vault, environment });
|
|
23
|
+
}));
|
|
32
24
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../src/tools/service-tokens.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA8E9D,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6DjE"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getToken } from '@ferrlabs/mcp-core';
|
|
3
|
+
import { serviceTokenPath, serviceTokensPath, vaultRequest } from '../api.js';
|
|
4
|
+
import { describeError, guarded, textResult } from '../results.js';
|
|
5
|
+
import { environmentSlug, expiresAt, secretName, serviceTokenId, serviceTokenName, vaultRole, vaultSlug, } from '../schemas.js';
|
|
6
|
+
import { upsertSecret, outcome } from '../upsert.js';
|
|
7
|
+
function metadata(row, scope) {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
name: row.label,
|
|
11
|
+
vault: scope.vault,
|
|
12
|
+
environment: scope.environment,
|
|
13
|
+
role: row.role,
|
|
14
|
+
created_by: row.created_by,
|
|
15
|
+
created_at: row.created_at,
|
|
16
|
+
expires_at: row.expires_at,
|
|
17
|
+
last_used_at: row.last_used_at,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function revokeQuietly(token, scope, id) {
|
|
21
|
+
try {
|
|
22
|
+
await vaultRequest(serviceTokenPath(scope, id), { token, method: 'DELETE' });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function storeOrRevoke(token, scope, created, into) {
|
|
30
|
+
try {
|
|
31
|
+
return await upsertSecret(token, { vault: into.vault, environment: into.environment }, into.name, created.token);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const revoked = await revokeQuietly(token, scope, created.id);
|
|
35
|
+
throw new Error(`storing service token ${created.id} failed (${describeError(err)}); ${revoked ? 'the token was revoked' : 'revoking it failed too, revoke it by id'}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const store = z
|
|
39
|
+
.object({ vault: vaultSlug, environment: environmentSlug, name: secretName })
|
|
40
|
+
.describe('Secret the new token value is written to, created or versioned');
|
|
41
|
+
export function registerServiceTokenTools(server) {
|
|
42
|
+
server.tool('list_ferrvault_service_tokens', 'List the active service tokens (operator tokens) bound to one vault environment: id, name, role, creator, creation, expiry and last use. Never includes the token value. Requires the admin role on the vault.', { vault: vaultSlug, environment: environmentSlug }, ({ vault, environment }) => guarded(async () => {
|
|
43
|
+
const token = await getToken();
|
|
44
|
+
const scope = { vault, environment };
|
|
45
|
+
const rows = await vaultRequest(serviceTokensPath(scope), { token });
|
|
46
|
+
return textResult(rows.map((row) => metadata(row, scope)));
|
|
47
|
+
}));
|
|
48
|
+
server.tool('create_ferrvault_service_token', 'Create a service token (operator token) bound to one vault environment with a role, and write its value into a secret (created, or versioned when it exists) inside the MCP server. The token value is never returned, so it never enters the conversation. If the write fails, the new token is revoked. Requires the admin role on the vault, and write access to the store secret.', {
|
|
49
|
+
vault: vaultSlug,
|
|
50
|
+
environment: environmentSlug,
|
|
51
|
+
name: serviceTokenName,
|
|
52
|
+
role: vaultRole,
|
|
53
|
+
expires_at: expiresAt.optional(),
|
|
54
|
+
store,
|
|
55
|
+
}, ({ vault, environment, name, role, expires_at, store: into }) => guarded(async (redactLater) => {
|
|
56
|
+
const token = await getToken();
|
|
57
|
+
const scope = { vault, environment };
|
|
58
|
+
const created = await vaultRequest(serviceTokensPath(scope), {
|
|
59
|
+
token,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: { label: name, role, expires_at },
|
|
62
|
+
});
|
|
63
|
+
redactLater(created.token);
|
|
64
|
+
const written = await storeOrRevoke(token, scope, created, into);
|
|
65
|
+
return textResult({
|
|
66
|
+
token: metadata(created, scope),
|
|
67
|
+
stored: {
|
|
68
|
+
outcome: outcome(written, 'versioned'),
|
|
69
|
+
...into,
|
|
70
|
+
version: written.secret.current_version,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}));
|
|
74
|
+
server.tool('revoke_ferrvault_service_token', 'Revoke a service token of one vault environment by id. Workloads using it lose access immediately. Requires the admin role on the vault.', { vault: vaultSlug, environment: environmentSlug, id: serviceTokenId }, ({ vault, environment, id }) => guarded(async () => {
|
|
75
|
+
const token = await getToken();
|
|
76
|
+
await vaultRequest(serviceTokenPath({ vault, environment }, id), {
|
|
77
|
+
token,
|
|
78
|
+
method: 'DELETE',
|
|
79
|
+
});
|
|
80
|
+
return textResult({ revoked: id, vault, environment });
|
|
81
|
+
}));
|
|
82
|
+
}
|
package/dist/tools/vaults.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vaults.d.ts","sourceRoot":"","sources":["../../src/tools/vaults.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vaults.d.ts","sourceRoot":"","sources":["../../src/tools/vaults.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAK9D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsD1D"}
|