@opengeni/api-router 2.5.0 → 2.6.4-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.
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +206 -29
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/index.ts +13 -1
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +328 -27
- package/src/mcp/session-wait.ts +56 -8
- package/src/model-catalog.ts +45 -337
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/automations.ts +186 -19
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +47 -11
- package/src/routes/sessions.ts +75 -53
- package/src/routes/skills.ts +3 -3
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +665 -56
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
package/src/model-catalog.ts
CHANGED
|
@@ -1,62 +1,55 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
withCodexCatalogProvider,
|
|
5
|
-
withXaiSubscriptionCatalogProvider,
|
|
6
|
-
withWorkspaceGatewayCatalogProvider,
|
|
2
|
+
OPENROUTER_PROVIDER_ID,
|
|
3
|
+
WORKSPACE_OPENROUTER_PROVIDER_ID,
|
|
7
4
|
type ConfiguredModel,
|
|
8
|
-
type Settings,
|
|
9
5
|
} from "@opengeni/config";
|
|
10
6
|
import {
|
|
11
7
|
ClientModel,
|
|
12
8
|
WorkspaceModelCatalogResponse,
|
|
13
|
-
evaluateWorkspaceModelPolicy,
|
|
14
|
-
type ModelAvailabilityV1,
|
|
15
|
-
type ModelCredentialReadinessV1,
|
|
16
9
|
type WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponseType,
|
|
17
|
-
type WorkspaceModelPolicyContract,
|
|
18
10
|
} from "@opengeni/contracts";
|
|
11
|
+
import { resolveWorkspaceModelSelection, type WorkspaceModelSelectionInput } from "@opengeni/core";
|
|
19
12
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ModelCredentialReadinessObservation =
|
|
27
|
-
| { status: "ready"; checkedAt: string }
|
|
28
|
-
| {
|
|
29
|
-
status: "not_ready";
|
|
30
|
-
reason: "prerequisites_missing" | "needs_reauth";
|
|
31
|
-
checkedAt: string;
|
|
32
|
-
}
|
|
33
|
-
| { status: "error"; reason: "resolver_error"; checkedAt: string };
|
|
34
|
-
|
|
35
|
-
export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
|
|
13
|
+
export {
|
|
14
|
+
MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS,
|
|
15
|
+
type ModelAvailabilityObservation,
|
|
16
|
+
type ModelCredentialReadinessObservation,
|
|
17
|
+
} from "@opengeni/core";
|
|
36
18
|
|
|
37
19
|
/** Static, client-safe definition projection. No provider secret is reachable. */
|
|
38
20
|
export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
39
21
|
const anonymousProvider =
|
|
40
22
|
model.credentialSource.kind === "deployment" && model.credentialSource.mechanism === "none";
|
|
23
|
+
// Keep the established closed `source` enum compatible for older same-major
|
|
24
|
+
// clients. OpenRouter remains truthfully identified by its public provider
|
|
25
|
+
// id/label and billing metadata; omitting this optional legacy grouping field
|
|
26
|
+
// lets tolerant older contracts parse the additive provider.
|
|
41
27
|
const source =
|
|
42
|
-
model.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
model.providerId === OPENROUTER_PROVIDER_ID ||
|
|
29
|
+
model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
|
|
30
|
+
? undefined
|
|
31
|
+
: model.credentialSource.kind === "connected_subscription"
|
|
32
|
+
? model.credentialSource.provider === "xai"
|
|
33
|
+
? "supergrok"
|
|
34
|
+
: "codex"
|
|
35
|
+
: model.credentialSource.kind === "workspace_connection"
|
|
36
|
+
? "workspace_gateway"
|
|
37
|
+
: anonymousProvider
|
|
38
|
+
? undefined
|
|
39
|
+
: "opengeni";
|
|
51
40
|
const publicProvider = anonymousProvider
|
|
52
41
|
? { provider: model.providerId, providerLabel: model.providerLabel }
|
|
53
|
-
:
|
|
54
|
-
? { provider: "
|
|
55
|
-
:
|
|
56
|
-
? { provider: "
|
|
57
|
-
: source === "
|
|
58
|
-
? { provider: "
|
|
59
|
-
:
|
|
42
|
+
: model.providerId === OPENROUTER_PROVIDER_ID
|
|
43
|
+
? { provider: "openrouter", providerLabel: "OpenRouter" }
|
|
44
|
+
: model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
|
|
45
|
+
? { provider: "workspace-openrouter", providerLabel: "Your OpenRouter" }
|
|
46
|
+
: source === "codex"
|
|
47
|
+
? { provider: "codex", providerLabel: "Codex" }
|
|
48
|
+
: source === "supergrok"
|
|
49
|
+
? { provider: "supergrok", providerLabel: "SuperGrok" }
|
|
50
|
+
: source === "workspace_gateway"
|
|
51
|
+
? { provider: "workspace-gateway", providerLabel: "Your Gateway" }
|
|
52
|
+
: { provider: "opengeni", providerLabel: "OpenGeni" };
|
|
60
53
|
return ClientModel.parse({
|
|
61
54
|
id: model.id,
|
|
62
55
|
label: model.label,
|
|
@@ -71,243 +64,13 @@ export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
|
71
64
|
aliases: model.aliases,
|
|
72
65
|
executionLimits: model.executionLimits,
|
|
73
66
|
billing: model.billing,
|
|
67
|
+
cost: model.cost,
|
|
74
68
|
capabilities: model.capabilities,
|
|
75
69
|
...(model.pricing === undefined ? {} : { pricing: model.pricing }),
|
|
76
70
|
definitionVersion: model.definitionVersion,
|
|
77
71
|
});
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
function modelDefinitionRunnable(model: ConfiguredModel): boolean {
|
|
81
|
-
return (
|
|
82
|
-
model.capabilities.inputModalities.includes("text") &&
|
|
83
|
-
model.capabilities.outputModalities.includes("text") &&
|
|
84
|
-
model.capabilities.transports.sse.runnable
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function observedCredentialReadiness(input: {
|
|
89
|
-
observation: ModelCredentialReadinessObservation | undefined;
|
|
90
|
-
basis: "connection" | "resolver";
|
|
91
|
-
nowMs: number;
|
|
92
|
-
maxAgeMs: number;
|
|
93
|
-
}): ModelCredentialReadinessV1 {
|
|
94
|
-
if (!input.observation) {
|
|
95
|
-
return {
|
|
96
|
-
status: "not_ready",
|
|
97
|
-
reason: "prerequisites_missing",
|
|
98
|
-
basis: input.basis,
|
|
99
|
-
checkedAt: null,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const checkedAtMs = Date.parse(input.observation.checkedAt);
|
|
103
|
-
if (!Number.isFinite(checkedAtMs)) {
|
|
104
|
-
return {
|
|
105
|
-
status: "error",
|
|
106
|
-
reason: "resolver_error",
|
|
107
|
-
basis: input.basis,
|
|
108
|
-
checkedAt: null,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const checkedAt = new Date(checkedAtMs).toISOString();
|
|
112
|
-
if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
|
|
113
|
-
return {
|
|
114
|
-
status: "not_ready",
|
|
115
|
-
reason: "observation_stale",
|
|
116
|
-
basis: input.basis,
|
|
117
|
-
checkedAt,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (input.observation.status === "ready") {
|
|
121
|
-
return { status: "ready", reason: null, basis: input.basis, checkedAt };
|
|
122
|
-
}
|
|
123
|
-
if (input.observation.status === "not_ready") {
|
|
124
|
-
return {
|
|
125
|
-
status: "not_ready",
|
|
126
|
-
reason:
|
|
127
|
-
input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
|
|
128
|
-
basis: input.basis,
|
|
129
|
-
checkedAt,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
status: "error",
|
|
134
|
-
reason: "resolver_error",
|
|
135
|
-
basis: input.basis,
|
|
136
|
-
checkedAt,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function credentialReadinessFor(input: {
|
|
141
|
-
model: ConfiguredModel;
|
|
142
|
-
provider: ReturnType<typeof configuredProviders>[number] | undefined;
|
|
143
|
-
codexSubscriptionActive: boolean;
|
|
144
|
-
xaiSubscriptionActive: boolean;
|
|
145
|
-
workspaceGatewayConnectionActive: boolean;
|
|
146
|
-
observation: ModelCredentialReadinessObservation | undefined;
|
|
147
|
-
nowMs: number;
|
|
148
|
-
maxAgeMs: number;
|
|
149
|
-
}): ModelCredentialReadinessV1 {
|
|
150
|
-
const source = input.model.credentialSource;
|
|
151
|
-
if (source.kind === "connected_subscription") {
|
|
152
|
-
const active =
|
|
153
|
-
source.provider === "xai" ? input.xaiSubscriptionActive : input.codexSubscriptionActive;
|
|
154
|
-
return active
|
|
155
|
-
? { status: "ready", reason: null, basis: "connection", checkedAt: null }
|
|
156
|
-
: {
|
|
157
|
-
status: "not_ready",
|
|
158
|
-
reason: "needs_reauth",
|
|
159
|
-
basis: "connection",
|
|
160
|
-
checkedAt: null,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (source.kind === "workspace_connection") {
|
|
164
|
-
return input.workspaceGatewayConnectionActive
|
|
165
|
-
? { status: "ready", reason: null, basis: "connection", checkedAt: null }
|
|
166
|
-
: {
|
|
167
|
-
status: "not_ready",
|
|
168
|
-
reason: "needs_reauth",
|
|
169
|
-
basis: "connection",
|
|
170
|
-
checkedAt: null,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
if (source.kind === "deployment" && source.mechanism === "none") {
|
|
174
|
-
return { status: "ready", reason: null, basis: "configuration", checkedAt: null };
|
|
175
|
-
}
|
|
176
|
-
if (source.kind === "deployment" && source.mechanism === "api_key") {
|
|
177
|
-
return input.provider?.apiKey
|
|
178
|
-
? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
|
|
179
|
-
: {
|
|
180
|
-
status: "not_ready",
|
|
181
|
-
reason: "missing_credential",
|
|
182
|
-
basis: "configuration",
|
|
183
|
-
checkedAt: null,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return observedCredentialReadiness({
|
|
187
|
-
observation: input.observation,
|
|
188
|
-
basis: "resolver",
|
|
189
|
-
nowMs: input.nowMs,
|
|
190
|
-
maxAgeMs: input.maxAgeMs,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function isXaiGrokModel(model: ConfiguredModel): boolean {
|
|
195
|
-
return model.providerId === "xai" && model.id.startsWith("xai/grok-");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function observationTimestamp(observation: ModelAvailabilityObservation | undefined): {
|
|
199
|
-
checkedAt: string | null;
|
|
200
|
-
checkedAtMs: number | null;
|
|
201
|
-
} {
|
|
202
|
-
if (!observation || typeof observation.checkedAt !== "string") {
|
|
203
|
-
return { checkedAt: null, checkedAtMs: null };
|
|
204
|
-
}
|
|
205
|
-
const checkedAtMs = Date.parse(observation.checkedAt);
|
|
206
|
-
if (!Number.isFinite(checkedAtMs)) {
|
|
207
|
-
return { checkedAt: null, checkedAtMs: null };
|
|
208
|
-
}
|
|
209
|
-
return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function xaiGrokAvailabilityFor(input: {
|
|
213
|
-
observation: ModelAvailabilityObservation | undefined;
|
|
214
|
-
nowMs: number;
|
|
215
|
-
maxAgeMs: number;
|
|
216
|
-
}): ModelAvailabilityV1 {
|
|
217
|
-
const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
|
|
218
|
-
const freshSuccessfulObservation =
|
|
219
|
-
input.observation?.status === "available" &&
|
|
220
|
-
input.observation.reason === null &&
|
|
221
|
-
checkedAtMs !== null &&
|
|
222
|
-
checkedAtMs <= input.nowMs &&
|
|
223
|
-
input.nowMs - checkedAtMs <= input.maxAgeMs;
|
|
224
|
-
|
|
225
|
-
if (freshSuccessfulObservation) {
|
|
226
|
-
return {
|
|
227
|
-
status: "available",
|
|
228
|
-
selectable: true,
|
|
229
|
-
reason: null,
|
|
230
|
-
checkedAt,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
status: "unavailable",
|
|
236
|
-
selectable: false,
|
|
237
|
-
reason:
|
|
238
|
-
input.observation?.status === "unavailable"
|
|
239
|
-
? (input.observation.reason ?? "provider_unhealthy")
|
|
240
|
-
: "provider_unhealthy",
|
|
241
|
-
checkedAt,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function availabilityFor(input: {
|
|
246
|
-
model: ConfiguredModel;
|
|
247
|
-
credentialReadiness: ModelCredentialReadinessV1;
|
|
248
|
-
policyAllowed: boolean;
|
|
249
|
-
observation?: ModelAvailabilityObservation | undefined;
|
|
250
|
-
nowMs: number;
|
|
251
|
-
maxAgeMs: number;
|
|
252
|
-
}): ModelAvailabilityV1 {
|
|
253
|
-
if (!modelDefinitionRunnable(input.model)) {
|
|
254
|
-
return {
|
|
255
|
-
status: "unavailable",
|
|
256
|
-
selectable: false,
|
|
257
|
-
reason: "unsupported",
|
|
258
|
-
checkedAt: null,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (input.credentialReadiness.status !== "ready") {
|
|
262
|
-
return {
|
|
263
|
-
status: "unavailable",
|
|
264
|
-
selectable: false,
|
|
265
|
-
reason:
|
|
266
|
-
input.credentialReadiness.reason === "missing_credential"
|
|
267
|
-
? "missing_credential"
|
|
268
|
-
: input.credentialReadiness.reason === "needs_reauth"
|
|
269
|
-
? "needs_reauth"
|
|
270
|
-
: "credential_not_ready",
|
|
271
|
-
checkedAt: input.credentialReadiness.checkedAt,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
if (!input.policyAllowed) {
|
|
275
|
-
return {
|
|
276
|
-
status: "unavailable",
|
|
277
|
-
selectable: false,
|
|
278
|
-
reason: "policy_blocked",
|
|
279
|
-
checkedAt: null,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
if (isXaiGrokModel(input.model)) {
|
|
283
|
-
return xaiGrokAvailabilityFor({
|
|
284
|
-
observation: input.observation,
|
|
285
|
-
nowMs: input.nowMs,
|
|
286
|
-
maxAgeMs: input.maxAgeMs,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
if (!input.observation) {
|
|
290
|
-
// Credential readiness and policy are known-good, but no current
|
|
291
|
-
// provider-health observation is available. Unknown is intentionally
|
|
292
|
-
// selectable; the execution boundary rechecks all authoritative gates.
|
|
293
|
-
return { status: "unknown", selectable: true, reason: null, checkedAt: null };
|
|
294
|
-
}
|
|
295
|
-
if (input.observation.status === "unavailable") {
|
|
296
|
-
return {
|
|
297
|
-
status: "unavailable",
|
|
298
|
-
selectable: false,
|
|
299
|
-
reason: input.observation.reason ?? "provider_unhealthy",
|
|
300
|
-
checkedAt: input.observation.checkedAt,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
return {
|
|
304
|
-
status: input.observation.status,
|
|
305
|
-
selectable: true,
|
|
306
|
-
reason: null,
|
|
307
|
-
checkedAt: input.observation.checkedAt,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
74
|
/**
|
|
312
75
|
* Authenticated workspace catalog. Codex definitions are included only when
|
|
313
76
|
* the deployment enables that connection type; concrete readiness is supplied
|
|
@@ -316,69 +79,14 @@ function availabilityFor(input: {
|
|
|
316
79
|
* successful typed resolver observation; credential health and provider health
|
|
317
80
|
* are separate inputs and neither is fabricated.
|
|
318
81
|
*/
|
|
319
|
-
export function buildWorkspaceModelCatalog(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
|
|
329
|
-
now?: Date | undefined;
|
|
330
|
-
credentialReadinessMaxAgeMs?: number | undefined;
|
|
331
|
-
}): WorkspaceModelCatalogResponseType {
|
|
332
|
-
const codexSettings = input.settings.codexSubscriptionEnabled
|
|
333
|
-
? withCodexCatalogProvider(input.settings)
|
|
334
|
-
: input.settings;
|
|
335
|
-
const xaiSettings = input.settings.supergrokSubscriptionEnabled
|
|
336
|
-
? withXaiSubscriptionCatalogProvider(codexSettings)
|
|
337
|
-
: codexSettings;
|
|
338
|
-
const catalogSettings = withWorkspaceGatewayCatalogProvider(xaiSettings);
|
|
339
|
-
const providers = new Map(
|
|
340
|
-
configuredProviders(catalogSettings).map((provider) => [provider.id, provider]),
|
|
341
|
-
);
|
|
342
|
-
const requestedNowMs = input.now?.getTime();
|
|
343
|
-
const nowMs =
|
|
344
|
-
typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs)
|
|
345
|
-
? requestedNowMs
|
|
346
|
-
: Date.now();
|
|
347
|
-
const maxAgeMs =
|
|
348
|
-
typeof input.credentialReadinessMaxAgeMs === "number" &&
|
|
349
|
-
Number.isFinite(input.credentialReadinessMaxAgeMs) &&
|
|
350
|
-
input.credentialReadinessMaxAgeMs >= 0
|
|
351
|
-
? input.credentialReadinessMaxAgeMs
|
|
352
|
-
: MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
|
|
353
|
-
const models = configuredModels(catalogSettings).map((model) => {
|
|
354
|
-
const provider = providers.get(model.providerId);
|
|
355
|
-
const policyAllowed = evaluateWorkspaceModelPolicy(input.policy, {
|
|
356
|
-
providerId: model.providerId,
|
|
357
|
-
modelId: model.id,
|
|
358
|
-
}).allowed;
|
|
359
|
-
const credentialReadiness = credentialReadinessFor({
|
|
360
|
-
model,
|
|
361
|
-
provider,
|
|
362
|
-
codexSubscriptionActive: input.codexSubscriptionActive,
|
|
363
|
-
xaiSubscriptionActive: input.xaiSubscriptionActive === true,
|
|
364
|
-
workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
|
|
365
|
-
observation: input.credentialReadinessObservations?.[model.definitionVersion],
|
|
366
|
-
nowMs,
|
|
367
|
-
maxAgeMs,
|
|
368
|
-
});
|
|
369
|
-
return {
|
|
370
|
-
...projectClientModel(model),
|
|
371
|
-
credentialReadiness,
|
|
372
|
-
policyAllowed,
|
|
373
|
-
availability: availabilityFor({
|
|
374
|
-
model,
|
|
375
|
-
credentialReadiness,
|
|
376
|
-
policyAllowed,
|
|
377
|
-
observation: input.observations?.[model.definitionVersion],
|
|
378
|
-
nowMs,
|
|
379
|
-
maxAgeMs,
|
|
380
|
-
}),
|
|
381
|
-
};
|
|
382
|
-
});
|
|
82
|
+
export function buildWorkspaceModelCatalog(
|
|
83
|
+
input: WorkspaceModelSelectionInput,
|
|
84
|
+
): WorkspaceModelCatalogResponseType {
|
|
85
|
+
const models = resolveWorkspaceModelSelection(input).map((selection) => ({
|
|
86
|
+
...projectClientModel(selection.model),
|
|
87
|
+
credentialReadiness: selection.credentialReadiness,
|
|
88
|
+
policyAllowed: selection.policyAllowed,
|
|
89
|
+
availability: selection.availability,
|
|
90
|
+
}));
|
|
383
91
|
return WorkspaceModelCatalogResponse.parse({ models });
|
|
384
92
|
}
|
|
@@ -148,7 +148,7 @@ export function registerApiIntegrationRoutes(
|
|
|
148
148
|
|
|
149
149
|
app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
|
|
150
150
|
const workspaceId = c.req.param("workspaceId");
|
|
151
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
151
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
152
152
|
const payload = InstallApiIntegrationRequest.parse(await c.req.json());
|
|
153
153
|
const resolved = await resolveForRoute({
|
|
154
154
|
deps,
|
|
@@ -261,7 +261,7 @@ export function registerApiIntegrationRoutes(
|
|
|
261
261
|
"/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
|
|
262
262
|
async (c) => {
|
|
263
263
|
const workspaceId = c.req.param("workspaceId");
|
|
264
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
264
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
265
265
|
const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
|
|
266
266
|
const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
|
|
267
267
|
const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
|
package/src/routes/api-keys.ts
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CreateApiKeyRequest,
|
|
3
|
+
CreateApiKeyResponse,
|
|
4
|
+
CreateOrganizationApiKeyRequest,
|
|
5
|
+
Permission,
|
|
6
|
+
type AccessContext,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
createApiKey,
|
|
10
|
+
createOrganizationApiKey as createOrganizationApiKeyRecord,
|
|
11
|
+
listApiKeys,
|
|
12
|
+
listOrganizationApiKeys,
|
|
13
|
+
OrganizationApiKeyLimitExceededError,
|
|
14
|
+
revokeApiKey,
|
|
15
|
+
revokeOrganizationApiKey,
|
|
16
|
+
} from "@opengeni/db";
|
|
17
|
+
import { configuredStaticUsageLimits } from "@opengeni/config";
|
|
3
18
|
import { zValidator } from "@hono/zod-validator";
|
|
4
19
|
import type { Hono } from "hono";
|
|
5
20
|
import { HTTPException } from "hono/http-exception";
|
|
6
21
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
7
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
accountScopedApiKeyWorkspaceAuthority,
|
|
24
|
+
requireAccessContext,
|
|
25
|
+
requireAccessGrant,
|
|
26
|
+
requireAccessGrantAuthorization,
|
|
27
|
+
type AccessGrantAuthorization,
|
|
28
|
+
} from "@opengeni/core";
|
|
8
29
|
import { requireLimit } from "@opengeni/core";
|
|
9
30
|
|
|
31
|
+
export const organizationApiKeyPermissions: Permission[] = [
|
|
32
|
+
"account:read",
|
|
33
|
+
"workspace:create",
|
|
34
|
+
"workspace:read",
|
|
35
|
+
"workspace:admin",
|
|
36
|
+
"api_keys:manage",
|
|
37
|
+
];
|
|
38
|
+
|
|
10
39
|
export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
11
40
|
app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
|
|
12
41
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -19,11 +48,17 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
19
48
|
zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
|
|
20
49
|
async (c) => {
|
|
21
50
|
const workspaceId = c.req.param("workspaceId");
|
|
22
|
-
const
|
|
51
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
52
|
+
c,
|
|
53
|
+
deps,
|
|
54
|
+
workspaceId,
|
|
55
|
+
"api_keys:manage",
|
|
56
|
+
);
|
|
57
|
+
const grant = authorization.grant;
|
|
23
58
|
const body = c.req.valid("json");
|
|
24
59
|
const permissions: Permission[] =
|
|
25
60
|
body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
|
|
26
|
-
ensureDelegablePermissions(
|
|
61
|
+
ensureDelegablePermissions(authorization, permissions);
|
|
27
62
|
await requireLimit(deps, {
|
|
28
63
|
accountId: grant.accountId,
|
|
29
64
|
workspaceId,
|
|
@@ -51,12 +86,90 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
51
86
|
await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
|
|
52
87
|
return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
|
|
53
88
|
});
|
|
89
|
+
|
|
90
|
+
app.get("/v1/organizations/:organizationId/api-keys", async (c) => {
|
|
91
|
+
const organizationId = c.req.param("organizationId");
|
|
92
|
+
const context = await requireAccessContext(c, deps);
|
|
93
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
94
|
+
return c.json({ apiKeys: await listOrganizationApiKeys(deps.db, organizationId) });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post(
|
|
98
|
+
"/v1/organizations/:organizationId/api-keys",
|
|
99
|
+
zValidator("json", CreateOrganizationApiKeyRequest),
|
|
100
|
+
async (c) => {
|
|
101
|
+
const organizationId = c.req.param("organizationId");
|
|
102
|
+
const context = await requireAccessContext(c, deps);
|
|
103
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
104
|
+
const body = c.req.valid("json");
|
|
105
|
+
const token = generateApiKeyToken();
|
|
106
|
+
try {
|
|
107
|
+
const apiKey = await createOrganizationApiKeyRecord(deps.db, {
|
|
108
|
+
accountId: organizationId,
|
|
109
|
+
name: body.name,
|
|
110
|
+
description: body.description ?? null,
|
|
111
|
+
prefix: token.slice(0, 14),
|
|
112
|
+
keyHash: await sha256Hex(token),
|
|
113
|
+
permissions: organizationApiKeyPermissions,
|
|
114
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
|
115
|
+
maxActiveKeys: organizationApiKeyLimit(deps),
|
|
116
|
+
rotationSourceApiKeyId: authenticatedApiKeyId(context),
|
|
117
|
+
});
|
|
118
|
+
return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof OrganizationApiKeyLimitExceededError) {
|
|
121
|
+
throw new HTTPException(429, { message: error.message });
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
app.delete("/v1/organizations/:organizationId/api-keys/:apiKeyId", async (c) => {
|
|
129
|
+
const organizationId = c.req.param("organizationId");
|
|
130
|
+
const context = await requireAccessContext(c, deps);
|
|
131
|
+
requireOrganizationApiKeyControlPermission(context, organizationId);
|
|
132
|
+
const apiKey = await revokeOrganizationApiKey(deps.db, organizationId, c.req.param("apiKeyId"));
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
throw new HTTPException(404, { message: "API key not found" });
|
|
135
|
+
}
|
|
136
|
+
return c.json(apiKey);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireAccountPermission(
|
|
141
|
+
context: AccessContext,
|
|
142
|
+
accountId: string,
|
|
143
|
+
permission: Permission,
|
|
144
|
+
): void {
|
|
145
|
+
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
146
|
+
if (
|
|
147
|
+
!grant ||
|
|
148
|
+
(!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
|
|
149
|
+
) {
|
|
150
|
+
throw new HTTPException(403, { message: `missing permission: ${permission}` });
|
|
151
|
+
}
|
|
54
152
|
}
|
|
55
153
|
|
|
56
|
-
function ensureDelegablePermissions(
|
|
154
|
+
function ensureDelegablePermissions(
|
|
155
|
+
authorization: AccessGrantAuthorization,
|
|
156
|
+
requested: Permission[],
|
|
157
|
+
): void {
|
|
158
|
+
const grantPermissions = authorization.grant.permissions;
|
|
57
159
|
if (grantPermissions.includes("workspace:admin")) {
|
|
160
|
+
const accountLiteralPermissions = new Set<Permission>([
|
|
161
|
+
"account:read",
|
|
162
|
+
"account:admin",
|
|
163
|
+
"workspace:create",
|
|
164
|
+
"billing:read",
|
|
165
|
+
"billing:manage",
|
|
166
|
+
]);
|
|
167
|
+
const workspaceLiteralPermissions = new Set<Permission>(["members:manage", "secrets:read"]);
|
|
58
168
|
const highTrustMissing = requested.filter(
|
|
59
|
-
(permission) =>
|
|
169
|
+
(permission) =>
|
|
170
|
+
(accountLiteralPermissions.has(permission) &&
|
|
171
|
+
!authorization.accountGrant?.permissions.includes(permission)) ||
|
|
172
|
+
(workspaceLiteralPermissions.has(permission) && !grantPermissions.includes(permission)),
|
|
60
173
|
);
|
|
61
174
|
if (highTrustMissing.length === 0) return;
|
|
62
175
|
throw new HTTPException(403, {
|
|
@@ -71,6 +184,35 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
|
|
|
71
184
|
}
|
|
72
185
|
}
|
|
73
186
|
|
|
187
|
+
function requireOrganizationApiKeyControlPermission(
|
|
188
|
+
context: AccessContext,
|
|
189
|
+
organizationId: string,
|
|
190
|
+
): void {
|
|
191
|
+
requireAccountPermission(context, organizationId, "api_keys:manage");
|
|
192
|
+
if (!context.subjectId.startsWith("api_key:")) return;
|
|
193
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
194
|
+
if (
|
|
195
|
+
!authority ||
|
|
196
|
+
authority.accountId !== organizationId ||
|
|
197
|
+
!authority.permissions.includes("api_keys:manage")
|
|
198
|
+
) {
|
|
199
|
+
throw new HTTPException(403, { message: "organization API key authority required" });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function authenticatedApiKeyId(context: AccessContext): string | null {
|
|
204
|
+
return context.subjectId.startsWith("api_key:")
|
|
205
|
+
? context.subjectId.slice("api_key:".length)
|
|
206
|
+
: null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function organizationApiKeyLimit(deps: ApiRouteDeps): number | null {
|
|
210
|
+
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return configuredStaticUsageLimits(deps.settings).maxApiKeysPerWorkspace ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
74
216
|
function generateApiKeyToken(): string {
|
|
75
217
|
const bytes = new Uint8Array(32);
|
|
76
218
|
crypto.getRandomValues(bytes);
|