@sanctuary-framework/mcp-server 0.10.0 → 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/index.cjs CHANGED
@@ -1981,6 +1981,7 @@ var AuditLog = class {
1981
1981
  maxTotalSizeBytes;
1982
1982
  maxEntries;
1983
1983
  rotationInFlight = false;
1984
+ pendingWrites = /* @__PURE__ */ new Set();
1984
1985
  constructor(storage, masterKey, config) {
1985
1986
  this.storage = storage;
1986
1987
  this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
@@ -1989,6 +1990,15 @@ var AuditLog = class {
1989
1990
  }
1990
1991
  /**
1991
1992
  * Append an audit entry.
1993
+ *
1994
+ * The on-disk persist is async and tracked via `pendingWrites`. Long-lived
1995
+ * callers (the main MCP server) can ignore that tracking and let writes
1996
+ * drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
1997
+ * `process.exit()`s immediately after returning from a broker mutation —
1998
+ * MUST await `flush()` before exiting, or in-flight writes get killed
1999
+ * with the event loop and the entry is silently lost. That was the
2000
+ * v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
2001
+ * after a clean 7-verb lifecycle.
1992
2002
  */
1993
2003
  append(layer, operation, identityId, details, result = "success") {
1994
2004
  const entry = {
@@ -2000,8 +2010,22 @@ var AuditLog = class {
2000
2010
  details
2001
2011
  };
2002
2012
  this.entries.push(entry);
2003
- this.persistEntry(entry).catch(() => {
2013
+ const writePromise = this.persistEntry(entry).catch(() => {
2004
2014
  });
2015
+ this.pendingWrites.add(writePromise);
2016
+ void writePromise.finally(() => this.pendingWrites.delete(writePromise));
2017
+ }
2018
+ /**
2019
+ * Wait for every in-flight `append()` persist (and its rotation pass) to
2020
+ * settle. Safe to call multiple times — newly-appended entries during a
2021
+ * flush are also awaited. Re-entrant only at the granularity of "drain
2022
+ * everything queued so far". Short-lived CLIs MUST call this before
2023
+ * `process.exit()` to keep audit writes durable.
2024
+ */
2025
+ async flush() {
2026
+ while (this.pendingWrites.size > 0) {
2027
+ await Promise.allSettled([...this.pendingWrites]);
2028
+ }
2005
2029
  }
2006
2030
  async persistEntry(entry) {
2007
2031
  const key = `${Date.now()}-${this.counter++}`;
@@ -2012,7 +2036,7 @@ var AuditLog = class {
2012
2036
  key,
2013
2037
  stringToBytes(JSON.stringify(encrypted))
2014
2038
  );
2015
- this.maybeRotate().catch(() => {
2039
+ await this.maybeRotate().catch(() => {
2016
2040
  });
2017
2041
  }
2018
2042
  /**
@@ -8679,6 +8703,24 @@ var DashboardApprovalChannel = class {
8679
8703
  rateLimits = /* @__PURE__ */ new Map();
8680
8704
  /** Whether the dashboard is running in standalone mode (no MCP server) */
8681
8705
  _standaloneMode = false;
8706
+ /**
8707
+ * v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
8708
+ * are treated as authenticated without requiring a Bearer token or
8709
+ * dashboard session cookie. Only the `startStandaloneDashboard` boot
8710
+ * path enables this, and ONLY after the supplied passphrase successfully
8711
+ * decrypts at least one stored identity — proving the caller already
8712
+ * holds the primary secret that protects every piece of Sanctuary state.
8713
+ *
8714
+ * Rationale: the dashboard auth token is a dashboard-access credential
8715
+ * layered on top of the master-key unlock. Once the operator has already
8716
+ * presented the passphrase on the command line (terminal-side auth), a
8717
+ * second login prompt in the auto-opened browser just trains users to
8718
+ * paste secrets into web forms — the exact habit Sanctuary exists to
8719
+ * discourage. Remote (non-loopback) callers still require the bearer
8720
+ * token, so this is a localhost-only ergonomics unlock, not a network
8721
+ * policy change.
8722
+ */
8723
+ _autoAuthLocalhost = false;
8682
8724
  constructor(config) {
8683
8725
  this.config = config;
8684
8726
  this.authToken = config.auth_token;
@@ -8714,6 +8756,26 @@ var DashboardApprovalChannel = class {
8714
8756
  setStandaloneMode(standalone) {
8715
8757
  this._standaloneMode = standalone;
8716
8758
  }
8759
+ /**
8760
+ * v0.10.2: enable (or disable) the loopback auto-auth fast path. See
8761
+ * {@link _autoAuthLocalhost} for the rationale and threat model. Callers
8762
+ * should gate this on both (a) the dashboard host being a loopback
8763
+ * interface and (b) the master-key unlock having succeeded against
8764
+ * on-disk state.
8765
+ */
8766
+ setAutoAuthLocalhost(enabled) {
8767
+ this._autoAuthLocalhost = enabled;
8768
+ }
8769
+ /**
8770
+ * v0.10.2: is this request from a loopback interface? We treat the
8771
+ * standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
8772
+ * as loopback so LAN clients never accidentally hit the unauthenticated
8773
+ * fast path even on hosts where the HTTP server binds 0.0.0.0.
8774
+ */
8775
+ isLoopbackRequest(req) {
8776
+ const addr = this.getRemoteAddr(req);
8777
+ return addr === "127.0.0.1" || addr === "::1" || addr === "localhost";
8778
+ }
8717
8779
  /**
8718
8780
  * Start the HTTP(S) server for the dashboard.
8719
8781
  */
@@ -8863,6 +8925,9 @@ var DashboardApprovalChannel = class {
8863
8925
  */
8864
8926
  checkAuth(req, url, res) {
8865
8927
  if (!this.authToken) return true;
8928
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
8929
+ return true;
8930
+ }
8866
8931
  const authHeader = req.headers.authorization;
8867
8932
  if (authHeader) {
8868
8933
  const parts = authHeader.split(" ");
@@ -8888,6 +8953,9 @@ var DashboardApprovalChannel = class {
8888
8953
  */
8889
8954
  isAuthenticated(req, url) {
8890
8955
  if (!this.authToken) return true;
8956
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
8957
+ return true;
8958
+ }
8891
8959
  const authHeader = req.headers.authorization;
8892
8960
  if (authHeader) {
8893
8961
  const parts = authHeader.split(" ");