@opengeni/api-router 0.23.1 → 0.26.1

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 (107) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/browser-controller-authority.d.ts +43 -0
  4. package/dist/browser-state-authority.d.ts +27 -0
  5. package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
  6. package/dist/chunk-JIKNR5YL.js.map +1 -0
  7. package/dist/codemode.d.ts +23 -0
  8. package/dist/editable-artifact-live-hints.d.ts +11 -0
  9. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  10. package/dist/editable-artifact-office-import.d.ts +22 -0
  11. package/dist/editable-artifact-production.d.ts +29 -0
  12. package/dist/editable-artifact-websocket.d.ts +49 -0
  13. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  14. package/dist/github-browser-flow.d.ts +6 -0
  15. package/dist/http/cors.d.ts +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1796 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/api-integrations.d.ts +24 -0
  20. package/dist/integrations/atlassian.d.ts +176 -0
  21. package/dist/integrations/github-skill-source.d.ts +5 -0
  22. package/dist/integrations/google-drive.d.ts +85 -0
  23. package/dist/integrations/oauth-client.d.ts +22 -1
  24. package/dist/integrations/provider-oauth.d.ts +19 -0
  25. package/dist/integrations/slack-bot.d.ts +4 -0
  26. package/dist/integrations/slack-interactions.d.ts +8 -2
  27. package/dist/integrations/social-api.d.ts +2 -1
  28. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  29. package/dist/mcp/editable-artifacts.d.ts +13 -0
  30. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  31. package/dist/mcp/server.d.ts +14 -3
  32. package/dist/memory-slack-delivery.d.ts +9 -0
  33. package/dist/routes/api-integrations.d.ts +8 -0
  34. package/dist/routes/browser-identities.d.ts +5 -0
  35. package/dist/routes/browser-sessions.d.ts +6 -0
  36. package/dist/routes/company-profile.d.ts +3 -0
  37. package/dist/routes/computer-sessions.d.ts +6 -0
  38. package/dist/routes/editable-artifacts.d.ts +44 -0
  39. package/dist/routes/integration-features.d.ts +3 -0
  40. package/dist/routes/memory-slack-publications.d.ts +6 -0
  41. package/dist/routes/plugins.d.ts +8 -0
  42. package/dist/routes/sessions.d.ts +17 -2
  43. package/dist/routes/skills.d.ts +6 -0
  44. package/dist/routes/video-generation.d.ts +3 -0
  45. package/dist/sandbox/channel-a.d.ts +55 -2
  46. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  47. package/dist/sandbox/viewer.d.ts +4 -2
  48. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  49. package/package.json +19 -14
  50. package/src/app.ts +274 -37
  51. package/src/browser-controller-authority.ts +137 -0
  52. package/src/browser-state-authority.ts +236 -0
  53. package/src/codemode.ts +186 -0
  54. package/src/editable-artifact-live-hints.ts +64 -0
  55. package/src/editable-artifact-native-kernel.ts +659 -0
  56. package/src/editable-artifact-office-import.ts +230 -0
  57. package/src/editable-artifact-production.ts +419 -0
  58. package/src/editable-artifact-websocket.ts +311 -0
  59. package/src/editable-artifact-workspace-files.ts +186 -0
  60. package/src/github-browser-flow.ts +35 -6
  61. package/src/http/auth.ts +2 -0
  62. package/src/http/cors.ts +3 -0
  63. package/src/index.ts +119 -20
  64. package/src/integrations/api-integrations.ts +350 -0
  65. package/src/integrations/atlassian.ts +1621 -0
  66. package/src/integrations/github-skill-source.ts +142 -0
  67. package/src/integrations/google-drive.ts +1000 -64
  68. package/src/integrations/oauth-client.ts +123 -33
  69. package/src/integrations/provider-oauth.ts +777 -0
  70. package/src/integrations/slack-bot.ts +31 -2
  71. package/src/integrations/slack-interactions.ts +487 -27
  72. package/src/integrations/social-api.ts +11 -0
  73. package/src/mcp/documents.ts +32 -1
  74. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  75. package/src/mcp/editable-artifacts.ts +448 -0
  76. package/src/mcp/scheduled-task-view.ts +34 -0
  77. package/src/mcp/server.ts +923 -145
  78. package/src/memory-slack-delivery.ts +209 -0
  79. package/src/routes/api-integrations.ts +407 -0
  80. package/src/routes/browser-identities.ts +136 -0
  81. package/src/routes/browser-sessions.ts +2543 -0
  82. package/src/routes/company-profile.ts +255 -0
  83. package/src/routes/computer-sessions.ts +1247 -0
  84. package/src/routes/connections.ts +284 -99
  85. package/src/routes/documents.ts +22 -3
  86. package/src/routes/editable-artifacts.ts +1159 -0
  87. package/src/routes/files.ts +106 -4
  88. package/src/routes/github.ts +18 -2
  89. package/src/routes/install.ts +7 -1
  90. package/src/routes/integration-features.ts +258 -0
  91. package/src/routes/memory-slack-publications.ts +216 -0
  92. package/src/routes/packs.ts +437 -7
  93. package/src/routes/plugins.ts +751 -0
  94. package/src/routes/rigs.ts +77 -20
  95. package/src/routes/scheduled-tasks.ts +67 -40
  96. package/src/routes/sessions.ts +421 -245
  97. package/src/routes/skills.ts +174 -0
  98. package/src/routes/video-generation.ts +132 -0
  99. package/src/routes/workspaces.ts +46 -24
  100. package/src/sandbox/channel-a.ts +723 -116
  101. package/src/sandbox/metrics-ingestion.ts +121 -3
  102. package/src/sandbox/rematerialize.ts +35 -47
  103. package/src/sandbox/viewer.ts +29 -9
  104. package/src/temporal-schedule-cleanup.ts +135 -0
  105. package/dist/chunk-T4T2PGU4.js.map +0 -1
  106. package/dist/mcp/toolspace.d.ts +0 -71
  107. package/src/mcp/toolspace.ts +0 -1190
@@ -0,0 +1,236 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { BROWSER_PROFILE_ARTIFACT_FORMAT } from "@opengeni/contracts";
3
+
4
+ const ROOT_KEY_BYTES = 32;
5
+ const DATA_KEY_BYTES = 32;
6
+ const IV_BYTES = 12;
7
+ const TAG_BYTES = 16;
8
+ const WRAPPED_PREFIX = "ogbk1";
9
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
10
+ 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}$/iu;
11
+
12
+ export type BrowserStateOperationScope = {
13
+ accountId: string;
14
+ workspaceId: string;
15
+ browserSessionId: string;
16
+ operationId: string;
17
+ objectKey: string;
18
+ };
19
+
20
+ export type BrowserStateArtifactScope = {
21
+ accountId: string;
22
+ workspaceId: string;
23
+ objectKey: string;
24
+ artifactDigest: string;
25
+ contentDigest: string;
26
+ };
27
+
28
+ /** Deterministic only within one idempotent publication operation. This lets a
29
+ * control-plane retry present browserd with exactly the same secret authority
30
+ * without storing a plaintext key or placing it in an operation row. */
31
+ export function deriveBrowserStateDataKey(
32
+ rootKey: Uint8Array,
33
+ scopeInput: BrowserStateOperationScope,
34
+ ): Buffer {
35
+ const scope = operationScope(scopeInput);
36
+ return createHmac("sha256", root(rootKey))
37
+ .update("opengeni.browser-state.data-key.v1\0", "utf8")
38
+ .update(canonicalJson(scope), "utf8")
39
+ .digest();
40
+ }
41
+
42
+ /** Associated data for the encrypted profile object. It is reproducible from
43
+ * durable non-secret artifact authority during restore. */
44
+ export function browserStateArtifactAad(
45
+ scopeInput: Pick<BrowserStateArtifactScope, "accountId" | "workspaceId" | "objectKey">,
46
+ ): Buffer {
47
+ const scope = baseArtifactScope(scopeInput);
48
+ return Buffer.from(`opengeni.browser-state.artifact-aad.v1\0${canonicalJson(scope)}`, "utf8");
49
+ }
50
+
51
+ /** Wrap a 32-byte artifact data key under the operator-held root key. The
52
+ * ciphertext is bound to workspace, object, and both integrity digests. */
53
+ export function wrapBrowserStateDataKey(
54
+ rootKey: Uint8Array,
55
+ dataKeyInput: Uint8Array,
56
+ scopeInput: BrowserStateArtifactScope,
57
+ ): string {
58
+ const dataKey = exactKey(dataKeyInput, "browser state data key");
59
+ const scope = artifactScope(scopeInput);
60
+ const iv = randomBytes(IV_BYTES);
61
+ const wrappingKey = browserStateWrappingKey(rootKey, scope);
62
+ try {
63
+ const cipher = createCipheriv("aes-256-gcm", wrappingKey, iv);
64
+ cipher.setAAD(wrapAad(scope));
65
+ const ciphertext = Buffer.concat([cipher.update(dataKey), cipher.final()]);
66
+ const payload = Buffer.concat([ciphertext, cipher.getAuthTag()]);
67
+ return `${WRAPPED_PREFIX}:${iv.toString("base64")}:${payload.toString("base64")}`;
68
+ } finally {
69
+ wrappingKey.fill(0);
70
+ }
71
+ }
72
+
73
+ export function unwrapBrowserStateDataKey(
74
+ rootKey: Uint8Array,
75
+ wrapped: string,
76
+ scopeInput: BrowserStateArtifactScope,
77
+ ): Buffer {
78
+ const scope = artifactScope(scopeInput);
79
+ const parts = wrapped.split(":");
80
+ if (parts.length !== 3 || parts[0] !== WRAPPED_PREFIX) {
81
+ throw new Error("browser state data-key envelope is unsupported");
82
+ }
83
+ const iv = canonicalBase64(parts[1]!, IV_BYTES, "browser state data-key envelope");
84
+ const payload = canonicalBase64(
85
+ parts[2]!,
86
+ DATA_KEY_BYTES + TAG_BYTES,
87
+ "browser state data-key envelope",
88
+ );
89
+ const ciphertext = payload.subarray(0, DATA_KEY_BYTES);
90
+ const tag = payload.subarray(DATA_KEY_BYTES);
91
+ const wrappingKey = browserStateWrappingKey(rootKey, scope);
92
+ try {
93
+ const decipher = createDecipheriv("aes-256-gcm", wrappingKey, iv);
94
+ decipher.setAAD(wrapAad(scope));
95
+ decipher.setAuthTag(tag);
96
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
97
+ } catch {
98
+ throw new Error("browser state data-key decryption failed");
99
+ } finally {
100
+ wrappingKey.fill(0);
101
+ }
102
+ }
103
+
104
+ export function browserStateObjectKey(workspaceIdInput: string, operationIdInput: string): string {
105
+ const workspaceId = uuid(workspaceIdInput, "workspace id");
106
+ const operationId = uuid(operationIdInput, "browser state operation id");
107
+ return `workspaces/${workspaceId}/browser-state/revisions/${operationId}/chromium-profile.ogbs`;
108
+ }
109
+
110
+ export function browserStateManifestDigest(manifest: unknown): string {
111
+ return createHash("sha256").update(canonicalJson(manifest), "utf8").digest("hex");
112
+ }
113
+
114
+ function operationScope(input: BrowserStateOperationScope) {
115
+ const base = baseArtifactScope(input);
116
+ return {
117
+ ...base,
118
+ browserSessionId: uuid(input.browserSessionId, "browser session id"),
119
+ operationId: uuid(input.operationId, "browser state operation id"),
120
+ };
121
+ }
122
+
123
+ function artifactScope(input: BrowserStateArtifactScope) {
124
+ return {
125
+ ...baseArtifactScope(input),
126
+ artifactDigest: sha256(input.artifactDigest, "browser state artifact digest"),
127
+ contentDigest: sha256(input.contentDigest, "browser state content digest"),
128
+ };
129
+ }
130
+
131
+ function baseArtifactScope(
132
+ input: Pick<BrowserStateArtifactScope, "accountId" | "workspaceId" | "objectKey">,
133
+ ) {
134
+ const workspaceId = uuid(input.workspaceId, "workspace id");
135
+ const objectKey = browserStateObjectKeyValue(input.objectKey, workspaceId);
136
+ return {
137
+ version: 1,
138
+ accountId: uuid(input.accountId, "account id"),
139
+ workspaceId,
140
+ objectKey,
141
+ format: BROWSER_PROFILE_ARTIFACT_FORMAT,
142
+ };
143
+ }
144
+
145
+ function wrapAad(scope: ReturnType<typeof artifactScope>): Buffer {
146
+ return Buffer.from(`opengeni.browser-state.data-key-wrap.v1\0${canonicalJson(scope)}`, "utf8");
147
+ }
148
+
149
+ function browserStateWrappingKey(
150
+ rootKey: Uint8Array,
151
+ scope: ReturnType<typeof artifactScope>,
152
+ ): Buffer {
153
+ return createHmac("sha256", root(rootKey))
154
+ .update("opengeni.browser-state.wrapping-key.v1\0", "utf8")
155
+ .update(canonicalJson(scope), "utf8")
156
+ .digest();
157
+ }
158
+
159
+ function browserStateObjectKeyValue(value: string, workspaceId: string): string {
160
+ if (
161
+ typeof value !== "string" ||
162
+ Buffer.byteLength(value) > 2_048 ||
163
+ !new RegExp(
164
+ `^workspaces/${workspaceId}/browser-state/[A-Za-z0-9._=-]+(?:/[A-Za-z0-9._=-]+)*$`,
165
+ "u",
166
+ ).test(value)
167
+ ) {
168
+ throw new Error("browser state object key is invalid");
169
+ }
170
+ return value;
171
+ }
172
+
173
+ function root(value: Uint8Array): Buffer {
174
+ return exactKey(value, "browser state root key");
175
+ }
176
+
177
+ function exactKey(value: Uint8Array, label: string): Buffer {
178
+ if (!(value instanceof Uint8Array) || value.byteLength !== ROOT_KEY_BYTES) {
179
+ throw new Error(`${label} must be exactly ${ROOT_KEY_BYTES} bytes`);
180
+ }
181
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
182
+ }
183
+
184
+ function uuid(value: string, label: string): string {
185
+ if (typeof value !== "string" || !UUID_PATTERN.test(value)) {
186
+ throw new Error(`${label} must be a UUID`);
187
+ }
188
+ return value.toLowerCase();
189
+ }
190
+
191
+ function sha256(value: string, label: string): string {
192
+ if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
193
+ throw new Error(`${label} is invalid`);
194
+ }
195
+ return value;
196
+ }
197
+
198
+ function canonicalBase64(value: string, length: number, label: string): Buffer {
199
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)) {
200
+ throw new Error(`${label} is unsupported`);
201
+ }
202
+ const decoded = Buffer.from(value, "base64");
203
+ if (decoded.byteLength !== length || decoded.toString("base64") !== value) {
204
+ decoded.fill(0);
205
+ throw new Error(`${label} is unsupported`);
206
+ }
207
+ return decoded;
208
+ }
209
+
210
+ function canonicalJson(value: unknown): string {
211
+ return JSON.stringify(canonicalValue(value));
212
+ }
213
+
214
+ function canonicalValue(value: unknown): unknown {
215
+ if (
216
+ value === null ||
217
+ typeof value === "string" ||
218
+ typeof value === "boolean" ||
219
+ (typeof value === "number" && Number.isFinite(value))
220
+ ) {
221
+ return value;
222
+ }
223
+ if (Array.isArray(value)) return value.map(canonicalValue);
224
+ if (typeof value === "object") {
225
+ const input = value as Record<string, unknown>;
226
+ const output: Record<string, unknown> = {};
227
+ for (const key of Object.keys(input).sort()) {
228
+ if (input[key] === undefined) {
229
+ throw new Error("canonical JSON cannot contain undefined");
230
+ }
231
+ output[key] = canonicalValue(input[key]);
232
+ }
233
+ return output;
234
+ }
235
+ throw new Error("value cannot be represented as canonical JSON");
236
+ }
@@ -0,0 +1,186 @@
1
+ import {
2
+ codemodeDispatchSubject,
3
+ decodeCodemodeDispatchAck,
4
+ encodeCodemodeDispatchRequest,
5
+ } from "@opengeni/codemode";
6
+ import {
7
+ CODEMODE_DISPATCH_TIMEOUT_MS,
8
+ CodemodeCallRequest,
9
+ CodemodeCallSubmission,
10
+ type AccessGrant,
11
+ type AttemptToolCatalog,
12
+ type CodemodeCallSubmission as CodemodeCallSubmissionValue,
13
+ type CodemodeOperation,
14
+ } from "@opengeni/contracts";
15
+ import type { ApiRouteDeps } from "@opengeni/core";
16
+ import { hasPermission, requireSessionAuthorization } from "@opengeni/core";
17
+ import {
18
+ getActiveSessionTurnForExecution,
19
+ getAttemptToolCatalog,
20
+ getCodemodeOperation,
21
+ submitCodemodeOperation,
22
+ } from "@opengeni/db";
23
+
24
+ export type CodemodeGrantAuthority = {
25
+ accountId: string;
26
+ workspaceId: string;
27
+ sessionId: string;
28
+ turnId: string;
29
+ attemptId: string;
30
+ executionGeneration: number;
31
+ subjectId: string;
32
+ };
33
+
34
+ export class CodemodeAuthorityError extends Error {
35
+ readonly code = "codemode_authority_invalid";
36
+
37
+ constructor() {
38
+ super("Codemode bearer is not bound to an active execution attempt");
39
+ this.name = "CodemodeAuthorityError";
40
+ }
41
+ }
42
+
43
+ export function codemodeAuthorityForGrant(grant: AccessGrant): CodemodeGrantAuthority | null {
44
+ const metadata = grant.metadata;
45
+ if (
46
+ grant.principalKind !== "agent_attempt" ||
47
+ metadata?.delegated !== true ||
48
+ typeof metadata.sessionId !== "string" ||
49
+ typeof metadata.turnId !== "string" ||
50
+ typeof metadata.attemptId !== "string" ||
51
+ typeof metadata.executionGeneration !== "number" ||
52
+ !Number.isInteger(metadata.executionGeneration) ||
53
+ metadata.executionGeneration < 1
54
+ ) {
55
+ return null;
56
+ }
57
+ return {
58
+ accountId: grant.accountId,
59
+ workspaceId: grant.workspaceId,
60
+ sessionId: metadata.sessionId,
61
+ turnId: metadata.turnId,
62
+ attemptId: metadata.attemptId,
63
+ executionGeneration: metadata.executionGeneration,
64
+ subjectId: grant.subjectId,
65
+ };
66
+ }
67
+
68
+ export function isCodemodeGrant(grant: AccessGrant): boolean {
69
+ return (
70
+ hasPermission(grant.permissions, "codemode:call") && codemodeAuthorityForGrant(grant) !== null
71
+ );
72
+ }
73
+
74
+ export async function requireActiveCodemodeCatalog(
75
+ deps: ApiRouteDeps,
76
+ grant: AccessGrant,
77
+ ): Promise<{ authority: CodemodeGrantAuthority; catalog: AttemptToolCatalog }> {
78
+ if (!isCodemodeGrant(grant)) throw new CodemodeAuthorityError();
79
+ const authority = codemodeAuthorityForGrant(grant)!;
80
+ await requireSessionAuthorization(deps, grant, {
81
+ sessionId: authority.sessionId,
82
+ operation: "session.codemode.call",
83
+ surface: "codemode",
84
+ });
85
+ const active = await getActiveSessionTurnForExecution(
86
+ deps.db,
87
+ authority.workspaceId,
88
+ authority.sessionId,
89
+ );
90
+ if (
91
+ !active ||
92
+ active.status !== "running" ||
93
+ active.id !== authority.turnId ||
94
+ active.activeAttemptId !== authority.attemptId ||
95
+ active.executionGeneration !== authority.executionGeneration
96
+ ) {
97
+ throw new CodemodeAuthorityError();
98
+ }
99
+ const catalog = await getAttemptToolCatalog(deps.db, {
100
+ accountId: authority.accountId,
101
+ workspaceId: authority.workspaceId,
102
+ attemptId: authority.attemptId,
103
+ });
104
+ if (
105
+ !catalog ||
106
+ catalog.accountId !== authority.accountId ||
107
+ catalog.workspaceId !== authority.workspaceId ||
108
+ catalog.sessionId !== authority.sessionId ||
109
+ catalog.turnId !== authority.turnId ||
110
+ catalog.attemptId !== authority.attemptId ||
111
+ catalog.executionGeneration !== authority.executionGeneration
112
+ ) {
113
+ throw new CodemodeAuthorityError();
114
+ }
115
+ return { authority, catalog };
116
+ }
117
+
118
+ export async function submitAndDispatchCodemodeCall(
119
+ deps: ApiRouteDeps,
120
+ grant: AccessGrant,
121
+ rawRequest: unknown,
122
+ ): Promise<CodemodeCallSubmissionValue> {
123
+ const request = CodemodeCallRequest.parse(rawRequest);
124
+ const { authority, catalog } = await requireActiveCodemodeCatalog(deps, grant);
125
+ const submitted = await submitCodemodeOperation(deps.db, {
126
+ ...authority,
127
+ call: {
128
+ ...request,
129
+ caller: { kind: "codemode", subjectId: authority.subjectId },
130
+ },
131
+ callLimit: deps.settings.codemodeMaxCallsPerTurn,
132
+ });
133
+ let operation = submitted.operation;
134
+ let dispatch: CodemodeCallSubmissionValue["dispatch"] = terminal(operation)
135
+ ? "terminal"
136
+ : operation.state === "running"
137
+ ? "already_running"
138
+ : "unavailable";
139
+ if (operation.state === "queued") {
140
+ try {
141
+ const reply = await deps.bus.request(
142
+ codemodeDispatchSubject(authority.workspaceId, authority.attemptId),
143
+ encodeCodemodeDispatchRequest({
144
+ version: 1,
145
+ operationId: operation.operationId,
146
+ catalogDigest: catalog.digest,
147
+ }),
148
+ { timeoutMs: CODEMODE_DISPATCH_TIMEOUT_MS },
149
+ );
150
+ dispatch = decodeCodemodeDispatchAck(reply.data).status;
151
+ } catch {
152
+ dispatch = "unavailable";
153
+ }
154
+ operation =
155
+ (await getCodemodeOperation(deps.db, {
156
+ accountId: authority.accountId,
157
+ workspaceId: authority.workspaceId,
158
+ attemptId: authority.attemptId,
159
+ operationId: operation.operationId,
160
+ })) ?? operation;
161
+ if (terminal(operation)) dispatch = "terminal";
162
+ else if (operation.state === "running" && dispatch === "unavailable") {
163
+ dispatch = "already_running";
164
+ }
165
+ }
166
+ return CodemodeCallSubmission.parse({ operation, dispatch });
167
+ }
168
+
169
+ export async function readCodemodeOperation(
170
+ deps: ApiRouteDeps,
171
+ grant: AccessGrant,
172
+ operationId: string,
173
+ ): Promise<CodemodeOperation | null> {
174
+ if (!isCodemodeGrant(grant)) throw new CodemodeAuthorityError();
175
+ const authority = codemodeAuthorityForGrant(grant)!;
176
+ return await getCodemodeOperation(deps.db, {
177
+ accountId: authority.accountId,
178
+ workspaceId: authority.workspaceId,
179
+ attemptId: authority.attemptId,
180
+ operationId,
181
+ });
182
+ }
183
+
184
+ function terminal(operation: CodemodeOperation): boolean {
185
+ return ["completed", "failed", "outcome_unknown", "cancelled"].includes(operation.state);
186
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ decodeEditableArtifactBrokerHint,
3
+ editableArtifactLiveHintSubject,
4
+ type EditableArtifactLiveHintPort,
5
+ } from "@opengeni/core";
6
+ import type { EventBus, OpStreamConnection } from "@opengeni/events";
7
+
8
+ /**
9
+ * Uses the API's existing resilient NATS connection. Hints carry no operation
10
+ * bytes; every callback causes a durable DB head read and contiguous gap-fill.
11
+ */
12
+ export class EventBusEditableArtifactLiveHints implements EditableArtifactLiveHintPort {
13
+ constructor(private readonly bus: Pick<EventBus, "getOpStreamConnection">) {}
14
+
15
+ async subscribe(
16
+ input: Parameters<EditableArtifactLiveHintPort["subscribe"]>[0],
17
+ ): Promise<() => void> {
18
+ const connection = this.bus.getOpStreamConnection?.();
19
+ if (!connection) throw new Error("Event bus has no artifact hint subscription transport");
20
+ const subject = editableArtifactLiveHintSubject(input.scope, input.artifactId);
21
+ const subscription = connection.subscribe(subject);
22
+ let active = true;
23
+ try {
24
+ await requireFlushBarrier(connection);
25
+ } catch (error) {
26
+ subscription.unsubscribe();
27
+ throw error;
28
+ }
29
+ void (async () => {
30
+ try {
31
+ for await (const message of subscription) {
32
+ if (!active) return;
33
+ try {
34
+ const hint = decodeEditableArtifactBrokerHint(message.data, {
35
+ scope: input.scope,
36
+ artifactId: input.artifactId,
37
+ });
38
+ input.onHint({
39
+ artifactId: input.artifactId,
40
+ headSequence: hint.headSequence,
41
+ });
42
+ } catch {
43
+ // Broker input is advisory and untrusted. Drop poison; periodic
44
+ // durable reconciliation remains authoritative.
45
+ }
46
+ }
47
+ } catch {
48
+ if (active) input.onReconnect();
49
+ }
50
+ })();
51
+ return () => {
52
+ if (!active) return;
53
+ active = false;
54
+ subscription.unsubscribe();
55
+ };
56
+ }
57
+ }
58
+
59
+ async function requireFlushBarrier(connection: OpStreamConnection): Promise<void> {
60
+ if (!connection.flush) {
61
+ throw new Error("Artifact hint subscription transport lacks a flush barrier");
62
+ }
63
+ await connection.flush();
64
+ }