@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.
- package/content/integrations/calendar.json +3 -0
- package/content/integrations/crm.json +10 -0
- package/content/integrations/deploy-github.json +1 -0
- package/content/integrations/erp.json +4 -0
- package/content/integrations/gmail.json +4 -0
- package/content/integrations/linkedin.json +4 -0
- package/content/integrations/mail.json +1 -0
- package/content/integrations/mollie.json +5 -0
- package/content/integrations/records.json +4 -0
- package/dist/control-plane/index.mjs +151 -32
- package/dist/mcp-server/http.mjs +156 -37
- package/dist/ui/assets/index-B7qy0B9J.js +123 -0
- package/dist/ui/assets/index-DyZY5sLI.css +1 -0
- 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/node_modules/undici/docs/docs/api/Client.md +2 -0
- package/node_modules/undici/lib/core/connect.js +10 -4
- package/node_modules/undici/lib/core/symbols.js +1 -0
- package/node_modules/undici/lib/dispatcher/agent.js +6 -2
- package/node_modules/undici/lib/dispatcher/client-h1.js +0 -2
- package/node_modules/undici/lib/dispatcher/client-h2.js +0 -4
- package/node_modules/undici/lib/dispatcher/client.js +3 -2
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +12 -0
- package/node_modules/undici/lib/handler/cache-handler.js +48 -0
- package/node_modules/undici/lib/handler/deduplication-handler.js +57 -13
- package/node_modules/undici/lib/handler/redirect-handler.js +8 -1
- package/node_modules/undici/lib/interceptor/decompress.js +15 -5
- package/node_modules/undici/lib/web/cookies/index.js +3 -1
- package/node_modules/undici/lib/web/cookies/parse.js +138 -137
- package/node_modules/undici/lib/web/cookies/util.js +21 -2
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +90 -9
- package/node_modules/undici/lib/web/eventsource/eventsource.js +4 -1
- package/node_modules/undici/lib/web/fetch/index.js +3 -3
- package/node_modules/undici/lib/web/infra/index.js +14 -0
- package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +5 -5
- package/node_modules/undici/lib/web/websocket/receiver.js +13 -0
- package/node_modules/undici/package.json +1 -1
- package/node_modules/undici/types/client.d.ts +14 -0
- package/package.json +1 -1
- package/dist/ui/assets/index-DUIlMVlP.css +0 -1
- package/dist/ui/assets/index-wtX8LuiU.js +0 -123
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 {
|
|
@@ -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
|
-
|
|
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
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
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
|
|
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." });
|