@sanctuary-framework/mcp-server 0.10.1 → 0.10.2

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