@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.
@@ -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 readFileSync15, renameSync as renameSync3 } from "fs";
9
- import { dirname as dirname10, join as join15 } from "path";
10
- import { homedir as homedir8 } from "os";
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(dirname11 = MODULE_DIR) {
3387
+ function findCli(dirname12 = MODULE_DIR) {
3380
3388
  const candidates = [
3381
3389
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
3382
- resolve(dirname11, "..", "..", "bin", "suveren-gateway.js"),
3390
+ resolve(dirname12, "..", "..", "bin", "suveren-gateway.js"),
3383
3391
  // Bundled, were the routes/ directory ever preserved.
3384
- resolve(dirname11, "..", "..", "..", "bin", "suveren-gateway.js"),
3392
+ resolve(dirname12, "..", "..", "..", "bin", "suveren-gateway.js"),
3385
3393
  // Dev, running from source: the repo's own bundle directory.
3386
- resolve(dirname11, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
3387
- resolve(dirname11, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
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/internal-events.ts
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 = ["proposal-added", "action-approval-needed", "execution-failed"];
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 = Router19();
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 Router20 } from "express";
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 = Router20();
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 readFileSync14 } from "fs";
5061
- import { join as join14 } from "path";
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 = join14(dataDir, "denials.enc.json");
5064
- const plainPath = join14(dataDir, "denials.json");
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(readFileSync14(encPath, "utf-8"));
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(readFileSync14(plainPath, "utf-8")).records ?? [];
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 = join15(homedir8(), ".hap");
5095
- const newDir = join15(homedir8(), ".suveren");
5154
+ const oldDir = join16(homedir9(), ".hap");
5155
+ const newDir = join16(homedir9(), ".suveren");
5096
5156
  if (!existsSync13(newDir) && existsSync13(oldDir)) {
5097
5157
  try {
5098
- renameSync3(oldDir, newDir);
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 ?? join15(homedir8(), ".suveren");
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 ?? join15(import.meta.dirname ?? __dirname, "../../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 ?? join15(homedir8(), ".suveren");
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: mkdirSync9, writeFileSync: writeFileSync9, renameSync: renameSync4 } = await import("fs");
5719
+ const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync10, renameSync: renameSync5 } = await import("fs");
5650
5720
  const filePath = agentBriefPath();
5651
- mkdirSync9(dirname10(filePath), { recursive: true });
5721
+ mkdirSync10(dirname11(filePath), { recursive: true });
5652
5722
  const tmpPath = `${filePath}.tmp`;
5653
- writeFileSync9(tmpPath, content, "utf-8");
5654
- renameSync4(tmpPath, filePath);
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 = join15(dir, "..", "..", "package.json");
5816
+ const bundlePkg = join16(dir, "..", "..", "package.json");
5747
5817
  if (existsSync13(bundlePkg)) {
5748
5818
  try {
5749
- const pkg = JSON.parse(readFileSync15(bundlePkg, "utf8"));
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(join15(UI_DIST, "index.html"));
5871
+ res.sendFile(join16(UI_DIST, "index.html"));
5802
5872
  });
5803
5873
  } else {
5804
5874
  app.get("/", (_req, res) => {
@@ -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" || auth.signedCommitmentMode === "review_above_cap";
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
- * Re-resolve {@link activeWorkspaceGroupId} from the Authority Server.
1175
- * Called by the resync-gates flow (http.ts's `/internal/resync-gates`),
1176
- * never on the hot tool-call path — it makes up to two AS calls. Leaves
1177
- * the previous value in place on any failure (network error, unexpected
1178
- * shape): losing a known-good value on a transient blip would turn this
1179
- * defence-in-depth check into a false refusal of every cached mandate,
1180
- * which is worse than leaving it momentarily stale.
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) this.activeWorkspaceGroupId = 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
- readMaxAge = localAge ?? (ageBoundField ? maxReadAgeDays(
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 spBody = err.body;
5980
- let pendingApprovers = spBody.approvers ?? [];
5981
- if (pendingApprovers.length === 0) {
5982
- try {
5983
- const summary = await state2.spClient.getAuthorizationSummary(authzId);
5984
- if (summary?.approvers_frozen) {
5985
- pendingApprovers = summary.approvers_frozen;
5986
- }
5987
- if (summary?.created_by) {
5988
- pendingApprovers = [summary.created_by, ...pendingApprovers];
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: uniqueApprovers
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." });