@opengeni/api-router 2.3.2-canary.2 → 2.4.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.
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +4 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
- package/dist/{chunk-IBV7Z6F4.js → chunk-L7GVVSSQ.js} +2996 -373
- package/dist/chunk-L7GVVSSQ.js.map +1 -0
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/managed-auth-session-sets.d.ts +19 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/workspaces.d.ts +1 -0
- package/package.json +18 -18
- package/src/app.ts +133 -1
- package/src/auth/managed-auth-attempt-context.ts +24 -0
- package/src/auth/managed-auth-session-adapter.ts +205 -0
- package/src/auth/managed-auth.ts +52 -2
- package/src/fatal-process-boundary.ts +231 -0
- package/src/http/sse.ts +7 -0
- package/src/index.ts +25 -5
- package/src/integrations/slack-interactions.ts +30 -17
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/server.ts +45 -39
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/canonical-human-identities.ts +29 -14
- package/src/routes/codex.ts +5 -1
- package/src/routes/environments.ts +23 -0
- package/src/routes/interaction-resources.ts +3 -0
- package/src/routes/managed-auth-session-sets.ts +994 -0
- package/src/routes/managed-onboarding.ts +2 -0
- package/src/routes/organization-memberships.ts +2 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/sessions.ts +24 -39
- package/src/routes/supergrok.ts +5 -1
- package/src/routes/workspaces.ts +23 -1
- package/dist/chunk-IBV7Z6F4.js.map +0 -1
package/src/auth/managed-auth.ts
CHANGED
|
@@ -9,14 +9,20 @@ import { ensureManagedAccessForUser } from "@opengeni/db";
|
|
|
9
9
|
import {
|
|
10
10
|
ensureCanonicalHumanIdentityForAuthUser,
|
|
11
11
|
getCanonicalHumanIdentityProjection,
|
|
12
|
+
getCanonicalHumanExactLoginBindingForAuthUser,
|
|
12
13
|
synchronizeCanonicalHumanLoginBindings,
|
|
13
14
|
} from "@opengeni/db/canonical-human-identities";
|
|
14
15
|
import { betterAuth } from "better-auth";
|
|
15
16
|
import { createEmailVerificationToken } from "better-auth/api";
|
|
16
17
|
import { hashPassword } from "better-auth/crypto";
|
|
18
|
+
import { sql } from "drizzle-orm";
|
|
17
19
|
import { Pool } from "pg";
|
|
18
20
|
|
|
19
21
|
import { decideCanonicalHumanSessionAdmission } from "./canonical-human-session-admission";
|
|
22
|
+
import {
|
|
23
|
+
currentManagedAuthAttemptId,
|
|
24
|
+
shouldDiscardCurrentManagedAuthProviderSession,
|
|
25
|
+
} from "./managed-auth-attempt-context";
|
|
20
26
|
|
|
21
27
|
// `ManagedAuth` (the Better Auth `Auth<any>` alias) is owned by @opengeni/core
|
|
22
28
|
// (`managed-auth-type.ts`) — `dependencies.ts`/`access` reference it as a
|
|
@@ -121,6 +127,25 @@ export function createManagedAuth(
|
|
|
121
127
|
returned: false,
|
|
122
128
|
bigint: true,
|
|
123
129
|
},
|
|
130
|
+
loginBindingId: {
|
|
131
|
+
type: "string",
|
|
132
|
+
fieldName: "login_binding_id",
|
|
133
|
+
input: false,
|
|
134
|
+
returned: false,
|
|
135
|
+
},
|
|
136
|
+
loginBindingRevision: {
|
|
137
|
+
type: "number",
|
|
138
|
+
fieldName: "login_binding_revision",
|
|
139
|
+
input: false,
|
|
140
|
+
returned: false,
|
|
141
|
+
bigint: true,
|
|
142
|
+
},
|
|
143
|
+
managedAuthLoginTransactionId: {
|
|
144
|
+
type: "string",
|
|
145
|
+
fieldName: "managed_auth_login_transaction_id",
|
|
146
|
+
input: false,
|
|
147
|
+
returned: false,
|
|
148
|
+
},
|
|
124
149
|
},
|
|
125
150
|
},
|
|
126
151
|
account: {
|
|
@@ -215,8 +240,13 @@ export function createManagedAuth(
|
|
|
215
240
|
binding: null,
|
|
216
241
|
});
|
|
217
242
|
if (!preflight.allowed) {
|
|
243
|
+
const exactRecoveryBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
|
|
244
|
+
authUserId: session.userId,
|
|
245
|
+
providerId: "credential",
|
|
246
|
+
});
|
|
218
247
|
const recoveryBinding = preflightProjection.loginBindings.find(
|
|
219
|
-
(binding) =>
|
|
248
|
+
(binding) =>
|
|
249
|
+
binding.id === exactRecoveryBinding.id && binding.status === "recovery_pending",
|
|
220
250
|
);
|
|
221
251
|
const recoveryAdmission = decideCanonicalHumanSessionAdmission({
|
|
222
252
|
intent: "recovery_completion",
|
|
@@ -235,17 +265,27 @@ export function createManagedAuth(
|
|
|
235
265
|
return {
|
|
236
266
|
data: {
|
|
237
267
|
...session,
|
|
268
|
+
...(shouldDiscardCurrentManagedAuthProviderSession()
|
|
269
|
+
? { expiresAt: new Date(0) }
|
|
270
|
+
: {}),
|
|
238
271
|
identityId: preflightProjection.activeIdentity.id,
|
|
239
272
|
identityRevision: preflightProjection.activeIdentity.identityRevision,
|
|
240
273
|
authRevision: preflightProjection.activeIdentity.authRevision,
|
|
274
|
+
loginBindingId: recoveryBinding!.id,
|
|
275
|
+
loginBindingRevision: recoveryBinding!.revision,
|
|
276
|
+
managedAuthLoginTransactionId: currentManagedAuthAttemptId(),
|
|
241
277
|
},
|
|
242
278
|
};
|
|
243
279
|
}
|
|
244
280
|
|
|
245
281
|
await synchronizeCanonicalHumanLoginBindings(db, session.userId);
|
|
246
282
|
const projection = await getCanonicalHumanIdentityProjection(db, session.userId);
|
|
283
|
+
const exactBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
|
|
284
|
+
authUserId: session.userId,
|
|
285
|
+
providerId: "credential",
|
|
286
|
+
});
|
|
247
287
|
const activeBinding = projection.loginBindings.find(
|
|
248
|
-
(binding) => binding.id ===
|
|
288
|
+
(binding) => binding.id === exactBinding.id,
|
|
249
289
|
);
|
|
250
290
|
const admission = decideCanonicalHumanSessionAdmission({
|
|
251
291
|
intent: "ordinary_session",
|
|
@@ -265,12 +305,22 @@ export function createManagedAuth(
|
|
|
265
305
|
return {
|
|
266
306
|
data: {
|
|
267
307
|
...session,
|
|
308
|
+
...(shouldDiscardCurrentManagedAuthProviderSession()
|
|
309
|
+
? { expiresAt: new Date(0) }
|
|
310
|
+
: {}),
|
|
268
311
|
identityId: projection.activeIdentity.id,
|
|
269
312
|
identityRevision: projection.activeIdentity.identityRevision,
|
|
270
313
|
authRevision: projection.activeIdentity.authRevision,
|
|
314
|
+
loginBindingId: exactBinding.id,
|
|
315
|
+
loginBindingRevision: exactBinding.revision,
|
|
316
|
+
managedAuthLoginTransactionId: currentManagedAuthAttemptId(),
|
|
271
317
|
},
|
|
272
318
|
};
|
|
273
319
|
},
|
|
320
|
+
after: async (session) => {
|
|
321
|
+
if (!shouldDiscardCurrentManagedAuthProviderSession()) return;
|
|
322
|
+
await db.execute(sql`delete from auth_sessions where id = ${session.id}`);
|
|
323
|
+
},
|
|
274
324
|
},
|
|
275
325
|
},
|
|
276
326
|
user: {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Attributes, Observability, Span } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
export type ApiFatalEvent = "startup_failure" | "unhandled_rejection" | "uncaught_exception";
|
|
5
|
+
export type ApiFatalPhase = "startup" | "running";
|
|
6
|
+
export type ApiFatalReasonKind =
|
|
7
|
+
| "bigint"
|
|
8
|
+
| "boolean"
|
|
9
|
+
| "error"
|
|
10
|
+
| "function"
|
|
11
|
+
| "null"
|
|
12
|
+
| "number"
|
|
13
|
+
| "object"
|
|
14
|
+
| "string"
|
|
15
|
+
| "symbol"
|
|
16
|
+
| "undefined";
|
|
17
|
+
|
|
18
|
+
type ApiFatalObservability = Pick<Observability, "error" | "flush" | "startSpan">;
|
|
19
|
+
|
|
20
|
+
type ApiFatalProcess = {
|
|
21
|
+
on: (
|
|
22
|
+
event: "unhandledRejection" | "uncaughtException",
|
|
23
|
+
listener: (reason: unknown) => void,
|
|
24
|
+
) => void;
|
|
25
|
+
off: (
|
|
26
|
+
event: "unhandledRejection" | "uncaughtException",
|
|
27
|
+
listener: (reason: unknown) => void,
|
|
28
|
+
) => void;
|
|
29
|
+
exit: (code: number) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ApiFatalProcessBoundaryOptions = {
|
|
33
|
+
process?: ApiFatalProcess;
|
|
34
|
+
observability?: ApiFatalObservability;
|
|
35
|
+
flushTimeoutMs?: number;
|
|
36
|
+
correlationId?: () => string;
|
|
37
|
+
fallbackLog?: (message: string) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ApiFatalProcessBoundary = {
|
|
41
|
+
attachObservability: (observability: ApiFatalObservability) => void;
|
|
42
|
+
markRunning: () => void;
|
|
43
|
+
reportStartupFailure: (reason: unknown) => Promise<void>;
|
|
44
|
+
dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const API_FATAL_FLUSH_TIMEOUT_MS = 1_000;
|
|
48
|
+
const API_FATAL_CORRELATION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
49
|
+
|
|
50
|
+
const FATAL_ERROR_CODES = {
|
|
51
|
+
startup_failure: "api_startup_failed",
|
|
52
|
+
unhandled_rejection: "api_unhandled_rejection",
|
|
53
|
+
uncaught_exception: "api_uncaught_exception",
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export function installApiFatalProcessBoundary(
|
|
57
|
+
options: ApiFatalProcessBoundaryOptions = {},
|
|
58
|
+
): ApiFatalProcessBoundary {
|
|
59
|
+
const runtimeProcess = options.process ?? defaultProcessBoundary();
|
|
60
|
+
const flushTimeoutMs = options.flushTimeoutMs ?? API_FATAL_FLUSH_TIMEOUT_MS;
|
|
61
|
+
const correlationId = options.correlationId ?? (() => `api-fatal.${randomUUID()}`);
|
|
62
|
+
const fallbackLog = options.fallbackLog ?? ((message: string) => console.error(message));
|
|
63
|
+
let observability = options.observability;
|
|
64
|
+
let phase: ApiFatalPhase = "startup";
|
|
65
|
+
let reporting = false;
|
|
66
|
+
|
|
67
|
+
const report = async (event: ApiFatalEvent, reason: unknown): Promise<void> => {
|
|
68
|
+
if (reporting) return;
|
|
69
|
+
reporting = true;
|
|
70
|
+
|
|
71
|
+
const diagnostic = apiFatalDiagnostic(event, phase, reason, correlationId);
|
|
72
|
+
const message = apiFatalMessage(diagnostic);
|
|
73
|
+
const activeObservability = observability;
|
|
74
|
+
try {
|
|
75
|
+
if (activeObservability) {
|
|
76
|
+
let logged = false;
|
|
77
|
+
try {
|
|
78
|
+
activeObservability.error(message, diagnostic);
|
|
79
|
+
logged = true;
|
|
80
|
+
} catch {
|
|
81
|
+
// The fatal boundary must still report and terminate if logging is unhealthy.
|
|
82
|
+
}
|
|
83
|
+
if (!logged) {
|
|
84
|
+
safeFallbackLog(fallbackLog, message);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const span: Span = activeObservability.startSpan("api.process.fatal", diagnostic);
|
|
88
|
+
span.end({ error: true });
|
|
89
|
+
} catch {
|
|
90
|
+
// The synchronous log remains authoritative when span creation fails.
|
|
91
|
+
}
|
|
92
|
+
await flushWithin(activeObservability, flushTimeoutMs);
|
|
93
|
+
} else {
|
|
94
|
+
safeFallbackLog(fallbackLog, message);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
runtimeProcess.exit(1);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onUnhandledRejection = (reason: unknown): void => {
|
|
102
|
+
void report("unhandled_rejection", reason);
|
|
103
|
+
};
|
|
104
|
+
const onUncaughtException = (reason: unknown): void => {
|
|
105
|
+
void report("uncaught_exception", reason);
|
|
106
|
+
};
|
|
107
|
+
runtimeProcess.on("unhandledRejection", onUnhandledRejection);
|
|
108
|
+
runtimeProcess.on("uncaughtException", onUncaughtException);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
attachObservability: (value) => {
|
|
112
|
+
observability = value;
|
|
113
|
+
},
|
|
114
|
+
markRunning: () => {
|
|
115
|
+
phase = "running";
|
|
116
|
+
},
|
|
117
|
+
reportStartupFailure: async (reason) => {
|
|
118
|
+
await report("startup_failure", reason);
|
|
119
|
+
},
|
|
120
|
+
dispose: () => {
|
|
121
|
+
runtimeProcess.off("unhandledRejection", onUnhandledRejection);
|
|
122
|
+
runtimeProcess.off("uncaughtException", onUncaughtException);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function apiFatalDiagnostic(
|
|
128
|
+
event: ApiFatalEvent,
|
|
129
|
+
phase: ApiFatalPhase,
|
|
130
|
+
reason: unknown,
|
|
131
|
+
correlationId: () => string,
|
|
132
|
+
): Attributes & {
|
|
133
|
+
errorClass: "ApiFatalOperationError";
|
|
134
|
+
errorCode: (typeof FATAL_ERROR_CODES)[ApiFatalEvent];
|
|
135
|
+
origin: "api";
|
|
136
|
+
phase: ApiFatalPhase;
|
|
137
|
+
reasonKind: ApiFatalReasonKind;
|
|
138
|
+
correlationId: string;
|
|
139
|
+
} {
|
|
140
|
+
let safeCorrelationId = "api-fatal.fallback";
|
|
141
|
+
try {
|
|
142
|
+
const candidate = correlationId();
|
|
143
|
+
if (API_FATAL_CORRELATION_ID_PATTERN.test(candidate)) {
|
|
144
|
+
safeCorrelationId = candidate;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// A fixed valid fallback preserves the fatal report and nonzero exit.
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
errorClass: "ApiFatalOperationError",
|
|
151
|
+
errorCode: FATAL_ERROR_CODES[event],
|
|
152
|
+
origin: "api",
|
|
153
|
+
phase,
|
|
154
|
+
reasonKind: apiFatalReasonKind(reason),
|
|
155
|
+
correlationId: safeCorrelationId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function apiFatalReasonKind(reason: unknown): ApiFatalReasonKind {
|
|
160
|
+
if (reason === null) return "null";
|
|
161
|
+
const kind = typeof reason;
|
|
162
|
+
if (kind !== "object") return kind;
|
|
163
|
+
try {
|
|
164
|
+
return reason instanceof Error ? "error" : "object";
|
|
165
|
+
} catch {
|
|
166
|
+
return "object";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function apiFatalMessage(diagnostic: ReturnType<typeof apiFatalDiagnostic>): string {
|
|
171
|
+
return (
|
|
172
|
+
`OpenGeni API fatal process failure (${diagnostic.errorCode}; ` +
|
|
173
|
+
`phase=${diagnostic.phase}; reason_kind=${diagnostic.reasonKind}; ` +
|
|
174
|
+
`correlation_id=${diagnostic.correlationId})`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function flushWithin(
|
|
179
|
+
observability: Pick<Observability, "flush">,
|
|
180
|
+
timeoutMs: number,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
let flush: Promise<void>;
|
|
183
|
+
try {
|
|
184
|
+
flush = Promise.resolve(observability.flush()).catch(() => undefined);
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return;
|
|
189
|
+
|
|
190
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
191
|
+
try {
|
|
192
|
+
await Promise.race([
|
|
193
|
+
flush,
|
|
194
|
+
new Promise<void>((resolve) => {
|
|
195
|
+
timeout = setTimeout(resolve, timeoutMs);
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
} finally {
|
|
199
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function safeFallbackLog(fallbackLog: (message: string) => void, message: string): void {
|
|
204
|
+
try {
|
|
205
|
+
fallbackLog(message);
|
|
206
|
+
} catch {
|
|
207
|
+
// Process termination remains mandatory even when every diagnostic sink fails.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function defaultProcessBoundary(): ApiFatalProcess {
|
|
212
|
+
return {
|
|
213
|
+
on: (event, listener) => {
|
|
214
|
+
if (event === "unhandledRejection") {
|
|
215
|
+
process.on("unhandledRejection", listener);
|
|
216
|
+
} else {
|
|
217
|
+
process.on("uncaughtException", listener);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
off: (event, listener) => {
|
|
221
|
+
if (event === "unhandledRejection") {
|
|
222
|
+
process.off("unhandledRejection", listener);
|
|
223
|
+
} else {
|
|
224
|
+
process.off("uncaughtException", listener);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
exit: (code) => {
|
|
228
|
+
process.exit(code);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
package/src/http/sse.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type EventBus,
|
|
20
20
|
} from "@opengeni/events";
|
|
21
21
|
import type { Observability } from "@opengeni/observability";
|
|
22
|
+
import { MANAGED_AUTH_ACTOR_EPOCH_HEADER } from "@opengeni/core/managed-auth-session-sets";
|
|
22
23
|
|
|
23
24
|
const SESSION_REPLAY_PAGE_SIZE = 100;
|
|
24
25
|
const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
|
|
@@ -463,6 +464,7 @@ export async function sseSessionStream(
|
|
|
463
464
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
464
465
|
"Cache-Control": "no-cache, no-transform",
|
|
465
466
|
Connection: "keep-alive",
|
|
467
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
466
468
|
},
|
|
467
469
|
});
|
|
468
470
|
}
|
|
@@ -636,6 +638,7 @@ export async function sseWorkspaceControlStream(
|
|
|
636
638
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
637
639
|
"Cache-Control": "no-cache, no-transform",
|
|
638
640
|
Connection: "keep-alive",
|
|
641
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
639
642
|
},
|
|
640
643
|
});
|
|
641
644
|
}
|
|
@@ -763,6 +766,7 @@ export async function sseWorkspaceLiveStream(
|
|
|
763
766
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
764
767
|
"Cache-Control": "no-cache, no-transform",
|
|
765
768
|
Connection: "keep-alive",
|
|
769
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
766
770
|
},
|
|
767
771
|
});
|
|
768
772
|
}
|
|
@@ -851,6 +855,7 @@ export async function sseWorkspaceInteractionRevisionStream(
|
|
|
851
855
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
852
856
|
"Cache-Control": "no-cache, no-transform",
|
|
853
857
|
Connection: "keep-alive",
|
|
858
|
+
...(options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}),
|
|
854
859
|
},
|
|
855
860
|
});
|
|
856
861
|
}
|
|
@@ -922,6 +927,8 @@ export type SseDeliveryOptions = {
|
|
|
922
927
|
/** Current ACL re-check, run even while the event stream is idle. */
|
|
923
928
|
reauthorize?: (() => Promise<void>) | undefined;
|
|
924
929
|
reauthorizeAfterMs?: number | undefined;
|
|
930
|
+
/** Exact selected actor emitted on the stream response for cross-tab fencing. */
|
|
931
|
+
actorEpoch?: string | undefined;
|
|
925
932
|
};
|
|
926
933
|
|
|
927
934
|
export type SessionSseDeliveryOptions = SseDeliveryOptions;
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
type Database,
|
|
22
22
|
} from "@opengeni/db";
|
|
23
23
|
import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
createObservability,
|
|
26
|
+
logStartupDependencyRetry,
|
|
27
|
+
type Observability,
|
|
28
|
+
} from "@opengeni/observability";
|
|
25
29
|
import { createObjectStorage } from "@opengeni/storage";
|
|
26
30
|
import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
|
|
27
31
|
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
@@ -51,6 +55,7 @@ import {
|
|
|
51
55
|
createStandaloneEditableArtifactApplication,
|
|
52
56
|
type StandaloneEditableArtifactApplication,
|
|
53
57
|
} from "./editable-artifact-production";
|
|
58
|
+
import { installApiFatalProcessBoundary } from "./fatal-process-boundary";
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* A REJECT_DUPLICATE start collides on the deterministic workflowId when the
|
|
@@ -299,9 +304,15 @@ export async function createTemporalWorkflowClient(
|
|
|
299
304
|
};
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
export async function startApi(
|
|
303
|
-
|
|
304
|
-
|
|
307
|
+
export async function startApi(
|
|
308
|
+
options: {
|
|
309
|
+
settings?: ReturnType<typeof getSettings>;
|
|
310
|
+
observability?: Observability;
|
|
311
|
+
} = {},
|
|
312
|
+
) {
|
|
313
|
+
const settings = options.settings ?? getSettings();
|
|
314
|
+
const observability =
|
|
315
|
+
options.observability ?? createObservability(settings, { component: "api" });
|
|
305
316
|
// Step I: standalone → dbSchema unset → searchPath undefined → today's plain
|
|
306
317
|
// handle (public). Embedded → scoped to the dedicated schema + the host's RLS
|
|
307
318
|
// strategy.
|
|
@@ -531,7 +542,16 @@ export async function startApi() {
|
|
|
531
542
|
}
|
|
532
543
|
|
|
533
544
|
if (import.meta.main) {
|
|
534
|
-
|
|
545
|
+
const fatalBoundary = installApiFatalProcessBoundary();
|
|
546
|
+
try {
|
|
547
|
+
const settings = getSettings();
|
|
548
|
+
const observability = createObservability(settings, { component: "api" });
|
|
549
|
+
fatalBoundary.attachObservability(observability);
|
|
550
|
+
await startApi({ settings, observability });
|
|
551
|
+
fatalBoundary.markRunning();
|
|
552
|
+
} catch (error) {
|
|
553
|
+
await fatalBoundary.reportStartupFailure(error);
|
|
554
|
+
}
|
|
535
555
|
}
|
|
536
556
|
|
|
537
557
|
export function temporalOverlapPolicy(policy: ScheduledTaskOverlapPolicy): ScheduleOverlapPolicy {
|
|
@@ -3203,13 +3203,15 @@ async function continueSlackSession(
|
|
|
3203
3203
|
respondedBy: grant.subjectId,
|
|
3204
3204
|
clientEventId: `slack:${entry.providerEventId}`,
|
|
3205
3205
|
});
|
|
3206
|
-
if (accepted.action === "accepted") {
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3206
|
+
if (accepted.action === "accepted" || accepted.action === "completed") {
|
|
3207
|
+
if (accepted.events.length > 0) {
|
|
3208
|
+
await publishDurableSessionEvents(
|
|
3209
|
+
deps.bus,
|
|
3210
|
+
grant.workspaceId,
|
|
3211
|
+
interaction.sessionId,
|
|
3212
|
+
accepted.events,
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3213
3215
|
if (accepted.workflowWakeRevision !== null) {
|
|
3214
3216
|
await deps.workflowClient.signalApprovalDecision({
|
|
3215
3217
|
accountId: grant.accountId,
|
|
@@ -3220,7 +3222,8 @@ async function continueSlackSession(
|
|
|
3220
3222
|
workflowWakeRevision: accepted.workflowWakeRevision,
|
|
3221
3223
|
});
|
|
3222
3224
|
}
|
|
3223
|
-
|
|
3225
|
+
const committedOutcome = accepted.request.response?.outcome;
|
|
3226
|
+
if (committedOutcome === "answered" || committedOutcome === "skipped") return;
|
|
3224
3227
|
}
|
|
3225
3228
|
}
|
|
3226
3229
|
}
|
|
@@ -3502,19 +3505,21 @@ async function executeSlackAction(
|
|
|
3502
3505
|
respondedBy: grant.subjectId,
|
|
3503
3506
|
clientEventId: `slack-action:${handle.id}`,
|
|
3504
3507
|
});
|
|
3505
|
-
if (accepted.action
|
|
3508
|
+
if (accepted.action === "not_found" || accepted.action === "conflict") {
|
|
3506
3509
|
return {
|
|
3507
3510
|
result: "stale",
|
|
3508
3511
|
stale: true,
|
|
3509
3512
|
text: `${mention}This question is no longer pending.`,
|
|
3510
3513
|
};
|
|
3511
3514
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3515
|
+
if (accepted.events.length > 0) {
|
|
3516
|
+
await publishDurableSessionEvents(
|
|
3517
|
+
deps.bus,
|
|
3518
|
+
grant.workspaceId,
|
|
3519
|
+
handle.sessionId,
|
|
3520
|
+
accepted.events,
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3518
3523
|
if (accepted.workflowWakeRevision !== null) {
|
|
3519
3524
|
await deps.workflowClient.signalApprovalDecision({
|
|
3520
3525
|
accountId: grant.accountId,
|
|
@@ -3525,9 +3530,17 @@ async function executeSlackAction(
|
|
|
3525
3530
|
workflowWakeRevision: accepted.workflowWakeRevision,
|
|
3526
3531
|
});
|
|
3527
3532
|
}
|
|
3533
|
+
const committedOutcome = accepted.request.response?.outcome ?? response.outcome;
|
|
3534
|
+
if (committedOutcome !== "answered" && committedOutcome !== "skipped") {
|
|
3535
|
+
return {
|
|
3536
|
+
result: "stale",
|
|
3537
|
+
stale: true,
|
|
3538
|
+
text: `${mention}This question is already ${committedOutcome}.`,
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3528
3541
|
return {
|
|
3529
|
-
result:
|
|
3530
|
-
text: `${mention}${
|
|
3542
|
+
result: committedOutcome === "skipped" ? "skipped" : "answered",
|
|
3543
|
+
text: `${mention}${committedOutcome === "skipped" ? "Skipped the question" : "Answer submitted"}. OpenGeni will continue.`,
|
|
3531
3544
|
};
|
|
3532
3545
|
}
|
|
3533
3546
|
if (handle.actionKind === "session_pause" || handle.actionKind === "session_resume") {
|
package/src/mcp/receipts.ts
CHANGED
|
@@ -26,6 +26,40 @@ export function mcpMutationReceipt(input: McpMutationReceiptInput): McpMutationR
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function sessionControlMutationReceipt(input: {
|
|
30
|
+
operation: "session_pause" | "session_resume";
|
|
31
|
+
sessionId: string;
|
|
32
|
+
state: string;
|
|
33
|
+
receiptId: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
outcome: "changed" | "unchanged" | "replayed";
|
|
36
|
+
interruptionCount: number;
|
|
37
|
+
}): McpMutationReceiptType {
|
|
38
|
+
return mcpMutationReceipt({
|
|
39
|
+
operation: input.operation,
|
|
40
|
+
committed: true,
|
|
41
|
+
outcome:
|
|
42
|
+
input.outcome === "changed"
|
|
43
|
+
? "updated"
|
|
44
|
+
: input.outcome === "unchanged"
|
|
45
|
+
? "unchanged"
|
|
46
|
+
: "replayed",
|
|
47
|
+
changed: input.outcome === "changed",
|
|
48
|
+
resource: {
|
|
49
|
+
type: "session",
|
|
50
|
+
id: input.sessionId,
|
|
51
|
+
state: input.state,
|
|
52
|
+
},
|
|
53
|
+
relatedResources: [{ type: "session_command_receipt", id: input.receiptId }],
|
|
54
|
+
timestamp: input.timestamp,
|
|
55
|
+
idempotency: {
|
|
56
|
+
status: input.outcome === "replayed" ? "replayed" : "applied",
|
|
57
|
+
},
|
|
58
|
+
facts: { interruptionCount: input.interruptionCount },
|
|
59
|
+
nextAction: { tool: "session_get", arguments: { sessionId: input.sessionId } },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
export type SessionCreateReceiptResult = {
|
|
30
64
|
session: {
|
|
31
65
|
id: string;
|