@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.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(" ");
|