@opengeni/api-router 2.11.0-canary.0 → 2.11.2-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 (38) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-EAYCECQK.js → chunk-5X3XM6CE.js} +1841 -1528
  3. package/dist/chunk-5X3XM6CE.js.map +1 -0
  4. package/dist/http/sse.d.ts +4 -3
  5. package/dist/index.js +5 -3
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/github-skill-source.d.ts +1 -5
  8. package/dist/mcp/remember.d.ts +1 -1
  9. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  10. package/dist/mcp/server.d.ts +4 -4
  11. package/dist/routes/skill-content.d.ts +4 -0
  12. package/dist/routes/skill-install-authority.d.ts +6 -0
  13. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  14. package/dist/transcription/service.d.ts +2 -1
  15. package/package.json +19 -19
  16. package/src/app.ts +18 -0
  17. package/src/http/sse.ts +9 -25
  18. package/src/index.ts +3 -1
  19. package/src/integrations/github-skill-source.ts +2 -142
  20. package/src/mcp/company-brain-governed-writes.ts +15 -21
  21. package/src/mcp/remember.ts +14 -15
  22. package/src/mcp/server.ts +7 -4
  23. package/src/routes/automations.ts +35 -0
  24. package/src/routes/packs.ts +61 -8
  25. package/src/routes/plugins.ts +48 -5
  26. package/src/routes/preference-registry.ts +12 -93
  27. package/src/routes/sessions.ts +52 -20
  28. package/src/routes/skill-content.ts +226 -0
  29. package/src/routes/skill-install-authority.ts +64 -0
  30. package/src/routes/skills.ts +38 -3
  31. package/src/routes/transcription-recordings.ts +30 -1
  32. package/src/routes/transcriptions.ts +4 -0
  33. package/src/transcription/providers/codex-subscription.ts +2 -0
  34. package/src/transcription/providers/openai.ts +1 -0
  35. package/src/transcription/providers/xai-subscription.ts +49 -3
  36. package/src/transcription/service.ts +43 -6
  37. package/src/xai-subscription-auth.ts +9 -2
  38. package/dist/chunk-EAYCECQK.js.map +0 -1
@@ -13,6 +13,7 @@ import {
13
13
  portableSkillCapabilityId,
14
14
  portableSkillPluginKey,
15
15
  requireAccessGrant,
16
+ requireAccessGrantAuthorization,
16
17
  resolveSkillImport,
17
18
  type ApiRouteDeps,
18
19
  type GitHubSkillSourceClient,
@@ -23,7 +24,9 @@ import {
23
24
  listInstalledSkills,
24
25
  PortableSkillInstallationVersionConflictError,
25
26
  PortableSkillInstallationVersionRequiredError,
27
+ PortableSkillSourcePathConflictError,
26
28
  uninstallPortableSkill,
29
+ SkillSourceRemovalAuthorityError,
27
30
  } from "@opengeni/db";
28
31
  import { loadSkillLibrarySkill, skillLibraryRepositoryUrl } from "@opengeni/runtime/skill-library";
29
32
  import { createHash } from "node:crypto";
@@ -31,6 +34,8 @@ import { HTTPException } from "hono/http-exception";
31
34
  import type { Hono } from "hono";
32
35
 
33
36
  import { createGitHubSkillSourceClient } from "../integrations/github-skill-source";
37
+ import { registerSkillContentRoutes } from "./skill-content";
38
+ import { skillInstallerActor, skillRemovalActor } from "./skill-install-authority";
34
39
 
35
40
  export type SkillRouteOverrides = Readonly<{
36
41
  github?: GitHubSkillSourceClient;
@@ -41,6 +46,7 @@ export function registerSkillRoutes(
41
46
  deps: ApiRouteDeps,
42
47
  overrides: SkillRouteOverrides = {},
43
48
  ): void {
49
+ registerSkillContentRoutes(app, deps);
44
50
  const github = overrides.github ?? createGitHubSkillSourceClient(deps.settings);
45
51
 
46
52
  app.get("/v1/workspaces/:workspaceId/skills", async (c) => {
@@ -55,7 +61,14 @@ export function registerSkillRoutes(
55
61
 
56
62
  app.post("/v1/workspaces/:workspaceId/skills/library/:libraryId/install", async (c) => {
57
63
  const workspaceId = c.req.param("workspaceId");
58
- const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
64
+ const access = await requireAccessGrantAuthorization(
65
+ c,
66
+ deps,
67
+ workspaceId,
68
+ "capabilities:manage",
69
+ );
70
+ const { grant } = access;
71
+ const skillActor = skillInstallerActor(access);
59
72
  const libraryId = decodeURIComponent(c.req.param("libraryId"));
60
73
  const payload = InstallLibrarySkillRequest.parse(await c.req.json());
61
74
  let loaded: ReturnType<typeof loadSkillLibrarySkill>;
@@ -81,6 +94,7 @@ export function registerSkillRoutes(
81
94
  });
82
95
  try {
83
96
  const installed = await installPortableSkill(deps.db, {
97
+ skillActor,
84
98
  accountId: grant.accountId,
85
99
  workspaceId,
86
100
  subjectId: grant.subjectId,
@@ -136,7 +150,14 @@ export function registerSkillRoutes(
136
150
 
137
151
  app.post("/v1/workspaces/:workspaceId/skills/install", async (c) => {
138
152
  const workspaceId = c.req.param("workspaceId");
139
- const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
153
+ const access = await requireAccessGrantAuthorization(
154
+ c,
155
+ deps,
156
+ workspaceId,
157
+ "capabilities:manage",
158
+ );
159
+ const { grant } = access;
160
+ const skillActor = skillInstallerActor(access);
140
161
  const payload = InstallSkillRequest.parse(await c.req.json());
141
162
  const resolved = await resolveForRoute(payload.url, github);
142
163
  if (
@@ -153,6 +174,7 @@ export function registerSkillRoutes(
153
174
  );
154
175
  try {
155
176
  const installed = await installPortableSkill(deps.db, {
177
+ skillActor,
156
178
  accountId: grant.accountId,
157
179
  workspaceId,
158
180
  subjectId: grant.subjectId,
@@ -204,13 +226,21 @@ export function registerSkillRoutes(
204
226
 
205
227
  app.delete("/v1/workspaces/:workspaceId/skills/:capabilityId", async (c) => {
206
228
  const workspaceId = c.req.param("workspaceId");
207
- const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
229
+ const access = await requireAccessGrantAuthorization(
230
+ c,
231
+ deps,
232
+ workspaceId,
233
+ "capabilities:manage",
234
+ );
235
+ const { grant } = access;
236
+ const skillActor = skillRemovalActor(access);
208
237
  const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
209
238
  const payload = UninstallSkillRequest.parse(await c.req.json());
210
239
  try {
211
240
  return c.json(
212
241
  UninstallSkillResult.parse(
213
242
  await uninstallPortableSkill(deps.db, {
243
+ ...(skillActor ? { skillActor } : {}),
214
244
  accountId: grant.accountId,
215
245
  workspaceId,
216
246
  capabilityId,
@@ -224,12 +254,17 @@ export function registerSkillRoutes(
224
254
  message: "The Skill changed after preview. Review uninstall impact again.",
225
255
  });
226
256
  }
257
+ if (error instanceof SkillSourceRemovalAuthorityError)
258
+ throw new HTTPException(403, { message: error.message });
227
259
  throw error;
228
260
  }
229
261
  });
230
262
  }
231
263
 
232
264
  function portableSkillMutationHttpError(error: unknown): Error {
265
+ if (error instanceof PortableSkillSourcePathConflictError) {
266
+ return new HTTPException(409, { message: error.message });
267
+ }
233
268
  if (error instanceof PortableSkillInstallationVersionRequiredError) {
234
269
  return new HTTPException(400, { message: error.message });
235
270
  }
@@ -5,6 +5,7 @@ import {
5
5
  resolveWorkspaceVoiceInputEnabled,
6
6
  TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
7
7
  TRANSCRIPTION_RECORDING_RECOVERY_RETRY_AFTER_MILLISECONDS,
8
+ WorkspaceVoiceInputSettings,
8
9
  type TranscriptionRecordingErrorCode,
9
10
  type TranscriptionRecordingResponse,
10
11
  type UploadTranscriptionRecordingChunkResponse,
@@ -58,6 +59,7 @@ type RecordingAuthority = {
58
59
  accountId: string;
59
60
  workspaceId: string;
60
61
  subjectId: string;
62
+ voiceInput?: WorkspaceVoiceInputSettings | undefined;
61
63
  };
62
64
 
63
65
  class RecordingProcessingError extends Error {
@@ -357,6 +359,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
357
359
  let authority: RecordingAuthority | null = null;
358
360
  let attemptId: string | null = null;
359
361
  let segmentNumber: number | null = null;
362
+ let attemptedProvider: string | null = null;
363
+ let fallbackAllowed = false;
360
364
  try {
361
365
  authority = await requireRecordingAuthority(c, deps, true);
362
366
  const service = deps.transcription;
@@ -372,6 +376,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
372
376
  }
373
377
  const selectedProvider = service.selectProvider
374
378
  ? await service.selectProvider({
379
+ preferredProvider: authority.voiceInput?.preferredProvider,
380
+ fallbackEnabled: authority.voiceInput?.fallbackEnabled,
375
381
  workspaceId: authority.workspaceId,
376
382
  subjectId: authority.subjectId,
377
383
  })
@@ -395,6 +401,8 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
395
401
  );
396
402
  }
397
403
  segmentNumber = claim.segment.segmentNumber;
404
+ attemptedProvider = claim.segment.providerId;
405
+ fallbackAllowed = claim.fallbackAllowed === true;
398
406
  const segmentKey = claim.segment.objectKey;
399
407
  const stored = deps.objectStorage
400
408
  ? await retryWhileMissing(async () => deps.objectStorage!.getObjectBytes(segmentKey))
@@ -452,12 +460,32 @@ export function registerResumableTranscriptionRoutes(app: Hono, deps: ApiRouteDe
452
460
  } catch (error) {
453
461
  if (authority && attemptId && segmentNumber !== null) {
454
462
  const failure = processingFailure(error);
463
+ // Only an explicit rejection may move an untouched recording to another vendor.
464
+ // The database fences this against stale attempts and completed segments.
465
+ const fallbackProviderId =
466
+ fallbackAllowed &&
467
+ error instanceof TranscriptionServiceError &&
468
+ error.fallbackSafe &&
469
+ authority.voiceInput?.fallbackEnabled !== false &&
470
+ attemptedProvider &&
471
+ deps.transcription?.selectProvider
472
+ ? await Promise.resolve(
473
+ deps.transcription.selectProvider({
474
+ workspaceId: authority.workspaceId,
475
+ subjectId: authority.subjectId,
476
+ preferredProvider: authority.voiceInput?.preferredProvider,
477
+ excludedProviders: [attemptedProvider],
478
+ afterProvider: attemptedProvider,
479
+ }),
480
+ ).catch(() => null)
481
+ : null;
455
482
  const persisted = await failTranscriptionRecordingSegment(deps.db, {
456
483
  workspaceId: authority.workspaceId,
457
484
  subjectId: authority.subjectId,
458
485
  recordingId: uuidParam(c, "recordingId"),
459
486
  segmentNumber,
460
487
  attemptId,
488
+ fallbackProviderId,
461
489
  errorCode: failure.code,
462
490
  retryable: failure.retryable,
463
491
  }).catch(() => null);
@@ -505,14 +533,15 @@ async function requireRecordingAuthority(
505
533
  throw new TranscriptionRecordingNotFoundError("Workspace not found");
506
534
  }
507
535
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
536
+ const workspace = await getWorkspace(deps.db, workspaceId);
508
537
  if (requirePolicy) {
509
- const workspace = await getWorkspace(deps.db, workspaceId);
510
538
  if (!workspace) throw new TranscriptionRecordingNotFoundError("Workspace not found");
511
539
  if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
512
540
  throw new RecordingProcessingError("Voice input is disabled", "policy_blocked", false);
513
541
  }
514
542
  }
515
543
  return {
544
+ voiceInput: WorkspaceVoiceInputSettings.safeParse(workspace?.settings.voiceInput).data,
516
545
  accountId: grant.accountId,
517
546
  workspaceId,
518
547
  subjectId: grant.subjectId,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  resolveWorkspaceVoiceInputEnabled,
3
+ WorkspaceVoiceInputSettings,
3
4
  type TranscribeAudioResponse,
4
5
  } from "@opengeni/contracts";
5
6
  import { type ApiRouteDeps, requireAccessGrant, TranscriptionServiceError } from "@opengeni/core";
@@ -17,6 +18,7 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
17
18
  if (resolveWorkspaceVoiceInputEnabled(workspace.settings) === false) {
18
19
  return c.json({ code: "policy_blocked" }, 403);
19
20
  }
21
+ const preferences = WorkspaceVoiceInputSettings.safeParse(workspace.settings.voiceInput).data;
20
22
  const service = deps.transcription;
21
23
  if (!service || !(await service.available({ workspaceId, subjectId: grant.subjectId }))) {
22
24
  return c.json({ code: "unavailable" }, 503);
@@ -24,6 +26,8 @@ export function registerTranscriptionRoutes(app: Hono, deps: ApiRouteDeps): void
24
26
  try {
25
27
  const body = await audioRequest(c.req.raw, service.limits().maxSizeBytes);
26
28
  const result = await service.transcribe({
29
+ preferredProvider: preferences?.preferredProvider,
30
+ fallbackEnabled: preferences?.fallbackEnabled,
27
31
  workspaceId,
28
32
  accountId: grant.accountId,
29
33
  subjectId: grant.subjectId,
@@ -45,6 +45,7 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
45
45
  );
46
46
  if (!account) {
47
47
  throw new TranscriptionServiceError({
48
+ fallbackSafe: true,
48
49
  code: "unavailable",
49
50
  message: "Transcription is unavailable.",
50
51
  });
@@ -55,6 +56,7 @@ export function createCodexSubscriptionTranscriptionProvider(input: {
55
56
  token = await resolver.getToken();
56
57
  } catch {
57
58
  throw new TranscriptionServiceError({
59
+ fallbackSafe: true,
58
60
  code: "unavailable",
59
61
  message: "Transcription is unavailable.",
60
62
  });
@@ -53,6 +53,7 @@ function audioBlob(audio: Uint8Array, mimeType: string): Blob {
53
53
  export function responseError(status: number): TranscriptionServiceError {
54
54
  if (status === 401 || status === 403) {
55
55
  return new TranscriptionServiceError({
56
+ fallbackSafe: true,
56
57
  code: "unavailable",
57
58
  message: "Transcription is unavailable.",
58
59
  });
@@ -62,6 +62,7 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
62
62
  } catch {
63
63
  throw new TranscriptionServiceError({
64
64
  code: "unavailable",
65
+ fallbackSafe: true,
65
66
  message: "Transcription is unavailable.",
66
67
  });
67
68
  }
@@ -86,16 +87,28 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
86
87
  ...(signal ? { signal } : {}),
87
88
  });
88
89
  };
90
+ const tokenForRequest = async (refresh: boolean) => {
91
+ try {
92
+ return await (refresh ? auth.context.refresh() : auth.context.getToken());
93
+ } catch {
94
+ throw new TranscriptionServiceError({
95
+ code: "unavailable",
96
+ message: "Reconnect the SuperGrok account to use transcription.",
97
+ fallbackSafe: true,
98
+ });
99
+ }
100
+ };
89
101
  let response: Response;
90
102
  try {
91
- let token = await auth.context.getToken();
103
+ let token = await tokenForRequest(false);
92
104
  response = await request(token.accessToken);
93
- if (response.status === 401) {
105
+ if (response.status === 401 || (await isXaiInvalidCredentialResponse(response))) {
94
106
  await response.body?.cancel().catch(() => undefined);
95
- token = await auth.context.refresh();
107
+ token = await tokenForRequest(true);
96
108
  response = await request(token.accessToken);
97
109
  }
98
110
  } catch (error) {
111
+ if (error instanceof TranscriptionServiceError) throw error;
99
112
  throw fetchError(error);
100
113
  }
101
114
  if (!response.ok) throw responseError(response.status);
@@ -108,3 +121,36 @@ export function createXaiSubscriptionTranscriptionProvider(input: {
108
121
  },
109
122
  };
110
123
  }
124
+
125
+ export async function isXaiInvalidCredentialResponse(response: Response): Promise<boolean> {
126
+ if (response.status !== 403) return false;
127
+ const reader = response.clone().body?.getReader();
128
+ if (!reader) return false;
129
+ const chunks: Uint8Array[] = [];
130
+ let size = 0;
131
+ try {
132
+ while (true) {
133
+ const chunk = await reader.read();
134
+ if (chunk.done) break;
135
+ size += chunk.value.byteLength;
136
+ if (size > 16_384) return false;
137
+ chunks.push(chunk.value);
138
+ }
139
+ const bytes = new Uint8Array(size);
140
+ let offset = 0;
141
+ for (const chunk of chunks) {
142
+ bytes.set(chunk, offset);
143
+ offset += chunk.byteLength;
144
+ }
145
+ const body = JSON.parse(new TextDecoder().decode(bytes));
146
+ return (
147
+ typeof body?.error === "string" &&
148
+ body.error.includes("[WKE=unauthenticated:bad-credentials]")
149
+ );
150
+ } catch {
151
+ return false;
152
+ } finally {
153
+ void reader.cancel().catch(() => undefined);
154
+ reader.releaseLock();
155
+ }
156
+ }
@@ -74,7 +74,7 @@ export function createTranscriptionService(input: {
74
74
  );
75
75
  },
76
76
  async selectProvider(context) {
77
- return (await firstAvailable(providers, context))?.id ?? null;
77
+ return (await firstAvailable(orderedProviders(providers, context), context))?.id ?? null;
78
78
  },
79
79
  async transcribe(request) {
80
80
  const mimeType = normalizeMimeType(request.mimeType);
@@ -106,12 +106,10 @@ export function createTranscriptionService(input: {
106
106
  workspaceId: request.workspaceId,
107
107
  subjectId: request.subjectId,
108
108
  })
109
- : await firstAvailable(providers, {
110
- workspaceId: request.workspaceId,
111
- subjectId: request.subjectId,
112
- });
109
+ : await firstAvailable(orderedProviders(providers, request), request);
113
110
  if (!provider) {
114
111
  throw new TranscriptionServiceError({
112
+ fallbackSafe: true,
115
113
  code: "unavailable",
116
114
  message: "Transcription is unavailable.",
117
115
  });
@@ -164,6 +162,23 @@ export function createTranscriptionService(input: {
164
162
  retryable: true,
165
163
  });
166
164
  }
165
+ if (
166
+ error instanceof TranscriptionServiceError &&
167
+ error.fallbackSafe &&
168
+ !request.providerId &&
169
+ request.fallbackEnabled !== false &&
170
+ !request.signal?.aborted
171
+ ) {
172
+ const excludedProviders = [...(request.excludedProviders ?? []), provider.id];
173
+ if (
174
+ await firstAvailable(
175
+ orderedProviders(providers, { ...request, excludedProviders }),
176
+ request,
177
+ )
178
+ ) {
179
+ return await this.transcribe({ ...request, excludedProviders });
180
+ }
181
+ }
167
182
  throw error;
168
183
  } finally {
169
184
  deadline.dispose();
@@ -217,12 +232,34 @@ function createProviderRequestDeadline(
217
232
  };
218
233
  }
219
234
 
235
+ export function orderedProviders(
236
+ providers: readonly TranscriptionProvider[],
237
+ context: TranscriptionAvailabilityContext,
238
+ ): TranscriptionProvider[] {
239
+ const preferred = providers.filter((provider) => provider.id === context.preferredProvider);
240
+ const ordered = context.preferredProvider
241
+ ? context.fallbackEnabled === false
242
+ ? preferred
243
+ : [...preferred, ...providers.filter((provider) => provider.id !== context.preferredProvider)]
244
+ : context.fallbackEnabled === false
245
+ ? providers.slice(0, 1)
246
+ : [...providers];
247
+ const remaining = context.afterProvider
248
+ ? ordered.slice(ordered.findIndex((provider) => provider.id === context.afterProvider) + 1)
249
+ : ordered;
250
+ return remaining.filter((provider) => !context.excludedProviders?.includes(provider.id));
251
+ }
252
+
220
253
  async function firstAvailable(
221
254
  providers: readonly TranscriptionProvider[],
222
255
  context: TranscriptionAvailabilityContext,
223
256
  ) {
224
257
  for (const provider of providers) {
225
- if (await provider.available(context)) return provider;
258
+ try {
259
+ if (await provider.available(context)) return provider;
260
+ } catch {
261
+ /* No audio sent; another configured provider may be ready. */
262
+ }
226
263
  }
227
264
  return null;
228
265
  }
@@ -77,14 +77,20 @@ export async function buildXaiSubscriptionAuthorization(input: {
77
77
  userId: credential.providerAccountId,
78
78
  };
79
79
  };
80
- return {
80
+ const authorization: XaiSubscriptionAuthorization = {
81
81
  credentialId,
82
82
  authoritySnapshot,
83
83
  rotationEnabled: selected.rotationEnabled,
84
84
  context: {
85
85
  clientVersion: XAI_CLIENT_VERSION,
86
86
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
87
- getToken: async () => tokenSnapshot(),
87
+ getToken: async () => {
88
+ const expiresAt = xaiAccessTokenExpiry(credential.secret.accessToken ?? "");
89
+ if (expiresAt && expiresAt.getTime() <= Date.now() + 60_000) {
90
+ return await authorization.context.refresh();
91
+ }
92
+ return tokenSnapshot();
93
+ },
88
94
  refresh: async () => {
89
95
  const observedAccessToken = credential.secret.accessToken;
90
96
  const observedRefreshToken = credential.secret.refreshToken;
@@ -129,4 +135,5 @@ export async function buildXaiSubscriptionAuthorization(input: {
129
135
  },
130
136
  },
131
137
  };
138
+ return authorization;
132
139
  }