@opengeni/db 0.27.8 → 0.27.11
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-M6EOXYHK.js → chunk-JYQPJ6Y5.js} +59 -50
- package/dist/chunk-JYQPJ6Y5.js.map +1 -0
- package/dist/{chunk-IHPCI4GV.js → chunk-RPHPNVWW.js} +3 -1
- package/dist/chunk-RPHPNVWW.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 +83 -184
- package/dist/index.js +2983 -2691
- 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 +3 -3
- package/dist/schema.d.ts +155 -96
- package/dist/schema.js +3 -1
- package/dist/scoped-knowledge.d.ts +1 -1
- package/dist/session-control.d.ts +1 -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/0171_social_connection_subject_ownership.sql +53 -0
- package/drizzle/0172_retire_model_visible_github_token.sql +86 -0
- package/drizzle/0173_codex_auth_boundaries.sql +107 -0
- package/drizzle/0174_session_wake_live_interruption.sql +80 -0
- package/package.json +4 -4
- 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 +747 -645
- 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 +3 -1
- package/src/schema.ts +70 -50
- package/src/scoped-knowledge.ts +2 -2
- package/src/session-control.ts +1 -1
- package/src/session-queue-commands.ts +1 -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-IHPCI4GV.js.map +0 -1
- package/dist/chunk-M6EOXYHK.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
|
|
@@ -11790,11 +11473,10 @@ export async function upsertCodexSubscriptionCredential(
|
|
|
11790
11473
|
// it when still null) so a re-connect never clobbers a rename.
|
|
11791
11474
|
accountEmail: input.accountEmail ?? null,
|
|
11792
11475
|
label: sql`coalesce(${schema.codexSubscriptionCredentials.label}, ${input.label ?? null})`,
|
|
11793
|
-
//
|
|
11794
|
-
//
|
|
11795
|
-
//
|
|
11796
|
-
|
|
11797
|
-
connectedBySubjectId: input.connectedBySubjectId ?? null,
|
|
11476
|
+
// Reconnect refreshes credential material, never ownership. A row
|
|
11477
|
+
// without an owner may be claimed by its first direct managed human;
|
|
11478
|
+
// after that, disconnect is the explicit ownership-reset boundary.
|
|
11479
|
+
connectedBySubjectId: sql`coalesce(${schema.codexSubscriptionCredentials.connectedBySubjectId}, ${input.connectedBySubjectId ?? null})`,
|
|
11798
11480
|
status: "active",
|
|
11799
11481
|
lastError: null,
|
|
11800
11482
|
version: sql`${schema.codexSubscriptionCredentials.version} + 1`,
|
|
@@ -11820,6 +11502,333 @@ export async function upsertCodexSubscriptionCredential(
|
|
|
11820
11502
|
);
|
|
11821
11503
|
}
|
|
11822
11504
|
|
|
11505
|
+
export type CodexAppsSettings = {
|
|
11506
|
+
credentialId: string | null;
|
|
11507
|
+
version: number;
|
|
11508
|
+
designatedAt: Date | null;
|
|
11509
|
+
};
|
|
11510
|
+
|
|
11511
|
+
export async function getCodexAppsSettings(
|
|
11512
|
+
db: Database,
|
|
11513
|
+
workspaceId: string,
|
|
11514
|
+
): Promise<CodexAppsSettings> {
|
|
11515
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
11516
|
+
const [row] = await scopedDb
|
|
11517
|
+
.select({
|
|
11518
|
+
credentialId: schema.codexAppsSettings.credentialId,
|
|
11519
|
+
version: schema.codexAppsSettings.version,
|
|
11520
|
+
designatedAt: schema.codexAppsSettings.designatedAt,
|
|
11521
|
+
})
|
|
11522
|
+
.from(schema.codexAppsSettings)
|
|
11523
|
+
.where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
|
|
11524
|
+
.limit(1);
|
|
11525
|
+
return row ?? { credentialId: null, version: 0, designatedAt: null };
|
|
11526
|
+
});
|
|
11527
|
+
}
|
|
11528
|
+
|
|
11529
|
+
export type CodexAppsCredentialAuthorization = {
|
|
11530
|
+
credentialId: string;
|
|
11531
|
+
ownerSubjectId: string;
|
|
11532
|
+
};
|
|
11533
|
+
|
|
11534
|
+
function canManageCodexApps(permissions: unknown): boolean {
|
|
11535
|
+
return (
|
|
11536
|
+
Array.isArray(permissions) &&
|
|
11537
|
+
(permissions.includes("connections:write") || permissions.includes("workspace:admin"))
|
|
11538
|
+
);
|
|
11539
|
+
}
|
|
11540
|
+
|
|
11541
|
+
export class CodexAppsAuthorizationRevokedError extends Error {
|
|
11542
|
+
constructor() {
|
|
11543
|
+
super("Codex Apps authorization is no longer active");
|
|
11544
|
+
this.name = "CodexAppsAuthorizationRevokedError";
|
|
11545
|
+
}
|
|
11546
|
+
}
|
|
11547
|
+
|
|
11548
|
+
/** Exact Apps credential and owner, independent of every inference-capacity field. */
|
|
11549
|
+
export async function getCodexAppsCredentialAuthorizationForRun(
|
|
11550
|
+
db: Database,
|
|
11551
|
+
workspaceId: string,
|
|
11552
|
+
): Promise<CodexAppsCredentialAuthorization | null> {
|
|
11553
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
11554
|
+
const [row] = await scopedDb
|
|
11555
|
+
.select({
|
|
11556
|
+
credentialId: schema.codexAppsSettings.credentialId,
|
|
11557
|
+
ownerSubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
|
|
11558
|
+
})
|
|
11559
|
+
.from(schema.codexAppsSettings)
|
|
11560
|
+
.innerJoin(
|
|
11561
|
+
schema.codexSubscriptionCredentials,
|
|
11562
|
+
and(
|
|
11563
|
+
eq(schema.codexSubscriptionCredentials.id, schema.codexAppsSettings.credentialId),
|
|
11564
|
+
eq(schema.codexSubscriptionCredentials.accountId, schema.codexAppsSettings.accountId),
|
|
11565
|
+
eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId),
|
|
11566
|
+
eq(schema.codexSubscriptionCredentials.status, "active"),
|
|
11567
|
+
),
|
|
11568
|
+
)
|
|
11569
|
+
.where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
|
|
11570
|
+
.limit(1);
|
|
11571
|
+
return row?.credentialId && row.ownerSubjectId
|
|
11572
|
+
? { credentialId: row.credentialId, ownerSubjectId: row.ownerSubjectId }
|
|
11573
|
+
: null;
|
|
11574
|
+
});
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
/**
|
|
11578
|
+
* Hold the exact designation, credential, and owner-membership authorization
|
|
11579
|
+
* stable through one outbound Apps request. Clear/disconnect operations share
|
|
11580
|
+
* the workspace advisory lock; credential or membership revocation must wait on
|
|
11581
|
+
* the row locks. The callback deliberately runs inside this short transaction so
|
|
11582
|
+
* no revocation can commit in the gap between a final check and network dispatch.
|
|
11583
|
+
*/
|
|
11584
|
+
export async function withCodexAppsRequestAuthorization<T>(
|
|
11585
|
+
db: Database,
|
|
11586
|
+
input: { workspaceId: string; credentialId: string },
|
|
11587
|
+
use: () => Promise<T>,
|
|
11588
|
+
): Promise<T> {
|
|
11589
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
11590
|
+
await scopedDb.execute(
|
|
11591
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
|
|
11592
|
+
);
|
|
11593
|
+
const [designation] = await scopedDb
|
|
11594
|
+
.select({ credentialId: schema.codexAppsSettings.credentialId })
|
|
11595
|
+
.from(schema.codexAppsSettings)
|
|
11596
|
+
.where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
|
|
11597
|
+
.for("share")
|
|
11598
|
+
.limit(1);
|
|
11599
|
+
if (designation?.credentialId !== input.credentialId) {
|
|
11600
|
+
throw new CodexAppsAuthorizationRevokedError();
|
|
11601
|
+
}
|
|
11602
|
+
const [credential] = await scopedDb
|
|
11603
|
+
.select({
|
|
11604
|
+
ownerSubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
|
|
11605
|
+
status: schema.codexSubscriptionCredentials.status,
|
|
11606
|
+
})
|
|
11607
|
+
.from(schema.codexSubscriptionCredentials)
|
|
11608
|
+
.where(
|
|
11609
|
+
and(
|
|
11610
|
+
eq(schema.codexSubscriptionCredentials.workspaceId, input.workspaceId),
|
|
11611
|
+
eq(schema.codexSubscriptionCredentials.id, input.credentialId),
|
|
11612
|
+
),
|
|
11613
|
+
)
|
|
11614
|
+
.for("share")
|
|
11615
|
+
.limit(1);
|
|
11616
|
+
if (!credential?.ownerSubjectId || credential.status !== "active") {
|
|
11617
|
+
throw new CodexAppsAuthorizationRevokedError();
|
|
11618
|
+
}
|
|
11619
|
+
const [membership] = await scopedDb
|
|
11620
|
+
.select({ permissions: schema.workspaceMemberships.permissions })
|
|
11621
|
+
.from(schema.workspaceMemberships)
|
|
11622
|
+
.where(
|
|
11623
|
+
and(
|
|
11624
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
11625
|
+
eq(schema.workspaceMemberships.subjectId, credential.ownerSubjectId),
|
|
11626
|
+
),
|
|
11627
|
+
)
|
|
11628
|
+
.for("share")
|
|
11629
|
+
.limit(1);
|
|
11630
|
+
if (!canManageCodexApps(membership?.permissions)) {
|
|
11631
|
+
throw new CodexAppsAuthorizationRevokedError();
|
|
11632
|
+
}
|
|
11633
|
+
return await use();
|
|
11634
|
+
});
|
|
11635
|
+
}
|
|
11636
|
+
|
|
11637
|
+
export type DesignateCodexAppsCredentialResult =
|
|
11638
|
+
| ({ kind: "updated" } & CodexAppsSettings)
|
|
11639
|
+
| ({ kind: "conflict" | "already_designated" } & CodexAppsSettings)
|
|
11640
|
+
| { kind: "not_found" }
|
|
11641
|
+
| { kind: "not_owner" }
|
|
11642
|
+
| { kind: "forbidden" }
|
|
11643
|
+
| { kind: "unavailable" };
|
|
11644
|
+
|
|
11645
|
+
export async function designateCodexAppsCredential(
|
|
11646
|
+
db: Database,
|
|
11647
|
+
input: {
|
|
11648
|
+
accountId: string;
|
|
11649
|
+
workspaceId: string;
|
|
11650
|
+
credentialId: string;
|
|
11651
|
+
subjectId: string;
|
|
11652
|
+
expectedVersion: number;
|
|
11653
|
+
},
|
|
11654
|
+
): Promise<DesignateCodexAppsCredentialResult> {
|
|
11655
|
+
return await withRlsContext(
|
|
11656
|
+
db,
|
|
11657
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
11658
|
+
async (scopedDb) => {
|
|
11659
|
+
await scopedDb.execute(
|
|
11660
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
|
|
11661
|
+
);
|
|
11662
|
+
const [currentRow] = await scopedDb
|
|
11663
|
+
.select()
|
|
11664
|
+
.from(schema.codexAppsSettings)
|
|
11665
|
+
.where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
|
|
11666
|
+
.for("update")
|
|
11667
|
+
.limit(1);
|
|
11668
|
+
const current: CodexAppsSettings = currentRow
|
|
11669
|
+
? {
|
|
11670
|
+
credentialId: currentRow.credentialId,
|
|
11671
|
+
version: currentRow.version,
|
|
11672
|
+
designatedAt: currentRow.designatedAt,
|
|
11673
|
+
}
|
|
11674
|
+
: { credentialId: null, version: 0, designatedAt: null };
|
|
11675
|
+
if (current.version !== input.expectedVersion) return { kind: "conflict", ...current };
|
|
11676
|
+
if (current.credentialId !== null) return { kind: "already_designated", ...current };
|
|
11677
|
+
|
|
11678
|
+
const [credential] = await scopedDb
|
|
11679
|
+
.select({
|
|
11680
|
+
connectedBySubjectId: schema.codexSubscriptionCredentials.connectedBySubjectId,
|
|
11681
|
+
status: schema.codexSubscriptionCredentials.status,
|
|
11682
|
+
})
|
|
11683
|
+
.from(schema.codexSubscriptionCredentials)
|
|
11684
|
+
.where(
|
|
11685
|
+
and(
|
|
11686
|
+
eq(schema.codexSubscriptionCredentials.accountId, input.accountId),
|
|
11687
|
+
eq(schema.codexSubscriptionCredentials.workspaceId, input.workspaceId),
|
|
11688
|
+
eq(schema.codexSubscriptionCredentials.id, input.credentialId),
|
|
11689
|
+
),
|
|
11690
|
+
)
|
|
11691
|
+
.for("update")
|
|
11692
|
+
.limit(1);
|
|
11693
|
+
if (!credential) return { kind: "not_found" };
|
|
11694
|
+
if (credential.connectedBySubjectId !== input.subjectId) return { kind: "not_owner" };
|
|
11695
|
+
if (credential.status !== "active") return { kind: "unavailable" };
|
|
11696
|
+
|
|
11697
|
+
const [membership] = await scopedDb
|
|
11698
|
+
.select({ permissions: schema.workspaceMemberships.permissions })
|
|
11699
|
+
.from(schema.workspaceMemberships)
|
|
11700
|
+
.where(
|
|
11701
|
+
and(
|
|
11702
|
+
eq(schema.workspaceMemberships.accountId, input.accountId),
|
|
11703
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
11704
|
+
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
11705
|
+
),
|
|
11706
|
+
)
|
|
11707
|
+
.for("update")
|
|
11708
|
+
.limit(1);
|
|
11709
|
+
if (!canManageCodexApps(membership?.permissions)) return { kind: "forbidden" };
|
|
11710
|
+
|
|
11711
|
+
const now = new Date();
|
|
11712
|
+
const version = current.version + 1;
|
|
11713
|
+
const [updated] = await scopedDb
|
|
11714
|
+
.insert(schema.codexAppsSettings)
|
|
11715
|
+
.values({
|
|
11716
|
+
accountId: input.accountId,
|
|
11717
|
+
workspaceId: input.workspaceId,
|
|
11718
|
+
credentialId: input.credentialId,
|
|
11719
|
+
version,
|
|
11720
|
+
designatedAt: now,
|
|
11721
|
+
updatedAt: now,
|
|
11722
|
+
})
|
|
11723
|
+
.onConflictDoUpdate({
|
|
11724
|
+
target: schema.codexAppsSettings.workspaceId,
|
|
11725
|
+
set: {
|
|
11726
|
+
credentialId: input.credentialId,
|
|
11727
|
+
version,
|
|
11728
|
+
designatedAt: now,
|
|
11729
|
+
updatedAt: now,
|
|
11730
|
+
},
|
|
11731
|
+
})
|
|
11732
|
+
.returning({
|
|
11733
|
+
credentialId: schema.codexAppsSettings.credentialId,
|
|
11734
|
+
version: schema.codexAppsSettings.version,
|
|
11735
|
+
designatedAt: schema.codexAppsSettings.designatedAt,
|
|
11736
|
+
});
|
|
11737
|
+
if (!updated?.credentialId || !updated.designatedAt) {
|
|
11738
|
+
throw new Error("Codex Apps designation was not persisted");
|
|
11739
|
+
}
|
|
11740
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
11741
|
+
accountId: input.accountId,
|
|
11742
|
+
workspaceId: input.workspaceId,
|
|
11743
|
+
subjectId: input.subjectId,
|
|
11744
|
+
action: "codex_apps.designated",
|
|
11745
|
+
targetType: "codex_subscription_credential",
|
|
11746
|
+
targetId: input.credentialId,
|
|
11747
|
+
metadata: { version },
|
|
11748
|
+
});
|
|
11749
|
+
return { kind: "updated", ...updated };
|
|
11750
|
+
},
|
|
11751
|
+
);
|
|
11752
|
+
}
|
|
11753
|
+
|
|
11754
|
+
export type ClearCodexAppsCredentialResult = {
|
|
11755
|
+
kind: "updated" | "unchanged" | "conflict" | "forbidden";
|
|
11756
|
+
} & CodexAppsSettings;
|
|
11757
|
+
|
|
11758
|
+
export async function clearCodexAppsCredential(
|
|
11759
|
+
db: Database,
|
|
11760
|
+
input: {
|
|
11761
|
+
accountId: string;
|
|
11762
|
+
workspaceId: string;
|
|
11763
|
+
subjectId: string;
|
|
11764
|
+
expectedVersion: number;
|
|
11765
|
+
},
|
|
11766
|
+
): Promise<ClearCodexAppsCredentialResult> {
|
|
11767
|
+
return await withRlsContext(
|
|
11768
|
+
db,
|
|
11769
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
11770
|
+
async (scopedDb) => {
|
|
11771
|
+
await scopedDb.execute(
|
|
11772
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${input.workspaceId}`}, 0))`,
|
|
11773
|
+
);
|
|
11774
|
+
const [row] = await scopedDb
|
|
11775
|
+
.select()
|
|
11776
|
+
.from(schema.codexAppsSettings)
|
|
11777
|
+
.where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
|
|
11778
|
+
.for("update")
|
|
11779
|
+
.limit(1);
|
|
11780
|
+
const current: CodexAppsSettings = row
|
|
11781
|
+
? { credentialId: row.credentialId, version: row.version, designatedAt: row.designatedAt }
|
|
11782
|
+
: { credentialId: null, version: 0, designatedAt: null };
|
|
11783
|
+
const [membership] = await scopedDb
|
|
11784
|
+
.select({ permissions: schema.workspaceMemberships.permissions })
|
|
11785
|
+
.from(schema.workspaceMemberships)
|
|
11786
|
+
.where(
|
|
11787
|
+
and(
|
|
11788
|
+
eq(schema.workspaceMemberships.accountId, input.accountId),
|
|
11789
|
+
eq(schema.workspaceMemberships.workspaceId, input.workspaceId),
|
|
11790
|
+
eq(schema.workspaceMemberships.subjectId, input.subjectId),
|
|
11791
|
+
),
|
|
11792
|
+
)
|
|
11793
|
+
.for("update")
|
|
11794
|
+
.limit(1);
|
|
11795
|
+
if (!canManageCodexApps(membership?.permissions)) {
|
|
11796
|
+
return { kind: "forbidden", ...current };
|
|
11797
|
+
}
|
|
11798
|
+
if (current.version !== input.expectedVersion) return { kind: "conflict", ...current };
|
|
11799
|
+
if (current.credentialId === null) return { kind: "unchanged", ...current };
|
|
11800
|
+
|
|
11801
|
+
const now = new Date();
|
|
11802
|
+
const version = current.version + 1;
|
|
11803
|
+
const [updated] = await scopedDb
|
|
11804
|
+
.update(schema.codexAppsSettings)
|
|
11805
|
+
.set({
|
|
11806
|
+
credentialId: null,
|
|
11807
|
+
version,
|
|
11808
|
+
designatedAt: null,
|
|
11809
|
+
updatedAt: now,
|
|
11810
|
+
})
|
|
11811
|
+
.where(eq(schema.codexAppsSettings.workspaceId, input.workspaceId))
|
|
11812
|
+
.returning({
|
|
11813
|
+
credentialId: schema.codexAppsSettings.credentialId,
|
|
11814
|
+
version: schema.codexAppsSettings.version,
|
|
11815
|
+
designatedAt: schema.codexAppsSettings.designatedAt,
|
|
11816
|
+
});
|
|
11817
|
+
if (!updated) throw new Error("Codex Apps designation clear was not persisted");
|
|
11818
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
11819
|
+
accountId: input.accountId,
|
|
11820
|
+
workspaceId: input.workspaceId,
|
|
11821
|
+
subjectId: input.subjectId,
|
|
11822
|
+
action: "codex_apps.cleared",
|
|
11823
|
+
targetType: "codex_subscription_credential",
|
|
11824
|
+
targetId: current.credentialId,
|
|
11825
|
+
metadata: { version },
|
|
11826
|
+
});
|
|
11827
|
+
return { kind: "updated", ...updated };
|
|
11828
|
+
},
|
|
11829
|
+
);
|
|
11830
|
+
}
|
|
11831
|
+
|
|
11823
11832
|
/**
|
|
11824
11833
|
* The ONLY decrypt-read accessor. Fails closed. Never call from an API route that
|
|
11825
11834
|
* returns the result.
|
|
@@ -12306,11 +12315,6 @@ export type CodexAccountStatus = {
|
|
|
12306
12315
|
// P3 rotation cooldown: when set and in the future, this account is cooling-down
|
|
12307
12316
|
// (rotated-off after a usage cap) and the engine skips it. null ⇒ not cooling.
|
|
12308
12317
|
exhaustedUntil: Date | null;
|
|
12309
|
-
// P4 connector-aware rotation: the ORIGINAL-dotted connector namespaces this
|
|
12310
|
-
// account exposes via codex_apps (github/gmail/linear/…). null ⇒ never probed
|
|
12311
|
-
// (the ranker treats it as unknown: never credited as covering, never excluded).
|
|
12312
|
-
connectorNamespaces: string[] | null;
|
|
12313
|
-
connectorsCheckedAt: Date | null;
|
|
12314
12318
|
};
|
|
12315
12319
|
|
|
12316
12320
|
/**
|
|
@@ -12429,8 +12433,6 @@ type CodexLeaseCandidateRow = {
|
|
|
12429
12433
|
secondary_reset_at: Date | string | null;
|
|
12430
12434
|
usage_checked_at: Date | string | null;
|
|
12431
12435
|
exhausted_until: Date | string | null;
|
|
12432
|
-
connector_namespaces: string[] | null;
|
|
12433
|
-
connectors_checked_at: Date | string | null;
|
|
12434
12436
|
selection_count: number;
|
|
12435
12437
|
last_selected_at: Date | string | null;
|
|
12436
12438
|
active_lease_count: number;
|
|
@@ -12463,8 +12465,6 @@ function mapCodexLeaseCandidate(
|
|
|
12463
12465
|
secondaryResetAt: codexMetadataDate(row.secondary_reset_at),
|
|
12464
12466
|
usageCheckedAt: codexMetadataDate(row.usage_checked_at),
|
|
12465
12467
|
exhaustedUntil: codexMetadataDate(row.exhausted_until),
|
|
12466
|
-
connectorNamespaces: row.connector_namespaces,
|
|
12467
|
-
connectorsCheckedAt: codexMetadataDate(row.connectors_checked_at),
|
|
12468
12468
|
selectionCount: Number(row.selection_count),
|
|
12469
12469
|
lastSelectedAt: codexMetadataDate(row.last_selected_at),
|
|
12470
12470
|
activeLeaseCount: Number(row.active_lease_count),
|
|
@@ -12524,8 +12524,6 @@ async function listCodexLeaseCandidatesInTransaction(
|
|
|
12524
12524
|
c.secondary_reset_at,
|
|
12525
12525
|
c.usage_checked_at,
|
|
12526
12526
|
c.exhausted_until,
|
|
12527
|
-
c.connector_namespaces,
|
|
12528
|
-
c.connectors_checked_at,
|
|
12529
12527
|
c.selection_count,
|
|
12530
12528
|
c.last_selected_at,
|
|
12531
12529
|
count(l.id) filter (
|
|
@@ -12572,8 +12570,6 @@ export async function acquireCodexCredentialLease<
|
|
|
12572
12570
|
holderId: string;
|
|
12573
12571
|
/** Pins must not move the workspace-global cursor. */
|
|
12574
12572
|
advanceActivePointer: boolean;
|
|
12575
|
-
/** Exact frozen credential for this same durable turn, if it is resuming. */
|
|
12576
|
-
continuationCredentialId?: string | null;
|
|
12577
12573
|
/**
|
|
12578
12574
|
* Optional downstream parser for private accepted-turn policy metadata.
|
|
12579
12575
|
* It is pure, runs under the turn/rotation transaction, and must not query
|
|
@@ -12582,8 +12578,8 @@ export async function acquireCodexCredentialLease<
|
|
|
12582
12578
|
resolvePolicyScope?: CodexCredentialLeasePolicyScopeResolver<TPolicyScope>;
|
|
12583
12579
|
/**
|
|
12584
12580
|
* Optional downstream membership policy for NEW allocations only. A live
|
|
12585
|
-
* lease
|
|
12586
|
-
*
|
|
12581
|
+
* exact-turn lease is offered to the selector against the complete workspace
|
|
12582
|
+
* rows first and can never be filtered out here.
|
|
12587
12583
|
*/
|
|
12588
12584
|
filterNewAllocationCandidates?: CodexCredentialLeaseCandidateFilter<
|
|
12589
12585
|
TPolicyScope,
|
|
@@ -12649,23 +12645,6 @@ export async function acquireCodexCredentialLease<
|
|
|
12649
12645
|
throw new Error(`Session turn not found for Codex lease: ${input.turnId}`);
|
|
12650
12646
|
}
|
|
12651
12647
|
const policyScope = input.resolvePolicyScope?.(turns[0].metadata ?? {}) ?? null;
|
|
12652
|
-
const continuationRows = input.continuationCredentialId
|
|
12653
|
-
? await tx.execute(
|
|
12654
|
-
sql<{ frozen_codex_credential_id: string | null }>`
|
|
12655
|
-
select frozen_codex_credential_id
|
|
12656
|
-
from agent_run_states
|
|
12657
|
-
where account_id = ${input.accountId}
|
|
12658
|
-
and workspace_id = ${input.workspaceId}
|
|
12659
|
-
and turn_id = ${input.turnId}
|
|
12660
|
-
order by state_version desc
|
|
12661
|
-
limit 1
|
|
12662
|
-
`,
|
|
12663
|
-
)
|
|
12664
|
-
: [];
|
|
12665
|
-
const validatedContinuationCredentialId =
|
|
12666
|
-
continuationRows[0]?.frozen_codex_credential_id === input.continuationCredentialId
|
|
12667
|
-
? input.continuationCredentialId
|
|
12668
|
-
: null;
|
|
12669
12648
|
const activeCredentialId = settingsRow.active_credential_id;
|
|
12670
12649
|
const rotationEnabled = settingsRow.rotation_enabled;
|
|
12671
12650
|
// Fail closed on a torn/manual legacy write. The user-intent bit and the
|
|
@@ -12707,7 +12686,7 @@ export async function acquireCodexCredentialLease<
|
|
|
12707
12686
|
activeCredentialId,
|
|
12708
12687
|
excludeTurnId: input.turnId,
|
|
12709
12688
|
});
|
|
12710
|
-
const sameTurnCredentialId = existingCredentialId
|
|
12689
|
+
const sameTurnCredentialId = existingCredentialId;
|
|
12711
12690
|
const selectionContext = (
|
|
12712
12691
|
accounts: CodexLeaseAccountStatus[],
|
|
12713
12692
|
unavailableDiagnostics: readonly TUnavailableDiagnostic[],
|
|
@@ -12731,10 +12710,9 @@ export async function acquireCodexCredentialLease<
|
|
|
12731
12710
|
}
|
|
12732
12711
|
}
|
|
12733
12712
|
|
|
12734
|
-
//
|
|
12713
|
+
// A live exact-turn lease is resolved before any future pool membership
|
|
12735
12714
|
// filter. The normal selector still owns health validation: a quarantined
|
|
12736
|
-
//
|
|
12737
|
-
// being reused blindly.
|
|
12715
|
+
// row falls through to scoped new acquisition rather than being reused.
|
|
12738
12716
|
if (!selected) {
|
|
12739
12717
|
const filtered = filterCodexLeaseCandidatesForPolicy(
|
|
12740
12718
|
allAccounts,
|
|
@@ -12771,11 +12749,7 @@ export async function acquireCodexCredentialLease<
|
|
|
12771
12749
|
if (!selectedAccount) {
|
|
12772
12750
|
throw new Error("Codex lease selector returned a credential outside the workspace pool");
|
|
12773
12751
|
}
|
|
12774
|
-
if (
|
|
12775
|
-
!selectedAccount.allocatorEnabled &&
|
|
12776
|
-
selectedAccount.id !== existingCredentialId &&
|
|
12777
|
-
selectedAccount.id !== validatedContinuationCredentialId
|
|
12778
|
-
) {
|
|
12752
|
+
if (!selectedAccount.allocatorEnabled && selectedAccount.id !== existingCredentialId) {
|
|
12779
12753
|
throw new Error("Codex lease selector returned a credential disabled for new allocations");
|
|
12780
12754
|
}
|
|
12781
12755
|
|
|
@@ -14190,9 +14164,6 @@ export async function listCodexAccountStatuses(
|
|
|
14190
14164
|
secondaryResetAt: schema.codexSubscriptionCredentials.secondaryResetAt,
|
|
14191
14165
|
usageCheckedAt: schema.codexSubscriptionCredentials.usageCheckedAt,
|
|
14192
14166
|
exhaustedUntil: schema.codexSubscriptionCredentials.exhaustedUntil,
|
|
14193
|
-
// P4 connector-set cache — metadata-only, rides along on this read.
|
|
14194
|
-
connectorNamespaces: schema.codexSubscriptionCredentials.connectorNamespaces,
|
|
14195
|
-
connectorsCheckedAt: schema.codexSubscriptionCredentials.connectorsCheckedAt,
|
|
14196
14167
|
})
|
|
14197
14168
|
.from(schema.codexSubscriptionCredentials)
|
|
14198
14169
|
.where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
|
|
@@ -14210,7 +14181,6 @@ export async function listCodexAccountStatuses(
|
|
|
14210
14181
|
secondaryResetAt: codexMetadataDate(row.secondaryResetAt),
|
|
14211
14182
|
usageCheckedAt: codexMetadataDate(row.usageCheckedAt),
|
|
14212
14183
|
exhaustedUntil: codexMetadataDate(row.exhaustedUntil),
|
|
14213
|
-
connectorsCheckedAt: codexMetadataDate(row.connectorsCheckedAt),
|
|
14214
14184
|
isActive: row.id === activeId,
|
|
14215
14185
|
}));
|
|
14216
14186
|
});
|
|
@@ -15111,16 +15081,6 @@ export async function completeCodexResetRedemption(
|
|
|
15111
15081
|
}
|
|
15112
15082
|
|
|
15113
15083
|
/** 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
15084
|
|
|
15125
15085
|
/**
|
|
15126
15086
|
* Cache-write for P2 quota bars: persist the five plaintext usage columns on a
|
|
@@ -15483,46 +15443,6 @@ export async function countConsecutiveReactiveRotations(
|
|
|
15483
15443
|
});
|
|
15484
15444
|
}
|
|
15485
15445
|
|
|
15486
|
-
/**
|
|
15487
|
-
* P4 connector-set cache writer: persist the set of ORIGINAL-dotted connector
|
|
15488
|
-
* namespaces a SPECIFIC credential exposes via codex_apps (+ the freshness clock).
|
|
15489
|
-
* Modeled byte-for-byte on recordCodexAccountUsage / setCodexCredentialExhausted:
|
|
15490
|
-
* RLS-scoped, guarded by (id, workspace_id), and — critically — NO `version` bump and
|
|
15491
|
-
* NO `updatedAt` touch, so it can never race the (id, version) token-refresh CAS.
|
|
15492
|
-
*
|
|
15493
|
-
* The CALLER must only invoke this with a NON-EMPTY set: codex_apps connects
|
|
15494
|
-
* best-effort (a transient failure yields an empty tools/list), and overwriting a
|
|
15495
|
-
* known non-empty set with [] would falsely "drop" coverage on a flaky turn. A
|
|
15496
|
-
* genuinely connector-less account stays null (the ranker treats null as unknown).
|
|
15497
|
-
* Returns true iff a row was updated (false ⇒ the credential was disconnected under us).
|
|
15498
|
-
*/
|
|
15499
|
-
export async function recordCodexAccountConnectors(
|
|
15500
|
-
db: Database,
|
|
15501
|
-
workspaceId: string,
|
|
15502
|
-
credentialId: string,
|
|
15503
|
-
namespaces: string[],
|
|
15504
|
-
): Promise<boolean> {
|
|
15505
|
-
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
15506
|
-
const updated = await scopedDb
|
|
15507
|
-
.update(schema.codexSubscriptionCredentials)
|
|
15508
|
-
.set({
|
|
15509
|
-
connectorNamespaces: namespaces,
|
|
15510
|
-
connectorsCheckedAt: new Date(),
|
|
15511
|
-
// NB: no `version` bump and no `updatedAt` touch — connector set is non-credential
|
|
15512
|
-
// metadata and must NOT race the (id, version) refresh CAS (same discipline as
|
|
15513
|
-
// recordCodexAccountUsage / setCodexCredentialExhausted).
|
|
15514
|
-
})
|
|
15515
|
-
.where(
|
|
15516
|
-
and(
|
|
15517
|
-
eq(schema.codexSubscriptionCredentials.id, credentialId),
|
|
15518
|
-
eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId),
|
|
15519
|
-
),
|
|
15520
|
-
)
|
|
15521
|
-
.returning({ id: schema.codexSubscriptionCredentials.id });
|
|
15522
|
-
return updated.length > 0;
|
|
15523
|
-
});
|
|
15524
|
-
}
|
|
15525
|
-
|
|
15526
15446
|
/** The supported rotation strategies (P3). */
|
|
15527
15447
|
export const CODEX_ROTATION_STRATEGIES = [
|
|
15528
15448
|
"most_remaining",
|
|
@@ -15733,6 +15653,7 @@ export async function disconnectCodexAccount(
|
|
|
15733
15653
|
db: Database,
|
|
15734
15654
|
workspaceId: string,
|
|
15735
15655
|
credentialId: string,
|
|
15656
|
+
actorSubjectId: string | null = null,
|
|
15736
15657
|
): Promise<{
|
|
15737
15658
|
removed: boolean;
|
|
15738
15659
|
newActiveCredentialId: string | null;
|
|
@@ -15744,8 +15665,20 @@ export async function disconnectCodexAccount(
|
|
|
15744
15665
|
where workspace_id = ${workspaceId}
|
|
15745
15666
|
for update
|
|
15746
15667
|
`);
|
|
15668
|
+
await scopedDb.execute(
|
|
15669
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${workspaceId}`}, 0))`,
|
|
15670
|
+
);
|
|
15671
|
+
const [appsSettings] = await scopedDb
|
|
15672
|
+
.select()
|
|
15673
|
+
.from(schema.codexAppsSettings)
|
|
15674
|
+
.where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
|
|
15675
|
+
.for("update")
|
|
15676
|
+
.limit(1);
|
|
15747
15677
|
const [credential] = await scopedDb
|
|
15748
|
-
.select({
|
|
15678
|
+
.select({
|
|
15679
|
+
id: schema.codexSubscriptionCredentials.id,
|
|
15680
|
+
accountId: schema.codexSubscriptionCredentials.accountId,
|
|
15681
|
+
})
|
|
15749
15682
|
.from(schema.codexSubscriptionCredentials)
|
|
15750
15683
|
.where(
|
|
15751
15684
|
and(
|
|
@@ -15787,6 +15720,27 @@ export async function disconnectCodexAccount(
|
|
|
15787
15720
|
blockedByUnresolvedRedemption: true,
|
|
15788
15721
|
};
|
|
15789
15722
|
}
|
|
15723
|
+
if (appsSettings?.credentialId === credentialId) {
|
|
15724
|
+
const version = appsSettings.version + 1;
|
|
15725
|
+
await scopedDb
|
|
15726
|
+
.update(schema.codexAppsSettings)
|
|
15727
|
+
.set({
|
|
15728
|
+
credentialId: null,
|
|
15729
|
+
version,
|
|
15730
|
+
designatedAt: null,
|
|
15731
|
+
updatedAt: new Date(),
|
|
15732
|
+
})
|
|
15733
|
+
.where(eq(schema.codexAppsSettings.id, appsSettings.id));
|
|
15734
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
15735
|
+
accountId: credential.accountId,
|
|
15736
|
+
workspaceId,
|
|
15737
|
+
subjectId: actorSubjectId,
|
|
15738
|
+
action: "codex_apps.cleared_on_disconnect",
|
|
15739
|
+
targetType: "codex_subscription_credential",
|
|
15740
|
+
targetId: credentialId,
|
|
15741
|
+
metadata: { version },
|
|
15742
|
+
});
|
|
15743
|
+
}
|
|
15790
15744
|
const removedRows = await scopedDb
|
|
15791
15745
|
.delete(schema.codexSubscriptionCredentials)
|
|
15792
15746
|
.where(
|
|
@@ -15839,10 +15793,23 @@ export async function disconnectCodexAccount(
|
|
|
15839
15793
|
export async function disconnectAllCodexAccounts(
|
|
15840
15794
|
db: Database,
|
|
15841
15795
|
workspaceId: string,
|
|
15796
|
+
actorSubjectId: string | null = null,
|
|
15842
15797
|
): Promise<{ removed: number; blockedCredentialIds: string[] }> {
|
|
15843
15798
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
15799
|
+
await scopedDb.execute(
|
|
15800
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`codex-apps-settings:${workspaceId}`}, 0))`,
|
|
15801
|
+
);
|
|
15802
|
+
const [appsSettings] = await scopedDb
|
|
15803
|
+
.select()
|
|
15804
|
+
.from(schema.codexAppsSettings)
|
|
15805
|
+
.where(eq(schema.codexAppsSettings.workspaceId, workspaceId))
|
|
15806
|
+
.for("update")
|
|
15807
|
+
.limit(1);
|
|
15844
15808
|
const credentials = await scopedDb
|
|
15845
|
-
.select({
|
|
15809
|
+
.select({
|
|
15810
|
+
id: schema.codexSubscriptionCredentials.id,
|
|
15811
|
+
accountId: schema.codexSubscriptionCredentials.accountId,
|
|
15812
|
+
})
|
|
15846
15813
|
.from(schema.codexSubscriptionCredentials)
|
|
15847
15814
|
.where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
|
|
15848
15815
|
.orderBy(asc(schema.codexSubscriptionCredentials.id))
|
|
@@ -15865,6 +15832,27 @@ export async function disconnectAllCodexAccounts(
|
|
|
15865
15832
|
blockedCredentialIds: blocked.map((row) => row.credentialId).sort(),
|
|
15866
15833
|
};
|
|
15867
15834
|
}
|
|
15835
|
+
if (appsSettings?.credentialId) {
|
|
15836
|
+
const version = appsSettings.version + 1;
|
|
15837
|
+
await scopedDb
|
|
15838
|
+
.update(schema.codexAppsSettings)
|
|
15839
|
+
.set({
|
|
15840
|
+
credentialId: null,
|
|
15841
|
+
version,
|
|
15842
|
+
designatedAt: null,
|
|
15843
|
+
updatedAt: new Date(),
|
|
15844
|
+
})
|
|
15845
|
+
.where(eq(schema.codexAppsSettings.id, appsSettings.id));
|
|
15846
|
+
await scopedDb.insert(schema.auditEvents).values({
|
|
15847
|
+
accountId: credentials[0]!.accountId,
|
|
15848
|
+
workspaceId,
|
|
15849
|
+
subjectId: actorSubjectId,
|
|
15850
|
+
action: "codex_apps.cleared_on_disconnect",
|
|
15851
|
+
targetType: "codex_subscription_credential",
|
|
15852
|
+
targetId: appsSettings.credentialId,
|
|
15853
|
+
metadata: { version },
|
|
15854
|
+
});
|
|
15855
|
+
}
|
|
15868
15856
|
const rows = await scopedDb
|
|
15869
15857
|
.delete(schema.codexSubscriptionCredentials)
|
|
15870
15858
|
.where(eq(schema.codexSubscriptionCredentials.workspaceId, workspaceId))
|
|
@@ -17444,6 +17432,18 @@ async function lockSessionCreateIdempotencyKey(
|
|
|
17444
17432
|
);
|
|
17445
17433
|
}
|
|
17446
17434
|
|
|
17435
|
+
async function lockAgentSessionCreate(tx: Database, input: SessionCreateInput): Promise<void> {
|
|
17436
|
+
const actorSessionId = input.createdByActor?.sessionId;
|
|
17437
|
+
if (!actorSessionId) return;
|
|
17438
|
+
// Agent-originated child creation first reads the parent under FOR SHARE,
|
|
17439
|
+
// then verifies the live attempt under FOR UPDATE. Serialize creates from
|
|
17440
|
+
// the same parent session so concurrent tool calls cannot deadlock while
|
|
17441
|
+
// upgrading that row lock. Unrelated sessions retain full concurrency.
|
|
17442
|
+
await tx.execute(
|
|
17443
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`agent-session-create:${input.workspaceId}:${actorSessionId}`}, 0))`,
|
|
17444
|
+
);
|
|
17445
|
+
}
|
|
17446
|
+
|
|
17447
17447
|
async function existingSpawnDenialForKey(
|
|
17448
17448
|
tx: Database,
|
|
17449
17449
|
workspaceId: string,
|
|
@@ -17549,6 +17549,8 @@ async function createSessionInTransaction(
|
|
|
17549
17549
|
}
|
|
17550
17550
|
}
|
|
17551
17551
|
|
|
17552
|
+
await lockAgentSessionCreate(tx, input);
|
|
17553
|
+
|
|
17552
17554
|
const decision = await resolveSessionDepthDecision(tx, input, id, workspace, deploymentPolicy);
|
|
17553
17555
|
if (decision.denied) {
|
|
17554
17556
|
return {
|
|
@@ -20577,11 +20579,6 @@ export async function getLatestRunState(
|
|
|
20577
20579
|
turnId: string | null;
|
|
20578
20580
|
serializedRunState: string;
|
|
20579
20581
|
pendingApprovals: unknown[];
|
|
20580
|
-
// The codex account that froze this state (pin > workspace-active), or null
|
|
20581
|
-
// when frozen on the non-codex path / before the column existed. The replay
|
|
20582
|
-
// path compares it to the resuming turn's codex account to decide whether the
|
|
20583
|
-
// blob's account-bound reasoning must be neutralized before being replayed.
|
|
20584
|
-
frozenCodexCredentialId: string | null;
|
|
20585
20582
|
providerArtifactInvalidatedAt: Date | null;
|
|
20586
20583
|
} | null> {
|
|
20587
20584
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
@@ -20602,7 +20599,6 @@ export async function getLatestRunState(
|
|
|
20602
20599
|
turnId: row.turnId ?? null,
|
|
20603
20600
|
serializedRunState: row.serializedRunState,
|
|
20604
20601
|
pendingApprovals: row.pendingApprovals,
|
|
20605
|
-
frozenCodexCredentialId: row.frozenCodexCredentialId ?? null,
|
|
20606
20602
|
providerArtifactInvalidatedAt: row.providerArtifactInvalidatedAt ?? null,
|
|
20607
20603
|
}
|
|
20608
20604
|
: null;
|
|
@@ -21359,10 +21355,6 @@ export async function appendSessionHistoryItems(
|
|
|
21359
21355
|
turnId: string;
|
|
21360
21356
|
expectedExecutionGeneration: number;
|
|
21361
21357
|
expectedAttemptId: string;
|
|
21362
|
-
// The codex account that produced these items (the turn's resolved credential
|
|
21363
|
-
// id), or null/undefined on the non-codex path. Stored verbatim so the read
|
|
21364
|
-
// path can strip cross-account reasoning.encrypted_content blobs per turn.
|
|
21365
|
-
producerCodexCredentialId?: string | null;
|
|
21366
21358
|
modelToolOutputTruncationTokens?: number;
|
|
21367
21359
|
items: Array<{ position: number; item: Record<string, unknown> }>;
|
|
21368
21360
|
},
|
|
@@ -21391,7 +21383,6 @@ export async function appendSessionHistoryItems(
|
|
|
21391
21383
|
workspaceId: input.workspaceId,
|
|
21392
21384
|
sessionId: input.sessionId,
|
|
21393
21385
|
turnId: input.turnId,
|
|
21394
|
-
producerCodexCredentialId: input.producerCodexCredentialId ?? null,
|
|
21395
21386
|
position: entry.position,
|
|
21396
21387
|
// This is the canonical model-memory boundary. The pending-call
|
|
21397
21388
|
// ledger and audit event may retain their separate raw/preview
|
|
@@ -21770,18 +21761,18 @@ export async function getActiveSessionHistoryItems(
|
|
|
21770
21761
|
sessionId: string,
|
|
21771
21762
|
): Promise<
|
|
21772
21763
|
Array<{
|
|
21764
|
+
id: string;
|
|
21773
21765
|
position: number;
|
|
21774
21766
|
item: Record<string, unknown>;
|
|
21775
|
-
producerCodexCredentialId: string | null;
|
|
21776
21767
|
providerArtifactInvalidatedAt: Date | null;
|
|
21777
21768
|
}>
|
|
21778
21769
|
> {
|
|
21779
21770
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
21780
21771
|
const rows = await scopedDb
|
|
21781
21772
|
.select({
|
|
21773
|
+
id: schema.sessionHistoryItems.id,
|
|
21782
21774
|
position: schema.sessionHistoryItems.position,
|
|
21783
21775
|
item: schema.sessionHistoryItems.item,
|
|
21784
|
-
producerCodexCredentialId: schema.sessionHistoryItems.producerCodexCredentialId,
|
|
21785
21776
|
providerArtifactInvalidatedAt: schema.sessionHistoryItems.providerArtifactInvalidatedAt,
|
|
21786
21777
|
})
|
|
21787
21778
|
.from(schema.sessionHistoryItems)
|
|
@@ -21979,8 +21970,6 @@ export async function applyContextCompaction(
|
|
|
21979
21970
|
replacementInputTokens: number;
|
|
21980
21971
|
clearRequestedCompaction?: boolean;
|
|
21981
21972
|
eventPayload?: Record<string, unknown>;
|
|
21982
|
-
/** Tag inserted history rows with the Codex credential that produced them. */
|
|
21983
|
-
producerCodexCredentialId?: string | null;
|
|
21984
21973
|
},
|
|
21985
21974
|
): Promise<ApplyContextCompactionResult> {
|
|
21986
21975
|
return await withRlsContext(
|
|
@@ -22030,9 +22019,6 @@ export async function applyContextCompaction(
|
|
|
22030
22019
|
position: supersededFrom + index,
|
|
22031
22020
|
item: sanitizeModelPayload(item),
|
|
22032
22021
|
active: true,
|
|
22033
|
-
...(input.producerCodexCredentialId
|
|
22034
|
-
? { producerCodexCredentialId: input.producerCodexCredentialId }
|
|
22035
|
-
: {}),
|
|
22036
22022
|
})),
|
|
22037
22023
|
);
|
|
22038
22024
|
}
|
|
@@ -22045,9 +22031,6 @@ export async function applyContextCompaction(
|
|
|
22045
22031
|
position: summaryPosition,
|
|
22046
22032
|
item: sanitizeModelPayload(input.summaryItem),
|
|
22047
22033
|
active: true,
|
|
22048
|
-
...(input.producerCodexCredentialId
|
|
22049
|
-
? { producerCodexCredentialId: input.producerCodexCredentialId }
|
|
22050
|
-
: {}),
|
|
22051
22034
|
});
|
|
22052
22035
|
const insertedEvents = input.eventPayload
|
|
22053
22036
|
? await tx
|
|
@@ -34743,11 +34726,6 @@ export async function saveRunState(
|
|
|
34743
34726
|
expectedAttemptId: string;
|
|
34744
34727
|
serializedRunState: string;
|
|
34745
34728
|
pendingApprovals: unknown[];
|
|
34746
|
-
// The codex account freezing this state (the turn's resolved credential id),
|
|
34747
|
-
// or null on a non-codex turn. Stamped so a resume on a DIFFERENT codex
|
|
34748
|
-
// account can strip the blob's account-bound reasoning. Defaults null so
|
|
34749
|
-
// every legacy caller (and the non-codex path) is byte-identical.
|
|
34750
|
-
frozenCodexCredentialId?: string | null;
|
|
34751
34729
|
},
|
|
34752
34730
|
): Promise<boolean> {
|
|
34753
34731
|
return await withRlsContext(
|
|
@@ -34782,7 +34760,6 @@ export async function saveRunState(
|
|
|
34782
34760
|
stateVersion: Number(maxVersion) + 1,
|
|
34783
34761
|
serializedRunState: input.serializedRunState,
|
|
34784
34762
|
pendingApprovals: input.pendingApprovals,
|
|
34785
|
-
frozenCodexCredentialId: input.frozenCodexCredentialId ?? null,
|
|
34786
34763
|
});
|
|
34787
34764
|
return true;
|
|
34788
34765
|
});
|
|
@@ -37492,7 +37469,6 @@ export async function claimSessionWorkForAttempt(
|
|
|
37492
37469
|
turnId,
|
|
37493
37470
|
position: Number(position),
|
|
37494
37471
|
item: sanitizeModelPayload(delivered.historyItem),
|
|
37495
|
-
producerCodexCredentialId: null,
|
|
37496
37472
|
});
|
|
37497
37473
|
};
|
|
37498
37474
|
|
|
@@ -38540,7 +38516,6 @@ export async function claimSessionWorkForAttempt(
|
|
|
38540
38516
|
row.prompt,
|
|
38541
38517
|
Array.isArray(row.resources) ? (row.resources as ResourceRef[]) : [],
|
|
38542
38518
|
),
|
|
38543
|
-
producerCodexCredentialId: null,
|
|
38544
38519
|
});
|
|
38545
38520
|
const providerDelegatedTurn = isSessionRealtimeDelegationTurnMetadata(row.metadata);
|
|
38546
38521
|
// Cross-session updates are already projected through
|
|
@@ -40176,7 +40151,6 @@ export type ApplySessionTurnSettlementInput = {
|
|
|
40176
40151
|
runState?: {
|
|
40177
40152
|
serializedRunState: string;
|
|
40178
40153
|
pendingApprovals: unknown[];
|
|
40179
|
-
frozenCodexCredentialId?: string | null;
|
|
40180
40154
|
humanInputRequests?: Array<{
|
|
40181
40155
|
id: string;
|
|
40182
40156
|
toolCallId: string;
|
|
@@ -40405,7 +40379,6 @@ export async function applySessionTurnSettlement(
|
|
|
40405
40379
|
stateVersion: Number(maxVersion) + 1,
|
|
40406
40380
|
serializedRunState: input.runState.serializedRunState,
|
|
40407
40381
|
pendingApprovals: input.runState.pendingApprovals,
|
|
40408
|
-
frozenCodexCredentialId: input.runState.frozenCodexCredentialId ?? null,
|
|
40409
40382
|
});
|
|
40410
40383
|
if (humanInputRequests.length > 0) {
|
|
40411
40384
|
for (const request of humanInputRequests) {
|
|
@@ -41390,7 +41363,8 @@ export type RequestSessionTurnRecoveryInput = {
|
|
|
41390
41363
|
providerRecoveryCount?: number;
|
|
41391
41364
|
fromStatuses?: SessionTurnStatus[];
|
|
41392
41365
|
providerArtifactInvalidation?: {
|
|
41393
|
-
|
|
41366
|
+
historyItemIds: string[];
|
|
41367
|
+
runStateId?: string;
|
|
41394
41368
|
reason: "encrypted_content_rejected";
|
|
41395
41369
|
};
|
|
41396
41370
|
};
|
|
@@ -41493,47 +41467,36 @@ export async function requestSessionTurnRecovery(
|
|
|
41493
41467
|
}
|
|
41494
41468
|
let providerArtifactsInvalidated = 0;
|
|
41495
41469
|
if (input.providerArtifactInvalidation) {
|
|
41496
|
-
const
|
|
41497
|
-
|
|
41498
|
-
.
|
|
41499
|
-
|
|
41500
|
-
|
|
41501
|
-
|
|
41502
|
-
|
|
41503
|
-
|
|
41504
|
-
|
|
41505
|
-
|
|
41506
|
-
|
|
41507
|
-
|
|
41508
|
-
|
|
41509
|
-
|
|
41510
|
-
|
|
41511
|
-
|
|
41512
|
-
|
|
41513
|
-
|
|
41514
|
-
|
|
41515
|
-
|
|
41516
|
-
|
|
41517
|
-
|
|
41518
|
-
|
|
41519
|
-
|
|
41520
|
-
|
|
41521
|
-
|
|
41522
|
-
|
|
41523
|
-
|
|
41524
|
-
|
|
41525
|
-
|
|
41526
|
-
eq(schema.agentRunStates.turnId, input.turnId),
|
|
41527
|
-
eq(
|
|
41528
|
-
schema.agentRunStates.frozenCodexCredentialId,
|
|
41529
|
-
input.providerArtifactInvalidation.codexCredentialId,
|
|
41530
|
-
),
|
|
41531
|
-
isNull(schema.agentRunStates.providerArtifactInvalidatedAt),
|
|
41532
|
-
),
|
|
41533
|
-
)
|
|
41534
|
-
.orderBy(desc(schema.agentRunStates.stateVersion))
|
|
41535
|
-
.limit(1);
|
|
41536
|
-
const invalidatedRunState = latestRunState
|
|
41470
|
+
const historyItemIds = [...new Set(input.providerArtifactInvalidation.historyItemIds)];
|
|
41471
|
+
const invalidatedHistory =
|
|
41472
|
+
historyItemIds.length > 0
|
|
41473
|
+
? await tx
|
|
41474
|
+
.update(schema.sessionHistoryItems)
|
|
41475
|
+
.set({
|
|
41476
|
+
providerArtifactInvalidatedAt: now,
|
|
41477
|
+
providerArtifactInvalidationReason: input.providerArtifactInvalidation.reason,
|
|
41478
|
+
providerArtifactInvalidatedByAttemptId: input.attemptId,
|
|
41479
|
+
})
|
|
41480
|
+
.where(
|
|
41481
|
+
and(
|
|
41482
|
+
eq(schema.sessionHistoryItems.accountId, session.accountId),
|
|
41483
|
+
eq(schema.sessionHistoryItems.workspaceId, workspaceId),
|
|
41484
|
+
eq(schema.sessionHistoryItems.sessionId, input.sessionId),
|
|
41485
|
+
eq(schema.sessionHistoryItems.active, true),
|
|
41486
|
+
inArray(schema.sessionHistoryItems.id, historyItemIds),
|
|
41487
|
+
isNull(schema.sessionHistoryItems.providerArtifactInvalidatedAt),
|
|
41488
|
+
sql`${schema.sessionHistoryItems.item} ->> 'type' in ('reasoning', 'compaction')`,
|
|
41489
|
+
sql`(
|
|
41490
|
+
nullif(${schema.sessionHistoryItems.item} ->> 'encrypted_content', '') is not null
|
|
41491
|
+
or nullif(${schema.sessionHistoryItems.item} ->> 'encryptedContent', '') is not null
|
|
41492
|
+
or nullif(${schema.sessionHistoryItems.item} -> 'providerData' ->> 'encrypted_content', '') is not null
|
|
41493
|
+
or nullif(${schema.sessionHistoryItems.item} -> 'providerData' ->> 'encryptedContent', '') is not null
|
|
41494
|
+
)`,
|
|
41495
|
+
),
|
|
41496
|
+
)
|
|
41497
|
+
.returning({ id: schema.sessionHistoryItems.id })
|
|
41498
|
+
: [];
|
|
41499
|
+
const invalidatedRunState = input.providerArtifactInvalidation.runStateId
|
|
41537
41500
|
? await tx
|
|
41538
41501
|
.update(schema.agentRunStates)
|
|
41539
41502
|
.set({
|
|
@@ -41543,8 +41506,20 @@ export async function requestSessionTurnRecovery(
|
|
|
41543
41506
|
})
|
|
41544
41507
|
.where(
|
|
41545
41508
|
and(
|
|
41546
|
-
eq(schema.agentRunStates.
|
|
41509
|
+
eq(schema.agentRunStates.accountId, session.accountId),
|
|
41510
|
+
eq(schema.agentRunStates.workspaceId, workspaceId),
|
|
41511
|
+
eq(schema.agentRunStates.sessionId, input.sessionId),
|
|
41512
|
+
eq(schema.agentRunStates.turnId, input.turnId),
|
|
41513
|
+
eq(schema.agentRunStates.id, input.providerArtifactInvalidation.runStateId),
|
|
41547
41514
|
isNull(schema.agentRunStates.providerArtifactInvalidatedAt),
|
|
41515
|
+
sql`${schema.agentRunStates.stateVersion} = (
|
|
41516
|
+
select max(latest.state_version)
|
|
41517
|
+
from agent_run_states latest
|
|
41518
|
+
where latest.account_id = ${session.accountId}
|
|
41519
|
+
and latest.workspace_id = ${workspaceId}
|
|
41520
|
+
and latest.session_id = ${input.sessionId}
|
|
41521
|
+
and latest.turn_id = ${input.turnId}
|
|
41522
|
+
)`,
|
|
41548
41523
|
),
|
|
41549
41524
|
)
|
|
41550
41525
|
.returning({ id: schema.agentRunStates.id })
|
|
@@ -44809,6 +44784,7 @@ function mapSocialConnection(
|
|
|
44809
44784
|
accountHandle: row.accountHandle,
|
|
44810
44785
|
accountName: row.accountName,
|
|
44811
44786
|
externalAccountId: row.externalAccountId,
|
|
44787
|
+
ownership: row.subjectId ? "personal" : "workspace",
|
|
44812
44788
|
status: row.status as SocialConnectionStatus,
|
|
44813
44789
|
scopes: row.scopes,
|
|
44814
44790
|
credentialRef: row.credentialRef,
|
|
@@ -44819,6 +44795,25 @@ function mapSocialConnection(
|
|
|
44819
44795
|
};
|
|
44820
44796
|
}
|
|
44821
44797
|
|
|
44798
|
+
function socialConnectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
44799
|
+
return subjectId
|
|
44800
|
+
? or(
|
|
44801
|
+
isNull(schema.socialConnections.subjectId),
|
|
44802
|
+
eq(schema.socialConnections.subjectId, subjectId),
|
|
44803
|
+
)!
|
|
44804
|
+
: isNull(schema.socialConnections.subjectId);
|
|
44805
|
+
}
|
|
44806
|
+
async function withSocialConnectionSubjectRls<T>(
|
|
44807
|
+
db: Database,
|
|
44808
|
+
workspaceId: string,
|
|
44809
|
+
subjectId: string | null | undefined,
|
|
44810
|
+
fn: (db: Database) => Promise<T>,
|
|
44811
|
+
): Promise<T> {
|
|
44812
|
+
return subjectId
|
|
44813
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
|
|
44814
|
+
: await withWorkspaceRls(db, workspaceId, fn);
|
|
44815
|
+
}
|
|
44816
|
+
|
|
44822
44817
|
function mapApiKey(row: typeof schema.apiKeys.$inferSelect): ApiKey {
|
|
44823
44818
|
return {
|
|
44824
44819
|
id: row.id,
|
|
@@ -45027,11 +45022,118 @@ function shortHash(value: string): string {
|
|
|
45027
45022
|
return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 7);
|
|
45028
45023
|
}
|
|
45029
45024
|
|
|
45030
|
-
//
|
|
45031
|
-
//
|
|
45032
|
-
//
|
|
45033
|
-
|
|
45034
|
-
|
|
45035
|
-
|
|
45036
|
-
|
|
45025
|
+
// The resolver modules are cycle-free orchestration leaves. The root barrel is
|
|
45026
|
+
// the composition point that supplies their existing persistence accessors while
|
|
45027
|
+
// retaining the historical public builder/fetcher signatures.
|
|
45028
|
+
function codexAuthDeps(): CodexAuthDeps {
|
|
45029
|
+
return {
|
|
45030
|
+
loadCredential: loadCodexCredentialForRun,
|
|
45031
|
+
recordRefresh: recordCodexTokenRefresh,
|
|
45032
|
+
setStatus: setCodexCredentialStatus,
|
|
45033
|
+
refresh: refreshCodexToken,
|
|
45034
|
+
encrypt: encryptEnvironmentValue,
|
|
45035
|
+
keyBytes: environmentsEncryptionKeyBytes,
|
|
45036
|
+
withRefreshLock: withCodexCredentialRefreshLock,
|
|
45037
|
+
recordUsage: recordCodexAccountUsage,
|
|
45038
|
+
};
|
|
45039
|
+
}
|
|
45040
|
+
|
|
45041
|
+
function connectionBrokerDeps(): ConnectionBrokerDeps {
|
|
45042
|
+
return {
|
|
45043
|
+
loadCredential: loadConnectionCredentialForBroker,
|
|
45044
|
+
recordRefresh: recordConnectionTokenRefresh,
|
|
45045
|
+
setStatus: setConnectionStatus,
|
|
45046
|
+
recordUsed: recordConnectionUsed,
|
|
45047
|
+
refresh: refreshOAuthConnectionCredentialCore,
|
|
45048
|
+
encrypt: encryptEnvironmentValue,
|
|
45049
|
+
keyBytes: environmentsEncryptionKeyBytes,
|
|
45050
|
+
now: () => new Date(),
|
|
45051
|
+
};
|
|
45052
|
+
}
|
|
45053
|
+
|
|
45054
|
+
export function buildCodexTokenResolver(
|
|
45055
|
+
db: Database,
|
|
45056
|
+
settings: Settings,
|
|
45057
|
+
workspaceId: string,
|
|
45058
|
+
credentialId: string,
|
|
45059
|
+
deps: CodexAuthDeps = codexAuthDeps(),
|
|
45060
|
+
): ReturnType<typeof buildCodexTokenResolverCore> {
|
|
45061
|
+
return buildCodexTokenResolverCore(db, settings, workspaceId, credentialId, deps);
|
|
45062
|
+
}
|
|
45063
|
+
|
|
45064
|
+
export async function fetchCodexUsageForAccount(
|
|
45065
|
+
db: Database,
|
|
45066
|
+
settings: Settings,
|
|
45067
|
+
workspaceId: string,
|
|
45068
|
+
credentialId: string,
|
|
45069
|
+
fetchImpl: CodexFetch = fetch,
|
|
45070
|
+
): ReturnType<typeof fetchCodexUsageForAccountCore> {
|
|
45071
|
+
return await fetchCodexUsageForAccountCore(
|
|
45072
|
+
db,
|
|
45073
|
+
settings,
|
|
45074
|
+
workspaceId,
|
|
45075
|
+
credentialId,
|
|
45076
|
+
codexAuthDeps(),
|
|
45077
|
+
fetchImpl,
|
|
45078
|
+
);
|
|
45079
|
+
}
|
|
45080
|
+
|
|
45081
|
+
export async function fetchCodexRateLimitResetCreditsForAccount(
|
|
45082
|
+
db: Database,
|
|
45083
|
+
settings: Settings,
|
|
45084
|
+
workspaceId: string,
|
|
45085
|
+
credentialId: string,
|
|
45086
|
+
fetchImpl: CodexFetch = fetch,
|
|
45087
|
+
): ReturnType<typeof fetchCodexRateLimitResetCreditsForAccountCore> {
|
|
45088
|
+
return await fetchCodexRateLimitResetCreditsForAccountCore(
|
|
45089
|
+
db,
|
|
45090
|
+
settings,
|
|
45091
|
+
workspaceId,
|
|
45092
|
+
credentialId,
|
|
45093
|
+
codexAuthDeps(),
|
|
45094
|
+
fetchImpl,
|
|
45095
|
+
);
|
|
45096
|
+
}
|
|
45097
|
+
|
|
45098
|
+
export function buildConnectionTokenResolver(
|
|
45099
|
+
db: Database,
|
|
45100
|
+
settings: Settings,
|
|
45101
|
+
deps: ConnectionBrokerDeps = connectionBrokerDeps(),
|
|
45102
|
+
options: ConnectionTokenResolverOptions = {},
|
|
45103
|
+
): ReturnType<typeof buildConnectionTokenResolverCore> {
|
|
45104
|
+
return buildConnectionTokenResolverCore(db, settings, deps, options);
|
|
45105
|
+
}
|
|
45106
|
+
|
|
45107
|
+
export {
|
|
45108
|
+
withCodexTokenDeadline,
|
|
45109
|
+
type CodexAccountUsageSnapshot,
|
|
45110
|
+
type CodexAuthDeps,
|
|
45111
|
+
type CodexCredentialForRun,
|
|
45112
|
+
type CodexCredentialTokens,
|
|
45113
|
+
type CodexRateLimitResetCreditsAccountResult,
|
|
45114
|
+
type CodexTokenDeadlineClock,
|
|
45115
|
+
type CodexTokenDeadlineOptions,
|
|
45116
|
+
} from "./codex-token-resolver";
|
|
45117
|
+
|
|
45118
|
+
export {
|
|
45119
|
+
buildHostConnectionTokenResolver,
|
|
45120
|
+
ConnectionRefreshHttpError,
|
|
45121
|
+
HostMcpCredentialBindingError,
|
|
45122
|
+
HostMcpCredentialScopeError,
|
|
45123
|
+
isPrivateAddress,
|
|
45124
|
+
normalizeBearerScheme,
|
|
45125
|
+
refreshOAuthConnectionCredential,
|
|
45126
|
+
type ConnectionBrokerDeps,
|
|
45127
|
+
type ConnectionCredentialForBroker,
|
|
45128
|
+
type ConnectionCredentialLookupInput,
|
|
45129
|
+
type ConnectionStatusGuard,
|
|
45130
|
+
type ConnectionTokenRefreshInput,
|
|
45131
|
+
type ConnectionTokenResolverOptions,
|
|
45132
|
+
type HostMcpCredentialResolverContext,
|
|
45133
|
+
type PermanentConnectionRefreshFailure,
|
|
45134
|
+
type RefreshTransportOptions,
|
|
45135
|
+
type ResolveConnectionCredentialInput,
|
|
45136
|
+
type ResolveConnectionCredentialResult,
|
|
45137
|
+
} from "./connection-token-resolver";
|
|
45138
|
+
|
|
45037
45139
|
export * from "./workspace-artifacts";
|