@opengeni/api-router 2.11.3 → 2.12.1-canary.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 (64) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-5X3XM6CE.js → chunk-Y3EPAIEQ.js} +17643 -14398
  3. package/dist/chunk-Y3EPAIEQ.js.map +1 -0
  4. package/dist/connection-ownership.d.ts +5 -1
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/atlassian.d.ts +4 -0
  8. package/dist/integrations/connect-authority.d.ts +1 -0
  9. package/dist/integrations/connect-callback-return.d.ts +6 -0
  10. package/dist/integrations/fiken.d.ts +7 -0
  11. package/dist/integrations/github-app-connect.d.ts +47 -0
  12. package/dist/integrations/github-installation-proof.d.ts +3 -0
  13. package/dist/integrations/github-lens-connect.d.ts +153 -0
  14. package/dist/integrations/google-drive.d.ts +4 -0
  15. package/dist/integrations/oauth-client.d.ts +4 -0
  16. package/dist/integrations/oauth-return-path.d.ts +1 -0
  17. package/dist/integrations/personal-github.d.ts +2 -0
  18. package/dist/integrations/provider-oauth.d.ts +11 -1
  19. package/dist/integrations/slack-install.d.ts +20 -0
  20. package/dist/integrations/social-oauth.d.ts +2 -0
  21. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  22. package/dist/routes/api-integrations.d.ts +14 -0
  23. package/dist/routes/connect.d.ts +5 -0
  24. package/dist/routes/external-identity-links.d.ts +6 -0
  25. package/dist/routes/host-mcp-bindings.d.ts +5 -0
  26. package/dist/workspace-tool-gateway.d.ts +1 -0
  27. package/package.json +19 -19
  28. package/src/app.ts +22 -2
  29. package/src/connection-ownership.ts +14 -1
  30. package/src/index.ts +2 -0
  31. package/src/integrations/atlassian.ts +143 -39
  32. package/src/integrations/connect-authority.ts +2 -0
  33. package/src/integrations/connect-callback-return.ts +86 -0
  34. package/src/integrations/fiken.ts +222 -40
  35. package/src/integrations/github-app-connect.ts +389 -0
  36. package/src/integrations/github-installation-proof.ts +60 -0
  37. package/src/integrations/github-lens-connect.ts +97 -0
  38. package/src/integrations/google-drive.ts +152 -47
  39. package/src/integrations/oauth-client.ts +180 -84
  40. package/src/integrations/oauth-return-path.ts +18 -0
  41. package/src/integrations/personal-github.ts +146 -52
  42. package/src/integrations/provider-oauth.ts +132 -21
  43. package/src/integrations/slack-install.ts +83 -0
  44. package/src/integrations/social-oauth.ts +142 -1
  45. package/src/mcp/documents.ts +1 -1
  46. package/src/mcp/server.ts +4 -4
  47. package/src/routes/api-integrations.ts +74 -64
  48. package/src/routes/connect.ts +1869 -0
  49. package/src/routes/connections.ts +263 -221
  50. package/src/routes/documents.ts +2 -2
  51. package/src/routes/external-identity-links.ts +200 -0
  52. package/src/routes/github.ts +39 -64
  53. package/src/routes/host-mcp-bindings.ts +122 -0
  54. package/src/routes/organization-memberships.ts +23 -0
  55. package/src/routes/organization-sessions.ts +2 -2
  56. package/src/routes/personal-github.ts +8 -1
  57. package/src/routes/pr-review-github.ts +30 -3
  58. package/src/routes/scheduled-tasks.ts +29 -4
  59. package/src/routes/sessions.ts +61 -32
  60. package/src/routes/social.ts +5 -1
  61. package/src/routes/supergrok.ts +90 -3
  62. package/src/routes/workspaces.ts +16 -0
  63. package/src/workspace-tool-gateway.ts +136 -24
  64. package/dist/chunk-5X3XM6CE.js.map +0 -1
@@ -1,10 +1,15 @@
1
+ import { safeReturnPath } from "./oauth-return-path";
1
2
  import type { ApiRouteDeps } from "@opengeni/core";
2
3
  import type { Settings } from "@opengeni/config";
4
+ import { createHash } from "node:crypto";
5
+ import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
6
+ import { requireConnectOwnerAuthority } from "@opengeni/core";
3
7
  import {
4
8
  FIKEN_CREDENTIAL_LABEL,
5
9
  FIKEN_CREDENTIAL_ROLE,
6
10
  FIKEN_PROVIDER_DOMAIN,
7
11
  FikenOAuthStartResponse,
12
+ FikenInstallRequest,
8
13
  type AccessGrant,
9
14
  type ConnectionMetadata,
10
15
  type FikenCompanySummary,
@@ -13,7 +18,6 @@ import {
13
18
  } from "@opengeni/contracts";
14
19
  import {
15
20
  fikenConnectionMetadata,
16
- hasPermission,
17
21
  isFikenConnection,
18
22
  preferredFikenConnection,
19
23
  requireEnvironmentEncryption,
@@ -24,8 +28,11 @@ import {
24
28
  consumeIntegrationOAuthStateNonce,
25
29
  createConnection,
26
30
  encryptEnvironmentValue,
31
+ decryptEnvironmentValue,
32
+ getConnectAttempt,
33
+ claimConnectOperation,
34
+ finishConnectOperation,
27
35
  getConnectionMetadata,
28
- getWorkspaceGrant,
29
36
  listConnectionsMetadata,
30
37
  loadConnectionCredentialForBroker,
31
38
  recordAuditEvent,
@@ -55,6 +62,84 @@ const MAX_DRAFT_LINES = 100;
55
62
  // Fiken company slugs as issued by Fiken: lowercase alphanumerics and dashes.
56
63
  const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
57
64
 
65
+ /** Shared native/embedded token adapter. Provider verification happens before
66
+ * persistence; the caller reauthorizes inside its durable commit transaction. */
67
+ export async function prepareFikenTokenInstall(
68
+ deps: ApiRouteDeps,
69
+ grant: Pick<AccessGrant, "accountId" | "workspaceId" | "subjectId">,
70
+ raw: unknown,
71
+ expectedVersion?: number,
72
+ ): Promise<(tx: Database) => Promise<ConnectionMetadata>> {
73
+ const payload = FikenInstallRequest.parse(raw);
74
+ const key = requireEnvironmentEncryption(deps.settings);
75
+ const existing = payload.connectionId
76
+ ? await getConnectionMetadata(deps.db, grant.workspaceId, payload.connectionId, null)
77
+ : null;
78
+ if (payload.connectionId && !existing)
79
+ throw new HTTPException(404, { message: "connection not found" });
80
+ if (existing && !isFikenConnection(existing))
81
+ throw new HTTPException(422, { message: "connectionId is not a Fiken connection" });
82
+ if (expectedVersion !== undefined && existing?.version !== expectedVersion)
83
+ throw new HTTPException(409, { message: "The selected Fiken account changed" });
84
+ const verified = await verifyFikenApiToken(payload.apiToken, deps.fikenFetch ?? fetch);
85
+ const defaultCompanySlug = resolveFikenDefaultCompanySlug({
86
+ requested: payload.defaultCompanySlug ?? null,
87
+ previous: existing
88
+ ? (fikenConnectionMetadata(existing.metadata)?.defaultCompanySlug ?? null)
89
+ : null,
90
+ companies: verified.companies,
91
+ });
92
+ if (payload.defaultCompanySlug && defaultCompanySlug !== payload.defaultCompanySlug)
93
+ throw new HTTPException(422, {
94
+ message: "The selected company is not accessible with this token",
95
+ });
96
+ const metadata = {
97
+ credentialRole: FIKEN_CREDENTIAL_ROLE,
98
+ credentialLabel: FIKEN_CREDENTIAL_LABEL,
99
+ companies: verified.companies,
100
+ defaultCompanySlug,
101
+ verifiedAt: new Date().toISOString(),
102
+ };
103
+ const credentialEncrypted = encryptEnvironmentValue(
104
+ key,
105
+ JSON.stringify(fikenCredentialBundle(payload.apiToken)),
106
+ );
107
+ return async (tx) => {
108
+ if (existing) {
109
+ const updated = await updateConnection(tx, {
110
+ workspaceId: grant.workspaceId,
111
+ connectionId: existing.id,
112
+ visibleToSubjectId: null,
113
+ expectedVersion: existing.version,
114
+ kind: "api_key",
115
+ status: "active",
116
+ credentialEncrypted,
117
+ grantedScopes: [],
118
+ expiresAt: null,
119
+ metadata,
120
+ updatedBySubjectId: grant.subjectId,
121
+ });
122
+ if (!updated)
123
+ throw new HTTPException(409, {
124
+ message: "The Fiken connection changed; reload before retrying",
125
+ });
126
+ return updated;
127
+ }
128
+ return createConnection(tx, {
129
+ accountId: grant.accountId,
130
+ workspaceId: grant.workspaceId,
131
+ subjectId: null,
132
+ providerDomain: FIKEN_PROVIDER_DOMAIN,
133
+ kind: "api_key",
134
+ credentialEncrypted,
135
+ grantedScopes: [],
136
+ expiresAt: null,
137
+ metadata,
138
+ createdBySubjectId: grant.subjectId,
139
+ });
140
+ };
141
+ }
142
+
58
143
  export type FikenOperation =
59
144
  | "companies.list"
60
145
  | "contacts.list"
@@ -978,6 +1063,8 @@ export class FikenOAuthCallbackError extends Error {
978
1063
  }
979
1064
 
980
1065
  type FikenOAuthState = {
1066
+ externalContinuation?: ExternalActorContinuation;
1067
+ connectAttemptId?: string;
981
1068
  accountId: string;
982
1069
  workspaceId: string;
983
1070
  subjectId: string;
@@ -1011,6 +1098,8 @@ export async function startFikenOAuth(
1011
1098
  subjectId: string;
1012
1099
  requestUrl: string;
1013
1100
  payload: FikenOAuthStartRequest;
1101
+ externalContinuation?: ExternalActorContinuation;
1102
+ connectAttemptId?: string;
1014
1103
  },
1015
1104
  ): Promise<FikenOAuthStartResponse> {
1016
1105
  const fiken = requireFikenOAuthSettings(deps.settings);
@@ -1028,7 +1117,18 @@ export async function startFikenOAuth(
1028
1117
  accountId: input.accountId,
1029
1118
  workspaceId: input.workspaceId,
1030
1119
  subjectId: input.subjectId,
1031
- returnPath: `/workspaces/${input.workspaceId}/capabilities`,
1120
+ returnPath: safeReturnPath(
1121
+ input.payload.returnPath ?? `/workspaces/${input.workspaceId}/capabilities`,
1122
+ ),
1123
+ ...(input.connectAttemptId ? { connectAttemptId: input.connectAttemptId } : {}),
1124
+ ...(input.externalContinuation
1125
+ ? {
1126
+ encryptedExternalContinuation: encryptEnvironmentValue(
1127
+ requireEnvironmentEncryption(deps.settings),
1128
+ JSON.stringify(ExternalActorContinuation.parse(input.externalContinuation)),
1129
+ ),
1130
+ }
1131
+ : {}),
1032
1132
  ...(existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}),
1033
1133
  });
1034
1134
  const authorizationUrl = new URL(FIKEN_AUTHORIZE_URL);
@@ -1050,12 +1150,32 @@ export async function completeFikenOAuthCallback(
1050
1150
  error?: string | undefined;
1051
1151
  requestUrl: string;
1052
1152
  },
1053
- ): Promise<{ redirectTo: string }> {
1153
+ ): Promise<{ redirectTo: string; exactReturn?: boolean }> {
1054
1154
  const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
1055
1155
  const returnBaseUrl = deps.settings.webBaseUrl?.replace(/\/+$/, "") ?? baseUrl;
1056
1156
  let state: FikenOAuthState | null = null;
1157
+ let exactReturnUrl: string | undefined;
1158
+ let operation: { attemptId: string; operationId: string; inputDigest: string } | undefined;
1057
1159
  try {
1058
1160
  state = readFikenOAuthState(input.state, deps.settings);
1161
+ if (state.connectAttemptId) {
1162
+ const stored = await getConnectAttempt(deps.db, state, state.connectAttemptId);
1163
+ if (stored.attempt.providerId !== "fiken-oauth" || stored.attempt.ownership !== "workspace")
1164
+ throw new FikenOAuthCallbackError("invalid_state");
1165
+ exactReturnUrl = stored.returnUrl;
1166
+ operation = {
1167
+ attemptId: state.connectAttemptId,
1168
+ operationId: `oauth:${state.nonce}`,
1169
+ inputDigest: createHash("sha256").update(input.state!).digest("hex"),
1170
+ };
1171
+ const claim = await claimConnectOperation(deps.db, state, {
1172
+ ...operation,
1173
+ expectedRevision: stored.attempt.revision,
1174
+ authorize: (tx, _attempt, origin) =>
1175
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
1176
+ });
1177
+ if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
1178
+ }
1059
1179
  await requireFikenCallbackGrant(deps.db, state);
1060
1180
  const consumed = await consumeIntegrationOAuthStateNonce(deps.db, {
1061
1181
  accountId: state.accountId,
@@ -1068,6 +1188,27 @@ export async function completeFikenOAuthCallback(
1068
1188
  if (!consumed) {
1069
1189
  throw new FikenOAuthCallbackError("state_replayed");
1070
1190
  }
1191
+ // A proven pre-exchange rejection is terminal, not an uncertain provider
1192
+ // effect. Persist its receipt so repeated browser callbacks cannot retry it.
1193
+ if (operation && (input.error || !input.code)) {
1194
+ await finishConnectOperation(deps.db, state, {
1195
+ ...operation,
1196
+ authorize: (tx, _attempt, origin) =>
1197
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
1198
+ commit: async (_tx, current) => ({
1199
+ ...current,
1200
+ revision: current.revision + 1,
1201
+ state: input.error === "access_denied" ? "cancelled" : "failed",
1202
+ nextAction: { type: "none" },
1203
+ error: {
1204
+ code: input.error ? "provider_denied" : "missing_code",
1205
+ message: "Authorization was not completed. Start a new connection attempt.",
1206
+ retryable: false,
1207
+ },
1208
+ }),
1209
+ });
1210
+ return { redirectTo: exactReturnUrl!, exactReturn: true };
1211
+ }
1071
1212
  if (input.error) {
1072
1213
  throw new FikenOAuthCallbackError(
1073
1214
  input.error === "access_denied" ? "provider_denied" : "provider_error",
@@ -1132,34 +1273,66 @@ export async function completeFikenOAuthCallback(
1132
1273
  defaultCompanySlug,
1133
1274
  verifiedAt: new Date().toISOString(),
1134
1275
  };
1135
- const connection = existing
1136
- ? await updateConnection(deps.db, {
1137
- workspaceId: state.workspaceId,
1138
- connectionId: existing.id,
1139
- visibleToSubjectId: null,
1140
- expectedVersion: existing.version,
1141
- kind: "oauth2",
1142
- status: "active",
1143
- credentialEncrypted,
1144
- expiresAt: token.expiresAt,
1145
- metadata,
1146
- updatedBySubjectId: state.subjectId,
1147
- })
1148
- : await createConnection(deps.db, {
1149
- accountId: state.accountId,
1150
- workspaceId: state.workspaceId,
1151
- // Workspace-owned by design, like the pasted-token install: the
1152
- // first-party fiken tools resolve only workspace connections until
1153
- // the delegation-snapshot lane exists for personal ownership.
1154
- subjectId: null,
1155
- providerDomain: FIKEN_PROVIDER_DOMAIN,
1156
- kind: "oauth2",
1157
- credentialEncrypted,
1158
- grantedScopes: [],
1159
- expiresAt: token.expiresAt,
1160
- metadata,
1161
- createdBySubjectId: state.subjectId,
1162
- });
1276
+ const persist = (tx: Database) =>
1277
+ existing
1278
+ ? updateConnection(tx, {
1279
+ workspaceId: state!.workspaceId,
1280
+ connectionId: existing.id,
1281
+ visibleToSubjectId: null,
1282
+ expectedVersion: existing.version,
1283
+ kind: "oauth2",
1284
+ status: "active",
1285
+ credentialEncrypted,
1286
+ expiresAt: token.expiresAt,
1287
+ metadata,
1288
+ updatedBySubjectId: state!.subjectId,
1289
+ })
1290
+ : createConnection(tx, {
1291
+ accountId: state!.accountId,
1292
+ workspaceId: state!.workspaceId,
1293
+ // Workspace-owned by design, like the pasted-token install: the
1294
+ // first-party fiken tools resolve only workspace connections until
1295
+ // the delegation-snapshot lane exists for personal ownership.
1296
+ subjectId: null,
1297
+ providerDomain: FIKEN_PROVIDER_DOMAIN,
1298
+ kind: "oauth2",
1299
+ credentialEncrypted,
1300
+ grantedScopes: [],
1301
+ expiresAt: token.expiresAt,
1302
+ metadata,
1303
+ createdBySubjectId: state!.subjectId,
1304
+ });
1305
+ if (operation) {
1306
+ await finishConnectOperation(deps.db, state, {
1307
+ ...operation,
1308
+ authorize: (tx, _attempt, origin) =>
1309
+ requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
1310
+ commit: async (tx, current) => {
1311
+ const connection = await persist(tx);
1312
+ if (!connection) throw new FikenOAuthCallbackError("connection_conflict");
1313
+ return {
1314
+ ...current,
1315
+ revision: current.revision + 1,
1316
+ state: "complete",
1317
+ credentialsCommitted: true,
1318
+ nextAction: { type: "none" },
1319
+ account: {
1320
+ id: connection.id,
1321
+ version: connection.version,
1322
+ providerId: "fiken-oauth",
1323
+ label: "Fiken",
1324
+ ownership: "workspace",
1325
+ status: "connected",
1326
+ },
1327
+ };
1328
+ },
1329
+ });
1330
+ return { redirectTo: exactReturnUrl!, exactReturn: true };
1331
+ }
1332
+ const connection = await deps.db.transaction(async (tx) => {
1333
+ await requireFikenCallbackGrant(tx, state!);
1334
+ return persist(tx);
1335
+ });
1163
1336
  if (!connection) {
1164
1337
  throw new FikenOAuthCallbackError("connection_conflict");
1165
1338
  }
@@ -1167,6 +1340,7 @@ export async function completeFikenOAuthCallback(
1167
1340
  redirectTo: fikenReturnUrl(returnBaseUrl, state.returnPath, "connected", connection.id),
1168
1341
  };
1169
1342
  } catch (error) {
1343
+ if (exactReturnUrl) return { redirectTo: exactReturnUrl, exactReturn: true };
1170
1344
  return {
1171
1345
  redirectTo: fikenReturnUrl(
1172
1346
  returnBaseUrl,
@@ -1270,6 +1444,21 @@ function readFikenOAuthState(raw: string | undefined, settings: Settings): Fiken
1270
1444
  workspaceId: required(payload.workspaceId),
1271
1445
  subjectId: required(payload.subjectId),
1272
1446
  returnPath: required(payload.returnPath),
1447
+ ...(typeof payload.connectAttemptId === "string"
1448
+ ? { connectAttemptId: required(payload.connectAttemptId) }
1449
+ : {}),
1450
+ ...(typeof payload.encryptedExternalContinuation === "string"
1451
+ ? {
1452
+ externalContinuation: ExternalActorContinuation.parse(
1453
+ JSON.parse(
1454
+ decryptEnvironmentValue(
1455
+ requireEnvironmentEncryption(settings),
1456
+ payload.encryptedExternalContinuation,
1457
+ ),
1458
+ ),
1459
+ ),
1460
+ }
1461
+ : {}),
1273
1462
  ...(typeof payload.connectionId === "string" ? { connectionId: payload.connectionId } : {}),
1274
1463
  ...(typeof payload.connectionVersion === "number"
1275
1464
  ? { connectionVersion: payload.connectionVersion }
@@ -1280,14 +1469,7 @@ function readFikenOAuthState(raw: string | undefined, settings: Settings): Fiken
1280
1469
  }
1281
1470
 
1282
1471
  async function requireFikenCallbackGrant(db: Database, state: FikenOAuthState): Promise<void> {
1283
- const grant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
1284
- if (
1285
- !grant ||
1286
- grant.accountId !== state.accountId ||
1287
- !hasPermission(grant.permissions, "connections:write")
1288
- ) {
1289
- throw new FikenOAuthCallbackError("permission_lost");
1290
- }
1472
+ await requireConnectOwnerAuthority(db, state);
1291
1473
  }
1292
1474
 
1293
1475
  function fikenReturnUrl(