@sanctuary-framework/mcp-server 0.10.1 → 0.10.3

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/cli.cjs CHANGED
@@ -8832,6 +8832,24 @@ var init_dashboard = __esm({
8832
8832
  rateLimits = /* @__PURE__ */ new Map();
8833
8833
  /** Whether the dashboard is running in standalone mode (no MCP server) */
8834
8834
  _standaloneMode = false;
8835
+ /**
8836
+ * v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
8837
+ * are treated as authenticated without requiring a Bearer token or
8838
+ * dashboard session cookie. Only the `startStandaloneDashboard` boot
8839
+ * path enables this, and ONLY after the supplied passphrase successfully
8840
+ * decrypts at least one stored identity — proving the caller already
8841
+ * holds the primary secret that protects every piece of Sanctuary state.
8842
+ *
8843
+ * Rationale: the dashboard auth token is a dashboard-access credential
8844
+ * layered on top of the master-key unlock. Once the operator has already
8845
+ * presented the passphrase on the command line (terminal-side auth), a
8846
+ * second login prompt in the auto-opened browser just trains users to
8847
+ * paste secrets into web forms — the exact habit Sanctuary exists to
8848
+ * discourage. Remote (non-loopback) callers still require the bearer
8849
+ * token, so this is a localhost-only ergonomics unlock, not a network
8850
+ * policy change.
8851
+ */
8852
+ _autoAuthLocalhost = false;
8835
8853
  constructor(config) {
8836
8854
  this.config = config;
8837
8855
  this.authToken = config.auth_token;
@@ -8867,6 +8885,26 @@ var init_dashboard = __esm({
8867
8885
  setStandaloneMode(standalone) {
8868
8886
  this._standaloneMode = standalone;
8869
8887
  }
8888
+ /**
8889
+ * v0.10.2: enable (or disable) the loopback auto-auth fast path. See
8890
+ * {@link _autoAuthLocalhost} for the rationale and threat model. Callers
8891
+ * should gate this on both (a) the dashboard host being a loopback
8892
+ * interface and (b) the master-key unlock having succeeded against
8893
+ * on-disk state.
8894
+ */
8895
+ setAutoAuthLocalhost(enabled) {
8896
+ this._autoAuthLocalhost = enabled;
8897
+ }
8898
+ /**
8899
+ * v0.10.2: is this request from a loopback interface? We treat the
8900
+ * standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
8901
+ * as loopback so LAN clients never accidentally hit the unauthenticated
8902
+ * fast path even on hosts where the HTTP server binds 0.0.0.0.
8903
+ */
8904
+ isLoopbackRequest(req) {
8905
+ const addr = this.getRemoteAddr(req);
8906
+ return addr === "127.0.0.1" || addr === "::1" || addr === "localhost";
8907
+ }
8870
8908
  /**
8871
8909
  * Start the HTTP(S) server for the dashboard.
8872
8910
  */
@@ -9016,6 +9054,9 @@ var init_dashboard = __esm({
9016
9054
  */
9017
9055
  checkAuth(req, url, res) {
9018
9056
  if (!this.authToken) return true;
9057
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
9058
+ return true;
9059
+ }
9019
9060
  const authHeader = req.headers.authorization;
9020
9061
  if (authHeader) {
9021
9062
  const parts = authHeader.split(" ");
@@ -9041,6 +9082,9 @@ var init_dashboard = __esm({
9041
9082
  */
9042
9083
  isAuthenticated(req, url) {
9043
9084
  if (!this.authToken) return true;
9085
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
9086
+ return true;
9087
+ }
9044
9088
  const authHeader = req.headers.authorization;
9045
9089
  if (authHeader) {
9046
9090
  const parts = authHeader.split(" ");
@@ -27387,7 +27431,28 @@ async function startStandaloneDashboard(options = {}) {
27387
27431
  await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
27388
27432
  const storage = new FilesystemStorage(`${config.storage_path}/state`);
27389
27433
  let masterKey;
27390
- const passphrase = options.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
27434
+ let passphrase = options.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
27435
+ let passphraseSource = null;
27436
+ if (passphrase) {
27437
+ passphraseSource = options.passphrase !== void 0 ? "option" : "env";
27438
+ } else {
27439
+ try {
27440
+ const stored = await readStoredPassphrase({
27441
+ storagePath: config.storage_path
27442
+ });
27443
+ if (stored) {
27444
+ passphrase = stored.value;
27445
+ passphraseSource = stored.source === "keychain" ? "keychain" : "fallback-file";
27446
+ console.error(
27447
+ `Passphrase: loaded from ${stored.location} (service ${keychainServiceFor(config.storage_path, os.homedir())})`
27448
+ );
27449
+ }
27450
+ } catch (err) {
27451
+ if (err instanceof PassphraseUnreadableError) {
27452
+ throw err;
27453
+ }
27454
+ }
27455
+ }
27391
27456
  if (passphrase) {
27392
27457
  let existingParams;
27393
27458
  try {
@@ -27400,6 +27465,14 @@ async function startStandaloneDashboard(options = {}) {
27400
27465
  }
27401
27466
  const result = await deriveMasterKey(passphrase, existingParams);
27402
27467
  masterKey = result.key;
27468
+ if (!existingParams) {
27469
+ const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
27470
+ await storage.write(
27471
+ "_meta",
27472
+ "key-params",
27473
+ stringToBytes2(JSON.stringify(result.params))
27474
+ );
27475
+ }
27403
27476
  } else {
27404
27477
  const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
27405
27478
  const { stringToBytes: stringToBytes2, bytesToString: bytesToString2, fromBase64url: fromBase64url2, constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
@@ -27498,6 +27571,10 @@ async function startStandaloneDashboard(options = {}) {
27498
27571
  profileStore
27499
27572
  });
27500
27573
  dashboard.setStandaloneMode(true);
27574
+ const hostIsLoopback = dashboardHost === "127.0.0.1" || dashboardHost === "::1" || dashboardHost === "localhost";
27575
+ if (hostIsLoopback && loadResult.loaded > 0) {
27576
+ dashboard.setAutoAuthLocalhost(true);
27577
+ }
27501
27578
  await dashboard.start();
27502
27579
  await writeTenantRuntime(config.storage_path, {
27503
27580
  version: SANCTUARY_VERSION,
@@ -27523,21 +27600,29 @@ async function startStandaloneDashboard(options = {}) {
27523
27600
  console.error(`Identities loaded: ${loadResult.loaded}`);
27524
27601
  console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
27525
27602
  if (loadResult.total > 0 && loadResult.loaded === 0) {
27603
+ const service = keychainServiceFor(config.storage_path, os.homedir());
27604
+ const sourceLabel = passphraseSource === "option" ? "--passphrase option" : passphraseSource === "env" ? "SANCTUARY_PASSPHRASE env var" : passphraseSource === "keychain" ? `macOS Keychain (service ${service})` : passphraseSource === "fallback-file" ? "encrypted fallback file" : "recovery key";
27526
27605
  console.error(
27527
27606
  `
27528
- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
27529
- \u2551 \u26A0 WARNING: Encrypted identities found but NONE loaded \u2551
27530
- \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
27531
- \u2551 ${loadResult.total} encrypted identity file(s) found on disk \u2551
27532
- \u2551 0 could be decrypted with the current master key \u2551
27533
- \u2551 \u2551
27534
- \u2551 This usually means SANCTUARY_PASSPHRASE is missing or \u2551
27535
- \u2551 incorrect. The dashboard will show empty panels. \u2551
27536
- \u2551 \u2551
27537
- \u2551 To fix: restart with the correct SANCTUARY_PASSPHRASE: \u2551
27538
- \u2551 SANCTUARY_PASSPHRASE=<your-passphrase> npx \\ \u2551
27539
- \u2551 @sanctuary-framework/mcp-server dashboard \u2551
27540
- \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
27607
+ \u26A0 WARNING: Encrypted identities found but NONE loaded
27608
+ ${loadResult.total} encrypted identity file(s) in ${config.storage_path}/state/_identities/
27609
+ 0 could be decrypted with the master key derived from the ${sourceLabel}.
27610
+
27611
+ The dashboard will show empty panels. Since v0.10.0 each wrapped
27612
+ tenant has its OWN passphrase, stored under a per-tenant Keychain
27613
+ service name. A single SANCTUARY_PASSPHRASE unlocks at most one
27614
+ tenant \u2014 it is not a global master credential.
27615
+
27616
+ To diagnose:
27617
+ \u2022 List tenants on this host: sanctuary agents
27618
+ \u2022 Multi-tenant overview (no decrypt): sanctuary dashboard --multi
27619
+ \u2022 Point at a specific tenant: SANCTUARY_STORAGE_PATH=<path> sanctuary dashboard
27620
+ \u2022 Inspect this tenant's Keychain: security find-generic-password -s '${service}' -w
27621
+
27622
+ If this tenant's passphrase lives only in a different Keychain item
27623
+ or on another machine, restore it before this dashboard can read
27624
+ any state. Sanctuary will never auto-regenerate \u2014 that would
27625
+ permanently destroy the data encrypted under the prior key.
27541
27626
  `
27542
27627
  );
27543
27628
  } else if (loadResult.failed > 0) {
@@ -27567,6 +27652,7 @@ var init_dashboard_standalone = __esm({
27567
27652
  init_tools();
27568
27653
  init_sovereignty_profile();
27569
27654
  init_runtime();
27655
+ init_passphrase();
27570
27656
  }
27571
27657
  });
27572
27658