@opengeni/api-router 0.23.1 → 0.30.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 (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. package/src/mcp/toolspace.ts +0 -1190
@@ -0,0 +1,181 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { FileResourceRef } from "@opengeni/contracts";
3
+ import { completeFileUpload, prepareGeneratedWorkspaceFile, type Database } from "@opengeni/db";
4
+ import type { ObjectHead, ObjectStorage } from "@opengeni/storage";
5
+ import type { DownloadedSlackReactionImage } from "./integrations/slack-bot";
6
+
7
+ const IMPORT_IDENTITY_VERSION = "slack-reaction-image-v1";
8
+ const UPLOAD_INTENT_TTL_MS = 60 * 60_000;
9
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
10
+
11
+ export type SlackReactionImageImportSource = Readonly<{
12
+ accountId: string;
13
+ workspaceId: string;
14
+ connectionId: string;
15
+ slackTeamId: string;
16
+ slackChannelId: string;
17
+ slackMessageTs: string;
18
+ }>;
19
+
20
+ export type ImportedSlackReactionImage = Readonly<{
21
+ resource: FileResourceRef;
22
+ filename: string;
23
+ contentType: "image/png" | "image/jpeg" | "image/webp";
24
+ sizeBytes: number;
25
+ }>;
26
+
27
+ export async function importSlackReactionImage(
28
+ dependencies: Readonly<{
29
+ db: Database;
30
+ objectStorage: ObjectStorage;
31
+ now?: () => Date;
32
+ prepareFile?: typeof prepareGeneratedWorkspaceFile;
33
+ completeFile?: typeof completeFileUpload;
34
+ }>,
35
+ source: SlackReactionImageImportSource,
36
+ image: DownloadedSlackReactionImage,
37
+ ordinal: number,
38
+ ): Promise<ImportedSlackReactionImage> {
39
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || ordinal > 4) {
40
+ throw new RangeError("Slack reaction attachment ordinal is invalid");
41
+ }
42
+ const storage = dependencies.objectStorage;
43
+ if (!storage.headObject) {
44
+ throw new Error("Object storage lacks immutable object verification support");
45
+ }
46
+ if (image.bytes.byteLength < 1 || image.bytes.byteLength > 4 * 1024 * 1024) {
47
+ throw new Error("Slack reaction image size is invalid");
48
+ }
49
+ const sha256 = createHash("sha256").update(image.bytes).digest("hex");
50
+ const identity = slackReactionImageIdentity(source, image.fileId);
51
+ const fileId = deterministicUuid(`${identity}:file`);
52
+ const uploadId = deterministicUuid(`${identity}:upload`);
53
+ const safeFilename = safeSlackImageFilename(image.filename, image.contentType);
54
+ const objectKey = `workspaces/${source.workspaceId}/files/${fileId}/slack-reactions/${safeFilename}`;
55
+ const filename = safeFilename;
56
+ const prepared = await (dependencies.prepareFile ?? prepareGeneratedWorkspaceFile)(
57
+ dependencies.db,
58
+ {
59
+ accountId: source.accountId,
60
+ workspaceId: source.workspaceId,
61
+ fileId,
62
+ uploadId,
63
+ filename,
64
+ safeFilename,
65
+ contentType: image.contentType,
66
+ sizeBytes: image.bytes.byteLength,
67
+ sha256,
68
+ bucket: storage.bucket,
69
+ objectKey,
70
+ expiresAt: new Date((dependencies.now?.() ?? new Date()).getTime() + UPLOAD_INTENT_TTL_MS),
71
+ },
72
+ );
73
+
74
+ let file = prepared.file;
75
+ if (file.status !== "ready") {
76
+ const existing = await storage.headObject(objectKey);
77
+ if (existing) {
78
+ assertStoredSlackImage(existing, image, sha256);
79
+ } else if (storage.putObjectIfAbsent) {
80
+ await storage.putObjectIfAbsent({
81
+ key: objectKey,
82
+ contentType: image.contentType,
83
+ body: image.bytes,
84
+ sha256,
85
+ });
86
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
87
+ } else {
88
+ await storage.putObject({
89
+ key: objectKey,
90
+ contentType: image.contentType,
91
+ body: image.bytes,
92
+ sha256,
93
+ });
94
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
95
+ }
96
+ file = await (dependencies.completeFile ?? completeFileUpload)(
97
+ dependencies.db,
98
+ source.workspaceId,
99
+ uploadId,
100
+ );
101
+ } else {
102
+ assertStoredSlackImage(await storage.headObject(objectKey), image, sha256);
103
+ }
104
+
105
+ if (
106
+ file.id !== fileId ||
107
+ file.status !== "ready" ||
108
+ file.filename !== filename ||
109
+ file.safeFilename !== safeFilename ||
110
+ file.contentType !== image.contentType ||
111
+ file.sizeBytes !== image.bytes.byteLength ||
112
+ file.sha256 !== sha256
113
+ ) {
114
+ throw new Error("Imported Slack image differs from its immutable workspace file");
115
+ }
116
+ const mountPath = `attachments/slack/${String(ordinal).padStart(2, "0")}-${safeFilename}`;
117
+ return Object.freeze({
118
+ resource: Object.freeze({ kind: "file", fileId, mountPath }),
119
+ filename,
120
+ contentType: image.contentType,
121
+ sizeBytes: image.bytes.byteLength,
122
+ });
123
+ }
124
+
125
+ export function slackReactionImageIdentity(
126
+ source: SlackReactionImageImportSource,
127
+ slackFileId: string,
128
+ ): string {
129
+ return [
130
+ IMPORT_IDENTITY_VERSION,
131
+ source.workspaceId,
132
+ source.connectionId,
133
+ source.slackTeamId,
134
+ source.slackChannelId,
135
+ source.slackMessageTs,
136
+ slackFileId,
137
+ ].join(":");
138
+ }
139
+
140
+ export function safeSlackImageFilename(
141
+ value: string,
142
+ contentType: "image/png" | "image/jpeg" | "image/webp",
143
+ ): string {
144
+ const extension =
145
+ contentType === "image/png" ? ".png" : contentType === "image/jpeg" ? ".jpg" : ".webp";
146
+ const normalized = value
147
+ .normalize("NFKC")
148
+ .replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-")
149
+ .replace(/\s+/gu, " ")
150
+ .trim();
151
+ const withoutKnownExtension = normalized.replace(/\.(?:png|jpe?g|webp)$/iu, "");
152
+ const stem = withoutKnownExtension.replace(/^[. -]+|[. ]+$/gu, "").slice(0, 180) || "slack-image";
153
+ return `${stem}${extension}`;
154
+ }
155
+
156
+ function assertStoredSlackImage(
157
+ head: ObjectHead | null,
158
+ image: DownloadedSlackReactionImage,
159
+ sha256: string,
160
+ ): void {
161
+ if (
162
+ !head ||
163
+ head.ContentLength !== image.bytes.byteLength ||
164
+ head.ContentType !== image.contentType ||
165
+ head.Metadata?.sha256 !== sha256 ||
166
+ typeof head.VersionToken !== "string" ||
167
+ head.VersionToken.length < 1
168
+ ) {
169
+ throw new Error("Stored Slack image differs from its immutable import");
170
+ }
171
+ }
172
+
173
+ function deterministicUuid(seed: string): string {
174
+ const bytes = createHash("sha256").update(seed, "utf8").digest().subarray(0, 16);
175
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
176
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
177
+ const hex = bytes.toString("hex");
178
+ const value = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
179
+ if (!UUID_PATTERN.test(value)) throw new Error("Slack reaction file identity is invalid");
180
+ return value;
181
+ }
@@ -0,0 +1,135 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ claimTemporalScheduleCleanups,
4
+ settleTemporalScheduleCleanup,
5
+ type Database,
6
+ type TemporalScheduleCleanupClaim,
7
+ } from "@opengeni/db";
8
+ import type { Observability } from "@opengeni/observability";
9
+
10
+ const CLEANUP_BATCH_SIZE = 32;
11
+ const CLEANUP_CLAIM_SECONDS = 15;
12
+ const CLEANUP_POLL_INTERVAL_MS = 1_000;
13
+
14
+ type TemporalScheduleCleanupDependencies = {
15
+ db: Database;
16
+ deleteSchedule: (temporalScheduleId: string) => Promise<void>;
17
+ observability?: Pick<Observability, "info" | "warn" | "error">;
18
+ };
19
+
20
+ export type TemporalScheduleCleanupBatchResult = {
21
+ claimed: number;
22
+ deleted: number;
23
+ failed: number;
24
+ stale: number;
25
+ };
26
+
27
+ function cleanupError(error: unknown): string {
28
+ if (error instanceof Error) {
29
+ return `${error.name}: ${error.message}`.slice(0, 2000);
30
+ }
31
+ return String(error).slice(0, 2000);
32
+ }
33
+
34
+ /** Settle exact claims concurrently; every external delete is idempotent. */
35
+ export async function processTemporalScheduleCleanupClaims(
36
+ deps: TemporalScheduleCleanupDependencies,
37
+ claims: readonly TemporalScheduleCleanupClaim[],
38
+ ): Promise<TemporalScheduleCleanupBatchResult> {
39
+ const results = await Promise.all(
40
+ claims.map(async (claim): Promise<"deleted" | "failed" | "stale"> => {
41
+ try {
42
+ await deps.deleteSchedule(claim.temporalScheduleId);
43
+ const settled = await settleTemporalScheduleCleanup(deps.db, {
44
+ id: claim.id,
45
+ claimId: claim.claimId,
46
+ });
47
+ return settled ? "deleted" : "stale";
48
+ } catch (error) {
49
+ const message = cleanupError(error);
50
+ try {
51
+ const settled = await settleTemporalScheduleCleanup(deps.db, {
52
+ id: claim.id,
53
+ claimId: claim.claimId,
54
+ error: message,
55
+ });
56
+ if (!settled) return "stale";
57
+ } catch (settlementError) {
58
+ deps.observability?.error("Temporal schedule cleanup settlement failed", {
59
+ cleanupId: claim.id,
60
+ temporalScheduleId: claim.temporalScheduleId,
61
+ attemptCount: claim.attemptCount,
62
+ error: cleanupError(settlementError),
63
+ });
64
+ }
65
+ deps.observability?.warn("Temporal schedule cleanup will retry", {
66
+ cleanupId: claim.id,
67
+ temporalScheduleId: claim.temporalScheduleId,
68
+ attemptCount: claim.attemptCount,
69
+ error: message,
70
+ });
71
+ return "failed";
72
+ }
73
+ }),
74
+ );
75
+
76
+ return {
77
+ claimed: claims.length,
78
+ deleted: results.filter((result) => result === "deleted").length,
79
+ failed: results.filter((result) => result === "failed").length,
80
+ stale: results.filter((result) => result === "stale").length,
81
+ };
82
+ }
83
+
84
+ /** Claim and process one globally bounded batch. */
85
+ export async function drainTemporalScheduleCleanupOutbox(
86
+ deps: TemporalScheduleCleanupDependencies,
87
+ ): Promise<TemporalScheduleCleanupBatchResult> {
88
+ const claims = await claimTemporalScheduleCleanups(deps.db, {
89
+ claimId: randomUUID(),
90
+ limit: CLEANUP_BATCH_SIZE,
91
+ claimSeconds: CLEANUP_CLAIM_SECONDS,
92
+ });
93
+ return await processTemporalScheduleCleanupClaims(deps, claims);
94
+ }
95
+
96
+ /**
97
+ * Start one non-overlapping reconciliation loop. PostgreSQL claims coordinate
98
+ * replicas; an API crash merely delays the exact row until its short claim
99
+ * expires.
100
+ */
101
+ export function startTemporalScheduleCleanupPump(
102
+ deps: TemporalScheduleCleanupDependencies,
103
+ options: { intervalMs?: number } = {},
104
+ ): () => Promise<void> {
105
+ const intervalMs = Math.max(100, options.intervalMs ?? CLEANUP_POLL_INTERVAL_MS);
106
+ let stopped = false;
107
+ let running: Promise<void> | undefined;
108
+
109
+ const tick = (): void => {
110
+ if (stopped || running) return;
111
+ running = drainTemporalScheduleCleanupOutbox(deps)
112
+ .then((result) => {
113
+ if (result.claimed > 0) {
114
+ deps.observability?.info("Temporal schedule cleanup batch settled", result);
115
+ }
116
+ })
117
+ .catch((error: unknown) => {
118
+ deps.observability?.error("Temporal schedule cleanup claim failed", {
119
+ error: cleanupError(error),
120
+ });
121
+ })
122
+ .finally(() => {
123
+ running = undefined;
124
+ });
125
+ };
126
+
127
+ const timer = setInterval(tick, intervalMs);
128
+ queueMicrotask(tick);
129
+
130
+ return async () => {
131
+ stopped = true;
132
+ clearInterval(timer);
133
+ await running;
134
+ };
135
+ }
@@ -0,0 +1,110 @@
1
+ import { type Settings } from "@opengeni/config";
2
+ import {
3
+ type TranscriptionAvailabilityContext,
4
+ type TranscriptionProvider,
5
+ TranscriptionServiceError,
6
+ } from "@opengeni/core";
7
+ import { workspaceXaiSubscriptionActive, type Database } from "@opengeni/db";
8
+ import {
9
+ XAI_CLIENT_MODE,
10
+ XAI_CLIENT_VERSION,
11
+ XAI_PUBLIC_API_BASE_URL,
12
+ type XaiFetch,
13
+ } from "@opengeni/xai-subscription";
14
+ import { buildXaiSubscriptionAuthorization } from "../../xai-subscription-auth";
15
+ import { fetchError, responseError } from "./openai";
16
+
17
+ const TRANSCRIBE_URL = `${XAI_PUBLIC_API_BASE_URL}/stt`;
18
+
19
+ export function createXaiSubscriptionTranscriptionProvider(input: {
20
+ settings: Settings;
21
+ db: Database;
22
+ fetch?: typeof fetch;
23
+ }): TranscriptionProvider {
24
+ const fetchImpl = input.fetch ?? fetch;
25
+ return {
26
+ id: "supergrok-subscription",
27
+ supportsServerDeadline: true,
28
+ experimental: true,
29
+ async available(context?: TranscriptionAvailabilityContext) {
30
+ if (!context?.workspaceId || !context.subjectId) {
31
+ return input.settings.supergrokSubscriptionEnabled;
32
+ }
33
+ return await workspaceXaiSubscriptionActive(
34
+ input.db,
35
+ input.settings,
36
+ context.workspaceId,
37
+ context.subjectId,
38
+ );
39
+ },
40
+ async transcribe({
41
+ audio,
42
+ mimeType,
43
+ filename,
44
+ workspaceId,
45
+ accountId,
46
+ subjectId,
47
+ requestId,
48
+ signal,
49
+ }) {
50
+ let auth: Awaited<ReturnType<typeof buildXaiSubscriptionAuthorization>>;
51
+ try {
52
+ auth = await buildXaiSubscriptionAuthorization({
53
+ db: input.db,
54
+ settings: input.settings,
55
+ accountId,
56
+ workspaceId,
57
+ subjectId,
58
+ shardKey: requestId,
59
+ sessionId: requestId,
60
+ ...(input.fetch ? { fetch: input.fetch as XaiFetch } : {}),
61
+ });
62
+ } catch {
63
+ throw new TranscriptionServiceError({
64
+ code: "unavailable",
65
+ message: "Transcription is unavailable.",
66
+ });
67
+ }
68
+ const request = async (accessToken: string) => {
69
+ const form = new FormData();
70
+ form.append(
71
+ "file",
72
+ new Blob([Uint8Array.from(audio).buffer], { type: mimeType }),
73
+ filename,
74
+ );
75
+ return await fetchImpl(TRANSCRIBE_URL, {
76
+ method: "POST",
77
+ headers: {
78
+ Authorization: `Bearer ${accessToken}`,
79
+ "User-Agent": `opengeni/${XAI_CLIENT_VERSION}`,
80
+ "x-grok-client-version": XAI_CLIENT_VERSION,
81
+ "x-grok-client-identifier": "opengeni",
82
+ "x-grok-client-mode": XAI_CLIENT_MODE,
83
+ "x-grok-session-id": requestId,
84
+ },
85
+ body: form,
86
+ ...(signal ? { signal } : {}),
87
+ });
88
+ };
89
+ let response: Response;
90
+ try {
91
+ let token = await auth.context.getToken();
92
+ response = await request(token.accessToken);
93
+ if (response.status === 401) {
94
+ await response.body?.cancel().catch(() => undefined);
95
+ token = await auth.context.refresh();
96
+ response = await request(token.accessToken);
97
+ }
98
+ } catch (error) {
99
+ throw fetchError(error);
100
+ }
101
+ if (!response.ok) throw responseError(response.status);
102
+ const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
103
+ if (!body || typeof body.text !== "string") throw responseError(502);
104
+ return {
105
+ text: body.text,
106
+ languages: typeof body.language === "string" && body.language ? [body.language] : [],
107
+ };
108
+ },
109
+ };
110
+ }
@@ -14,6 +14,7 @@ import type { Database } from "@opengeni/db";
14
14
  import { createAzureOpenAiTranscriptionProvider } from "./providers/azure-openai";
15
15
  import { createCodexSubscriptionTranscriptionProvider } from "./providers/codex-subscription";
16
16
  import { createOpenAiTranscriptionProvider } from "./providers/openai";
17
+ import { createXaiSubscriptionTranscriptionProvider } from "./providers/xai-subscription";
17
18
 
18
19
  export function createTranscriptionService(input: {
19
20
  settings: Settings;
@@ -46,6 +47,12 @@ export function createTranscriptionService(input: {
46
47
  ...(input.codexFetch ? { fetch: input.codexFetch } : {}),
47
48
  ...(input.probeCodex ? { probe: input.probeCodex } : {}),
48
49
  });
50
+ case "supergrok-subscription":
51
+ return createXaiSubscriptionTranscriptionProvider({
52
+ settings: input.settings,
53
+ db: input.db,
54
+ ...(input.fetch ? { fetch: input.fetch } : {}),
55
+ });
49
56
  default:
50
57
  throw new Error("Unsupported voice-input provider.");
51
58
  }
@@ -95,8 +102,14 @@ export function createTranscriptionService(input: {
95
102
  });
96
103
  }
97
104
  const provider = request.providerId
98
- ? await exactAvailable(providers, request.providerId, { workspaceId: request.workspaceId })
99
- : await firstAvailable(providers, { workspaceId: request.workspaceId });
105
+ ? await exactAvailable(providers, request.providerId, {
106
+ workspaceId: request.workspaceId,
107
+ subjectId: request.subjectId,
108
+ })
109
+ : await firstAvailable(providers, {
110
+ workspaceId: request.workspaceId,
111
+ subjectId: request.subjectId,
112
+ });
100
113
  if (!provider) {
101
114
  throw new TranscriptionServiceError({
102
115
  code: "unavailable",
@@ -131,6 +144,8 @@ export function createTranscriptionService(input: {
131
144
  mimeType,
132
145
  filename: filenameForMimeType(mimeType),
133
146
  workspaceId: request.workspaceId,
147
+ accountId: request.accountId,
148
+ subjectId: request.subjectId,
134
149
  requestId: request.requestId,
135
150
  signal: deadline.signal,
136
151
  });
@@ -0,0 +1,216 @@
1
+ import { SUPERGROK_REALTIME_MODEL_ID, type Settings } from "@opengeni/config";
2
+ import type { GatewayRealtimeInitialItem } from "@opengeni/contracts";
3
+ import {
4
+ getActiveSessionHistoryItems,
5
+ getSessionRealtimeContinuityEntries,
6
+ getXaiSessionAccountPin,
7
+ resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
8
+ setXaiSessionAccountPin,
9
+ type Database,
10
+ } from "@opengeni/db";
11
+ import {
12
+ XAI_CLIENT_MODE,
13
+ XAI_CLIENT_VERSION,
14
+ XAI_PUBLIC_API_BASE_URL,
15
+ type XaiFetch,
16
+ } from "@opengeni/xai-subscription";
17
+
18
+ import { openGeniRealtimeInstructions } from "./codex-realtime";
19
+ import { projectSessionRealtimeInitialItems } from "./session-realtime-context";
20
+ import { buildXaiSubscriptionAuthorization } from "./xai-subscription-auth";
21
+
22
+ const UPSTREAM_MODEL_ID = "grok-voice-think-fast-2.0";
23
+
24
+ export class XaiRealtimeBrokerError extends Error {
25
+ constructor(
26
+ readonly code:
27
+ | "model_unavailable"
28
+ | "credential_unavailable"
29
+ | "provider_error"
30
+ | "invalid_provider_response",
31
+ message: string,
32
+ readonly providerStatus: number | null = null,
33
+ ) {
34
+ super(message);
35
+ this.name = "XaiRealtimeBrokerError";
36
+ }
37
+ }
38
+
39
+ export type XaiRealtimeConnectionSecret = {
40
+ token: string;
41
+ url: string;
42
+ upstreamModelId: string;
43
+ expiresAt: number | null;
44
+ initialItems: GatewayRealtimeInitialItem[];
45
+ instructions: string;
46
+ };
47
+
48
+ export async function createXaiRealtimeConnectionSecret(input: {
49
+ db: Database;
50
+ settings: Settings;
51
+ accountId: string;
52
+ workspaceId: string;
53
+ subjectId: string;
54
+ sessionId: string;
55
+ model: string;
56
+ fetchImpl?: typeof fetch;
57
+ }): Promise<XaiRealtimeConnectionSecret> {
58
+ if (input.model !== SUPERGROK_REALTIME_MODEL_ID) {
59
+ throw new XaiRealtimeBrokerError(
60
+ "model_unavailable",
61
+ "The selected model is not a connected SuperGrok realtime model",
62
+ );
63
+ }
64
+ const authoritySnapshot = await resolveXaiProviderAccountAuthoritySnapshotForAcceptance(
65
+ input.db,
66
+ { workspaceId: input.workspaceId, subjectId: input.subjectId },
67
+ );
68
+ let pin = await getXaiSessionAccountPin(input.db, {
69
+ workspaceId: input.workspaceId,
70
+ subjectId: input.subjectId,
71
+ sessionId: input.sessionId,
72
+ authoritySnapshot,
73
+ });
74
+ let auth: Awaited<ReturnType<typeof buildXaiSubscriptionAuthorization>>;
75
+ try {
76
+ auth = await buildXaiSubscriptionAuthorization({
77
+ db: input.db,
78
+ settings: input.settings,
79
+ accountId: input.accountId,
80
+ workspaceId: input.workspaceId,
81
+ subjectId: input.subjectId,
82
+ shardKey: input.sessionId,
83
+ pinnedCredentialId: pin?.pinnedCredentialId ?? null,
84
+ pinSource: pin?.pinSource === "manual" || pin?.pinSource === "policy" ? pin.pinSource : null,
85
+ authoritySnapshot,
86
+ sessionId: input.sessionId,
87
+ ...(input.fetchImpl ? { fetch: input.fetchImpl as XaiFetch } : {}),
88
+ });
89
+ if (
90
+ auth.rotationEnabled &&
91
+ pin?.pinSource !== "manual" &&
92
+ pin?.pinnedCredentialId !== auth.credentialId
93
+ ) {
94
+ try {
95
+ pin = await setXaiSessionAccountPin(input.db, {
96
+ accountId: input.accountId,
97
+ workspaceId: input.workspaceId,
98
+ subjectId: input.subjectId,
99
+ sessionId: input.sessionId,
100
+ authoritySnapshot,
101
+ credentialId: auth.credentialId,
102
+ pinSource: "policy",
103
+ expectedVersion: pin?.version ?? null,
104
+ });
105
+ } catch {
106
+ const current = await getXaiSessionAccountPin(input.db, {
107
+ workspaceId: input.workspaceId,
108
+ subjectId: input.subjectId,
109
+ sessionId: input.sessionId,
110
+ authoritySnapshot,
111
+ });
112
+ if (current?.pinSource === "manual" && current.pinnedCredentialId !== auth.credentialId) {
113
+ auth = await buildXaiSubscriptionAuthorization({
114
+ db: input.db,
115
+ settings: input.settings,
116
+ accountId: input.accountId,
117
+ workspaceId: input.workspaceId,
118
+ subjectId: input.subjectId,
119
+ shardKey: input.sessionId,
120
+ pinnedCredentialId: current.pinnedCredentialId,
121
+ pinSource: "manual",
122
+ authoritySnapshot,
123
+ sessionId: input.sessionId,
124
+ ...(input.fetchImpl ? { fetch: input.fetchImpl as XaiFetch } : {}),
125
+ });
126
+ }
127
+ }
128
+ }
129
+ } catch {
130
+ throw new XaiRealtimeBrokerError(
131
+ "credential_unavailable",
132
+ "No eligible connected SuperGrok account is available",
133
+ );
134
+ }
135
+
136
+ const [history, continuity, minted] = await Promise.all([
137
+ getActiveSessionHistoryItems(input.db, input.workspaceId, input.sessionId),
138
+ getSessionRealtimeContinuityEntries(input.db, input.workspaceId, input.sessionId),
139
+ mintXaiClientSecret({
140
+ auth,
141
+ fetchImpl: input.fetchImpl ?? fetch,
142
+ }),
143
+ ]);
144
+ return {
145
+ ...minted,
146
+ upstreamModelId: UPSTREAM_MODEL_ID,
147
+ initialItems: projectSessionRealtimeInitialItems(history, continuity),
148
+ instructions: openGeniRealtimeInstructions(),
149
+ };
150
+ }
151
+
152
+ async function mintXaiClientSecret(input: {
153
+ auth: Awaited<ReturnType<typeof buildXaiSubscriptionAuthorization>>;
154
+ fetchImpl: typeof fetch;
155
+ }): Promise<{ token: string; url: string; expiresAt: number | null }> {
156
+ const request = async (accessToken: string) =>
157
+ await input.fetchImpl(`${XAI_PUBLIC_API_BASE_URL}/realtime/client_secrets`, {
158
+ method: "POST",
159
+ headers: {
160
+ authorization: `Bearer ${accessToken}`,
161
+ "content-type": "application/json",
162
+ "user-agent": `opengeni/${XAI_CLIENT_VERSION}`,
163
+ "x-grok-client-version": XAI_CLIENT_VERSION,
164
+ "x-grok-client-identifier": "opengeni",
165
+ "x-grok-client-mode": XAI_CLIENT_MODE,
166
+ },
167
+ body: JSON.stringify({ expires_after: { seconds: 120 } }),
168
+ });
169
+ let response: Response;
170
+ try {
171
+ let token = await input.auth.context.getToken();
172
+ response = await request(token.accessToken);
173
+ if (response.status === 401) {
174
+ await response.body?.cancel().catch(() => undefined);
175
+ token = await input.auth.context.refresh();
176
+ response = await request(token.accessToken);
177
+ }
178
+ } catch {
179
+ throw new XaiRealtimeBrokerError("provider_error", "xAI realtime token request failed");
180
+ }
181
+ if (!response.ok) {
182
+ throw new XaiRealtimeBrokerError(
183
+ response.status === 401 || response.status === 403
184
+ ? "credential_unavailable"
185
+ : "provider_error",
186
+ response.status === 401 || response.status === 403
187
+ ? "SuperGrok credentials were rejected"
188
+ : "xAI realtime token request failed",
189
+ response.status,
190
+ );
191
+ }
192
+ const body = (await response.json().catch(() => null)) as Record<string, unknown> | null;
193
+ const token = body?.value;
194
+ const expiresAt = normalizeExpiry(body?.expires_at ?? body?.expiresAt);
195
+ if (typeof token !== "string" || token.length === 0) {
196
+ throw new XaiRealtimeBrokerError(
197
+ "invalid_provider_response",
198
+ "xAI returned an invalid realtime token",
199
+ response.status,
200
+ );
201
+ }
202
+ const url = new URL("wss://api.x.ai/v1/realtime");
203
+ url.searchParams.set("model", UPSTREAM_MODEL_ID);
204
+ return { token, url: url.toString(), expiresAt };
205
+ }
206
+
207
+ function normalizeExpiry(value: unknown): number | null {
208
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
209
+ return Math.trunc(value < 10_000_000_000 ? value * 1_000 : value);
210
+ }
211
+ if (typeof value === "string") {
212
+ const parsed = Date.parse(value);
213
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
214
+ }
215
+ return null;
216
+ }