@opengeni/api-router 0.12.12 → 0.14.4

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 (87) hide show
  1. package/dist/app.d.ts +18 -38
  2. package/dist/app.js +3 -1
  3. package/dist/auth/managed-auth.d.ts +35 -0
  4. package/dist/{chunk-5TULDPWX.js → chunk-36PW33ND.js} +4111 -1277
  5. package/dist/chunk-36PW33ND.js.map +1 -0
  6. package/dist/codex-redemption-security.d.ts +15 -0
  7. package/dist/github-access.d.ts +12 -0
  8. package/dist/github-browser-flow.d.ts +14 -0
  9. package/dist/http/auth.d.ts +3 -0
  10. package/dist/http/common.d.ts +3 -0
  11. package/dist/http/sse.d.ts +70 -0
  12. package/dist/index.d.ts +11 -13
  13. package/dist/index.js +1 -1
  14. package/dist/integrations/google-drive.d.ts +76 -0
  15. package/dist/integrations/oauth-client.d.ts +42 -0
  16. package/dist/integrations/provider-domain.d.ts +7 -0
  17. package/dist/integrations/slack-bot.d.ts +343 -0
  18. package/dist/mcp/documents.d.ts +8 -0
  19. package/dist/mcp/files.d.ts +5 -0
  20. package/dist/mcp/server.d.ts +107 -0
  21. package/dist/mcp/session-view.d.ts +191 -0
  22. package/dist/mcp/toolspace.d.ts +61 -0
  23. package/dist/model-catalog.d.ts +39 -0
  24. package/dist/observability.d.ts +3 -0
  25. package/dist/routes/api-keys.d.ts +3 -0
  26. package/dist/routes/billing.d.ts +16 -0
  27. package/dist/routes/capabilities.d.ts +3 -0
  28. package/dist/routes/catalog-assets.d.ts +5 -0
  29. package/dist/routes/codex.d.ts +10 -0
  30. package/dist/routes/connections.d.ts +3 -0
  31. package/dist/routes/documents.d.ts +3 -0
  32. package/dist/routes/enrollments.d.ts +3 -0
  33. package/dist/routes/environments.d.ts +5 -0
  34. package/dist/routes/files.d.ts +4 -0
  35. package/dist/routes/github.d.ts +3 -0
  36. package/dist/routes/insights.d.ts +3 -0
  37. package/dist/routes/install.d.ts +5 -0
  38. package/dist/routes/machines.d.ts +3 -0
  39. package/dist/routes/packs.d.ts +3 -0
  40. package/dist/routes/preference-registry.d.ts +5 -0
  41. package/dist/routes/rigs.d.ts +3 -0
  42. package/dist/routes/scheduled-tasks.d.ts +3 -0
  43. package/dist/routes/sessions.d.ts +15 -0
  44. package/dist/routes/social.d.ts +3 -0
  45. package/dist/routes/transcriptions.d.ts +3 -0
  46. package/dist/routes/workspace-capture.d.ts +59 -0
  47. package/dist/routes/workspace-instruction-policies.d.ts +3 -0
  48. package/dist/routes/workspace-state.d.ts +3 -0
  49. package/dist/routes/workspaces.d.ts +5 -0
  50. package/dist/sandbox/access.d.ts +21 -0
  51. package/dist/sandbox/auth-callout.d.ts +30 -0
  52. package/dist/sandbox/channel-a.d.ts +37 -0
  53. package/dist/sandbox/enrollment.d.ts +120 -0
  54. package/dist/sandbox/machines.d.ts +29 -0
  55. package/dist/sandbox/metrics-ingestion.d.ts +128 -0
  56. package/dist/sandbox/rematerialize.d.ts +24 -0
  57. package/dist/sandbox/viewer.d.ts +251 -0
  58. package/dist/transcription/providers/azure-openai.d.ts +9 -0
  59. package/dist/transcription/providers/codex-subscription.d.ts +9 -0
  60. package/dist/transcription/providers/openai.d.ts +9 -0
  61. package/dist/transcription/service.d.ts +10 -0
  62. package/dist/workspace-state-projection.d.ts +16 -0
  63. package/package.json +13 -13
  64. package/src/app.ts +67 -5
  65. package/src/integrations/google-drive.ts +869 -0
  66. package/src/integrations/oauth-client.ts +41 -6
  67. package/src/integrations/slack-bot.ts +756 -26
  68. package/src/mcp/server.ts +200 -3
  69. package/src/mcp/session-view.ts +1 -0
  70. package/src/mcp/toolspace.ts +17 -9
  71. package/src/routes/connections.ts +137 -7
  72. package/src/routes/documents.ts +13 -2
  73. package/src/routes/github.ts +7 -5
  74. package/src/routes/insights.ts +30 -0
  75. package/src/routes/preference-registry.ts +482 -0
  76. package/src/routes/sessions.ts +10 -0
  77. package/src/routes/transcriptions.ts +155 -0
  78. package/src/routes/workspace-state.ts +61 -0
  79. package/src/sandbox/channel-a.ts +2 -0
  80. package/src/sandbox/rematerialize.ts +111 -4
  81. package/src/sandbox/viewer.ts +29 -12
  82. package/src/transcription/providers/azure-openai.ts +45 -0
  83. package/src/transcription/providers/codex-subscription.ts +100 -0
  84. package/src/transcription/providers/openai.ts +93 -0
  85. package/src/transcription/service.ts +121 -0
  86. package/src/workspace-state-projection.ts +244 -0
  87. package/dist/chunk-5TULDPWX.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ import { InsightsRange, WorkspaceInsightsResponse } from "@opengeni/contracts";
2
+ import { getWorkspaceInsights, requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
3
+ import type { Hono } from "hono";
4
+ import { HTTPException } from "hono/http-exception";
5
+
6
+ export function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void {
7
+ app.get("/v1/workspaces/:workspaceId/insights", async (c) => {
8
+ const workspaceId = c.req.param("workspaceId");
9
+ await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
10
+
11
+ const rangeRaw = c.req.query("range") ?? "week";
12
+ const rangeParsed = InsightsRange.safeParse(rangeRaw);
13
+ if (!rangeParsed.success) {
14
+ throw new HTTPException(400, {
15
+ message: "range must be one of today|week|month|ytd",
16
+ });
17
+ }
18
+ const provider = c.req.query("provider");
19
+ const model = c.req.query("model");
20
+
21
+ const response = await getWorkspaceInsights(deps.db, deps.settings, {
22
+ workspaceId,
23
+ range: rangeParsed.data,
24
+ provider: provider && provider !== "all" ? provider : null,
25
+ model: model && model !== "all" ? model : null,
26
+ });
27
+ c.header("cache-control", "private, no-store");
28
+ return c.json(WorkspaceInsightsResponse.parse(response));
29
+ });
30
+ }
@@ -0,0 +1,482 @@
1
+ import {
2
+ ActivatePreferenceRegistryRevisionRequest,
3
+ ChangePreferenceRegistryScopeRequest,
4
+ CorrectPreferenceRegistryRequest,
5
+ CreatePreferenceRegistryProposalRequest,
6
+ DeactivatePreferenceRegistryRequest,
7
+ PreferenceRegistryConflictResponse,
8
+ PreferenceRegistryDetailResponse,
9
+ PreferenceRegistryFullContentRequest,
10
+ PreferenceRegistryFullContent,
11
+ PreferenceRegistryListQuery,
12
+ PreferenceRegistryListResponse,
13
+ PreferenceRegistryMutationResponse,
14
+ PreferenceRegistryRecord,
15
+ PreferenceRegistrySnapshot,
16
+ RejectPreferenceRegistryProposalRequest,
17
+ SupersedePreferenceRegistryRequest,
18
+ type AccessGrant,
19
+ type PreferenceRegistryScope,
20
+ } from "@opengeni/contracts";
21
+ import {
22
+ hasPermission,
23
+ requireAccessGrant,
24
+ requireAccessGrantAuthorization,
25
+ type AccessGrantAuthorization,
26
+ type ApiRouteDeps,
27
+ } from "@opengeni/core";
28
+ import {
29
+ activatePreferenceRegistryRevision,
30
+ changePreferenceRegistryScope,
31
+ correctPreferenceRegistry,
32
+ createPreferenceRegistryProposal,
33
+ deactivatePreferenceRegistry,
34
+ getOrCreatePreferenceRegistrySnapshot,
35
+ getPreferenceRegistryDetail,
36
+ getPreferenceRegistryDetailForAttempt,
37
+ getPreferenceRegistryFullContent,
38
+ listPreferenceRegistry,
39
+ listPreferenceRegistryForAttempt,
40
+ PreferenceRegistryConflictError,
41
+ PreferenceRegistryInitiatorError,
42
+ PreferenceRegistryInvalidOperationError,
43
+ PreferenceRegistryNotFoundError,
44
+ PreferenceRegistryStableKeyConflictError,
45
+ rejectPreferenceRegistryProposal,
46
+ supersedePreferenceRegistry,
47
+ type PreferenceRegistryAttemptClaims,
48
+ } from "@opengeni/db";
49
+ import type { Context, Hono } from "hono";
50
+ import { HTTPException } from "hono/http-exception";
51
+ import { z } from "zod";
52
+
53
+ const Id = z.string().uuid();
54
+
55
+ async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
56
+ const parsed = schema.safeParse(await context.req.json().catch(() => null));
57
+ if (!parsed.success)
58
+ throw new HTTPException(422, { message: "Invalid preference registry request" });
59
+ return parsed.data;
60
+ }
61
+
62
+ function exactAttemptClaims(grant: AccessGrant) {
63
+ const metadata = grant.metadata ?? {};
64
+ const values = {
65
+ sessionId: metadata["sessionId"],
66
+ turnId: metadata["turnId"],
67
+ attemptId: metadata["attemptId"],
68
+ executionGeneration: metadata["executionGeneration"],
69
+ };
70
+ const present = Object.values(values).filter((value) => value !== undefined).length;
71
+ if (present === 0) return null;
72
+ if (
73
+ typeof values.sessionId !== "string" ||
74
+ typeof values.turnId !== "string" ||
75
+ typeof values.attemptId !== "string" ||
76
+ typeof values.executionGeneration !== "number" ||
77
+ !Number.isSafeInteger(values.executionGeneration) ||
78
+ values.executionGeneration < 1 ||
79
+ values.executionGeneration > 2_147_483_647
80
+ ) {
81
+ throw new HTTPException(403, { message: "Incomplete signed preference attempt authority" });
82
+ }
83
+ return values as {
84
+ sessionId: string;
85
+ turnId: string;
86
+ attemptId: string;
87
+ executionGeneration: number;
88
+ };
89
+ }
90
+
91
+ function requiredAttemptClaims(grant: AccessGrant): PreferenceRegistryAttemptClaims {
92
+ const claims = exactAttemptClaims(grant);
93
+ if (!claims) {
94
+ throw new HTTPException(403, {
95
+ message: "Preference snapshot retrieval requires a signed session, turn, and attempt",
96
+ });
97
+ }
98
+ return {
99
+ accountId: grant.accountId,
100
+ workspaceId: grant.workspaceId,
101
+ ...claims,
102
+ };
103
+ }
104
+
105
+ function requireHumanMutation(access: AccessGrantAuthorization): void {
106
+ const { grant } = access;
107
+ if (
108
+ !access.contextIntegrity ||
109
+ access.authenticatedSubjectId !== grant.subjectId ||
110
+ grant.principalKind !== "human_session" ||
111
+ exactAttemptClaims(grant) !== null ||
112
+ grant.serviceInitiator ||
113
+ grant.serviceInitiatorContext ||
114
+ grant.subjectId.startsWith("api_key:")
115
+ ) {
116
+ throw new HTTPException(403, {
117
+ message: "Preference activation and scope mutation require a direct human-authorized request",
118
+ });
119
+ }
120
+ }
121
+
122
+ export function authorizePreferenceRegistryScopeMutation(
123
+ access: AccessGrantAuthorization,
124
+ scope: PreferenceRegistryScope,
125
+ ): void {
126
+ const { grant } = access;
127
+ requireHumanMutation(access);
128
+ if (scope === "organization") {
129
+ // Deliberately do not use hasPermission: workspace:admin must not expand to
130
+ // account:admin for an organization-wide preference. Account authority is
131
+ // carried separately from the authenticated context and must match this
132
+ // exact workspace grant's account and subject.
133
+ if (!access.accountGrant?.permissions.includes("account:admin")) {
134
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
135
+ }
136
+ return;
137
+ }
138
+ if (scope === "workspace" && !hasPermission(grant.permissions, "workspace:admin")) {
139
+ throw new HTTPException(403, { message: "missing permission: workspace:admin" });
140
+ }
141
+ // User scope is always self-only: the route derives the target from the
142
+ // authenticated actor and never accepts a subject selector.
143
+ }
144
+
145
+ function preferenceError(context: Context, error: unknown): Response {
146
+ if (error instanceof PreferenceRegistryConflictError) {
147
+ return context.json(
148
+ PreferenceRegistryConflictResponse.parse({
149
+ code: error.code,
150
+ message: error.message,
151
+ currentRevisionId: error.currentRevisionId,
152
+ scopeVersion: error.scopeVersion,
153
+ }),
154
+ 409,
155
+ );
156
+ }
157
+ if (error instanceof PreferenceRegistryNotFoundError) {
158
+ return context.json({ code: "PREFERENCE_REGISTRY_NOT_FOUND", message: error.message }, 404);
159
+ }
160
+ if (error instanceof PreferenceRegistryStableKeyConflictError) {
161
+ return context.json({ code: error.code, message: error.message }, 409);
162
+ }
163
+ if (error instanceof PreferenceRegistryInvalidOperationError) {
164
+ return context.json(
165
+ { code: "INVALID_PREFERENCE_REGISTRY_OPERATION", message: error.message },
166
+ 422,
167
+ );
168
+ }
169
+ if (error instanceof PreferenceRegistryInitiatorError) {
170
+ return context.json(
171
+ { code: "PREFERENCE_REGISTRY_ATTEMPT_REJECTED", message: error.message },
172
+ 403,
173
+ );
174
+ }
175
+ throw error;
176
+ }
177
+
178
+ function preferenceId(context: Context): string {
179
+ const parsed = Id.safeParse(context.req.param("preferenceId"));
180
+ if (!parsed.success) throw new HTTPException(422, { message: "Invalid preference id" });
181
+ return parsed.data;
182
+ }
183
+
184
+ export function registerPreferenceRegistryRoutes(app: Hono, deps: ApiRouteDeps): void {
185
+ const base = "/v1/workspaces/:workspaceId/preferences";
186
+
187
+ app.get(base, async (context) => {
188
+ const workspaceId = context.req.param("workspaceId");
189
+ const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
190
+ const query = PreferenceRegistryListQuery.safeParse({
191
+ scope: context.req.query("scope"),
192
+ status: context.req.query("status"),
193
+ limit: context.req.query("limit"),
194
+ });
195
+ if (!query.success)
196
+ throw new HTTPException(422, { message: "Invalid preference registry query" });
197
+ try {
198
+ const claims = exactAttemptClaims(grant);
199
+ const result = claims
200
+ ? await listPreferenceRegistryForAttempt(deps.db, {
201
+ accountId: grant.accountId,
202
+ workspaceId,
203
+ ...claims,
204
+ ...query.data,
205
+ })
206
+ : await listPreferenceRegistry(deps.db, {
207
+ workspaceId,
208
+ subjectId: grant.subjectId,
209
+ ...query.data,
210
+ });
211
+ return context.json(PreferenceRegistryListResponse.parse(result));
212
+ } catch (error) {
213
+ return preferenceError(context, error);
214
+ }
215
+ });
216
+
217
+ app.post(`${base}/proposals`, async (context) => {
218
+ const workspaceId = context.req.param("workspaceId");
219
+ const access = await requireAccessGrantAuthorization(
220
+ context,
221
+ deps,
222
+ workspaceId,
223
+ "workspace:read",
224
+ );
225
+ const { grant } = access;
226
+ const request = await parseBody(context, CreatePreferenceRegistryProposalRequest);
227
+ authorizePreferenceRegistryScopeMutation(access, request.scope);
228
+ try {
229
+ return context.json(
230
+ PreferenceRegistryRecord.parse(
231
+ await createPreferenceRegistryProposal(deps.db, {
232
+ ...request,
233
+ accountId: grant.accountId,
234
+ workspaceId,
235
+ actorSubjectId: grant.subjectId,
236
+ principalKind: grant.principalKind,
237
+ }),
238
+ ),
239
+ 201,
240
+ );
241
+ } catch (error) {
242
+ return preferenceError(context, error);
243
+ }
244
+ });
245
+
246
+ app.get(`${base}/summary`, async (context) => {
247
+ const workspaceId = context.req.param("workspaceId");
248
+ const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
249
+ try {
250
+ return context.json(
251
+ PreferenceRegistrySnapshot.parse(
252
+ await getOrCreatePreferenceRegistrySnapshot(deps.db, requiredAttemptClaims(grant)),
253
+ ),
254
+ );
255
+ } catch (error) {
256
+ return preferenceError(context, error);
257
+ }
258
+ });
259
+
260
+ app.post(`${base}/full-content`, async (context) => {
261
+ const workspaceId = context.req.param("workspaceId");
262
+ const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
263
+ const request = await parseBody(context, PreferenceRegistryFullContentRequest);
264
+ try {
265
+ return context.json(
266
+ PreferenceRegistryFullContent.parse(
267
+ await getPreferenceRegistryFullContent(
268
+ deps.db,
269
+ requiredAttemptClaims(grant),
270
+ request.retrievalHandle,
271
+ ),
272
+ ),
273
+ );
274
+ } catch (error) {
275
+ return preferenceError(context, error);
276
+ }
277
+ });
278
+
279
+ app.get(`${base}/:preferenceId`, async (context) => {
280
+ const workspaceId = context.req.param("workspaceId");
281
+ const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
282
+ try {
283
+ const claims = exactAttemptClaims(grant);
284
+ const id = preferenceId(context);
285
+ const detail = claims
286
+ ? await getPreferenceRegistryDetailForAttempt(deps.db, {
287
+ accountId: grant.accountId,
288
+ workspaceId,
289
+ ...claims,
290
+ preferenceId: id,
291
+ })
292
+ : await getPreferenceRegistryDetail(deps.db, {
293
+ workspaceId,
294
+ subjectId: grant.subjectId,
295
+ preferenceId: id,
296
+ });
297
+ return context.json(PreferenceRegistryDetailResponse.parse(detail));
298
+ } catch (error) {
299
+ return preferenceError(context, error);
300
+ }
301
+ });
302
+
303
+ app.post(`${base}/:preferenceId/activate`, async (context) => {
304
+ const workspaceId = context.req.param("workspaceId");
305
+ const access = await requireAccessGrantAuthorization(
306
+ context,
307
+ deps,
308
+ workspaceId,
309
+ "workspace:read",
310
+ );
311
+ const { grant } = access;
312
+ const id = preferenceId(context);
313
+ const request = await parseBody(context, ActivatePreferenceRegistryRevisionRequest);
314
+ try {
315
+ return context.json(
316
+ PreferenceRegistryMutationResponse.parse(
317
+ await activatePreferenceRegistryRevision(deps.db, {
318
+ accountId: grant.accountId,
319
+ workspaceId,
320
+ actorSubjectId: grant.subjectId,
321
+ principalKind: grant.principalKind,
322
+ preferenceId: id,
323
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
324
+ ...request,
325
+ }),
326
+ ),
327
+ );
328
+ } catch (error) {
329
+ return preferenceError(context, error);
330
+ }
331
+ });
332
+
333
+ app.post(`${base}/:preferenceId/correct`, async (context) => {
334
+ const workspaceId = context.req.param("workspaceId");
335
+ const access = await requireAccessGrantAuthorization(
336
+ context,
337
+ deps,
338
+ workspaceId,
339
+ "workspace:read",
340
+ );
341
+ const { grant } = access;
342
+ const id = preferenceId(context);
343
+ const request = await parseBody(context, CorrectPreferenceRegistryRequest);
344
+ try {
345
+ return context.json(
346
+ PreferenceRegistryMutationResponse.parse(
347
+ await correctPreferenceRegistry(deps.db, {
348
+ ...request,
349
+ accountId: grant.accountId,
350
+ workspaceId,
351
+ actorSubjectId: grant.subjectId,
352
+ principalKind: grant.principalKind,
353
+ preferenceId: id,
354
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
355
+ }),
356
+ ),
357
+ );
358
+ } catch (error) {
359
+ return preferenceError(context, error);
360
+ }
361
+ });
362
+
363
+ app.post(`${base}/:preferenceId/scope`, async (context) => {
364
+ const workspaceId = context.req.param("workspaceId");
365
+ const access = await requireAccessGrantAuthorization(
366
+ context,
367
+ deps,
368
+ workspaceId,
369
+ "workspace:read",
370
+ );
371
+ const { grant } = access;
372
+ const id = preferenceId(context);
373
+ const request = await parseBody(context, ChangePreferenceRegistryScopeRequest);
374
+ try {
375
+ return context.json(
376
+ PreferenceRegistryMutationResponse.parse(
377
+ await changePreferenceRegistryScope(deps.db, {
378
+ ...request,
379
+ accountId: grant.accountId,
380
+ workspaceId,
381
+ actorSubjectId: grant.subjectId,
382
+ principalKind: grant.principalKind,
383
+ preferenceId: id,
384
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
385
+ }),
386
+ ),
387
+ );
388
+ } catch (error) {
389
+ return preferenceError(context, error);
390
+ }
391
+ });
392
+
393
+ app.post(`${base}/:preferenceId/deactivate`, async (context) => {
394
+ const workspaceId = context.req.param("workspaceId");
395
+ const access = await requireAccessGrantAuthorization(
396
+ context,
397
+ deps,
398
+ workspaceId,
399
+ "workspace:read",
400
+ );
401
+ const { grant } = access;
402
+ const id = preferenceId(context);
403
+ const request = await parseBody(context, DeactivatePreferenceRegistryRequest);
404
+ try {
405
+ return context.json(
406
+ PreferenceRegistryMutationResponse.parse(
407
+ await deactivatePreferenceRegistry(deps.db, {
408
+ ...request,
409
+ accountId: grant.accountId,
410
+ workspaceId,
411
+ actorSubjectId: grant.subjectId,
412
+ principalKind: grant.principalKind,
413
+ preferenceId: id,
414
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
415
+ }),
416
+ ),
417
+ );
418
+ } catch (error) {
419
+ return preferenceError(context, error);
420
+ }
421
+ });
422
+
423
+ app.post(`${base}/:preferenceId/supersede`, async (context) => {
424
+ const workspaceId = context.req.param("workspaceId");
425
+ const access = await requireAccessGrantAuthorization(
426
+ context,
427
+ deps,
428
+ workspaceId,
429
+ "workspace:read",
430
+ );
431
+ const { grant } = access;
432
+ const id = preferenceId(context);
433
+ const request = await parseBody(context, SupersedePreferenceRegistryRequest);
434
+ try {
435
+ return context.json(
436
+ PreferenceRegistryMutationResponse.parse(
437
+ await supersedePreferenceRegistry(deps.db, {
438
+ ...request,
439
+ accountId: grant.accountId,
440
+ workspaceId,
441
+ actorSubjectId: grant.subjectId,
442
+ principalKind: grant.principalKind,
443
+ preferenceId: id,
444
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
445
+ }),
446
+ ),
447
+ );
448
+ } catch (error) {
449
+ return preferenceError(context, error);
450
+ }
451
+ });
452
+
453
+ app.post(`${base}/:preferenceId/reject`, async (context) => {
454
+ const workspaceId = context.req.param("workspaceId");
455
+ const access = await requireAccessGrantAuthorization(
456
+ context,
457
+ deps,
458
+ workspaceId,
459
+ "workspace:read",
460
+ );
461
+ const { grant } = access;
462
+ const id = preferenceId(context);
463
+ const request = await parseBody(context, RejectPreferenceRegistryProposalRequest);
464
+ try {
465
+ return context.json(
466
+ PreferenceRegistryMutationResponse.parse(
467
+ await rejectPreferenceRegistryProposal(deps.db, {
468
+ ...request,
469
+ accountId: grant.accountId,
470
+ workspaceId,
471
+ actorSubjectId: grant.subjectId,
472
+ principalKind: grant.principalKind,
473
+ preferenceId: id,
474
+ authorizeScope: (scope) => authorizePreferenceRegistryScopeMutation(access, scope),
475
+ }),
476
+ ),
477
+ );
478
+ } catch (error) {
479
+ return preferenceError(context, error);
480
+ }
481
+ });
482
+ }
@@ -76,6 +76,7 @@ import {
76
76
  listSessionHumanInputRequests,
77
77
  listSessionIdsInGroup,
78
78
  listSessionsForSubject,
79
+ getLatestStartedSessionTurn,
79
80
  listSessionTurns,
80
81
  projectEffectiveControlForRelatedAccess,
81
82
  projectSessionForRelatedAccess,
@@ -1077,6 +1078,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1077
1078
  await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1078
1079
  const sessionId = c.req.param("sessionId");
1079
1080
  await assertSessionExists(db, workspaceId, sessionId);
1081
+ // Exact turn that most recently emitted durable `turn.started` — the same
1082
+ // boundary goal continuations use for inherited model/effort. Queued-only
1083
+ // or preflight-rejected turns are deliberately excluded.
1084
+ if (c.req.query("latestStarted") === "1" || c.req.query("latestStarted") === "true") {
1085
+ const latest = await getLatestStartedSessionTurn(db, workspaceId, sessionId);
1086
+ return c.json(latest ? [latest] : []);
1087
+ }
1080
1088
  return c.json(
1081
1089
  await listSessionTurns(db, workspaceId, sessionId, boundedLimit(c.req.query("limit"))),
1082
1090
  );
@@ -1284,6 +1292,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1284
1292
  resources: payload.resources,
1285
1293
  model: payload.model ?? null,
1286
1294
  reasoningEffort: payload.reasoningEffort ?? null,
1295
+ latencyMode: payload.latencyMode ?? null,
1287
1296
  mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
1288
1297
  delivery: "steer",
1289
1298
  origin: "human",
@@ -1326,6 +1335,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1326
1335
  resources: event.payload.resources ?? [],
1327
1336
  model: event.payload.model ?? null,
1328
1337
  reasoningEffort: event.payload.reasoningEffort ?? null,
1338
+ latencyMode: event.payload.latencyMode ?? null,
1329
1339
  mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
1330
1340
  ...(event.payload.controlEtag !== undefined
1331
1341
  ? { controlEtag: event.payload.controlEtag }
@@ -0,0 +1,155 @@
1
+ import {
2
+ resolveWorkspaceVoiceInputEnabled,
3
+ type TranscribeAudioResponse,
4
+ } from "@opengeni/contracts";
5
+ import { type ApiRouteDeps, requireAccessGrant, TranscriptionServiceError } from "@opengeni/core";
6
+ import { getWorkspace } from "@opengeni/db";
7
+ import type { Hono } from "hono";
8
+
9
+ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void {
10
+ app.post("/v1/workspaces/:workspaceId/transcriptions", async (c) => {
11
+ const workspaceId = c.req.param("workspaceId");
12
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
13
+ const workspace = await getWorkspace(deps.db, workspaceId);
14
+ if (!workspace) return c.json({ code: "not_found" }, 404);
15
+ if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
16
+ return c.json({ code: "policy_blocked" }, 403);
17
+ }
18
+ const service = deps.transcription;
19
+ if (!service || !(await service.available())) {
20
+ return c.json({ code: "unavailable" }, 503);
21
+ }
22
+ try {
23
+ const body = await audioRequest(c.req.raw, service.limits().maxSizeBytes);
24
+ const result = await service.transcribe({
25
+ workspaceId,
26
+ accountId: grant.accountId,
27
+ audio: body.audio,
28
+ mimeType: body.mimeType,
29
+ durationSeconds: body.durationSeconds,
30
+ signal: c.req.raw.signal,
31
+ requestId: c.req.header("x-opengeni-correlation-id") ?? crypto.randomUUID(),
32
+ });
33
+ const response: TranscribeAudioResponse = {
34
+ text: result.text,
35
+ languages: result.languages,
36
+ };
37
+ return c.json(response);
38
+ } catch (error) {
39
+ if (error instanceof TranscriptionServiceError) {
40
+ return c.json({ code: error.code }, error.status as never);
41
+ }
42
+ if (isAbort(error)) return c.json({ code: "cancelled" }, 499 as never);
43
+ return c.json({ code: "unknown" }, 500);
44
+ }
45
+ });
46
+ }
47
+
48
+ async function audioRequest(
49
+ request: Request,
50
+ maxSizeBytes: number,
51
+ ): Promise<{ audio: Uint8Array; mimeType: string; durationSeconds?: number }> {
52
+ const contentLength = Number(request.headers.get("content-length"));
53
+ if (Number.isFinite(contentLength) && contentLength > maxSizeBytes + 64 * 1024) {
54
+ throw new TranscriptionServiceError({
55
+ code: "too_large",
56
+ message: "Audio is too large.",
57
+ });
58
+ }
59
+ const contentType = request.headers.get("content-type") ?? "";
60
+ if (contentType.toLowerCase().startsWith("multipart/form-data")) {
61
+ const form = await request.formData();
62
+ const audio = form.get("audio");
63
+ if (!(audio instanceof File)) {
64
+ throw new TranscriptionServiceError({
65
+ code: "invalid_audio",
66
+ message: "Audio file is required.",
67
+ });
68
+ }
69
+ if (audio.size > maxSizeBytes) {
70
+ throw new TranscriptionServiceError({
71
+ code: "too_large",
72
+ message: "Audio is too large.",
73
+ });
74
+ }
75
+ const durationSeconds = durationValue(stringValue(form.get("durationSeconds")));
76
+ return {
77
+ audio: new Uint8Array(await audio.arrayBuffer()),
78
+ mimeType: stringValue(form.get("mimeType")) ?? audio.type,
79
+ ...(durationSeconds === undefined ? {} : { durationSeconds }),
80
+ };
81
+ }
82
+ const mimeType = contentType.split(";", 1)[0]?.trim() ?? "";
83
+ if (!mimeType.toLowerCase().startsWith("audio/")) {
84
+ throw new TranscriptionServiceError({
85
+ code: "not_supported",
86
+ message: "Unsupported audio format.",
87
+ });
88
+ }
89
+ const durationSeconds = durationValue(request.headers.get("x-opengeni-audio-duration-seconds"));
90
+ return {
91
+ audio: await readBounded(request.body, maxSizeBytes, request.signal),
92
+ mimeType,
93
+ ...(durationSeconds === undefined ? {} : { durationSeconds }),
94
+ };
95
+ }
96
+
97
+ async function readBounded(
98
+ body: ReadableStream<Uint8Array> | null,
99
+ maxSizeBytes: number,
100
+ signal: AbortSignal,
101
+ ): Promise<Uint8Array> {
102
+ if (!body)
103
+ throw new TranscriptionServiceError({
104
+ code: "invalid_audio",
105
+ message: "Audio is required.",
106
+ });
107
+ const reader = body.getReader();
108
+ const chunks: Uint8Array[] = [];
109
+ let size = 0;
110
+ try {
111
+ while (true) {
112
+ if (signal.aborted) throw new DOMException("Aborted", "AbortError");
113
+ const next = await reader.read();
114
+ if (next.done) break;
115
+ size += next.value.byteLength;
116
+ if (size > maxSizeBytes) {
117
+ await reader.cancel();
118
+ throw new TranscriptionServiceError({
119
+ code: "too_large",
120
+ message: "Audio is too large.",
121
+ });
122
+ }
123
+ chunks.push(next.value);
124
+ }
125
+ } finally {
126
+ reader.releaseLock();
127
+ }
128
+ const audio = new Uint8Array(size);
129
+ let offset = 0;
130
+ for (const chunk of chunks) {
131
+ audio.set(chunk, offset);
132
+ offset += chunk.byteLength;
133
+ }
134
+ return audio;
135
+ }
136
+
137
+ function durationValue(value: string | null | undefined): number | undefined {
138
+ if (value == null || value.trim() === "") return undefined;
139
+ const duration = Number(value);
140
+ if (!Number.isFinite(duration)) {
141
+ throw new TranscriptionServiceError({
142
+ code: "invalid_audio",
143
+ message: "Invalid audio duration.",
144
+ });
145
+ }
146
+ return duration;
147
+ }
148
+
149
+ function stringValue(value: FormDataEntryValue | null): string | undefined {
150
+ return typeof value === "string" ? value : undefined;
151
+ }
152
+
153
+ function isAbort(error: unknown): boolean {
154
+ return error instanceof DOMException && error.name === "AbortError";
155
+ }