@opengeni/api-router 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +16 -0
- package/dist/app.js +35 -0
- package/dist/app.js.map +1 -0
- package/dist/chunk-XSYUDIX3.js +6331 -0
- package/dist/chunk-XSYUDIX3.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/app.ts +351 -0
- package/src/auth/managed-auth.ts +237 -0
- package/src/http/auth.ts +92 -0
- package/src/http/common.ts +16 -0
- package/src/http/sse.ts +89 -0
- package/src/index.ts +362 -0
- package/src/mcp/documents.ts +57 -0
- package/src/mcp/server.ts +961 -0
- package/src/mcp/session-view.ts +281 -0
- package/src/routes/api-keys.ts +65 -0
- package/src/routes/billing.ts +495 -0
- package/src/routes/capabilities.ts +80 -0
- package/src/routes/codex.ts +393 -0
- package/src/routes/documents.ts +185 -0
- package/src/routes/enrollments.ts +357 -0
- package/src/routes/environments.ts +175 -0
- package/src/routes/files.ts +148 -0
- package/src/routes/github.ts +341 -0
- package/src/routes/install.ts +218 -0
- package/src/routes/machines.ts +107 -0
- package/src/routes/packs.ts +241 -0
- package/src/routes/scheduled-tasks.ts +126 -0
- package/src/routes/sessions.ts +1083 -0
- package/src/routes/social.ts +119 -0
- package/src/routes/workspaces.ts +206 -0
- package/src/sandbox/access.ts +89 -0
- package/src/sandbox/auth-callout.ts +178 -0
- package/src/sandbox/channel-a.ts +265 -0
- package/src/sandbox/enrollment.ts +498 -0
- package/src/sandbox/machines.ts +255 -0
- package/src/sandbox/metrics-ingestion.ts +289 -0
- package/src/sandbox/viewer.ts +993 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getSettings } from '@opengeni/config';
|
|
2
|
+
import { ScheduledTaskOverlapPolicy, ScheduledTaskScheduleSpec } from '@opengeni/contracts';
|
|
3
|
+
import { ScheduleOverlapPolicy, ScheduleSpec } from '@temporalio/client';
|
|
4
|
+
import { SessionWorkflowClient, DocumentIndexClient } from '@opengeni/core';
|
|
5
|
+
|
|
6
|
+
declare function createTemporalWorkflowClient(settings: ReturnType<typeof getSettings>): Promise<{
|
|
7
|
+
client: SessionWorkflowClient;
|
|
8
|
+
documentIndexer: DocumentIndexClient;
|
|
9
|
+
close: () => Promise<void>;
|
|
10
|
+
}>;
|
|
11
|
+
declare function startApi(): Promise<{
|
|
12
|
+
server: Bun.Server<undefined>;
|
|
13
|
+
close: () => Promise<void>;
|
|
14
|
+
}>;
|
|
15
|
+
declare function temporalOverlapPolicy(policy: ScheduledTaskOverlapPolicy): ScheduleOverlapPolicy;
|
|
16
|
+
declare function shouldCreateScheduleAfterUpdateError(error: unknown): boolean;
|
|
17
|
+
declare function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): ScheduleSpec;
|
|
18
|
+
|
|
19
|
+
export { createTemporalWorkflowClient, shouldCreateScheduleAfterUpdateError, startApi, temporalOverlapPolicy, temporalScheduleSpec };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApp
|
|
3
|
+
} from "./chunk-XSYUDIX3.js";
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { dbSearchPath, getSettings, resolveNatsCalloutConfig, resolveNatsControlPlaneAuth, retryStartupDependency, startupRetryOptions } from "@opengeni/config";
|
|
7
|
+
import { createDb } from "@opengeni/db";
|
|
8
|
+
import { createNatsEventBus } from "@opengeni/events";
|
|
9
|
+
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
10
|
+
import { Connection, Client as TemporalClient, ScheduleNotFoundError, ScheduleOverlapPolicy, WorkflowExecutionAlreadyStartedError } from "@temporalio/client";
|
|
11
|
+
|
|
12
|
+
// src/sandbox/auth-callout.ts
|
|
13
|
+
import { resolveEnrollmentSigningSecret } from "@opengeni/config";
|
|
14
|
+
import { verifyEnrollmentBearer } from "@opengeni/contracts";
|
|
15
|
+
import { getEnrollment } from "@opengeni/db";
|
|
16
|
+
import {
|
|
17
|
+
createResponderConnection,
|
|
18
|
+
decodeAuthRequest,
|
|
19
|
+
mintAuthResponse,
|
|
20
|
+
mintUserJwt,
|
|
21
|
+
workspaceAgentPermissions
|
|
22
|
+
} from "@opengeni/events";
|
|
23
|
+
var AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
|
|
24
|
+
async function handleAuthorizationRequest(deps, requestBytes) {
|
|
25
|
+
const requestJwt = Buffer.from(requestBytes).toString("utf8");
|
|
26
|
+
const decoded = decodeAuthRequest(requestJwt);
|
|
27
|
+
if (!decoded) {
|
|
28
|
+
deps.observability?.warn?.("auth-callout: undecodable authorization request", {});
|
|
29
|
+
throw new Error("undecodable authorization request");
|
|
30
|
+
}
|
|
31
|
+
const deny = (reason) => {
|
|
32
|
+
const response2 = mintAuthResponse({
|
|
33
|
+
userPublicKey: decoded.userNkey,
|
|
34
|
+
serverId: decoded.serverId,
|
|
35
|
+
accountSeed: deps.callout.accountSeed,
|
|
36
|
+
error: reason
|
|
37
|
+
});
|
|
38
|
+
return Buffer.from(response2, "utf8");
|
|
39
|
+
};
|
|
40
|
+
const bearer = decoded.authToken;
|
|
41
|
+
if (!bearer) {
|
|
42
|
+
return deny("missing enrollment bearer");
|
|
43
|
+
}
|
|
44
|
+
const secret = resolveEnrollmentSigningSecret(deps.settings);
|
|
45
|
+
if (!secret) {
|
|
46
|
+
return deny("enrollment credential plane disabled");
|
|
47
|
+
}
|
|
48
|
+
const claims = await verifyEnrollmentBearer(secret, bearer);
|
|
49
|
+
if (!claims) {
|
|
50
|
+
deps.observability?.warn?.("auth-callout: rejected an invalid enrollment bearer", {});
|
|
51
|
+
return deny("invalid or expired enrollment bearer");
|
|
52
|
+
}
|
|
53
|
+
const enrollment = await getEnrollment(deps.db, claims.workspaceId, claims.enrollmentId);
|
|
54
|
+
if (!enrollment || enrollment.status !== "active") {
|
|
55
|
+
deps.observability?.warn?.("auth-callout: denied a revoked or unknown enrollment", {
|
|
56
|
+
workspaceId: claims.workspaceId,
|
|
57
|
+
agentId: claims.agentId
|
|
58
|
+
});
|
|
59
|
+
return deny("enrollment is not active");
|
|
60
|
+
}
|
|
61
|
+
if (enrollment.id !== claims.enrollmentId) {
|
|
62
|
+
return deny("enrollment identity mismatch");
|
|
63
|
+
}
|
|
64
|
+
const permissions = workspaceAgentPermissions(claims.workspaceId);
|
|
65
|
+
const userJwt = mintUserJwt({
|
|
66
|
+
userPublicKey: decoded.userNkey,
|
|
67
|
+
accountSeed: deps.callout.accountSeed,
|
|
68
|
+
name: claims.agentId,
|
|
69
|
+
permissions,
|
|
70
|
+
// Server-config-mode placement: the embedded user JWT's `aud` is the account
|
|
71
|
+
// the user binds to (the configured `auth_callout.account`). All agents +
|
|
72
|
+
// the privileged control plane share this account so subjects route; the
|
|
73
|
+
// per-workspace isolation is carried by the subject permissions above.
|
|
74
|
+
audienceAccount: deps.callout.accountName,
|
|
75
|
+
// Tie the credential's life to the bearer's remaining life: a revoked/expired
|
|
76
|
+
// enrollment cannot outlive its bearer at the NATS layer either.
|
|
77
|
+
expiresAtSeconds: claims.exp
|
|
78
|
+
});
|
|
79
|
+
const response = mintAuthResponse({
|
|
80
|
+
userPublicKey: decoded.userNkey,
|
|
81
|
+
serverId: decoded.serverId,
|
|
82
|
+
accountSeed: deps.callout.accountSeed,
|
|
83
|
+
userJwt
|
|
84
|
+
});
|
|
85
|
+
deps.observability?.info?.("auth-callout: granted a workspace-scoped NATS credential", {
|
|
86
|
+
workspaceId: claims.workspaceId,
|
|
87
|
+
agentId: claims.agentId
|
|
88
|
+
});
|
|
89
|
+
return Buffer.from(response, "utf8");
|
|
90
|
+
}
|
|
91
|
+
async function startAuthCalloutResponder(deps, natsUrl) {
|
|
92
|
+
const connection = await createResponderConnection(
|
|
93
|
+
natsUrl,
|
|
94
|
+
{ kind: "user-password", user: deps.callout.user, pass: deps.callout.password },
|
|
95
|
+
AUTH_CALLOUT_SUBJECT,
|
|
96
|
+
(bytes) => handleAuthorizationRequest(deps, bytes),
|
|
97
|
+
{ name: "opengeni-auth-callout" }
|
|
98
|
+
);
|
|
99
|
+
deps.observability?.info?.("OpenGeni NATS auth-callout responder started", {
|
|
100
|
+
subject: AUTH_CALLOUT_SUBJECT
|
|
101
|
+
});
|
|
102
|
+
return connection;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/sandbox/metrics-ingestion.ts
|
|
106
|
+
import {
|
|
107
|
+
getEnrollment as getEnrollment2,
|
|
108
|
+
ingestMachineMetricsSample,
|
|
109
|
+
setEnrollmentHasDisplay,
|
|
110
|
+
touchEnrollmentLastSeen
|
|
111
|
+
} from "@opengeni/db";
|
|
112
|
+
import { AgentEvent, Hello } from "@opengeni/agent-proto";
|
|
113
|
+
var AGENT_EVENTS_SUBJECT = "agent.*.*.events";
|
|
114
|
+
var AGENT_HELLO_SUBJECT = "agent.*.*.hello";
|
|
115
|
+
function parseAgentSubject(subject, tail) {
|
|
116
|
+
const parts = subject.split(".");
|
|
117
|
+
if (parts.length !== 4 || parts[0] !== "agent" || parts[3] !== tail) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { workspaceId: parts[1], agentId: parts[2] };
|
|
121
|
+
}
|
|
122
|
+
function parseAgentEventSubject(subject) {
|
|
123
|
+
return parseAgentSubject(subject, "events");
|
|
124
|
+
}
|
|
125
|
+
function parseAgentHelloSubject(subject) {
|
|
126
|
+
return parseAgentSubject(subject, "hello");
|
|
127
|
+
}
|
|
128
|
+
function wireSampleToDbSample(wire) {
|
|
129
|
+
const num = (v) => typeof v === "number" ? v : Number(v);
|
|
130
|
+
const firstGpu = wire.gpus[0];
|
|
131
|
+
return {
|
|
132
|
+
cpuPercent: wire.cpuPercent,
|
|
133
|
+
load1: wire.load1,
|
|
134
|
+
load5: wire.load5,
|
|
135
|
+
load15: wire.load15,
|
|
136
|
+
memUsedBytes: num(wire.memUsedBytes),
|
|
137
|
+
memTotalBytes: num(wire.memTotalBytes),
|
|
138
|
+
diskUsedBytes: num(wire.diskUsedBytes),
|
|
139
|
+
diskTotalBytes: num(wire.diskTotalBytes),
|
|
140
|
+
gpuUtilPercent: firstGpu ? firstGpu.utilPercent : null,
|
|
141
|
+
gpuMemUsedBytes: firstGpu ? num(firstGpu.memUsedBytes) : null,
|
|
142
|
+
gpuMemTotalBytes: firstGpu ? num(firstGpu.memTotalBytes) : null,
|
|
143
|
+
contention: wire.runQueue,
|
|
144
|
+
// The sample carries its own wall-clock stamp (epoch ms); fall back to now on
|
|
145
|
+
// a missing/zero stamp so a series row is never NULL-dated.
|
|
146
|
+
sampledAt: wire.sampledAtMs && Number(wire.sampledAtMs) > 0 ? new Date(Number(wire.sampledAtMs)) : /* @__PURE__ */ new Date()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function ingestHeartbeat(db, input) {
|
|
150
|
+
const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
|
|
151
|
+
if (!enrollment) {
|
|
152
|
+
return { ingested: false, seriesAppended: false };
|
|
153
|
+
}
|
|
154
|
+
const sample = wireSampleToDbSample(input.sample);
|
|
155
|
+
await touchEnrollmentLastSeen(db, {
|
|
156
|
+
accountId: enrollment.accountId,
|
|
157
|
+
workspaceId: input.workspaceId,
|
|
158
|
+
enrollmentId: input.agentId
|
|
159
|
+
});
|
|
160
|
+
const result = await ingestMachineMetricsSample(db, {
|
|
161
|
+
accountId: enrollment.accountId,
|
|
162
|
+
workspaceId: input.workspaceId,
|
|
163
|
+
enrollmentId: input.agentId,
|
|
164
|
+
sample
|
|
165
|
+
});
|
|
166
|
+
return { ingested: true, seriesAppended: result.seriesAppended };
|
|
167
|
+
}
|
|
168
|
+
async function handleAgentEventPayload(db, observability, payload, subject) {
|
|
169
|
+
const ids = parseAgentEventSubject(subject);
|
|
170
|
+
if (!ids) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let event;
|
|
174
|
+
try {
|
|
175
|
+
event = AgentEvent.decode(payload);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
observability?.warn?.("Failed to decode an agent event for metrics ingestion", {
|
|
178
|
+
subject,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error)
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (event.event?.$case !== "heartbeat") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const metrics = event.event.heartbeat.metrics;
|
|
187
|
+
if (!metrics) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await ingestHeartbeat(db, { workspaceId: ids.workspaceId, agentId: ids.agentId, sample: metrics });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
observability?.warn?.("Failed to ingest a machine metrics heartbeat", {
|
|
194
|
+
subject,
|
|
195
|
+
error: error instanceof Error ? error.message : String(error)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function startMetricsIngestion(deps) {
|
|
200
|
+
return deps.bus.subscribeAgentEvents(
|
|
201
|
+
AGENT_EVENTS_SUBJECT,
|
|
202
|
+
(payload, subject) => handleAgentEventPayload(deps.db, deps.observability, payload, subject)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
function helloReportsDisplay(hello) {
|
|
206
|
+
const caps = hello.capabilities;
|
|
207
|
+
if (!caps) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return caps.desktop === true || caps.display != null;
|
|
211
|
+
}
|
|
212
|
+
async function refreshEnrollmentDisplay(db, input) {
|
|
213
|
+
const enrollment = await getEnrollment2(db, input.workspaceId, input.agentId);
|
|
214
|
+
if (!enrollment) {
|
|
215
|
+
return { updated: false };
|
|
216
|
+
}
|
|
217
|
+
if (enrollment.hasDisplay === input.hasDisplay) {
|
|
218
|
+
return { updated: false };
|
|
219
|
+
}
|
|
220
|
+
return await setEnrollmentHasDisplay(db, {
|
|
221
|
+
accountId: enrollment.accountId,
|
|
222
|
+
workspaceId: input.workspaceId,
|
|
223
|
+
enrollmentId: input.agentId,
|
|
224
|
+
hasDisplay: input.hasDisplay
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function handleHelloPayload(db, observability, payload, subject) {
|
|
228
|
+
const ids = parseAgentHelloSubject(subject);
|
|
229
|
+
if (!ids) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let hello;
|
|
233
|
+
try {
|
|
234
|
+
hello = Hello.decode(payload);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
observability?.warn?.("Failed to decode an agent Hello for display refresh", {
|
|
237
|
+
subject,
|
|
238
|
+
error: error instanceof Error ? error.message : String(error)
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await refreshEnrollmentDisplay(db, {
|
|
244
|
+
workspaceId: ids.workspaceId,
|
|
245
|
+
agentId: ids.agentId,
|
|
246
|
+
hasDisplay: helloReportsDisplay(hello)
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
observability?.warn?.("Failed to refresh an enrollment's display from a Hello", {
|
|
250
|
+
subject,
|
|
251
|
+
error: error instanceof Error ? error.message : String(error)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function startHelloIngestion(deps) {
|
|
256
|
+
return deps.bus.subscribeAgentEvents(
|
|
257
|
+
AGENT_HELLO_SUBJECT,
|
|
258
|
+
(payload, subject) => handleHelloPayload(deps.db, deps.observability, payload, subject)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/index.ts
|
|
263
|
+
function isWorkflowAlreadyStarted(error) {
|
|
264
|
+
return error instanceof WorkflowExecutionAlreadyStartedError;
|
|
265
|
+
}
|
|
266
|
+
var TEMPORAL_MONTHS = [
|
|
267
|
+
"JANUARY",
|
|
268
|
+
"FEBRUARY",
|
|
269
|
+
"MARCH",
|
|
270
|
+
"APRIL",
|
|
271
|
+
"MAY",
|
|
272
|
+
"JUNE",
|
|
273
|
+
"JULY",
|
|
274
|
+
"AUGUST",
|
|
275
|
+
"SEPTEMBER",
|
|
276
|
+
"OCTOBER",
|
|
277
|
+
"NOVEMBER",
|
|
278
|
+
"DECEMBER"
|
|
279
|
+
];
|
|
280
|
+
async function createTemporalWorkflowClient(settings) {
|
|
281
|
+
const connection = await Connection.connect({ address: settings.temporalHost });
|
|
282
|
+
const temporal = new TemporalClient({
|
|
283
|
+
connection,
|
|
284
|
+
namespace: settings.temporalNamespace
|
|
285
|
+
});
|
|
286
|
+
const client = {
|
|
287
|
+
signalUserMessage: async ({ eventId, workflowId }) => {
|
|
288
|
+
await temporal.workflow.getHandle(workflowId).signal("userMessage", eventId);
|
|
289
|
+
},
|
|
290
|
+
wakeSessionWorkflow: async ({ accountId, workspaceId, sessionId, workflowId }) => {
|
|
291
|
+
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
292
|
+
taskQueue: settings.temporalTaskQueue,
|
|
293
|
+
workflowId,
|
|
294
|
+
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
295
|
+
args: [{ accountId, workspaceId, sessionId }],
|
|
296
|
+
signal: "queueChanged"
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
signalApprovalDecision: async ({ eventId, workflowId }) => {
|
|
300
|
+
await temporal.workflow.getHandle(workflowId).signal("approvalDecision", eventId);
|
|
301
|
+
},
|
|
302
|
+
signalInterrupt: async ({ accountId, workspaceId, sessionId, eventId, workflowId }) => {
|
|
303
|
+
await temporal.workflow.signalWithStart("sessionWorkflow", {
|
|
304
|
+
taskQueue: settings.temporalTaskQueue,
|
|
305
|
+
workflowId,
|
|
306
|
+
workflowIdReusePolicy: "ALLOW_DUPLICATE",
|
|
307
|
+
args: [{ accountId, workspaceId, sessionId }],
|
|
308
|
+
signal: "interrupt",
|
|
309
|
+
signalArgs: [eventId]
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
syncScheduledTask: async ({ task }) => {
|
|
313
|
+
const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
|
|
314
|
+
const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
|
|
315
|
+
try {
|
|
316
|
+
await schedule.update(() => temporalScheduleUpdateOptions(options));
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!shouldCreateScheduleAfterUpdateError(error)) {
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
await temporal.schedule.create(options);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
325
|
+
await temporal.schedule.getHandle(temporalScheduleId).delete().catch(() => void 0);
|
|
326
|
+
},
|
|
327
|
+
triggerScheduledTask: async ({ task, agentRunUsageIdempotencyKey, triggerWorkflowId }) => {
|
|
328
|
+
const workflowId = triggerWorkflowId ?? `scheduled-task-${task.id}-manual-${crypto.randomUUID()}`;
|
|
329
|
+
try {
|
|
330
|
+
await temporal.workflow.start("scheduledTaskFireWorkflow", {
|
|
331
|
+
taskQueue: settings.temporalTaskQueue,
|
|
332
|
+
workflowId,
|
|
333
|
+
workflowIdReusePolicy: "REJECT_DUPLICATE",
|
|
334
|
+
args: [{
|
|
335
|
+
accountId: task.accountId,
|
|
336
|
+
workspaceId: task.workspaceId,
|
|
337
|
+
taskId: task.id,
|
|
338
|
+
triggerType: "manual",
|
|
339
|
+
agentRunUsageIdempotencyKey
|
|
340
|
+
}]
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (isWorkflowAlreadyStarted(error)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const documentIndexer = {
|
|
351
|
+
indexDocument: async ({ accountId, workspaceId, documentId }) => {
|
|
352
|
+
const workflowId = `document-index-${documentId}-${crypto.randomUUID()}`;
|
|
353
|
+
await temporal.workflow.start("documentIndexWorkflow", {
|
|
354
|
+
taskQueue: settings.temporalTaskQueue,
|
|
355
|
+
workflowId,
|
|
356
|
+
args: [{ accountId, workspaceId, documentId }]
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
return {
|
|
361
|
+
client,
|
|
362
|
+
documentIndexer,
|
|
363
|
+
close: async () => {
|
|
364
|
+
await connection.close();
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
async function startApi() {
|
|
369
|
+
const settings = getSettings();
|
|
370
|
+
const observability = createObservability(settings, { component: "api" });
|
|
371
|
+
const searchPath = dbSearchPath(settings);
|
|
372
|
+
const dbClient = createDb(settings.databaseUrl, {
|
|
373
|
+
...searchPath ? { searchPath } : {},
|
|
374
|
+
rlsStrategy: settings.rlsStrategy
|
|
375
|
+
});
|
|
376
|
+
let bus;
|
|
377
|
+
let workflowClient;
|
|
378
|
+
const retryOptions = startupRetryOptions(settings);
|
|
379
|
+
const onRetry = (event) => logStartupDependencyRetry(observability, event);
|
|
380
|
+
const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);
|
|
381
|
+
try {
|
|
382
|
+
bus = await retryStartupDependency(
|
|
383
|
+
"NATS",
|
|
384
|
+
() => createNatsEventBus(
|
|
385
|
+
settings.natsUrl,
|
|
386
|
+
controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : void 0
|
|
387
|
+
),
|
|
388
|
+
{
|
|
389
|
+
...retryOptions,
|
|
390
|
+
onRetry
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
workflowClient = await retryStartupDependency("Temporal", () => createTemporalWorkflowClient(settings), {
|
|
394
|
+
...retryOptions,
|
|
395
|
+
onRetry
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
await Promise.allSettled([
|
|
399
|
+
bus?.close(),
|
|
400
|
+
workflowClient?.close(),
|
|
401
|
+
dbClient.close()
|
|
402
|
+
]);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
if (!bus || !workflowClient) {
|
|
406
|
+
await dbClient.close();
|
|
407
|
+
throw new Error("OpenGeni API startup dependencies were not initialized");
|
|
408
|
+
}
|
|
409
|
+
const app = createApp({
|
|
410
|
+
settings,
|
|
411
|
+
db: dbClient.db,
|
|
412
|
+
bus,
|
|
413
|
+
workflowClient: workflowClient.client,
|
|
414
|
+
documentIndexer: workflowClient.documentIndexer,
|
|
415
|
+
observability
|
|
416
|
+
});
|
|
417
|
+
const server = Bun.serve({
|
|
418
|
+
hostname: settings.apiHost,
|
|
419
|
+
port: settings.apiPort,
|
|
420
|
+
idleTimeout: 255,
|
|
421
|
+
fetch: app.fetch
|
|
422
|
+
});
|
|
423
|
+
let stopMetricsIngestion;
|
|
424
|
+
let stopHelloIngestion;
|
|
425
|
+
let authCalloutResponder;
|
|
426
|
+
if (settings.sandboxSelfhostedEnabled) {
|
|
427
|
+
stopMetricsIngestion = startMetricsIngestion({ db: dbClient.db, bus, observability });
|
|
428
|
+
stopHelloIngestion = startHelloIngestion({ db: dbClient.db, bus, observability });
|
|
429
|
+
observability.info("OpenGeni machine-metrics + hello ingestion consumers started", {});
|
|
430
|
+
const callout = resolveNatsCalloutConfig(settings);
|
|
431
|
+
if (callout) {
|
|
432
|
+
try {
|
|
433
|
+
authCalloutResponder = await startAuthCalloutResponder(
|
|
434
|
+
{ db: dbClient.db, settings, callout, observability },
|
|
435
|
+
settings.natsUrl
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
observability.error("OpenGeni NATS auth-callout responder failed to start", {
|
|
439
|
+
error: error instanceof Error ? error.message : String(error)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
observability.warn(
|
|
444
|
+
"OpenGeni selfhosted enabled but the NATS auth-callout plane is not configured; selfhosted agents cannot connect",
|
|
445
|
+
{}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
observability.info("OpenGeni API listening", {
|
|
450
|
+
host: settings.apiHost,
|
|
451
|
+
port: settings.apiPort
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
server,
|
|
455
|
+
close: async () => {
|
|
456
|
+
server.stop(true);
|
|
457
|
+
stopMetricsIngestion?.();
|
|
458
|
+
stopHelloIngestion?.();
|
|
459
|
+
await Promise.allSettled([
|
|
460
|
+
authCalloutResponder?.close(),
|
|
461
|
+
bus.close(),
|
|
462
|
+
workflowClient.close(),
|
|
463
|
+
dbClient.close()
|
|
464
|
+
]);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (import.meta.main) {
|
|
469
|
+
await startApi();
|
|
470
|
+
}
|
|
471
|
+
function temporalOverlapPolicy(policy) {
|
|
472
|
+
if (policy === "skip") {
|
|
473
|
+
return ScheduleOverlapPolicy.SKIP;
|
|
474
|
+
}
|
|
475
|
+
if (policy === "buffer_one") {
|
|
476
|
+
return ScheduleOverlapPolicy.BUFFER_ONE;
|
|
477
|
+
}
|
|
478
|
+
return ScheduleOverlapPolicy.ALLOW_ALL;
|
|
479
|
+
}
|
|
480
|
+
function shouldCreateScheduleAfterUpdateError(error) {
|
|
481
|
+
return error instanceof ScheduleNotFoundError;
|
|
482
|
+
}
|
|
483
|
+
function temporalScheduleSpec(schedule) {
|
|
484
|
+
if (schedule.type === "interval") {
|
|
485
|
+
return {
|
|
486
|
+
intervals: [{ every: `${schedule.everySeconds}s` }],
|
|
487
|
+
...schedule.startAt ? { startAt: new Date(schedule.startAt) } : {},
|
|
488
|
+
...schedule.endAt ? { endAt: new Date(schedule.endAt) } : {}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (schedule.type === "calendar") {
|
|
492
|
+
return {
|
|
493
|
+
calendars: [{
|
|
494
|
+
hour: schedule.hour,
|
|
495
|
+
minute: schedule.minute,
|
|
496
|
+
second: 0,
|
|
497
|
+
...schedule.daysOfWeek ? { dayOfWeek: schedule.daysOfWeek } : {}
|
|
498
|
+
}],
|
|
499
|
+
timezone: schedule.timeZone
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const runAt = new Date(schedule.runAt);
|
|
503
|
+
return {
|
|
504
|
+
calendars: [{
|
|
505
|
+
year: runAt.getUTCFullYear(),
|
|
506
|
+
month: temporalMonth(runAt.getUTCMonth()),
|
|
507
|
+
dayOfMonth: runAt.getUTCDate(),
|
|
508
|
+
hour: runAt.getUTCHours(),
|
|
509
|
+
minute: runAt.getUTCMinutes(),
|
|
510
|
+
second: runAt.getUTCSeconds()
|
|
511
|
+
}],
|
|
512
|
+
timezone: "UTC"
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function temporalMonth(monthIndex) {
|
|
516
|
+
return TEMPORAL_MONTHS[monthIndex];
|
|
517
|
+
}
|
|
518
|
+
function temporalScheduleOptions(task, taskQueue) {
|
|
519
|
+
return {
|
|
520
|
+
scheduleId: task.temporalScheduleId,
|
|
521
|
+
spec: temporalScheduleSpec(task.schedule),
|
|
522
|
+
action: {
|
|
523
|
+
type: "startWorkflow",
|
|
524
|
+
workflowType: "scheduledTaskFireWorkflow",
|
|
525
|
+
taskQueue,
|
|
526
|
+
args: [{
|
|
527
|
+
accountId: task.accountId,
|
|
528
|
+
workspaceId: task.workspaceId,
|
|
529
|
+
taskId: task.id,
|
|
530
|
+
triggerType: "scheduled"
|
|
531
|
+
}]
|
|
532
|
+
},
|
|
533
|
+
policies: {
|
|
534
|
+
overlap: temporalOverlapPolicy(task.overlapPolicy),
|
|
535
|
+
catchupWindow: "24h",
|
|
536
|
+
pauseOnFailure: false
|
|
537
|
+
},
|
|
538
|
+
state: {
|
|
539
|
+
paused: task.status === "paused",
|
|
540
|
+
...task.schedule.type === "once" ? { remainingActions: 1 } : {}
|
|
541
|
+
},
|
|
542
|
+
memo: {
|
|
543
|
+
accountId: task.accountId,
|
|
544
|
+
workspaceId: task.workspaceId,
|
|
545
|
+
scheduledTaskId: task.id,
|
|
546
|
+
name: task.name
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function temporalScheduleUpdateOptions(options) {
|
|
551
|
+
return {
|
|
552
|
+
spec: options.spec,
|
|
553
|
+
action: options.action,
|
|
554
|
+
...options.policies ? { policies: options.policies } : {},
|
|
555
|
+
state: options.state ?? {},
|
|
556
|
+
...options.searchAttributes ? { searchAttributes: options.searchAttributes } : {},
|
|
557
|
+
...options.typedSearchAttributes ? { typedSearchAttributes: options.typedSearchAttributes } : {}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
export {
|
|
561
|
+
createTemporalWorkflowClient,
|
|
562
|
+
shouldCreateScheduleAfterUpdateError,
|
|
563
|
+
startApi,
|
|
564
|
+
temporalOverlapPolicy,
|
|
565
|
+
temporalScheduleSpec
|
|
566
|
+
};
|
|
567
|
+
//# sourceMappingURL=index.js.map
|