@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/cli.cjs +141 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +141 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +70 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +70 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(" ");
|