@nylorun/runtime 0.6.0-beta → 0.7.0-beta

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.
@@ -0,0 +1,31 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { McpServerManifest } from "@nylorun/core/define";
3
+ import type { AuthorizeResult } from "../vault/service.js";
4
+ import { type McpDiagnostic, type McpToolRecord } from "./snapshot.js";
5
+ export interface LiveConnection {
6
+ readonly client: Client;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare function openMcpServer(input: {
10
+ server: McpServerManifest;
11
+ authorize?: (url: string) => Promise<AuthorizeResult>;
12
+ pluginRoot?: string;
13
+ pluginData: string;
14
+ }): Promise<LiveConnection>;
15
+ export declare function listMcpTools(client: Client, input: {
16
+ capabilityId: string;
17
+ serverName: string;
18
+ taken: Set<string>;
19
+ }): Promise<{
20
+ tools: McpToolRecord[];
21
+ omitted: string[];
22
+ }>;
23
+ export declare function callMcpTool(client: Client, serverToolName: string, args: unknown): Promise<{
24
+ kind: "completed";
25
+ output: unknown;
26
+ } | {
27
+ kind: "failed";
28
+ code: string;
29
+ message: string;
30
+ }>;
31
+ export declare function diagnosticFromError(capabilityId: string, serverName: string, error: unknown): McpDiagnostic;
@@ -0,0 +1,190 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { schemaFromJSON } from "@nylorun/core/define";
6
+ import { modelToolName, } from "./snapshot.js";
7
+ import { prepareStdioLaunch } from "./stdio.js";
8
+ export async function openMcpServer(input) {
9
+ if (input.server.type === "stdio") {
10
+ const launch = prepareStdioLaunch(input.server, {
11
+ pluginRoot: input.pluginRoot,
12
+ pluginData: input.pluginData,
13
+ });
14
+ const transport = new StdioClientTransport({
15
+ command: launch.command,
16
+ args: [...launch.args],
17
+ cwd: launch.cwd,
18
+ env: { ...launch.env },
19
+ stderr: "pipe",
20
+ });
21
+ transport.stderr?.on("data", () => { });
22
+ const client = createClient();
23
+ await client.connect(transport);
24
+ return { client, close: () => client.close() };
25
+ }
26
+ const authorize = input.authorize;
27
+ if (!authorize)
28
+ throw new Error("HTTP MCP server requires authorization");
29
+ const initial = await authorize(input.server.url);
30
+ if (initial.status === "refused") {
31
+ const error = new Error(`MCP authorization refused: ${initial.reason}`);
32
+ error.credentialIds =
33
+ initial.credentialIds;
34
+ throw error;
35
+ }
36
+ const fetchImpl = authorizedFetch(input.server, authorize);
37
+ const url = new URL(input.server.url);
38
+ const transport = input.server.type === "sse"
39
+ ? new SSEClientTransport(url, { fetch: fetchImpl })
40
+ : new StreamableHTTPClientTransport(url, { fetch: fetchImpl });
41
+ const client = createClient();
42
+ await client.connect(transport);
43
+ return { client, close: () => client.close() };
44
+ }
45
+ export async function listMcpTools(client, input) {
46
+ const tools = [];
47
+ const omitted = [];
48
+ let cursor;
49
+ for (let page = 0; page < 100; page += 1) {
50
+ const listed = await client.listTools(cursor ? { cursor } : undefined);
51
+ for (const tool of listed.tools) {
52
+ if (typeof tool.name !== "string" || tool.name.length === 0)
53
+ continue;
54
+ const name = modelToolName(input.serverName, tool.name);
55
+ const inputSchema = usableSchema(tool.inputSchema);
56
+ if (!inputSchema || input.taken.has(name)) {
57
+ omitted.push(name);
58
+ continue;
59
+ }
60
+ const outputSchema = tool.outputSchema
61
+ ? usableSchema(tool.outputSchema)
62
+ : undefined;
63
+ input.taken.add(name);
64
+ tools.push({
65
+ capabilityId: input.capabilityId,
66
+ serverName: input.serverName,
67
+ serverToolName: tool.name,
68
+ name,
69
+ ...(typeof tool.description === "string" && tool.description.length > 0
70
+ ? { description: tool.description }
71
+ : {}),
72
+ inputSchema,
73
+ ...(outputSchema === undefined ? {} : { outputSchema }),
74
+ });
75
+ }
76
+ cursor = listed.nextCursor;
77
+ if (!cursor)
78
+ break;
79
+ }
80
+ return { tools, omitted };
81
+ }
82
+ export async function callMcpTool(client, serverToolName, args) {
83
+ const result = await client.callTool({
84
+ name: serverToolName,
85
+ arguments: args && typeof args === "object" && !Array.isArray(args)
86
+ ? args
87
+ : {},
88
+ });
89
+ if ("isError" in result && result.isError) {
90
+ return {
91
+ kind: "failed",
92
+ code: "mcp.tool",
93
+ message: textContent(result.content) || "MCP tool failed",
94
+ };
95
+ }
96
+ if ("structuredContent" in result && result.structuredContent !== undefined)
97
+ return { kind: "completed", output: result.structuredContent };
98
+ const text = "content" in result ? textContent(result.content) : undefined;
99
+ if (text !== undefined)
100
+ return { kind: "completed", output: text };
101
+ return {
102
+ kind: "completed",
103
+ output: "content" in result ? result.content : result,
104
+ };
105
+ }
106
+ export function diagnosticFromError(capabilityId, serverName, error) {
107
+ const credentialIds = error && typeof error === "object" && "credentialIds" in error
108
+ ? error.credentialIds
109
+ : undefined;
110
+ const refused = error instanceof Error && error.message.startsWith("MCP authorization refused");
111
+ if (refused) {
112
+ return {
113
+ capabilityId,
114
+ serverName,
115
+ outcome: "refused",
116
+ message: error.message,
117
+ ...(credentialIds === undefined ? {} : { credentialIds }),
118
+ };
119
+ }
120
+ const code = error && typeof error === "object" && "code" in error
121
+ ? error.code
122
+ : undefined;
123
+ const unauthorized = code === 401 ||
124
+ (error instanceof Error && /\b401\b/.test(error.message));
125
+ return {
126
+ capabilityId,
127
+ serverName,
128
+ outcome: "failed",
129
+ message: unauthorized
130
+ ? "MCP server returned 401"
131
+ : error instanceof Error
132
+ ? error.message.slice(0, 500)
133
+ : "MCP server connection failed",
134
+ };
135
+ }
136
+ function createClient() {
137
+ return new Client({ name: "nylorun-runtime", version: "0.6.0" }, { capabilities: {} });
138
+ }
139
+ function authorizedFetch(server, authorize) {
140
+ return async (url, init) => {
141
+ const target = typeof url === "string" ? url : url.href;
142
+ if (!sameOrigin(target, server.url))
143
+ throw new Error("MCP request origin does not match the declared server");
144
+ const result = await authorize(server.url);
145
+ if (result.status === "refused") {
146
+ const error = new Error(`MCP authorization refused: ${result.reason}`);
147
+ error.credentialIds =
148
+ result.credentialIds;
149
+ throw error;
150
+ }
151
+ const headers = new Headers();
152
+ for (const [key, value] of Object.entries(server.headers ?? {}))
153
+ headers.set(key, value);
154
+ new Headers(init?.headers).forEach((value, key) => headers.set(key, value));
155
+ if (result.status === "authorized")
156
+ headers.set("authorization", result.headers.authorization);
157
+ return fetch(target, { ...init, headers, redirect: "error" });
158
+ };
159
+ }
160
+ function sameOrigin(requestUrl, serverUrl) {
161
+ try {
162
+ return new URL(requestUrl).origin === new URL(serverUrl).origin;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ function usableSchema(value) {
169
+ if (!value || typeof value !== "object" || Array.isArray(value))
170
+ return undefined;
171
+ const schema = JSON.parse(JSON.stringify(value));
172
+ try {
173
+ schemaFromJSON(schema);
174
+ }
175
+ catch {
176
+ return undefined;
177
+ }
178
+ return schema;
179
+ }
180
+ function textContent(content) {
181
+ if (!Array.isArray(content))
182
+ return undefined;
183
+ const text = content
184
+ .filter((part) => !!part &&
185
+ typeof part === "object" &&
186
+ part.type === "text" &&
187
+ typeof part.text === "string")
188
+ .map((part) => part.text);
189
+ return text.length === 0 ? undefined : text.join("\n");
190
+ }
@@ -0,0 +1,45 @@
1
+ import type { AgentManifest } from "@nylorun/core/define";
2
+ import type { AuthorizeResult } from "../vault/service.js";
3
+ import { callMcpTool } from "./connect.js";
4
+ import { type McpDiagnostic, type McpSnapshot, type McpToolRecord } from "./snapshot.js";
5
+ export declare class McpPool {
6
+ private readonly options;
7
+ private readonly live;
8
+ constructor(options: {
9
+ readonly dataDir: string;
10
+ readonly authorize: (sessionId: string, request: {
11
+ url: string;
12
+ serverName: string;
13
+ }) => Promise<AuthorizeResult>;
14
+ });
15
+ discover(input: {
16
+ sessionId: string;
17
+ manifest: AgentManifest;
18
+ manifestHash: string;
19
+ pluginRoots: Readonly<Record<string, string>>;
20
+ signal?: AbortSignal;
21
+ }): Promise<{
22
+ snapshot: McpSnapshot;
23
+ diagnostics: McpDiagnostic[];
24
+ }>;
25
+ reconnect(input: {
26
+ sessionId: string;
27
+ manifest: AgentManifest;
28
+ pluginRoots: Readonly<Record<string, string>>;
29
+ tools: readonly McpToolRecord[];
30
+ signal?: AbortSignal;
31
+ }): Promise<McpDiagnostic[]>;
32
+ call(input: {
33
+ sessionId: string;
34
+ capabilityId: string;
35
+ serverName: string;
36
+ serverToolName: string;
37
+ args: unknown;
38
+ manifest: AgentManifest;
39
+ pluginRoots: Readonly<Record<string, string>>;
40
+ }): Promise<Awaited<ReturnType<typeof callMcpTool>>>;
41
+ close(): Promise<void>;
42
+ private remember;
43
+ private liveKey;
44
+ private connectDeclared;
45
+ }
@@ -0,0 +1,143 @@
1
+ import { join } from "node:path";
2
+ import { callMcpTool, diagnosticFromError, listMcpTools, openMcpServer, } from "./connect.js";
3
+ import { declaredToolNames, } from "./snapshot.js";
4
+ export class McpPool {
5
+ options;
6
+ live = new Map();
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async discover(input) {
11
+ const taken = declaredToolNames(input.manifest);
12
+ const tools = [];
13
+ const diagnostics = [];
14
+ for (const declared of serversOf(input.manifest)) {
15
+ input.signal?.throwIfAborted();
16
+ const opened = await this.connectDeclared(input, declared);
17
+ if (!opened.ok) {
18
+ diagnostics.push(opened.diagnostic);
19
+ continue;
20
+ }
21
+ try {
22
+ const listed = await listMcpTools(opened.connection.client, {
23
+ capabilityId: declared.capabilityId,
24
+ serverName: declared.server.name,
25
+ taken,
26
+ });
27
+ tools.push(...listed.tools);
28
+ diagnostics.push({
29
+ capabilityId: declared.capabilityId,
30
+ serverName: declared.server.name,
31
+ outcome: "connected",
32
+ message: listed.omitted.length === 0
33
+ ? "Connected"
34
+ : `Connected. Omitted ${listed.omitted.join(", ")}`,
35
+ });
36
+ this.remember(input.sessionId, declared, opened.connection);
37
+ }
38
+ catch (error) {
39
+ await opened.connection.close().catch(() => { });
40
+ diagnostics.push(diagnosticFromError(declared.capabilityId, declared.server.name, error));
41
+ }
42
+ }
43
+ return {
44
+ snapshot: {
45
+ snapshotSchemaVersion: 1,
46
+ manifestHash: input.manifestHash,
47
+ mcpTools: tools,
48
+ },
49
+ diagnostics,
50
+ };
51
+ }
52
+ async reconnect(input) {
53
+ const diagnostics = [];
54
+ const seen = new Set();
55
+ for (const tool of input.tools) {
56
+ const key = `${tool.capabilityId}\0${tool.serverName}`;
57
+ if (seen.has(key))
58
+ continue;
59
+ seen.add(key);
60
+ if (this.live.has(this.liveKey(input.sessionId, tool.capabilityId, tool.serverName)))
61
+ continue;
62
+ input.signal?.throwIfAborted();
63
+ const declared = serversOf(input.manifest).find((item) => item.capabilityId === tool.capabilityId && item.server.name === tool.serverName);
64
+ if (!declared) {
65
+ diagnostics.push({
66
+ capabilityId: tool.capabilityId,
67
+ serverName: tool.serverName,
68
+ outcome: "failed",
69
+ message: "Declared MCP server is missing from the pinned manifest",
70
+ });
71
+ continue;
72
+ }
73
+ const opened = await this.connectDeclared(input, declared);
74
+ if (!opened.ok) {
75
+ diagnostics.push(opened.diagnostic);
76
+ continue;
77
+ }
78
+ this.remember(input.sessionId, declared, opened.connection);
79
+ }
80
+ return diagnostics;
81
+ }
82
+ async call(input) {
83
+ const key = this.liveKey(input.sessionId, input.capabilityId, input.serverName);
84
+ let live = this.live.get(key);
85
+ if (!live) {
86
+ const declared = serversOf(input.manifest).find((item) => item.capabilityId === input.capabilityId && item.server.name === input.serverName);
87
+ if (!declared)
88
+ throw new Error(`MCP server '${input.serverName}' is not declared`);
89
+ const opened = await this.connectDeclared(input, declared);
90
+ if (!opened.ok)
91
+ throw new Error(opened.diagnostic.message);
92
+ this.remember(input.sessionId, declared, opened.connection);
93
+ live = this.live.get(key);
94
+ }
95
+ if (!live)
96
+ throw new Error(`MCP server '${input.serverName}' is not connected`);
97
+ return callMcpTool(live.connection.client, input.serverToolName, input.args);
98
+ }
99
+ async close() {
100
+ const connections = [...this.live.values()];
101
+ this.live.clear();
102
+ await Promise.all(connections.map((item) => item.connection.close().catch(() => { })));
103
+ }
104
+ remember(sessionId, declared, connection) {
105
+ this.live.set(this.liveKey(sessionId, declared.capabilityId, declared.server.name), {
106
+ capabilityId: declared.capabilityId,
107
+ serverName: declared.server.name,
108
+ connection,
109
+ });
110
+ }
111
+ liveKey(sessionId, capabilityId, serverName) {
112
+ return JSON.stringify([sessionId, capabilityId, serverName]);
113
+ }
114
+ async connectDeclared(input, declared) {
115
+ try {
116
+ const connection = await openMcpServer({
117
+ server: declared.server,
118
+ pluginRoot: input.pluginRoots[declared.capabilityId],
119
+ pluginData: join(this.options.dataDir, "plugin-data", declared.capabilityId),
120
+ authorize: declared.server.type === "stdio"
121
+ ? undefined
122
+ : (url) => this.options.authorize(input.sessionId, {
123
+ url,
124
+ serverName: declared.server.name,
125
+ }),
126
+ });
127
+ return { ok: true, connection };
128
+ }
129
+ catch (error) {
130
+ return {
131
+ ok: false,
132
+ diagnostic: diagnosticFromError(declared.capabilityId, declared.server.name, error),
133
+ };
134
+ }
135
+ }
136
+ }
137
+ function serversOf(manifest) {
138
+ const servers = [];
139
+ for (const capability of manifest.capabilities)
140
+ for (const server of Object.values(capability.mcpServers ?? {}))
141
+ servers.push({ capabilityId: capability.id, server });
142
+ return servers;
143
+ }
@@ -0,0 +1,25 @@
1
+ import type { JsonObject } from "@nylorun/core/define";
2
+ import type { AgentManifest } from "@nylorun/core/define";
3
+ export interface McpToolRecord {
4
+ readonly capabilityId: string;
5
+ readonly serverName: string;
6
+ readonly serverToolName: string;
7
+ readonly name: string;
8
+ readonly description?: string;
9
+ readonly inputSchema: JsonObject;
10
+ readonly outputSchema?: JsonObject;
11
+ }
12
+ export interface McpSnapshot {
13
+ readonly snapshotSchemaVersion: 1;
14
+ readonly manifestHash: string;
15
+ readonly mcpTools: readonly McpToolRecord[];
16
+ }
17
+ export interface McpDiagnostic {
18
+ readonly capabilityId: string;
19
+ readonly serverName: string;
20
+ readonly outcome: "connected" | "refused" | "failed";
21
+ readonly message: string;
22
+ readonly credentialIds?: readonly string[];
23
+ }
24
+ export declare function modelToolName(serverName: string, serverToolName: string): string;
25
+ export declare function declaredToolNames(manifest: AgentManifest): Set<string>;
@@ -0,0 +1,10 @@
1
+ export function modelToolName(serverName, serverToolName) {
2
+ return `${serverName}__${serverToolName}`;
3
+ }
4
+ export function declaredToolNames(manifest) {
5
+ const names = new Set();
6
+ for (const capability of manifest.capabilities)
7
+ for (const tool of capability.tools ?? [])
8
+ names.add(tool.name);
9
+ return names;
10
+ }
@@ -0,0 +1,15 @@
1
+ import type { McpServerManifest } from "@nylorun/core/define";
2
+ /** Single-pass replacement. Text inserted for a token is not scanned again. */
3
+ export declare function expandPluginPlaceholders(value: string, pluginRoot: string, pluginData: string): string;
4
+ export interface StdioLaunch {
5
+ readonly command: string;
6
+ readonly args: readonly string[];
7
+ readonly cwd: string;
8
+ readonly env: Readonly<Record<string, string>>;
9
+ }
10
+ export declare function prepareStdioLaunch(server: Extract<McpServerManifest, {
11
+ type: "stdio";
12
+ }>, options: {
13
+ readonly pluginRoot?: string;
14
+ readonly pluginData: string;
15
+ }): StdioLaunch;
@@ -0,0 +1,80 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ const ROOT_TOKEN = "${PLUGIN_ROOT}";
4
+ const DATA_TOKEN = "${PLUGIN_DATA}";
5
+ /** Single-pass replacement. Text inserted for a token is not scanned again. */
6
+ export function expandPluginPlaceholders(value, pluginRoot, pluginData) {
7
+ let out = "";
8
+ for (let index = 0; index < value.length;) {
9
+ if (value.startsWith(ROOT_TOKEN, index)) {
10
+ out += pluginRoot;
11
+ index += ROOT_TOKEN.length;
12
+ continue;
13
+ }
14
+ if (value.startsWith(DATA_TOKEN, index)) {
15
+ out += pluginData;
16
+ index += DATA_TOKEN.length;
17
+ continue;
18
+ }
19
+ out += value[index];
20
+ index += 1;
21
+ }
22
+ return out;
23
+ }
24
+ export function prepareStdioLaunch(server, options) {
25
+ const pluginData = options.pluginData;
26
+ mkdirSync(pluginData, { recursive: true });
27
+ const pluginRoot = options.pluginRoot;
28
+ if (needsPluginRoot(server) && !pluginRoot)
29
+ throw new Error("stdio server requires a plugin root");
30
+ for (const key of Object.keys(server.env ?? {})) {
31
+ if (key === "PLUGIN_ROOT" || key === "PLUGIN_DATA")
32
+ throw new Error(`stdio env must not set ${key}`);
33
+ }
34
+ const command = server.command.startsWith("./")
35
+ ? resolve(pluginRoot, server.command)
36
+ : server.command;
37
+ const args = (server.args ?? []).map((arg) => expand(arg, pluginRoot, pluginData));
38
+ const cwd = server.cwd
39
+ ? resolveContainedCwd(server.cwd, pluginRoot, pluginData)
40
+ : (pluginRoot ?? pluginData);
41
+ const env = {};
42
+ for (const [key, value] of Object.entries(server.env ?? {}))
43
+ env[key] = expand(value, pluginRoot, pluginData);
44
+ if (pluginRoot)
45
+ env.PLUGIN_ROOT = pluginRoot;
46
+ env.PLUGIN_DATA = pluginData;
47
+ return { command, args, cwd, env };
48
+ }
49
+ function needsPluginRoot(server) {
50
+ if (server.command.startsWith("./"))
51
+ return true;
52
+ const values = [
53
+ ...(server.args ?? []),
54
+ ...Object.values(server.env ?? {}),
55
+ ...(server.cwd === undefined ? [] : [server.cwd]),
56
+ ];
57
+ return values.some((value) => value.includes(ROOT_TOKEN));
58
+ }
59
+ function expand(value, pluginRoot, pluginData) {
60
+ if (value.includes(ROOT_TOKEN) && !pluginRoot)
61
+ throw new Error("stdio server requires a plugin root");
62
+ return expandPluginPlaceholders(value, pluginRoot ?? "", pluginData);
63
+ }
64
+ function resolveContainedCwd(cwd, pluginRoot, pluginData) {
65
+ const expanded = expand(cwd, pluginRoot, pluginData);
66
+ const base = cwdTargetsData(cwd) ? pluginData : pluginRoot;
67
+ if (!base)
68
+ throw new Error("stdio server requires a plugin root");
69
+ const absolute = isAbsolute(expanded) ? expanded : resolve(base, expanded);
70
+ if (!isInside(base, absolute))
71
+ throw new Error(`Working directory escapes ${base}`);
72
+ return absolute;
73
+ }
74
+ function cwdTargetsData(cwd) {
75
+ return cwd === DATA_TOKEN || cwd.startsWith(`${DATA_TOKEN}/`);
76
+ }
77
+ function isInside(root, target) {
78
+ const rel = relative(root, target);
79
+ return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
80
+ }
@@ -0,0 +1,12 @@
1
+ export declare class VaultCryptoError extends Error {
2
+ constructor();
3
+ }
4
+ export declare function kekId(kek: Buffer): string;
5
+ export declare function parseKek(value: Buffer | string): Buffer;
6
+ export declare function encryptSecret(kek: Buffer, aad: Buffer, plaintext: Buffer): {
7
+ nonce: Buffer;
8
+ ciphertext: Buffer;
9
+ wrappedDek: Buffer;
10
+ kekId: string;
11
+ };
12
+ export declare function decryptSecret(kek: Buffer, aad: Buffer, nonce: Buffer, ciphertext: Buffer, wrappedDek: Buffer, expectedKekId: string): Buffer;
@@ -0,0 +1,80 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, } from "node:crypto";
2
+ const IV_BYTES = 12;
3
+ const TAG_BYTES = 16;
4
+ export class VaultCryptoError extends Error {
5
+ constructor() {
6
+ super("Vault credential could not be read");
7
+ }
8
+ }
9
+ export function kekId(kek) {
10
+ return createHash("sha256").update(kek).digest("hex");
11
+ }
12
+ export function parseKek(value) {
13
+ const bytes = typeof value === "string" ? Buffer.from(value.trim(), "base64") : value;
14
+ if (bytes.length !== 32)
15
+ throw new Error("Vault key-encryption key must be 32 bytes");
16
+ return Buffer.from(bytes);
17
+ }
18
+ export function encryptSecret(kek, aad, plaintext) {
19
+ const dek = randomBytes(32);
20
+ try {
21
+ const nonce = randomBytes(IV_BYTES);
22
+ const cipher = createCipheriv("aes-256-gcm", dek, nonce);
23
+ cipher.setAAD(aad);
24
+ const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
25
+ const tag = cipher.getAuthTag();
26
+ return {
27
+ nonce,
28
+ ciphertext: Buffer.concat([body, tag]),
29
+ wrappedDek: wrapKey(kek, dek),
30
+ kekId: kekId(kek),
31
+ };
32
+ }
33
+ finally {
34
+ dek.fill(0);
35
+ }
36
+ }
37
+ export function decryptSecret(kek, aad, nonce, ciphertext, wrappedDek, expectedKekId) {
38
+ if (expectedKekId !== kekId(kek))
39
+ throw new VaultCryptoError();
40
+ const dek = unwrapKey(kek, wrappedDek);
41
+ try {
42
+ if (ciphertext.length < TAG_BYTES)
43
+ throw new VaultCryptoError();
44
+ const tag = ciphertext.subarray(ciphertext.length - TAG_BYTES);
45
+ const body = ciphertext.subarray(0, ciphertext.length - TAG_BYTES);
46
+ const decipher = createDecipheriv("aes-256-gcm", dek, nonce);
47
+ decipher.setAAD(aad);
48
+ decipher.setAuthTag(tag);
49
+ return Buffer.concat([decipher.update(body), decipher.final()]);
50
+ }
51
+ catch (error) {
52
+ if (error instanceof VaultCryptoError)
53
+ throw error;
54
+ throw new VaultCryptoError();
55
+ }
56
+ finally {
57
+ dek.fill(0);
58
+ }
59
+ }
60
+ function wrapKey(kek, dek) {
61
+ const nonce = randomBytes(IV_BYTES);
62
+ const cipher = createCipheriv("aes-256-gcm", kek, nonce);
63
+ const body = Buffer.concat([cipher.update(dek), cipher.final()]);
64
+ return Buffer.concat([nonce, body, cipher.getAuthTag()]);
65
+ }
66
+ function unwrapKey(kek, wrapped) {
67
+ try {
68
+ if (wrapped.length < IV_BYTES + TAG_BYTES)
69
+ throw new VaultCryptoError();
70
+ const nonce = wrapped.subarray(0, IV_BYTES);
71
+ const tag = wrapped.subarray(wrapped.length - TAG_BYTES);
72
+ const body = wrapped.subarray(IV_BYTES, wrapped.length - TAG_BYTES);
73
+ const decipher = createDecipheriv("aes-256-gcm", kek, nonce);
74
+ decipher.setAuthTag(tag);
75
+ return Buffer.concat([decipher.update(body), decipher.final()]);
76
+ }
77
+ catch {
78
+ throw new VaultCryptoError();
79
+ }
80
+ }
@@ -0,0 +1,4 @@
1
+ export declare class VaultError extends Error {
2
+ readonly status: number;
3
+ constructor(status: number, message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ export class VaultError extends Error {
2
+ status;
3
+ constructor(status, message) {
4
+ super(message);
5
+ this.status = status;
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ export declare function defaultKekPath(): string;
2
+ export declare function readVaultKek(options: {
3
+ vaultKek?: Buffer | string | null;
4
+ vaultKekPath?: string;
5
+ }): Buffer | undefined;
6
+ export declare function createKekFile(path: string): Buffer;