@safekb/mcp 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/safekb-mcp.js +53 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -381,9 +381,11 @@ custom fields and the key name of every custom field (sealed ones too) stay read
|
|
|
381
381
|
(never argv), `secret_http` sends one request with it in the url, headers or body (redirects are not
|
|
382
382
|
followed), and `secret_adb_input` types it on an Android device (`adb shell input text`, value on stdin;
|
|
383
383
|
printable ASCII only). The agent names it by handle, `{{secret://<credential_id>/<field>}}`; a `totp`
|
|
384
|
-
handle becomes the current code. `
|
|
385
|
-
|
|
386
|
-
|
|
384
|
+
handle becomes the current code. `|form`, `|json` or `|url` encodes the value for where it goes
|
|
385
|
+
(`{{secret://<id>/password|form}}` in a form body); without one it goes in as is, URL-encoded in a url.
|
|
386
|
+
`visible` and `use_only` fields can be used; `human_only` fields never.
|
|
387
|
+
Output comes back scrubbed of the value, in any mix of percent-, form-, JSON- and adb-encoding and as
|
|
388
|
+
base64 or hex, with the credentials and fields used. Scrubbing stops careless leaks, not a program written to leak one.
|
|
387
389
|
- Visible fields can be edited without the secret. Such an edit is flagged (`visible_tampered`) with the
|
|
388
390
|
changed fields until a signed save accepts it (`accept_visible_changes`).
|
|
389
391
|
|
package/dist/safekb-mcp.js
CHANGED
|
@@ -2581,7 +2581,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2581
2581
|
// package.json
|
|
2582
2582
|
var package_default = {
|
|
2583
2583
|
name: "@safekb/mcp",
|
|
2584
|
-
version: "0.12.
|
|
2584
|
+
version: "0.12.1",
|
|
2585
2585
|
description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
|
|
2586
2586
|
license: "SEE LICENSE IN LICENSE",
|
|
2587
2587
|
author: "Mustafa Öztürk",
|
|
@@ -9010,17 +9010,26 @@ class CredentialAccess {
|
|
|
9010
9010
|
|
|
9011
9011
|
// src/sinks/redaction.ts
|
|
9012
9012
|
var MAX_SHOWN_CHARS2 = 32 * 1024;
|
|
9013
|
-
|
|
9013
|
+
var escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9014
|
+
var bothCases = (hex) => hex.toUpperCase() === hex.toLowerCase() ? hex : `(?:${hex.toUpperCase()}|${hex.toLowerCase()})`;
|
|
9015
|
+
function characterPattern(char) {
|
|
9016
|
+
const percent = [...Buffer.from(char, "utf8")].map((byte) => `%${bothCases(byte.toString(16).padStart(2, "0"))}`).join("");
|
|
9017
|
+
const unicode = [...char].map((unit) => `\\\\u${bothCases(unit.charCodeAt(0).toString(16).padStart(4, "0"))}`).join("");
|
|
9018
|
+
const forms = [escapeRegex(char), percent, unicode];
|
|
9019
|
+
if (char === " ")
|
|
9020
|
+
forms.push("\\+", "%s");
|
|
9021
|
+
if (char === "'")
|
|
9022
|
+
forms.push(escapeRegex(`'\\''`));
|
|
9023
|
+
if (char === '"' || char === "\\" || char === "/") {
|
|
9024
|
+
forms.push(escapeRegex(`\\${char}`));
|
|
9025
|
+
}
|
|
9026
|
+
return `(?:${forms.join("|")})`;
|
|
9027
|
+
}
|
|
9028
|
+
function wholeForms(value) {
|
|
9014
9029
|
const bytes = Buffer.from(value, "utf8");
|
|
9015
9030
|
const base64 = bytes.toString("base64");
|
|
9016
9031
|
const hex = bytes.toString("hex");
|
|
9017
9032
|
return [
|
|
9018
|
-
value,
|
|
9019
|
-
encodeURIComponent(value),
|
|
9020
|
-
new URLSearchParams({ v: value }).toString().slice(2),
|
|
9021
|
-
value.replaceAll(" ", "%s"),
|
|
9022
|
-
value.replaceAll(" ", "%s").replaceAll("'", `'\\''`),
|
|
9023
|
-
JSON.stringify(value).slice(1, -1),
|
|
9024
9033
|
base64,
|
|
9025
9034
|
base64.replace(/=+$/, ""),
|
|
9026
9035
|
bytes.toString("base64url"),
|
|
@@ -9028,28 +9037,27 @@ function encodedForms(value) {
|
|
|
9028
9037
|
hex.toUpperCase()
|
|
9029
9038
|
];
|
|
9030
9039
|
}
|
|
9031
|
-
var escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9032
9040
|
|
|
9033
9041
|
class Redactor {
|
|
9034
|
-
markers;
|
|
9035
9042
|
pattern;
|
|
9043
|
+
markers;
|
|
9036
9044
|
longest;
|
|
9037
9045
|
constructor(secrets) {
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
}
|
|
9045
|
-
const forms = [...this.markers.keys()].sort((a, b) => b.length - a.length);
|
|
9046
|
-
this.pattern = forms.length === 0 ? null : new RegExp(forms.map(escapeRegex).join("|"), "g");
|
|
9047
|
-
this.longest = forms[0]?.length ?? 0;
|
|
9046
|
+
const kept = secrets.filter(({ value }) => value.length > 0).sort((a, b) => b.value.length - a.value.length);
|
|
9047
|
+
this.markers = kept.map(({ name }) => `«redacted:${name}»`);
|
|
9048
|
+
this.pattern = kept.length === 0 ? null : new RegExp(kept.map(({ value }) => `(${[
|
|
9049
|
+
[...value].map(characterPattern).join(""),
|
|
9050
|
+
...wholeForms(value).map(escapeRegex)
|
|
9051
|
+
].join("|")})`).join("|"), "g");
|
|
9052
|
+
this.longest = Math.max(0, ...kept.map(({ value }) => Buffer.byteLength(value, "utf8") * 6));
|
|
9048
9053
|
}
|
|
9049
9054
|
redact(text) {
|
|
9050
9055
|
if (this.pattern === null)
|
|
9051
9056
|
return text;
|
|
9052
|
-
return text.replace(this.pattern, (
|
|
9057
|
+
return text.replace(this.pattern, (...match) => {
|
|
9058
|
+
const groups = match.slice(1, this.markers.length + 1);
|
|
9059
|
+
return this.markers[groups.findIndex((g) => g !== undefined)];
|
|
9060
|
+
});
|
|
9053
9061
|
}
|
|
9054
9062
|
shown(text, cut) {
|
|
9055
9063
|
const whole = cut ? text.slice(0, Math.max(0, text.length - this.longest)) : text;
|
|
@@ -9062,25 +9070,40 @@ class Redactor {
|
|
|
9062
9070
|
}
|
|
9063
9071
|
|
|
9064
9072
|
// src/sinks/secretHandles.ts
|
|
9065
|
-
var HANDLE = /\{\{secret:\/\/([0-9a-f]{24})\/([^{}]+)
|
|
9073
|
+
var HANDLE = /\{\{secret:\/\/([0-9a-f]{24})\/([^{}|]+)(?:\|([^{}|]*))?\}\}/g;
|
|
9066
9074
|
var ANY_HANDLE = /secret:\/\//;
|
|
9075
|
+
var ENCODINGS = {
|
|
9076
|
+
raw: (value) => value,
|
|
9077
|
+
url: encodeURIComponent,
|
|
9078
|
+
form: (value) => new URLSearchParams({ v: value }).toString().slice(2),
|
|
9079
|
+
json: (value) => JSON.stringify(value).slice(1, -1)
|
|
9080
|
+
};
|
|
9081
|
+
function encodingOf(name, fallback) {
|
|
9082
|
+
if (name === undefined)
|
|
9083
|
+
return fallback;
|
|
9084
|
+
if (!Object.hasOwn(ENCODINGS, name)) {
|
|
9085
|
+
throw new ClientError(`Unknown handle encoding '|${name}': use |url, |form, |json or |raw`);
|
|
9086
|
+
}
|
|
9087
|
+
return name;
|
|
9088
|
+
}
|
|
9067
9089
|
var handleKey = ({ credentialId, field }) => `${credentialId}/${field}`;
|
|
9068
9090
|
function refsIn(text) {
|
|
9069
9091
|
const refs = [];
|
|
9070
|
-
const rest = text.replace(HANDLE, (_, credentialId, field) => {
|
|
9092
|
+
const rest = text.replace(HANDLE, (_, credentialId, field, encoding2) => {
|
|
9093
|
+
encodingOf(encoding2, "raw");
|
|
9071
9094
|
refs.push({ credentialId, field });
|
|
9072
9095
|
return "";
|
|
9073
9096
|
});
|
|
9074
9097
|
if (ANY_HANDLE.test(rest)) {
|
|
9075
|
-
throw new ClientError("A secret handle must be written {{secret://<24-character credential id>/<field>}}, e.g. {{secret://0123456789abcdef01234567/password}}");
|
|
9098
|
+
throw new ClientError("A secret handle must be written {{secret://<24-character credential id>/<field>}}, optionally with |url, |form or |json, e.g. {{secret://0123456789abcdef01234567/password|form}}");
|
|
9076
9099
|
}
|
|
9077
9100
|
return refs;
|
|
9078
9101
|
}
|
|
9079
9102
|
function hasHandle(text) {
|
|
9080
9103
|
return ANY_HANDLE.test(text);
|
|
9081
9104
|
}
|
|
9082
|
-
function fillHandles(text, lookup) {
|
|
9083
|
-
return text.replace(HANDLE, (_, credentialId, field) => lookup({ credentialId, field }));
|
|
9105
|
+
function fillHandles(text, lookup, fallback = "raw") {
|
|
9106
|
+
return text.replace(HANDLE, (_, credentialId, field, encoding2) => ENCODINGS[encodingOf(encoding2, fallback)](lookup({ credentialId, field })));
|
|
9084
9107
|
}
|
|
9085
9108
|
function requireHandlesAfterOrigin(url) {
|
|
9086
9109
|
const first = url.search(ANY_HANDLE);
|
|
@@ -9106,8 +9129,8 @@ class ResolvedSecrets {
|
|
|
9106
9129
|
this.byKey = new Map(resolved.map((item) => [handleKey(item.ref), item]));
|
|
9107
9130
|
this.redactor = new Redactor(resolved.map(({ ref, value }) => ({ name: handleKey(ref), value })));
|
|
9108
9131
|
}
|
|
9109
|
-
fill(text,
|
|
9110
|
-
return fillHandles(text, (ref) =>
|
|
9132
|
+
fill(text, fallback = "raw") {
|
|
9133
|
+
return fillHandles(text, (ref) => this.valueOf(ref), fallback);
|
|
9111
9134
|
}
|
|
9112
9135
|
valueOf(ref) {
|
|
9113
9136
|
const resolved = this.byKey.get(handleKey(ref));
|
|
@@ -36146,7 +36169,7 @@ class HttpSink {
|
|
|
36146
36169
|
}
|
|
36147
36170
|
async exchange(args, headers, secrets) {
|
|
36148
36171
|
try {
|
|
36149
|
-
const response = await fetch(secrets.fill(args.url,
|
|
36172
|
+
const response = await fetch(secrets.fill(args.url, "url"), {
|
|
36150
36173
|
method: args.method,
|
|
36151
36174
|
headers: Object.fromEntries(Object.entries(headers).map(([name, value]) => [
|
|
36152
36175
|
secrets.fill(name),
|
|
@@ -36358,7 +36381,7 @@ function documentTools(creator, documents, search, names) {
|
|
|
36358
36381
|
}
|
|
36359
36382
|
|
|
36360
36383
|
// src/tools/sinkTools.ts
|
|
36361
|
-
var HANDLE_NOTE = "Name a secret by its handle {{secret://<credential_id>/<field>}} (field: password, totp, sealed_notes or custom:{key}); it is decrypted on this machine and the value never enters the conversation. A totp handle becomes the current code. Fields whose llm_visibility is human_only are refused.";
|
|
36384
|
+
var HANDLE_NOTE = "Name a secret by its handle {{secret://<credential_id>/<field>}} (field: password, totp, sealed_notes or custom:{key}); it is decrypted on this machine and the value never enters the conversation. A totp handle becomes the current code. Add |form, |json or |url to encode the value for where it goes, e.g. {{secret://<id>/password|form}} in a form body; without it the value goes in as is (in a url: URL-encoded). Fields whose llm_visibility is human_only are refused.";
|
|
36362
36385
|
var SCRUB_NOTE = "What comes back is scrubbed of the values (as is, URL-encoded, base64, hex, JSON-escaped) and names the secrets used. Scrubbing guards against careless output, not against a program written to leak: run only code you trust.";
|
|
36363
36386
|
var timeout = (fallback) => exports_external.number().int().min(1).max(600).default(fallback).describe("Seconds.");
|
|
36364
36387
|
function sinkTools(exec, http, adb) {
|
package/package.json
CHANGED