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