@opengeni/core 2.4.0-canary.2 → 2.5.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.
@@ -1204,6 +1204,8 @@ export async function postUserMessageTurn(input: {
1204
1204
  annotations?: TimelineAnnotation[];
1205
1205
  modelContext?: string | null;
1206
1206
  resources: ResourceRef[];
1207
+ /** Actor-owned resources used only for the exact durable-draft fence. */
1208
+ composerDraftResources?: ResourceRef[];
1207
1209
  model?: string | null;
1208
1210
  reasoningEffort?: Settings["openaiReasoningEffort"] | null;
1209
1211
  latencyMode?: "standard" | "priority" | "fast" | null;
@@ -1281,6 +1283,9 @@ export async function postUserMessageTurn(input: {
1281
1283
  annotations: input.annotations ?? [],
1282
1284
  modelContext: input.modelContext ?? null,
1283
1285
  resources: input.resources,
1286
+ ...(input.composerDraftResources
1287
+ ? { composerDraftResources: input.composerDraftResources }
1288
+ : {}),
1284
1289
  model: requestedModel,
1285
1290
  reasoningEffort: requestedReasoningEffort,
1286
1291
  latencyMode: input.latencyMode ?? null,
@@ -2429,6 +2434,8 @@ export async function acceptSessionUserMessageWithOutcome(
2429
2434
  annotations?: SubmittedTimelineAnnotation[];
2430
2435
  modelContext?: string | null;
2431
2436
  resources?: ResourceRef[];
2437
+ /** Actor-owned resources used only for the exact durable-draft fence. */
2438
+ composerDraftResources?: ResourceRef[];
2432
2439
  model?: string | null;
2433
2440
  reasoningEffort?: ReasoningEffort | null;
2434
2441
  latencyMode?: "standard" | "priority" | "fast" | null;
@@ -2497,6 +2504,20 @@ export async function acceptSessionUserMessageWithOutcome(
2497
2504
  latencyModeSource: input.latencyMode == null ? "session" : "explicit",
2498
2505
  });
2499
2506
  const requestedResources = normalizeResources(input.resources ?? []);
2507
+ const composerDraftResources = input.composerDraftResources
2508
+ ? normalizeResources(input.composerDraftResources)
2509
+ : undefined;
2510
+ if (composerDraftResources) {
2511
+ const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
2512
+ const unacceptedDraftResource = composerDraftResources.find(
2513
+ (resource) => !acceptedResources.has(stableJson(resource)),
2514
+ );
2515
+ if (unacceptedDraftResource) {
2516
+ throw new HTTPException(422, {
2517
+ message: "composer draft resources must be included in the accepted resource set",
2518
+ });
2519
+ }
2520
+ }
2500
2521
  const annotations = await validateSubmittedTimelineAnnotations(
2501
2522
  db,
2502
2523
  workspaceId,
@@ -2585,6 +2606,7 @@ export async function acceptSessionUserMessageWithOutcome(
2585
2606
  annotations,
2586
2607
  modelContext: input.modelContext ?? null,
2587
2608
  resources: requestedResources,
2609
+ ...(composerDraftResources ? { composerDraftResources } : {}),
2588
2610
  model: input.model ?? null,
2589
2611
  reasoningEffort: input.reasoningEffort ?? null,
2590
2612
  latencyMode: input.latencyMode ?? null,
package/src/index.ts CHANGED
@@ -35,7 +35,19 @@ export * from "./workflow-wake-contract";
35
35
  // structural TYPES live here.
36
36
  export * from "./sandbox-types";
37
37
  export * from "./managed-auth-type";
38
- export * from "./managed-session";
38
+ export {
39
+ getManagedAuthRequestActorAbortSignal,
40
+ getManagedAuthRequestActorAdmissionStamp,
41
+ getManagedAuthRequestActorEpoch,
42
+ getManagedAuthRequestActorLeaseStamp,
43
+ getManagedSession,
44
+ ManagedAuthActorLeaseOutcomeUnknownError,
45
+ markManagedAuthRequestActorTransitionApplied,
46
+ releaseManagedAuthRequestActorLease,
47
+ validateManagedAuthRequestActorLease,
48
+ type ManagedAuthActorAdmissionStamp,
49
+ type ManagedAuthActorMutationLeaseStamp,
50
+ } from "./managed-session";
39
51
  export * from "./transcription";
40
52
 
41
53
  // Sandbox fleet/routing service — the closure of `domain/sessions.ts`
@@ -87,6 +99,7 @@ export * from "./domain/video-generation";
87
99
  export * from "./domain/video-generation-capabilities";
88
100
  export * from "./domain/organization-membership-lifecycle";
89
101
  export * from "./application/new-session-drafts";
102
+ export * from "./application/composer-submit";
90
103
  export * from "./application/session-commands";
91
104
  export * from "./application/session-tenancy";
92
105
  export * from "./application/user-resource-grants";
@@ -0,0 +1,344 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import type {
3
+ ManagedAuthSessionSetMode,
4
+ ManagedAuthSessionSetProjection,
5
+ } from "@opengeni/contracts/managed-auth-session-sets";
6
+ import {
7
+ completeManagedAuthLoginTransaction,
8
+ getManagedAuthSessionSetOperationReceipt,
9
+ getManagedAuthSessionSetSnapshot,
10
+ type Database,
11
+ type ManagedAuthDatabaseProjection,
12
+ type ManagedAuthSelectedSession,
13
+ } from "@opengeni/db";
14
+
15
+ export const MANAGED_AUTH_SESSION_SET_COOKIE = "opengeni.session_set" as const;
16
+ export const MANAGED_AUTH_LOGIN_TRANSACTION_COOKIE = "opengeni.login_transaction" as const;
17
+ export const MANAGED_AUTH_CSRF_HEADER = "x-opengeni-session-csrf" as const;
18
+ export const MANAGED_AUTH_ACTOR_EPOCH_HEADER = "x-opengeni-actor-epoch" as const;
19
+ export const MANAGED_AUTH_LOGIN_TRANSACTION_COOKIE_PATH =
20
+ "/v1/auth/session-set/transactions" as const;
21
+
22
+ export type ManagedAuthResolvedSession = {
23
+ session: { id: string; userId: string; [key: string]: unknown };
24
+ user: {
25
+ id: string;
26
+ email: string;
27
+ name: string;
28
+ emailVerified: boolean;
29
+ [key: string]: unknown;
30
+ };
31
+ };
32
+
33
+ /** Provider-neutral boundary; provider credentials and tokens never cross its output. */
34
+ export interface ManagedAuthSessionAdapter {
35
+ authenticate(input: {
36
+ provider: "email_password";
37
+ transactionId: string;
38
+ credentials: { email: string; password: string };
39
+ headers: Headers;
40
+ }): Promise<{ authSessionId: string }>;
41
+ /** Verify an ambient provider cookie without sliding expiry or emitting cookies. */
42
+ resolveAmbientSession(headers: Headers): Promise<ManagedAuthResolvedSession | null>;
43
+ resolveSelectedSession(
44
+ input: ManagedAuthSelectedSession,
45
+ ): Promise<ManagedAuthResolvedSession | null>;
46
+ refreshSelectedSession(
47
+ input: ManagedAuthSelectedSession,
48
+ ): Promise<ManagedAuthResolvedSession | null>;
49
+ revokeSession(input: { authSessionId: string }): Promise<void>;
50
+ /** Dual-mode exact selected-session cookie plus stale provider-cache invalidations. */
51
+ createLegacySelectedSessionCookies(
52
+ input: ManagedAuthSelectedSession | null,
53
+ currentCookieHeader?: string | null,
54
+ ): Promise<string[]>;
55
+ }
56
+
57
+ export class ManagedAuthActorChangeError extends Error {
58
+ readonly name = "ManagedAuthActorChangeError";
59
+ readonly code = "actor_change_required";
60
+ constructor() {
61
+ super("The selected browser actor changed");
62
+ }
63
+ }
64
+
65
+ export class ManagedAuthRequestAdmissionError extends Error {
66
+ readonly name = "ManagedAuthRequestAdmissionError";
67
+ readonly code = "origin_rejected";
68
+ }
69
+
70
+ export class ManagedAuthCompletionOutcomeUnknownError extends Error {
71
+ readonly name = "ManagedAuthCompletionOutcomeUnknownError";
72
+ readonly code = "operation_outcome_unknown";
73
+ constructor(options?: ErrorOptions) {
74
+ super("The managed authentication completion outcome is unknown", options);
75
+ }
76
+ }
77
+
78
+ export function requireManagedAuthActorFence(input: {
79
+ mode: ManagedAuthSessionSetMode;
80
+ actorEpoch: string;
81
+ expectedActorEpoch: string | null;
82
+ selectedAuthSessionId: string | null;
83
+ legacyAmbientSessionId?: string | null;
84
+ }): void {
85
+ if (
86
+ (input.expectedActorEpoch !== null && input.expectedActorEpoch !== input.actorEpoch) ||
87
+ (input.expectedActorEpoch === null &&
88
+ (input.mode === "broker" ||
89
+ input.actorEpoch !== "1" ||
90
+ (input.selectedAuthSessionId !== null &&
91
+ input.legacyAmbientSessionId !== input.selectedAuthSessionId)))
92
+ ) {
93
+ throw new ManagedAuthActorChangeError();
94
+ }
95
+ }
96
+
97
+ export function managedAuthRandomAuthority(): string {
98
+ return randomBytes(32).toString("base64url");
99
+ }
100
+
101
+ export function managedAuthSha256(value: string): string {
102
+ return createHash("sha256").update(value, "utf8").digest("hex");
103
+ }
104
+
105
+ export function managedAuthCsrfHash(authority: string): string {
106
+ return managedAuthSha256(`opengeni:managed-auth:csrf-authority:v1\n${authority}`);
107
+ }
108
+
109
+ export function managedAuthCsrfToken(
110
+ signingSecret: string,
111
+ authority: string,
112
+ generation: string,
113
+ ): string {
114
+ return createHmac("sha256", signingSecret)
115
+ .update(`opengeni:managed-auth:csrf:v1\n${authority}\n${generation}`, "utf8")
116
+ .digest("base64url");
117
+ }
118
+
119
+ export function managedAuthTransactionSecret(
120
+ signingSecret: string,
121
+ authority: string,
122
+ operationId: string,
123
+ ): string {
124
+ return createHmac("sha256", signingSecret)
125
+ .update(`opengeni:managed-auth:transaction:v1\n${authority}\n${operationId}`, "utf8")
126
+ .digest("base64url");
127
+ }
128
+
129
+ export function managedAuthDerivedUuid(namespace: string, value: string): string {
130
+ const bytes = createHash("sha256")
131
+ .update(`${namespace}\n${value}`, "utf8")
132
+ .digest()
133
+ .subarray(0, 16);
134
+ bytes[6] = (bytes[6]! & 0x0f) | 0x50;
135
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
136
+ const hex = bytes.toString("hex");
137
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
138
+ }
139
+
140
+ function canonicalJson(value: unknown): string {
141
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
142
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
143
+ return `{${Object.entries(value as Record<string, unknown>)
144
+ .sort(([left], [right]) => left.localeCompare(right))
145
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
146
+ .join(",")}}`;
147
+ }
148
+
149
+ export function managedAuthRequestDigest(value: unknown): string {
150
+ return managedAuthSha256(canonicalJson(value));
151
+ }
152
+
153
+ export function managedAuthSecretRequestDigest(signingSecret: string, value: unknown): string {
154
+ return createHmac("sha256", signingSecret)
155
+ .update(`opengeni:managed-auth:request:v1\n${canonicalJson(value)}`, "utf8")
156
+ .digest("hex");
157
+ }
158
+
159
+ export function withManagedAuthCsrfToken(
160
+ projection: ManagedAuthDatabaseProjection,
161
+ signingSecret: string,
162
+ authority: string,
163
+ ): ManagedAuthSessionSetProjection {
164
+ return {
165
+ ...projection,
166
+ csrfToken: managedAuthCsrfToken(signingSecret, authority, projection.generation),
167
+ };
168
+ }
169
+
170
+ function equalSecret(left: string, right: string): boolean {
171
+ const leftBytes = Buffer.from(left, "utf8");
172
+ const rightBytes = Buffer.from(right, "utf8");
173
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
174
+ }
175
+
176
+ export function requireManagedAuthMutationAdmission(input: {
177
+ request: Request;
178
+ allowedOrigins: readonly string[];
179
+ authority: string;
180
+ signingSecret: string;
181
+ expectedGeneration: string;
182
+ }): void {
183
+ const origin = input.request.headers.get("origin");
184
+ const fetchSite = input.request.headers.get("sec-fetch-site");
185
+ const contentType = input.request.headers.get("content-type")?.split(";", 1)[0]?.trim();
186
+ const csrf = input.request.headers.get(MANAGED_AUTH_CSRF_HEADER);
187
+ const allowed = new Set(input.allowedOrigins.map((candidate) => new URL(candidate).origin));
188
+ const expectedCsrf = managedAuthCsrfToken(
189
+ input.signingSecret,
190
+ input.authority,
191
+ input.expectedGeneration,
192
+ );
193
+ if (
194
+ !origin ||
195
+ !allowed.has(origin) ||
196
+ fetchSite !== "same-origin" ||
197
+ contentType !== "application/json" ||
198
+ !csrf ||
199
+ !equalSecret(csrf, expectedCsrf)
200
+ ) {
201
+ throw new ManagedAuthRequestAdmissionError("Browser session-set mutation admission failed");
202
+ }
203
+ }
204
+
205
+ export async function resolveManagedAuthSelectedSession(input: {
206
+ db: Database;
207
+ adapter: ManagedAuthSessionAdapter;
208
+ authority: string;
209
+ mode: ManagedAuthSessionSetMode;
210
+ expectedActorEpoch: string | null;
211
+ legacyAmbientSessionId?: string | null;
212
+ allowRecovery?: boolean;
213
+ }): Promise<{
214
+ session: ManagedAuthResolvedSession | null;
215
+ projection: ManagedAuthDatabaseProjection;
216
+ } | null> {
217
+ const snapshot = await getManagedAuthSessionSetSnapshot(input.db, {
218
+ authorityHash: managedAuthSha256(input.authority),
219
+ mode: input.mode,
220
+ includeInternal: true,
221
+ allowRecovery: input.allowRecovery ?? false,
222
+ readOnly: true,
223
+ });
224
+ if (!snapshot) return null;
225
+ if (snapshot.projection.state === "actor_change_required") {
226
+ throw new ManagedAuthActorChangeError();
227
+ }
228
+ requireManagedAuthActorFence({
229
+ mode: input.mode,
230
+ actorEpoch: snapshot.projection.actorEpoch,
231
+ expectedActorEpoch: input.expectedActorEpoch,
232
+ selectedAuthSessionId: snapshot.selected?.authSessionId ?? null,
233
+ legacyAmbientSessionId: input.legacyAmbientSessionId ?? null,
234
+ });
235
+ if (!snapshot.selected) return { session: null, projection: snapshot.projection };
236
+ const resolved = await input.adapter.resolveSelectedSession(snapshot.selected);
237
+ if (
238
+ !resolved ||
239
+ resolved.session.id !== snapshot.selected.authSessionId ||
240
+ resolved.user.id !== snapshot.selected.authUserId
241
+ ) {
242
+ return { session: null, projection: snapshot.projection };
243
+ }
244
+ return { session: resolved, projection: snapshot.projection };
245
+ }
246
+
247
+ export async function authenticateAndAdoptManagedAuthSession(input: {
248
+ db: Database;
249
+ adapter: ManagedAuthSessionAdapter;
250
+ isolatedHeaders: Headers;
251
+ authority: string;
252
+ csrfHash: string;
253
+ operationId: string;
254
+ requestDigest: string;
255
+ expectedGeneration: string;
256
+ expectedActorEpoch: string;
257
+ transactionId: string;
258
+ transactionSecret: string;
259
+ email: string;
260
+ password: string;
261
+ mode: ManagedAuthSessionSetMode;
262
+ }): Promise<{ projection: ManagedAuthDatabaseProjection; returnIntent: string | null }> {
263
+ try {
264
+ const existing = await getManagedAuthSessionSetOperationReceipt(input.db, {
265
+ authorityHash: managedAuthSha256(input.authority),
266
+ operationId: input.operationId,
267
+ requestDigest: input.requestDigest,
268
+ });
269
+ if (existing) return existing;
270
+ } catch (error) {
271
+ // Provider authentication creates a durable session. Do not perform it
272
+ // while exact-replay reconciliation is unavailable.
273
+ throw new ManagedAuthCompletionOutcomeUnknownError({ cause: error });
274
+ }
275
+ const created = await input.adapter.authenticate({
276
+ provider: "email_password",
277
+ transactionId: input.transactionId,
278
+ credentials: { email: input.email, password: input.password },
279
+ headers: input.isolatedHeaders,
280
+ });
281
+ let completed: { projection: ManagedAuthDatabaseProjection; returnIntent: string | null };
282
+ try {
283
+ completed = await completeManagedAuthLoginTransaction(input.db, {
284
+ authorityHash: managedAuthSha256(input.authority),
285
+ csrfHash: input.csrfHash,
286
+ operationId: input.operationId,
287
+ requestDigest: input.requestDigest,
288
+ expectedGeneration: input.expectedGeneration,
289
+ expectedActorEpoch: input.expectedActorEpoch,
290
+ transactionId: input.transactionId,
291
+ transactionSecretHash: managedAuthSha256(input.transactionSecret),
292
+ authSessionId: created.authSessionId,
293
+ mode: input.mode,
294
+ });
295
+ } catch (error) {
296
+ let receipt: Awaited<ReturnType<typeof getManagedAuthSessionSetOperationReceipt>>;
297
+ try {
298
+ receipt = await getManagedAuthSessionSetOperationReceipt(input.db, {
299
+ authorityHash: managedAuthSha256(input.authority),
300
+ operationId: input.operationId,
301
+ requestDigest: input.requestDigest,
302
+ });
303
+ } catch (receiptError) {
304
+ throw new ManagedAuthCompletionOutcomeUnknownError({ cause: receiptError });
305
+ }
306
+ if (receipt) {
307
+ await reconcileCreatedManagedAuthSession(input, created.authSessionId);
308
+ return receipt;
309
+ }
310
+ await input.adapter.revokeSession(created).catch(() => undefined);
311
+ throw error;
312
+ }
313
+ try {
314
+ await reconcileCreatedManagedAuthSession(input, created.authSessionId);
315
+ } catch (error) {
316
+ throw new ManagedAuthCompletionOutcomeUnknownError({ cause: error });
317
+ }
318
+ return completed;
319
+ }
320
+
321
+ async function reconcileCreatedManagedAuthSession(
322
+ input: Pick<
323
+ Parameters<typeof authenticateAndAdoptManagedAuthSession>[0],
324
+ "db" | "adapter" | "authority" | "mode"
325
+ >,
326
+ authSessionId: string,
327
+ ): Promise<void> {
328
+ const snapshot = await getManagedAuthSessionSetSnapshot(input.db, {
329
+ authorityHash: managedAuthSha256(input.authority),
330
+ mode: input.mode,
331
+ includeInternal: true,
332
+ readOnly: true,
333
+ });
334
+ if (snapshot?.internalSlots.some((slot) => slot.authSessionId === authSessionId)) return;
335
+ await input.adapter.revokeSession({ authSessionId });
336
+ }
337
+
338
+ export function isolatedManagedAuthHeaders(request: Request): Headers {
339
+ const headers = new Headers(request.headers);
340
+ headers.delete("cookie");
341
+ headers.delete("authorization");
342
+ headers.delete("x-forwarded-user");
343
+ return headers;
344
+ }