@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.d.cts CHANGED
@@ -159,11 +159,29 @@ declare class AuditLog {
159
159
  private readonly maxTotalSizeBytes;
160
160
  private readonly maxEntries;
161
161
  private rotationInFlight;
162
+ private readonly pendingWrites;
162
163
  constructor(storage: StorageBackend, masterKey: Uint8Array, config?: AuditLogConfig);
163
164
  /**
164
165
  * Append an audit entry.
166
+ *
167
+ * The on-disk persist is async and tracked via `pendingWrites`. Long-lived
168
+ * callers (the main MCP server) can ignore that tracking and let writes
169
+ * drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
170
+ * `process.exit()`s immediately after returning from a broker mutation —
171
+ * MUST await `flush()` before exiting, or in-flight writes get killed
172
+ * with the event loop and the entry is silently lost. That was the
173
+ * v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
174
+ * after a clean 7-verb lifecycle.
165
175
  */
166
176
  append(layer: AuditEntry["layer"], operation: string, identityId: string, details?: Record<string, unknown>, result?: "success" | "failure"): void;
177
+ /**
178
+ * Wait for every in-flight `append()` persist (and its rotation pass) to
179
+ * settle. Safe to call multiple times — newly-appended entries during a
180
+ * flush are also awaited. Re-entrant only at the granularity of "drain
181
+ * everything queued so far". Short-lived CLIs MUST call this before
182
+ * `process.exit()` to keep audit writes durable.
183
+ */
184
+ flush(): Promise<void>;
167
185
  private persistEntry;
168
186
  /**
169
187
  * Prune oldest audit entries when storage exceeds configured limits.
@@ -2853,6 +2871,24 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2853
2871
  private rateLimits;
2854
2872
  /** Whether the dashboard is running in standalone mode (no MCP server) */
2855
2873
  private _standaloneMode;
2874
+ /**
2875
+ * v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
2876
+ * are treated as authenticated without requiring a Bearer token or
2877
+ * dashboard session cookie. Only the `startStandaloneDashboard` boot
2878
+ * path enables this, and ONLY after the supplied passphrase successfully
2879
+ * decrypts at least one stored identity — proving the caller already
2880
+ * holds the primary secret that protects every piece of Sanctuary state.
2881
+ *
2882
+ * Rationale: the dashboard auth token is a dashboard-access credential
2883
+ * layered on top of the master-key unlock. Once the operator has already
2884
+ * presented the passphrase on the command line (terminal-side auth), a
2885
+ * second login prompt in the auto-opened browser just trains users to
2886
+ * paste secrets into web forms — the exact habit Sanctuary exists to
2887
+ * discourage. Remote (non-loopback) callers still require the bearer
2888
+ * token, so this is a localhost-only ergonomics unlock, not a network
2889
+ * policy change.
2890
+ */
2891
+ private _autoAuthLocalhost;
2856
2892
  constructor(config: DashboardConfig);
2857
2893
  /**
2858
2894
  * Inject dependencies after construction.
@@ -2874,6 +2910,21 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2874
2910
  * Exposed via /api/status so the frontend can show an appropriate banner.
2875
2911
  */
2876
2912
  setStandaloneMode(standalone: boolean): void;
2913
+ /**
2914
+ * v0.10.2: enable (or disable) the loopback auto-auth fast path. See
2915
+ * {@link _autoAuthLocalhost} for the rationale and threat model. Callers
2916
+ * should gate this on both (a) the dashboard host being a loopback
2917
+ * interface and (b) the master-key unlock having succeeded against
2918
+ * on-disk state.
2919
+ */
2920
+ setAutoAuthLocalhost(enabled: boolean): void;
2921
+ /**
2922
+ * v0.10.2: is this request from a loopback interface? We treat the
2923
+ * standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
2924
+ * as loopback so LAN clients never accidentally hit the unauthenticated
2925
+ * fast path even on hosts where the HTTP server binds 0.0.0.0.
2926
+ */
2927
+ private isLoopbackRequest;
2877
2928
  /**
2878
2929
  * Start the HTTP(S) server for the dashboard.
2879
2930
  */
package/dist/index.d.ts CHANGED
@@ -159,11 +159,29 @@ declare class AuditLog {
159
159
  private readonly maxTotalSizeBytes;
160
160
  private readonly maxEntries;
161
161
  private rotationInFlight;
162
+ private readonly pendingWrites;
162
163
  constructor(storage: StorageBackend, masterKey: Uint8Array, config?: AuditLogConfig);
163
164
  /**
164
165
  * Append an audit entry.
166
+ *
167
+ * The on-disk persist is async and tracked via `pendingWrites`. Long-lived
168
+ * callers (the main MCP server) can ignore that tracking and let writes
169
+ * drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
170
+ * `process.exit()`s immediately after returning from a broker mutation —
171
+ * MUST await `flush()` before exiting, or in-flight writes get killed
172
+ * with the event loop and the entry is silently lost. That was the
173
+ * v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
174
+ * after a clean 7-verb lifecycle.
165
175
  */
166
176
  append(layer: AuditEntry["layer"], operation: string, identityId: string, details?: Record<string, unknown>, result?: "success" | "failure"): void;
177
+ /**
178
+ * Wait for every in-flight `append()` persist (and its rotation pass) to
179
+ * settle. Safe to call multiple times — newly-appended entries during a
180
+ * flush are also awaited. Re-entrant only at the granularity of "drain
181
+ * everything queued so far". Short-lived CLIs MUST call this before
182
+ * `process.exit()` to keep audit writes durable.
183
+ */
184
+ flush(): Promise<void>;
167
185
  private persistEntry;
168
186
  /**
169
187
  * Prune oldest audit entries when storage exceeds configured limits.
@@ -2853,6 +2871,24 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2853
2871
  private rateLimits;
2854
2872
  /** Whether the dashboard is running in standalone mode (no MCP server) */
2855
2873
  private _standaloneMode;
2874
+ /**
2875
+ * v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
2876
+ * are treated as authenticated without requiring a Bearer token or
2877
+ * dashboard session cookie. Only the `startStandaloneDashboard` boot
2878
+ * path enables this, and ONLY after the supplied passphrase successfully
2879
+ * decrypts at least one stored identity — proving the caller already
2880
+ * holds the primary secret that protects every piece of Sanctuary state.
2881
+ *
2882
+ * Rationale: the dashboard auth token is a dashboard-access credential
2883
+ * layered on top of the master-key unlock. Once the operator has already
2884
+ * presented the passphrase on the command line (terminal-side auth), a
2885
+ * second login prompt in the auto-opened browser just trains users to
2886
+ * paste secrets into web forms — the exact habit Sanctuary exists to
2887
+ * discourage. Remote (non-loopback) callers still require the bearer
2888
+ * token, so this is a localhost-only ergonomics unlock, not a network
2889
+ * policy change.
2890
+ */
2891
+ private _autoAuthLocalhost;
2856
2892
  constructor(config: DashboardConfig);
2857
2893
  /**
2858
2894
  * Inject dependencies after construction.
@@ -2874,6 +2910,21 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2874
2910
  * Exposed via /api/status so the frontend can show an appropriate banner.
2875
2911
  */
2876
2912
  setStandaloneMode(standalone: boolean): void;
2913
+ /**
2914
+ * v0.10.2: enable (or disable) the loopback auto-auth fast path. See
2915
+ * {@link _autoAuthLocalhost} for the rationale and threat model. Callers
2916
+ * should gate this on both (a) the dashboard host being a loopback
2917
+ * interface and (b) the master-key unlock having succeeded against
2918
+ * on-disk state.
2919
+ */
2920
+ setAutoAuthLocalhost(enabled: boolean): void;
2921
+ /**
2922
+ * v0.10.2: is this request from a loopback interface? We treat the
2923
+ * standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
2924
+ * as loopback so LAN clients never accidentally hit the unauthenticated
2925
+ * fast path even on hosts where the HTTP server binds 0.0.0.0.
2926
+ */
2927
+ private isLoopbackRequest;
2877
2928
  /**
2878
2929
  * Start the HTTP(S) server for the dashboard.
2879
2930
  */
package/dist/index.js CHANGED
@@ -1978,6 +1978,7 @@ var AuditLog = class {
1978
1978
  maxTotalSizeBytes;
1979
1979
  maxEntries;
1980
1980
  rotationInFlight = false;
1981
+ pendingWrites = /* @__PURE__ */ new Set();
1981
1982
  constructor(storage, masterKey, config) {
1982
1983
  this.storage = storage;
1983
1984
  this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
@@ -1986,6 +1987,15 @@ var AuditLog = class {
1986
1987
  }
1987
1988
  /**
1988
1989
  * Append an audit entry.
1990
+ *
1991
+ * The on-disk persist is async and tracked via `pendingWrites`. Long-lived
1992
+ * callers (the main MCP server) can ignore that tracking and let writes
1993
+ * drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
1994
+ * `process.exit()`s immediately after returning from a broker mutation —
1995
+ * MUST await `flush()` before exiting, or in-flight writes get killed
1996
+ * with the event loop and the entry is silently lost. That was the
1997
+ * v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
1998
+ * after a clean 7-verb lifecycle.
1989
1999
  */
1990
2000
  append(layer, operation, identityId, details, result = "success") {
1991
2001
  const entry = {
@@ -1997,8 +2007,22 @@ var AuditLog = class {
1997
2007
  details
1998
2008
  };
1999
2009
  this.entries.push(entry);
2000
- this.persistEntry(entry).catch(() => {
2010
+ const writePromise = this.persistEntry(entry).catch(() => {
2001
2011
  });
2012
+ this.pendingWrites.add(writePromise);
2013
+ void writePromise.finally(() => this.pendingWrites.delete(writePromise));
2014
+ }
2015
+ /**
2016
+ * Wait for every in-flight `append()` persist (and its rotation pass) to
2017
+ * settle. Safe to call multiple times — newly-appended entries during a
2018
+ * flush are also awaited. Re-entrant only at the granularity of "drain
2019
+ * everything queued so far". Short-lived CLIs MUST call this before
2020
+ * `process.exit()` to keep audit writes durable.
2021
+ */
2022
+ async flush() {
2023
+ while (this.pendingWrites.size > 0) {
2024
+ await Promise.allSettled([...this.pendingWrites]);
2025
+ }
2002
2026
  }
2003
2027
  async persistEntry(entry) {
2004
2028
  const key = `${Date.now()}-${this.counter++}`;
@@ -2009,7 +2033,7 @@ var AuditLog = class {
2009
2033
  key,
2010
2034
  stringToBytes(JSON.stringify(encrypted))
2011
2035
  );
2012
- this.maybeRotate().catch(() => {
2036
+ await this.maybeRotate().catch(() => {
2013
2037
  });
2014
2038
  }
2015
2039
  /**
@@ -8676,6 +8700,24 @@ var DashboardApprovalChannel = class {
8676
8700
  rateLimits = /* @__PURE__ */ new Map();
8677
8701
  /** Whether the dashboard is running in standalone mode (no MCP server) */
8678
8702
  _standaloneMode = false;
8703
+ /**
8704
+ * v0.10.2: when set, requests from loopback addresses (127.0.0.1 / ::1)
8705
+ * are treated as authenticated without requiring a Bearer token or
8706
+ * dashboard session cookie. Only the `startStandaloneDashboard` boot
8707
+ * path enables this, and ONLY after the supplied passphrase successfully
8708
+ * decrypts at least one stored identity — proving the caller already
8709
+ * holds the primary secret that protects every piece of Sanctuary state.
8710
+ *
8711
+ * Rationale: the dashboard auth token is a dashboard-access credential
8712
+ * layered on top of the master-key unlock. Once the operator has already
8713
+ * presented the passphrase on the command line (terminal-side auth), a
8714
+ * second login prompt in the auto-opened browser just trains users to
8715
+ * paste secrets into web forms — the exact habit Sanctuary exists to
8716
+ * discourage. Remote (non-loopback) callers still require the bearer
8717
+ * token, so this is a localhost-only ergonomics unlock, not a network
8718
+ * policy change.
8719
+ */
8720
+ _autoAuthLocalhost = false;
8679
8721
  constructor(config) {
8680
8722
  this.config = config;
8681
8723
  this.authToken = config.auth_token;
@@ -8711,6 +8753,26 @@ var DashboardApprovalChannel = class {
8711
8753
  setStandaloneMode(standalone) {
8712
8754
  this._standaloneMode = standalone;
8713
8755
  }
8756
+ /**
8757
+ * v0.10.2: enable (or disable) the loopback auto-auth fast path. See
8758
+ * {@link _autoAuthLocalhost} for the rationale and threat model. Callers
8759
+ * should gate this on both (a) the dashboard host being a loopback
8760
+ * interface and (b) the master-key unlock having succeeded against
8761
+ * on-disk state.
8762
+ */
8763
+ setAutoAuthLocalhost(enabled) {
8764
+ this._autoAuthLocalhost = enabled;
8765
+ }
8766
+ /**
8767
+ * v0.10.2: is this request from a loopback interface? We treat the
8768
+ * standard IPv4/IPv6 loopback addresses plus the IPv4-mapped IPv6 form
8769
+ * as loopback so LAN clients never accidentally hit the unauthenticated
8770
+ * fast path even on hosts where the HTTP server binds 0.0.0.0.
8771
+ */
8772
+ isLoopbackRequest(req) {
8773
+ const addr = this.getRemoteAddr(req);
8774
+ return addr === "127.0.0.1" || addr === "::1" || addr === "localhost";
8775
+ }
8714
8776
  /**
8715
8777
  * Start the HTTP(S) server for the dashboard.
8716
8778
  */
@@ -8860,6 +8922,9 @@ var DashboardApprovalChannel = class {
8860
8922
  */
8861
8923
  checkAuth(req, url, res) {
8862
8924
  if (!this.authToken) return true;
8925
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
8926
+ return true;
8927
+ }
8863
8928
  const authHeader = req.headers.authorization;
8864
8929
  if (authHeader) {
8865
8930
  const parts = authHeader.split(" ");
@@ -8885,6 +8950,9 @@ var DashboardApprovalChannel = class {
8885
8950
  */
8886
8951
  isAuthenticated(req, url) {
8887
8952
  if (!this.authToken) return true;
8953
+ if (this._autoAuthLocalhost && this.isLoopbackRequest(req)) {
8954
+ return true;
8955
+ }
8888
8956
  const authHeader = req.headers.authorization;
8889
8957
  if (authHeader) {
8890
8958
  const parts = authHeader.split(" ");