@suveren/gateway 0.29.1 → 0.30.0
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/control-plane/index.mjs +102 -32
- package/dist/mcp-server/http.mjs +120 -36
- package/dist/ui/assets/index-C2BnlNib.js +123 -0
- package/dist/ui/assets/{index-DUIlMVlP.css → index-DPgewsLG.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
- package/node_modules/qs/CHANGELOG.md +38 -14
- package/node_modules/qs/README.md +31 -3
- package/node_modules/qs/dist/qs.js +20 -20
- package/node_modules/qs/lib/parse.js +94 -4
- package/node_modules/qs/lib/stringify.js +4 -4
- package/node_modules/qs/lib/utils.js +130 -6
- package/node_modules/qs/package.json +4 -4
- package/node_modules/qs/test/parse.js +174 -0
- package/node_modules/qs/test/stringify.js +263 -33
- package/node_modules/qs/test/utils.js +184 -0
- package/package.json +1 -1
- package/dist/ui/assets/index-wtX8LuiU.js +0 -123
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
8
|
-
import { existsSync as existsSync13, readFileSync as
|
|
9
|
-
import { dirname as
|
|
10
|
-
import { homedir as
|
|
8
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16, renameSync as renameSync4 } from "fs";
|
|
9
|
+
import { dirname as dirname11, join as join16 } from "path";
|
|
10
|
+
import { homedir as homedir9 } from "os";
|
|
11
11
|
import express from "express";
|
|
12
12
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
13
13
|
|
|
@@ -643,6 +643,13 @@ async function getSkippedCommitments() {
|
|
|
643
643
|
if (!res.ok) throw new Error("Failed to fetch skipped commitments");
|
|
644
644
|
return res.json();
|
|
645
645
|
}
|
|
646
|
+
async function getMyPendingProposals() {
|
|
647
|
+
const res = await fetch(`${MCP_BASE}/internal/my-pending-proposals`, {
|
|
648
|
+
headers: internalHeaders()
|
|
649
|
+
});
|
|
650
|
+
if (!res.ok) throw new Error("Failed to fetch my pending proposals");
|
|
651
|
+
return res.json();
|
|
652
|
+
}
|
|
646
653
|
async function getLocalEvidence() {
|
|
647
654
|
const res = await fetch(`${MCP_BASE}/internal/evidence`, {
|
|
648
655
|
headers: internalHeaders()
|
|
@@ -2258,6 +2265,7 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablis
|
|
|
2258
2265
|
}
|
|
2259
2266
|
vault2.wipe();
|
|
2260
2267
|
await vault2.deriveAndSetKey(apiKey);
|
|
2268
|
+
vault2.setSpCookie(sessionCookie);
|
|
2261
2269
|
}
|
|
2262
2270
|
const sessionExpiresAt = typeof data.sessionExpiresAt === "number" ? data.sessionExpiresAt : null;
|
|
2263
2271
|
vault2.setSessionExpiresAt(sessionExpiresAt);
|
|
@@ -3376,15 +3384,15 @@ import { promisify } from "util";
|
|
|
3376
3384
|
var run = promisify(execFile);
|
|
3377
3385
|
var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
|
|
3378
3386
|
var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
|
|
3379
|
-
function findCli(
|
|
3387
|
+
function findCli(dirname12 = MODULE_DIR) {
|
|
3380
3388
|
const candidates = [
|
|
3381
3389
|
// Bundled (the shipped layout): dist/control-plane → dist → root → bin
|
|
3382
|
-
resolve(
|
|
3390
|
+
resolve(dirname12, "..", "..", "bin", "suveren-gateway.js"),
|
|
3383
3391
|
// Bundled, were the routes/ directory ever preserved.
|
|
3384
|
-
resolve(
|
|
3392
|
+
resolve(dirname12, "..", "..", "..", "bin", "suveren-gateway.js"),
|
|
3385
3393
|
// Dev, running from source: the repo's own bundle directory.
|
|
3386
|
-
resolve(
|
|
3387
|
-
resolve(
|
|
3394
|
+
resolve(dirname12, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
|
|
3395
|
+
resolve(dirname12, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
|
|
3388
3396
|
// Deliberately NO process.cwd() fallback: it makes the result depend on
|
|
3389
3397
|
// where the process happened to be launched from, so the same install
|
|
3390
3398
|
// resolves differently between a shell and a login service — and it would
|
|
@@ -4622,10 +4630,62 @@ function createArchivedMandatesRouter(dataDir) {
|
|
|
4622
4630
|
return router;
|
|
4623
4631
|
}
|
|
4624
4632
|
|
|
4625
|
-
// src/routes/
|
|
4633
|
+
// src/routes/blocked-seen.ts
|
|
4626
4634
|
import { Router as Router19 } from "express";
|
|
4635
|
+
|
|
4636
|
+
// src/lib/blocked-seen-store.ts
|
|
4637
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, renameSync as renameSync3 } from "fs";
|
|
4638
|
+
import { homedir as homedir8 } from "os";
|
|
4639
|
+
import { join as join14, dirname as dirname10 } from "path";
|
|
4640
|
+
function storePath2(dataDir) {
|
|
4641
|
+
const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join14(homedir8(), ".suveren");
|
|
4642
|
+
return join14(dir, "blocked-seen.json");
|
|
4643
|
+
}
|
|
4644
|
+
function readLastSeenBlockedAt(dataDir) {
|
|
4645
|
+
try {
|
|
4646
|
+
const raw = JSON.parse(readFileSync14(storePath2(dataDir), "utf8"));
|
|
4647
|
+
return typeof raw.lastSeenBlockedAt === "number" && Number.isFinite(raw.lastSeenBlockedAt) ? raw.lastSeenBlockedAt : 0;
|
|
4648
|
+
} catch {
|
|
4649
|
+
return 0;
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
function markBlockedSeen(ts, dataDir) {
|
|
4653
|
+
const safeTs = typeof ts === "number" && Number.isFinite(ts) ? ts : Date.now();
|
|
4654
|
+
const path = storePath2(dataDir);
|
|
4655
|
+
mkdirSync9(dirname10(path), { recursive: true });
|
|
4656
|
+
const tmp = `${path}.tmp`;
|
|
4657
|
+
writeFileSync9(tmp, JSON.stringify({ lastSeenBlockedAt: safeTs }, null, 2) + "\n", "utf8");
|
|
4658
|
+
renameSync3(tmp, path);
|
|
4659
|
+
return safeTs;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// src/routes/blocked-seen.ts
|
|
4663
|
+
function createBlockedSeenRouter(dataDir) {
|
|
4664
|
+
const router = Router19();
|
|
4665
|
+
router.get("/", (_req, res) => {
|
|
4666
|
+
res.json({ lastSeenBlockedAt: readLastSeenBlockedAt(dataDir) });
|
|
4667
|
+
});
|
|
4668
|
+
router.post("/", (req, res) => {
|
|
4669
|
+
const body = req.body;
|
|
4670
|
+
const ts = typeof body?.ts === "number" && Number.isFinite(body.ts) ? body.ts : Date.now();
|
|
4671
|
+
try {
|
|
4672
|
+
res.json({ lastSeenBlockedAt: markBlockedSeen(ts, dataDir) });
|
|
4673
|
+
} catch (err) {
|
|
4674
|
+
res.status(500).json({ error: `Could not save: ${err.message}` });
|
|
4675
|
+
}
|
|
4676
|
+
});
|
|
4677
|
+
return router;
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
// src/routes/internal-events.ts
|
|
4681
|
+
import { Router as Router20 } from "express";
|
|
4627
4682
|
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4628
|
-
var ALLOWED = [
|
|
4683
|
+
var ALLOWED = [
|
|
4684
|
+
"proposal-added",
|
|
4685
|
+
"action-approval-needed",
|
|
4686
|
+
"execution-failed",
|
|
4687
|
+
"denial-recorded"
|
|
4688
|
+
];
|
|
4629
4689
|
var SESSION_EXPIRED = "session-expired";
|
|
4630
4690
|
var AS_KEY_MISMATCH = "as-key-mismatch";
|
|
4631
4691
|
var AS_TLS_MISMATCH = "as-tls-mismatch";
|
|
@@ -4638,7 +4698,7 @@ function secretMatches2(provided, expected) {
|
|
|
4638
4698
|
return timingSafeEqual3(a, b);
|
|
4639
4699
|
}
|
|
4640
4700
|
function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch, onAsTlsMismatch) {
|
|
4641
|
-
const router =
|
|
4701
|
+
const router = Router20();
|
|
4642
4702
|
router.post("/event", (req, res) => {
|
|
4643
4703
|
if (!secretMatches2(req.headers["x-internal-secret"], getSecret())) {
|
|
4644
4704
|
res.status(401).json({ error: "Unauthorized" });
|
|
@@ -4672,7 +4732,7 @@ function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch
|
|
|
4672
4732
|
}
|
|
4673
4733
|
|
|
4674
4734
|
// src/routes/internal-mandate.ts
|
|
4675
|
-
import { Router as
|
|
4735
|
+
import { Router as Router21 } from "express";
|
|
4676
4736
|
import { timingSafeEqual as timingSafeEqual4, randomUUID } from "crypto";
|
|
4677
4737
|
|
|
4678
4738
|
// src/lib/mandate-ceremony.ts
|
|
@@ -4854,7 +4914,7 @@ function secretMatches3(provided, expected) {
|
|
|
4854
4914
|
return a.length === b.length && timingSafeEqual4(a, b);
|
|
4855
4915
|
}
|
|
4856
4916
|
function createInternalMandateRouter(getSecret, deps) {
|
|
4857
|
-
const router =
|
|
4917
|
+
const router = Router21();
|
|
4858
4918
|
router.post("/mandate", async (req, res) => {
|
|
4859
4919
|
if (!secretMatches3(req.header("X-Internal-Secret") ?? void 0, getSecret())) {
|
|
4860
4920
|
res.status(401).json({ error: "unauthorized" });
|
|
@@ -5057,17 +5117,17 @@ function buildSessionHealth(vault2) {
|
|
|
5057
5117
|
}
|
|
5058
5118
|
|
|
5059
5119
|
// src/lib/denials-reader.ts
|
|
5060
|
-
import { existsSync as existsSync12, readFileSync as
|
|
5061
|
-
import { join as
|
|
5120
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
5121
|
+
import { join as join15 } from "path";
|
|
5062
5122
|
function loadDenials(dataDir, decrypt) {
|
|
5063
|
-
const encPath =
|
|
5064
|
-
const plainPath =
|
|
5123
|
+
const encPath = join15(dataDir, "denials.enc.json");
|
|
5124
|
+
const plainPath = join15(dataDir, "denials.json");
|
|
5065
5125
|
if (existsSync12(encPath)) {
|
|
5066
|
-
const { blob } = JSON.parse(
|
|
5126
|
+
const { blob } = JSON.parse(readFileSync15(encPath, "utf-8"));
|
|
5067
5127
|
return JSON.parse(decrypt(blob)).records ?? [];
|
|
5068
5128
|
}
|
|
5069
5129
|
if (existsSync12(plainPath)) {
|
|
5070
|
-
return JSON.parse(
|
|
5130
|
+
return JSON.parse(readFileSync15(plainPath, "utf-8")).records ?? [];
|
|
5071
5131
|
}
|
|
5072
5132
|
return [];
|
|
5073
5133
|
}
|
|
@@ -5091,11 +5151,11 @@ function isSimulationMode() {
|
|
|
5091
5151
|
// src/index.ts
|
|
5092
5152
|
(function migrateDataDir() {
|
|
5093
5153
|
if (process.env.SUVEREN_DATA_DIR) return;
|
|
5094
|
-
const oldDir =
|
|
5095
|
-
const newDir =
|
|
5154
|
+
const oldDir = join16(homedir9(), ".hap");
|
|
5155
|
+
const newDir = join16(homedir9(), ".suveren");
|
|
5096
5156
|
if (!existsSync13(newDir) && existsSync13(oldDir)) {
|
|
5097
5157
|
try {
|
|
5098
|
-
|
|
5158
|
+
renameSync4(oldDir, newDir);
|
|
5099
5159
|
console.error(`[Control Plane] Migrated data directory: ${oldDir} \u2192 ${newDir}`);
|
|
5100
5160
|
} catch (err) {
|
|
5101
5161
|
console.error(`[Control Plane] FATAL: could not migrate data directory ${oldDir} \u2192 ${newDir}:`, err);
|
|
@@ -5103,7 +5163,7 @@ function isSimulationMode() {
|
|
|
5103
5163
|
}
|
|
5104
5164
|
}
|
|
5105
5165
|
})();
|
|
5106
|
-
var DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
5166
|
+
var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join16(homedir9(), ".suveren");
|
|
5107
5167
|
var SP_URL = resolveAsUrl(DATA_DIR);
|
|
5108
5168
|
validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
|
|
5109
5169
|
var POLICY_LOCKED = [...readPolicy().locked];
|
|
@@ -5111,7 +5171,7 @@ var POLICY_LOCKED_INSTALL_METHOD = readPolicy().policy.installMethod;
|
|
|
5111
5171
|
var POLICY_LOCKED_TEAM = readPolicy().policy.team;
|
|
5112
5172
|
var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
|
|
5113
5173
|
var HAP_MODE = process.env.HAP_MODE ?? "personal";
|
|
5114
|
-
var UI_DIST = process.env.HAP_UI_DIST ??
|
|
5174
|
+
var UI_DIST = process.env.HAP_UI_DIST ?? join16(import.meta.dirname ?? __dirname, "../../ui/dist");
|
|
5115
5175
|
var vault = new Vault(DATA_DIR);
|
|
5116
5176
|
var shownSnapshotStore = new ShownSnapshotStore(DATA_DIR);
|
|
5117
5177
|
(function repairIfAsUrlChanged() {
|
|
@@ -5420,6 +5480,7 @@ app.use("/mcp", jsonParser, authGuard, createMCPRouter());
|
|
|
5420
5480
|
app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
|
|
5421
5481
|
app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRouter());
|
|
5422
5482
|
app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
|
|
5483
|
+
app.use("/api/blocked-seen", jsonParser, authGuard, createBlockedSeenRouter());
|
|
5423
5484
|
app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
|
|
5424
5485
|
app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
|
|
5425
5486
|
app.use("/proposal-status", jsonParser, authGuard, createProposalStatusRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch, shownSnapshotStore));
|
|
@@ -5564,6 +5625,15 @@ app.get("/skipped-commitments", authGuard, async (_req, res) => {
|
|
|
5564
5625
|
res.status(500).json({ error: "Failed to fetch skipped commitments from MCP server" });
|
|
5565
5626
|
}
|
|
5566
5627
|
});
|
|
5628
|
+
app.get("/my-pending-proposals", authGuard, async (_req, res) => {
|
|
5629
|
+
try {
|
|
5630
|
+
const data = await getMyPendingProposals();
|
|
5631
|
+
res.json(data);
|
|
5632
|
+
} catch (err) {
|
|
5633
|
+
console.error("[Control Plane] My-pending-proposals retrieval error:", err);
|
|
5634
|
+
res.status(500).json({ error: "Failed to fetch pending proposals from MCP server" });
|
|
5635
|
+
}
|
|
5636
|
+
});
|
|
5567
5637
|
app.get("/as-pairing", authGuard, (_req, res) => {
|
|
5568
5638
|
const pairing = readPairing(DATA_DIR);
|
|
5569
5639
|
const paired = pairing !== null && pairing.asUrl === SP_URL;
|
|
@@ -5587,7 +5657,7 @@ app.get("/as-pairing", authGuard, (_req, res) => {
|
|
|
5587
5657
|
});
|
|
5588
5658
|
app.get("/denials", authGuard, (req, res) => {
|
|
5589
5659
|
try {
|
|
5590
|
-
const dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
5660
|
+
const dataDir = process.env.SUVEREN_DATA_DIR ?? join16(homedir9(), ".suveren");
|
|
5591
5661
|
const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
|
|
5592
5662
|
const since = Number(req.query.since);
|
|
5593
5663
|
const limit = Number(req.query.limit);
|
|
@@ -5646,12 +5716,12 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
|
|
|
5646
5716
|
res.status(413).json({ error: `Context exceeds ${AGENT_CONTEXT_MAX_BYTES}-byte cap` });
|
|
5647
5717
|
return;
|
|
5648
5718
|
}
|
|
5649
|
-
const { mkdirSync:
|
|
5719
|
+
const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync10, renameSync: renameSync5 } = await import("fs");
|
|
5650
5720
|
const filePath = agentBriefPath();
|
|
5651
|
-
|
|
5721
|
+
mkdirSync10(dirname11(filePath), { recursive: true });
|
|
5652
5722
|
const tmpPath = `${filePath}.tmp`;
|
|
5653
|
-
|
|
5654
|
-
|
|
5723
|
+
writeFileSync10(tmpPath, content, "utf-8");
|
|
5724
|
+
renameSync5(tmpPath, filePath);
|
|
5655
5725
|
res.json({ ok: true });
|
|
5656
5726
|
} catch (err) {
|
|
5657
5727
|
console.error("[Control Plane] agent-brief/context PUT failed:", err);
|
|
@@ -5743,10 +5813,10 @@ function detectRunningVersion() {
|
|
|
5743
5813
|
return process.env.HAP_BUILD_SHA ?? "dev";
|
|
5744
5814
|
}
|
|
5745
5815
|
const dir = import.meta.dirname ?? __dirname;
|
|
5746
|
-
const bundlePkg =
|
|
5816
|
+
const bundlePkg = join16(dir, "..", "..", "package.json");
|
|
5747
5817
|
if (existsSync13(bundlePkg)) {
|
|
5748
5818
|
try {
|
|
5749
|
-
const pkg = JSON.parse(
|
|
5819
|
+
const pkg = JSON.parse(readFileSync16(bundlePkg, "utf8"));
|
|
5750
5820
|
if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
|
|
5751
5821
|
return pkg.version;
|
|
5752
5822
|
}
|
|
@@ -5798,7 +5868,7 @@ if (existsSync13(UI_DIST)) {
|
|
|
5798
5868
|
}));
|
|
5799
5869
|
app.get("*", (_req, res) => {
|
|
5800
5870
|
res.setHeader("Cache-Control", "no-store, must-revalidate");
|
|
5801
|
-
res.sendFile(
|
|
5871
|
+
res.sendFile(join16(UI_DIST, "index.html"));
|
|
5802
5872
|
});
|
|
5803
5873
|
} else {
|
|
5804
5874
|
app.get("/", (_req, res) => {
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1071,6 +1071,25 @@ var SPClient = class {
|
|
|
1071
1071
|
if (!res.ok) return null;
|
|
1072
1072
|
return res.json();
|
|
1073
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* One proposal by id, whatever its status — `GET /api/proposals/:id`
|
|
1076
|
+
* (the AS admits the creator, an approver, or the authorization's owner;
|
|
1077
|
+
* this gateway only ever calls it for an id it itself submitted, via
|
|
1078
|
+
* proposalSubmissions, so the creator case always applies). Used by the
|
|
1079
|
+
* `/internal/my-pending-proposals` stand-in (bin/http.ts) — see that
|
|
1080
|
+
* route's doc comment for why it exists at all: the AS has no endpoint
|
|
1081
|
+
* to list "proposals I created" scoped by creator, only `?domain=`,
|
|
1082
|
+
* `?status=committed/rejected`, and `?approver=me`, none of which ever
|
|
1083
|
+
* match an above-cap proposal that has no pendingDomains unless the
|
|
1084
|
+
* caller also happens to be a pendingApprover.
|
|
1085
|
+
*/
|
|
1086
|
+
async getProposal(id) {
|
|
1087
|
+
const res = await this.fetch(`/api/proposals/${encodeURIComponent(id)}`);
|
|
1088
|
+
if (res.status === 404) return null;
|
|
1089
|
+
if (!res.ok) throw new Error(`SP proposal request failed: ${res.status}`);
|
|
1090
|
+
const data = await res.json();
|
|
1091
|
+
return data.proposal;
|
|
1092
|
+
}
|
|
1074
1093
|
/**
|
|
1075
1094
|
* Get proposals that have been fully committed and are ready for execution.
|
|
1076
1095
|
*/
|
|
@@ -1124,7 +1143,7 @@ var AsKeyMismatchError = class extends Error {
|
|
|
1124
1143
|
}
|
|
1125
1144
|
};
|
|
1126
1145
|
function isCommitmentDowngrade(auth) {
|
|
1127
|
-
const requiresReview = auth.signedCommitmentMode === "review"
|
|
1146
|
+
const requiresReview = auth.signedCommitmentMode === "review";
|
|
1128
1147
|
return requiresReview && (auth.deferredCommitmentDomains ?? []).length === 0;
|
|
1129
1148
|
}
|
|
1130
1149
|
var AttestationCache = class {
|
|
@@ -1171,23 +1190,37 @@ var AttestationCache = class {
|
|
|
1171
1190
|
*/
|
|
1172
1191
|
activeWorkspaceGroupId = null;
|
|
1173
1192
|
/**
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* the
|
|
1178
|
-
*
|
|
1179
|
-
|
|
1180
|
-
|
|
1193
|
+
* TR7 (work-plan "Added 2026-10-14"): the account's own Personal group id
|
|
1194
|
+
* — learned alongside {@link activeWorkspaceGroupId} so {@link isTeamGroup}
|
|
1195
|
+
* can tell a TEAM-owned authorization apart from a Personal one without a
|
|
1196
|
+
* live AS call on the hot read path. `null` = not yet known, same
|
|
1197
|
+
* "skip rather than guess" posture as `activeWorkspaceGroupId`.
|
|
1198
|
+
*/
|
|
1199
|
+
personalGroupId = null;
|
|
1200
|
+
/**
|
|
1201
|
+
* Re-resolve {@link activeWorkspaceGroupId} and {@link personalGroupId}
|
|
1202
|
+
* from the Authority Server. Called by the resync-gates flow (http.ts's
|
|
1203
|
+
* `/internal/resync-gates`), never on the hot tool-call path — it makes up
|
|
1204
|
+
* to two AS calls (`getActiveTeamGroupId` + `getPersonalGroupId`, the
|
|
1205
|
+
* second already needed for both values). Leaves the previous value(s) in
|
|
1206
|
+
* place on any failure (network error, unexpected shape): losing a
|
|
1207
|
+
* known-good value on a transient blip would turn this defence-in-depth
|
|
1208
|
+
* check into a false refusal of every cached mandate, which is worse than
|
|
1209
|
+
* leaving it momentarily stale.
|
|
1181
1210
|
*/
|
|
1182
1211
|
async refreshActiveWorkspace() {
|
|
1212
|
+
let activeTeam = null;
|
|
1213
|
+
try {
|
|
1214
|
+
activeTeam = await this.spClient.getActiveTeamGroupId();
|
|
1215
|
+
if (activeTeam) this.activeWorkspaceGroupId = activeTeam;
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1183
1218
|
try {
|
|
1184
|
-
const activeTeam = await this.spClient.getActiveTeamGroupId();
|
|
1185
|
-
if (activeTeam) {
|
|
1186
|
-
this.activeWorkspaceGroupId = activeTeam;
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
1219
|
const personalId = await this.spClient.getPersonalGroupId();
|
|
1190
|
-
if (personalId)
|
|
1220
|
+
if (personalId) {
|
|
1221
|
+
this.personalGroupId = personalId;
|
|
1222
|
+
if (!activeTeam) this.activeWorkspaceGroupId = personalId;
|
|
1223
|
+
}
|
|
1191
1224
|
} catch {
|
|
1192
1225
|
}
|
|
1193
1226
|
}
|
|
@@ -1200,6 +1233,19 @@ var AttestationCache = class {
|
|
|
1200
1233
|
isForeignWorkspace(groupId) {
|
|
1201
1234
|
return groupId !== void 0 && this.activeWorkspaceGroupId !== null && groupId !== this.activeWorkspaceGroupId;
|
|
1202
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* TR7 — is `groupId` a TEAM group (not the account's own Personal
|
|
1238
|
+
* workspace)? `undefined` ("don't know") when either `groupId` itself or
|
|
1239
|
+
* the account's Personal group id hasn't been learned yet — callers MUST
|
|
1240
|
+
* treat that the same as Personal (read enforcement has no Authority
|
|
1241
|
+
* Server backstop to fail closed to, unlike {@link isForeignWorkspace}'s
|
|
1242
|
+
* ticket-time safety net, so an unknown status must never be read as
|
|
1243
|
+
* "team" — that would apply a ceiling nobody has actually confirmed).
|
|
1244
|
+
*/
|
|
1245
|
+
isTeamGroup(groupId) {
|
|
1246
|
+
if (groupId === void 0 || this.personalGroupId === null) return void 0;
|
|
1247
|
+
return groupId !== this.personalGroupId;
|
|
1248
|
+
}
|
|
1203
1249
|
/**
|
|
1204
1250
|
* Get the SP public key, fetching from SP if not cached or expired, then —
|
|
1205
1251
|
* when a pin exists for this exact AS URL — verifying the live key still
|
|
@@ -1779,6 +1825,7 @@ var DenialLog = class {
|
|
|
1779
1825
|
} catch (err) {
|
|
1780
1826
|
console.error("[DenialLog] failed to record denial:", err);
|
|
1781
1827
|
}
|
|
1828
|
+
void notifyControlPlane("denial-recorded");
|
|
1782
1829
|
}
|
|
1783
1830
|
/** Recent denials, newest first. `sinceMs` and `limit` are optional filters. */
|
|
1784
1831
|
getRecent(sinceMs, limit = MAX_RECORDS) {
|
|
@@ -2374,6 +2421,18 @@ var ProposalSubmissionStore = class {
|
|
|
2374
2421
|
get(proposalId) {
|
|
2375
2422
|
return this.load().entries.find((e) => e.proposalId === proposalId);
|
|
2376
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Every submission still on record (newest-pruned by `MAX_AGE_SECONDS`
|
|
2426
|
+
* — see `load`). "Awaiting others" stand-in (2026-10-15, real-gateway
|
|
2427
|
+
* e2e): until the Authority Server has a creator-scoped proposal query,
|
|
2428
|
+
* this (every id THIS gateway has ever submitted) is the only local
|
|
2429
|
+
* source for "proposals I created" — bin/http.ts's
|
|
2430
|
+
* `/internal/my-pending-proposals` resolves each id's CURRENT state from
|
|
2431
|
+
* the AS and filters to `status: 'pending'`.
|
|
2432
|
+
*/
|
|
2433
|
+
list() {
|
|
2434
|
+
return this.load().entries;
|
|
2435
|
+
}
|
|
2377
2436
|
load() {
|
|
2378
2437
|
if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
|
|
2379
2438
|
try {
|
|
@@ -5523,10 +5582,12 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5523
5582
|
let readMaxAge = null;
|
|
5524
5583
|
if (hasAgeDimension) {
|
|
5525
5584
|
const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
|
|
5526
|
-
|
|
5585
|
+
const signedAge = ageBoundField ? maxReadAgeDays(
|
|
5527
5586
|
matchingAuths.map((a) => a.bounds ?? a.frame),
|
|
5528
5587
|
ageBoundField
|
|
5529
|
-
) : null
|
|
5588
|
+
) : null;
|
|
5589
|
+
const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(matchingAuths[0]?.groupId) : void 0;
|
|
5590
|
+
readMaxAge = isTeam === true && signedAge !== null ? localAge !== null ? Math.min(localAge, signedAge) : signedAge : localAge ?? signedAge;
|
|
5530
5591
|
if (readMaxAge === null) {
|
|
5531
5592
|
return denyRead(
|
|
5532
5593
|
state2,
|
|
@@ -5976,29 +6037,33 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
5976
6037
|
return attemptWithCandidates(remaining);
|
|
5977
6038
|
}
|
|
5978
6039
|
if (err instanceof SPReceiptError && err.statusCode === 409) {
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
}
|
|
5990
|
-
} catch {
|
|
5991
|
-
}
|
|
5992
|
-
} else {
|
|
5993
|
-
try {
|
|
5994
|
-
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
5995
|
-
if (summary?.created_by) {
|
|
5996
|
-
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
6040
|
+
const rawBody = err.body;
|
|
6041
|
+
const spBody = rawBody?.errors?.[0] ?? rawBody;
|
|
6042
|
+
let pendingApprovers = spBody?.approvers ?? [];
|
|
6043
|
+
let summary;
|
|
6044
|
+
const authSummary = async () => {
|
|
6045
|
+
if (summary === void 0) {
|
|
6046
|
+
try {
|
|
6047
|
+
summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
6048
|
+
} catch {
|
|
6049
|
+
summary = null;
|
|
5997
6050
|
}
|
|
5998
|
-
} catch {
|
|
5999
6051
|
}
|
|
6052
|
+
return summary;
|
|
6053
|
+
};
|
|
6054
|
+
if (pendingApprovers.length === 0) {
|
|
6055
|
+
const s = await authSummary();
|
|
6056
|
+
if (s?.approvers_frozen) pendingApprovers = s.approvers_frozen;
|
|
6057
|
+
}
|
|
6058
|
+
const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(auth.groupId) : void 0;
|
|
6059
|
+
const ownerId = (await authSummary())?.created_by;
|
|
6060
|
+
if (isTeam === true) {
|
|
6061
|
+
if (ownerId) pendingApprovers = pendingApprovers.filter((uid) => uid !== ownerId);
|
|
6062
|
+
} else if (ownerId) {
|
|
6063
|
+
pendingApprovers = [ownerId, ...pendingApprovers];
|
|
6000
6064
|
}
|
|
6001
6065
|
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
6066
|
+
const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
|
|
6002
6067
|
try {
|
|
6003
6068
|
const enrichedArgs = await attachImagePreview(args);
|
|
6004
6069
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
@@ -6011,7 +6076,8 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6011
6076
|
toolArgs: enrichedArgs,
|
|
6012
6077
|
executionContext: wireExecCtx,
|
|
6013
6078
|
pendingApprovers: uniqueApprovers,
|
|
6014
|
-
recipientUserIds
|
|
6079
|
+
recipientUserIds,
|
|
6080
|
+
...ownerId ? { createdBy: ownerId } : {}
|
|
6015
6081
|
});
|
|
6016
6082
|
const previewHash = await computeSubmissionPreviewHash(
|
|
6017
6083
|
integrationManager2,
|
|
@@ -10347,6 +10413,24 @@ app.get("/internal/skipped-commitments", internalOnly, async (_req, res) => {
|
|
|
10347
10413
|
res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
|
|
10348
10414
|
}
|
|
10349
10415
|
});
|
|
10416
|
+
app.get("/internal/my-pending-proposals", internalOnly, async (_req, res) => {
|
|
10417
|
+
try {
|
|
10418
|
+
const submitted = state.proposalSubmissions.list();
|
|
10419
|
+
const resolved = await Promise.all(
|
|
10420
|
+
submitted.map(async (s) => {
|
|
10421
|
+
try {
|
|
10422
|
+
return await state.spClient.getProposal(s.proposalId);
|
|
10423
|
+
} catch {
|
|
10424
|
+
return null;
|
|
10425
|
+
}
|
|
10426
|
+
})
|
|
10427
|
+
);
|
|
10428
|
+
const pending = resolved.filter((p) => p !== null && p.status === "pending");
|
|
10429
|
+
res.json({ proposals: pending });
|
|
10430
|
+
} catch (err) {
|
|
10431
|
+
res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
|
|
10432
|
+
}
|
|
10433
|
+
});
|
|
10350
10434
|
app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
10351
10435
|
if (state.receiptArchive.isLocked()) {
|
|
10352
10436
|
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|