@opengeni/db 6.0.1 → 6.0.3-canary.36199476632001
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/browser-sessions.d.ts +42 -1
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-reconciliation.js +2 -2
- package/dist/{chunk-PGIFHEBA.js → chunk-2ECQTYFE.js} +2 -2
- package/dist/{chunk-GSB4ISHE.js → chunk-6PRATKUO.js} +7 -1
- package/dist/chunk-6PRATKUO.js.map +1 -0
- package/dist/{chunk-I7HQL6FK.js → chunk-6TNYHO5J.js} +3 -3
- package/dist/{chunk-CSVHCF3R.js → chunk-BUU6E72R.js} +11 -4
- package/dist/{chunk-CSVHCF3R.js.map → chunk-BUU6E72R.js.map} +1 -1
- package/dist/{chunk-6ORJPCYW.js → chunk-EVLC5YYT.js} +2 -2
- package/dist/{chunk-LO6BVXXH.js → chunk-GOS25XY6.js} +2 -2
- package/dist/{chunk-4CCFFLDZ.js → chunk-JEKW3C74.js} +13 -2
- package/dist/chunk-JEKW3C74.js.map +1 -0
- package/dist/{chunk-MAMBI2HR.js → chunk-JPYN5XKV.js} +39 -3
- package/dist/chunk-JPYN5XKV.js.map +1 -0
- package/dist/{chunk-RXH5CFG3.js → chunk-NO53BF7E.js} +12 -8
- package/dist/{chunk-RXH5CFG3.js.map → chunk-NO53BF7E.js.map} +1 -1
- package/dist/chunk-OYD7FLKH.js +796 -0
- package/dist/chunk-OYD7FLKH.js.map +1 -0
- package/dist/{chunk-JILAWE26.js → chunk-PIVVJPQ3.js} +2 -2
- package/dist/{chunk-FULLE5CF.js → chunk-RUEJOEIC.js} +3 -3
- package/dist/{chunk-H7BC444C.js → chunk-SRAMNVHT.js} +17 -1
- package/dist/chunk-SRAMNVHT.js.map +1 -0
- package/dist/{chunk-4RDUZ5UV.js → chunk-VNNQAS7Y.js} +2 -2
- package/dist/{chunk-XK6HXON2.js → chunk-YDBISTFL.js} +2 -2
- package/dist/{chunk-LSLNMENV.js → chunk-YKSJABR2.js} +4 -4
- package/dist/code-search-policy.d.ts +3 -0
- package/dist/connect-attempts.js +3 -3
- package/dist/connection-token-resolver.d.ts +4 -1
- package/dist/database.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +81 -21
- package/dist/index.js +2052 -2292
- package/dist/index.js.map +1 -1
- package/dist/knowledge-entries.d.ts +2 -0
- package/dist/knowledge-indexing.d.ts +17 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +1 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.d.ts +10 -1
- package/dist/migrate.js +3 -1
- package/dist/new-session-drafts.d.ts +8 -0
- package/dist/organization-integration-policy.js +3 -3
- package/dist/organization-membership-lifecycle.d.ts +1 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +4 -2
- package/dist/retained-provider-commands.js +6 -6
- package/dist/runtime-posture.d.ts +1 -1
- package/dist/schema.d.ts +44 -10
- package/dist/schema.js +1 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +5 -5
- package/dist/session-event-slices.js +3 -3
- package/dist/session-mcp-credential-rotation.d.ts +15 -4
- package/dist/session-mcp-credential-rotation.js +38 -7
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +4 -4
- package/dist/xai-subscription.d.ts +9 -0
- package/drizzle/0508_deadline_command_workspace_capture.sql +75 -0
- package/drizzle/0509_verified_signup_trial_credits.sql +76 -0
- package/drizzle/0510_knowledge_index_funding_wait.sql +174 -0
- package/drizzle/0511_knowledge_visible_index_status.sql +56 -0
- package/drizzle/0512_insights_fact_reads_hash_join.sql +31 -0
- package/drizzle/0513_message_fork_prefix_compaction.sql +85 -0
- package/drizzle/0514_failed_session_variable_set_attach.sql +25 -0
- package/drizzle/0515_autonomous_learning_defaults.sql +35 -0
- package/drizzle/0516_member_connection_read.sql +105 -0
- package/drizzle/0517_member_connection_read_backfill_index.sql +21 -0
- package/drizzle/0518_member_connection_read_backfill.sql +37 -0
- package/drizzle/0519_session_recovery_backlog_excludes_paused.sql +167 -0
- package/drizzle/0520_session_code_search_frozen.sql +6 -0
- package/package.json +6 -6
- package/src/browser-sessions.ts +161 -6
- package/src/code-search-policy.ts +25 -0
- package/src/connection-token-resolver.ts +16 -1
- package/src/database.ts +18 -0
- package/src/index.ts +759 -257
- package/src/insights.ts +44 -36
- package/src/knowledge-entries.ts +43 -2
- package/src/knowledge-indexing.ts +79 -1
- package/src/managed-user-setup.ts +18 -1
- package/src/migrate.ts +58 -1
- package/src/new-session-drafts.ts +25 -1
- package/src/organization-membership-lifecycle.ts +11 -0
- package/src/provision-roles.ts +12 -0
- package/src/retained-provider-commands.ts +16 -6
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +6 -0
- package/src/session-mcp-credential-rotation.ts +57 -9
- package/src/xai-subscription.ts +14 -1
- package/dist/chunk-4CCFFLDZ.js.map +0 -1
- package/dist/chunk-GSB4ISHE.js.map +0 -1
- package/dist/chunk-H7BC444C.js.map +0 -1
- package/dist/chunk-MAMBI2HR.js.map +0 -1
- /package/dist/{chunk-PGIFHEBA.js.map → chunk-2ECQTYFE.js.map} +0 -0
- /package/dist/{chunk-I7HQL6FK.js.map → chunk-6TNYHO5J.js.map} +0 -0
- /package/dist/{chunk-6ORJPCYW.js.map → chunk-EVLC5YYT.js.map} +0 -0
- /package/dist/{chunk-LO6BVXXH.js.map → chunk-GOS25XY6.js.map} +0 -0
- /package/dist/{chunk-JILAWE26.js.map → chunk-PIVVJPQ3.js.map} +0 -0
- /package/dist/{chunk-FULLE5CF.js.map → chunk-RUEJOEIC.js.map} +0 -0
- /package/dist/{chunk-4RDUZ5UV.js.map → chunk-VNNQAS7Y.js.map} +0 -0
- /package/dist/{chunk-XK6HXON2.js.map → chunk-YDBISTFL.js.map} +0 -0
- /package/dist/{chunk-LSLNMENV.js.map → chunk-YKSJABR2.js.map} +0 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setSubjectRlsContext,
|
|
3
|
+
withRlsContext
|
|
4
|
+
} from "./chunk-JEKW3C74.js";
|
|
5
|
+
|
|
6
|
+
// src/connection-token-resolver.ts
|
|
7
|
+
import {
|
|
8
|
+
PERSONAL_GITHUB_REQUESTED_SCOPES,
|
|
9
|
+
PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
10
|
+
PERSONAL_GITHUB_TOKEN_URL,
|
|
11
|
+
PersonalGitHubConnectionMetadata
|
|
12
|
+
} from "@opengeni/contracts/personal-github";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import {
|
|
15
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
16
|
+
pinnedFetch,
|
|
17
|
+
readResponseJsonBounded,
|
|
18
|
+
undiciFetch,
|
|
19
|
+
validateHttpUrl
|
|
20
|
+
} from "@opengeni/network";
|
|
21
|
+
import { isPrivateAddress } from "@opengeni/network";
|
|
22
|
+
import { Buffer } from "buffer";
|
|
23
|
+
|
|
24
|
+
// src/connection-refresh-lock.ts
|
|
25
|
+
import { sql } from "drizzle-orm";
|
|
26
|
+
async function withConnectionRefreshLock(db, credential, work) {
|
|
27
|
+
return withRlsContext(
|
|
28
|
+
db,
|
|
29
|
+
{ accountId: credential.accountId, workspaceId: credential.workspaceId },
|
|
30
|
+
async (tx) => {
|
|
31
|
+
if (credential.subjectId) await setSubjectRlsContext(tx, credential.subjectId);
|
|
32
|
+
await tx.execute(
|
|
33
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`connection-refresh:${credential.id}`}, 0))`
|
|
34
|
+
);
|
|
35
|
+
return work(tx);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/connection-scopes.ts
|
|
41
|
+
function connectionScopeKey(providerDomain, value) {
|
|
42
|
+
const scope = value.trim();
|
|
43
|
+
const domain = providerDomain.toLowerCase();
|
|
44
|
+
if (domain === "googleapis.com" || domain.endsWith(".googleapis.com")) {
|
|
45
|
+
if (scope === "email" || scope === "https://www.googleapis.com/auth/userinfo.email") {
|
|
46
|
+
return "google:email";
|
|
47
|
+
}
|
|
48
|
+
if (scope === "profile" || scope === "https://www.googleapis.com/auth/userinfo.profile") {
|
|
49
|
+
return "google:profile";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return domain === "graph.microsoft.com" ? scope.toLowerCase() : scope;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/connection-token-resolver.ts
|
|
56
|
+
var MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
57
|
+
var MAX_CREDENTIAL_NAME_BYTES = 256;
|
|
58
|
+
var MAX_CREDENTIAL_VALUE_BYTES = 16384;
|
|
59
|
+
var HEADER_NAME_PATTERN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
60
|
+
var QUERY_NAME_PATTERN = /^[A-Za-z0-9._~-]+$/;
|
|
61
|
+
var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
62
|
+
var FORBIDDEN_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
|
|
63
|
+
"connection",
|
|
64
|
+
"content-length",
|
|
65
|
+
"host",
|
|
66
|
+
"proxy-authorization",
|
|
67
|
+
"proxy-connection",
|
|
68
|
+
"te",
|
|
69
|
+
"trailer",
|
|
70
|
+
"transfer-encoding",
|
|
71
|
+
"upgrade"
|
|
72
|
+
]);
|
|
73
|
+
var OFFICIAL_GMAIL_MCP_RESOURCE = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
74
|
+
var OFFICIAL_GMAIL_REST_HOST = "gmail.googleapis.com";
|
|
75
|
+
function isOfficialGmailRestDestination(destinationUrl, ref) {
|
|
76
|
+
if (ref.providerDomain.toLowerCase() !== "gmailmcp.googleapis.com" || ref.kind !== "oauth2" || ref.subjectScope !== "subject") {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const destination = new URL(destinationUrl);
|
|
80
|
+
return destination.protocol === "https:" && destination.hostname.toLowerCase() === OFFICIAL_GMAIL_REST_HOST && (destination.pathname === "/gmail/v1/users/me" || destination.pathname.startsWith("/gmail/v1/users/me/"));
|
|
81
|
+
}
|
|
82
|
+
function copySelectedResources(resources) {
|
|
83
|
+
if (resources.length === 0 || resources.length > 256) {
|
|
84
|
+
throw new Error("connection credential returned an invalid selected resource count");
|
|
85
|
+
}
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
return resources.map((resource) => {
|
|
88
|
+
if (resource.kind !== "repository" || typeof resource.id !== "string" || resource.id.length === 0 || resource.id.length > 512) {
|
|
89
|
+
throw new Error("connection credential returned an invalid selected resource");
|
|
90
|
+
}
|
|
91
|
+
const key = `${resource.kind}\0${resource.id}`;
|
|
92
|
+
if (seen.has(key)) {
|
|
93
|
+
throw new Error("connection credential returned duplicate selected resources");
|
|
94
|
+
}
|
|
95
|
+
seen.add(key);
|
|
96
|
+
return { kind: resource.kind, id: resource.id };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function normalizedCredentialHeaders(headers, allowEmpty = false) {
|
|
100
|
+
const entries = Object.entries(headers);
|
|
101
|
+
if (!allowEmpty && entries.length === 0 || entries.length > 32) {
|
|
102
|
+
throw new Error("connection credential returned an invalid header count");
|
|
103
|
+
}
|
|
104
|
+
const normalized = {};
|
|
105
|
+
for (const [name, value] of entries) {
|
|
106
|
+
if (name.length > 256 || !HEADER_NAME_PATTERN.test(name) || value.length === 0 || value.length > 16384 || /[\r\n\0]/.test(value) || FORBIDDEN_CREDENTIAL_HEADERS.has(name.toLowerCase()) || name.toLowerCase().startsWith("sec-")) {
|
|
107
|
+
throw new Error("connection credential returned an invalid header");
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(normalized).some((existing) => existing.toLowerCase() === name.toLowerCase())) {
|
|
110
|
+
throw new Error("connection credential returned duplicate headers");
|
|
111
|
+
}
|
|
112
|
+
normalized[name] = value;
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
function normalizedCredentialPlacements(value) {
|
|
117
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_CREDENTIAL_PLACEMENTS) {
|
|
118
|
+
throw new Error("connection credential returned an invalid placement count");
|
|
119
|
+
}
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
return value.map((raw) => {
|
|
122
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
123
|
+
throw new Error("connection credential returned an invalid placement");
|
|
124
|
+
}
|
|
125
|
+
const record = raw;
|
|
126
|
+
if (Object.keys(record).some(
|
|
127
|
+
(key2) => key2 !== "carrier" && key2 !== "name" && key2 !== "value" && key2 !== "prefix"
|
|
128
|
+
)) {
|
|
129
|
+
throw new Error("connection credential returned an invalid placement");
|
|
130
|
+
}
|
|
131
|
+
const carrier = record.carrier;
|
|
132
|
+
const name = record.name;
|
|
133
|
+
const credentialValue = record.value;
|
|
134
|
+
const prefix = record.prefix;
|
|
135
|
+
if (carrier !== "header" && carrier !== "query" && carrier !== "cookie" || typeof name !== "string" || name.length === 0 || Buffer.byteLength(name) > MAX_CREDENTIAL_NAME_BYTES || typeof credentialValue !== "string" || credentialValue.length === 0 || typeof prefix !== "undefined" && typeof prefix !== "string" || /[\r\n\0]/.test(name) || /[\r\n\0]/.test(credentialValue) || typeof prefix === "string" && /[\r\n\0]/.test(prefix) || Buffer.byteLength(`${typeof prefix === "string" ? prefix : ""}${credentialValue}`) > MAX_CREDENTIAL_VALUE_BYTES) {
|
|
136
|
+
throw new Error("connection credential returned an invalid placement");
|
|
137
|
+
}
|
|
138
|
+
const normalizedName = carrier === "header" ? name.toLowerCase() : name;
|
|
139
|
+
if (carrier === "header" && (!HEADER_NAME_PATTERN.test(name) || FORBIDDEN_CREDENTIAL_HEADERS.has(normalizedName) || normalizedName.startsWith("sec-")) || carrier === "query" && !QUERY_NAME_PATTERN.test(name) || carrier === "cookie" && (!COOKIE_NAME_PATTERN.test(name) || /;/.test(`${prefix ?? ""}${credentialValue}`))) {
|
|
140
|
+
throw new Error("connection credential returned an invalid placement");
|
|
141
|
+
}
|
|
142
|
+
const key = `${carrier}\0${normalizedName}`;
|
|
143
|
+
if (seen.has(key)) {
|
|
144
|
+
throw new Error("connection credential returned duplicate placements");
|
|
145
|
+
}
|
|
146
|
+
seen.add(key);
|
|
147
|
+
return {
|
|
148
|
+
carrier,
|
|
149
|
+
name,
|
|
150
|
+
value: credentialValue,
|
|
151
|
+
...typeof prefix === "string" && prefix.length > 0 ? { prefix } : {}
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function headerMapForPlacements(placements) {
|
|
156
|
+
const headers = {};
|
|
157
|
+
for (const placement of placements) {
|
|
158
|
+
if (placement.carrier === "header") {
|
|
159
|
+
headers[placement.name] = `${placement.prefix ?? ""}${placement.value}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return headers;
|
|
163
|
+
}
|
|
164
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
165
|
+
var REFRESH_WINDOW_MS = 6e4;
|
|
166
|
+
var CONNECTION_REFRESH_TIMEOUT_MS = 1e4;
|
|
167
|
+
function buildConnectionTokenResolver(db, settings, deps, options = {}) {
|
|
168
|
+
const load = async (input, lookupDb = db) => {
|
|
169
|
+
const subjectOwned = input.connectionRef.subjectScope === "subject";
|
|
170
|
+
if (subjectOwned && !input.subjectId) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const request = {
|
|
174
|
+
workspaceId: input.workspaceId,
|
|
175
|
+
providerDomain: input.connectionRef.providerDomain,
|
|
176
|
+
allowSubjectOwned: subjectOwned,
|
|
177
|
+
...subjectOwned ? { subjectId: input.subjectId } : {},
|
|
178
|
+
...input.expectedAuthorityGeneration !== void 0 ? { expectedAuthorityGeneration: input.expectedAuthorityGeneration } : {}
|
|
179
|
+
};
|
|
180
|
+
if (input.connectionRef.connectionId !== void 0) {
|
|
181
|
+
request.connectionId = input.connectionRef.connectionId;
|
|
182
|
+
}
|
|
183
|
+
if (input.connectionRef.kind !== void 0) {
|
|
184
|
+
request.kind = input.connectionRef.kind;
|
|
185
|
+
}
|
|
186
|
+
const credential = await deps.loadCredential(lookupDb, settings, request);
|
|
187
|
+
if (!credential) return null;
|
|
188
|
+
if (input.expectedAuthorityGeneration !== void 0 && credential.authorityGeneration !== input.expectedAuthorityGeneration) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (subjectOwned) {
|
|
192
|
+
return credential.subjectId === input.subjectId ? credential : null;
|
|
193
|
+
}
|
|
194
|
+
return credential.subjectId === null ? credential : null;
|
|
195
|
+
};
|
|
196
|
+
const snapshot = async (cred, ref, destinationUrl, inputCredentialTarget, connectionUseAttribution, recordUsage = true) => {
|
|
197
|
+
if (cred.status !== "active") {
|
|
198
|
+
return authNeededForStatus(cred, ref);
|
|
199
|
+
}
|
|
200
|
+
if (!connectionBindingMatches(cred, ref, destinationUrl)) {
|
|
201
|
+
return authNeeded(ref, "missing_connection", cred.id);
|
|
202
|
+
}
|
|
203
|
+
const missingScopes = missingRequestedScopes(
|
|
204
|
+
ref.scopes,
|
|
205
|
+
cred.grantedScopes,
|
|
206
|
+
cred.providerDomain
|
|
207
|
+
);
|
|
208
|
+
if (missingScopes.length > 0) {
|
|
209
|
+
return {
|
|
210
|
+
status: "auth_needed",
|
|
211
|
+
reason: "insufficient_scope",
|
|
212
|
+
providerDomain: ref.providerDomain,
|
|
213
|
+
...ref.provider ? { provider: ref.provider } : {},
|
|
214
|
+
connectionId: cred.id,
|
|
215
|
+
scopes: missingScopes,
|
|
216
|
+
...ref.resource ? { resource: ref.resource } : {},
|
|
217
|
+
...ref.selectedResources ? { selectedResources: copySelectedResources(ref.selectedResources) } : {}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const material = credentialMaterialForConnection(cred, inputCredentialTarget);
|
|
221
|
+
if (material.status !== "ok") {
|
|
222
|
+
return {
|
|
223
|
+
status: "auth_needed",
|
|
224
|
+
reason: material.status === "unsupported" ? "unsupported_auth" : "refresh_failed",
|
|
225
|
+
providerDomain: ref.providerDomain,
|
|
226
|
+
...ref.provider ? { provider: ref.provider } : {},
|
|
227
|
+
connectionId: cred.id,
|
|
228
|
+
...ref.scopes ? { scopes: ref.scopes } : {},
|
|
229
|
+
...ref.resource ? { resource: ref.resource } : {},
|
|
230
|
+
...ref.selectedResources ? { selectedResources: copySelectedResources(ref.selectedResources) } : {}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (recordUsage) {
|
|
234
|
+
await deps.recordUsed(db, cred.workspaceId, cred.id, cred.subjectId);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
status: "ok",
|
|
238
|
+
headers: material.headers,
|
|
239
|
+
...material.placements ? { placements: material.placements } : {},
|
|
240
|
+
connectionId: cred.id,
|
|
241
|
+
connectionVersion: cred.version,
|
|
242
|
+
...connectionUseAttribution ? { connectionUseAttribution } : {},
|
|
243
|
+
expiresAt: cred.expiresAt
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const performRefresh = async (cred, ref, lockedDb) => {
|
|
247
|
+
const key = deps.keyBytes(settings);
|
|
248
|
+
if (!key) {
|
|
249
|
+
throw new Error("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured");
|
|
250
|
+
}
|
|
251
|
+
const refreshed = await deps.refresh(cred, ref, settings, options.refreshTransport);
|
|
252
|
+
const refreshRecord = {
|
|
253
|
+
id: cred.id,
|
|
254
|
+
version: cred.version,
|
|
255
|
+
workspaceId: cred.workspaceId,
|
|
256
|
+
credentialEncrypted: deps.encrypt(key, JSON.stringify(refreshed.credential)),
|
|
257
|
+
expiresAt: refreshed.expiresAt,
|
|
258
|
+
lastRefreshAt: deps.now(),
|
|
259
|
+
subjectId: cred.subjectId
|
|
260
|
+
};
|
|
261
|
+
if (refreshed.grantedScopes !== void 0) {
|
|
262
|
+
refreshRecord.grantedScopes = refreshed.grantedScopes;
|
|
263
|
+
}
|
|
264
|
+
if (refreshed.metadata !== void 0) {
|
|
265
|
+
refreshRecord.metadata = refreshed.metadata;
|
|
266
|
+
}
|
|
267
|
+
const persisted = await deps.recordRefresh(lockedDb, refreshRecord);
|
|
268
|
+
if (persisted) {
|
|
269
|
+
const current = await load(
|
|
270
|
+
{
|
|
271
|
+
workspaceId: cred.workspaceId,
|
|
272
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
273
|
+
...cred.subjectId ? { subjectId: cred.subjectId } : {}
|
|
274
|
+
},
|
|
275
|
+
lockedDb
|
|
276
|
+
);
|
|
277
|
+
if (current) {
|
|
278
|
+
return current;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const winner = await load(
|
|
282
|
+
{
|
|
283
|
+
workspaceId: cred.workspaceId,
|
|
284
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
285
|
+
...cred.subjectId ? { subjectId: cred.subjectId } : {}
|
|
286
|
+
},
|
|
287
|
+
lockedDb
|
|
288
|
+
);
|
|
289
|
+
if (winner?.status === "active") {
|
|
290
|
+
return winner;
|
|
291
|
+
}
|
|
292
|
+
throw new Error("connection credential changed during token refresh");
|
|
293
|
+
};
|
|
294
|
+
const refreshSingleFlight = (cred, ref) => {
|
|
295
|
+
const key = `${cred.subjectId ?? "workspace"}:${cred.id}:${cred.version}`;
|
|
296
|
+
const existing = inflight.get(key);
|
|
297
|
+
if (existing) {
|
|
298
|
+
return existing;
|
|
299
|
+
}
|
|
300
|
+
const promise = (deps.withRefreshLock ?? withConnectionRefreshLock)(
|
|
301
|
+
db,
|
|
302
|
+
cred,
|
|
303
|
+
async (lockedDb) => {
|
|
304
|
+
const current = await load(
|
|
305
|
+
{
|
|
306
|
+
workspaceId: cred.workspaceId,
|
|
307
|
+
connectionRef: { ...ref, connectionId: cred.id },
|
|
308
|
+
...cred.subjectId ? { subjectId: cred.subjectId } : {}
|
|
309
|
+
},
|
|
310
|
+
lockedDb
|
|
311
|
+
);
|
|
312
|
+
if (!current || current.authorityGeneration !== cred.authorityGeneration)
|
|
313
|
+
throw new Error("Connection authority changed before token refresh");
|
|
314
|
+
if (current.status !== "active" || current.version !== cred.version) return current;
|
|
315
|
+
return performRefresh(current, ref, lockedDb);
|
|
316
|
+
}
|
|
317
|
+
).finally(() => {
|
|
318
|
+
if (inflight.get(key) === promise) {
|
|
319
|
+
inflight.delete(key);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
inflight.set(key, promise);
|
|
323
|
+
return promise;
|
|
324
|
+
};
|
|
325
|
+
return async (input) => {
|
|
326
|
+
const credentialResolutionMode = input.credentialResolutionMode ?? "execution";
|
|
327
|
+
let ref = input.connectionRef;
|
|
328
|
+
let subjectId = input.subjectId;
|
|
329
|
+
let credentialWorkspaceId = input.workspaceId;
|
|
330
|
+
let expectedAuthorityGeneration = input.expectedAuthorityGeneration;
|
|
331
|
+
let connectionUseAttribution;
|
|
332
|
+
if (ref.authoritySource === "host") {
|
|
333
|
+
return authNeeded(ref, "unsupported_auth", ref.connectionId);
|
|
334
|
+
}
|
|
335
|
+
if (ref.selectedResources) {
|
|
336
|
+
return authNeeded(ref, "resource_scope_unavailable", ref.connectionId);
|
|
337
|
+
}
|
|
338
|
+
if (input.connectionUseContext !== void 0) {
|
|
339
|
+
if (!deps.authorizeAcceptedUse) {
|
|
340
|
+
return authNeeded(
|
|
341
|
+
ref,
|
|
342
|
+
ref.subjectScope === "subject" ? "personal_authority_unavailable" : "missing_connection",
|
|
343
|
+
ref.connectionId
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const authorization = await deps.authorizeAcceptedUse(db, {
|
|
347
|
+
...input.connectionUseContext,
|
|
348
|
+
serverId: input.serverId,
|
|
349
|
+
...ref.connectionId ? { connectionId: ref.connectionId } : {},
|
|
350
|
+
providerDomain: ref.providerDomain,
|
|
351
|
+
...ref.kind ? { connectionKind: ref.kind } : {},
|
|
352
|
+
subjectScope: ref.subjectScope === "subject" ? "subject" : "workspace",
|
|
353
|
+
// An owner binding belongs only to the personal lanes. Interactive
|
|
354
|
+
// turns stamp the initiating human's subjectId on every credential
|
|
355
|
+
// request regardless of ref scope; forwarding it for a workspace ref
|
|
356
|
+
// would make the 0279 workspace lane deny the ambient shared row.
|
|
357
|
+
...ref.subjectScope === "subject" && subjectId ? { ownerSubjectId: subjectId } : {}
|
|
358
|
+
});
|
|
359
|
+
if (authorization.status === "denied") {
|
|
360
|
+
return authNeeded(
|
|
361
|
+
ref,
|
|
362
|
+
ref.subjectScope === "subject" ? "personal_authority_unavailable" : "missing_connection",
|
|
363
|
+
ref.connectionId
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (expectedAuthorityGeneration !== void 0 && authorization.attribution.connectionGeneration !== expectedAuthorityGeneration) {
|
|
367
|
+
return authNeeded(
|
|
368
|
+
ref,
|
|
369
|
+
authorityReasonForScope(ref.subjectScope === "subject"),
|
|
370
|
+
ref.connectionId
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
connectionUseAttribution = authorization.attribution;
|
|
374
|
+
expectedAuthorityGeneration = authorization.attribution.connectionGeneration;
|
|
375
|
+
const expectedPersonal = authorization.attribution.scope !== "workspace";
|
|
376
|
+
credentialWorkspaceId = authorization.originWorkspaceId;
|
|
377
|
+
subjectId = authorization.attribution.ownerSubjectId ?? void 0;
|
|
378
|
+
ref = {
|
|
379
|
+
...ref,
|
|
380
|
+
connectionId: authorization.attribution.connectionId,
|
|
381
|
+
kind: authorization.connectionKind,
|
|
382
|
+
subjectScope: expectedPersonal ? "subject" : "workspace"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (ref.subjectScope === "subject" && !subjectId) {
|
|
386
|
+
return authNeeded(ref, "personal_authority_unavailable", ref.connectionId);
|
|
387
|
+
}
|
|
388
|
+
let cred;
|
|
389
|
+
try {
|
|
390
|
+
cred = await load({
|
|
391
|
+
workspaceId: credentialWorkspaceId,
|
|
392
|
+
connectionRef: ref,
|
|
393
|
+
...subjectId ? { subjectId } : {},
|
|
394
|
+
...expectedAuthorityGeneration !== void 0 ? { expectedAuthorityGeneration } : {}
|
|
395
|
+
});
|
|
396
|
+
} catch {
|
|
397
|
+
return authNeeded(ref, "refresh_failed");
|
|
398
|
+
}
|
|
399
|
+
if (!cred) {
|
|
400
|
+
return authNeeded(ref, "missing_connection");
|
|
401
|
+
}
|
|
402
|
+
if (cred.status !== "active") {
|
|
403
|
+
return authNeededForStatus(cred, ref);
|
|
404
|
+
}
|
|
405
|
+
if (!connectionBindingMatches(cred, ref, input.destinationUrl)) {
|
|
406
|
+
return authNeeded(ref, "missing_connection", cred.id);
|
|
407
|
+
}
|
|
408
|
+
const now = deps.now();
|
|
409
|
+
if (credentialResolutionMode === "preflight" && cred.kind === "oauth2" && (input.forceRefresh === true || cred.expiresAt !== null && cred.expiresAt.getTime() <= now.getTime())) {
|
|
410
|
+
return authNeeded(ref, "refresh_failed", cred.id);
|
|
411
|
+
}
|
|
412
|
+
if (credentialResolutionMode === "execution" && shouldRefresh(cred, input.forceRefresh === true, now)) {
|
|
413
|
+
try {
|
|
414
|
+
cred = await refreshSingleFlight(cred, ref);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (isPermanentRefreshError(error)) {
|
|
417
|
+
let handled = false;
|
|
418
|
+
if (options.transitionPermanentRefreshFailure) {
|
|
419
|
+
try {
|
|
420
|
+
handled = await options.transitionPermanentRefreshFailure({
|
|
421
|
+
workspaceId: cred.workspaceId,
|
|
422
|
+
connectionId: cred.id,
|
|
423
|
+
connectionVersion: cred.version,
|
|
424
|
+
subjectId: cred.subjectId,
|
|
425
|
+
providerDomain: cred.providerDomain,
|
|
426
|
+
httpStatus: error.httpStatus,
|
|
427
|
+
oauthErrorCode: error.oauthErrorCode
|
|
428
|
+
});
|
|
429
|
+
} catch {
|
|
430
|
+
handled = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!handled) {
|
|
434
|
+
await deps.setStatus(db, cred.workspaceId, "needs_reauth", error.message, {
|
|
435
|
+
id: cred.id,
|
|
436
|
+
version: cred.version,
|
|
437
|
+
subjectId: cred.subjectId
|
|
438
|
+
}).catch(() => void 0);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return authNeeded(ref, "refresh_failed", cred.id);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (expectedAuthorityGeneration !== void 0 && cred.authorityGeneration !== expectedAuthorityGeneration) {
|
|
445
|
+
return authNeeded(ref, authorityReasonForScope(ref.subjectScope === "subject"), cred.id);
|
|
446
|
+
}
|
|
447
|
+
return await snapshot(
|
|
448
|
+
cred,
|
|
449
|
+
ref,
|
|
450
|
+
input.destinationUrl,
|
|
451
|
+
input.credentialTarget ?? "mcp",
|
|
452
|
+
connectionUseAttribution,
|
|
453
|
+
credentialResolutionMode === "execution"
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function authorityReasonForScope(personal) {
|
|
458
|
+
return personal ? "personal_authority_unavailable" : "missing_connection";
|
|
459
|
+
}
|
|
460
|
+
function connectionBindingMatches(cred, ref, destinationUrl) {
|
|
461
|
+
if (cred.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase()) return false;
|
|
462
|
+
if (ref.kind && cred.kind !== ref.kind) return false;
|
|
463
|
+
const credential = cred.credential;
|
|
464
|
+
const metadata = cred.metadata;
|
|
465
|
+
const boundMcpUrl = stringValue(credential.mcp_url) ?? stringValue(metadata.mcpUrl);
|
|
466
|
+
const destination = canonicalHttpUrl(destinationUrl);
|
|
467
|
+
if (!destination) return false;
|
|
468
|
+
if (boundMcpUrl) {
|
|
469
|
+
const binding = canonicalHttpUrl(boundMcpUrl);
|
|
470
|
+
if (!binding || destination !== binding && !(binding === canonicalHttpUrl(OFFICIAL_GMAIL_MCP_RESOURCE) && isOfficialGmailRestDestination(destination, ref))) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
} else if (!destinationHostMatchesProvider(destination, cred.providerDomain)) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
if (cred.kind !== "oauth2") return true;
|
|
477
|
+
const boundResource = stringValue(credential.resource) ?? stringValue(metadata.resource);
|
|
478
|
+
if (ref.resource) {
|
|
479
|
+
if (!boundResource) return false;
|
|
480
|
+
if (canonicalResource(ref.resource) !== canonicalResource(boundResource)) return false;
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
function connectionMetadataMatchesBinding(connection, ref, destinationUrl) {
|
|
485
|
+
return connectionBindingMatches({ ...connection, credential: {} }, ref, destinationUrl) && missingRequestedScopes(ref.scopes, connection.grantedScopes, connection.providerDomain).length === 0;
|
|
486
|
+
}
|
|
487
|
+
function destinationHostMatchesProvider(destinationUrl, providerDomain) {
|
|
488
|
+
const destinationHost = new URL(destinationUrl).hostname.toLowerCase();
|
|
489
|
+
const provider = providerDomain.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
490
|
+
return Boolean(provider) && (destinationHost === provider || destinationHost.endsWith(`.${provider}`));
|
|
491
|
+
}
|
|
492
|
+
function canonicalHttpUrl(value) {
|
|
493
|
+
try {
|
|
494
|
+
const url = new URL(value);
|
|
495
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
496
|
+
url.hash = "";
|
|
497
|
+
url.hostname = url.hostname.toLowerCase();
|
|
498
|
+
if (url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80") {
|
|
499
|
+
url.port = "";
|
|
500
|
+
}
|
|
501
|
+
url.pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
502
|
+
return url.toString();
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function canonicalResource(value) {
|
|
508
|
+
return canonicalHttpUrl(value) ?? value.trim();
|
|
509
|
+
}
|
|
510
|
+
var ConnectionRefreshHttpError = class extends Error {
|
|
511
|
+
httpStatus;
|
|
512
|
+
oauthErrorCode;
|
|
513
|
+
constructor(httpStatus, oauthErrorCode = null) {
|
|
514
|
+
super(`connection refresh failed with HTTP ${httpStatus}`);
|
|
515
|
+
this.name = "ConnectionRefreshHttpError";
|
|
516
|
+
this.httpStatus = httpStatus;
|
|
517
|
+
this.oauthErrorCode = oauthErrorCode;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
function isPermanentRefreshError(error) {
|
|
521
|
+
return error instanceof ConnectionRefreshHttpError && error.httpStatus >= 400 && error.httpStatus < 500 && error.httpStatus !== 408 && error.httpStatus !== 429;
|
|
522
|
+
}
|
|
523
|
+
function shouldRefresh(cred, force, now) {
|
|
524
|
+
if (cred.kind !== "oauth2") {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (force) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
if (!cred.expiresAt) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return cred.expiresAt.getTime() <= now.getTime() + REFRESH_WINDOW_MS;
|
|
534
|
+
}
|
|
535
|
+
function authNeeded(ref, reason, connectionId) {
|
|
536
|
+
return {
|
|
537
|
+
status: "auth_needed",
|
|
538
|
+
reason,
|
|
539
|
+
providerDomain: ref.providerDomain,
|
|
540
|
+
...ref.provider ? { provider: ref.provider } : {},
|
|
541
|
+
...connectionId ? { connectionId } : {},
|
|
542
|
+
...ref.scopes ? { scopes: ref.scopes } : {},
|
|
543
|
+
...ref.resource ? { resource: ref.resource } : {},
|
|
544
|
+
...ref.selectedResources ? { selectedResources: copySelectedResources(ref.selectedResources) } : {}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function authNeededForStatus(cred, ref) {
|
|
548
|
+
if (cred.status === "revoked") {
|
|
549
|
+
return authNeeded(ref, "missing_connection", cred.id);
|
|
550
|
+
}
|
|
551
|
+
return authNeeded(
|
|
552
|
+
ref,
|
|
553
|
+
cred.expiresAt && cred.expiresAt.getTime() <= Date.now() ? "expired" : "refresh_failed",
|
|
554
|
+
cred.id
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
function missingRequestedScopes(requested, granted, providerDomain) {
|
|
558
|
+
if (!requested?.length) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
const grantedSet = new Set(granted.map((scope) => connectionScopeKey(providerDomain, scope)));
|
|
562
|
+
return requested.filter((scope) => !grantedSet.has(connectionScopeKey(providerDomain, scope)));
|
|
563
|
+
}
|
|
564
|
+
function normalizeBearerScheme(tokenType) {
|
|
565
|
+
return !tokenType || /^bearer$/i.test(tokenType) ? "Bearer" : tokenType;
|
|
566
|
+
}
|
|
567
|
+
function credentialMaterialForConnection(cred, target) {
|
|
568
|
+
if (cred.kind === "oauth2") {
|
|
569
|
+
const accessToken = stringValue(cred.credential.access_token);
|
|
570
|
+
if (!accessToken) {
|
|
571
|
+
return { status: "invalid" };
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
status: "ok",
|
|
575
|
+
headers: {
|
|
576
|
+
authorization: `${normalizeBearerScheme(stringValue(cred.credential.token_type))} ${accessToken}`
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const rawPlacements = cred.credential.placements;
|
|
581
|
+
if (rawPlacements !== void 0) {
|
|
582
|
+
let placements;
|
|
583
|
+
try {
|
|
584
|
+
placements = normalizedCredentialPlacements(rawPlacements);
|
|
585
|
+
} catch {
|
|
586
|
+
return { status: "invalid" };
|
|
587
|
+
}
|
|
588
|
+
if (target !== "http_api" && placements.some((placement) => placement.carrier !== "header")) {
|
|
589
|
+
return { status: "unsupported" };
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
status: "ok",
|
|
593
|
+
headers: headerMapForPlacements(placements),
|
|
594
|
+
placements
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const headers = normalizedCredentialHeaders(
|
|
599
|
+
stringRecord(cred.credential.headers) ?? {}
|
|
600
|
+
);
|
|
601
|
+
return { status: "ok", headers };
|
|
602
|
+
} catch {
|
|
603
|
+
return { status: "invalid" };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function refreshOAuthConnectionCredential(cred, ref, settings, transportOptions = {}) {
|
|
607
|
+
if (cred.kind !== "oauth2") {
|
|
608
|
+
return {
|
|
609
|
+
credential: cred.credential,
|
|
610
|
+
expiresAt: cred.expiresAt,
|
|
611
|
+
grantedScopes: cred.grantedScopes
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const refreshToken = stringValue(cred.credential.refresh_token);
|
|
615
|
+
const personalGitHubMetadata = PersonalGitHubConnectionMetadata.safeParse(cred.metadata);
|
|
616
|
+
const personalGitHub = cred.providerDomain.toLowerCase() === PERSONAL_GITHUB_PROVIDER_DOMAIN && personalGitHubMetadata.success;
|
|
617
|
+
const storedTokenEndpoint = stringValue(cred.credential.token_endpoint) ?? stringValue(cred.metadata.tokenEndpoint) ?? stringValue(cred.metadata.token_endpoint);
|
|
618
|
+
const tokenEndpoint = personalGitHub ? PERSONAL_GITHUB_TOKEN_URL : storedTokenEndpoint;
|
|
619
|
+
if (!refreshToken || !tokenEndpoint) {
|
|
620
|
+
throw new Error("connection has no refresh token endpoint");
|
|
621
|
+
}
|
|
622
|
+
if (personalGitHub && storedTokenEndpoint !== PERSONAL_GITHUB_TOKEN_URL) {
|
|
623
|
+
throw new ConnectionRefreshHttpError(401, "client_unavailable");
|
|
624
|
+
}
|
|
625
|
+
let validatedTokenEndpoint;
|
|
626
|
+
try {
|
|
627
|
+
validatedTokenEndpoint = validateHttpUrl(tokenEndpoint, {
|
|
628
|
+
label: "OAuth refresh token endpoint",
|
|
629
|
+
allowLoopbackHttp: settings.environment === "local" || settings.environment === "test"
|
|
630
|
+
});
|
|
631
|
+
} catch {
|
|
632
|
+
throw new Error("connection has an invalid refresh token endpoint");
|
|
633
|
+
}
|
|
634
|
+
const body = new URLSearchParams();
|
|
635
|
+
body.set("grant_type", "refresh_token");
|
|
636
|
+
body.set("refresh_token", refreshToken);
|
|
637
|
+
const clientId = stringValue(cred.credential.client_id) ?? stringValue(cred.metadata.clientId) ?? stringValue(cred.metadata.client_id);
|
|
638
|
+
const clientSecret = personalGitHub ? settings.githubPersonalOauthClientSecret : stringValue(cred.credential.client_secret);
|
|
639
|
+
const authMethod = stringValue(
|
|
640
|
+
cred.credential.token_endpoint_auth_method
|
|
641
|
+
) ?? "none";
|
|
642
|
+
if (personalGitHub && (!settings.githubPersonalOauthEnabled || !clientId || clientId !== settings.githubPersonalOauthClientId || personalGitHubMetadata.data.oauthEnvironment !== settings.environment || personalGitHubMetadata.data.oauthClientMarker !== createHash("sha256").update(clientId).digest("hex").slice(0, 32) || !clientSecret)) {
|
|
643
|
+
throw new ConnectionRefreshHttpError(401, "client_unavailable");
|
|
644
|
+
}
|
|
645
|
+
if (clientId) {
|
|
646
|
+
body.set("client_id", clientId);
|
|
647
|
+
}
|
|
648
|
+
const tokenRequestEncoding = stringValue(
|
|
649
|
+
cred.credential.token_request_encoding
|
|
650
|
+
) === "json" ? "json" : "form";
|
|
651
|
+
const headers = {
|
|
652
|
+
"content-type": tokenRequestEncoding === "json" ? "application/json" : "application/x-www-form-urlencoded"
|
|
653
|
+
};
|
|
654
|
+
if (clientSecret && authMethod === "client_secret_post") {
|
|
655
|
+
body.set("client_secret", clientSecret);
|
|
656
|
+
} else if (clientId && clientSecret && authMethod === "client_secret_basic") {
|
|
657
|
+
headers.authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
658
|
+
}
|
|
659
|
+
const resource = ref.resource ?? stringValue(cred.credential.resource);
|
|
660
|
+
const resourceParameterSupported = cred.credential.resource_parameter_supported !== false;
|
|
661
|
+
if (resource && resourceParameterSupported) {
|
|
662
|
+
body.set("resource", resource);
|
|
663
|
+
}
|
|
664
|
+
const scopeParameterSupported = cred.credential.scope_parameter_supported !== false;
|
|
665
|
+
if (ref.scopes?.length && scopeParameterSupported) {
|
|
666
|
+
body.set("scope", ref.scopes.join(" "));
|
|
667
|
+
}
|
|
668
|
+
const requestBody = tokenRequestEncoding === "json" ? JSON.stringify(Object.fromEntries(body.entries())) : body;
|
|
669
|
+
const response = await pinnedFetch(
|
|
670
|
+
validatedTokenEndpoint,
|
|
671
|
+
{
|
|
672
|
+
method: "POST",
|
|
673
|
+
headers,
|
|
674
|
+
body: requestBody,
|
|
675
|
+
signal: AbortSignal.timeout(CONNECTION_REFRESH_TIMEOUT_MS)
|
|
676
|
+
},
|
|
677
|
+
settings,
|
|
678
|
+
{
|
|
679
|
+
fetchImpl: transportOptions.fetchImpl ?? undiciFetch,
|
|
680
|
+
...transportOptions.dnsLookup ? { dnsLookup: transportOptions.dnsLookup } : {},
|
|
681
|
+
label: "OAuth token endpoint",
|
|
682
|
+
requireHttpsOutsideLocalTest: true
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
if (response.status >= 300 && response.status < 400) {
|
|
686
|
+
await cancelResponseBody(response);
|
|
687
|
+
throw new ConnectionRefreshHttpError(response.status);
|
|
688
|
+
}
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
throw new ConnectionRefreshHttpError(
|
|
691
|
+
response.status,
|
|
692
|
+
await readOAuthRefreshErrorCode(response)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const payload = await readResponseJsonBounded(
|
|
696
|
+
response,
|
|
697
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
698
|
+
"OAuth refresh token response"
|
|
699
|
+
);
|
|
700
|
+
const accessToken = stringValue(payload.access_token);
|
|
701
|
+
if (!accessToken) {
|
|
702
|
+
throw new Error("connection refresh response did not include access_token");
|
|
703
|
+
}
|
|
704
|
+
const expiresAt = expiresAtFromTokenResponse(payload, cred.expiresAt);
|
|
705
|
+
const scopeText = stringValue(payload.scope);
|
|
706
|
+
const returnedScopes = scopeText?.split(/[\s,]+/).filter(Boolean) ?? [];
|
|
707
|
+
if (scopeText && ref.providerDomain.toLowerCase() === "graph.microsoft.com" && !returnedScopes.some((scope) => scope.toLowerCase() === "offline_access")) {
|
|
708
|
+
returnedScopes.push("offline_access");
|
|
709
|
+
}
|
|
710
|
+
const refreshTokenExpiresIn = typeof payload.refresh_token_expires_in === "number" ? payload.refresh_token_expires_in : void 0;
|
|
711
|
+
const refreshTokenExpiresAt = personalGitHub && refreshTokenExpiresIn && refreshTokenExpiresIn > 0 ? new Date(Date.now() + refreshTokenExpiresIn * 1e3).toISOString() : null;
|
|
712
|
+
if (personalGitHub && scopeText && (returnedScopes.length !== PERSONAL_GITHUB_REQUESTED_SCOPES.length || returnedScopes.some((scope) => !PERSONAL_GITHUB_REQUESTED_SCOPES.includes(scope)))) {
|
|
713
|
+
throw new ConnectionRefreshHttpError(401, "scope_changed");
|
|
714
|
+
}
|
|
715
|
+
const nextCredential = {
|
|
716
|
+
...cred.credential,
|
|
717
|
+
access_token: accessToken,
|
|
718
|
+
refresh_token: stringValue(payload.refresh_token) ?? refreshToken,
|
|
719
|
+
token_type: stringValue(payload.token_type) ?? stringValue(cred.credential.token_type) ?? "Bearer",
|
|
720
|
+
...expiresAt ? { expires_at: expiresAt.toISOString() } : {},
|
|
721
|
+
...resource ? { resource } : {},
|
|
722
|
+
...scopeText ? {
|
|
723
|
+
scope: ref.providerDomain.toLowerCase() === "graph.microsoft.com" ? returnedScopes.join(" ") : scopeText
|
|
724
|
+
} : {},
|
|
725
|
+
...refreshTokenExpiresAt ? { refresh_token_expires_at: refreshTokenExpiresAt } : {},
|
|
726
|
+
...clientSecret && !personalGitHub ? { client_secret: clientSecret, token_endpoint_auth_method: authMethod } : {}
|
|
727
|
+
};
|
|
728
|
+
return {
|
|
729
|
+
credential: nextCredential,
|
|
730
|
+
expiresAt,
|
|
731
|
+
...personalGitHub ? { grantedScopes: [...PERSONAL_GITHUB_REQUESTED_SCOPES] } : scopeText ? { grantedScopes: returnedScopes } : {},
|
|
732
|
+
...refreshTokenExpiresAt ? {
|
|
733
|
+
metadata: PersonalGitHubConnectionMetadata.parse({
|
|
734
|
+
...personalGitHubMetadata.data,
|
|
735
|
+
refreshTokenExpiresAt
|
|
736
|
+
})
|
|
737
|
+
} : {}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
async function readOAuthRefreshErrorCode(response) {
|
|
741
|
+
try {
|
|
742
|
+
const payload = await readResponseJsonBounded(
|
|
743
|
+
response,
|
|
744
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
745
|
+
"OAuth refresh error response"
|
|
746
|
+
);
|
|
747
|
+
const code = stringValue(payload.error);
|
|
748
|
+
return code && /^[a-z0-9_.-]{1,64}$/i.test(code) ? code : null;
|
|
749
|
+
} catch {
|
|
750
|
+
await cancelResponseBody(response);
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function expiresAtFromTokenResponse(payload, fallback) {
|
|
755
|
+
const expiresAt = stringValue(payload.expires_at);
|
|
756
|
+
if (expiresAt) {
|
|
757
|
+
const parsed = new Date(expiresAt);
|
|
758
|
+
return Number.isNaN(parsed.getTime()) ? fallback : parsed;
|
|
759
|
+
}
|
|
760
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : void 0;
|
|
761
|
+
if (expiresIn && Number.isFinite(expiresIn) && expiresIn > 0) {
|
|
762
|
+
return new Date(Date.now() + expiresIn * 1e3);
|
|
763
|
+
}
|
|
764
|
+
return fallback;
|
|
765
|
+
}
|
|
766
|
+
function stringRecord(value) {
|
|
767
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const out = {};
|
|
771
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
772
|
+
if (typeof raw !== "string") {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
out[key] = raw;
|
|
776
|
+
}
|
|
777
|
+
return out;
|
|
778
|
+
}
|
|
779
|
+
function stringValue(value) {
|
|
780
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
781
|
+
}
|
|
782
|
+
async function cancelResponseBody(response) {
|
|
783
|
+
await response.body?.cancel().catch(() => void 0);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export {
|
|
787
|
+
connectionScopeKey,
|
|
788
|
+
normalizedCredentialHeaders,
|
|
789
|
+
buildConnectionTokenResolver,
|
|
790
|
+
connectionMetadataMatchesBinding,
|
|
791
|
+
ConnectionRefreshHttpError,
|
|
792
|
+
normalizeBearerScheme,
|
|
793
|
+
refreshOAuthConnectionCredential,
|
|
794
|
+
isPrivateAddress
|
|
795
|
+
};
|
|
796
|
+
//# sourceMappingURL=chunk-OYD7FLKH.js.map
|