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