@opengeni/db 0.27.8 → 0.27.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/{chunk-IHPCI4GV.js → chunk-2JFRTKTG.js} +1 -1
  2. package/dist/chunk-2JFRTKTG.js.map +1 -0
  3. package/dist/{chunk-M6EOXYHK.js → chunk-NX2JNJJP.js} +8 -4
  4. package/dist/{chunk-M6EOXYHK.js.map → chunk-NX2JNJJP.js.map} +1 -1
  5. package/dist/codex-token-resolver.d.ts +50 -8
  6. package/dist/connection-token-resolver.d.ts +45 -7
  7. package/dist/database.d.ts +137 -0
  8. package/dist/index.d.ts +21 -154
  9. package/dist/index.js +2694 -2615
  10. package/dist/index.js.map +1 -1
  11. package/dist/insights.d.ts +1 -1
  12. package/dist/memory-governance.d.ts +1 -1
  13. package/dist/new-session-drafts.d.ts +1 -1
  14. package/dist/preference-registry.d.ts +1 -1
  15. package/dist/provision-roles.d.ts +1 -1
  16. package/dist/provision-roles.js +1 -1
  17. package/dist/runtime-posture.d.ts +1 -1
  18. package/dist/schema.d.ts +27 -0
  19. package/dist/schema.js +1 -1
  20. package/dist/scoped-knowledge.d.ts +1 -1
  21. package/dist/session-control.d.ts +1 -1
  22. package/dist/session-queue-commands.d.ts +1 -1
  23. package/dist/session-realtime-context.d.ts +1 -1
  24. package/dist/session-realtime-ledger.d.ts +1 -1
  25. package/dist/session-realtime-mirror.d.ts +1 -1
  26. package/dist/session-realtime-state.d.ts +1 -1
  27. package/dist/session-realtime-terminal.d.ts +1 -1
  28. package/dist/session-realtime.d.ts +1 -1
  29. package/dist/session-tool-call-settlement.d.ts +1 -1
  30. package/dist/turn-initiator.d.ts +1 -1
  31. package/dist/workspace-artifacts.d.ts +1 -1
  32. package/dist/workspace-instruction-policies.d.ts +1 -1
  33. package/drizzle/0171_social_connection_subject_ownership.sql +53 -0
  34. package/package.json +3 -3
  35. package/src/codex-token-resolver.ts +102 -49
  36. package/src/connection-token-resolver.ts +67 -26
  37. package/src/database.ts +393 -0
  38. package/src/index.ts +279 -479
  39. package/src/insights.ts +2 -2
  40. package/src/memory-governance.ts +2 -2
  41. package/src/new-session-drafts.ts +1 -1
  42. package/src/preference-registry.ts +2 -2
  43. package/src/provision-roles.ts +1 -1
  44. package/src/runtime-posture.ts +1 -1
  45. package/src/schema.ts +11 -3
  46. package/src/scoped-knowledge.ts +2 -2
  47. package/src/session-control.ts +1 -1
  48. package/src/session-queue-commands.ts +1 -1
  49. package/src/session-realtime-context.ts +1 -1
  50. package/src/session-realtime-ledger.ts +1 -1
  51. package/src/session-realtime-mirror.ts +1 -1
  52. package/src/session-realtime-state.ts +1 -1
  53. package/src/session-realtime-terminal.ts +1 -1
  54. package/src/session-realtime.ts +1 -1
  55. package/src/session-tool-call-settlement.ts +1 -1
  56. package/src/turn-initiator.ts +1 -1
  57. package/src/workspace-artifacts.ts +2 -2
  58. package/src/workspace-instruction-policies.ts +2 -2
  59. package/dist/chunk-IHPCI4GV.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import type { Database } from "./index";
1
+ import type { Database } from "./database";
2
2
  export type InsightsTimeWindow = {
3
3
  since: Date;
4
4
  until: Date;
@@ -1,4 +1,4 @@
1
- import type { Database } from "./index";
1
+ import type { Database } from "./database";
2
2
  import { type MemoryOperationPlanInput, type MemoryRevertPlanInput } from "./memory-domain";
3
3
  export declare class MemoryGovernanceAuthorityError extends Error {
4
4
  readonly name = "MemoryGovernanceAuthorityError";
@@ -1,5 +1,5 @@
1
1
  import type { NewSessionDraftOptions, LatencyMode, ReasoningEffort, ResourceRef, ToolRef } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import * as schema from "./schema";
4
4
  export type NewSessionDraftRow = typeof schema.newSessionDrafts.$inferSelect;
5
5
  export declare class NewSessionDraftConflictError extends Error {
@@ -1,5 +1,5 @@
1
1
  import { PreferenceRegistryDescriptor, type CreatePreferenceRegistryProposalRequest, type PreferenceRegistryConflictStrategy, type PreferenceRegistryScope, type PreferenceRegistryStatus } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  export declare class PreferenceRegistryNotFoundError extends Error {
4
4
  readonly name = "PreferenceRegistryNotFoundError";
5
5
  }
@@ -1,4 +1,4 @@
1
- import type { RlsStrategy } from "./index";
1
+ import type { RlsStrategy } from "./database";
2
2
  export type ProvisionResult = {
3
3
  appRole: string | null;
4
4
  hostExportRole: string | null;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  provisionRoles
3
- } from "./chunk-IHPCI4GV.js";
3
+ } from "./chunk-2JFRTKTG.js";
4
4
  import "./chunk-PZ5AY32C.js";
5
5
  export {
6
6
  provisionRoles
@@ -1,4 +1,4 @@
1
- import type { Database, RlsStrategy } from "./index";
1
+ import type { Database, RlsStrategy } from "./database";
2
2
  /**
3
3
  * The complete standalone tenant-table contract. Adding or removing a
4
4
  * FORCE-RLS table is an architectural change: update this list in the same
package/dist/schema.d.ts CHANGED
@@ -6930,6 +6930,7 @@ export declare const sessions: import("drizzle-orm/pg-core").PgTableWithColumns<
6930
6930
  ownerSubjectId: string;
6931
6931
  providerDomain: string;
6932
6932
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
6933
+ connectionType?: "mcp" | "social" | undefined;
6933
6934
  }[];
6934
6935
  driverParam: unknown;
6935
6936
  notNull: true;
@@ -6948,6 +6949,7 @@ export declare const sessions: import("drizzle-orm/pg-core").PgTableWithColumns<
6948
6949
  ownerSubjectId: string;
6949
6950
  providerDomain: string;
6950
6951
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
6952
+ connectionType?: "mcp" | "social" | undefined;
6951
6953
  }[];
6952
6954
  }>;
6953
6955
  toolPolicy: import("drizzle-orm/pg-core").PgColumn<{
@@ -11958,6 +11960,7 @@ export declare const sessionTurns: import("drizzle-orm/pg-core").PgTableWithColu
11958
11960
  ownerSubjectId: string;
11959
11961
  providerDomain: string;
11960
11962
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
11963
+ connectionType?: "mcp" | "social" | undefined;
11961
11964
  }[];
11962
11965
  driverParam: unknown;
11963
11966
  notNull: true;
@@ -11976,6 +11979,7 @@ export declare const sessionTurns: import("drizzle-orm/pg-core").PgTableWithColu
11976
11979
  ownerSubjectId: string;
11977
11980
  providerDomain: string;
11978
11981
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
11982
+ connectionType?: "mcp" | "social" | undefined;
11979
11983
  }[];
11980
11984
  }>;
11981
11985
  cancelledBy: import("drizzle-orm/pg-core").PgColumn<{
@@ -14751,6 +14755,7 @@ export declare const sessionSystemUpdates: import("drizzle-orm/pg-core").PgTable
14751
14755
  ownerSubjectId: string;
14752
14756
  providerDomain: string;
14753
14757
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
14758
+ connectionType?: "mcp" | "social" | undefined;
14754
14759
  }[];
14755
14760
  driverParam: unknown;
14756
14761
  notNull: true;
@@ -14769,6 +14774,7 @@ export declare const sessionSystemUpdates: import("drizzle-orm/pg-core").PgTable
14769
14774
  ownerSubjectId: string;
14770
14775
  providerDomain: string;
14771
14776
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
14777
+ connectionType?: "mcp" | "social" | undefined;
14772
14778
  }[];
14773
14779
  }>;
14774
14780
  state: import("drizzle-orm/pg-core").PgColumn<{
@@ -15088,6 +15094,7 @@ export declare const sessionSystemUpdateOutbox: import("drizzle-orm/pg-core").Pg
15088
15094
  ownerSubjectId: string;
15089
15095
  providerDomain: string;
15090
15096
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
15097
+ connectionType?: "mcp" | "social" | undefined;
15091
15098
  }[];
15092
15099
  driverParam: unknown;
15093
15100
  notNull: true;
@@ -15106,6 +15113,7 @@ export declare const sessionSystemUpdateOutbox: import("drizzle-orm/pg-core").Pg
15106
15113
  ownerSubjectId: string;
15107
15114
  providerDomain: string;
15108
15115
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
15116
+ connectionType?: "mcp" | "social" | undefined;
15109
15117
  }[];
15110
15118
  }>;
15111
15119
  status: import("drizzle-orm/pg-core").PgColumn<{
@@ -22720,6 +22728,7 @@ export declare const scheduledTasks: import("drizzle-orm/pg-core").PgTableWithCo
22720
22728
  ownerSubjectId: string;
22721
22729
  providerDomain: string;
22722
22730
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
22731
+ connectionType?: "mcp" | "social" | undefined;
22723
22732
  }[];
22724
22733
  driverParam: unknown;
22725
22734
  notNull: true;
@@ -22738,6 +22747,7 @@ export declare const scheduledTasks: import("drizzle-orm/pg-core").PgTableWithCo
22738
22747
  ownerSubjectId: string;
22739
22748
  providerDomain: string;
22740
22749
  kind?: "api_key" | "app_install" | "delegated" | "oauth2" | undefined;
22750
+ connectionType?: "mcp" | "social" | undefined;
22741
22751
  }[];
22742
22752
  }>;
22743
22753
  reusableSessionId: import("drizzle-orm/pg-core").PgColumn<{
@@ -27176,6 +27186,23 @@ export declare const socialConnections: import("drizzle-orm/pg-core").PgTableWit
27176
27186
  identity: undefined;
27177
27187
  generated: undefined;
27178
27188
  }, {}, {}>;
27189
+ subjectId: import("drizzle-orm/pg-core").PgColumn<{
27190
+ name: "subject_id";
27191
+ tableName: "social_connections";
27192
+ dataType: "string";
27193
+ columnType: "PgText";
27194
+ data: string;
27195
+ driverParam: string;
27196
+ notNull: false;
27197
+ hasDefault: false;
27198
+ isPrimaryKey: false;
27199
+ isAutoincrement: false;
27200
+ hasRuntimeDefault: false;
27201
+ enumValues: [string, ...string[]];
27202
+ baseColumn: never;
27203
+ identity: undefined;
27204
+ generated: undefined;
27205
+ }, {}, {}>;
27179
27206
  provider: import("drizzle-orm/pg-core").PgColumn<{
27180
27207
  name: "provider";
27181
27208
  tableName: "social_connections";
package/dist/schema.js CHANGED
@@ -139,7 +139,7 @@ import {
139
139
  workspaceVariableSetVariables,
140
140
  workspaceVariableSets,
141
141
  workspaces
142
- } from "./chunk-M6EOXYHK.js";
142
+ } from "./chunk-NX2JNJJP.js";
143
143
  import "./chunk-PZ5AY32C.js";
144
144
  export {
145
145
  agentRunStates,
@@ -1,5 +1,5 @@
1
1
  import type { EligibleKnowledgeClaim, KnowledgeChangeProposalRecord, KnowledgeClaimEvidencePolarity, KnowledgeClaimOrigin, KnowledgeClaimRecord, KnowledgeClaimRelationType, KnowledgeClaimReviewState, KnowledgeDocumentVersionRecord, KnowledgeFactObjectKind, KnowledgeFactRecord, KnowledgeLifecycleEventType, KnowledgeLifecycleState, KnowledgeProviderRecord, KnowledgeSourceAclVersionRecord, KnowledgeSourceObjectRecord, KnowledgeSourceRecord, KnowledgeSyncRunRecord, ScopedKnowledgeActor, ScopedKnowledgeScope } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  type KnowledgeWriteContext = {
4
4
  accountId: string;
5
5
  workspaceId: string;
@@ -1,5 +1,5 @@
1
1
  import { type SessionMcpApprovalPolicy, type TurnInitiatorContext } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import * as schema from "./schema";
4
4
  export declare const SESSION_ANCESTRY_LIMIT = 10000;
5
5
  export type WorkspaceControlLockMode = "none" | "share" | "update";
@@ -1,5 +1,5 @@
1
1
  import { ResourceRef, type McpPersonalConnectionDelegation, type LatencyMode, type ReasoningEffort, type TurnExecutionPolicyV1 } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import { type SessionCommandActor, type SessionCommandReceiptRow } from "./session-control";
4
4
  import * as schema from "./schema";
5
5
  export type QueueCommandConflictCode = "QUEUE_VERSION_CHANGED" | "QUEUE_PROMPT_STARTED" | "QUEUE_ANCHOR_CHANGED" | "PROMPT_CHANGED" | "DRAFT_CHANGED" | "DRAFT_NOT_EMPTY" | "EDIT_SOURCE_CHANGED";
@@ -1,4 +1,4 @@
1
- import type { Database } from "./index";
1
+ import type { Database } from "./database";
2
2
  export declare const SESSION_REALTIME_CONTEXT_MAX_BYTES = 65536;
3
3
  export declare const SESSION_REALTIME_TAIL_SOURCE = "transcript_tail_flush";
4
4
  export declare const SESSION_REALTIME_TAIL_INSTRUCTION = "The user just ended the realtime voice session but remains reachable by text. Ending voice changes only the communication mode; it does not stop, pause, or complete existing work. Treat the remaining transcript tail as additional context. If work was already underway, continue it from the current state. Change or stop that work only if the user explicitly requested it. If nothing was underway and the transcript contains no unhandled request, acknowledge briefly; otherwise handle any unhandled request.";
@@ -1,5 +1,5 @@
1
1
  import { type SessionRealtimeMode } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import { type AssertSessionRealtimeOwnerInput } from "./session-realtime";
4
4
  export declare const SESSION_REALTIME_LEDGER_MAX_BATCH = 64;
5
5
  export declare const SESSION_REALTIME_LEDGER_MAX_TEXT_BYTES = 131072;
@@ -1,5 +1,5 @@
1
1
  import type { HumanInputQuestion, HumanInputResponse } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import * as schema from "./schema";
4
4
  export type SessionRealtimeMirrorChannel = "speakable" | "commentary" | null;
5
5
  export type MirrorSessionRealtimeContextInput = {
@@ -1,2 +1,2 @@
1
- import type { Database } from "./index";
1
+ import type { Database } from "./database";
2
2
  export declare function sessionRealtimeIsActiveInTransaction(db: Database, workspaceId: string, sessionId: string, now?: Date): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import type { Database } from "./index";
1
+ import type { Database } from "./database";
2
2
  export type ProjectSessionRealtimeDelegationTerminalInput = {
3
3
  accountId: string;
4
4
  workspaceId: string;
@@ -1,5 +1,5 @@
1
1
  import type { SessionRealtimeEndReason, SessionRealtimeMode, SessionRealtimeModel } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import * as schema from "./schema";
4
4
  export { sessionRealtimeIsActiveInTransaction } from "./session-realtime-state";
5
5
  export declare const SESSION_REALTIME_LEASE_MS = 30000;
@@ -1,5 +1,5 @@
1
1
  import type { SessionEvent } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  export declare const TOOL_RESULT_TYPE_BY_CALL_TYPE: Readonly<Record<string, string>>;
4
4
  export declare function historyCallId(item: Record<string, unknown>): string | null;
5
5
  export declare function historyItemType(item: Record<string, unknown>): string | null;
@@ -1,5 +1,5 @@
1
1
  import { type TurnInitiator, type TurnInitiatorContext } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  import type { SessionCommandActor } from "./session-control";
4
4
  export declare const UNATTRIBUTED_LEGACY_INITIATOR: TurnInitiator;
5
5
  export type FrozenTurnInitiator = {
@@ -1,5 +1,5 @@
1
1
  import type { WorkspaceArtifact, WorkspaceArtifactDetailResponse, WorkspaceArtifactMutationResponse, WorkspaceArtifactVersion } from "@opengeni/contracts";
2
- import type { Database } from "./index";
2
+ import type { Database } from "./database";
3
3
  type ArtifactMutationToolName = "artifacts_create" | "artifacts_publish" | "artifacts_rollback";
4
4
  export declare class WorkspaceArtifactNotFoundError extends Error {
5
5
  readonly name = "WorkspaceArtifactNotFoundError";
@@ -1,6 +1,6 @@
1
1
  import { WorkspaceInstructionPolicySnapshot } from "@opengeni/contracts";
2
2
  import type { WorkspaceInstructionPolicyActivationResponse, WorkspaceInstructionPolicyDiffResponse, WorkspaceInstructionPolicyDraftProvenanceSource, WorkspaceInstructionPolicyHead, WorkspaceInstructionPolicyListQuery, WorkspaceInstructionPolicyListResponse, WorkspaceInstructionPolicyOnboardingProposal, WorkspaceInstructionPolicyOnboardingProposalListQuery, WorkspaceInstructionPolicyOnboardingProposalListResponse, WorkspaceInstructionPolicyProvenanceSource, WorkspaceInstructionPolicyRevision, WorkspaceInstructionPolicyTarget } from "@opengeni/contracts";
3
- import type { Database } from "./index";
3
+ import type { Database } from "./database";
4
4
  type DraftRequestInput = WorkspaceInstructionPolicyTarget & {
5
5
  operationId: string;
6
6
  accountId: string;
@@ -0,0 +1,53 @@
1
+ -- deployment-mode: rolling
2
+ -- Extend first-party social credentials with the same workspace/personal ownership boundary as MCP connections.
3
+
4
+ ALTER TABLE social_connections
5
+ ADD COLUMN subject_id text;
6
+
7
+ DROP INDEX social_connections_workspace_provider_handle_idx;
8
+ CREATE UNIQUE INDEX social_connections_workspace_provider_handle_idx
9
+ ON social_connections (workspace_id, provider, account_handle)
10
+ WHERE subject_id IS NULL;
11
+ CREATE UNIQUE INDEX social_connections_subject_provider_handle_idx
12
+ ON social_connections (workspace_id, subject_id, provider)
13
+ WHERE subject_id IS NOT NULL;
14
+ CREATE INDEX social_connections_subject_provider_status_idx
15
+ ON social_connections (workspace_id, subject_id, provider, status);
16
+
17
+ DROP POLICY workspace_isolation ON social_connections;
18
+ CREATE POLICY workspace_isolation ON social_connections
19
+ USING (
20
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
21
+ AND (
22
+ subject_id IS NULL
23
+ OR subject_id = nullif(current_setting('opengeni.subject_id', true), '')
24
+ )
25
+ )
26
+ WITH CHECK (
27
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
28
+ AND (
29
+ subject_id IS NULL
30
+ OR subject_id = nullif(current_setting('opengeni.subject_id', true), '')
31
+ )
32
+ );
33
+
34
+ DROP POLICY workspace_isolation ON social_posts;
35
+ CREATE POLICY workspace_isolation ON social_posts
36
+ USING (
37
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
38
+ AND EXISTS (
39
+ SELECT 1
40
+ FROM social_connections connection
41
+ WHERE connection.id = social_posts.connection_id
42
+ AND connection.workspace_id = social_posts.workspace_id
43
+ )
44
+ )
45
+ WITH CHECK (
46
+ opengeni_private.workspace_rls_visible(account_id, workspace_id)
47
+ AND EXISTS (
48
+ SELECT 1
49
+ FROM social_connections connection
50
+ WHERE connection.id = social_posts.connection_id
51
+ AND connection.workspace_id = social_posts.workspace_id
52
+ )
53
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.27.8",
3
+ "version": "0.27.9",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@opengeni/codex": "^0.2.10",
54
- "@opengeni/config": "^0.10.11",
55
- "@opengeni/contracts": "^0.38.1",
54
+ "@opengeni/config": "^0.10.12",
55
+ "@opengeni/contracts": "^0.38.2",
56
56
  "@opengeni/network": "^0.2.0",
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "postgres": "^3.4.7"
@@ -3,7 +3,7 @@
3
3
  // Hoisted here from apps/worker/src/activities/codex-auth.ts so BOTH the worker
4
4
  // (turn-time bearer for the streamed run) AND the api (the /wham/usage quota-bar
5
5
  // reads) drive ONE resolver — no duplicated refresh/CAS/single-flight logic. The
6
- // worker re-exports buildCodexTokenResolver from this module for back-compat, so
6
+ // worker re-exports buildCodexTokenResolver from @opengeni/db for back-compat, so
7
7
  // the agent-turn.ts call site is unchanged.
8
8
  //
9
9
  // Why @opengeni/db is the right home: the resolver only orchestrates accessors
@@ -35,15 +35,52 @@ import {
35
35
  refreshCodexToken,
36
36
  } from "@opengeni/codex";
37
37
  import { encryptEnvironmentValue } from "./environment-crypto";
38
- import {
39
- loadCodexCredentialForRun,
40
- recordCodexAccountUsage,
41
- recordCodexTokenRefresh,
42
- setCodexCredentialStatus,
43
- withCodexCredentialRefreshLock,
44
- type CodexCredentialForRun,
45
- type Database,
46
- } from "./index";
38
+ import type { Database } from "./database";
39
+
40
+ export type CodexCredentialTokens = {
41
+ accessToken: string;
42
+ refreshToken: string;
43
+ idToken: string;
44
+ };
45
+
46
+ export type CodexCredentialForRun = {
47
+ id: string;
48
+ version: number;
49
+ workspaceId: string;
50
+ tokens: CodexCredentialTokens;
51
+ chatgptAccountId: string | null;
52
+ scopes: string | null;
53
+ planType: string | null;
54
+ isFedramp: boolean;
55
+ expiresAt: Date | null;
56
+ lastRefreshAt: Date | null;
57
+ status: string;
58
+ lastError: string | null;
59
+ };
60
+
61
+ export type CodexAccountUsageSnapshot = {
62
+ primaryUsedPercent?: number | null;
63
+ primaryResetAt?: Date | null;
64
+ secondaryUsedPercent?: number | null;
65
+ secondaryResetAt?: Date | null;
66
+ checkedAt?: Date;
67
+ resetCreditAvailableCount?: number | null;
68
+ resetCreditsCheckedAt?: Date | null;
69
+ };
70
+
71
+ type CodexCredentialRefreshInput = {
72
+ id: string;
73
+ version: number;
74
+ workspaceId: string;
75
+ credentialEncrypted: string;
76
+ expiresAt: Date | null;
77
+ lastRefreshAt: Date;
78
+ };
79
+
80
+ type CodexCredentialStatusTarget = {
81
+ id: string;
82
+ version: number;
83
+ };
47
84
 
48
85
  // Single-flight per CREDENTIAL INSTANCE (row id + version), process-module scope.
49
86
  // Keying by the loaded credential's id+version — NOT by workspaceId alone (P1-b) —
@@ -142,25 +179,37 @@ export async function withCodexTokenDeadline<T>(
142
179
 
143
180
  // Dependencies are injectable so the lifecycle logic (single-flight, staleness,
144
181
  // needs_relogin transition) is unit-testable without a database. Production uses
145
- // the real db + codex functions via the default bag.
182
+ // the root composition wrapper supplies the real db + codex functions.
146
183
  export type CodexAuthDeps = {
147
- loadCredential: typeof loadCodexCredentialForRun;
148
- recordRefresh: typeof recordCodexTokenRefresh;
149
- setStatus: typeof setCodexCredentialStatus;
184
+ loadCredential: (
185
+ db: Database,
186
+ settings: Settings,
187
+ workspaceId: string,
188
+ credentialId: string,
189
+ ) => Promise<CodexCredentialForRun | null>;
190
+ recordRefresh: (db: Database, input: CodexCredentialRefreshInput) => Promise<boolean>;
191
+ setStatus: (
192
+ db: Database,
193
+ workspaceId: string,
194
+ status: "active" | "needs_relogin" | "error",
195
+ lastError: string | null,
196
+ target: CodexCredentialStatusTarget,
197
+ ) => Promise<boolean>;
150
198
  refresh: typeof refreshCodexToken;
151
199
  encrypt: typeof encryptEnvironmentValue;
152
200
  keyBytes: typeof environmentsEncryptionKeyBytes;
153
- withRefreshLock: typeof withCodexCredentialRefreshLock;
154
- };
155
-
156
- const defaultDeps: CodexAuthDeps = {
157
- loadCredential: loadCodexCredentialForRun,
158
- recordRefresh: recordCodexTokenRefresh,
159
- setStatus: setCodexCredentialStatus,
160
- refresh: refreshCodexToken,
161
- encrypt: encryptEnvironmentValue,
162
- keyBytes: environmentsEncryptionKeyBytes,
163
- withRefreshLock: withCodexCredentialRefreshLock,
201
+ withRefreshLock: <T>(
202
+ db: Database,
203
+ workspaceId: string,
204
+ credentialId: string,
205
+ fn: (lockedDb: Database) => Promise<T>,
206
+ ) => Promise<T>;
207
+ recordUsage?: (
208
+ db: Database,
209
+ workspaceId: string,
210
+ credentialId: string,
211
+ snapshot: CodexAccountUsageSnapshot,
212
+ ) => Promise<boolean>;
164
213
  };
165
214
 
166
215
  export function buildCodexTokenResolver(
@@ -173,7 +222,7 @@ export function buildCodexTokenResolver(
173
222
  // 0 rows against the now-inactive row — so a refresh racing a switch can never
174
223
  // clobber the newly-active account. The single-flight map needs zero change.
175
224
  credentialId: string,
176
- deps: CodexAuthDeps = defaultDeps,
225
+ deps: CodexAuthDeps,
177
226
  ): { getToken: () => Promise<CodexTokenSnapshot>; refresh: () => Promise<CodexTokenSnapshot> } {
178
227
  const snapshot = (cred: CodexCredentialForRun): CodexTokenSnapshot => ({
179
228
  accessToken: cred.tokens.accessToken,
@@ -336,9 +385,10 @@ export async function fetchCodexUsageForAccount(
336
385
  settings: Settings,
337
386
  workspaceId: string,
338
387
  credentialId: string,
388
+ deps: CodexAuthDeps,
339
389
  fetchImpl: CodexFetch = fetch,
340
390
  ): Promise<CodexUsagePayload> {
341
- const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
391
+ const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId, deps);
342
392
  let token: CodexTokenSnapshot;
343
393
  try {
344
394
  token = await resolver.getToken();
@@ -373,27 +423,29 @@ export async function fetchCodexUsageForAccount(
373
423
  // cached without erasing or falsely refreshing the last valid quota truth.
374
424
  // Cache-write is best-effort: a disconnect under us (false) or a transient
375
425
  // write error must NOT sink the freshly-read result we are about to return.
376
- await recordCodexAccountUsage(db, workspaceId, credentialId, {
377
- ...(parsedQuota
378
- ? {
379
- primaryUsedPercent: normalized.fiveHour?.percent ?? null,
380
- primaryResetAt: normalized.fiveHour?.resetAt
381
- ? new Date(normalized.fiveHour.resetAt)
382
- : null,
383
- secondaryUsedPercent: normalized.weekly?.percent ?? null,
384
- secondaryResetAt: normalized.weekly?.resetAt
385
- ? new Date(normalized.weekly.resetAt)
386
- : null,
387
- checkedAt,
388
- }
389
- : {}),
390
- ...(normalized.rateLimitResetCredits
391
- ? {
392
- resetCreditAvailableCount: normalized.rateLimitResetCredits.availableCount,
393
- resetCreditsCheckedAt: checkedAt,
394
- }
395
- : {}),
396
- }).catch(() => undefined);
426
+ await deps
427
+ .recordUsage?.(db, workspaceId, credentialId, {
428
+ ...(parsedQuota
429
+ ? {
430
+ primaryUsedPercent: normalized.fiveHour?.percent ?? null,
431
+ primaryResetAt: normalized.fiveHour?.resetAt
432
+ ? new Date(normalized.fiveHour.resetAt)
433
+ : null,
434
+ secondaryUsedPercent: normalized.weekly?.percent ?? null,
435
+ secondaryResetAt: normalized.weekly?.resetAt
436
+ ? new Date(normalized.weekly.resetAt)
437
+ : null,
438
+ checkedAt,
439
+ }
440
+ : {}),
441
+ ...(normalized.rateLimitResetCredits
442
+ ? {
443
+ resetCreditAvailableCount: normalized.rateLimitResetCredits.availableCount,
444
+ resetCreditsCheckedAt: checkedAt,
445
+ }
446
+ : {}),
447
+ })
448
+ .catch(() => undefined);
397
449
  }
398
450
 
399
451
  return normalized;
@@ -418,9 +470,10 @@ export async function fetchCodexRateLimitResetCreditsForAccount(
418
470
  settings: Settings,
419
471
  workspaceId: string,
420
472
  credentialId: string,
473
+ deps: CodexAuthDeps,
421
474
  fetchImpl: CodexFetch = fetch,
422
475
  ): Promise<CodexRateLimitResetCreditsAccountResult> {
423
- const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId);
476
+ const resolver = buildCodexTokenResolver(db, settings, workspaceId, credentialId, deps);
424
477
  let token: CodexTokenSnapshot;
425
478
  try {
426
479
  token = await resolver.getToken();