@sanctuary-framework/mcp-server 0.8.0 → 0.10.1
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 +6363 -1915
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +6368 -1920
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1604 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +403 -4
- package/dist/index.d.ts +403 -4
- package/dist/index.js +1600 -73
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -13,8 +13,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
13
13
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
14
|
import { createServer as createServer$2 } from 'http';
|
|
15
15
|
import { createServer as createServer$1 } from 'https';
|
|
16
|
-
import { readFileSync, statSync } from 'fs';
|
|
17
16
|
import { exec, execSync } from 'child_process';
|
|
17
|
+
import { statSync } from 'fs';
|
|
18
18
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
19
19
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
20
20
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
@@ -380,6 +380,48 @@ var init_identity = __esm({
|
|
|
380
380
|
init_random();
|
|
381
381
|
}
|
|
382
382
|
});
|
|
383
|
+
async function tightenStoragePermissions(root) {
|
|
384
|
+
try {
|
|
385
|
+
await tightenEntry(root);
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function tightenEntry(path) {
|
|
390
|
+
let info;
|
|
391
|
+
try {
|
|
392
|
+
info = await stat(path);
|
|
393
|
+
} catch {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (info.isDirectory()) {
|
|
397
|
+
const current = info.mode & 511;
|
|
398
|
+
if (current !== 448) {
|
|
399
|
+
try {
|
|
400
|
+
await chmod(path, 448);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error(` Warning: could not chmod dir ${path}: ${err.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
let entries;
|
|
406
|
+
try {
|
|
407
|
+
entries = await readdir(path);
|
|
408
|
+
} catch {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
await tightenEntry(join(path, entry));
|
|
413
|
+
}
|
|
414
|
+
} else if (info.isFile()) {
|
|
415
|
+
const current = info.mode & 511;
|
|
416
|
+
if (current !== 384) {
|
|
417
|
+
try {
|
|
418
|
+
await chmod(path, 384);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(` Warning: could not chmod file ${path}: ${err.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
383
425
|
var require2 = createRequire(import.meta.url);
|
|
384
426
|
var { version: PKG_VERSION } = require2("../package.json");
|
|
385
427
|
var SANCTUARY_VERSION = PKG_VERSION;
|
|
@@ -773,17 +815,48 @@ var RESERVED_NAMESPACE_PREFIXES = [
|
|
|
773
815
|
"_sovereignty_profile",
|
|
774
816
|
"_context_gate_policies"
|
|
775
817
|
];
|
|
776
|
-
var StateStore = class {
|
|
818
|
+
var StateStore = class _StateStore {
|
|
777
819
|
storage;
|
|
778
820
|
masterKey;
|
|
779
821
|
// Cache of version numbers per namespace/key for anti-rollback
|
|
780
822
|
versionCache = /* @__PURE__ */ new Map();
|
|
781
823
|
// Cache of content hashes per namespace for Merkle tree computation
|
|
782
824
|
contentHashes = /* @__PURE__ */ new Map();
|
|
825
|
+
// LRU-with-TTL cache for derived namespace keys (avoids repeated HKDF)
|
|
826
|
+
namespaceKeyCache = /* @__PURE__ */ new Map();
|
|
827
|
+
static KEY_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
828
|
+
// 15 minutes
|
|
829
|
+
static KEY_CACHE_MAX_ENTRIES = 128;
|
|
783
830
|
constructor(storage, masterKey) {
|
|
784
831
|
this.storage = storage;
|
|
785
832
|
this.masterKey = masterKey;
|
|
786
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* Get or derive a namespace encryption key, with caching.
|
|
836
|
+
* Cache entries expire after 15 minutes and are evicted LRU when
|
|
837
|
+
* the cache exceeds 128 entries.
|
|
838
|
+
*/
|
|
839
|
+
getNamespaceKey(namespace) {
|
|
840
|
+
const now = Date.now();
|
|
841
|
+
const cached = this.namespaceKeyCache.get(namespace);
|
|
842
|
+
if (cached && cached.expiresAt > now) {
|
|
843
|
+
return cached.key;
|
|
844
|
+
}
|
|
845
|
+
if (this.namespaceKeyCache.size >= _StateStore.KEY_CACHE_MAX_ENTRIES) {
|
|
846
|
+
const firstKey = this.namespaceKeyCache.keys().next().value;
|
|
847
|
+
if (firstKey !== void 0) this.namespaceKeyCache.delete(firstKey);
|
|
848
|
+
}
|
|
849
|
+
const derived = deriveNamespaceKey(this.masterKey, namespace);
|
|
850
|
+
this.namespaceKeyCache.set(namespace, {
|
|
851
|
+
key: derived,
|
|
852
|
+
expiresAt: now + _StateStore.KEY_CACHE_TTL_MS
|
|
853
|
+
});
|
|
854
|
+
return derived;
|
|
855
|
+
}
|
|
856
|
+
/** Invalidate all cached namespace keys (call on master key rotation). */
|
|
857
|
+
invalidateKeyCache() {
|
|
858
|
+
this.namespaceKeyCache.clear();
|
|
859
|
+
}
|
|
787
860
|
versionKey(namespace, key) {
|
|
788
861
|
return `${namespace}/${key}`;
|
|
789
862
|
}
|
|
@@ -825,7 +898,7 @@ var StateStore = class {
|
|
|
825
898
|
* @param options - Optional metadata
|
|
826
899
|
*/
|
|
827
900
|
async write(namespace, key, value, identityId, encryptedPrivateKey, identityEncryptionKey, options = {}) {
|
|
828
|
-
const namespaceKey =
|
|
901
|
+
const namespaceKey = this.getNamespaceKey(namespace);
|
|
829
902
|
const plaintext = stringToBytes(value);
|
|
830
903
|
const integrityHash = hashToString(plaintext);
|
|
831
904
|
const payload = encrypt(plaintext, namespaceKey);
|
|
@@ -906,7 +979,7 @@ var StateStore = class {
|
|
|
906
979
|
);
|
|
907
980
|
}
|
|
908
981
|
}
|
|
909
|
-
const namespaceKey =
|
|
982
|
+
const namespaceKey = this.getNamespaceKey(namespace);
|
|
910
983
|
const plaintext = decrypt(stateEntry.payload, namespaceKey);
|
|
911
984
|
const value = bytesToString(plaintext);
|
|
912
985
|
const computedHash = hashToString(plaintext);
|
|
@@ -1428,6 +1501,19 @@ var IdentityManager = class {
|
|
|
1428
1501
|
key_protection: si.key_protection
|
|
1429
1502
|
}));
|
|
1430
1503
|
}
|
|
1504
|
+
/** List identities with rotation count (for dashboard display). */
|
|
1505
|
+
listWithRotationCount() {
|
|
1506
|
+
return Array.from(this.identities.values()).map((si) => ({
|
|
1507
|
+
identity_id: si.identity_id,
|
|
1508
|
+
label: si.label,
|
|
1509
|
+
public_key: si.public_key,
|
|
1510
|
+
did: si.did,
|
|
1511
|
+
created_at: si.created_at,
|
|
1512
|
+
key_type: si.key_type,
|
|
1513
|
+
key_protection: si.key_protection,
|
|
1514
|
+
rotation_count: si.rotation_history?.length ?? 0
|
|
1515
|
+
}));
|
|
1516
|
+
}
|
|
1431
1517
|
};
|
|
1432
1518
|
function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog) {
|
|
1433
1519
|
const identityMgr = new IdentityManager(storage, masterKey);
|
|
@@ -1882,17 +1968,34 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1882
1968
|
// src/l2-operational/audit-log.ts
|
|
1883
1969
|
init_encryption();
|
|
1884
1970
|
init_encoding();
|
|
1971
|
+
var DEFAULT_MAX_TOTAL_SIZE_BYTES = 100 * 1024 * 1024;
|
|
1972
|
+
var DEFAULT_MAX_ENTRIES = 1e5;
|
|
1885
1973
|
var AuditLog = class {
|
|
1886
1974
|
storage;
|
|
1887
1975
|
encryptionKey;
|
|
1888
1976
|
entries = [];
|
|
1889
1977
|
counter = 0;
|
|
1890
|
-
|
|
1978
|
+
maxTotalSizeBytes;
|
|
1979
|
+
maxEntries;
|
|
1980
|
+
rotationInFlight = false;
|
|
1981
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
1982
|
+
constructor(storage, masterKey, config) {
|
|
1891
1983
|
this.storage = storage;
|
|
1892
1984
|
this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
|
|
1985
|
+
this.maxTotalSizeBytes = config?.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL_SIZE_BYTES;
|
|
1986
|
+
this.maxEntries = config?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
1893
1987
|
}
|
|
1894
1988
|
/**
|
|
1895
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.
|
|
1896
1999
|
*/
|
|
1897
2000
|
append(layer, operation, identityId, details, result = "success") {
|
|
1898
2001
|
const entry = {
|
|
@@ -1904,8 +2007,22 @@ var AuditLog = class {
|
|
|
1904
2007
|
details
|
|
1905
2008
|
};
|
|
1906
2009
|
this.entries.push(entry);
|
|
1907
|
-
this.persistEntry(entry).catch(() => {
|
|
2010
|
+
const writePromise = this.persistEntry(entry).catch(() => {
|
|
1908
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
|
+
}
|
|
1909
2026
|
}
|
|
1910
2027
|
async persistEntry(entry) {
|
|
1911
2028
|
const key = `${Date.now()}-${this.counter++}`;
|
|
@@ -1916,6 +2033,38 @@ var AuditLog = class {
|
|
|
1916
2033
|
key,
|
|
1917
2034
|
stringToBytes(JSON.stringify(encrypted))
|
|
1918
2035
|
);
|
|
2036
|
+
await this.maybeRotate().catch(() => {
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Prune oldest audit entries when storage exceeds configured limits.
|
|
2041
|
+
* Entries are sorted by key (timestamp-based) so oldest are pruned first.
|
|
2042
|
+
*/
|
|
2043
|
+
async maybeRotate() {
|
|
2044
|
+
if (this.rotationInFlight) return;
|
|
2045
|
+
this.rotationInFlight = true;
|
|
2046
|
+
try {
|
|
2047
|
+
const metas = await this.storage.list("_audit");
|
|
2048
|
+
if (metas.length === 0) return;
|
|
2049
|
+
metas.sort((a, b) => a.key.localeCompare(b.key));
|
|
2050
|
+
const totalSize = metas.reduce((sum, m) => sum + m.size_bytes, 0);
|
|
2051
|
+
let toDelete = 0;
|
|
2052
|
+
if (metas.length > this.maxEntries) {
|
|
2053
|
+
toDelete = metas.length - this.maxEntries;
|
|
2054
|
+
}
|
|
2055
|
+
if (totalSize > this.maxTotalSizeBytes) {
|
|
2056
|
+
let runningSize = totalSize;
|
|
2057
|
+
for (let i = toDelete; i < metas.length && runningSize > this.maxTotalSizeBytes; i++) {
|
|
2058
|
+
runningSize -= metas[i].size_bytes;
|
|
2059
|
+
toDelete = i + 1;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
for (let i = 0; i < toDelete; i++) {
|
|
2063
|
+
await this.storage.delete("_audit", metas[i].key);
|
|
2064
|
+
}
|
|
2065
|
+
} finally {
|
|
2066
|
+
this.rotationInFlight = false;
|
|
2067
|
+
}
|
|
1919
2068
|
}
|
|
1920
2069
|
/**
|
|
1921
2070
|
* Query the audit log with filtering.
|
|
@@ -1997,7 +2146,9 @@ function verifyCommitment(commitment, value, blindingFactor) {
|
|
|
1997
2146
|
const valueBytes = stringToBytes(value);
|
|
1998
2147
|
const combined = concatBytes(valueBytes, blindingBytes);
|
|
1999
2148
|
const expectedHash = toBase64url(hash(combined));
|
|
2000
|
-
|
|
2149
|
+
const commitmentBytes = fromBase64url(commitment);
|
|
2150
|
+
const expectedBytes = fromBase64url(expectedHash);
|
|
2151
|
+
return constantTimeEqual(commitmentBytes, expectedBytes);
|
|
2001
2152
|
}
|
|
2002
2153
|
var CommitmentStore = class {
|
|
2003
2154
|
storage;
|
|
@@ -3179,6 +3330,51 @@ var ReputationStore = class {
|
|
|
3179
3330
|
);
|
|
3180
3331
|
return guarantee;
|
|
3181
3332
|
}
|
|
3333
|
+
// ─── L4 Evidence Summary ─────────────────────────────────────────────
|
|
3334
|
+
/**
|
|
3335
|
+
* Summarize attestations for the L4 degradation emitter and dashboard widget.
|
|
3336
|
+
*
|
|
3337
|
+
* Returns aggregate evidence about the identity's reputation state —
|
|
3338
|
+
* counts, tier distribution, recency, dispute counts, context coverage —
|
|
3339
|
+
* without exposing raw attestations. The caller combines this with an
|
|
3340
|
+
* audit-log check for Verascore link state to produce the final
|
|
3341
|
+
* `L4Evidence` struct consumed by the SHR generator.
|
|
3342
|
+
*
|
|
3343
|
+
* @param participantDid - If provided, only count attestations where the
|
|
3344
|
+
* `participant_did` matches. If omitted, covers all attestations in the
|
|
3345
|
+
* store.
|
|
3346
|
+
*/
|
|
3347
|
+
async summarizeForSHR(participantDid) {
|
|
3348
|
+
const all = await this.loadAll();
|
|
3349
|
+
const filtered = participantDid ? all.filter((a) => a.attestation.data.participant_did === participantDid) : all;
|
|
3350
|
+
const tierDist = {
|
|
3351
|
+
"verified-sovereign": 0,
|
|
3352
|
+
"verified-degraded": 0,
|
|
3353
|
+
"self-attested": 0,
|
|
3354
|
+
"unverified": 0
|
|
3355
|
+
};
|
|
3356
|
+
const contextBreakdown = {};
|
|
3357
|
+
let mostRecentMs = null;
|
|
3358
|
+
let disputeCount = 0;
|
|
3359
|
+
for (const a of filtered) {
|
|
3360
|
+
const tier = a.attestation.data.sovereignty_tier;
|
|
3361
|
+
if (tier) tierDist[tier]++;
|
|
3362
|
+
const ctx = a.attestation.data.context;
|
|
3363
|
+
if (ctx) contextBreakdown[ctx] = (contextBreakdown[ctx] ?? 0) + 1;
|
|
3364
|
+
const ts = new Date(a.attestation.data.timestamp).getTime();
|
|
3365
|
+
if (!isNaN(ts) && (mostRecentMs === null || ts > mostRecentMs)) {
|
|
3366
|
+
mostRecentMs = ts;
|
|
3367
|
+
}
|
|
3368
|
+
if (a.attestation.data.outcome_result === "disputed") disputeCount++;
|
|
3369
|
+
}
|
|
3370
|
+
return {
|
|
3371
|
+
attestation_count: filtered.length,
|
|
3372
|
+
tier_distribution: tierDist,
|
|
3373
|
+
most_recent_attestation_at: mostRecentMs !== null ? new Date(mostRecentMs).toISOString() : null,
|
|
3374
|
+
dispute_count: disputeCount,
|
|
3375
|
+
context_breakdown: contextBreakdown
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3182
3378
|
// ─── Tier-Aware Access ───────────────────────────────────────────────
|
|
3183
3379
|
/**
|
|
3184
3380
|
* Load attestations for tier-weighted scoring.
|
|
@@ -3200,21 +3396,37 @@ var ReputationStore = class {
|
|
|
3200
3396
|
// ─── Internal ─────────────────────────────────────────────────────────
|
|
3201
3397
|
async loadAll() {
|
|
3202
3398
|
const results = [];
|
|
3399
|
+
for await (const page of this.loadAllPaginated(100)) {
|
|
3400
|
+
results.push(...page);
|
|
3401
|
+
}
|
|
3402
|
+
return results;
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Cursor-based async iterator that loads attestations in pages.
|
|
3406
|
+
* Prevents OOM at 100K+ records by reading and decrypting in batches.
|
|
3407
|
+
*/
|
|
3408
|
+
async *loadAllPaginated(pageSize = 100) {
|
|
3409
|
+
let entries;
|
|
3203
3410
|
try {
|
|
3204
|
-
|
|
3205
|
-
|
|
3411
|
+
entries = await this.storage.list("_reputation");
|
|
3412
|
+
} catch {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
for (let i = 0; i < entries.length; i += pageSize) {
|
|
3416
|
+
const page = [];
|
|
3417
|
+
const slice = entries.slice(i, i + pageSize);
|
|
3418
|
+
for (const meta of slice) {
|
|
3206
3419
|
const raw = await this.storage.read("_reputation", meta.key);
|
|
3207
3420
|
if (!raw) continue;
|
|
3208
3421
|
try {
|
|
3209
3422
|
const encrypted = JSON.parse(bytesToString(raw));
|
|
3210
3423
|
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
3211
|
-
|
|
3424
|
+
page.push(JSON.parse(bytesToString(decrypted)));
|
|
3212
3425
|
} catch {
|
|
3213
3426
|
}
|
|
3214
3427
|
}
|
|
3215
|
-
|
|
3428
|
+
if (page.length > 0) yield page;
|
|
3216
3429
|
}
|
|
3217
|
-
return results;
|
|
3218
3430
|
}
|
|
3219
3431
|
};
|
|
3220
3432
|
|
|
@@ -4441,13 +4653,78 @@ function canonicalizeForSigning(body) {
|
|
|
4441
4653
|
init_identity();
|
|
4442
4654
|
init_encoding();
|
|
4443
4655
|
var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
|
|
4656
|
+
var DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
|
|
4657
|
+
var DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
|
|
4658
|
+
function deriveL4Degradations(evidence, now = /* @__PURE__ */ new Date()) {
|
|
4659
|
+
const out = [];
|
|
4660
|
+
const freshnessDays = evidence.thresholds?.freshness_window_days ?? DEFAULT_FRESHNESS_WINDOW_DAYS;
|
|
4661
|
+
const dominanceThreshold = evidence.thresholds?.low_tier_dominance_threshold ?? DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD;
|
|
4662
|
+
if (evidence.attestation_count === 0) {
|
|
4663
|
+
out.push({
|
|
4664
|
+
layer: "l4",
|
|
4665
|
+
code: "NO_REPUTATION_HISTORY",
|
|
4666
|
+
severity: "warning",
|
|
4667
|
+
description: "Signing identity has no recorded reputation attestations",
|
|
4668
|
+
mitigation: "Complete interactions that produce reputation_record calls, or import a portable reputation bundle"
|
|
4669
|
+
});
|
|
4670
|
+
} else {
|
|
4671
|
+
const lowTierCount = (evidence.tier_distribution["self-attested"] ?? 0) + (evidence.tier_distribution["unverified"] ?? 0);
|
|
4672
|
+
const lowTierShare = lowTierCount / evidence.attestation_count;
|
|
4673
|
+
if (lowTierShare > dominanceThreshold) {
|
|
4674
|
+
const pct = Math.round(lowTierShare * 100);
|
|
4675
|
+
out.push({
|
|
4676
|
+
layer: "l4",
|
|
4677
|
+
code: "LOW_TIER_DOMINANCE",
|
|
4678
|
+
severity: "info",
|
|
4679
|
+
description: `${pct}% of attestations are self-attested or unverified`,
|
|
4680
|
+
mitigation: "Complete sovereignty handshakes with counterparties to upgrade future attestations to verified tiers"
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
if (evidence.most_recent_attestation_at) {
|
|
4684
|
+
const mostRecentMs = new Date(evidence.most_recent_attestation_at).getTime();
|
|
4685
|
+
if (!isNaN(mostRecentMs)) {
|
|
4686
|
+
const ageMs = now.getTime() - mostRecentMs;
|
|
4687
|
+
const windowMs = freshnessDays * 24 * 60 * 60 * 1e3;
|
|
4688
|
+
if (ageMs > windowMs) {
|
|
4689
|
+
const ageDays = Math.round(ageMs / (24 * 60 * 60 * 1e3));
|
|
4690
|
+
out.push({
|
|
4691
|
+
layer: "l4",
|
|
4692
|
+
code: "STALE_REPUTATION",
|
|
4693
|
+
severity: "info",
|
|
4694
|
+
description: `Most recent attestation is ${ageDays} days old (freshness window: ${freshnessDays} days)`,
|
|
4695
|
+
mitigation: "Record a fresh interaction outcome or refresh reputation from active counterparties"
|
|
4696
|
+
});
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
if (evidence.dispute_count > 0) {
|
|
4701
|
+
out.push({
|
|
4702
|
+
layer: "l4",
|
|
4703
|
+
code: "DISPUTE_ON_RECORD",
|
|
4704
|
+
severity: "warning",
|
|
4705
|
+
description: `${evidence.dispute_count} attestation${evidence.dispute_count === 1 ? "" : "s"} marked as disputed`,
|
|
4706
|
+
mitigation: "Review disputed interactions; counterparties may weigh this signal when evaluating trust"
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
if (!evidence.verascore_linked) {
|
|
4711
|
+
out.push({
|
|
4712
|
+
layer: "l4",
|
|
4713
|
+
code: "NO_VERASCORE_LINK",
|
|
4714
|
+
severity: "info",
|
|
4715
|
+
description: "No successful reputation_publish call for this identity \u2014 reputation is not externally discoverable",
|
|
4716
|
+
mitigation: "Run reputation_publish to link this identity to a Verascore profile"
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
return out;
|
|
4720
|
+
}
|
|
4444
4721
|
function generateSHR(identityId, opts) {
|
|
4445
|
-
const { config, identityManager, masterKey, validityMs } = opts;
|
|
4722
|
+
const { config, identityManager, masterKey, validityMs, l4Evidence, now: nowOverride } = opts;
|
|
4446
4723
|
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
4447
4724
|
if (!identity) {
|
|
4448
4725
|
return "No identity available for signing. Create an identity first.";
|
|
4449
4726
|
}
|
|
4450
|
-
const now = /* @__PURE__ */ new Date();
|
|
4727
|
+
const now = nowOverride ?? /* @__PURE__ */ new Date();
|
|
4451
4728
|
const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
|
|
4452
4729
|
const degradations = [];
|
|
4453
4730
|
if (config.execution.environment === "local-process") {
|
|
@@ -4466,6 +4743,9 @@ function generateSHR(identityId, opts) {
|
|
|
4466
4743
|
mitigation: "TEE attestation planned for a future release"
|
|
4467
4744
|
});
|
|
4468
4745
|
}
|
|
4746
|
+
const l4Degradations = l4Evidence ? deriveL4Degradations(l4Evidence, now) : [];
|
|
4747
|
+
degradations.push(...l4Degradations);
|
|
4748
|
+
const l4Status = l4Degradations.length > 0 ? "degraded" : "active";
|
|
4469
4749
|
const body = {
|
|
4470
4750
|
shr_version: "1.0",
|
|
4471
4751
|
implementation: {
|
|
@@ -4496,7 +4776,7 @@ function generateSHR(identityId, opts) {
|
|
|
4496
4776
|
selective_disclosure: true
|
|
4497
4777
|
},
|
|
4498
4778
|
l4: {
|
|
4499
|
-
status:
|
|
4779
|
+
status: l4Status,
|
|
4500
4780
|
reputation_mode: config.reputation.mode,
|
|
4501
4781
|
attestation_format: config.reputation.attestation_format,
|
|
4502
4782
|
reputation_portable: true
|
|
@@ -7457,7 +7737,7 @@ function generateFortressViewHTML(options) {
|
|
|
7457
7737
|
<head>
|
|
7458
7738
|
<meta charset="UTF-8">
|
|
7459
7739
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7460
|
-
<title>Sanctuary
|
|
7740
|
+
<title>Sanctuary</title>
|
|
7461
7741
|
<style>
|
|
7462
7742
|
:root {
|
|
7463
7743
|
--bg: #0d1117;
|
|
@@ -7846,7 +8126,7 @@ function generateFortressViewHTML(options) {
|
|
|
7846
8126
|
<div class="fortress-brand">
|
|
7847
8127
|
<div class="shield">🛡</div>
|
|
7848
8128
|
<div>
|
|
7849
|
-
<h1>Sanctuary
|
|
8129
|
+
<h1>Sanctuary</h1>
|
|
7850
8130
|
<div class="version">v${esc(options.serverVersion)}</div>
|
|
7851
8131
|
</div>
|
|
7852
8132
|
</div>
|
|
@@ -8388,6 +8668,10 @@ var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
|
8388
8668
|
var RATE_LIMIT_GENERAL = 120;
|
|
8389
8669
|
var RATE_LIMIT_DECISIONS = 20;
|
|
8390
8670
|
var MAX_RATE_LIMIT_ENTRIES = 1e4;
|
|
8671
|
+
function isDashboardViewRoute(method, path) {
|
|
8672
|
+
if (method !== "GET") return false;
|
|
8673
|
+
return path === "/" || path === "/dashboard" || path === "/fortress" || path === "/events";
|
|
8674
|
+
}
|
|
8391
8675
|
var DashboardApprovalChannel = class {
|
|
8392
8676
|
config;
|
|
8393
8677
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -8455,20 +8739,22 @@ var DashboardApprovalChannel = class {
|
|
|
8455
8739
|
* Start the HTTP(S) server for the dashboard.
|
|
8456
8740
|
*/
|
|
8457
8741
|
async start() {
|
|
8742
|
+
const handler = (req, res) => this.handleRequest(req, res);
|
|
8743
|
+
let server;
|
|
8744
|
+
if (this.useTLS && this.config.tls) {
|
|
8745
|
+
const tlsOpts = {
|
|
8746
|
+
cert: await readFile(this.config.tls.cert_path),
|
|
8747
|
+
key: await readFile(this.config.tls.key_path)
|
|
8748
|
+
};
|
|
8749
|
+
server = createServer$1(tlsOpts, handler);
|
|
8750
|
+
} else {
|
|
8751
|
+
server = createServer$2(handler);
|
|
8752
|
+
}
|
|
8753
|
+
this.httpServer = server;
|
|
8458
8754
|
return new Promise((resolve, reject) => {
|
|
8459
|
-
const handler = (req, res) => this.handleRequest(req, res);
|
|
8460
|
-
if (this.useTLS && this.config.tls) {
|
|
8461
|
-
const tlsOpts = {
|
|
8462
|
-
cert: readFileSync(this.config.tls.cert_path),
|
|
8463
|
-
key: readFileSync(this.config.tls.key_path)
|
|
8464
|
-
};
|
|
8465
|
-
this.httpServer = createServer$1(tlsOpts, handler);
|
|
8466
|
-
} else {
|
|
8467
|
-
this.httpServer = createServer$2(handler);
|
|
8468
|
-
}
|
|
8469
8755
|
const protocol = this.useTLS ? "https" : "http";
|
|
8470
8756
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
8471
|
-
|
|
8757
|
+
server.listen(this.config.port, this.config.host, () => {
|
|
8472
8758
|
const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
|
|
8473
8759
|
process.stderr.write(
|
|
8474
8760
|
`
|
|
@@ -8492,7 +8778,7 @@ var DashboardApprovalChannel = class {
|
|
|
8492
8778
|
}
|
|
8493
8779
|
resolve();
|
|
8494
8780
|
});
|
|
8495
|
-
|
|
8781
|
+
server.on("error", (err) => {
|
|
8496
8782
|
if (err.code === "EADDRINUSE") {
|
|
8497
8783
|
const port = this.config.port;
|
|
8498
8784
|
process.stderr.write(
|
|
@@ -8781,13 +9067,14 @@ var DashboardApprovalChannel = class {
|
|
|
8781
9067
|
}
|
|
8782
9068
|
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
8783
9069
|
if (!this.isAuthenticated(req, url)) {
|
|
8784
|
-
if (!this.checkRateLimit(req, res, "general")) return;
|
|
8785
9070
|
this.serveLoginPage(res);
|
|
8786
9071
|
return;
|
|
8787
9072
|
}
|
|
8788
9073
|
}
|
|
8789
9074
|
if (!this.checkAuth(req, url, res)) return;
|
|
8790
|
-
if (!
|
|
9075
|
+
if (!isDashboardViewRoute(method, url.pathname)) {
|
|
9076
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
9077
|
+
}
|
|
8791
9078
|
try {
|
|
8792
9079
|
if (method === "GET" && url.pathname === "/fortress") {
|
|
8793
9080
|
this.serveFortressView(res);
|
|
@@ -9081,16 +9368,7 @@ data: ${JSON.stringify(initData)}
|
|
|
9081
9368
|
res.end(JSON.stringify({ identities: [], count: 0 }));
|
|
9082
9369
|
return;
|
|
9083
9370
|
}
|
|
9084
|
-
const identities = this.identityManager.
|
|
9085
|
-
identity_id: id.identity_id,
|
|
9086
|
-
label: id.label,
|
|
9087
|
-
public_key: id.public_key,
|
|
9088
|
-
did: id.did,
|
|
9089
|
-
created_at: id.created_at,
|
|
9090
|
-
key_type: id.key_type,
|
|
9091
|
-
key_protection: id.key_protection,
|
|
9092
|
-
rotation_count: id.rotation_history?.length ?? 0
|
|
9093
|
-
}));
|
|
9371
|
+
const identities = this.identityManager.listWithRotationCount();
|
|
9094
9372
|
const primary = this.identityManager.getDefault();
|
|
9095
9373
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
9096
9374
|
res.end(JSON.stringify({
|
|
@@ -9695,16 +9973,6 @@ var INVISIBLE_CHARS = [
|
|
|
9695
9973
|
];
|
|
9696
9974
|
var VARIATION_SELECTOR_RANGE_START = 65024;
|
|
9697
9975
|
var VARIATION_SELECTOR_RANGE_END = 65039;
|
|
9698
|
-
var ZERO_WIDTH_CHARS = [
|
|
9699
|
-
"\u200B",
|
|
9700
|
-
// Zero-width space
|
|
9701
|
-
"\u200C",
|
|
9702
|
-
// Zero-width non-joiner
|
|
9703
|
-
"\u200D",
|
|
9704
|
-
// Zero-width joiner
|
|
9705
|
-
"\uFEFF"
|
|
9706
|
-
// Zero-width no-break space
|
|
9707
|
-
];
|
|
9708
9976
|
var BASE64_STANDARD_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
9709
9977
|
var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/;
|
|
9710
9978
|
var BASE64_BLOCK_PATTERN = /[A-Za-z0-9+/]{20,}={0,2}/g;
|
|
@@ -10321,10 +10589,8 @@ var InjectionDetector = class {
|
|
|
10321
10589
|
});
|
|
10322
10590
|
}
|
|
10323
10591
|
}
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
zeroWidthCount += (value.match(new RegExp(char, "g")) || []).length;
|
|
10327
|
-
}
|
|
10592
|
+
const zeroWidthMatches = value.match(/[\u200B\u200C\u200D\uFEFF]/g);
|
|
10593
|
+
const zeroWidthCount = zeroWidthMatches ? zeroWidthMatches.length : 0;
|
|
10328
10594
|
if (zeroWidthCount > 0) {
|
|
10329
10595
|
signals.push({
|
|
10330
10596
|
type: "encoding_evasion",
|
|
@@ -11317,6 +11583,16 @@ function transformDegradations(degradations) {
|
|
|
11317
11583
|
authzImpact = "Must share entire data context, cannot redact";
|
|
11318
11584
|
} else if (deg.code === "BASIC_SYBIL_ONLY") {
|
|
11319
11585
|
authzImpact = "Restrict to interactions with known agents only";
|
|
11586
|
+
} else if (deg.code === "NO_REPUTATION_HISTORY") {
|
|
11587
|
+
authzImpact = "No prior reputation \u2014 treat as a new counterparty; require escrow or human approval for value transfer";
|
|
11588
|
+
} else if (deg.code === "LOW_TIER_DOMINANCE") {
|
|
11589
|
+
authzImpact = "Reputation dominated by self-attested / unverified signers \u2014 weight accordingly, require additional confirmation on high-value actions";
|
|
11590
|
+
} else if (deg.code === "STALE_REPUTATION") {
|
|
11591
|
+
authzImpact = "Reputation is stale \u2014 may not reflect current behavior; refresh before relying on it";
|
|
11592
|
+
} else if (deg.code === "DISPUTE_ON_RECORD") {
|
|
11593
|
+
authzImpact = "Disputes on record \u2014 review dispute context before extending trust beyond low-value interactions";
|
|
11594
|
+
} else if (deg.code === "NO_VERASCORE_LINK") {
|
|
11595
|
+
authzImpact = "No external reputation profile \u2014 counterparty cannot independently discover reputation bundle";
|
|
11320
11596
|
} else {
|
|
11321
11597
|
authzImpact = "Unknown authorization impact";
|
|
11322
11598
|
}
|
|
@@ -11423,12 +11699,37 @@ function transformSHRGeneric(shr) {
|
|
|
11423
11699
|
}
|
|
11424
11700
|
|
|
11425
11701
|
// src/shr/tools.ts
|
|
11426
|
-
function
|
|
11702
|
+
async function gatherL4Evidence(reputationStore, auditLog, identity) {
|
|
11703
|
+
const summary = await reputationStore.summarizeForSHR(identity.did);
|
|
11704
|
+
const published = await auditLog.query({
|
|
11705
|
+
layer: "l4",
|
|
11706
|
+
operation_type: "reputation_publish",
|
|
11707
|
+
limit: 500
|
|
11708
|
+
});
|
|
11709
|
+
const verascoreLinked = published.entries.some(
|
|
11710
|
+
(e) => e.result === "success" && e.identity_id === identity.identity_id
|
|
11711
|
+
);
|
|
11712
|
+
return {
|
|
11713
|
+
attestation_count: summary.attestation_count,
|
|
11714
|
+
tier_distribution: summary.tier_distribution,
|
|
11715
|
+
most_recent_attestation_at: summary.most_recent_attestation_at,
|
|
11716
|
+
dispute_count: summary.dispute_count,
|
|
11717
|
+
context_breakdown: summary.context_breakdown,
|
|
11718
|
+
verascore_linked: verascoreLinked
|
|
11719
|
+
};
|
|
11720
|
+
}
|
|
11721
|
+
function createSHRTools(config, identityManager, masterKey, auditLog, reputationStore) {
|
|
11427
11722
|
const generatorOpts = {
|
|
11428
11723
|
config,
|
|
11429
11724
|
identityManager,
|
|
11430
11725
|
masterKey
|
|
11431
11726
|
};
|
|
11727
|
+
async function resolveL4Evidence(identityId) {
|
|
11728
|
+
if (!reputationStore) return void 0;
|
|
11729
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
11730
|
+
if (!identity) return void 0;
|
|
11731
|
+
return gatherL4Evidence(reputationStore, auditLog, identity);
|
|
11732
|
+
}
|
|
11432
11733
|
const tools = [
|
|
11433
11734
|
{
|
|
11434
11735
|
name: "shr_generate",
|
|
@@ -11448,9 +11749,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
11448
11749
|
},
|
|
11449
11750
|
handler: async (args) => {
|
|
11450
11751
|
const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
|
|
11451
|
-
const
|
|
11752
|
+
const identityId = args.identity_id;
|
|
11753
|
+
const l4Evidence = await resolveL4Evidence(identityId);
|
|
11754
|
+
const result = generateSHR(identityId, {
|
|
11452
11755
|
...generatorOpts,
|
|
11453
|
-
validityMs
|
|
11756
|
+
validityMs,
|
|
11757
|
+
l4Evidence
|
|
11454
11758
|
});
|
|
11455
11759
|
if (typeof result === "string") {
|
|
11456
11760
|
return toolResult({ error: result });
|
|
@@ -11509,9 +11813,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
11509
11813
|
handler: async (args) => {
|
|
11510
11814
|
const format = args.format || "ping";
|
|
11511
11815
|
const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
|
|
11512
|
-
const
|
|
11816
|
+
const identityId = args.identity_id;
|
|
11817
|
+
const l4Evidence = await resolveL4Evidence(identityId);
|
|
11818
|
+
const shrResult = generateSHR(identityId, {
|
|
11513
11819
|
...generatorOpts,
|
|
11514
|
-
validityMs
|
|
11820
|
+
validityMs,
|
|
11821
|
+
l4Evidence
|
|
11515
11822
|
});
|
|
11516
11823
|
if (typeof shrResult === "string") {
|
|
11517
11824
|
return toolResult({ error: shrResult });
|
|
@@ -17210,8 +17517,27 @@ function validateVerascoreUrl(urlStr, configuredUrl) {
|
|
|
17210
17517
|
}
|
|
17211
17518
|
}
|
|
17212
17519
|
function createSanctuaryTools(opts) {
|
|
17213
|
-
const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
|
|
17520
|
+
const { config, identityManager, masterKey, auditLog, policy, keyProtection, reputationStore } = opts;
|
|
17214
17521
|
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
17522
|
+
async function l4EvidenceForIdentity(identity) {
|
|
17523
|
+
if (!reputationStore) return void 0;
|
|
17524
|
+
return gatherL4Evidence(reputationStore, auditLog, identity);
|
|
17525
|
+
}
|
|
17526
|
+
function emptyL4Evidence() {
|
|
17527
|
+
return {
|
|
17528
|
+
attestation_count: 0,
|
|
17529
|
+
tier_distribution: {
|
|
17530
|
+
"verified-sovereign": 0,
|
|
17531
|
+
"verified-degraded": 0,
|
|
17532
|
+
"self-attested": 0,
|
|
17533
|
+
"unverified": 0
|
|
17534
|
+
},
|
|
17535
|
+
most_recent_attestation_at: null,
|
|
17536
|
+
dispute_count: 0,
|
|
17537
|
+
context_breakdown: {},
|
|
17538
|
+
verascore_linked: false
|
|
17539
|
+
};
|
|
17540
|
+
}
|
|
17215
17541
|
const tools = [
|
|
17216
17542
|
// ─── sanctuary_bootstrap ───────────────────────────────────────────
|
|
17217
17543
|
{
|
|
@@ -17251,7 +17577,8 @@ function createSanctuaryTools(opts) {
|
|
|
17251
17577
|
const shr = generateSHR(publicIdentity.identity_id, {
|
|
17252
17578
|
config,
|
|
17253
17579
|
identityManager,
|
|
17254
|
-
masterKey
|
|
17580
|
+
masterKey,
|
|
17581
|
+
l4Evidence: emptyL4Evidence()
|
|
17255
17582
|
});
|
|
17256
17583
|
if (typeof shr === "string") {
|
|
17257
17584
|
return toolResult({
|
|
@@ -17410,10 +17737,12 @@ function createSanctuaryTools(opts) {
|
|
|
17410
17737
|
error: "No identity found. Create one with identity_create first."
|
|
17411
17738
|
});
|
|
17412
17739
|
}
|
|
17740
|
+
const l4Evidence = await l4EvidenceForIdentity(identity);
|
|
17413
17741
|
const shr = generateSHR(identity.identity_id, {
|
|
17414
17742
|
config,
|
|
17415
17743
|
identityManager,
|
|
17416
|
-
masterKey
|
|
17744
|
+
masterKey,
|
|
17745
|
+
l4Evidence
|
|
17417
17746
|
});
|
|
17418
17747
|
const attestations = args.attestations ?? [];
|
|
17419
17748
|
const body = {
|
|
@@ -19215,7 +19544,7 @@ the Sanctuary oversight gate recorded the following activity:
|
|
|
19215
19544
|
| Outcome | Count |
|
|
19216
19545
|
|---|---|
|
|
19217
19546
|
| Gate allow (Tier 3 auto-allow or approved Tier 1/2) | {{ gate_allow_count }} |
|
|
19218
|
-
| Gate allow_proxy (
|
|
19547
|
+
| Gate allow_proxy (Sanctuary MCP-proxy pass-through) | {{ gate_allow_proxy_count }} |
|
|
19219
19548
|
| Gate deny (approval denied or timeout) | {{ gate_deny_count }} |
|
|
19220
19549
|
| Gate escalate (Tier 2 anomaly raised for human review) | {{ gate_escalate_count }} |
|
|
19221
19550
|
| Gate unclassified (no matching rule, default behaviour applied) | {{ gate_unclassified_count }} |
|
|
@@ -20848,10 +21177,1206 @@ var MemoryStorage = class {
|
|
|
20848
21177
|
}
|
|
20849
21178
|
};
|
|
20850
21179
|
|
|
21180
|
+
// src/dashboard/aggregator.ts
|
|
21181
|
+
var L4_DEGRADATION_IMPACT = {
|
|
21182
|
+
critical: 40,
|
|
21183
|
+
warning: 25,
|
|
21184
|
+
info: 10
|
|
21185
|
+
};
|
|
21186
|
+
function computeL4LayerScore(degradations, status) {
|
|
21187
|
+
if (status === "compromised") return 0;
|
|
21188
|
+
let score = 100;
|
|
21189
|
+
for (const deg of degradations) {
|
|
21190
|
+
score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
|
|
21191
|
+
}
|
|
21192
|
+
score = Math.max(0, score);
|
|
21193
|
+
if (degradations.length === 0 && score > 50) {
|
|
21194
|
+
score = Math.min(100, score + 5);
|
|
21195
|
+
}
|
|
21196
|
+
return Math.round(score);
|
|
21197
|
+
}
|
|
21198
|
+
var MAX_ACTIVITY = 50;
|
|
21199
|
+
var MAX_AUDIT = 50;
|
|
21200
|
+
function fingerprintDID(did) {
|
|
21201
|
+
const raw = did.replace(/^did:[a-z0-9]+:/i, "");
|
|
21202
|
+
if (raw.length <= 12) return raw;
|
|
21203
|
+
return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
|
|
21204
|
+
}
|
|
21205
|
+
function countInjectionsToday(audit) {
|
|
21206
|
+
const startOfDay = /* @__PURE__ */ new Date();
|
|
21207
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
21208
|
+
const cutoff = startOfDay.getTime();
|
|
21209
|
+
return audit.filter((e) => {
|
|
21210
|
+
const ts = new Date(e.timestamp).getTime();
|
|
21211
|
+
if (isNaN(ts) || ts < cutoff) return false;
|
|
21212
|
+
const op = (e.operation ?? "").toLowerCase();
|
|
21213
|
+
return op.includes("injection") || op.includes("blocked");
|
|
21214
|
+
}).length;
|
|
21215
|
+
}
|
|
21216
|
+
var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
|
|
21217
|
+
"zk_prove",
|
|
21218
|
+
"zk_range_prove",
|
|
21219
|
+
"proof_commitment"
|
|
21220
|
+
]);
|
|
21221
|
+
function countProofsToday(audit) {
|
|
21222
|
+
const startOfDay = /* @__PURE__ */ new Date();
|
|
21223
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
21224
|
+
const cutoff = startOfDay.getTime();
|
|
21225
|
+
return audit.filter((e) => {
|
|
21226
|
+
if (e.layer !== "l3") return false;
|
|
21227
|
+
if (!PROOF_CREATION_OPS.has(e.operation)) return false;
|
|
21228
|
+
const ts = new Date(e.timestamp).getTime();
|
|
21229
|
+
return !isNaN(ts) && ts >= cutoff;
|
|
21230
|
+
}).length;
|
|
21231
|
+
}
|
|
21232
|
+
function buildAgent(sources) {
|
|
21233
|
+
if (!sources.identityManager) {
|
|
21234
|
+
return {
|
|
21235
|
+
display_name: "Unclaimed agent",
|
|
21236
|
+
did: null,
|
|
21237
|
+
did_fingerprint: null,
|
|
21238
|
+
identity_count: 0,
|
|
21239
|
+
primary_identity_id: null
|
|
21240
|
+
};
|
|
21241
|
+
}
|
|
21242
|
+
const primary = sources.identityManager.getDefault();
|
|
21243
|
+
const identities = sources.identityManager.list();
|
|
21244
|
+
if (!primary) {
|
|
21245
|
+
return {
|
|
21246
|
+
display_name: "Unclaimed agent",
|
|
21247
|
+
did: null,
|
|
21248
|
+
did_fingerprint: null,
|
|
21249
|
+
identity_count: identities.length,
|
|
21250
|
+
primary_identity_id: null
|
|
21251
|
+
};
|
|
21252
|
+
}
|
|
21253
|
+
return {
|
|
21254
|
+
display_name: primary.label || "Sovereign agent",
|
|
21255
|
+
did: primary.did,
|
|
21256
|
+
did_fingerprint: fingerprintDID(primary.did),
|
|
21257
|
+
identity_count: identities.length,
|
|
21258
|
+
primary_identity_id: primary.identity_id
|
|
21259
|
+
};
|
|
21260
|
+
}
|
|
21261
|
+
function buildL1(sources, audit) {
|
|
21262
|
+
const hasIdentity = !!sources.identityManager?.getDefault();
|
|
21263
|
+
const state = hasIdentity ? "full" : "degraded";
|
|
21264
|
+
return {
|
|
21265
|
+
label: "L1 Cognitive",
|
|
21266
|
+
state,
|
|
21267
|
+
headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
|
|
21268
|
+
encryption: "AES-256-GCM + HKDF per namespace",
|
|
21269
|
+
injection_blocked_today: countInjectionsToday(audit),
|
|
21270
|
+
memory_attest_ready: hasIdentity
|
|
21271
|
+
};
|
|
21272
|
+
}
|
|
21273
|
+
function buildL2(sources) {
|
|
21274
|
+
const teeAvailable = sources.teeAvailable ?? false;
|
|
21275
|
+
const state = teeAvailable ? "full" : "degraded";
|
|
21276
|
+
return {
|
|
21277
|
+
label: "L2 Operational",
|
|
21278
|
+
state,
|
|
21279
|
+
headline: teeAvailable ? "Hardware isolation active" : "Process isolation \u2014 no TEE on this host",
|
|
21280
|
+
isolation_type: teeAvailable ? "hardware-tee" : "process-level",
|
|
21281
|
+
tee_available: teeAvailable,
|
|
21282
|
+
tee_status: teeAvailable ? "Attested" : "Not available \u2014 normal on local dev",
|
|
21283
|
+
sandbox_status: "Principal Policy gate active"
|
|
21284
|
+
};
|
|
21285
|
+
}
|
|
21286
|
+
function buildL3(sources, audit) {
|
|
21287
|
+
const VC_ISSUING_OPS = /* @__PURE__ */ new Set([
|
|
21288
|
+
"reputation_record",
|
|
21289
|
+
"bootstrap_provide_guarantee",
|
|
21290
|
+
"reputation_publish"
|
|
21291
|
+
]);
|
|
21292
|
+
const didActive = !!sources.identityManager?.getDefault()?.did;
|
|
21293
|
+
const vcCount = audit.filter(
|
|
21294
|
+
(e) => e.layer === "l4" && VC_ISSUING_OPS.has(e.operation)
|
|
21295
|
+
).length;
|
|
21296
|
+
return {
|
|
21297
|
+
label: "L3 Disclosure",
|
|
21298
|
+
state: didActive ? "full" : "degraded",
|
|
21299
|
+
headline: didActive ? "Selective disclosure ready" : "No DID \u2014 disclosure unavailable",
|
|
21300
|
+
did_active: didActive,
|
|
21301
|
+
vc_count: vcCount,
|
|
21302
|
+
proofs_today: countProofsToday(audit)
|
|
21303
|
+
};
|
|
21304
|
+
}
|
|
21305
|
+
function buildL4(sources) {
|
|
21306
|
+
const rep = sources.reputation;
|
|
21307
|
+
const hasDid = !!sources.identityManager?.getDefault()?.did;
|
|
21308
|
+
const evidenceBlock = buildL4EvidenceBlock(sources);
|
|
21309
|
+
const base = rep?.score != null ? {
|
|
21310
|
+
label: "L4 Reputation",
|
|
21311
|
+
state: "full",
|
|
21312
|
+
headline: "Verascore attached",
|
|
21313
|
+
score: rep.score,
|
|
21314
|
+
profile_url: rep.profile_url,
|
|
21315
|
+
claim_cta: null
|
|
21316
|
+
} : hasDid ? {
|
|
21317
|
+
label: "L4 Reputation",
|
|
21318
|
+
state: "degraded",
|
|
21319
|
+
headline: "Claim your profile",
|
|
21320
|
+
score: null,
|
|
21321
|
+
profile_url: null,
|
|
21322
|
+
claim_cta: "Claim your profile at verascore.ai"
|
|
21323
|
+
} : {
|
|
21324
|
+
label: "L4 Reputation",
|
|
21325
|
+
state: "degraded",
|
|
21326
|
+
headline: "No identity claimed",
|
|
21327
|
+
score: null,
|
|
21328
|
+
profile_url: null,
|
|
21329
|
+
claim_cta: "Claim your profile at verascore.ai"
|
|
21330
|
+
};
|
|
21331
|
+
if (!evidenceBlock) return base;
|
|
21332
|
+
const nextState = evidenceBlock.active_degradations.length > 0 && base.state === "full" ? "degraded" : base.state;
|
|
21333
|
+
const nextHeadline = nextState === base.state ? base.headline : "Attached, but evidence is degraded";
|
|
21334
|
+
return {
|
|
21335
|
+
...base,
|
|
21336
|
+
state: nextState,
|
|
21337
|
+
headline: nextHeadline,
|
|
21338
|
+
evidence: evidenceBlock.evidence,
|
|
21339
|
+
layer_score: evidenceBlock.layer_score,
|
|
21340
|
+
active_degradations: evidenceBlock.active_degradations
|
|
21341
|
+
};
|
|
21342
|
+
}
|
|
21343
|
+
function buildL4EvidenceBlock(sources) {
|
|
21344
|
+
const ev = sources.l4Evidence;
|
|
21345
|
+
if (!ev) return null;
|
|
21346
|
+
const degradations = deriveL4Degradations(ev, sources.l4Now ?? /* @__PURE__ */ new Date());
|
|
21347
|
+
const status = degradations.length > 0 ? "degraded" : "full";
|
|
21348
|
+
const layer_score = computeL4LayerScore(degradations, status);
|
|
21349
|
+
return {
|
|
21350
|
+
evidence: {
|
|
21351
|
+
attestation_count: ev.attestation_count,
|
|
21352
|
+
tier_distribution: ev.tier_distribution,
|
|
21353
|
+
most_recent_attestation_at: ev.most_recent_attestation_at,
|
|
21354
|
+
dispute_count: ev.dispute_count,
|
|
21355
|
+
context_breakdown: ev.context_breakdown ?? {},
|
|
21356
|
+
verascore_linked: ev.verascore_linked
|
|
21357
|
+
},
|
|
21358
|
+
layer_score,
|
|
21359
|
+
active_degradations: degradations.map((d) => ({
|
|
21360
|
+
code: d.code,
|
|
21361
|
+
severity: d.severity,
|
|
21362
|
+
description: d.description,
|
|
21363
|
+
...d.mitigation !== void 0 ? { mitigation: d.mitigation } : {}
|
|
21364
|
+
}))
|
|
21365
|
+
};
|
|
21366
|
+
}
|
|
21367
|
+
function computeOverall(l1, l2, l3, l4) {
|
|
21368
|
+
const critical = [l1.state, l3.state, l4.state];
|
|
21369
|
+
if (critical.includes("compromised") || l2.state === "compromised") {
|
|
21370
|
+
return {
|
|
21371
|
+
status: "compromised",
|
|
21372
|
+
light: "red",
|
|
21373
|
+
headline: "Sovereignty compromised"
|
|
21374
|
+
};
|
|
21375
|
+
}
|
|
21376
|
+
const allCriticalFull = critical.every((s) => s === "full");
|
|
21377
|
+
if (allCriticalFull && l2.state === "full") {
|
|
21378
|
+
return {
|
|
21379
|
+
status: "healthy",
|
|
21380
|
+
light: "green",
|
|
21381
|
+
headline: "All layers full"
|
|
21382
|
+
};
|
|
21383
|
+
}
|
|
21384
|
+
if (allCriticalFull && l2.state === "degraded") {
|
|
21385
|
+
return {
|
|
21386
|
+
status: "healthy",
|
|
21387
|
+
light: "green",
|
|
21388
|
+
headline: "L1\xB7L3\xB7L4 full \u2014 L2 degraded (no TEE on this host)"
|
|
21389
|
+
};
|
|
21390
|
+
}
|
|
21391
|
+
return {
|
|
21392
|
+
status: "degraded",
|
|
21393
|
+
light: "yellow",
|
|
21394
|
+
headline: "One or more layers degraded"
|
|
21395
|
+
};
|
|
21396
|
+
}
|
|
21397
|
+
function buildUpstreamServers(sources) {
|
|
21398
|
+
if (!sources.clientManager) return [];
|
|
21399
|
+
return sources.clientManager.getStatus().map((s) => {
|
|
21400
|
+
const entry = {
|
|
21401
|
+
name: s.name,
|
|
21402
|
+
state: s.state,
|
|
21403
|
+
tool_count: s.tool_count
|
|
21404
|
+
};
|
|
21405
|
+
if (s.error) entry.error = s.error;
|
|
21406
|
+
return entry;
|
|
21407
|
+
});
|
|
21408
|
+
}
|
|
21409
|
+
async function getProtectionSnapshot(sources) {
|
|
21410
|
+
let audit = [];
|
|
21411
|
+
if (sources.auditLog) {
|
|
21412
|
+
try {
|
|
21413
|
+
const result = await sources.auditLog.query({ limit: MAX_AUDIT });
|
|
21414
|
+
audit = result.entries;
|
|
21415
|
+
} catch {
|
|
21416
|
+
audit = [];
|
|
21417
|
+
}
|
|
21418
|
+
}
|
|
21419
|
+
const agent = buildAgent(sources);
|
|
21420
|
+
const l1 = buildL1(sources, audit);
|
|
21421
|
+
const l2 = buildL2(sources);
|
|
21422
|
+
const l3 = buildL3(sources, audit);
|
|
21423
|
+
const l4 = buildL4(sources);
|
|
21424
|
+
const activity = (sources.activity ?? []).slice(0, MAX_ACTIVITY);
|
|
21425
|
+
const pending_approvals = sources.pendingApprovals ?? [];
|
|
21426
|
+
const upstream_servers = buildUpstreamServers(sources);
|
|
21427
|
+
return {
|
|
21428
|
+
overall: computeOverall(l1, l2, l3, l4),
|
|
21429
|
+
agent,
|
|
21430
|
+
layers: { l1, l2, l3, l4 },
|
|
21431
|
+
activity,
|
|
21432
|
+
pending_approvals,
|
|
21433
|
+
audit: audit.slice(-MAX_AUDIT).reverse(),
|
|
21434
|
+
upstream_servers,
|
|
21435
|
+
mode: sources.mode,
|
|
21436
|
+
server_version: sources.server_version,
|
|
21437
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
21438
|
+
};
|
|
21439
|
+
}
|
|
21440
|
+
|
|
21441
|
+
// src/dashboard/html.ts
|
|
21442
|
+
var HERO_COPY = "Your agent is protected.";
|
|
21443
|
+
function escHtml(value) {
|
|
21444
|
+
if (value == null) return "";
|
|
21445
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21446
|
+
}
|
|
21447
|
+
function layerCard(layer, extra) {
|
|
21448
|
+
return `<section class="layer-card layer-${escHtml(layer.state)}" data-layer-label="${escHtml(layer.label)}">
|
|
21449
|
+
<div class="layer-head">
|
|
21450
|
+
<div class="layer-dot"></div>
|
|
21451
|
+
<div>
|
|
21452
|
+
<h3>${escHtml(layer.label)}</h3>
|
|
21453
|
+
<p class="layer-headline">${escHtml(layer.headline)}</p>
|
|
21454
|
+
</div>
|
|
21455
|
+
</div>
|
|
21456
|
+
<dl class="layer-detail">${extra}</dl>
|
|
21457
|
+
</section>`;
|
|
21458
|
+
}
|
|
21459
|
+
function l1Card(l1) {
|
|
21460
|
+
return layerCard(
|
|
21461
|
+
l1,
|
|
21462
|
+
`<div><dt>Encryption</dt><dd>${escHtml(l1.encryption)}</dd></div>
|
|
21463
|
+
<div><dt>Injections blocked today</dt><dd>${escHtml(l1.injection_blocked_today)}</dd></div>
|
|
21464
|
+
<div><dt>memory_attest</dt><dd>${l1.memory_attest_ready ? "Ready" : "Not ready"}</dd></div>`
|
|
21465
|
+
);
|
|
21466
|
+
}
|
|
21467
|
+
function l2Card(l2) {
|
|
21468
|
+
return layerCard(
|
|
21469
|
+
l2,
|
|
21470
|
+
`<div><dt>Isolation</dt><dd>${escHtml(l2.isolation_type)}</dd></div>
|
|
21471
|
+
<div><dt>TEE</dt><dd>${escHtml(l2.tee_status)}</dd></div>
|
|
21472
|
+
<div><dt>Sandbox</dt><dd>${escHtml(l2.sandbox_status)}</dd></div>`
|
|
21473
|
+
);
|
|
21474
|
+
}
|
|
21475
|
+
function l3Card(l3) {
|
|
21476
|
+
return layerCard(
|
|
21477
|
+
l3,
|
|
21478
|
+
`<div><dt>DID</dt><dd>${l3.did_active ? "Active" : "None"}</dd></div>
|
|
21479
|
+
<div><dt>Credentials</dt><dd>${escHtml(l3.vc_count)}</dd></div>
|
|
21480
|
+
<div><dt>Proofs today</dt><dd>${escHtml(l3.proofs_today)}</dd></div>`
|
|
21481
|
+
);
|
|
21482
|
+
}
|
|
21483
|
+
function l4Card(l4) {
|
|
21484
|
+
const score = l4.score != null ? `<div class="score-block"><span class="score-value">${escHtml(l4.score)}</span><span class="score-label">Verascore</span></div>` : `<div class="claim-block">${escHtml(l4.claim_cta ?? "Claim your profile at verascore.ai")}</div>`;
|
|
21485
|
+
return layerCard(
|
|
21486
|
+
l4,
|
|
21487
|
+
`<div class="layer-cta">${score}</div>${l4EvidenceBlock(l4)}`
|
|
21488
|
+
);
|
|
21489
|
+
}
|
|
21490
|
+
function formatRelativeDays(iso) {
|
|
21491
|
+
if (!iso) return "none on record";
|
|
21492
|
+
const ts = new Date(iso).getTime();
|
|
21493
|
+
if (isNaN(ts)) return "unknown";
|
|
21494
|
+
const days = Math.round((Date.now() - ts) / (24 * 60 * 60 * 1e3));
|
|
21495
|
+
if (days <= 0) return "today";
|
|
21496
|
+
if (days === 1) return "1 day ago";
|
|
21497
|
+
return `${days} days ago`;
|
|
21498
|
+
}
|
|
21499
|
+
function l4EvidenceBlock(l4) {
|
|
21500
|
+
if (!l4.evidence) return "";
|
|
21501
|
+
const ev = l4.evidence;
|
|
21502
|
+
const tierSovereign = ev.tier_distribution["verified-sovereign"];
|
|
21503
|
+
const tierDegraded = ev.tier_distribution["verified-degraded"];
|
|
21504
|
+
const tierSelf = ev.tier_distribution["self-attested"];
|
|
21505
|
+
const tierUnverified = ev.tier_distribution["unverified"];
|
|
21506
|
+
const contextCount = Object.keys(ev.context_breakdown).length;
|
|
21507
|
+
const mostRecent = formatRelativeDays(ev.most_recent_attestation_at);
|
|
21508
|
+
const score = l4.layer_score ?? 100;
|
|
21509
|
+
const summaryLine = `
|
|
21510
|
+
<div><dt>L4 score</dt><dd>${escHtml(score)} / 100</dd></div>
|
|
21511
|
+
<div><dt>Attestations</dt><dd>${escHtml(ev.attestation_count)}</dd></div>
|
|
21512
|
+
<div><dt>Verified tiers</dt><dd>${escHtml(tierSovereign)} sovereign \xB7 ${escHtml(tierDegraded)} degraded</dd></div>
|
|
21513
|
+
<div><dt>Lower tiers</dt><dd>${escHtml(tierSelf)} self \xB7 ${escHtml(tierUnverified)} unverified</dd></div>
|
|
21514
|
+
<div><dt>Contexts</dt><dd>${escHtml(contextCount)}</dd></div>
|
|
21515
|
+
<div><dt>Disputes</dt><dd>${escHtml(ev.dispute_count)}</dd></div>
|
|
21516
|
+
<div><dt>Last activity</dt><dd>${escHtml(mostRecent)}</dd></div>
|
|
21517
|
+
<div><dt>Verascore link</dt><dd>${ev.verascore_linked ? "Yes" : "Not linked"}</dd></div>
|
|
21518
|
+
`;
|
|
21519
|
+
const degs = l4.active_degradations ?? [];
|
|
21520
|
+
const degList = degs.length === 0 ? "" : `<ul class="l4-deg-list">${degs.map(
|
|
21521
|
+
(d) => `
|
|
21522
|
+
<li class="l4-deg l4-deg-${escHtml(d.severity)}">
|
|
21523
|
+
<div class="l4-deg-head">
|
|
21524
|
+
<span class="l4-deg-code">${escHtml(d.code)}</span>
|
|
21525
|
+
<span class="l4-deg-sev">${escHtml(d.severity)}</span>
|
|
21526
|
+
</div>
|
|
21527
|
+
<p class="l4-deg-desc">${escHtml(d.description)}</p>
|
|
21528
|
+
${d.mitigation ? `<p class="l4-deg-mit">${escHtml(d.mitigation)}</p>` : ""}
|
|
21529
|
+
</li>`
|
|
21530
|
+
).join("")}</ul>`;
|
|
21531
|
+
return `
|
|
21532
|
+
<div class="l4-evidence">
|
|
21533
|
+
<dl class="layer-detail l4-evidence-summary">${summaryLine}</dl>
|
|
21534
|
+
${degList}
|
|
21535
|
+
</div>
|
|
21536
|
+
`;
|
|
21537
|
+
}
|
|
21538
|
+
function renderDashboardHTML(options) {
|
|
21539
|
+
const { snapshot } = options;
|
|
21540
|
+
const { overall, agent, layers, activity, pending_approvals, audit, upstream_servers } = snapshot;
|
|
21541
|
+
const activityRows = activity.length === 0 ? `<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>` : activity.map((entry) => {
|
|
21542
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
21543
|
+
return `<tr class="result-${escHtml(entry.result)}">
|
|
21544
|
+
<td class="mono time">${escHtml(time)}</td>
|
|
21545
|
+
<td class="mono">${escHtml(entry.tool)}</td>
|
|
21546
|
+
<td class="mono">${escHtml(entry.server)}</td>
|
|
21547
|
+
<td class="tier tier-${escHtml(entry.tier)}">T${escHtml(entry.tier)}</td>
|
|
21548
|
+
<td class="result">${escHtml(entry.result)}</td>
|
|
21549
|
+
</tr>`;
|
|
21550
|
+
}).join("");
|
|
21551
|
+
const approvalItems = pending_approvals.length === 0 ? `<div class="empty-block">No pending approvals</div>` : pending_approvals.map((p) => `<article class="approval" data-id="${escHtml(p.id)}">
|
|
21552
|
+
<div class="approval-head">
|
|
21553
|
+
<span class="tier-chip tier-${escHtml(p.tier)}">Tier ${escHtml(p.tier)}</span>
|
|
21554
|
+
<span class="mono">${escHtml(p.operation)}</span>
|
|
21555
|
+
</div>
|
|
21556
|
+
<p class="approval-reason">${escHtml(p.reason)}</p>
|
|
21557
|
+
<div class="approval-actions">
|
|
21558
|
+
<button class="btn btn-allow" data-action="allow" data-id="${escHtml(p.id)}">Allow</button>
|
|
21559
|
+
<button class="btn btn-deny" data-action="deny" data-id="${escHtml(p.id)}">Deny</button>
|
|
21560
|
+
</div>
|
|
21561
|
+
</article>`).join("");
|
|
21562
|
+
const auditRows = audit.length === 0 ? `<tr class="empty"><td colspan="4">Audit log empty</td></tr>` : audit.map((entry) => `<tr data-kind="${escHtml(entry.layer)}" data-op="${escHtml(entry.operation)}">
|
|
21563
|
+
<td class="mono time">${escHtml(new Date(entry.timestamp).toLocaleTimeString())}</td>
|
|
21564
|
+
<td class="layer-pill">${escHtml(entry.layer.toUpperCase())}</td>
|
|
21565
|
+
<td class="mono">${escHtml(entry.operation)}</td>
|
|
21566
|
+
<td class="result-${escHtml(entry.result)}">${escHtml(entry.result)}</td>
|
|
21567
|
+
</tr>`).join("");
|
|
21568
|
+
const serverRows = upstream_servers.length === 0 ? `<li class="empty-block">No upstream servers connected</li>` : upstream_servers.map((s) => `<li class="server-row state-${escHtml(s.state)}">
|
|
21569
|
+
<span class="server-dot"></span>
|
|
21570
|
+
<span class="mono">${escHtml(s.name)}</span>
|
|
21571
|
+
<span class="server-meta">${escHtml(s.state)} \xB7 ${escHtml(s.tool_count)} tool${s.tool_count === 1 ? "" : "s"}</span>
|
|
21572
|
+
</li>`).join("");
|
|
21573
|
+
const initialSnapshot = JSON.stringify(snapshot).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
21574
|
+
return `<!DOCTYPE html>
|
|
21575
|
+
<html lang="en">
|
|
21576
|
+
<head>
|
|
21577
|
+
<meta charset="UTF-8">
|
|
21578
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
21579
|
+
<title>Sanctuary \u2014 Sovereignty Dashboard</title>
|
|
21580
|
+
<style>
|
|
21581
|
+
:root {
|
|
21582
|
+
--bg: #07080c;
|
|
21583
|
+
--bg-2: #0f1220;
|
|
21584
|
+
--surface: #131729;
|
|
21585
|
+
--surface-2: #1a1f36;
|
|
21586
|
+
--border: #26304d;
|
|
21587
|
+
--border-strong: #39436a;
|
|
21588
|
+
--ink: #eef1fb;
|
|
21589
|
+
--ink-dim: #b9c0dc;
|
|
21590
|
+
--ink-mute: #7e86a8;
|
|
21591
|
+
--indigo: #6e7bff;
|
|
21592
|
+
--indigo-deep: #3b4ad8;
|
|
21593
|
+
--green: #3ee08f;
|
|
21594
|
+
--green-deep: #168a4d;
|
|
21595
|
+
--amber: #f1c05a;
|
|
21596
|
+
--red: #ff6b7a;
|
|
21597
|
+
--violet: #a77bff;
|
|
21598
|
+
--radius: 14px;
|
|
21599
|
+
--radius-sm: 8px;
|
|
21600
|
+
--shadow: 0 14px 42px rgba(3, 6, 19, 0.45);
|
|
21601
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
21602
|
+
}
|
|
21603
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21604
|
+
html, body { background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif; min-height: 100vh; -webkit-font-smoothing: antialiased; }
|
|
21605
|
+
body { background: radial-gradient(circle at 50% -200px, rgba(110, 123, 255, 0.18), transparent 60%), var(--bg); }
|
|
21606
|
+
a { color: var(--indigo); text-decoration: none; }
|
|
21607
|
+
button { font: inherit; cursor: pointer; }
|
|
21608
|
+
|
|
21609
|
+
.wrap { max-width: 1180px; margin: 0 auto; padding: 40px 32px 120px; }
|
|
21610
|
+
|
|
21611
|
+
/* \u2500\u2500 Top meta row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21612
|
+
.meta-row {
|
|
21613
|
+
display: flex;
|
|
21614
|
+
justify-content: space-between;
|
|
21615
|
+
align-items: center;
|
|
21616
|
+
color: var(--ink-mute);
|
|
21617
|
+
font-size: 12px;
|
|
21618
|
+
letter-spacing: 0.08em;
|
|
21619
|
+
text-transform: uppercase;
|
|
21620
|
+
margin-bottom: 8px;
|
|
21621
|
+
}
|
|
21622
|
+
.meta-row .mode-pill {
|
|
21623
|
+
padding: 4px 10px;
|
|
21624
|
+
border-radius: 999px;
|
|
21625
|
+
border: 1px solid var(--border);
|
|
21626
|
+
background: var(--surface);
|
|
21627
|
+
font-family: var(--mono);
|
|
21628
|
+
text-transform: none;
|
|
21629
|
+
letter-spacing: 0;
|
|
21630
|
+
font-size: 11px;
|
|
21631
|
+
color: var(--ink-dim);
|
|
21632
|
+
}
|
|
21633
|
+
|
|
21634
|
+
/* \u2500\u2500 Hero \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21635
|
+
.hero {
|
|
21636
|
+
display: flex;
|
|
21637
|
+
flex-direction: column;
|
|
21638
|
+
align-items: center;
|
|
21639
|
+
text-align: center;
|
|
21640
|
+
padding: 48px 24px 56px;
|
|
21641
|
+
border-radius: 22px;
|
|
21642
|
+
background:
|
|
21643
|
+
radial-gradient(circle at 50% 0%, rgba(62, 224, 143, 0.08), transparent 70%),
|
|
21644
|
+
linear-gradient(180deg, var(--bg-2) 0%, var(--surface) 100%);
|
|
21645
|
+
border: 1px solid var(--border);
|
|
21646
|
+
box-shadow: var(--shadow);
|
|
21647
|
+
margin-bottom: 32px;
|
|
21648
|
+
position: relative;
|
|
21649
|
+
overflow: hidden;
|
|
21650
|
+
}
|
|
21651
|
+
.hero::after {
|
|
21652
|
+
content: "";
|
|
21653
|
+
position: absolute;
|
|
21654
|
+
inset: 0;
|
|
21655
|
+
background: radial-gradient(circle at 50% 100%, rgba(110, 123, 255, 0.10), transparent 60%);
|
|
21656
|
+
pointer-events: none;
|
|
21657
|
+
}
|
|
21658
|
+
.shield {
|
|
21659
|
+
width: 200px;
|
|
21660
|
+
height: 200px;
|
|
21661
|
+
position: relative;
|
|
21662
|
+
filter: drop-shadow(0 16px 32px rgba(62, 224, 143, 0.18));
|
|
21663
|
+
animation: hero-in 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
21664
|
+
}
|
|
21665
|
+
@keyframes hero-in {
|
|
21666
|
+
from { opacity: 0; transform: translateY(12px) scale(0.96); }
|
|
21667
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
21668
|
+
}
|
|
21669
|
+
.shield svg { width: 100%; height: 100%; display: block; }
|
|
21670
|
+
.shield.green .shield-ring { stroke: var(--green); }
|
|
21671
|
+
.shield.green .shield-core { fill: rgba(62, 224, 143, 0.14); }
|
|
21672
|
+
.shield.green .shield-mark { stroke: var(--green); }
|
|
21673
|
+
.shield.yellow .shield-ring { stroke: var(--amber); }
|
|
21674
|
+
.shield.yellow .shield-core { fill: rgba(241, 192, 90, 0.14); }
|
|
21675
|
+
.shield.yellow .shield-mark { stroke: var(--amber); }
|
|
21676
|
+
.shield.red .shield-ring { stroke: var(--red); }
|
|
21677
|
+
.shield.red .shield-core { fill: rgba(255, 107, 122, 0.16); }
|
|
21678
|
+
.shield.red .shield-mark { stroke: var(--red); }
|
|
21679
|
+
.shield .shield-ring {
|
|
21680
|
+
fill: none;
|
|
21681
|
+
stroke-width: 3;
|
|
21682
|
+
stroke-dasharray: 600;
|
|
21683
|
+
stroke-dashoffset: 0;
|
|
21684
|
+
transform-origin: center;
|
|
21685
|
+
transition: stroke 320ms ease;
|
|
21686
|
+
}
|
|
21687
|
+
.shield .shield-ring-bg { fill: none; stroke: rgba(255, 255, 255, 0.06); stroke-width: 3; }
|
|
21688
|
+
.shield .shield-mark { fill: none; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; transition: stroke 320ms ease; }
|
|
21689
|
+
|
|
21690
|
+
.hero h1 {
|
|
21691
|
+
font-size: 40px;
|
|
21692
|
+
font-weight: 650;
|
|
21693
|
+
letter-spacing: -0.02em;
|
|
21694
|
+
margin-top: 28px;
|
|
21695
|
+
position: relative;
|
|
21696
|
+
z-index: 1;
|
|
21697
|
+
}
|
|
21698
|
+
.hero .hero-sub {
|
|
21699
|
+
margin-top: 10px;
|
|
21700
|
+
color: var(--ink-dim);
|
|
21701
|
+
font-size: 15px;
|
|
21702
|
+
position: relative;
|
|
21703
|
+
z-index: 1;
|
|
21704
|
+
}
|
|
21705
|
+
.identity-line {
|
|
21706
|
+
margin-top: 22px;
|
|
21707
|
+
display: inline-flex;
|
|
21708
|
+
align-items: center;
|
|
21709
|
+
gap: 10px;
|
|
21710
|
+
padding: 10px 18px;
|
|
21711
|
+
border-radius: 999px;
|
|
21712
|
+
border: 1px solid var(--border);
|
|
21713
|
+
background: rgba(19, 23, 41, 0.7);
|
|
21714
|
+
font-family: var(--mono);
|
|
21715
|
+
font-size: 13px;
|
|
21716
|
+
color: var(--ink-dim);
|
|
21717
|
+
position: relative;
|
|
21718
|
+
z-index: 1;
|
|
21719
|
+
}
|
|
21720
|
+
.identity-line .name { color: var(--ink); font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif; letter-spacing: 0; }
|
|
21721
|
+
.identity-line .sep { color: var(--ink-mute); }
|
|
21722
|
+
.identity-line .did { color: var(--violet); }
|
|
21723
|
+
.identity-line .primary-flag {
|
|
21724
|
+
padding: 2px 8px;
|
|
21725
|
+
border-radius: 999px;
|
|
21726
|
+
background: rgba(110, 123, 255, 0.16);
|
|
21727
|
+
color: var(--indigo);
|
|
21728
|
+
font-size: 11px;
|
|
21729
|
+
text-transform: uppercase;
|
|
21730
|
+
letter-spacing: 0.1em;
|
|
21731
|
+
font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif;
|
|
21732
|
+
}
|
|
21733
|
+
|
|
21734
|
+
/* \u2500\u2500 Layer grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21735
|
+
.layer-grid {
|
|
21736
|
+
display: grid;
|
|
21737
|
+
grid-template-columns: repeat(4, 1fr);
|
|
21738
|
+
gap: 16px;
|
|
21739
|
+
margin-bottom: 32px;
|
|
21740
|
+
}
|
|
21741
|
+
@media (max-width: 960px) { .layer-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
21742
|
+
@media (max-width: 520px) { .layer-grid { grid-template-columns: 1fr; } }
|
|
21743
|
+
|
|
21744
|
+
.layer-card {
|
|
21745
|
+
padding: 20px;
|
|
21746
|
+
border-radius: var(--radius);
|
|
21747
|
+
background: var(--surface);
|
|
21748
|
+
border: 1px solid var(--border);
|
|
21749
|
+
transition: border 220ms ease, transform 220ms ease;
|
|
21750
|
+
}
|
|
21751
|
+
.layer-card:hover { border-color: var(--border-strong); transform: translateY(-1px); }
|
|
21752
|
+
|
|
21753
|
+
.layer-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 14px; }
|
|
21754
|
+
.layer-dot { width: 12px; height: 12px; border-radius: 50%; margin-top: 6px; background: var(--ink-mute); box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.04); }
|
|
21755
|
+
.layer-full .layer-dot { background: var(--green); box-shadow: 0 0 0 3px rgba(62, 224, 143, 0.18); }
|
|
21756
|
+
.layer-degraded .layer-dot { background: var(--amber); box-shadow: 0 0 0 3px rgba(241, 192, 90, 0.18); }
|
|
21757
|
+
.layer-compromised .layer-dot { background: var(--red); box-shadow: 0 0 0 3px rgba(255, 107, 122, 0.18); }
|
|
21758
|
+
.layer-head h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-dim); margin-bottom: 4px; }
|
|
21759
|
+
.layer-headline { font-size: 15px; color: var(--ink); line-height: 1.35; }
|
|
21760
|
+
|
|
21761
|
+
.layer-detail { display: flex; flex-direction: column; gap: 8px; }
|
|
21762
|
+
.layer-detail > div { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
|
|
21763
|
+
.layer-detail dt { color: var(--ink-mute); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
|
|
21764
|
+
.layer-detail dd { color: var(--ink-dim); text-align: right; font-family: var(--mono); font-size: 12px; }
|
|
21765
|
+
|
|
21766
|
+
.layer-cta { margin-top: 6px; text-align: center; padding: 16px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); }
|
|
21767
|
+
.score-block { display: flex; flex-direction: column; gap: 2px; }
|
|
21768
|
+
.score-value { font-size: 28px; font-weight: 650; color: var(--green); letter-spacing: -0.02em; }
|
|
21769
|
+
.score-label { font-size: 11px; text-transform: uppercase; color: var(--ink-mute); letter-spacing: 0.08em; }
|
|
21770
|
+
.claim-block { font-size: 13px; color: var(--violet); }
|
|
21771
|
+
|
|
21772
|
+
/* \u2500\u2500 L4 evidence widget (v0.9.1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21773
|
+
.l4-evidence { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border); }
|
|
21774
|
+
.l4-evidence-summary { gap: 6px; margin-bottom: 10px; }
|
|
21775
|
+
.l4-evidence-summary dt { font-size: 10px; }
|
|
21776
|
+
.l4-evidence-summary dd { font-size: 11px; }
|
|
21777
|
+
.l4-deg-list { list-style: none; display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
|
21778
|
+
.l4-deg { padding: 8px 10px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); font-size: 12px; }
|
|
21779
|
+
.l4-deg-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
|
|
21780
|
+
.l4-deg-code { font-family: var(--mono); font-size: 11px; color: var(--ink); letter-spacing: 0.02em; }
|
|
21781
|
+
.l4-deg-sev { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); }
|
|
21782
|
+
.l4-deg-warning { border-color: rgba(241, 192, 90, 0.4); }
|
|
21783
|
+
.l4-deg-warning .l4-deg-sev { color: var(--amber); }
|
|
21784
|
+
.l4-deg-critical { border-color: rgba(255, 107, 122, 0.5); }
|
|
21785
|
+
.l4-deg-critical .l4-deg-sev { color: var(--red); }
|
|
21786
|
+
.l4-deg-info .l4-deg-sev { color: var(--indigo); }
|
|
21787
|
+
.l4-deg-desc { color: var(--ink-dim); line-height: 1.35; font-size: 11px; }
|
|
21788
|
+
.l4-deg-mit { color: var(--ink-mute); line-height: 1.35; font-size: 10px; margin-top: 3px; font-style: italic; }
|
|
21789
|
+
|
|
21790
|
+
/* \u2500\u2500 Section headers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21791
|
+
.section { margin-bottom: 28px; }
|
|
21792
|
+
.section h2 {
|
|
21793
|
+
font-size: 13px;
|
|
21794
|
+
text-transform: uppercase;
|
|
21795
|
+
letter-spacing: 0.1em;
|
|
21796
|
+
color: var(--ink-dim);
|
|
21797
|
+
margin-bottom: 12px;
|
|
21798
|
+
display: flex;
|
|
21799
|
+
align-items: center;
|
|
21800
|
+
gap: 12px;
|
|
21801
|
+
}
|
|
21802
|
+
.section h2 .count {
|
|
21803
|
+
font-family: var(--mono);
|
|
21804
|
+
font-size: 11px;
|
|
21805
|
+
padding: 2px 8px;
|
|
21806
|
+
border-radius: 999px;
|
|
21807
|
+
background: var(--surface);
|
|
21808
|
+
border: 1px solid var(--border);
|
|
21809
|
+
color: var(--ink-mute);
|
|
21810
|
+
text-transform: none;
|
|
21811
|
+
letter-spacing: 0;
|
|
21812
|
+
}
|
|
21813
|
+
|
|
21814
|
+
/* \u2500\u2500 Approval queue \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21815
|
+
.approval-list { display: flex; flex-direction: column; gap: 10px; }
|
|
21816
|
+
.approval {
|
|
21817
|
+
padding: 16px;
|
|
21818
|
+
background: var(--surface);
|
|
21819
|
+
border: 1px solid var(--amber);
|
|
21820
|
+
border-radius: var(--radius);
|
|
21821
|
+
box-shadow: 0 0 0 1px rgba(241, 192, 90, 0.18);
|
|
21822
|
+
animation: fade-up 260ms ease both;
|
|
21823
|
+
}
|
|
21824
|
+
@keyframes fade-up { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
21825
|
+
.approval-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 14px; }
|
|
21826
|
+
.tier-chip { padding: 2px 8px; border-radius: 999px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; font-family: -apple-system, Inter, sans-serif; }
|
|
21827
|
+
.tier-chip.tier-1 { background: rgba(255, 107, 122, 0.16); color: var(--red); }
|
|
21828
|
+
.tier-chip.tier-2 { background: rgba(241, 192, 90, 0.16); color: var(--amber); }
|
|
21829
|
+
.approval-reason { font-size: 13px; color: var(--ink-dim); margin-bottom: 12px; }
|
|
21830
|
+
.approval-actions { display: flex; gap: 8px; }
|
|
21831
|
+
.btn {
|
|
21832
|
+
padding: 7px 14px;
|
|
21833
|
+
border-radius: var(--radius-sm);
|
|
21834
|
+
border: 1px solid var(--border);
|
|
21835
|
+
background: var(--surface-2);
|
|
21836
|
+
color: var(--ink);
|
|
21837
|
+
font-size: 13px;
|
|
21838
|
+
transition: all 160ms ease;
|
|
21839
|
+
}
|
|
21840
|
+
.btn:hover { border-color: var(--border-strong); background: #202641; }
|
|
21841
|
+
.btn-allow { background: var(--green-deep); border-color: var(--green-deep); color: white; }
|
|
21842
|
+
.btn-allow:hover { background: #1ba25b; border-color: #1ba25b; }
|
|
21843
|
+
.btn-deny { background: transparent; border-color: var(--red); color: var(--red); }
|
|
21844
|
+
.btn-deny:hover { background: rgba(255, 107, 122, 0.1); border-color: var(--red); }
|
|
21845
|
+
|
|
21846
|
+
/* \u2500\u2500 Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21847
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
21848
|
+
.panel-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--border); }
|
|
21849
|
+
.panel-head h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
|
|
21850
|
+
.filter-row { display: flex; gap: 6px; }
|
|
21851
|
+
.filter-row button {
|
|
21852
|
+
padding: 4px 10px;
|
|
21853
|
+
font-size: 11px;
|
|
21854
|
+
border-radius: 999px;
|
|
21855
|
+
background: transparent;
|
|
21856
|
+
border: 1px solid var(--border);
|
|
21857
|
+
color: var(--ink-mute);
|
|
21858
|
+
text-transform: uppercase;
|
|
21859
|
+
letter-spacing: 0.06em;
|
|
21860
|
+
transition: all 140ms ease;
|
|
21861
|
+
}
|
|
21862
|
+
.filter-row button.active, .filter-row button:hover { color: var(--ink); border-color: var(--border-strong); background: var(--surface-2); }
|
|
21863
|
+
|
|
21864
|
+
table { width: 100%; border-collapse: collapse; }
|
|
21865
|
+
th, td { padding: 10px 18px; text-align: left; font-size: 13px; border-bottom: 1px solid var(--border); }
|
|
21866
|
+
th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); font-weight: 500; background: var(--bg-2); }
|
|
21867
|
+
tr:last-child td { border-bottom: none; }
|
|
21868
|
+
tr.empty td { text-align: center; color: var(--ink-mute); padding: 32px 18px; }
|
|
21869
|
+
.mono { font-family: var(--mono); font-size: 12px; }
|
|
21870
|
+
.time { color: var(--ink-mute); white-space: nowrap; }
|
|
21871
|
+
.tier { text-align: center; font-family: var(--mono); font-size: 11px; font-weight: 600; }
|
|
21872
|
+
.tier-1 { color: var(--red); }
|
|
21873
|
+
.tier-2 { color: var(--amber); }
|
|
21874
|
+
.tier-3 { color: var(--green); }
|
|
21875
|
+
.result { font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
21876
|
+
.result-allowed, .result-success { color: var(--green); }
|
|
21877
|
+
.result-denied, .result-failure { color: var(--red); }
|
|
21878
|
+
.result-approved { color: var(--indigo); }
|
|
21879
|
+
.result-pending { color: var(--amber); }
|
|
21880
|
+
.layer-pill { font-family: var(--mono); font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--ink-dim); display: inline-block; }
|
|
21881
|
+
|
|
21882
|
+
/* \u2500\u2500 Upstream servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21883
|
+
.server-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
|
|
21884
|
+
.server-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
|
|
21885
|
+
.server-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-mute); }
|
|
21886
|
+
.server-row.state-connected .server-dot { background: var(--green); }
|
|
21887
|
+
.server-row.state-connecting .server-dot { background: var(--amber); }
|
|
21888
|
+
.server-row.state-disconnected .server-dot, .server-row.state-error .server-dot { background: var(--red); }
|
|
21889
|
+
.server-meta { margin-left: auto; font-size: 12px; color: var(--ink-mute); font-family: var(--mono); }
|
|
21890
|
+
|
|
21891
|
+
.empty-block { padding: 18px; color: var(--ink-mute); text-align: center; font-size: 13px; background: var(--surface); border: 1px dashed var(--border); border-radius: var(--radius-sm); }
|
|
21892
|
+
|
|
21893
|
+
/* \u2500\u2500 Audit collapsible \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
21894
|
+
details.audit-details { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
21895
|
+
details.audit-details summary { cursor: pointer; list-style: none; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; }
|
|
21896
|
+
details.audit-details summary::-webkit-details-marker { display: none; }
|
|
21897
|
+
details.audit-details summary h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
|
|
21898
|
+
details.audit-details summary .caret { color: var(--ink-mute); font-family: var(--mono); }
|
|
21899
|
+
details.audit-details[open] .caret { transform: rotate(90deg); display: inline-block; }
|
|
21900
|
+
details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px 12px; border-bottom: 1px solid var(--border); }
|
|
21901
|
+
</style>
|
|
21902
|
+
</head>
|
|
21903
|
+
<body>
|
|
21904
|
+
<div class="wrap">
|
|
21905
|
+
<div class="meta-row">
|
|
21906
|
+
<span>Sanctuary Framework</span>
|
|
21907
|
+
<span class="mode-pill" id="mode-pill">${escHtml(snapshot.mode)} \xB7 v${escHtml(snapshot.server_version)}</span>
|
|
21908
|
+
</div>
|
|
21909
|
+
|
|
21910
|
+
<section class="hero">
|
|
21911
|
+
<div class="shield ${escHtml(overall.light)}" id="shield">
|
|
21912
|
+
<svg viewBox="0 0 200 200" aria-hidden="true">
|
|
21913
|
+
<circle class="shield-ring-bg" cx="100" cy="100" r="92"></circle>
|
|
21914
|
+
<circle class="shield-ring" cx="100" cy="100" r="92"></circle>
|
|
21915
|
+
<circle class="shield-core" cx="100" cy="100" r="80" fill="rgba(255,255,255,0.02)"></circle>
|
|
21916
|
+
<path class="shield-mark" d="M100 52 L132 68 L132 108 C132 130 118 144 100 150 C82 144 68 130 68 108 L68 68 Z"></path>
|
|
21917
|
+
<path class="shield-mark" d="M85 102 L96 113 L118 90"></path>
|
|
21918
|
+
</svg>
|
|
21919
|
+
</div>
|
|
21920
|
+
<h1 id="hero-copy">${escHtml(HERO_COPY)}</h1>
|
|
21921
|
+
<p class="hero-sub" id="hero-sub">${escHtml(overall.headline)}</p>
|
|
21922
|
+
<div class="identity-line" id="identity-line">
|
|
21923
|
+
<span class="name" id="agent-name">${escHtml(agent.display_name)}</span>
|
|
21924
|
+
<span class="sep">\xB7</span>
|
|
21925
|
+
<span class="did" id="agent-did">${escHtml(agent.did_fingerprint ?? "unclaimed")}</span>
|
|
21926
|
+
${agent.primary_identity_id ? `<span class="primary-flag">Primary</span>` : ""}
|
|
21927
|
+
</div>
|
|
21928
|
+
</section>
|
|
21929
|
+
|
|
21930
|
+
<div class="layer-grid" id="layer-grid">
|
|
21931
|
+
${l1Card(layers.l1)}
|
|
21932
|
+
${l2Card(layers.l2)}
|
|
21933
|
+
${l3Card(layers.l3)}
|
|
21934
|
+
${l4Card(layers.l4)}
|
|
21935
|
+
</div>
|
|
21936
|
+
|
|
21937
|
+
<section class="section" id="approval-section">
|
|
21938
|
+
<h2>Needs approval <span class="count" id="approval-count">${pending_approvals.length}</span></h2>
|
|
21939
|
+
<div class="approval-list" id="approval-list">${approvalItems}</div>
|
|
21940
|
+
</section>
|
|
21941
|
+
|
|
21942
|
+
<section class="section">
|
|
21943
|
+
<h2>Upstream servers <span class="count">${upstream_servers.length}</span></h2>
|
|
21944
|
+
<ul class="server-list" id="server-list">${serverRows}</ul>
|
|
21945
|
+
</section>
|
|
21946
|
+
|
|
21947
|
+
<section class="section">
|
|
21948
|
+
<h2>Live activity <span class="count" id="activity-count">${activity.length}</span></h2>
|
|
21949
|
+
<div class="panel">
|
|
21950
|
+
<div class="panel-head"><h3>Recent tool calls</h3></div>
|
|
21951
|
+
<table>
|
|
21952
|
+
<thead><tr><th>Time</th><th>Tool</th><th>Server</th><th>Tier</th><th>Result</th></tr></thead>
|
|
21953
|
+
<tbody id="activity-body">${activityRows}</tbody>
|
|
21954
|
+
</table>
|
|
21955
|
+
</div>
|
|
21956
|
+
</section>
|
|
21957
|
+
|
|
21958
|
+
<section class="section">
|
|
21959
|
+
<details class="audit-details">
|
|
21960
|
+
<summary>
|
|
21961
|
+
<h3>Audit trail <span class="count" id="audit-count">${audit.length}</span></h3>
|
|
21962
|
+
<span class="caret">\u25B8</span>
|
|
21963
|
+
</summary>
|
|
21964
|
+
<div class="audit-filters">
|
|
21965
|
+
<div class="filter-row" id="audit-filter">
|
|
21966
|
+
<button class="active" data-filter="all">All</button>
|
|
21967
|
+
<button data-filter="l1">Cognitive</button>
|
|
21968
|
+
<button data-filter="l2">Operational</button>
|
|
21969
|
+
<button data-filter="l3">Disclosure</button>
|
|
21970
|
+
<button data-filter="l4">Reputation</button>
|
|
21971
|
+
</div>
|
|
21972
|
+
</div>
|
|
21973
|
+
<table>
|
|
21974
|
+
<thead><tr><th>Time</th><th>Layer</th><th>Operation</th><th>Result</th></tr></thead>
|
|
21975
|
+
<tbody id="audit-body">${auditRows}</tbody>
|
|
21976
|
+
</table>
|
|
21977
|
+
</details>
|
|
21978
|
+
</section>
|
|
21979
|
+
</div>
|
|
21980
|
+
|
|
21981
|
+
<script>
|
|
21982
|
+
(() => {
|
|
21983
|
+
const INITIAL_SNAPSHOT = ${initialSnapshot};
|
|
21984
|
+
const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
21985
|
+
const AUTH_HEADER = AUTH_TOKEN ? { "Authorization": "Bearer " + AUTH_TOKEN } : {};
|
|
21986
|
+
const AUTH_QS = AUTH_TOKEN ? "?token=" + encodeURIComponent(AUTH_TOKEN) : "";
|
|
21987
|
+
|
|
21988
|
+
let snapshot = INITIAL_SNAPSHOT;
|
|
21989
|
+
|
|
21990
|
+
function esc(value) {
|
|
21991
|
+
if (value == null) return "";
|
|
21992
|
+
return String(value)
|
|
21993
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
21994
|
+
.replace(/"/g, """).replace(/'/g, "'");
|
|
21995
|
+
}
|
|
21996
|
+
function fmtTime(iso) {
|
|
21997
|
+
try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
|
|
21998
|
+
}
|
|
21999
|
+
|
|
22000
|
+
function renderShield(light, headline) {
|
|
22001
|
+
const shield = document.getElementById("shield");
|
|
22002
|
+
if (!shield) return;
|
|
22003
|
+
shield.classList.remove("green", "yellow", "red");
|
|
22004
|
+
shield.classList.add(light);
|
|
22005
|
+
document.getElementById("hero-sub").textContent = headline;
|
|
22006
|
+
}
|
|
22007
|
+
|
|
22008
|
+
function renderActivity(entries) {
|
|
22009
|
+
const body = document.getElementById("activity-body");
|
|
22010
|
+
const count = document.getElementById("activity-count");
|
|
22011
|
+
if (!body) return;
|
|
22012
|
+
count.textContent = String(entries.length);
|
|
22013
|
+
if (!entries.length) {
|
|
22014
|
+
body.innerHTML = '<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>';
|
|
22015
|
+
return;
|
|
22016
|
+
}
|
|
22017
|
+
body.innerHTML = entries.map(e => (
|
|
22018
|
+
'<tr class="result-' + esc(e.result) + '">' +
|
|
22019
|
+
'<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
|
|
22020
|
+
'<td class="mono">' + esc(e.tool) + '</td>' +
|
|
22021
|
+
'<td class="mono">' + esc(e.server) + '</td>' +
|
|
22022
|
+
'<td class="tier tier-' + esc(e.tier) + '">T' + esc(e.tier) + '</td>' +
|
|
22023
|
+
'<td class="result">' + esc(e.result) + '</td>' +
|
|
22024
|
+
'</tr>'
|
|
22025
|
+
)).join("");
|
|
22026
|
+
}
|
|
22027
|
+
|
|
22028
|
+
function renderApprovals(list) {
|
|
22029
|
+
const container = document.getElementById("approval-list");
|
|
22030
|
+
const count = document.getElementById("approval-count");
|
|
22031
|
+
if (!container) return;
|
|
22032
|
+
count.textContent = String(list.length);
|
|
22033
|
+
if (!list.length) {
|
|
22034
|
+
container.innerHTML = '<div class="empty-block">No pending approvals</div>';
|
|
22035
|
+
return;
|
|
22036
|
+
}
|
|
22037
|
+
container.innerHTML = list.map(p => (
|
|
22038
|
+
'<article class="approval" data-id="' + esc(p.id) + '">' +
|
|
22039
|
+
'<div class="approval-head">' +
|
|
22040
|
+
'<span class="tier-chip tier-' + esc(p.tier) + '">Tier ' + esc(p.tier) + '</span>' +
|
|
22041
|
+
'<span class="mono">' + esc(p.operation) + '</span>' +
|
|
22042
|
+
'</div>' +
|
|
22043
|
+
'<p class="approval-reason">' + esc(p.reason) + '</p>' +
|
|
22044
|
+
'<div class="approval-actions">' +
|
|
22045
|
+
'<button class="btn btn-allow" data-action="allow" data-id="' + esc(p.id) + '">Allow</button>' +
|
|
22046
|
+
'<button class="btn btn-deny" data-action="deny" data-id="' + esc(p.id) + '">Deny</button>' +
|
|
22047
|
+
'</div>' +
|
|
22048
|
+
'</article>'
|
|
22049
|
+
)).join("");
|
|
22050
|
+
wireApprovalButtons();
|
|
22051
|
+
}
|
|
22052
|
+
|
|
22053
|
+
function renderAudit(entries, filter) {
|
|
22054
|
+
filter = filter || "all";
|
|
22055
|
+
const body = document.getElementById("audit-body");
|
|
22056
|
+
const count = document.getElementById("audit-count");
|
|
22057
|
+
if (!body) return;
|
|
22058
|
+
const visible = filter === "all" ? entries : entries.filter(e => e.layer === filter);
|
|
22059
|
+
count.textContent = String(visible.length);
|
|
22060
|
+
if (!visible.length) {
|
|
22061
|
+
body.innerHTML = '<tr class="empty"><td colspan="4">Audit log empty</td></tr>';
|
|
22062
|
+
return;
|
|
22063
|
+
}
|
|
22064
|
+
body.innerHTML = visible.map(e => (
|
|
22065
|
+
'<tr data-kind="' + esc(e.layer) + '" data-op="' + esc(e.operation) + '">' +
|
|
22066
|
+
'<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
|
|
22067
|
+
'<td><span class="layer-pill">' + esc(String(e.layer).toUpperCase()) + '</span></td>' +
|
|
22068
|
+
'<td class="mono">' + esc(e.operation) + '</td>' +
|
|
22069
|
+
'<td class="result-' + esc(e.result) + '">' + esc(e.result) + '</td>' +
|
|
22070
|
+
'</tr>'
|
|
22071
|
+
)).join("");
|
|
22072
|
+
}
|
|
22073
|
+
|
|
22074
|
+
function renderAll(snap) {
|
|
22075
|
+
snapshot = snap;
|
|
22076
|
+
renderShield(snap.overall.light, snap.overall.headline);
|
|
22077
|
+
const mp = document.getElementById("mode-pill");
|
|
22078
|
+
if (mp) mp.textContent = snap.mode + " \xB7 v" + snap.server_version;
|
|
22079
|
+
document.getElementById("agent-name").textContent = snap.agent.display_name;
|
|
22080
|
+
document.getElementById("agent-did").textContent = snap.agent.did_fingerprint || "unclaimed";
|
|
22081
|
+
renderActivity(snap.activity);
|
|
22082
|
+
renderApprovals(snap.pending_approvals);
|
|
22083
|
+
renderAudit(snap.audit, currentAuditFilter());
|
|
22084
|
+
}
|
|
22085
|
+
|
|
22086
|
+
function currentAuditFilter() {
|
|
22087
|
+
const active = document.querySelector("#audit-filter button.active");
|
|
22088
|
+
return active ? active.dataset.filter : "all";
|
|
22089
|
+
}
|
|
22090
|
+
|
|
22091
|
+
function wireApprovalButtons() {
|
|
22092
|
+
document.querySelectorAll("#approval-list button[data-action]").forEach(btn => {
|
|
22093
|
+
btn.addEventListener("click", async () => {
|
|
22094
|
+
const id = btn.dataset.id;
|
|
22095
|
+
const action = btn.dataset.action;
|
|
22096
|
+
btn.disabled = true;
|
|
22097
|
+
try {
|
|
22098
|
+
await fetch("/api/approvals/" + encodeURIComponent(id) + "/" + action + AUTH_QS, {
|
|
22099
|
+
method: "POST", headers: AUTH_HEADER
|
|
22100
|
+
});
|
|
22101
|
+
} catch {}
|
|
22102
|
+
await refreshSnapshot();
|
|
22103
|
+
});
|
|
22104
|
+
});
|
|
22105
|
+
}
|
|
22106
|
+
|
|
22107
|
+
function wireAuditFilter() {
|
|
22108
|
+
document.querySelectorAll("#audit-filter button").forEach(btn => {
|
|
22109
|
+
btn.addEventListener("click", () => {
|
|
22110
|
+
document.querySelectorAll("#audit-filter button").forEach(b => b.classList.remove("active"));
|
|
22111
|
+
btn.classList.add("active");
|
|
22112
|
+
renderAudit(snapshot.audit, btn.dataset.filter);
|
|
22113
|
+
});
|
|
22114
|
+
});
|
|
22115
|
+
}
|
|
22116
|
+
|
|
22117
|
+
async function refreshSnapshot() {
|
|
22118
|
+
try {
|
|
22119
|
+
const res = await fetch("/api/snapshot" + AUTH_QS, { headers: AUTH_HEADER });
|
|
22120
|
+
if (!res.ok) return;
|
|
22121
|
+
const snap = await res.json();
|
|
22122
|
+
renderAll(snap);
|
|
22123
|
+
} catch {}
|
|
22124
|
+
}
|
|
22125
|
+
|
|
22126
|
+
function connectSSE() {
|
|
22127
|
+
try {
|
|
22128
|
+
const es = new EventSource("/api/stream" + AUTH_QS);
|
|
22129
|
+
es.addEventListener("snapshot", (ev) => {
|
|
22130
|
+
try { renderAll(JSON.parse(ev.data)); } catch {}
|
|
22131
|
+
});
|
|
22132
|
+
es.addEventListener("activity", () => refreshSnapshot());
|
|
22133
|
+
es.addEventListener("approval", () => refreshSnapshot());
|
|
22134
|
+
es.onerror = () => { es.close(); setTimeout(connectSSE, 3000); };
|
|
22135
|
+
} catch {}
|
|
22136
|
+
}
|
|
22137
|
+
|
|
22138
|
+
wireApprovalButtons();
|
|
22139
|
+
wireAuditFilter();
|
|
22140
|
+
connectSSE();
|
|
22141
|
+
})();
|
|
22142
|
+
</script>
|
|
22143
|
+
</body>
|
|
22144
|
+
</html>`;
|
|
22145
|
+
}
|
|
22146
|
+
|
|
22147
|
+
// src/dashboard/api.ts
|
|
22148
|
+
function constantTimeEquals(a, b) {
|
|
22149
|
+
if (a.length !== b.length) return false;
|
|
22150
|
+
let diff = 0;
|
|
22151
|
+
for (let i = 0; i < a.length; i++) {
|
|
22152
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
22153
|
+
}
|
|
22154
|
+
return diff === 0;
|
|
22155
|
+
}
|
|
22156
|
+
function extractToken(req, url) {
|
|
22157
|
+
const header = req.headers.authorization;
|
|
22158
|
+
if (header && header.startsWith("Bearer ")) {
|
|
22159
|
+
return header.slice(7).trim();
|
|
22160
|
+
}
|
|
22161
|
+
const q = url.searchParams.get("token");
|
|
22162
|
+
return q ?? null;
|
|
22163
|
+
}
|
|
22164
|
+
function isAuthorized(deps, req, url) {
|
|
22165
|
+
if (!deps.authToken) return true;
|
|
22166
|
+
const token = extractToken(req, url);
|
|
22167
|
+
if (!token) return false;
|
|
22168
|
+
return constantTimeEquals(token, deps.authToken);
|
|
22169
|
+
}
|
|
22170
|
+
function writeJSON(res, status, payload) {
|
|
22171
|
+
res.writeHead(status, {
|
|
22172
|
+
"Content-Type": "application/json",
|
|
22173
|
+
"Cache-Control": "no-store"
|
|
22174
|
+
});
|
|
22175
|
+
res.end(JSON.stringify(payload));
|
|
22176
|
+
}
|
|
22177
|
+
function writeText(res, status, body, contentType = "text/plain") {
|
|
22178
|
+
res.writeHead(status, {
|
|
22179
|
+
"Content-Type": contentType,
|
|
22180
|
+
"Cache-Control": "no-store"
|
|
22181
|
+
});
|
|
22182
|
+
res.end(body);
|
|
22183
|
+
}
|
|
22184
|
+
async function handleRequest(deps, req, res) {
|
|
22185
|
+
const host = req.headers.host || "localhost";
|
|
22186
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
22187
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
22188
|
+
const path = url.pathname;
|
|
22189
|
+
if (!isAuthorized(deps, req, url)) {
|
|
22190
|
+
writeJSON(res, 401, { error: "unauthorized" });
|
|
22191
|
+
return true;
|
|
22192
|
+
}
|
|
22193
|
+
if (method === "GET" && path === "/api/health") {
|
|
22194
|
+
writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
|
|
22195
|
+
return true;
|
|
22196
|
+
}
|
|
22197
|
+
if (method === "GET" && (path === "/" || path === "/index.html")) {
|
|
22198
|
+
const snapshot = await getProtectionSnapshot(deps.sources);
|
|
22199
|
+
const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
|
|
22200
|
+
writeText(res, 200, html, "text/html; charset=utf-8");
|
|
22201
|
+
return true;
|
|
22202
|
+
}
|
|
22203
|
+
if (method === "GET" && path === "/api/snapshot") {
|
|
22204
|
+
const snapshot = await getProtectionSnapshot(deps.sources);
|
|
22205
|
+
writeJSON(res, 200, snapshot);
|
|
22206
|
+
return true;
|
|
22207
|
+
}
|
|
22208
|
+
const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
|
|
22209
|
+
if (method === "POST" && approvalMatch) {
|
|
22210
|
+
const id = decodeURIComponent(approvalMatch[1]);
|
|
22211
|
+
const action = approvalMatch[2];
|
|
22212
|
+
if (!deps.approvals) {
|
|
22213
|
+
writeJSON(res, 503, { error: "approvals_unavailable" });
|
|
22214
|
+
return true;
|
|
22215
|
+
}
|
|
22216
|
+
const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
|
|
22217
|
+
try {
|
|
22218
|
+
const ok = await handler(id);
|
|
22219
|
+
writeJSON(res, ok ? 200 : 404, { id, action, ok });
|
|
22220
|
+
} catch (err) {
|
|
22221
|
+
writeJSON(res, 500, { error: "approval_failed", message: err.message });
|
|
22222
|
+
}
|
|
22223
|
+
return true;
|
|
22224
|
+
}
|
|
22225
|
+
if (method === "GET" && path === "/api/stream") {
|
|
22226
|
+
await handleStream(deps, res);
|
|
22227
|
+
return true;
|
|
22228
|
+
}
|
|
22229
|
+
return false;
|
|
22230
|
+
}
|
|
22231
|
+
async function handleStream(deps, res) {
|
|
22232
|
+
res.writeHead(200, {
|
|
22233
|
+
"Content-Type": "text/event-stream",
|
|
22234
|
+
"Cache-Control": "no-cache, no-transform",
|
|
22235
|
+
Connection: "keep-alive",
|
|
22236
|
+
"X-Accel-Buffering": "no"
|
|
22237
|
+
});
|
|
22238
|
+
const snapshot = await getProtectionSnapshot(deps.sources);
|
|
22239
|
+
res.write(`event: snapshot
|
|
22240
|
+
data: ${JSON.stringify(snapshot)}
|
|
22241
|
+
|
|
22242
|
+
`);
|
|
22243
|
+
const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
|
|
22244
|
+
try {
|
|
22245
|
+
res.write(`event: ${event.type}
|
|
22246
|
+
data: ${JSON.stringify(event.data)}
|
|
22247
|
+
|
|
22248
|
+
`);
|
|
22249
|
+
} catch {
|
|
22250
|
+
}
|
|
22251
|
+
}) : () => {
|
|
22252
|
+
};
|
|
22253
|
+
const keepAlive = setInterval(() => {
|
|
22254
|
+
try {
|
|
22255
|
+
res.write(": keepalive\n\n");
|
|
22256
|
+
} catch {
|
|
22257
|
+
}
|
|
22258
|
+
}, 25e3);
|
|
22259
|
+
const cleanup = () => {
|
|
22260
|
+
clearInterval(keepAlive);
|
|
22261
|
+
unsubscribe();
|
|
22262
|
+
};
|
|
22263
|
+
res.on("close", cleanup);
|
|
22264
|
+
res.on("error", cleanup);
|
|
22265
|
+
}
|
|
22266
|
+
|
|
22267
|
+
// src/dashboard/server.ts
|
|
22268
|
+
var DEFAULT_PORT = 3501;
|
|
22269
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
22270
|
+
async function startDashboardServer(options) {
|
|
22271
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
22272
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
22273
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
22274
|
+
const onEvent = (listener) => {
|
|
22275
|
+
listeners.add(listener);
|
|
22276
|
+
return () => listeners.delete(listener);
|
|
22277
|
+
};
|
|
22278
|
+
const publish = (event) => {
|
|
22279
|
+
for (const listener of listeners) {
|
|
22280
|
+
try {
|
|
22281
|
+
listener(event);
|
|
22282
|
+
} catch {
|
|
22283
|
+
}
|
|
22284
|
+
}
|
|
22285
|
+
};
|
|
22286
|
+
const deps = {
|
|
22287
|
+
sources: options.sources,
|
|
22288
|
+
authToken: options.authToken,
|
|
22289
|
+
approvals: options.approvals,
|
|
22290
|
+
onEvent
|
|
22291
|
+
};
|
|
22292
|
+
const server = createServer$2(async (req, res) => {
|
|
22293
|
+
try {
|
|
22294
|
+
const served = await handleRequest(deps, req, res);
|
|
22295
|
+
if (!served) {
|
|
22296
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
22297
|
+
res.end(JSON.stringify({ error: "not_found", path: req.url }));
|
|
22298
|
+
}
|
|
22299
|
+
} catch (err) {
|
|
22300
|
+
try {
|
|
22301
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
22302
|
+
res.end(JSON.stringify({ error: "internal", message: err.message }));
|
|
22303
|
+
} catch {
|
|
22304
|
+
}
|
|
22305
|
+
}
|
|
22306
|
+
});
|
|
22307
|
+
await new Promise((resolve, reject) => {
|
|
22308
|
+
server.once("error", reject);
|
|
22309
|
+
server.listen(port, host, () => {
|
|
22310
|
+
server.off("error", reject);
|
|
22311
|
+
resolve();
|
|
22312
|
+
});
|
|
22313
|
+
});
|
|
22314
|
+
const actualPort = (() => {
|
|
22315
|
+
const addr = server.address();
|
|
22316
|
+
if (addr && typeof addr === "object") return addr.port;
|
|
22317
|
+
return port;
|
|
22318
|
+
})();
|
|
22319
|
+
const url = `http://${host}:${actualPort}`;
|
|
22320
|
+
return {
|
|
22321
|
+
url,
|
|
22322
|
+
port: actualPort,
|
|
22323
|
+
host,
|
|
22324
|
+
stop: () => new Promise((resolve, reject) => {
|
|
22325
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
22326
|
+
}),
|
|
22327
|
+
publish,
|
|
22328
|
+
publishActivity: (entry) => publish({ type: "activity", data: entry }),
|
|
22329
|
+
publishApproval: (approval) => publish({ type: "approval", data: approval })
|
|
22330
|
+
};
|
|
22331
|
+
}
|
|
22332
|
+
|
|
22333
|
+
// src/dashboard/index.ts
|
|
22334
|
+
async function startDashboard(options) {
|
|
22335
|
+
const activity = options.initialActivity ? [...options.initialActivity] : [];
|
|
22336
|
+
const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
|
|
22337
|
+
const sources = {
|
|
22338
|
+
mode: options.mode,
|
|
22339
|
+
server_version: options.serverVersion,
|
|
22340
|
+
...options.auditLog ? { auditLog: options.auditLog } : {},
|
|
22341
|
+
...options.identityManager ? { identityManager: options.identityManager } : {},
|
|
22342
|
+
...options.clientManager ? { clientManager: options.clientManager } : {},
|
|
22343
|
+
...options.baseline ? { baseline: options.baseline } : {},
|
|
22344
|
+
...options.policy ? { policy: options.policy } : {},
|
|
22345
|
+
...options.reputation ? { reputation: options.reputation } : {},
|
|
22346
|
+
...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
|
|
22347
|
+
...options.l4Evidence ? { l4Evidence: options.l4Evidence } : {},
|
|
22348
|
+
activity,
|
|
22349
|
+
pendingApprovals: pending
|
|
22350
|
+
};
|
|
22351
|
+
const serverOpts = {
|
|
22352
|
+
mode: options.mode,
|
|
22353
|
+
sources,
|
|
22354
|
+
...options.port != null ? { port: options.port } : {},
|
|
22355
|
+
...options.host ? { host: options.host } : {},
|
|
22356
|
+
...options.authToken ? { authToken: options.authToken } : {},
|
|
22357
|
+
...options.approvals ? { approvals: options.approvals } : {}
|
|
22358
|
+
};
|
|
22359
|
+
const handle = await startDashboardServer(serverOpts);
|
|
22360
|
+
const wrapped = {
|
|
22361
|
+
...handle,
|
|
22362
|
+
publishActivity: (entry) => {
|
|
22363
|
+
activity.unshift(entry);
|
|
22364
|
+
if (activity.length > 50) activity.length = 50;
|
|
22365
|
+
handle.publishActivity(entry);
|
|
22366
|
+
},
|
|
22367
|
+
publishApproval: (approval) => {
|
|
22368
|
+
pending.push(approval);
|
|
22369
|
+
handle.publishApproval(approval);
|
|
22370
|
+
}
|
|
22371
|
+
};
|
|
22372
|
+
return wrapped;
|
|
22373
|
+
}
|
|
22374
|
+
|
|
20851
22375
|
// src/index.ts
|
|
20852
22376
|
async function createSanctuaryServer(options) {
|
|
20853
22377
|
const config = await loadConfig(options?.configPath);
|
|
20854
22378
|
await mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
22379
|
+
await tightenStoragePermissions(config.storage_path);
|
|
20855
22380
|
const storage = options?.storage ?? new FilesystemStorage(
|
|
20856
22381
|
`${config.storage_path}/state`
|
|
20857
22382
|
);
|
|
@@ -21171,12 +22696,6 @@ async function createSanctuaryServer(options) {
|
|
|
21171
22696
|
}
|
|
21172
22697
|
};
|
|
21173
22698
|
const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
|
|
21174
|
-
const { tools: shrTools } = createSHRTools(
|
|
21175
|
-
config,
|
|
21176
|
-
identityManager,
|
|
21177
|
-
masterKey,
|
|
21178
|
-
auditLog
|
|
21179
|
-
);
|
|
21180
22699
|
const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
|
|
21181
22700
|
config,
|
|
21182
22701
|
identityManager,
|
|
@@ -21187,7 +22706,7 @@ async function createSanctuaryServer(options) {
|
|
|
21187
22706
|
verascoreUrl: config.verascore.url
|
|
21188
22707
|
}
|
|
21189
22708
|
);
|
|
21190
|
-
const { tools: l4Tools} = createL4Tools(
|
|
22709
|
+
const { tools: l4Tools, reputationStore } = createL4Tools(
|
|
21191
22710
|
storage,
|
|
21192
22711
|
masterKey,
|
|
21193
22712
|
identityManager,
|
|
@@ -21195,6 +22714,13 @@ async function createSanctuaryServer(options) {
|
|
|
21195
22714
|
handshakeResults,
|
|
21196
22715
|
config.verascore.url
|
|
21197
22716
|
);
|
|
22717
|
+
const { tools: shrTools } = createSHRTools(
|
|
22718
|
+
config,
|
|
22719
|
+
identityManager,
|
|
22720
|
+
masterKey,
|
|
22721
|
+
auditLog,
|
|
22722
|
+
reputationStore
|
|
22723
|
+
);
|
|
21198
22724
|
const { tools: federationTools } = createFederationTools(
|
|
21199
22725
|
auditLog,
|
|
21200
22726
|
handshakeResults
|
|
@@ -21285,7 +22811,8 @@ async function createSanctuaryServer(options) {
|
|
|
21285
22811
|
masterKey,
|
|
21286
22812
|
auditLog,
|
|
21287
22813
|
policy,
|
|
21288
|
-
keyProtection
|
|
22814
|
+
keyProtection,
|
|
22815
|
+
reputationStore
|
|
21289
22816
|
});
|
|
21290
22817
|
const { tools: memoryAttestTools } = createMemoryAttestTools(
|
|
21291
22818
|
identityManager,
|
|
@@ -21457,6 +22984,6 @@ async function createSanctuaryServer(options) {
|
|
|
21457
22984
|
};
|
|
21458
22985
|
}
|
|
21459
22986
|
|
|
21460
|
-
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getTemplate, initiateHandshake, listTemplateIds, loadConfig, loadPrincipalPolicy, recommendPolicy, resolveTier, respondToHandshake, signPayload, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
22987
|
+
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate, initiateHandshake, listTemplateIds, loadConfig, loadPrincipalPolicy, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
21461
22988
|
//# sourceMappingURL=index.js.map
|
|
21462
22989
|
//# sourceMappingURL=index.js.map
|