@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 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. `visible` and `use_only` fields can be used; `human_only` fields never.
385
- Output comes back scrubbed of the value (as is, URL-encoded, base64, hex, JSON-escaped) with the
386
- credentials and fields used. Scrubbing stops careless leaks, not a program written to leak one.
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
 
@@ -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.0",
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
- function encodedForms(value) {
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
- this.markers = new Map;
9039
- for (const { name, value } of secrets) {
9040
- for (const form of encodedForms(value)) {
9041
- if (form.length > 0)
9042
- this.markers.set(form, `«redacted:${name}»`);
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, (form) => this.markers.get(form));
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})\/([^{}]+)\}\}/g;
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, encode = (v) => v) {
9110
- return fillHandles(text, (ref) => encode(this.valueOf(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, encodeURIComponent), {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safekb/mcp",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Mustafa Öztürk",