@opengeni/api-router 2.3.2-canary.2 → 2.5.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 (44) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +4 -0
  3. package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
  4. package/dist/{chunk-IBV7Z6F4.js → chunk-QESX7HDK.js} +3645 -470
  5. package/dist/chunk-QESX7HDK.js.map +1 -0
  6. package/dist/fatal-process-boundary.d.ts +25 -0
  7. package/dist/http/sse.d.ts +2 -0
  8. package/dist/index.d.ts +5 -1
  9. package/dist/index.js +168 -6
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/slack-interactions.d.ts +1 -1
  12. package/dist/mcp/receipts.d.ts +9 -0
  13. package/dist/mcp/server.d.ts +33 -0
  14. package/dist/organization-recovery-notifications.d.ts +41 -0
  15. package/dist/routes/managed-auth-session-sets.d.ts +19 -0
  16. package/dist/routes/organization-recovery.d.ts +19 -0
  17. package/dist/routes/sessions.d.ts +17 -5
  18. package/dist/routes/workspaces.d.ts +1 -0
  19. package/dist/work-discovery-observability.d.ts +33 -0
  20. package/package.json +18 -18
  21. package/src/app.ts +133 -1
  22. package/src/auth/managed-auth-attempt-context.ts +24 -0
  23. package/src/auth/managed-auth-session-adapter.ts +205 -0
  24. package/src/auth/managed-auth.ts +52 -2
  25. package/src/fatal-process-boundary.ts +231 -0
  26. package/src/http/sse.ts +7 -0
  27. package/src/index.ts +25 -5
  28. package/src/integrations/slack-interactions.ts +30 -17
  29. package/src/mcp/receipts.ts +34 -0
  30. package/src/mcp/server.ts +349 -68
  31. package/src/organization-recovery-notifications.ts +103 -0
  32. package/src/routes/canonical-human-identities.ts +29 -14
  33. package/src/routes/codex.ts +5 -1
  34. package/src/routes/environments.ts +23 -0
  35. package/src/routes/interaction-resources.ts +3 -0
  36. package/src/routes/managed-auth-session-sets.ts +994 -0
  37. package/src/routes/managed-onboarding.ts +2 -0
  38. package/src/routes/organization-memberships.ts +2 -0
  39. package/src/routes/organization-recovery.ts +325 -0
  40. package/src/routes/sessions.ts +401 -120
  41. package/src/routes/supergrok.ts +5 -1
  42. package/src/routes/workspaces.ts +23 -1
  43. package/src/work-discovery-observability.ts +121 -0
  44. package/dist/chunk-IBV7Z6F4.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import type {
2
+ ManagedAuthResolvedSession,
3
+ ManagedAuthSessionAdapter,
4
+ } from "@opengeni/core/managed-auth-session-sets";
5
+ import { sql } from "drizzle-orm";
6
+ import type { Database } from "@opengeni/db";
7
+ import type { ManagedAuth } from "@opengeni/core";
8
+ import { createHmac, timingSafeEqual } from "node:crypto";
9
+ import { runManagedAuthAttempt } from "./managed-auth-attempt-context";
10
+
11
+ export function createBetterAuthSessionAdapter(
12
+ auth: ManagedAuth,
13
+ db: Database,
14
+ ): ManagedAuthSessionAdapter {
15
+ return {
16
+ async authenticate(input) {
17
+ if (input.provider !== "email_password") {
18
+ throw new Error("unsupported managed authentication provider");
19
+ }
20
+ const result = await runManagedAuthAttempt(
21
+ input.transactionId,
22
+ async () =>
23
+ await auth.api.signInEmail({
24
+ body: {
25
+ email: input.credentials.email,
26
+ password: input.credentials.password,
27
+ rememberMe: true,
28
+ },
29
+ headers: input.headers,
30
+ returnHeaders: true,
31
+ }),
32
+ );
33
+ const token = result.response?.token;
34
+ if (typeof token !== "string") {
35
+ throw new Error("managed authentication did not create an isolated session");
36
+ }
37
+ const resolved = await (await auth.$context).internalAdapter.findSession(token);
38
+ if (!resolved?.session?.id) {
39
+ throw new Error("managed authentication session could not be resolved");
40
+ }
41
+ return { authSessionId: resolved.session.id };
42
+ },
43
+
44
+ async resolveSelectedSession(input): Promise<ManagedAuthResolvedSession | null> {
45
+ const resolved = await (await auth.$context).internalAdapter.findSession(input.token);
46
+ return liveResolvedSession(resolved);
47
+ },
48
+
49
+ async resolveAmbientSession(headers): Promise<ManagedAuthResolvedSession | null> {
50
+ const context = await auth.$context;
51
+ const signed = cookieValue(headers.get("cookie"), context.authCookies.sessionToken.name);
52
+ const token = signed ? verifiedSignedCookieValue(signed, context.secret) : null;
53
+ if (!token) return null;
54
+ const resolved = await context.internalAdapter.findSession(token);
55
+ return liveResolvedSession(resolved);
56
+ },
57
+
58
+ async refreshSelectedSession(input): Promise<ManagedAuthResolvedSession | null> {
59
+ const context = await auth.$context;
60
+ const cookie = context.authCookies.sessionToken;
61
+ const headers = new Headers({
62
+ cookie: `${cookie.name}=${signedCookieValue(input.token, context.secret)}`,
63
+ });
64
+ const resolved = await auth.api.getSession({ headers, returnHeaders: true });
65
+ if (!resolved.response) return null;
66
+ // The provider may renew its durable expiry and emit token/cache cookies;
67
+ // this server-side selected-slot resolution intentionally discards them.
68
+ return resolved.response as ManagedAuthResolvedSession;
69
+ },
70
+
71
+ async revokeSession(input) {
72
+ await db.execute(sql`delete from auth_sessions where id = ${input.authSessionId}`);
73
+ },
74
+
75
+ async createLegacySelectedSessionCookies(input, currentCookieHeader) {
76
+ const context = await auth.$context;
77
+ const cookie = context.authCookies.sessionToken;
78
+ const value = input ? signedCookieValue(input.token, context.secret) : "";
79
+ const headers = [
80
+ serializeCookieHeader(cookie.name, value, {
81
+ ...cookie.attributes,
82
+ ...(input ? {} : { maxAge: 0, expires: new Date(0) }),
83
+ }),
84
+ ];
85
+ for (const cacheCookie of [
86
+ context.authCookies.sessionData,
87
+ context.authCookies.accountData,
88
+ context.authCookies.dontRememberToken,
89
+ ]) {
90
+ for (const name of cacheCookieNames(cacheCookie.name, currentCookieHeader)) {
91
+ headers.push(
92
+ serializeCookieHeader(name, "", {
93
+ ...cacheCookie.attributes,
94
+ maxAge: 0,
95
+ expires: new Date(0),
96
+ }),
97
+ );
98
+ }
99
+ }
100
+ return headers;
101
+ },
102
+ };
103
+ }
104
+
105
+ function liveResolvedSession(value: unknown): ManagedAuthResolvedSession | null {
106
+ if (!value || typeof value !== "object") return null;
107
+ const session = (value as { session?: { expiresAt?: unknown } }).session;
108
+ const expiresAt = session?.expiresAt;
109
+ const expiryMillis =
110
+ expiresAt instanceof Date
111
+ ? expiresAt.getTime()
112
+ : typeof expiresAt === "string" || typeof expiresAt === "number"
113
+ ? new Date(expiresAt).getTime()
114
+ : Number.NaN;
115
+ if (!Number.isFinite(expiryMillis) || expiryMillis <= Date.now()) return null;
116
+ return value as ManagedAuthResolvedSession;
117
+ }
118
+
119
+ function cookieValue(header: string | null, name: string): string | null {
120
+ if (!header) return null;
121
+ for (const part of header.split(";")) {
122
+ const separator = part.indexOf("=");
123
+ if (separator < 0 || part.slice(0, separator).trim() !== name) continue;
124
+ return part.slice(separator + 1).trim() || null;
125
+ }
126
+ return null;
127
+ }
128
+
129
+ function verifiedSignedCookieValue(value: string, secret: string): string | null {
130
+ let decoded: string;
131
+ try {
132
+ decoded = decodeURIComponent(value);
133
+ } catch {
134
+ return null;
135
+ }
136
+ const separator = decoded.lastIndexOf(".");
137
+ if (separator <= 0) return null;
138
+ const token = decoded.slice(0, separator);
139
+ const signature = decoded.slice(separator + 1);
140
+ const expected = createHmac("sha256", secret).update(token, "utf8").digest("base64");
141
+ const actualBytes = Buffer.from(signature, "utf8");
142
+ const expectedBytes = Buffer.from(expected, "utf8");
143
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes)
144
+ ? token
145
+ : null;
146
+ }
147
+
148
+ function signedCookieValue(value: string, secret: string): string {
149
+ return encodeURIComponent(
150
+ `${value}.${createHmac("sha256", secret).update(value, "utf8").digest("base64")}`,
151
+ );
152
+ }
153
+
154
+ function serializeCookieHeader(
155
+ name: string,
156
+ value: string,
157
+ attributes: {
158
+ domain?: string;
159
+ expires?: Date;
160
+ httpOnly?: boolean;
161
+ maxAge?: number;
162
+ path?: string;
163
+ partitioned?: boolean;
164
+ prefix?: "secure" | "host";
165
+ sameSite?: string;
166
+ secure?: boolean;
167
+ },
168
+ ): string {
169
+ let cookieName = name;
170
+ if (attributes.prefix === "secure" && !cookieName.startsWith("__Secure-")) {
171
+ cookieName = `__Secure-${cookieName}`;
172
+ } else if (attributes.prefix === "host" && !cookieName.startsWith("__Host-")) {
173
+ cookieName = `__Host-${cookieName}`;
174
+ }
175
+ if (cookieName.startsWith("__Secure-")) attributes.secure = true;
176
+ if (cookieName.startsWith("__Host-")) {
177
+ attributes.secure = true;
178
+ attributes.path = "/";
179
+ delete attributes.domain;
180
+ }
181
+ let header = `${cookieName}=${value}`;
182
+ if (attributes.maxAge !== undefined)
183
+ header += `; Max-Age=${Math.max(0, Math.floor(attributes.maxAge))}`;
184
+ if (attributes.domain) header += `; Domain=${attributes.domain}`;
185
+ if (attributes.path) header += `; Path=${attributes.path}`;
186
+ if (attributes.expires) header += `; Expires=${attributes.expires.toUTCString()}`;
187
+ if (attributes.httpOnly) header += "; HttpOnly";
188
+ if (attributes.secure) header += "; Secure";
189
+ if (attributes.sameSite) {
190
+ header += `; SameSite=${attributes.sameSite[0]?.toUpperCase()}${attributes.sameSite.slice(1)}`;
191
+ }
192
+ if (attributes.partitioned) header += "; Partitioned";
193
+ return header;
194
+ }
195
+
196
+ function cacheCookieNames(baseName: string, cookieHeader?: string | null): string[] {
197
+ const names = new Set([baseName]);
198
+ for (const part of cookieHeader?.split(";") ?? []) {
199
+ const separator = part.indexOf("=");
200
+ if (separator < 0) continue;
201
+ const name = part.slice(0, separator).trim();
202
+ if (name === baseName || name.startsWith(`${baseName}.`)) names.add(name);
203
+ }
204
+ return [...names].sort();
205
+ }
@@ -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) => binding.status === "recovery_pending",
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 === projection.activeIdentity.activeLoginBindingId,
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 { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
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
- const settings = getSettings();
304
- const observability = createObservability(settings, { component: "api" });
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
- await startApi();
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 {