@suveren/gateway 0.29.1 → 0.31.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.
Files changed (51) hide show
  1. package/content/integrations/calendar.json +3 -0
  2. package/content/integrations/crm.json +10 -0
  3. package/content/integrations/deploy-github.json +1 -0
  4. package/content/integrations/erp.json +4 -0
  5. package/content/integrations/gmail.json +4 -0
  6. package/content/integrations/linkedin.json +4 -0
  7. package/content/integrations/mail.json +1 -0
  8. package/content/integrations/mollie.json +5 -0
  9. package/content/integrations/records.json +4 -0
  10. package/dist/control-plane/index.mjs +151 -32
  11. package/dist/mcp-server/http.mjs +156 -37
  12. package/dist/ui/assets/index-B7qy0B9J.js +123 -0
  13. package/dist/ui/assets/index-DyZY5sLI.css +1 -0
  14. package/dist/ui/index.html +2 -2
  15. package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
  16. package/node_modules/qs/CHANGELOG.md +38 -14
  17. package/node_modules/qs/README.md +31 -3
  18. package/node_modules/qs/dist/qs.js +20 -20
  19. package/node_modules/qs/lib/parse.js +94 -4
  20. package/node_modules/qs/lib/stringify.js +4 -4
  21. package/node_modules/qs/lib/utils.js +130 -6
  22. package/node_modules/qs/package.json +4 -4
  23. package/node_modules/qs/test/parse.js +174 -0
  24. package/node_modules/qs/test/stringify.js +263 -33
  25. package/node_modules/qs/test/utils.js +184 -0
  26. package/node_modules/undici/docs/docs/api/Client.md +2 -0
  27. package/node_modules/undici/lib/core/connect.js +10 -4
  28. package/node_modules/undici/lib/core/symbols.js +1 -0
  29. package/node_modules/undici/lib/dispatcher/agent.js +6 -2
  30. package/node_modules/undici/lib/dispatcher/client-h1.js +0 -2
  31. package/node_modules/undici/lib/dispatcher/client-h2.js +0 -4
  32. package/node_modules/undici/lib/dispatcher/client.js +3 -2
  33. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +12 -0
  34. package/node_modules/undici/lib/handler/cache-handler.js +48 -0
  35. package/node_modules/undici/lib/handler/deduplication-handler.js +57 -13
  36. package/node_modules/undici/lib/handler/redirect-handler.js +8 -1
  37. package/node_modules/undici/lib/interceptor/decompress.js +15 -5
  38. package/node_modules/undici/lib/web/cookies/index.js +3 -1
  39. package/node_modules/undici/lib/web/cookies/parse.js +138 -137
  40. package/node_modules/undici/lib/web/cookies/util.js +21 -2
  41. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +90 -9
  42. package/node_modules/undici/lib/web/eventsource/eventsource.js +4 -1
  43. package/node_modules/undici/lib/web/fetch/index.js +3 -3
  44. package/node_modules/undici/lib/web/infra/index.js +14 -0
  45. package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +5 -5
  46. package/node_modules/undici/lib/web/websocket/receiver.js +13 -0
  47. package/node_modules/undici/package.json +1 -1
  48. package/node_modules/undici/types/client.d.ts +14 -0
  49. package/package.json +1 -1
  50. package/dist/ui/assets/index-DUIlMVlP.css +0 -1
  51. package/dist/ui/assets/index-wtX8LuiU.js +0 -123
@@ -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 {
@@ -3289,6 +3348,7 @@ var SharedState = class {
3289
3348
  } catch (err) {
3290
3349
  throw new ArchiveWriteError(`Receipt archive write failed: ${err instanceof Error ? err.message : String(err)}`);
3291
3350
  }
3351
+ void notifyControlPlane("ticket-recorded");
3292
3352
  }
3293
3353
  setGateContent(path, authorizationId, profileId, content, opts) {
3294
3354
  this.gateStore.set(authorizationId, {
@@ -3961,10 +4021,44 @@ function invalidDisplayHintsReason(toolName, entry) {
3961
4021
  }
3962
4022
  return null;
3963
4023
  }
4024
+ var SUMMARY_FORMATS = ["money", "percent", "list", "short", "date"];
4025
+ function invalidSummaryReason(toolName, entry) {
4026
+ if (entry === null || typeof entry !== "object") return null;
4027
+ const summary = entry.summary;
4028
+ if (summary === void 0) return null;
4029
+ const at = `override "${toolName}".summary`;
4030
+ if (typeof summary !== "string" || summary.trim().length === 0) return `${at} must be a non-empty string`;
4031
+ if (summary.length > 200) return `${at} must be at most 200 characters`;
4032
+ const balanced = (open, close) => {
4033
+ let d = 0;
4034
+ for (const ch of summary) {
4035
+ if (ch === open) {
4036
+ d += 1;
4037
+ if (d > 1) return false;
4038
+ }
4039
+ if (ch === close) {
4040
+ d -= 1;
4041
+ if (d < 0) return false;
4042
+ }
4043
+ }
4044
+ return d === 0;
4045
+ };
4046
+ if (!balanced("{", "}")) return `${at} has unbalanced or nested { }`;
4047
+ if (!balanced("[", "]")) return `${at} has unbalanced or nested [ ]`;
4048
+ for (const m of summary.matchAll(/\{([^{}]+)\}/g)) {
4049
+ const [paths, fmt, extra] = m[1].split("|").map((x) => x.trim());
4050
+ if (extra !== void 0) return `${at} {${m[1]}} has more than one format`;
4051
+ if (!paths.split(",").every((p) => /^[A-Za-z_][\w-]*(\.[A-Za-z_][\w-]*)*$/.test(p.trim()))) {
4052
+ return `${at} {${m[1]}} has an invalid field path`;
4053
+ }
4054
+ if (fmt !== void 0 && !SUMMARY_FORMATS.includes(fmt)) return `${at} {${m[1]}} has unknown format "${fmt}"`;
4055
+ }
4056
+ return null;
4057
+ }
3964
4058
  function invalidManifestPreviewReason(manifest) {
3965
4059
  const overrides = manifest.toolGating?.overrides ?? {};
3966
4060
  for (const [toolName, entry] of Object.entries(overrides)) {
3967
- const reason = invalidPreviewReason(toolName, entry) ?? invalidDisplayHintsReason(toolName, entry);
4061
+ const reason = invalidPreviewReason(toolName, entry) ?? invalidDisplayHintsReason(toolName, entry) ?? invalidSummaryReason(toolName, entry);
3968
4062
  if (reason) return reason;
3969
4063
  }
3970
4064
  return null;
@@ -5523,10 +5617,12 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
5523
5617
  let readMaxAge = null;
5524
5618
  if (hasAgeDimension) {
5525
5619
  const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
5526
- readMaxAge = localAge ?? (ageBoundField ? maxReadAgeDays(
5620
+ const signedAge = ageBoundField ? maxReadAgeDays(
5527
5621
  matchingAuths.map((a) => a.bounds ?? a.frame),
5528
5622
  ageBoundField
5529
- ) : null);
5623
+ ) : null;
5624
+ const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(matchingAuths[0]?.groupId) : void 0;
5625
+ readMaxAge = isTeam === true && signedAge !== null ? localAge !== null ? Math.min(localAge, signedAge) : signedAge : localAge ?? signedAge;
5530
5626
  if (readMaxAge === null) {
5531
5627
  return denyRead(
5532
5628
  state2,
@@ -5976,29 +6072,33 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
5976
6072
  return attemptWithCandidates(remaining);
5977
6073
  }
5978
6074
  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;
6075
+ const rawBody = err.body;
6076
+ const spBody = rawBody?.errors?.[0] ?? rawBody;
6077
+ let pendingApprovers = spBody?.approvers ?? [];
6078
+ let summary;
6079
+ const authSummary = async () => {
6080
+ if (summary === void 0) {
6081
+ try {
6082
+ summary = await state2.spClient.getAuthorizationSummary(authzId);
6083
+ } catch {
6084
+ summary = null;
5986
6085
  }
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];
5997
- }
5998
- } catch {
5999
6086
  }
6087
+ return summary;
6088
+ };
6089
+ if (pendingApprovers.length === 0) {
6090
+ const s = await authSummary();
6091
+ if (s?.approvers_frozen) pendingApprovers = s.approvers_frozen;
6092
+ }
6093
+ const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(auth.groupId) : void 0;
6094
+ const ownerId = (await authSummary())?.created_by;
6095
+ if (isTeam === true) {
6096
+ if (ownerId) pendingApprovers = pendingApprovers.filter((uid) => uid !== ownerId);
6097
+ } else if (ownerId) {
6098
+ pendingApprovers = [ownerId, ...pendingApprovers];
6000
6099
  }
6001
6100
  const uniqueApprovers = [...new Set(pendingApprovers)];
6101
+ const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
6002
6102
  try {
6003
6103
  const enrichedArgs = await attachImagePreview(args);
6004
6104
  const wireExecCtx = wireExecutionContext(auth.profileId, execution);
@@ -6011,7 +6111,8 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
6011
6111
  toolArgs: enrichedArgs,
6012
6112
  executionContext: wireExecCtx,
6013
6113
  pendingApprovers: uniqueApprovers,
6014
- recipientUserIds: uniqueApprovers
6114
+ recipientUserIds,
6115
+ ...ownerId ? { createdBy: ownerId } : {}
6015
6116
  });
6016
6117
  const previewHash = await computeSubmissionPreviewHash(
6017
6118
  integrationManager2,
@@ -10347,6 +10448,24 @@ app.get("/internal/skipped-commitments", internalOnly, async (_req, res) => {
10347
10448
  res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
10348
10449
  }
10349
10450
  });
10451
+ app.get("/internal/my-pending-proposals", internalOnly, async (_req, res) => {
10452
+ try {
10453
+ const submitted = state.proposalSubmissions.list();
10454
+ const resolved = await Promise.all(
10455
+ submitted.map(async (s) => {
10456
+ try {
10457
+ return await state.spClient.getProposal(s.proposalId);
10458
+ } catch {
10459
+ return null;
10460
+ }
10461
+ })
10462
+ );
10463
+ const pending = resolved.filter((p) => p !== null && p.status === "pending");
10464
+ res.json({ proposals: pending });
10465
+ } catch (err) {
10466
+ res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
10467
+ }
10468
+ });
10350
10469
  app.get("/internal/evidence", internalOnly, (_req, res) => {
10351
10470
  if (state.receiptArchive.isLocked()) {
10352
10471
  res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });