@opengeni/db 0.27.7 → 0.27.9
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/{chunk-IHPCI4GV.js → chunk-2JFRTKTG.js} +1 -1
- package/dist/chunk-2JFRTKTG.js.map +1 -0
- package/dist/{chunk-EX7CDSGH.js → chunk-NX2JNJJP.js} +13 -4
- package/dist/chunk-NX2JNJJP.js.map +1 -0
- package/dist/codex-token-resolver.d.ts +50 -8
- package/dist/connection-token-resolver.d.ts +45 -7
- package/dist/database.d.ts +137 -0
- package/dist/index.d.ts +25 -154
- package/dist/index.js +2796 -2634
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +1 -1
- package/dist/memory-governance.d.ts +1 -1
- package/dist/new-session-drafts.d.ts +1 -1
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +1 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +1 -1
- package/dist/schema.d.ts +44 -0
- package/dist/schema.js +1 -1
- package/dist/scoped-knowledge.d.ts +1 -1
- package/dist/session-control.d.ts +2 -1
- package/dist/session-queue-commands.d.ts +1 -1
- package/dist/session-realtime-context.d.ts +1 -1
- package/dist/session-realtime-ledger.d.ts +1 -1
- package/dist/session-realtime-mirror.d.ts +1 -1
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +1 -1
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/turn-initiator.d.ts +1 -1
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/drizzle/0170_session_control_wake_revision.sql +133 -0
- package/drizzle/0171_social_connection_subject_ownership.sql +53 -0
- package/package.json +3 -3
- package/src/codex-token-resolver.ts +102 -49
- package/src/connection-token-resolver.ts +67 -26
- package/src/database.ts +393 -0
- package/src/index.ts +439 -516
- package/src/insights.ts +2 -2
- package/src/memory-governance.ts +2 -2
- package/src/new-session-drafts.ts +1 -1
- package/src/preference-registry.ts +2 -2
- package/src/provision-roles.ts +1 -1
- package/src/runtime-posture.ts +1 -1
- package/src/schema.ts +16 -3
- package/src/scoped-knowledge.ts +2 -2
- package/src/session-control.ts +12 -5
- package/src/session-queue-commands.ts +4 -1
- package/src/session-realtime-context.ts +1 -1
- package/src/session-realtime-ledger.ts +1 -1
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-realtime-state.ts +1 -1
- package/src/session-realtime-terminal.ts +1 -1
- package/src/session-realtime.ts +1 -1
- package/src/session-tool-call-settlement.ts +1 -1
- package/src/turn-initiator.ts +1 -1
- package/src/workspace-artifacts.ts +2 -2
- package/src/workspace-instruction-policies.ts +2 -2
- package/dist/chunk-EX7CDSGH.js.map +0 -1
- package/dist/chunk-IHPCI4GV.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -154,7 +154,12 @@ import {
|
|
|
154
154
|
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
155
155
|
type Settings,
|
|
156
156
|
} from "@opengeni/config";
|
|
157
|
-
import {
|
|
157
|
+
import {
|
|
158
|
+
boundModelToolOutputItem,
|
|
159
|
+
isCodexBilledModel,
|
|
160
|
+
refreshCodexToken,
|
|
161
|
+
type CodexFetch,
|
|
162
|
+
} from "@opengeni/codex";
|
|
158
163
|
// Re-exported so consumers get the whole codex-billed detection surface (the pure
|
|
159
164
|
// prefix test + the credential-aware predicates below) from a single import.
|
|
160
165
|
export { isCodexBilledModel } from "@opengeni/codex";
|
|
@@ -187,16 +192,10 @@ import {
|
|
|
187
192
|
type FrozenTurnInitiator,
|
|
188
193
|
} from "./turn-initiator";
|
|
189
194
|
export { frozenInitiatorForCommandActor, type FrozenTurnInitiator } from "./turn-initiator";
|
|
190
|
-
import
|
|
191
|
-
import { drizzle } from "drizzle-orm/postgres-js";
|
|
192
|
-
import postgres from "postgres";
|
|
193
|
-
import { decryptEnvironmentValue } from "./environment-crypto";
|
|
195
|
+
import { decryptEnvironmentValue, encryptEnvironmentValue } from "./environment-crypto";
|
|
194
196
|
import { sanitizeEventPayload, sanitizeModelPayload } from "./event-payload-sanitizer";
|
|
195
197
|
import { seedNewSessionDraftInTransaction } from "./new-session-drafts";
|
|
196
|
-
import {
|
|
197
|
-
runIdempotentPersistenceTransaction,
|
|
198
|
-
type IdempotentPersistenceTransactionOptions,
|
|
199
|
-
} from "./persistence-errors";
|
|
198
|
+
import { runIdempotentPersistenceTransaction } from "./persistence-errors";
|
|
200
199
|
import {
|
|
201
200
|
closePendingSessionToolCallsInTransaction,
|
|
202
201
|
historyCallId,
|
|
@@ -303,22 +302,58 @@ export {
|
|
|
303
302
|
// Workspace Memory V1 pure domain surface (gates, render, canonical prompt text).
|
|
304
303
|
export * from "./memory-domain";
|
|
305
304
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
305
|
+
import {
|
|
306
|
+
dbBindingFor,
|
|
307
|
+
rawRows,
|
|
308
|
+
retryRlsPersistence,
|
|
309
|
+
retryWorkspacePersistence,
|
|
310
|
+
rlsContextForWorkspace,
|
|
311
|
+
setRlsContext,
|
|
312
|
+
setSubjectRlsContext,
|
|
313
|
+
withAccountRls,
|
|
314
|
+
withRlsContext,
|
|
315
|
+
withWorkspaceRls,
|
|
316
|
+
withWorkspaceSubjectRls,
|
|
317
|
+
withWorkspaceUsageLock,
|
|
318
|
+
type Database,
|
|
319
|
+
} from "./database";
|
|
320
|
+
export {
|
|
321
|
+
createDb,
|
|
322
|
+
registerDbBinding,
|
|
323
|
+
rlsContextForWorkspace,
|
|
324
|
+
rlsStrategyFor,
|
|
325
|
+
setRlsContext,
|
|
326
|
+
setSubjectRlsContext,
|
|
327
|
+
withAccountRls,
|
|
328
|
+
withDatabaseStatementTimeout,
|
|
329
|
+
withRlsContext,
|
|
330
|
+
withWorkspaceRls,
|
|
331
|
+
withWorkspaceSubjectRls,
|
|
332
|
+
withWorkspaceUsageLock,
|
|
333
|
+
type CreateDbOptions,
|
|
334
|
+
type Database,
|
|
335
|
+
type DbClient,
|
|
336
|
+
type RlsContext,
|
|
337
|
+
type RlsStrategy,
|
|
338
|
+
type UserLookup,
|
|
339
|
+
} from "./database";
|
|
340
|
+
import {
|
|
341
|
+
buildCodexTokenResolver as buildCodexTokenResolverCore,
|
|
342
|
+
fetchCodexRateLimitResetCreditsForAccount as fetchCodexRateLimitResetCreditsForAccountCore,
|
|
343
|
+
fetchCodexUsageForAccount as fetchCodexUsageForAccountCore,
|
|
344
|
+
type CodexAccountUsageSnapshot,
|
|
345
|
+
type CodexAuthDeps,
|
|
346
|
+
type CodexCredentialForRun,
|
|
347
|
+
type CodexCredentialTokens,
|
|
348
|
+
} from "./codex-token-resolver";
|
|
349
|
+
import {
|
|
350
|
+
buildConnectionTokenResolver as buildConnectionTokenResolverCore,
|
|
351
|
+
refreshOAuthConnectionCredential as refreshOAuthConnectionCredentialCore,
|
|
352
|
+
type ConnectionBrokerDeps,
|
|
353
|
+
type ConnectionCredentialForBroker,
|
|
354
|
+
type ConnectionTokenResolverOptions,
|
|
355
|
+
} from "./connection-token-resolver";
|
|
356
|
+
|
|
322
357
|
function parsedPersonalConnectionDelegations(
|
|
323
358
|
value: unknown,
|
|
324
359
|
context: string,
|
|
@@ -330,8 +365,6 @@ function parsedPersonalConnectionDelegations(
|
|
|
330
365
|
return parsed.data.map((delegation) => ({ ...delegation }));
|
|
331
366
|
}
|
|
332
367
|
|
|
333
|
-
export type Database = PgDatabase<any, typeof schema>;
|
|
334
|
-
|
|
335
368
|
/** Raised when a durable session tool-policy write lost its version fence. */
|
|
336
369
|
export class SessionToolPolicyVersionConflictError extends Error {
|
|
337
370
|
readonly code = "SESSION_TOOL_POLICY_CONFLICT";
|
|
@@ -342,16 +375,6 @@ export class SessionToolPolicyVersionConflictError extends Error {
|
|
|
342
375
|
}
|
|
343
376
|
}
|
|
344
377
|
|
|
345
|
-
export type DbClient = {
|
|
346
|
-
db: Database;
|
|
347
|
-
close: () => Promise<void>;
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
export type RlsContext = {
|
|
351
|
-
accountId: string;
|
|
352
|
-
workspaceId?: string | null;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
378
|
export type NestedAgentDepthPolicySource = "session" | "workspace" | "deployment" | "default";
|
|
356
379
|
|
|
357
380
|
export type SessionDepthPolicy = {
|
|
@@ -414,165 +437,6 @@ export class SessionSpawnDeniedDbError extends Error {
|
|
|
414
437
|
}
|
|
415
438
|
}
|
|
416
439
|
|
|
417
|
-
/**
|
|
418
|
-
* RLS posture for the connection OpenGeni's query layer runs over (Step I, §7.7).
|
|
419
|
-
*
|
|
420
|
-
* - `"force"` (DEFAULT — today's standalone behavior, byte-for-byte): OpenGeni
|
|
421
|
-
* connects as a NON-OWNER role (`opengeni_app`) and every table carries
|
|
422
|
-
* `FORCE ROW LEVEL SECURITY`, so the workspace/account GUCs set by
|
|
423
|
-
* `setRlsContext` are the ONLY thing that admits rows — even the table owner
|
|
424
|
-
* is subject to RLS. This is the Fork-A isolation guarantee.
|
|
425
|
-
* - `"scoped"` (embedded Fork-B opt-in): the host runs OpenGeni's queries over a
|
|
426
|
-
* role that OWNS the dedicated schema (RLS need not be forced for that role),
|
|
427
|
-
* relying on the host's own tenant boundary. OpenGeni STILL emits the
|
|
428
|
-
* `set_config('opengeni.account_id'/'workspace_id', …)` GUCs defensively on
|
|
429
|
-
* every scoped query, so the application query path is byte-identical between
|
|
430
|
-
* the two strategies and the app code is RLS-mode-agnostic. The strategy is a
|
|
431
|
-
* declared posture (consumed by `provisionRoles` and as a documented
|
|
432
|
-
* invariant), NOT a query-path branch — there is deliberately no `if
|
|
433
|
-
* (strategy === …)` anywhere in the helpers below. Picking `"scoped"` does not
|
|
434
|
-
* relax any GUC; it only changes which DB role the host provisions/connects as
|
|
435
|
-
* and asserts that the host accepts owning the isolation boundary.
|
|
436
|
-
*/
|
|
437
|
-
export type RlsStrategy = "force" | "scoped";
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Resolve a host-IdP/Better-Auth user *identifier* by email. Injected via
|
|
441
|
-
* `createDb({ userLookup })` (Step I). UNSET → today's raw parameterized select
|
|
442
|
-
* against Better Auth's `auth_users` table (see `getManagedUserByEmail`), which
|
|
443
|
-
* relies on the postgres-js array-shaped `db.execute` result. An embedded host
|
|
444
|
-
* whose identity lives elsewhere (a different IdP table, a different driver, or
|
|
445
|
-
* a non-`auth_users` user store) injects this closure so OpenGeni never touches
|
|
446
|
-
* `auth_users` directly. Returns the user id, or null when no such user exists.
|
|
447
|
-
*/
|
|
448
|
-
export type UserLookup = (db: Database, email: string) => Promise<string | null>;
|
|
449
|
-
|
|
450
|
-
export type CreateDbOptions = {
|
|
451
|
-
/**
|
|
452
|
-
* The Postgres `search_path` for this connection (Step I, §7.8 runtime half).
|
|
453
|
-
* UNSET → today's behavior: NO `search_path` startup parameter is sent, so the
|
|
454
|
-
* server default applies (`public` for standalone, where every table + the
|
|
455
|
-
* `vector` extension + `gen_random_uuid()` live). For an embedded dedicated
|
|
456
|
-
* schema, pass e.g. `"opengeni,opengeni_private,public"` — postgres-js sends
|
|
457
|
-
* it as a per-session startup parameter (the supported, query-param-free way;
|
|
458
|
-
* URL `?search_path=` is IGNORED by postgres-js). Keep `public` LAST so the
|
|
459
|
-
* `vector` type and `gen_random_uuid()` (which live in `public` on the
|
|
460
|
-
* pgvector image) still resolve — the schema-isolation contract live footgun.
|
|
461
|
-
*/
|
|
462
|
-
searchPath?: string;
|
|
463
|
-
/** RLS posture; defaults to `"force"` (today's standalone). */
|
|
464
|
-
rlsStrategy?: RlsStrategy;
|
|
465
|
-
/** Host-provided user-by-email resolver; unset → today's raw `auth_users` query. */
|
|
466
|
-
userLookup?: UserLookup;
|
|
467
|
-
/** postgres-js pool size; defaults to today's `10`. */
|
|
468
|
-
max?: number;
|
|
469
|
-
/**
|
|
470
|
-
* Connection-local default transaction isolation sent in the postgres-js
|
|
471
|
-
* startup parameters. This is intentionally not a role/database default:
|
|
472
|
-
* tests and embedded callers can exercise a different ambient isolation
|
|
473
|
-
* without mutating a shared PostgreSQL role or affecting other connections.
|
|
474
|
-
*/
|
|
475
|
-
isolationLevel?: postgres.ConnectionParameters["default_transaction_isolation"];
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* The active RLS strategy + userLookup for an injected `Database`, recorded in a
|
|
480
|
-
* side WeakMap so helpers (and `getManagedUserByEmail`) can consult the host's
|
|
481
|
-
* binding without changing every call signature. A handle with no recorded
|
|
482
|
-
* config (e.g. one built outside `createDb`, or in a test) falls back to the
|
|
483
|
-
* standalone defaults: `rlsStrategy: "force"`, raw `auth_users` lookup.
|
|
484
|
-
*/
|
|
485
|
-
type DbBinding = { rlsStrategy: RlsStrategy; userLookup?: UserLookup };
|
|
486
|
-
const dbBindings = new WeakMap<object, DbBinding>();
|
|
487
|
-
|
|
488
|
-
/** The strategy bound to a handle (or the `"force"` default). */
|
|
489
|
-
export function rlsStrategyFor(db: Database): RlsStrategy {
|
|
490
|
-
return dbBindings.get(db as unknown as object)?.rlsStrategy ?? "force";
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Run a raw SQL query and read its rows as a typed array.
|
|
495
|
-
*
|
|
496
|
-
* Why this exists: the Step I driver widening (`Database = PgDatabase<any, …>`)
|
|
497
|
-
* deliberately sets the query-result HKT to `any` so `db.execute(…)` is callable
|
|
498
|
-
* across drivers whose raw-result shapes differ (postgres-js → row array;
|
|
499
|
-
* node-postgres → `{ rows }`). A side effect is that `db.execute<T>(…)` now
|
|
500
|
-
* resolves to `any`, erasing the per-row element type at the call site. OpenGeni's
|
|
501
|
-
* OWN internal raw queries usually run over the postgres-js handle `createDb`
|
|
502
|
-
* builds (array result), while an embedded host may inject a node-postgres style
|
|
503
|
-
* driver (`{ rows }`). Normalize those two standard shapes in one place; reject
|
|
504
|
-
* an unknown driver result rather than silently treating it as an empty query.
|
|
505
|
-
*/
|
|
506
|
-
async function rawRows<T extends Record<string, unknown>>(
|
|
507
|
-
executor: Pick<Database, "execute">,
|
|
508
|
-
query: SQL,
|
|
509
|
-
): Promise<T[]> {
|
|
510
|
-
const result = await executor.execute<T>(query);
|
|
511
|
-
if (Array.isArray(result)) {
|
|
512
|
-
return result as unknown as T[];
|
|
513
|
-
}
|
|
514
|
-
const rows = (result as unknown as { rows?: unknown }).rows;
|
|
515
|
-
if (Array.isArray(rows)) {
|
|
516
|
-
return rows as T[];
|
|
517
|
-
}
|
|
518
|
-
throw new Error("Unsupported database execute() result shape");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export function createDb(databaseUrl: string, options: CreateDbOptions = {}): DbClient {
|
|
522
|
-
// `prepare: false` is REQUIRED for Azure Database for PostgreSQL Flexible
|
|
523
|
-
// Server's transaction-pooling PgBouncer: postgres-js's default named prepared
|
|
524
|
-
// statements (`s_N`) are bound to one backend, but a transaction pooler hands
|
|
525
|
-
// each transaction a different backend, so a later `execute` intermittently
|
|
526
|
-
// throws `prepared statement "s_N" does not exist`. Every RLS read in this
|
|
527
|
-
// module (set_config + SELECT inside one db.transaction) rides on this pool, so
|
|
528
|
-
// the failure surfaces as a "worked, then didn't" credential/permission read.
|
|
529
|
-
// idle_timeout + max_lifetime recycle connections so a pooler-recycled backend
|
|
530
|
-
// is never reused indefinitely; application_name aids server-side diagnostics.
|
|
531
|
-
const client = postgres(databaseUrl, {
|
|
532
|
-
max: options.max ?? 10,
|
|
533
|
-
prepare: false,
|
|
534
|
-
idle_timeout: 30,
|
|
535
|
-
max_lifetime: 1800,
|
|
536
|
-
// `connection` carries per-session Postgres STARTUP parameters. `application_name`
|
|
537
|
-
// (always) aids server-side diagnostics; `search_path` (embedded only) is the
|
|
538
|
-
// supported, query-param-free way to scope a connection to a dedicated schema —
|
|
539
|
-
// postgres-js IGNORES a URL `?search_path=`. Unset searchPath → omit it so the
|
|
540
|
-
// server default (`public`) is unchanged for standalone.
|
|
541
|
-
connection: {
|
|
542
|
-
application_name: "opengeni",
|
|
543
|
-
...(options.searchPath ? { search_path: options.searchPath } : {}),
|
|
544
|
-
...(options.isolationLevel ? { default_transaction_isolation: options.isolationLevel } : {}),
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
const db = drizzle(client, { schema });
|
|
548
|
-
dbBindings.set(db as unknown as object, {
|
|
549
|
-
rlsStrategy: options.rlsStrategy ?? "force",
|
|
550
|
-
...(options.userLookup ? { userLookup: options.userLookup } : {}),
|
|
551
|
-
});
|
|
552
|
-
return {
|
|
553
|
-
db,
|
|
554
|
-
close: async () => {
|
|
555
|
-
await client.end();
|
|
556
|
-
},
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Register a host's `rlsStrategy`/`userLookup` against an externally-constructed
|
|
562
|
-
* `Database` handle (e.g. one the embedded host built from its own driver and
|
|
563
|
-
* injected, rather than via `createDb`). Lets the same WeakMap-backed lookups
|
|
564
|
-
* work for injected handles. Standalone never calls this (it uses `createDb`).
|
|
565
|
-
*/
|
|
566
|
-
export function registerDbBinding(
|
|
567
|
-
db: Database,
|
|
568
|
-
binding: { rlsStrategy?: RlsStrategy; userLookup?: UserLookup },
|
|
569
|
-
): void {
|
|
570
|
-
dbBindings.set(db as unknown as object, {
|
|
571
|
-
rlsStrategy: binding.rlsStrategy ?? "force",
|
|
572
|
-
...(binding.userLookup ? { userLookup: binding.userLookup } : {}),
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
|
|
576
440
|
// ---------------------------------------------------------------------------
|
|
577
441
|
// Durable host export
|
|
578
442
|
// ---------------------------------------------------------------------------
|
|
@@ -1048,187 +912,6 @@ export async function getHostExportConsumerStatus(
|
|
|
1048
912
|
};
|
|
1049
913
|
}
|
|
1050
914
|
|
|
1051
|
-
export async function setRlsContext(db: Database, context: RlsContext): Promise<void> {
|
|
1052
|
-
// Fail loud on an empty/blank account id: a "" account would set an RLS GUC
|
|
1053
|
-
// that matches no tenant row, silently returning zero rows from every scoped
|
|
1054
|
-
// read (a phantom "not found" / "no active subscription"). An RLS context with
|
|
1055
|
-
// no account is always a bug at the call site, never a valid query scope.
|
|
1056
|
-
if (typeof context.accountId !== "string" || context.accountId.trim() === "") {
|
|
1057
|
-
throw new Error("setRlsContext: a non-empty accountId is required to establish an RLS context");
|
|
1058
|
-
}
|
|
1059
|
-
await db.execute(sql`select set_config('opengeni.account_id', ${context.accountId}, true)`);
|
|
1060
|
-
await db.execute(
|
|
1061
|
-
sql`select set_config('opengeni.workspace_id', ${context.workspaceId ?? ""}, true)`,
|
|
1062
|
-
);
|
|
1063
|
-
await db.execute(sql`select set_config('opengeni.sandbox_recovery_protocol_v2', '1', true)`);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
export async function withRlsContext<T>(
|
|
1067
|
-
db: Database,
|
|
1068
|
-
context: RlsContext,
|
|
1069
|
-
fn: (db: Database) => Promise<T>,
|
|
1070
|
-
transactionConfig?: PgTransactionConfig,
|
|
1071
|
-
): Promise<T> {
|
|
1072
|
-
return await db.transaction(async (tx) => {
|
|
1073
|
-
const scoped = tx as unknown as Database;
|
|
1074
|
-
await setRlsContext(scoped, context);
|
|
1075
|
-
// Defense-in-depth: read the LOCAL GUC back on THIS backend BEFORE running
|
|
1076
|
-
// the scoped query. The set_config and this read share one db.transaction,
|
|
1077
|
-
// which a transaction pooler pins to a single backend — so a mismatch here
|
|
1078
|
-
// means the context was genuinely lost (a torn transaction / pooler backend
|
|
1079
|
-
// swap), not normal operation. Without this guard such an event runs the
|
|
1080
|
-
// scoped read with an empty account_id and returns zero RLS-visible rows,
|
|
1081
|
-
// manufacturing a phantom "no active subscription" from a credential that is
|
|
1082
|
-
// in fact active. Convert that silent false into a loud, root-cause-bearing
|
|
1083
|
-
// error so the caller can retry rather than permanently mis-decide.
|
|
1084
|
-
const applied = await tx.execute<{
|
|
1085
|
-
account_id: string | null;
|
|
1086
|
-
workspace_id: string | null;
|
|
1087
|
-
}>(
|
|
1088
|
-
sql`select
|
|
1089
|
-
current_setting('opengeni.account_id', true) as account_id,
|
|
1090
|
-
current_setting('opengeni.workspace_id', true) as workspace_id`,
|
|
1091
|
-
);
|
|
1092
|
-
const appliedAccountId = applied[0]?.account_id ?? "";
|
|
1093
|
-
const expectedWorkspaceId = context.workspaceId ?? "";
|
|
1094
|
-
const appliedWorkspaceId = applied[0]?.workspace_id ?? "";
|
|
1095
|
-
if (appliedAccountId !== context.accountId) {
|
|
1096
|
-
throw new Error(
|
|
1097
|
-
`RLS context not applied on the active backend: expected account ${context.accountId}, got "${appliedAccountId}"`,
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
if (appliedWorkspaceId !== expectedWorkspaceId) {
|
|
1101
|
-
throw new Error(
|
|
1102
|
-
`RLS context not applied on the active backend: expected workspace "${expectedWorkspaceId}", got "${appliedWorkspaceId}"`,
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
return await fn(scoped);
|
|
1106
|
-
}, transactionConfig);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Run one bounded database operation on a transaction-pinned backend.
|
|
1111
|
-
*
|
|
1112
|
-
* Callers that also have an application deadline should check their abort
|
|
1113
|
-
* signal before returning from `fn`; throwing there rolls the transaction back
|
|
1114
|
-
* even when the application deadline won a surrounding Promise race.
|
|
1115
|
-
*/
|
|
1116
|
-
export async function withDatabaseStatementTimeout<T>(
|
|
1117
|
-
db: Database,
|
|
1118
|
-
timeoutMs: number,
|
|
1119
|
-
fn: (db: Database) => Promise<T>,
|
|
1120
|
-
): Promise<T> {
|
|
1121
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1122
|
-
throw new Error("withDatabaseStatementTimeout requires a positive timeout");
|
|
1123
|
-
}
|
|
1124
|
-
const boundedTimeoutMs = Math.max(1, Math.floor(timeoutMs));
|
|
1125
|
-
return await db.transaction(async (tx) => {
|
|
1126
|
-
const scoped = tx as unknown as Database;
|
|
1127
|
-
await scoped.execute(
|
|
1128
|
-
sql`select set_config('statement_timeout', ${`${boundedTimeoutMs}ms`}, true)`,
|
|
1129
|
-
);
|
|
1130
|
-
return await fn(scoped);
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
export async function rlsContextForWorkspace(
|
|
1135
|
-
db: Database,
|
|
1136
|
-
workspaceId: string,
|
|
1137
|
-
): Promise<RlsContext> {
|
|
1138
|
-
const [row] = await db
|
|
1139
|
-
.select({ accountId: schema.workspaces.accountId })
|
|
1140
|
-
.from(schema.workspaces)
|
|
1141
|
-
.where(eq(schema.workspaces.id, workspaceId))
|
|
1142
|
-
.limit(1);
|
|
1143
|
-
if (!row) {
|
|
1144
|
-
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
1145
|
-
}
|
|
1146
|
-
return { accountId: row.accountId, workspaceId };
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
export async function withWorkspaceRls<T>(
|
|
1150
|
-
db: Database,
|
|
1151
|
-
workspaceId: string,
|
|
1152
|
-
fn: (db: Database) => Promise<T>,
|
|
1153
|
-
): Promise<T> {
|
|
1154
|
-
return await withRlsContext(db, await rlsContextForWorkspace(db, workspaceId), fn);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
async function retryWorkspacePersistence<T>(
|
|
1158
|
-
db: Database,
|
|
1159
|
-
workspaceId: string,
|
|
1160
|
-
options: IdempotentPersistenceTransactionOptions,
|
|
1161
|
-
fn: (db: Database) => Promise<T>,
|
|
1162
|
-
): Promise<T> {
|
|
1163
|
-
return await runIdempotentPersistenceTransaction(options, async () => {
|
|
1164
|
-
return await withWorkspaceRls(db, workspaceId, fn);
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
async function retryRlsPersistence<T>(
|
|
1169
|
-
db: Database,
|
|
1170
|
-
context: RlsContext,
|
|
1171
|
-
options: IdempotentPersistenceTransactionOptions,
|
|
1172
|
-
fn: (db: Database) => Promise<T>,
|
|
1173
|
-
): Promise<T> {
|
|
1174
|
-
return await runIdempotentPersistenceTransaction(options, async () => {
|
|
1175
|
-
return await withRlsContext(db, context, fn);
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* Personal workspace data needs both tenant and authenticated-principal GUCs.
|
|
1181
|
-
* `session_pins` uses this helper so FORCE RLS rejects another member's rows
|
|
1182
|
-
* even if a future query accidentally omits its explicit subject predicate.
|
|
1183
|
-
*/
|
|
1184
|
-
export async function withWorkspaceSubjectRls<T>(
|
|
1185
|
-
db: Database,
|
|
1186
|
-
workspaceId: string,
|
|
1187
|
-
subjectId: string,
|
|
1188
|
-
fn: (db: Database) => Promise<T>,
|
|
1189
|
-
transactionConfig?: PgTransactionConfig,
|
|
1190
|
-
): Promise<T> {
|
|
1191
|
-
if (!subjectId.trim()) {
|
|
1192
|
-
throw new Error("withWorkspaceSubjectRls: a non-empty subjectId is required");
|
|
1193
|
-
}
|
|
1194
|
-
const context = await rlsContextForWorkspace(db, workspaceId);
|
|
1195
|
-
return await withRlsContext(
|
|
1196
|
-
db,
|
|
1197
|
-
context,
|
|
1198
|
-
async (scopedDb) => {
|
|
1199
|
-
await setSubjectRlsContext(scopedDb, subjectId);
|
|
1200
|
-
return await fn(scopedDb);
|
|
1201
|
-
},
|
|
1202
|
-
transactionConfig,
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
/** Apply and verify actor-private RLS on an already transaction-pinned handle. */
|
|
1207
|
-
export async function setSubjectRlsContext(db: Database, subjectId: string): Promise<void> {
|
|
1208
|
-
if (!subjectId.trim()) {
|
|
1209
|
-
throw new Error("setSubjectRlsContext: a non-empty subjectId is required");
|
|
1210
|
-
}
|
|
1211
|
-
await db.execute(sql`select set_config('opengeni.subject_id', ${subjectId}, true)`);
|
|
1212
|
-
const applied = await db.execute<{ subject_id: string | null }>(
|
|
1213
|
-
sql`select current_setting('opengeni.subject_id', true) as subject_id`,
|
|
1214
|
-
);
|
|
1215
|
-
if ((applied[0]?.subject_id ?? "") !== subjectId) {
|
|
1216
|
-
throw new Error("Authenticated subject RLS context was not applied on the active backend");
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
export async function withWorkspaceUsageLock<T>(
|
|
1221
|
-
db: Database,
|
|
1222
|
-
workspaceId: string,
|
|
1223
|
-
fn: (db: Database) => Promise<T>,
|
|
1224
|
-
): Promise<T> {
|
|
1225
|
-
const context = await rlsContextForWorkspace(db, workspaceId);
|
|
1226
|
-
return await withRlsContext(db, context, async (scopedDb) => {
|
|
1227
|
-
await scopedDb.execute(sql`select pg_advisory_xact_lock(hashtext(${`usage:${workspaceId}`}))`);
|
|
1228
|
-
return await fn(scopedDb);
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
915
|
export type DocumentIndexAuthority = {
|
|
1233
916
|
authorityKind: DocumentAuthorityKind;
|
|
1234
917
|
authorityWorkspaceId: string | null;
|
|
@@ -1278,14 +961,6 @@ export async function resolveDocumentIndexAuthority(
|
|
|
1278
961
|
};
|
|
1279
962
|
}
|
|
1280
963
|
|
|
1281
|
-
export async function withAccountRls<T>(
|
|
1282
|
-
db: Database,
|
|
1283
|
-
accountId: string,
|
|
1284
|
-
fn: (db: Database) => Promise<T>,
|
|
1285
|
-
): Promise<T> {
|
|
1286
|
-
return await withRlsContext(db, { accountId, workspaceId: null }, fn);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
964
|
export const allWorkspacePermissions: Permission[] = [
|
|
1290
965
|
"workspace:read",
|
|
1291
966
|
"workspace:admin",
|
|
@@ -2096,7 +1771,7 @@ export async function removeWorkspaceMember(
|
|
|
2096
1771
|
* unknown users are deferred.
|
|
2097
1772
|
*/
|
|
2098
1773
|
export async function getManagedUserByEmail(db: Database, email: string): Promise<string | null> {
|
|
2099
|
-
const binding =
|
|
1774
|
+
const binding = dbBindingFor(db);
|
|
2100
1775
|
if (binding?.userLookup) {
|
|
2101
1776
|
return await binding.userLookup(db, email);
|
|
2102
1777
|
}
|
|
@@ -3499,6 +3174,7 @@ export type WorkspaceStateMemoryRecord = {
|
|
|
3499
3174
|
export type CreateSocialConnectionInput = {
|
|
3500
3175
|
accountId: string;
|
|
3501
3176
|
workspaceId: string;
|
|
3177
|
+
subjectId?: string | null;
|
|
3502
3178
|
provider: SocialProvider;
|
|
3503
3179
|
accountHandle: string;
|
|
3504
3180
|
accountName?: string | null;
|
|
@@ -3514,6 +3190,7 @@ export type CreateSocialConnectionInput = {
|
|
|
3514
3190
|
export type UpsertSocialOAuthConnectionInput = {
|
|
3515
3191
|
accountId: string;
|
|
3516
3192
|
workspaceId: string;
|
|
3193
|
+
subjectId?: string | null;
|
|
3517
3194
|
provider: SocialProvider;
|
|
3518
3195
|
accountHandle: string;
|
|
3519
3196
|
accountName?: string | null;
|
|
@@ -3526,6 +3203,7 @@ export type UpsertSocialOAuthConnectionInput = {
|
|
|
3526
3203
|
export type UpdateSocialConnectionCredentialInput = {
|
|
3527
3204
|
workspaceId: string;
|
|
3528
3205
|
connectionId: string;
|
|
3206
|
+
subjectId?: string | null;
|
|
3529
3207
|
credentialEncrypted?: string | null;
|
|
3530
3208
|
status?: SocialConnectionStatus;
|
|
3531
3209
|
tokenMetadata?: Record<string, unknown>;
|
|
@@ -3534,6 +3212,7 @@ export type UpdateSocialConnectionCredentialInput = {
|
|
|
3534
3212
|
export type CreateSocialPostInput = {
|
|
3535
3213
|
accountId: string;
|
|
3536
3214
|
workspaceId: string;
|
|
3215
|
+
subjectId?: string | null;
|
|
3537
3216
|
connectionId: string;
|
|
3538
3217
|
externalPostId?: string | null;
|
|
3539
3218
|
url?: string | null;
|
|
@@ -3672,22 +3351,6 @@ export type SlackBotDeleteOperation = {
|
|
|
3672
3351
|
updatedAt: Date;
|
|
3673
3352
|
};
|
|
3674
3353
|
|
|
3675
|
-
export type ConnectionCredentialForBroker = {
|
|
3676
|
-
id: string;
|
|
3677
|
-
accountId: string;
|
|
3678
|
-
workspaceId: string;
|
|
3679
|
-
subjectId: string | null;
|
|
3680
|
-
providerDomain: string;
|
|
3681
|
-
kind: ConnectionKind;
|
|
3682
|
-
status: ConnectionStatus;
|
|
3683
|
-
credential: Record<string, unknown>;
|
|
3684
|
-
grantedScopes: string[];
|
|
3685
|
-
expiresAt: Date | null;
|
|
3686
|
-
lastRefreshAt: Date | null;
|
|
3687
|
-
version: number;
|
|
3688
|
-
metadata: Record<string, unknown>;
|
|
3689
|
-
};
|
|
3690
|
-
|
|
3691
3354
|
export type IntegrationOAuthClientForUse = {
|
|
3692
3355
|
id: string;
|
|
3693
3356
|
issuer: string;
|
|
@@ -9262,11 +8925,13 @@ export async function createSocialConnection(
|
|
|
9262
8925
|
db,
|
|
9263
8926
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
9264
8927
|
async (scopedDb) => {
|
|
8928
|
+
if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
9265
8929
|
const [row] = await scopedDb
|
|
9266
8930
|
.insert(schema.socialConnections)
|
|
9267
8931
|
.values({
|
|
9268
8932
|
accountId: input.accountId,
|
|
9269
8933
|
workspaceId: input.workspaceId,
|
|
8934
|
+
subjectId: input.subjectId ?? null,
|
|
9270
8935
|
provider: input.provider,
|
|
9271
8936
|
accountHandle: input.accountHandle,
|
|
9272
8937
|
accountName: input.accountName ?? null,
|
|
@@ -9300,11 +8965,13 @@ export async function upsertSocialOAuthConnection(
|
|
|
9300
8965
|
db,
|
|
9301
8966
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
9302
8967
|
async (scopedDb) => {
|
|
8968
|
+
if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
9303
8969
|
const [row] = await scopedDb
|
|
9304
8970
|
.insert(schema.socialConnections)
|
|
9305
8971
|
.values({
|
|
9306
8972
|
accountId: input.accountId,
|
|
9307
8973
|
workspaceId: input.workspaceId,
|
|
8974
|
+
subjectId: input.subjectId ?? null,
|
|
9308
8975
|
provider: input.provider,
|
|
9309
8976
|
accountHandle: input.accountHandle,
|
|
9310
8977
|
accountName: input.accountName ?? null,
|
|
@@ -9315,12 +8982,21 @@ export async function upsertSocialOAuthConnection(
|
|
|
9315
8982
|
tokenMetadata: input.tokenMetadata ?? {},
|
|
9316
8983
|
})
|
|
9317
8984
|
.onConflictDoUpdate({
|
|
9318
|
-
target:
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
8985
|
+
target: input.subjectId
|
|
8986
|
+
? [
|
|
8987
|
+
schema.socialConnections.workspaceId,
|
|
8988
|
+
schema.socialConnections.subjectId,
|
|
8989
|
+
schema.socialConnections.provider,
|
|
8990
|
+
]
|
|
8991
|
+
: [
|
|
8992
|
+
schema.socialConnections.workspaceId,
|
|
8993
|
+
schema.socialConnections.provider,
|
|
8994
|
+
schema.socialConnections.accountHandle,
|
|
8995
|
+
],
|
|
8996
|
+
targetWhere: input.subjectId ? sql`subject_id is not null` : sql`subject_id is null`,
|
|
9323
8997
|
set: {
|
|
8998
|
+
accountId: input.accountId,
|
|
8999
|
+
accountHandle: input.accountHandle,
|
|
9324
9000
|
accountName: input.accountName ?? null,
|
|
9325
9001
|
externalAccountId: input.externalAccountId ?? null,
|
|
9326
9002
|
status: "connected",
|
|
@@ -9348,8 +9024,9 @@ export async function loadSocialConnectionCredential(
|
|
|
9348
9024
|
db: Database,
|
|
9349
9025
|
workspaceId: string,
|
|
9350
9026
|
connectionId: string,
|
|
9027
|
+
subjectId?: string | null,
|
|
9351
9028
|
): Promise<{ connection: SocialConnection; credentialEncrypted: string | null } | null> {
|
|
9352
|
-
return await
|
|
9029
|
+
return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
9353
9030
|
const [row] = await scopedDb
|
|
9354
9031
|
.select()
|
|
9355
9032
|
.from(schema.socialConnections)
|
|
@@ -9357,6 +9034,7 @@ export async function loadSocialConnectionCredential(
|
|
|
9357
9034
|
and(
|
|
9358
9035
|
eq(schema.socialConnections.workspaceId, workspaceId),
|
|
9359
9036
|
eq(schema.socialConnections.id, connectionId),
|
|
9037
|
+
socialConnectionSubjectVisibility(subjectId),
|
|
9360
9038
|
),
|
|
9361
9039
|
)
|
|
9362
9040
|
.limit(1);
|
|
@@ -9371,26 +9049,32 @@ export async function updateSocialConnectionCredential(
|
|
|
9371
9049
|
db: Database,
|
|
9372
9050
|
input: UpdateSocialConnectionCredentialInput,
|
|
9373
9051
|
): Promise<SocialConnection | null> {
|
|
9374
|
-
return await
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9052
|
+
return await withSocialConnectionSubjectRls(
|
|
9053
|
+
db,
|
|
9054
|
+
input.workspaceId,
|
|
9055
|
+
input.subjectId,
|
|
9056
|
+
async (scopedDb) => {
|
|
9057
|
+
const [row] = await scopedDb
|
|
9058
|
+
.update(schema.socialConnections)
|
|
9059
|
+
.set({
|
|
9060
|
+
...(input.credentialEncrypted !== undefined
|
|
9061
|
+
? { credentialEncrypted: input.credentialEncrypted }
|
|
9062
|
+
: {}),
|
|
9063
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
9064
|
+
...(input.tokenMetadata !== undefined ? { tokenMetadata: input.tokenMetadata } : {}),
|
|
9065
|
+
updatedAt: new Date(),
|
|
9066
|
+
})
|
|
9067
|
+
.where(
|
|
9068
|
+
and(
|
|
9069
|
+
eq(schema.socialConnections.workspaceId, input.workspaceId),
|
|
9070
|
+
eq(schema.socialConnections.id, input.connectionId),
|
|
9071
|
+
socialConnectionSubjectVisibility(input.subjectId),
|
|
9072
|
+
),
|
|
9073
|
+
)
|
|
9074
|
+
.returning();
|
|
9075
|
+
return row ? mapSocialConnection(row) : null;
|
|
9076
|
+
},
|
|
9077
|
+
);
|
|
9394
9078
|
}
|
|
9395
9079
|
|
|
9396
9080
|
// List/get never select credential_encrypted (same posture as the broker
|
|
@@ -9403,12 +9087,18 @@ export async function listSocialConnections(
|
|
|
9403
9087
|
db: Database,
|
|
9404
9088
|
workspaceId: string,
|
|
9405
9089
|
limit = 100,
|
|
9090
|
+
subjectId?: string | null,
|
|
9406
9091
|
): Promise<SocialConnection[]> {
|
|
9407
|
-
return await
|
|
9092
|
+
return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
9408
9093
|
const rows = await scopedDb
|
|
9409
9094
|
.select(socialConnectionPublicColumns)
|
|
9410
9095
|
.from(schema.socialConnections)
|
|
9411
|
-
.where(
|
|
9096
|
+
.where(
|
|
9097
|
+
and(
|
|
9098
|
+
eq(schema.socialConnections.workspaceId, workspaceId),
|
|
9099
|
+
socialConnectionSubjectVisibility(subjectId),
|
|
9100
|
+
),
|
|
9101
|
+
)
|
|
9412
9102
|
.orderBy(desc(schema.socialConnections.createdAt))
|
|
9413
9103
|
.limit(limit);
|
|
9414
9104
|
return rows.map(mapSocialConnection);
|
|
@@ -9419,8 +9109,9 @@ export async function getSocialConnection(
|
|
|
9419
9109
|
db: Database,
|
|
9420
9110
|
workspaceId: string,
|
|
9421
9111
|
connectionId: string,
|
|
9112
|
+
subjectId?: string | null,
|
|
9422
9113
|
): Promise<SocialConnection | null> {
|
|
9423
|
-
return await
|
|
9114
|
+
return await withSocialConnectionSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
9424
9115
|
const [row] = await scopedDb
|
|
9425
9116
|
.select(socialConnectionPublicColumns)
|
|
9426
9117
|
.from(schema.socialConnections)
|
|
@@ -9428,6 +9119,7 @@ export async function getSocialConnection(
|
|
|
9428
9119
|
and(
|
|
9429
9120
|
eq(schema.socialConnections.workspaceId, workspaceId),
|
|
9430
9121
|
eq(schema.socialConnections.id, connectionId),
|
|
9122
|
+
socialConnectionSubjectVisibility(subjectId),
|
|
9431
9123
|
),
|
|
9432
9124
|
)
|
|
9433
9125
|
.limit(1);
|
|
@@ -9439,8 +9131,9 @@ export async function requireSocialConnection(
|
|
|
9439
9131
|
db: Database,
|
|
9440
9132
|
workspaceId: string,
|
|
9441
9133
|
connectionId: string,
|
|
9134
|
+
subjectId?: string | null,
|
|
9442
9135
|
): Promise<SocialConnection> {
|
|
9443
|
-
const connection = await getSocialConnection(db, workspaceId, connectionId);
|
|
9136
|
+
const connection = await getSocialConnection(db, workspaceId, connectionId, subjectId);
|
|
9444
9137
|
if (!connection) {
|
|
9445
9138
|
throw new Error(`Social connection not found: ${connectionId}`);
|
|
9446
9139
|
}
|
|
@@ -9455,10 +9148,12 @@ export async function createSocialPost(
|
|
|
9455
9148
|
db,
|
|
9456
9149
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
9457
9150
|
async (scopedDb) => {
|
|
9151
|
+
if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
9458
9152
|
const connection = await requireSocialConnection(
|
|
9459
9153
|
scopedDb,
|
|
9460
9154
|
input.workspaceId,
|
|
9461
9155
|
input.connectionId,
|
|
9156
|
+
input.subjectId,
|
|
9462
9157
|
);
|
|
9463
9158
|
const [row] = await scopedDb
|
|
9464
9159
|
.insert(schema.socialPosts)
|
|
@@ -9495,6 +9190,7 @@ export async function recordSyncedSocialPosts(
|
|
|
9495
9190
|
accountId: string;
|
|
9496
9191
|
workspaceId: string;
|
|
9497
9192
|
connectionId: string;
|
|
9193
|
+
subjectId?: string | null;
|
|
9498
9194
|
posts: Array<{
|
|
9499
9195
|
externalPostId: string;
|
|
9500
9196
|
url?: string | null;
|
|
@@ -9513,10 +9209,12 @@ export async function recordSyncedSocialPosts(
|
|
|
9513
9209
|
db,
|
|
9514
9210
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
9515
9211
|
async (scopedDb) => {
|
|
9212
|
+
if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
9516
9213
|
const connection = await requireSocialConnection(
|
|
9517
9214
|
scopedDb,
|
|
9518
9215
|
input.workspaceId,
|
|
9519
9216
|
input.connectionId,
|
|
9217
|
+
input.subjectId,
|
|
9520
9218
|
);
|
|
9521
9219
|
const rows = await scopedDb
|
|
9522
9220
|
.insert(schema.socialPosts)
|
|
@@ -9552,6 +9250,7 @@ export async function listSocialPosts(
|
|
|
9552
9250
|
db: Database,
|
|
9553
9251
|
options: {
|
|
9554
9252
|
workspaceId: string;
|
|
9253
|
+
subjectId?: string | null;
|
|
9555
9254
|
connectionIds?: string[];
|
|
9556
9255
|
since?: Date;
|
|
9557
9256
|
limit?: number;
|
|
@@ -9565,15 +9264,20 @@ export async function listSocialPosts(
|
|
|
9565
9264
|
conditions.push(gte(schema.socialPosts.publishedAt, options.since));
|
|
9566
9265
|
}
|
|
9567
9266
|
const limit = options.limit ?? 100;
|
|
9568
|
-
return await
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9267
|
+
return await withSocialConnectionSubjectRls(
|
|
9268
|
+
db,
|
|
9269
|
+
options.workspaceId,
|
|
9270
|
+
options.subjectId,
|
|
9271
|
+
async (scopedDb) => {
|
|
9272
|
+
const rows = await scopedDb
|
|
9273
|
+
.select()
|
|
9274
|
+
.from(schema.socialPosts)
|
|
9275
|
+
.where(and(...conditions))
|
|
9276
|
+
.orderBy(desc(schema.socialPosts.publishedAt))
|
|
9277
|
+
.limit(limit);
|
|
9278
|
+
return rows.map(mapSocialPost);
|
|
9279
|
+
},
|
|
9280
|
+
);
|
|
9577
9281
|
}
|
|
9578
9282
|
|
|
9579
9283
|
export async function createScheduledTask(
|
|
@@ -11646,27 +11350,6 @@ export type WorkspaceEnvironmentForRun = VariableSetForRun;
|
|
|
11646
11350
|
// `getCodexCredentialStatus` returns metadata only (never the secret column).
|
|
11647
11351
|
// ---------------------------------------------------------------------------
|
|
11648
11352
|
|
|
11649
|
-
export type CodexCredentialTokens = {
|
|
11650
|
-
accessToken: string;
|
|
11651
|
-
refreshToken: string;
|
|
11652
|
-
idToken: string;
|
|
11653
|
-
};
|
|
11654
|
-
|
|
11655
|
-
export type CodexCredentialForRun = {
|
|
11656
|
-
id: string; // row id — for compare-and-set writes (P1-c)
|
|
11657
|
-
version: number; // optimistic-concurrency version loaded with this snapshot
|
|
11658
|
-
workspaceId: string;
|
|
11659
|
-
tokens: CodexCredentialTokens; // decrypted — never logged, never returned by a route
|
|
11660
|
-
chatgptAccountId: string | null;
|
|
11661
|
-
scopes: string | null;
|
|
11662
|
-
planType: string | null;
|
|
11663
|
-
isFedramp: boolean;
|
|
11664
|
-
expiresAt: Date | null;
|
|
11665
|
-
lastRefreshAt: Date | null;
|
|
11666
|
-
status: string;
|
|
11667
|
-
lastError: string | null;
|
|
11668
|
-
};
|
|
11669
|
-
|
|
11670
11353
|
/**
|
|
11671
11354
|
* Login / rotation write (multi-account P1). Caller passes the PRE-encrypted
|
|
11672
11355
|
* credential blob. Keyed on the composite partial index (workspace, chatgpt
|
|
@@ -15111,16 +14794,6 @@ export async function completeCodexResetRedemption(
|
|
|
15111
14794
|
}
|
|
15112
14795
|
|
|
15113
14796
|
/** The P2 usage-cache snapshot written by the refreshing usage wrapper. */
|
|
15114
|
-
export type CodexAccountUsageSnapshot = {
|
|
15115
|
-
primaryUsedPercent?: number | null;
|
|
15116
|
-
primaryResetAt?: Date | null;
|
|
15117
|
-
secondaryUsedPercent?: number | null;
|
|
15118
|
-
secondaryResetAt?: Date | null;
|
|
15119
|
-
/** Present only when the quota body parsed successfully. */
|
|
15120
|
-
checkedAt?: Date;
|
|
15121
|
-
resetCreditAvailableCount?: number | null;
|
|
15122
|
-
resetCreditsCheckedAt?: Date | null;
|
|
15123
|
-
};
|
|
15124
14797
|
|
|
15125
14798
|
/**
|
|
15126
14799
|
* Cache-write for P2 quota bars: persist the five plaintext usage columns on a
|
|
@@ -37554,6 +37227,7 @@ export async function claimSessionWorkForAttempt(
|
|
|
37554
37227
|
"delivered",
|
|
37555
37228
|
"acknowledged",
|
|
37556
37229
|
"settled",
|
|
37230
|
+
"rejected_stale",
|
|
37557
37231
|
]),
|
|
37558
37232
|
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
37559
37233
|
),
|
|
@@ -38481,28 +38155,6 @@ export async function claimSessionWorkForAttempt(
|
|
|
38481
38155
|
turn: mapSessionTurnForExecution(internalTurn),
|
|
38482
38156
|
};
|
|
38483
38157
|
}
|
|
38484
|
-
const predecessorAttemptId = queuedSteerReplacementAttemptId(queuedTurn.metadata);
|
|
38485
|
-
if (predecessorAttemptId) {
|
|
38486
|
-
const [predecessor] = await tx
|
|
38487
|
-
.select({ quiescedAt: schema.sessionTurnAttempts.quiescedAt })
|
|
38488
|
-
.from(schema.sessionTurnAttempts)
|
|
38489
|
-
.where(
|
|
38490
|
-
and(
|
|
38491
|
-
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
38492
|
-
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
38493
|
-
eq(schema.sessionTurnAttempts.id, predecessorAttemptId),
|
|
38494
|
-
),
|
|
38495
|
-
)
|
|
38496
|
-
.limit(1);
|
|
38497
|
-
if (!predecessor) {
|
|
38498
|
-
throw new SessionControlInvariantError(
|
|
38499
|
-
`Queued Steer ${id} points to missing predecessor attempt ${predecessorAttemptId}`,
|
|
38500
|
-
);
|
|
38501
|
-
}
|
|
38502
|
-
if (!predecessor.quiescedAt) {
|
|
38503
|
-
return { action: "unclaimed", reason: "control-pending" };
|
|
38504
|
-
}
|
|
38505
|
-
}
|
|
38506
38158
|
// The database guard makes this function the only supported
|
|
38507
38159
|
// queued-to-running transition. Raw or stale claimers cannot bypass the
|
|
38508
38160
|
// generation/active-pointer transaction.
|
|
@@ -39136,6 +38788,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
39136
38788
|
interruptions.map((interruption) => interruption.id),
|
|
39137
38789
|
),
|
|
39138
38790
|
);
|
|
38791
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
38792
|
+
accountId: session.accountId,
|
|
38793
|
+
workspaceId,
|
|
38794
|
+
sessionId,
|
|
38795
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
|
|
38796
|
+
reason: "attempt_interruption_rejected_stale",
|
|
38797
|
+
controlRequested: true,
|
|
38798
|
+
});
|
|
39139
38799
|
return {
|
|
39140
38800
|
action: effectiveControl.state === "paused" ? "paused" : "continue",
|
|
39141
38801
|
events: [],
|
|
@@ -39327,6 +38987,14 @@ export async function settleSessionAttemptInterruptions(
|
|
|
39327
38987
|
interruptions.map((interruption) => interruption.id),
|
|
39328
38988
|
),
|
|
39329
38989
|
);
|
|
38990
|
+
await enqueueSessionWorkflowWakeInTransaction(tx as unknown as Database, {
|
|
38991
|
+
accountId: session.accountId,
|
|
38992
|
+
workspaceId,
|
|
38993
|
+
sessionId,
|
|
38994
|
+
temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
|
|
38995
|
+
reason: "attempt_interruption_settled",
|
|
38996
|
+
controlRequested: true,
|
|
38997
|
+
});
|
|
39330
38998
|
return {
|
|
39331
38999
|
action: effectiveControl.state === "paused" ? "paused" : "continue",
|
|
39332
39000
|
events: [...closedTools.events, ...eventRows.map(mapEvent)],
|
|
@@ -39355,6 +39023,50 @@ export type SessionWorkPeek =
|
|
|
39355
39023
|
| { kind: "cancellation-wait"; attemptId: string }
|
|
39356
39024
|
| { kind: "idle" };
|
|
39357
39025
|
|
|
39026
|
+
/**
|
|
39027
|
+
* Return the oldest exact attempt whose logical interruption settled but whose
|
|
39028
|
+
* physical quiescence receipt is still missing. This deliberately searches all
|
|
39029
|
+
* attempts: a provider-recovery race can create a newer generation before the
|
|
39030
|
+
* predecessor receipt is reconciled, and looking only at the newest attempt
|
|
39031
|
+
* would strand the replacement forever.
|
|
39032
|
+
*/
|
|
39033
|
+
async function nextSessionAttemptAwaitingQuiescence(
|
|
39034
|
+
db: Database,
|
|
39035
|
+
workspaceId: string,
|
|
39036
|
+
sessionId: string,
|
|
39037
|
+
): Promise<{
|
|
39038
|
+
attemptId: string;
|
|
39039
|
+
} | null> {
|
|
39040
|
+
const [row] = await db
|
|
39041
|
+
.select({
|
|
39042
|
+
attemptId: schema.sessionTurnAttempts.id,
|
|
39043
|
+
})
|
|
39044
|
+
.from(schema.sessionTurnAttempts)
|
|
39045
|
+
.innerJoin(
|
|
39046
|
+
schema.sessionAttemptInterruptions,
|
|
39047
|
+
and(
|
|
39048
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
|
|
39049
|
+
eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
|
|
39050
|
+
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
39051
|
+
),
|
|
39052
|
+
)
|
|
39053
|
+
.where(
|
|
39054
|
+
and(
|
|
39055
|
+
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
39056
|
+
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
39057
|
+
eq(schema.sessionTurnAttempts.state, "closed"),
|
|
39058
|
+
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
39059
|
+
inArray(schema.sessionAttemptInterruptions.state, ["settled", "rejected_stale"]),
|
|
39060
|
+
),
|
|
39061
|
+
)
|
|
39062
|
+
.orderBy(
|
|
39063
|
+
asc(schema.sessionAttemptInterruptions.requestedAt),
|
|
39064
|
+
asc(schema.sessionAttemptInterruptions.id),
|
|
39065
|
+
)
|
|
39066
|
+
.limit(1);
|
|
39067
|
+
return row ?? null;
|
|
39068
|
+
}
|
|
39069
|
+
|
|
39358
39070
|
async function latestSessionAttemptInterruption(
|
|
39359
39071
|
db: Database,
|
|
39360
39072
|
workspaceId: string,
|
|
@@ -39402,6 +39114,43 @@ async function latestSessionAttemptInterruption(
|
|
|
39402
39114
|
: null;
|
|
39403
39115
|
}
|
|
39404
39116
|
|
|
39117
|
+
async function queuedSteerHasUnquiescedPredecessor(
|
|
39118
|
+
db: Database,
|
|
39119
|
+
workspaceId: string,
|
|
39120
|
+
sessionId: string,
|
|
39121
|
+
predecessorAttemptIds: string[],
|
|
39122
|
+
): Promise<boolean> {
|
|
39123
|
+
if (predecessorAttemptIds.length === 0) return false;
|
|
39124
|
+
const [row] = await db
|
|
39125
|
+
.select({ attemptId: schema.sessionTurnAttempts.id })
|
|
39126
|
+
.from(schema.sessionTurnAttempts)
|
|
39127
|
+
.innerJoin(
|
|
39128
|
+
schema.sessionAttemptInterruptions,
|
|
39129
|
+
and(
|
|
39130
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, schema.sessionTurnAttempts.workspaceId),
|
|
39131
|
+
eq(schema.sessionAttemptInterruptions.sessionId, schema.sessionTurnAttempts.sessionId),
|
|
39132
|
+
eq(schema.sessionAttemptInterruptions.attemptId, schema.sessionTurnAttempts.id),
|
|
39133
|
+
),
|
|
39134
|
+
)
|
|
39135
|
+
.where(
|
|
39136
|
+
and(
|
|
39137
|
+
eq(schema.sessionTurnAttempts.workspaceId, workspaceId),
|
|
39138
|
+
eq(schema.sessionTurnAttempts.sessionId, sessionId),
|
|
39139
|
+
inArray(schema.sessionTurnAttempts.id, predecessorAttemptIds),
|
|
39140
|
+
isNull(schema.sessionTurnAttempts.quiescedAt),
|
|
39141
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
39142
|
+
"pending",
|
|
39143
|
+
"delivered",
|
|
39144
|
+
"acknowledged",
|
|
39145
|
+
"settled",
|
|
39146
|
+
"rejected_stale",
|
|
39147
|
+
]),
|
|
39148
|
+
),
|
|
39149
|
+
)
|
|
39150
|
+
.limit(1);
|
|
39151
|
+
return row !== undefined;
|
|
39152
|
+
}
|
|
39153
|
+
|
|
39405
39154
|
/** Read durable session state without reserving a turn-worker slot or mutating it. */
|
|
39406
39155
|
export async function peekSessionWork(
|
|
39407
39156
|
db: Database,
|
|
@@ -39445,19 +39194,15 @@ export async function peekSessionWork(
|
|
|
39445
39194
|
}
|
|
39446
39195
|
if (effectiveControl.state !== "active") return { kind: "idle" };
|
|
39447
39196
|
|
|
39448
|
-
const
|
|
39197
|
+
const awaitingQuiescence = await nextSessionAttemptAwaitingQuiescence(
|
|
39449
39198
|
scopedDb,
|
|
39450
39199
|
workspaceId,
|
|
39451
39200
|
sessionId,
|
|
39452
39201
|
);
|
|
39453
|
-
if (
|
|
39454
|
-
latestInterruption &&
|
|
39455
|
-
latestInterruption.quiescedAt === null &&
|
|
39456
|
-
latestInterruption.interruptionState === "settled"
|
|
39457
|
-
) {
|
|
39202
|
+
if (awaitingQuiescence) {
|
|
39458
39203
|
return {
|
|
39459
39204
|
kind: "cancellation-wait",
|
|
39460
|
-
attemptId:
|
|
39205
|
+
attemptId: awaitingQuiescence.attemptId,
|
|
39461
39206
|
};
|
|
39462
39207
|
}
|
|
39463
39208
|
|
|
@@ -41315,6 +41060,7 @@ export type RequestSessionTurnRecoveryInput = {
|
|
|
41315
41060
|
attemptId: string;
|
|
41316
41061
|
reason: string;
|
|
41317
41062
|
detail?: Record<string, unknown>;
|
|
41063
|
+
providerRecoveryCount?: number;
|
|
41318
41064
|
fromStatuses?: SessionTurnStatus[];
|
|
41319
41065
|
providerArtifactInvalidation?: {
|
|
41320
41066
|
codexCredentialId: string;
|
|
@@ -41368,6 +41114,25 @@ export async function requestSessionTurnRecovery(
|
|
|
41368
41114
|
{ workspaceControl: locks.control ?? undefined },
|
|
41369
41115
|
);
|
|
41370
41116
|
const turnStatus = (turn?.status as SessionTurnStatus | undefined) ?? null;
|
|
41117
|
+
const [pendingInterruption] = attempt
|
|
41118
|
+
? await tx
|
|
41119
|
+
.select({ id: schema.sessionAttemptInterruptions.id })
|
|
41120
|
+
.from(schema.sessionAttemptInterruptions)
|
|
41121
|
+
.where(
|
|
41122
|
+
and(
|
|
41123
|
+
eq(schema.sessionAttemptInterruptions.workspaceId, workspaceId),
|
|
41124
|
+
eq(schema.sessionAttemptInterruptions.sessionId, input.sessionId),
|
|
41125
|
+
eq(schema.sessionAttemptInterruptions.attemptId, input.attemptId),
|
|
41126
|
+
inArray(schema.sessionAttemptInterruptions.state, [
|
|
41127
|
+
"pending",
|
|
41128
|
+
"delivered",
|
|
41129
|
+
"acknowledged",
|
|
41130
|
+
]),
|
|
41131
|
+
),
|
|
41132
|
+
)
|
|
41133
|
+
.limit(1)
|
|
41134
|
+
.for("update")
|
|
41135
|
+
: [];
|
|
41371
41136
|
if (
|
|
41372
41137
|
!locks.workspace ||
|
|
41373
41138
|
!turn ||
|
|
@@ -41381,7 +41146,8 @@ export async function requestSessionTurnRecovery(
|
|
|
41381
41146
|
effectiveControl.state !== "active" ||
|
|
41382
41147
|
session.activeTurnId !== input.turnId ||
|
|
41383
41148
|
!fromStatuses.includes(turnStatus as SessionTurnStatus) ||
|
|
41384
|
-
turn.activeAttemptId !== input.attemptId
|
|
41149
|
+
turn.activeAttemptId !== input.attemptId ||
|
|
41150
|
+
pendingInterruption !== undefined
|
|
41385
41151
|
) {
|
|
41386
41152
|
return {
|
|
41387
41153
|
action: "stale" as const,
|
|
@@ -41392,6 +41158,12 @@ export async function requestSessionTurnRecovery(
|
|
|
41392
41158
|
}
|
|
41393
41159
|
|
|
41394
41160
|
const now = new Date();
|
|
41161
|
+
if (
|
|
41162
|
+
input.providerRecoveryCount !== undefined &&
|
|
41163
|
+
(!Number.isSafeInteger(input.providerRecoveryCount) || input.providerRecoveryCount <= 0)
|
|
41164
|
+
) {
|
|
41165
|
+
throw new Error("providerRecoveryCount must be a positive safe integer");
|
|
41166
|
+
}
|
|
41395
41167
|
let providerArtifactsInvalidated = 0;
|
|
41396
41168
|
if (input.providerArtifactInvalidation) {
|
|
41397
41169
|
const invalidatedHistory = await tx
|
|
@@ -41533,7 +41305,12 @@ export async function requestSessionTurnRecovery(
|
|
|
41533
41305
|
cancelledBy: null,
|
|
41534
41306
|
cancelReason: null,
|
|
41535
41307
|
version: turn.version + 1,
|
|
41536
|
-
metadata:
|
|
41308
|
+
metadata: {
|
|
41309
|
+
...metadataWithoutTurnDispatchAttempt(turn.metadata),
|
|
41310
|
+
...(input.providerRecoveryCount !== undefined
|
|
41311
|
+
? { providerRecoveryCount: input.providerRecoveryCount }
|
|
41312
|
+
: {}),
|
|
41313
|
+
},
|
|
41537
41314
|
updatedAt: now,
|
|
41538
41315
|
})
|
|
41539
41316
|
.where(
|
|
@@ -42142,12 +41919,25 @@ export async function getSessionQueueSnapshot(
|
|
|
42142
41919
|
asc(schema.sessionSystemUpdates.createdAt),
|
|
42143
41920
|
asc(schema.sessionSystemUpdates.id),
|
|
42144
41921
|
);
|
|
41922
|
+
const items = rows.map(mapSessionTurn);
|
|
42145
41923
|
const latestInterruption = await latestSessionAttemptInterruption(
|
|
42146
41924
|
scopedDb,
|
|
42147
41925
|
workspaceId,
|
|
42148
41926
|
sessionId,
|
|
42149
41927
|
);
|
|
42150
|
-
const
|
|
41928
|
+
const queuedSteerPredecessorIds = items
|
|
41929
|
+
.map((turn) => queuedSteerReplacementAttemptId(turn.metadata))
|
|
41930
|
+
.filter((attemptId): attemptId is string => attemptId !== null);
|
|
41931
|
+
const stoppingPreviousAttempt =
|
|
41932
|
+
(latestInterruption !== null &&
|
|
41933
|
+
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
41934
|
+
latestInterruption.quiescedAt === null) ||
|
|
41935
|
+
(await queuedSteerHasUnquiescedPredecessor(
|
|
41936
|
+
scopedDb,
|
|
41937
|
+
workspaceId,
|
|
41938
|
+
sessionId,
|
|
41939
|
+
queuedSteerPredecessorIds,
|
|
41940
|
+
));
|
|
42151
41941
|
const nextInputBatch = selectBoundedSystemUpdateBatch(pendingInputs);
|
|
42152
41942
|
const hasPendingAgentSteer = pendingInputs.some(
|
|
42153
41943
|
(update) => update.kind === "agent_steer_instruction",
|
|
@@ -42159,10 +41949,7 @@ export async function getSessionQueueSnapshot(
|
|
|
42159
41949
|
version: session.queueVersion,
|
|
42160
41950
|
effectiveControl: serializeEffectiveSessionControl(effectiveControl),
|
|
42161
41951
|
activePersonalConnections,
|
|
42162
|
-
stoppingPreviousAttempt
|
|
42163
|
-
latestInterruption !== null &&
|
|
42164
|
-
latestInterruption.interruptionState !== "rejected_stale" &&
|
|
42165
|
-
latestInterruption.quiescedAt === null,
|
|
41952
|
+
stoppingPreviousAttempt,
|
|
42166
41953
|
items,
|
|
42167
41954
|
pendingInputs: pendingInputs.map((update) => {
|
|
42168
41955
|
const canonical = mapSessionSystemUpdate(update);
|
|
@@ -42201,7 +41988,9 @@ function queuedSteerReplacementAttemptId(metadata: Record<string, unknown>): str
|
|
|
42201
41988
|
) {
|
|
42202
41989
|
return attemptId;
|
|
42203
41990
|
}
|
|
42204
|
-
|
|
41991
|
+
// The interruption ledger is authoritative. Historical or malformed display
|
|
41992
|
+
// metadata must not make the read-only queue projection fail.
|
|
41993
|
+
return null;
|
|
42205
41994
|
}
|
|
42206
41995
|
|
|
42207
41996
|
async function enqueueFailedChildOutboxForTurnTx(
|
|
@@ -42418,6 +42207,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42418
42207
|
temporalWorkflowId: string;
|
|
42419
42208
|
reason: string;
|
|
42420
42209
|
notBefore?: Date;
|
|
42210
|
+
controlRequested?: boolean;
|
|
42421
42211
|
},
|
|
42422
42212
|
): Promise<number> {
|
|
42423
42213
|
const now = new Date();
|
|
@@ -42429,6 +42219,7 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42429
42219
|
workspaceId: input.workspaceId,
|
|
42430
42220
|
sessionId: input.sessionId,
|
|
42431
42221
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
42222
|
+
controlRevision: input.controlRequested ? 1 : 0,
|
|
42432
42223
|
reason: input.reason,
|
|
42433
42224
|
nextAttemptAt,
|
|
42434
42225
|
})
|
|
@@ -42437,6 +42228,9 @@ export async function enqueueSessionWorkflowWakeInTransaction(
|
|
|
42437
42228
|
set: {
|
|
42438
42229
|
temporalWorkflowId: input.temporalWorkflowId,
|
|
42439
42230
|
wakeRevision: sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`,
|
|
42231
|
+
controlRevision: input.controlRequested
|
|
42232
|
+
? sql`${schema.sessionWorkflowWakeOutbox.wakeRevision} + 1`
|
|
42233
|
+
: sql`${schema.sessionWorkflowWakeOutbox.controlRevision}`,
|
|
42440
42234
|
reason: input.reason,
|
|
42441
42235
|
attempts: 0,
|
|
42442
42236
|
// Coalescing a delayed retry must never postpone an already-due wake
|
|
@@ -42462,6 +42256,7 @@ export async function enqueueSessionWorkflowWake(
|
|
|
42462
42256
|
temporalWorkflowId: string;
|
|
42463
42257
|
reason: string;
|
|
42464
42258
|
notBefore?: Date;
|
|
42259
|
+
controlRequested?: boolean;
|
|
42465
42260
|
},
|
|
42466
42261
|
): Promise<number> {
|
|
42467
42262
|
return await withRlsContext(
|
|
@@ -42485,6 +42280,7 @@ export async function enqueueSessionWorkflowWakeIfRunnable(
|
|
|
42485
42280
|
temporalWorkflowId: string;
|
|
42486
42281
|
reason: string;
|
|
42487
42282
|
notBefore?: Date;
|
|
42283
|
+
controlRequested?: boolean;
|
|
42488
42284
|
},
|
|
42489
42285
|
): Promise<number | null> {
|
|
42490
42286
|
return await withRlsContext(
|
|
@@ -44686,6 +44482,7 @@ function mapSocialConnection(
|
|
|
44686
44482
|
accountHandle: row.accountHandle,
|
|
44687
44483
|
accountName: row.accountName,
|
|
44688
44484
|
externalAccountId: row.externalAccountId,
|
|
44485
|
+
ownership: row.subjectId ? "personal" : "workspace",
|
|
44689
44486
|
status: row.status as SocialConnectionStatus,
|
|
44690
44487
|
scopes: row.scopes,
|
|
44691
44488
|
credentialRef: row.credentialRef,
|
|
@@ -44696,6 +44493,25 @@ function mapSocialConnection(
|
|
|
44696
44493
|
};
|
|
44697
44494
|
}
|
|
44698
44495
|
|
|
44496
|
+
function socialConnectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
44497
|
+
return subjectId
|
|
44498
|
+
? or(
|
|
44499
|
+
isNull(schema.socialConnections.subjectId),
|
|
44500
|
+
eq(schema.socialConnections.subjectId, subjectId),
|
|
44501
|
+
)!
|
|
44502
|
+
: isNull(schema.socialConnections.subjectId);
|
|
44503
|
+
}
|
|
44504
|
+
async function withSocialConnectionSubjectRls<T>(
|
|
44505
|
+
db: Database,
|
|
44506
|
+
workspaceId: string,
|
|
44507
|
+
subjectId: string | null | undefined,
|
|
44508
|
+
fn: (db: Database) => Promise<T>,
|
|
44509
|
+
): Promise<T> {
|
|
44510
|
+
return subjectId
|
|
44511
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
|
|
44512
|
+
: await withWorkspaceRls(db, workspaceId, fn);
|
|
44513
|
+
}
|
|
44514
|
+
|
|
44699
44515
|
function mapApiKey(row: typeof schema.apiKeys.$inferSelect): ApiKey {
|
|
44700
44516
|
return {
|
|
44701
44517
|
id: row.id,
|
|
@@ -44904,11 +44720,118 @@ function shortHash(value: string): string {
|
|
|
44904
44720
|
return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 7);
|
|
44905
44721
|
}
|
|
44906
44722
|
|
|
44907
|
-
//
|
|
44908
|
-
//
|
|
44909
|
-
//
|
|
44910
|
-
|
|
44911
|
-
|
|
44912
|
-
|
|
44913
|
-
|
|
44723
|
+
// The resolver modules are cycle-free orchestration leaves. The root barrel is
|
|
44724
|
+
// the composition point that supplies their existing persistence accessors while
|
|
44725
|
+
// retaining the historical public builder/fetcher signatures.
|
|
44726
|
+
function codexAuthDeps(): CodexAuthDeps {
|
|
44727
|
+
return {
|
|
44728
|
+
loadCredential: loadCodexCredentialForRun,
|
|
44729
|
+
recordRefresh: recordCodexTokenRefresh,
|
|
44730
|
+
setStatus: setCodexCredentialStatus,
|
|
44731
|
+
refresh: refreshCodexToken,
|
|
44732
|
+
encrypt: encryptEnvironmentValue,
|
|
44733
|
+
keyBytes: environmentsEncryptionKeyBytes,
|
|
44734
|
+
withRefreshLock: withCodexCredentialRefreshLock,
|
|
44735
|
+
recordUsage: recordCodexAccountUsage,
|
|
44736
|
+
};
|
|
44737
|
+
}
|
|
44738
|
+
|
|
44739
|
+
function connectionBrokerDeps(): ConnectionBrokerDeps {
|
|
44740
|
+
return {
|
|
44741
|
+
loadCredential: loadConnectionCredentialForBroker,
|
|
44742
|
+
recordRefresh: recordConnectionTokenRefresh,
|
|
44743
|
+
setStatus: setConnectionStatus,
|
|
44744
|
+
recordUsed: recordConnectionUsed,
|
|
44745
|
+
refresh: refreshOAuthConnectionCredentialCore,
|
|
44746
|
+
encrypt: encryptEnvironmentValue,
|
|
44747
|
+
keyBytes: environmentsEncryptionKeyBytes,
|
|
44748
|
+
now: () => new Date(),
|
|
44749
|
+
};
|
|
44750
|
+
}
|
|
44751
|
+
|
|
44752
|
+
export function buildCodexTokenResolver(
|
|
44753
|
+
db: Database,
|
|
44754
|
+
settings: Settings,
|
|
44755
|
+
workspaceId: string,
|
|
44756
|
+
credentialId: string,
|
|
44757
|
+
deps: CodexAuthDeps = codexAuthDeps(),
|
|
44758
|
+
): ReturnType<typeof buildCodexTokenResolverCore> {
|
|
44759
|
+
return buildCodexTokenResolverCore(db, settings, workspaceId, credentialId, deps);
|
|
44760
|
+
}
|
|
44761
|
+
|
|
44762
|
+
export async function fetchCodexUsageForAccount(
|
|
44763
|
+
db: Database,
|
|
44764
|
+
settings: Settings,
|
|
44765
|
+
workspaceId: string,
|
|
44766
|
+
credentialId: string,
|
|
44767
|
+
fetchImpl: CodexFetch = fetch,
|
|
44768
|
+
): ReturnType<typeof fetchCodexUsageForAccountCore> {
|
|
44769
|
+
return await fetchCodexUsageForAccountCore(
|
|
44770
|
+
db,
|
|
44771
|
+
settings,
|
|
44772
|
+
workspaceId,
|
|
44773
|
+
credentialId,
|
|
44774
|
+
codexAuthDeps(),
|
|
44775
|
+
fetchImpl,
|
|
44776
|
+
);
|
|
44777
|
+
}
|
|
44778
|
+
|
|
44779
|
+
export async function fetchCodexRateLimitResetCreditsForAccount(
|
|
44780
|
+
db: Database,
|
|
44781
|
+
settings: Settings,
|
|
44782
|
+
workspaceId: string,
|
|
44783
|
+
credentialId: string,
|
|
44784
|
+
fetchImpl: CodexFetch = fetch,
|
|
44785
|
+
): ReturnType<typeof fetchCodexRateLimitResetCreditsForAccountCore> {
|
|
44786
|
+
return await fetchCodexRateLimitResetCreditsForAccountCore(
|
|
44787
|
+
db,
|
|
44788
|
+
settings,
|
|
44789
|
+
workspaceId,
|
|
44790
|
+
credentialId,
|
|
44791
|
+
codexAuthDeps(),
|
|
44792
|
+
fetchImpl,
|
|
44793
|
+
);
|
|
44794
|
+
}
|
|
44795
|
+
|
|
44796
|
+
export function buildConnectionTokenResolver(
|
|
44797
|
+
db: Database,
|
|
44798
|
+
settings: Settings,
|
|
44799
|
+
deps: ConnectionBrokerDeps = connectionBrokerDeps(),
|
|
44800
|
+
options: ConnectionTokenResolverOptions = {},
|
|
44801
|
+
): ReturnType<typeof buildConnectionTokenResolverCore> {
|
|
44802
|
+
return buildConnectionTokenResolverCore(db, settings, deps, options);
|
|
44803
|
+
}
|
|
44804
|
+
|
|
44805
|
+
export {
|
|
44806
|
+
withCodexTokenDeadline,
|
|
44807
|
+
type CodexAccountUsageSnapshot,
|
|
44808
|
+
type CodexAuthDeps,
|
|
44809
|
+
type CodexCredentialForRun,
|
|
44810
|
+
type CodexCredentialTokens,
|
|
44811
|
+
type CodexRateLimitResetCreditsAccountResult,
|
|
44812
|
+
type CodexTokenDeadlineClock,
|
|
44813
|
+
type CodexTokenDeadlineOptions,
|
|
44814
|
+
} from "./codex-token-resolver";
|
|
44815
|
+
|
|
44816
|
+
export {
|
|
44817
|
+
buildHostConnectionTokenResolver,
|
|
44818
|
+
ConnectionRefreshHttpError,
|
|
44819
|
+
HostMcpCredentialBindingError,
|
|
44820
|
+
HostMcpCredentialScopeError,
|
|
44821
|
+
isPrivateAddress,
|
|
44822
|
+
normalizeBearerScheme,
|
|
44823
|
+
refreshOAuthConnectionCredential,
|
|
44824
|
+
type ConnectionBrokerDeps,
|
|
44825
|
+
type ConnectionCredentialForBroker,
|
|
44826
|
+
type ConnectionCredentialLookupInput,
|
|
44827
|
+
type ConnectionStatusGuard,
|
|
44828
|
+
type ConnectionTokenRefreshInput,
|
|
44829
|
+
type ConnectionTokenResolverOptions,
|
|
44830
|
+
type HostMcpCredentialResolverContext,
|
|
44831
|
+
type PermanentConnectionRefreshFailure,
|
|
44832
|
+
type RefreshTransportOptions,
|
|
44833
|
+
type ResolveConnectionCredentialInput,
|
|
44834
|
+
type ResolveConnectionCredentialResult,
|
|
44835
|
+
} from "./connection-token-resolver";
|
|
44836
|
+
|
|
44914
44837
|
export * from "./workspace-artifacts";
|