@opengeni/db 1.0.1 → 3.1.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attached-browser-devices.d.ts +7 -0
- package/dist/automations.d.ts +154 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +13 -0
- package/dist/child-lifecycle-notices.d.ts +68 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-EULAN2NP.js} +2 -2
- package/dist/{chunk-CUPM3FGB.js → chunk-NJVZPWXY.js} +3 -3
- package/dist/{chunk-4SR4HKKA.js → chunk-NMUBV7Z5.js} +19 -11
- package/dist/chunk-NMUBV7Z5.js.map +1 -0
- package/dist/chunk-OF5OBB76.js +4457 -0
- package/dist/chunk-OF5OBB76.js.map +1 -0
- package/dist/{chunk-6ZYKCRFO.js → chunk-PR2WMLOQ.js} +7580 -4417
- package/dist/chunk-PR2WMLOQ.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-SCPUJS53.js} +41 -4
- package/dist/chunk-SCPUJS53.js.map +1 -0
- package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
- package/dist/chunk-SIBYBA2D.js.map +1 -0
- package/dist/chunk-TIKF3KTI.js +383 -0
- package/dist/chunk-TIKF3KTI.js.map +1 -0
- package/dist/chunk-ZZ5LJEFX.js +516 -0
- package/dist/chunk-ZZ5LJEFX.js.map +1 -0
- package/dist/codemode-operations.d.ts +1 -6
- package/dist/company-brain-context-selection-schema.d.ts +705 -0
- package/dist/company-brain-context-selection.d.ts +94 -0
- package/dist/company-brain-governed-writes.d.ts +24 -0
- package/dist/company-brain.d.ts +55 -0
- package/dist/company-profile-agent-admin.d.ts +15 -0
- package/dist/company-profile-schema.d.ts +619 -2
- package/dist/company-profile.d.ts +9 -4
- package/dist/connection-authority.d.ts +95 -0
- package/dist/connection-scopes.d.ts +8 -0
- package/dist/connection-tenancy-backfill.d.ts +41 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +19 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1595 -81
- package/dist/index.js +21571 -8126
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +22 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +41 -4
- package/dist/session-realtime-ledger.d.ts +2 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +153 -0
- package/dist/slack-task-policy-schema.d.ts +34 -0
- package/dist/slack-task-policy.d.ts +8 -0
- package/dist/slack-user-link-access.d.ts +1 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/package.json +10 -6
- package/src/attached-browser-devices.ts +233 -5
- package/src/automations.ts +911 -0
- package/src/browser-sessions.ts +10 -0
- package/src/capability-integrations.ts +175 -1
- package/src/child-lifecycle-notices.ts +241 -0
- package/src/codemode-operations.ts +1 -35
- package/src/company-brain-context-selection-schema.ts +100 -0
- package/src/company-brain-context-selection.ts +359 -0
- package/src/company-brain-governed-writes.ts +1170 -0
- package/src/company-brain.ts +309 -0
- package/src/company-profile-agent-admin.ts +225 -0
- package/src/company-profile-schema.ts +121 -1
- package/src/company-profile.ts +21 -0
- package/src/computer-sessions.ts +10 -1
- package/src/connection-authority.ts +290 -0
- package/src/connection-scopes.ts +20 -0
- package/src/connection-tenancy-backfill.ts +81 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +75 -2
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +11651 -1548
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +985 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +877 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1291 -76
- package/src/schema.ts +3303 -428
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +437 -84
- package/src/session-realtime-context.ts +3 -3
- package/src/session-realtime-ledger.ts +22 -11
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +403 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +469 -0
- package/src/slack-task-policy-schema.ts +5 -0
- package/src/slack-task-policy.ts +18 -0
- package/src/slack-user-link-access.ts +17 -1
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-EULAN2NP.js.map} +0 -0
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-NJVZPWXY.js.map} +0 -0
|
@@ -14,6 +14,18 @@ import type {
|
|
|
14
14
|
TurnInitiator,
|
|
15
15
|
TurnInitiatorContext,
|
|
16
16
|
} from "@opengeni/contracts";
|
|
17
|
+
import {
|
|
18
|
+
ConnectionUseAuthoritySnapshot,
|
|
19
|
+
type ConnectionUseAttribution,
|
|
20
|
+
type ConnectionUseAuthorizationResult,
|
|
21
|
+
} from "@opengeni/contracts/connection-authority";
|
|
22
|
+
import {
|
|
23
|
+
PERSONAL_GITHUB_REQUESTED_SCOPES,
|
|
24
|
+
PERSONAL_GITHUB_PROVIDER_DOMAIN,
|
|
25
|
+
PERSONAL_GITHUB_TOKEN_URL,
|
|
26
|
+
PersonalGitHubConnectionMetadata,
|
|
27
|
+
} from "@opengeni/contracts/personal-github";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
17
29
|
import {
|
|
18
30
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
19
31
|
pinnedFetch,
|
|
@@ -28,6 +40,11 @@ import { Buffer } from "node:buffer";
|
|
|
28
40
|
import { isIP } from "node:net";
|
|
29
41
|
import { encryptEnvironmentValue } from "./environment-crypto";
|
|
30
42
|
import type { Database } from "./database";
|
|
43
|
+
import type {
|
|
44
|
+
AcceptedConnectionUseContext,
|
|
45
|
+
AcceptedConnectionUseResolution,
|
|
46
|
+
} from "./connection-authority";
|
|
47
|
+
import { connectionScopeKey } from "./connection-scopes";
|
|
31
48
|
|
|
32
49
|
const MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
33
50
|
const MAX_CREDENTIAL_NAME_BYTES = 256;
|
|
@@ -60,6 +77,8 @@ export type ConnectionCredentialForBroker = {
|
|
|
60
77
|
expiresAt: Date | null;
|
|
61
78
|
lastRefreshAt: Date | null;
|
|
62
79
|
version: number;
|
|
80
|
+
/** Immutable execution authority generation; distinct from token refresh version. */
|
|
81
|
+
authorityGeneration?: number;
|
|
63
82
|
metadata: Record<string, unknown>;
|
|
64
83
|
};
|
|
65
84
|
|
|
@@ -70,6 +89,7 @@ export type ConnectionCredentialLookupInput = {
|
|
|
70
89
|
kind?: ConnectionKind;
|
|
71
90
|
subjectId?: string | null;
|
|
72
91
|
allowSubjectOwned?: boolean;
|
|
92
|
+
expectedAuthorityGeneration?: number;
|
|
73
93
|
};
|
|
74
94
|
|
|
75
95
|
export type ConnectionTokenRefreshInput = {
|
|
@@ -79,6 +99,7 @@ export type ConnectionTokenRefreshInput = {
|
|
|
79
99
|
credentialEncrypted: string;
|
|
80
100
|
expiresAt: Date | null;
|
|
81
101
|
grantedScopes?: string[];
|
|
102
|
+
metadata?: Record<string, unknown>;
|
|
82
103
|
lastRefreshAt: Date;
|
|
83
104
|
subjectId?: string | null;
|
|
84
105
|
};
|
|
@@ -98,6 +119,14 @@ export type ResolveConnectionCredentialResult =
|
|
|
98
119
|
connectionId: string;
|
|
99
120
|
/** Exact durable version when the credential came from the local connection store. */
|
|
100
121
|
connectionVersion?: number;
|
|
122
|
+
/** Metadata-only owner attribution from the immediate pre-use fence. */
|
|
123
|
+
connectionUseAttribution?: ConnectionUseAttribution;
|
|
124
|
+
/**
|
|
125
|
+
* Revalidate the exact accepted attempt immediately before one target
|
|
126
|
+
* provider request. Credential lookup/refresh is a separate audited
|
|
127
|
+
* boundary and must not manufacture a provider-request fact by itself.
|
|
128
|
+
*/
|
|
129
|
+
authorizeProviderRequest?: () => Promise<boolean>;
|
|
101
130
|
expiresAt?: Date | null;
|
|
102
131
|
}
|
|
103
132
|
| {
|
|
@@ -129,6 +158,10 @@ export type ResolveConnectionCredentialInput = {
|
|
|
129
158
|
/** Defaults to header-only MCP transport. */
|
|
130
159
|
credentialTarget?: "mcp" | "http_api";
|
|
131
160
|
forceRefresh?: boolean;
|
|
161
|
+
/** Exact immutable accepted-work authority; never credential-bearing. */
|
|
162
|
+
connectionUseAuthority?: unknown;
|
|
163
|
+
/** Exact accepted attempt plus one stable physical-provider request id. */
|
|
164
|
+
connectionUseContext?: AcceptedConnectionUseContext;
|
|
132
165
|
};
|
|
133
166
|
|
|
134
167
|
export type HostMcpCredentialResolverContext = {
|
|
@@ -142,7 +175,6 @@ export type HostMcpCredentialResolverContext = {
|
|
|
142
175
|
initiator: TurnInitiator;
|
|
143
176
|
initiatorContext: TurnInitiatorContext;
|
|
144
177
|
surface: McpCredentialsRequest["surface"];
|
|
145
|
-
allowOfficialGmailRestDestination?: boolean;
|
|
146
178
|
};
|
|
147
179
|
|
|
148
180
|
export class HostMcpCredentialScopeError extends Error {
|
|
@@ -173,10 +205,11 @@ const OFFICIAL_GMAIL_MCP_RESOURCE = "https://gmailmcp.googleapis.com/mcp/v1";
|
|
|
173
205
|
const OFFICIAL_GMAIL_REST_HOST = "gmail.googleapis.com";
|
|
174
206
|
|
|
175
207
|
/**
|
|
176
|
-
* The
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
208
|
+
* The Gmail bridge reuses the exact OAuth grant created for the official
|
|
209
|
+
* hosted Gmail MCP resource, executing every tool against Gmail's REST API
|
|
210
|
+
* instead of Google's Developer Preview MCP endpoint. Keep this exception
|
|
211
|
+
* narrower than ordinary provider-domain binding: HTTPS only, Google's
|
|
212
|
+
* canonical Gmail API host, and the authenticated `users/me` path.
|
|
180
213
|
*/
|
|
181
214
|
function isOfficialGmailRestDestination(
|
|
182
215
|
destinationUrl: string,
|
|
@@ -216,10 +249,7 @@ export function buildHostConnectionTokenResolver(
|
|
|
216
249
|
if (
|
|
217
250
|
!destinationUrl ||
|
|
218
251
|
(!destinationHostMatchesProvider(destinationUrl, input.connectionRef.providerDomain) &&
|
|
219
|
-
!(
|
|
220
|
-
context.allowOfficialGmailRestDestination === true &&
|
|
221
|
-
isOfficialGmailRestDestination(destinationUrl, input.connectionRef)
|
|
222
|
-
))
|
|
252
|
+
!isOfficialGmailRestDestination(destinationUrl, input.connectionRef))
|
|
223
253
|
) {
|
|
224
254
|
throw new HostMcpCredentialBindingError("destinationUrl");
|
|
225
255
|
}
|
|
@@ -255,6 +285,12 @@ export function buildHostConnectionTokenResolver(
|
|
|
255
285
|
: {}),
|
|
256
286
|
},
|
|
257
287
|
forceRefresh: input.forceRefresh === true,
|
|
288
|
+
...(input.connectionUseAuthority !== undefined
|
|
289
|
+
? { connectionUseAuthority: input.connectionUseAuthority }
|
|
290
|
+
: {}),
|
|
291
|
+
...(input.connectionUseContext
|
|
292
|
+
? { connectionUseRequestId: input.connectionUseContext.physicalRequestId }
|
|
293
|
+
: {}),
|
|
258
294
|
...(toolName ? { toolName } : {}),
|
|
259
295
|
...(input.subjectId ? { callerSubjectId: input.subjectId } : {}),
|
|
260
296
|
};
|
|
@@ -575,6 +611,21 @@ export type ConnectionBrokerDeps = {
|
|
|
575
611
|
encrypt: typeof encryptEnvironmentValue;
|
|
576
612
|
keyBytes: typeof environmentsEncryptionKeyBytes;
|
|
577
613
|
now: () => Date;
|
|
614
|
+
authorizeUse?: (
|
|
615
|
+
db: Database,
|
|
616
|
+
input: { snapshot: unknown },
|
|
617
|
+
) => Promise<ConnectionUseAuthorizationResult>;
|
|
618
|
+
authorizeAcceptedUse?: (
|
|
619
|
+
db: Database,
|
|
620
|
+
input: AcceptedConnectionUseContext & {
|
|
621
|
+
serverId: string;
|
|
622
|
+
connectionId?: string;
|
|
623
|
+
providerDomain: string;
|
|
624
|
+
connectionKind?: ConnectionKind;
|
|
625
|
+
subjectScope?: "workspace" | "subject";
|
|
626
|
+
ownerSubjectId?: string;
|
|
627
|
+
},
|
|
628
|
+
) => Promise<AcceptedConnectionUseResolution>;
|
|
578
629
|
};
|
|
579
630
|
|
|
580
631
|
export type PermanentConnectionRefreshFailure = {
|
|
@@ -618,7 +669,7 @@ export function buildConnectionTokenResolver(
|
|
|
618
669
|
type CredentialLookupInput = Pick<
|
|
619
670
|
ResolveConnectionCredentialInput,
|
|
620
671
|
"workspaceId" | "connectionRef" | "subjectId"
|
|
621
|
-
|
|
672
|
+
> & { expectedAuthorityGeneration?: number };
|
|
622
673
|
const load = async (
|
|
623
674
|
input: CredentialLookupInput,
|
|
624
675
|
): Promise<ConnectionCredentialForBroker | null> => {
|
|
@@ -631,6 +682,9 @@ export function buildConnectionTokenResolver(
|
|
|
631
682
|
providerDomain: input.connectionRef.providerDomain,
|
|
632
683
|
allowSubjectOwned: subjectOwned,
|
|
633
684
|
...(subjectOwned ? { subjectId: input.subjectId! } : {}),
|
|
685
|
+
...(input.expectedAuthorityGeneration !== undefined
|
|
686
|
+
? { expectedAuthorityGeneration: input.expectedAuthorityGeneration }
|
|
687
|
+
: {}),
|
|
634
688
|
};
|
|
635
689
|
if (input.connectionRef.connectionId !== undefined) {
|
|
636
690
|
request.connectionId = input.connectionRef.connectionId;
|
|
@@ -640,6 +694,12 @@ export function buildConnectionTokenResolver(
|
|
|
640
694
|
}
|
|
641
695
|
const credential = await deps.loadCredential(db, settings, request);
|
|
642
696
|
if (!credential) return null;
|
|
697
|
+
if (
|
|
698
|
+
input.expectedAuthorityGeneration !== undefined &&
|
|
699
|
+
credential.authorityGeneration !== input.expectedAuthorityGeneration
|
|
700
|
+
) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
643
703
|
if (subjectOwned) {
|
|
644
704
|
return credential.subjectId === input.subjectId ? credential : null;
|
|
645
705
|
}
|
|
@@ -651,14 +711,19 @@ export function buildConnectionTokenResolver(
|
|
|
651
711
|
ref: McpServerConnectionRef,
|
|
652
712
|
destinationUrl: string,
|
|
653
713
|
inputCredentialTarget: "mcp" | "http_api",
|
|
714
|
+
connectionUseAttribution?: ConnectionUseAttribution,
|
|
654
715
|
): Promise<ResolveConnectionCredentialResult> => {
|
|
655
716
|
if (cred.status !== "active") {
|
|
656
717
|
return authNeededForStatus(cred, ref);
|
|
657
718
|
}
|
|
658
|
-
if (!connectionBindingMatches(cred, ref, destinationUrl
|
|
719
|
+
if (!connectionBindingMatches(cred, ref, destinationUrl)) {
|
|
659
720
|
return authNeeded(ref, "missing_connection", cred.id);
|
|
660
721
|
}
|
|
661
|
-
const missingScopes = missingRequestedScopes(
|
|
722
|
+
const missingScopes = missingRequestedScopes(
|
|
723
|
+
ref.scopes,
|
|
724
|
+
cred.grantedScopes,
|
|
725
|
+
cred.providerDomain,
|
|
726
|
+
);
|
|
662
727
|
if (missingScopes.length > 0) {
|
|
663
728
|
return {
|
|
664
729
|
status: "auth_needed",
|
|
@@ -695,6 +760,7 @@ export function buildConnectionTokenResolver(
|
|
|
695
760
|
...(material.placements ? { placements: material.placements } : {}),
|
|
696
761
|
connectionId: cred.id,
|
|
697
762
|
connectionVersion: cred.version,
|
|
763
|
+
...(connectionUseAttribution ? { connectionUseAttribution } : {}),
|
|
698
764
|
expiresAt: cred.expiresAt,
|
|
699
765
|
};
|
|
700
766
|
};
|
|
@@ -720,6 +786,9 @@ export function buildConnectionTokenResolver(
|
|
|
720
786
|
if (refreshed.grantedScopes !== undefined) {
|
|
721
787
|
refreshRecord.grantedScopes = refreshed.grantedScopes;
|
|
722
788
|
}
|
|
789
|
+
if (refreshed.metadata !== undefined) {
|
|
790
|
+
refreshRecord.metadata = refreshed.metadata;
|
|
791
|
+
}
|
|
723
792
|
const persisted = await deps.recordRefresh(db, refreshRecord);
|
|
724
793
|
if (persisted) {
|
|
725
794
|
const current = await load({
|
|
@@ -761,10 +830,11 @@ export function buildConnectionTokenResolver(
|
|
|
761
830
|
};
|
|
762
831
|
|
|
763
832
|
return async (input) => {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
833
|
+
let ref = input.connectionRef;
|
|
834
|
+
let subjectId = input.subjectId;
|
|
835
|
+
let credentialWorkspaceId = input.workspaceId;
|
|
836
|
+
let expectedAuthorityGeneration: number | undefined;
|
|
837
|
+
let connectionUseAttribution: ConnectionUseAttribution | undefined;
|
|
768
838
|
// Repository-scoped provider bindings require a broker that can prove the
|
|
769
839
|
// selected-resource boundary. The generic standalone credential store has
|
|
770
840
|
// no provider-specific containment adapter, so it must fail closed instead
|
|
@@ -772,9 +842,99 @@ export function buildConnectionTokenResolver(
|
|
|
772
842
|
if (ref.selectedResources) {
|
|
773
843
|
return authNeeded(ref, "resource_scope_unavailable", ref.connectionId);
|
|
774
844
|
}
|
|
845
|
+
if (input.connectionUseContext !== undefined) {
|
|
846
|
+
if (!deps.authorizeAcceptedUse) {
|
|
847
|
+
return authNeeded(
|
|
848
|
+
ref,
|
|
849
|
+
ref.subjectScope === "subject" ? "personal_authority_unavailable" : "missing_connection",
|
|
850
|
+
ref.connectionId,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const authorization = await deps.authorizeAcceptedUse(db, {
|
|
854
|
+
...input.connectionUseContext,
|
|
855
|
+
serverId: input.serverId,
|
|
856
|
+
...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
|
|
857
|
+
providerDomain: ref.providerDomain,
|
|
858
|
+
...(ref.kind ? { connectionKind: ref.kind } : {}),
|
|
859
|
+
subjectScope: ref.subjectScope === "subject" ? "subject" : "workspace",
|
|
860
|
+
// An owner binding belongs only to the personal lanes. Interactive
|
|
861
|
+
// turns stamp the initiating human's subjectId on every credential
|
|
862
|
+
// request regardless of ref scope; forwarding it for a workspace ref
|
|
863
|
+
// would make the 0279 workspace lane deny the ambient shared row.
|
|
864
|
+
...(ref.subjectScope === "subject" && subjectId ? { ownerSubjectId: subjectId } : {}),
|
|
865
|
+
});
|
|
866
|
+
if (authorization.status === "denied") {
|
|
867
|
+
return authNeeded(
|
|
868
|
+
ref,
|
|
869
|
+
ref.subjectScope === "subject" ? "personal_authority_unavailable" : "missing_connection",
|
|
870
|
+
ref.connectionId,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
connectionUseAttribution = authorization.attribution;
|
|
874
|
+
expectedAuthorityGeneration = authorization.attribution.connectionGeneration;
|
|
875
|
+
const expectedPersonal = authorization.attribution.scope !== "workspace";
|
|
876
|
+
credentialWorkspaceId = authorization.originWorkspaceId;
|
|
877
|
+
subjectId = authorization.attribution.ownerSubjectId ?? undefined;
|
|
878
|
+
ref = {
|
|
879
|
+
...ref,
|
|
880
|
+
connectionId: authorization.attribution.connectionId,
|
|
881
|
+
kind: authorization.connectionKind,
|
|
882
|
+
subjectScope: expectedPersonal ? "subject" : "workspace",
|
|
883
|
+
};
|
|
884
|
+
} else if (input.connectionUseAuthority !== undefined) {
|
|
885
|
+
const authority = ConnectionUseAuthoritySnapshot.parse(input.connectionUseAuthority);
|
|
886
|
+
const expectedPersonal = authority.scope === "user";
|
|
887
|
+
if (
|
|
888
|
+
authority.targetWorkspaceId !== input.workspaceId ||
|
|
889
|
+
authority.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase() ||
|
|
890
|
+
(ref.connectionId !== undefined && ref.connectionId !== authority.connectionId) ||
|
|
891
|
+
(ref.kind !== undefined && ref.kind !== authority.connectionKind) ||
|
|
892
|
+
(ref.subjectScope === "subject") !== expectedPersonal ||
|
|
893
|
+
!deps.authorizeUse
|
|
894
|
+
) {
|
|
895
|
+
return authNeeded(
|
|
896
|
+
ref,
|
|
897
|
+
expectedPersonal ? "personal_authority_unavailable" : "missing_connection",
|
|
898
|
+
ref.connectionId,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const authorization = await deps.authorizeUse(db, { snapshot: authority });
|
|
902
|
+
if (authorization.status === "denied") {
|
|
903
|
+
return authNeeded(
|
|
904
|
+
ref,
|
|
905
|
+
expectedPersonal ? "personal_authority_unavailable" : "missing_connection",
|
|
906
|
+
authority.connectionId,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
connectionUseAttribution = authorization.attribution;
|
|
910
|
+
expectedAuthorityGeneration = authority.connectionGeneration;
|
|
911
|
+
// Personal resources are organization-user owned and may originate in a
|
|
912
|
+
// different workspace from the session using them. Authorization is
|
|
913
|
+
// evaluated against the target workspace above; the exact credential is
|
|
914
|
+
// then loaded from its frozen physical origin, never rediscovered in the
|
|
915
|
+
// target workspace.
|
|
916
|
+
credentialWorkspaceId = expectedPersonal
|
|
917
|
+
? authority.originWorkspaceId
|
|
918
|
+
: authority.targetWorkspaceId;
|
|
919
|
+
subjectId = authority.ownerSubjectId ?? undefined;
|
|
920
|
+
ref = {
|
|
921
|
+
...ref,
|
|
922
|
+
connectionId: authority.connectionId,
|
|
923
|
+
kind: authority.connectionKind,
|
|
924
|
+
subjectScope: expectedPersonal ? "subject" : "workspace",
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (ref.subjectScope === "subject" && !subjectId) {
|
|
928
|
+
return authNeeded(ref, "personal_authority_unavailable", ref.connectionId);
|
|
929
|
+
}
|
|
775
930
|
let cred: ConnectionCredentialForBroker | null;
|
|
776
931
|
try {
|
|
777
|
-
cred = await load(
|
|
932
|
+
cred = await load({
|
|
933
|
+
workspaceId: credentialWorkspaceId,
|
|
934
|
+
connectionRef: ref,
|
|
935
|
+
...(subjectId ? { subjectId } : {}),
|
|
936
|
+
...(expectedAuthorityGeneration !== undefined ? { expectedAuthorityGeneration } : {}),
|
|
937
|
+
});
|
|
778
938
|
} catch {
|
|
779
939
|
return authNeeded(ref, "refresh_failed");
|
|
780
940
|
}
|
|
@@ -787,9 +947,7 @@ export function buildConnectionTokenResolver(
|
|
|
787
947
|
// Reject an audience/destination mismatch before any provider-side refresh
|
|
788
948
|
// or usage update. Refreshing first would still create an unauthorized
|
|
789
949
|
// external side effect even though the token was never sent to the target.
|
|
790
|
-
if (
|
|
791
|
-
!connectionBindingMatches(cred, ref, input.destinationUrl, settings.gmailRestAdapterEnabled)
|
|
792
|
-
) {
|
|
950
|
+
if (!connectionBindingMatches(cred, ref, input.destinationUrl)) {
|
|
793
951
|
return authNeeded(ref, "missing_connection", cred.id);
|
|
794
952
|
}
|
|
795
953
|
if (shouldRefresh(cred, input.forceRefresh === true, deps.now())) {
|
|
@@ -828,15 +986,30 @@ export function buildConnectionTokenResolver(
|
|
|
828
986
|
return authNeeded(ref, "refresh_failed", cred.id);
|
|
829
987
|
}
|
|
830
988
|
}
|
|
831
|
-
|
|
989
|
+
if (
|
|
990
|
+
expectedAuthorityGeneration !== undefined &&
|
|
991
|
+
cred.authorityGeneration !== expectedAuthorityGeneration
|
|
992
|
+
) {
|
|
993
|
+
return authNeeded(ref, authorityReasonForScope(ref.subjectScope === "subject"), cred.id);
|
|
994
|
+
}
|
|
995
|
+
return await snapshot(
|
|
996
|
+
cred,
|
|
997
|
+
ref,
|
|
998
|
+
input.destinationUrl,
|
|
999
|
+
input.credentialTarget ?? "mcp",
|
|
1000
|
+
connectionUseAttribution,
|
|
1001
|
+
);
|
|
832
1002
|
};
|
|
833
1003
|
}
|
|
834
1004
|
|
|
1005
|
+
function authorityReasonForScope(personal: boolean): AuthNeededReason {
|
|
1006
|
+
return personal ? "personal_authority_unavailable" : "missing_connection";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
835
1009
|
function connectionBindingMatches(
|
|
836
1010
|
cred: ConnectionCredentialForBroker,
|
|
837
1011
|
ref: McpServerConnectionRef,
|
|
838
1012
|
destinationUrl: string,
|
|
839
|
-
gmailRestAdapterEnabled: boolean,
|
|
840
1013
|
): boolean {
|
|
841
1014
|
if (cred.providerDomain.toLowerCase() !== ref.providerDomain.toLowerCase()) return false;
|
|
842
1015
|
if (ref.kind && cred.kind !== ref.kind) return false;
|
|
@@ -852,7 +1025,6 @@ function connectionBindingMatches(
|
|
|
852
1025
|
!binding ||
|
|
853
1026
|
(destination !== binding &&
|
|
854
1027
|
!(
|
|
855
|
-
gmailRestAdapterEnabled &&
|
|
856
1028
|
binding === canonicalHttpUrl(OFFICIAL_GMAIL_MCP_RESOURCE) &&
|
|
857
1029
|
isOfficialGmailRestDestination(destination, ref)
|
|
858
1030
|
))
|
|
@@ -979,12 +1151,16 @@ function authNeededForStatus(
|
|
|
979
1151
|
);
|
|
980
1152
|
}
|
|
981
1153
|
|
|
982
|
-
function missingRequestedScopes(
|
|
1154
|
+
function missingRequestedScopes(
|
|
1155
|
+
requested: string[] | undefined,
|
|
1156
|
+
granted: string[],
|
|
1157
|
+
providerDomain: string,
|
|
1158
|
+
): string[] {
|
|
983
1159
|
if (!requested?.length) {
|
|
984
1160
|
return [];
|
|
985
1161
|
}
|
|
986
|
-
const grantedSet = new Set(granted);
|
|
987
|
-
return requested.filter((scope) => !grantedSet.has(scope));
|
|
1162
|
+
const grantedSet = new Set(granted.map((scope) => connectionScopeKey(providerDomain, scope)));
|
|
1163
|
+
return requested.filter((scope) => !grantedSet.has(connectionScopeKey(providerDomain, scope)));
|
|
988
1164
|
}
|
|
989
1165
|
|
|
990
1166
|
/**
|
|
@@ -1057,6 +1233,7 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1057
1233
|
credential: Record<string, unknown>;
|
|
1058
1234
|
expiresAt: Date | null;
|
|
1059
1235
|
grantedScopes?: string[];
|
|
1236
|
+
metadata?: Record<string, unknown>;
|
|
1060
1237
|
}> {
|
|
1061
1238
|
if (cred.kind !== "oauth2") {
|
|
1062
1239
|
return {
|
|
@@ -1066,13 +1243,21 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1066
1243
|
};
|
|
1067
1244
|
}
|
|
1068
1245
|
const refreshToken = stringValue((cred.credential as { refresh_token?: unknown }).refresh_token);
|
|
1069
|
-
const
|
|
1246
|
+
const personalGitHubMetadata = PersonalGitHubConnectionMetadata.safeParse(cred.metadata);
|
|
1247
|
+
const personalGitHub =
|
|
1248
|
+
cred.providerDomain.toLowerCase() === PERSONAL_GITHUB_PROVIDER_DOMAIN &&
|
|
1249
|
+
personalGitHubMetadata.success;
|
|
1250
|
+
const storedTokenEndpoint =
|
|
1070
1251
|
stringValue((cred.credential as { token_endpoint?: unknown }).token_endpoint) ??
|
|
1071
1252
|
stringValue((cred.metadata as { tokenEndpoint?: unknown }).tokenEndpoint) ??
|
|
1072
1253
|
stringValue((cred.metadata as { token_endpoint?: unknown }).token_endpoint);
|
|
1254
|
+
const tokenEndpoint = personalGitHub ? PERSONAL_GITHUB_TOKEN_URL : storedTokenEndpoint;
|
|
1073
1255
|
if (!refreshToken || !tokenEndpoint) {
|
|
1074
1256
|
throw new Error("connection has no refresh token endpoint");
|
|
1075
1257
|
}
|
|
1258
|
+
if (personalGitHub && storedTokenEndpoint !== PERSONAL_GITHUB_TOKEN_URL) {
|
|
1259
|
+
throw new ConnectionRefreshHttpError(401, "client_unavailable");
|
|
1260
|
+
}
|
|
1076
1261
|
let validatedTokenEndpoint: string;
|
|
1077
1262
|
try {
|
|
1078
1263
|
validatedTokenEndpoint = validateHttpUrl(tokenEndpoint, {
|
|
@@ -1092,11 +1277,25 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1092
1277
|
stringValue((cred.credential as { client_id?: unknown }).client_id) ??
|
|
1093
1278
|
stringValue((cred.metadata as { clientId?: unknown }).clientId) ??
|
|
1094
1279
|
stringValue((cred.metadata as { client_id?: unknown }).client_id);
|
|
1095
|
-
const clientSecret =
|
|
1280
|
+
const clientSecret = personalGitHub
|
|
1281
|
+
? settings.githubPersonalOauthClientSecret
|
|
1282
|
+
: stringValue((cred.credential as { client_secret?: unknown }).client_secret);
|
|
1096
1283
|
const authMethod =
|
|
1097
1284
|
stringValue(
|
|
1098
1285
|
(cred.credential as { token_endpoint_auth_method?: unknown }).token_endpoint_auth_method,
|
|
1099
1286
|
) ?? "none";
|
|
1287
|
+
if (
|
|
1288
|
+
personalGitHub &&
|
|
1289
|
+
(!settings.githubPersonalOauthEnabled ||
|
|
1290
|
+
!clientId ||
|
|
1291
|
+
clientId !== settings.githubPersonalOauthClientId ||
|
|
1292
|
+
personalGitHubMetadata.data.oauthEnvironment !== settings.environment ||
|
|
1293
|
+
personalGitHubMetadata.data.oauthClientMarker !==
|
|
1294
|
+
createHash("sha256").update(clientId).digest("hex").slice(0, 32) ||
|
|
1295
|
+
!clientSecret)
|
|
1296
|
+
) {
|
|
1297
|
+
throw new ConnectionRefreshHttpError(401, "client_unavailable");
|
|
1298
|
+
}
|
|
1100
1299
|
if (clientId) {
|
|
1101
1300
|
body.set("client_id", clientId);
|
|
1102
1301
|
}
|
|
@@ -1123,7 +1322,10 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1123
1322
|
if (resource && resourceParameterSupported) {
|
|
1124
1323
|
body.set("resource", resource);
|
|
1125
1324
|
}
|
|
1126
|
-
|
|
1325
|
+
const scopeParameterSupported =
|
|
1326
|
+
(cred.credential as { scope_parameter_supported?: unknown }).scope_parameter_supported !==
|
|
1327
|
+
false;
|
|
1328
|
+
if (ref.scopes?.length && scopeParameterSupported) {
|
|
1127
1329
|
body.set("scope", ref.scopes.join(" "));
|
|
1128
1330
|
}
|
|
1129
1331
|
const requestBody: BodyInit =
|
|
@@ -1165,6 +1367,23 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1165
1367
|
}
|
|
1166
1368
|
const expiresAt = expiresAtFromTokenResponse(payload, cred.expiresAt);
|
|
1167
1369
|
const scopeText = stringValue(payload.scope);
|
|
1370
|
+
const returnedScopes = scopeText?.split(/[\s,]+/).filter(Boolean) ?? [];
|
|
1371
|
+
const refreshTokenExpiresIn =
|
|
1372
|
+
typeof payload.refresh_token_expires_in === "number"
|
|
1373
|
+
? payload.refresh_token_expires_in
|
|
1374
|
+
: undefined;
|
|
1375
|
+
const refreshTokenExpiresAt =
|
|
1376
|
+
personalGitHub && refreshTokenExpiresIn && refreshTokenExpiresIn > 0
|
|
1377
|
+
? new Date(Date.now() + refreshTokenExpiresIn * 1000).toISOString()
|
|
1378
|
+
: null;
|
|
1379
|
+
if (
|
|
1380
|
+
personalGitHub &&
|
|
1381
|
+
scopeText &&
|
|
1382
|
+
(returnedScopes.length !== PERSONAL_GITHUB_REQUESTED_SCOPES.length ||
|
|
1383
|
+
returnedScopes.some((scope) => !PERSONAL_GITHUB_REQUESTED_SCOPES.includes(scope as "repo")))
|
|
1384
|
+
) {
|
|
1385
|
+
throw new ConnectionRefreshHttpError(401, "scope_changed");
|
|
1386
|
+
}
|
|
1168
1387
|
const nextCredential = {
|
|
1169
1388
|
...cred.credential,
|
|
1170
1389
|
access_token: accessToken,
|
|
@@ -1176,14 +1395,27 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1176
1395
|
...(expiresAt ? { expires_at: expiresAt.toISOString() } : {}),
|
|
1177
1396
|
...(resource ? { resource } : {}),
|
|
1178
1397
|
...(scopeText ? { scope: scopeText } : {}),
|
|
1179
|
-
...(
|
|
1398
|
+
...(refreshTokenExpiresAt ? { refresh_token_expires_at: refreshTokenExpiresAt } : {}),
|
|
1399
|
+
...(clientSecret && !personalGitHub
|
|
1180
1400
|
? { client_secret: clientSecret, token_endpoint_auth_method: authMethod }
|
|
1181
1401
|
: {}),
|
|
1182
1402
|
};
|
|
1183
1403
|
return {
|
|
1184
1404
|
credential: nextCredential,
|
|
1185
1405
|
expiresAt,
|
|
1186
|
-
...(
|
|
1406
|
+
...(personalGitHub
|
|
1407
|
+
? { grantedScopes: [...PERSONAL_GITHUB_REQUESTED_SCOPES] }
|
|
1408
|
+
: scopeText
|
|
1409
|
+
? { grantedScopes: returnedScopes }
|
|
1410
|
+
: {}),
|
|
1411
|
+
...(refreshTokenExpiresAt
|
|
1412
|
+
? {
|
|
1413
|
+
metadata: PersonalGitHubConnectionMetadata.parse({
|
|
1414
|
+
...personalGitHubMetadata.data,
|
|
1415
|
+
refreshTokenExpiresAt,
|
|
1416
|
+
}),
|
|
1417
|
+
}
|
|
1418
|
+
: {}),
|
|
1187
1419
|
};
|
|
1188
1420
|
}
|
|
1189
1421
|
|
package/src/database.ts
CHANGED
|
@@ -115,6 +115,18 @@ export type RlsStrategy = "force" | "scoped";
|
|
|
115
115
|
*/
|
|
116
116
|
export type UserLookup = (db: Database, email: string) => Promise<string | null>;
|
|
117
117
|
|
|
118
|
+
export type ManagedUserProfile = {
|
|
119
|
+
id: string;
|
|
120
|
+
name: string | null;
|
|
121
|
+
email: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Host identity projection used by organization administration rosters. */
|
|
125
|
+
export type UserProfileLookup = (
|
|
126
|
+
db: Database,
|
|
127
|
+
userIds: readonly string[],
|
|
128
|
+
) => Promise<ManagedUserProfile[]>;
|
|
129
|
+
|
|
118
130
|
export type CreateDbOptions = {
|
|
119
131
|
/**
|
|
120
132
|
* The Postgres `search_path` for this connection (Step I, §7.8 runtime half).
|
|
@@ -132,6 +144,8 @@ export type CreateDbOptions = {
|
|
|
132
144
|
rlsStrategy?: RlsStrategy;
|
|
133
145
|
/** Host-provided user-by-email resolver; unset → today's raw `auth_users` query. */
|
|
134
146
|
userLookup?: UserLookup;
|
|
147
|
+
/** Host-provided user profile projection; unset → standalone `auth_users`. */
|
|
148
|
+
userProfileLookup?: UserProfileLookup;
|
|
135
149
|
/** postgres-js pool size; defaults to today's `10`. */
|
|
136
150
|
max?: number;
|
|
137
151
|
/**
|
|
@@ -150,7 +164,11 @@ export type CreateDbOptions = {
|
|
|
150
164
|
* config (e.g. one built outside `createDb`, or in a test) falls back to the
|
|
151
165
|
* standalone defaults: `rlsStrategy: "force"`, raw `auth_users` lookup.
|
|
152
166
|
*/
|
|
153
|
-
type DbBinding = {
|
|
167
|
+
type DbBinding = {
|
|
168
|
+
rlsStrategy: RlsStrategy;
|
|
169
|
+
userLookup?: UserLookup;
|
|
170
|
+
userProfileLookup?: UserProfileLookup;
|
|
171
|
+
};
|
|
154
172
|
|
|
155
173
|
const dbBindings = new WeakMap<object, DbBinding>();
|
|
156
174
|
|
|
@@ -314,6 +332,7 @@ export function createDb(databaseUrl: string, options: CreateDbOptions = {}): Db
|
|
|
314
332
|
dbBindings.set(db as unknown as object, {
|
|
315
333
|
rlsStrategy: options.rlsStrategy ?? "force",
|
|
316
334
|
...(options.userLookup ? { userLookup: options.userLookup } : {}),
|
|
335
|
+
...(options.userProfileLookup ? { userProfileLookup: options.userProfileLookup } : {}),
|
|
317
336
|
});
|
|
318
337
|
return {
|
|
319
338
|
db,
|
|
@@ -331,11 +350,16 @@ export function createDb(databaseUrl: string, options: CreateDbOptions = {}): Db
|
|
|
331
350
|
*/
|
|
332
351
|
export function registerDbBinding(
|
|
333
352
|
db: Database,
|
|
334
|
-
binding: {
|
|
353
|
+
binding: {
|
|
354
|
+
rlsStrategy?: RlsStrategy;
|
|
355
|
+
userLookup?: UserLookup;
|
|
356
|
+
userProfileLookup?: UserProfileLookup;
|
|
357
|
+
},
|
|
335
358
|
): void {
|
|
336
359
|
dbBindings.set(db as unknown as object, {
|
|
337
360
|
rlsStrategy: binding.rlsStrategy ?? "force",
|
|
338
361
|
...(binding.userLookup ? { userLookup: binding.userLookup } : {}),
|
|
362
|
+
...(binding.userProfileLookup ? { userProfileLookup: binding.userProfileLookup } : {}),
|
|
339
363
|
});
|
|
340
364
|
}
|
|
341
365
|
|
|
@@ -662,6 +686,14 @@ async function finalizeSessionActivityGate(
|
|
|
662
686
|
await db.execute(
|
|
663
687
|
sql`set constraints sessions_activity_insert_commit_guard, sessions_activity_update_commit_guard immediate`,
|
|
664
688
|
);
|
|
689
|
+
// A restored activity scope can be followed by another workspace scope, or
|
|
690
|
+
// by a SECURITY DEFINER lifecycle that opens and finalizes its own gate in
|
|
691
|
+
// the same outer transaction. Keep the guards deferred after proving this
|
|
692
|
+
// scope's final state so those later writes reach their own finalizer instead
|
|
693
|
+
// of firing the commit guard at the first session update.
|
|
694
|
+
await db.execute(
|
|
695
|
+
sql`set constraints sessions_activity_insert_commit_guard, sessions_activity_update_commit_guard deferred`,
|
|
696
|
+
);
|
|
665
697
|
}
|
|
666
698
|
|
|
667
699
|
/**
|
|
@@ -709,6 +741,47 @@ export async function withWorkspaceSessionActivityRls<T>(
|
|
|
709
741
|
);
|
|
710
742
|
}
|
|
711
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Run one of several ordered workspace activity scopes inside a caller-owned
|
|
746
|
+
* transaction. Each scope is fully finalized before restoring the caller's
|
|
747
|
+
* gate GUCs, so the next workspace can reuse the same outer transaction.
|
|
748
|
+
*/
|
|
749
|
+
export async function withRestoredSessionActivityRlsContext<T>(
|
|
750
|
+
db: Database,
|
|
751
|
+
context: RlsContext & { workspaceId: string },
|
|
752
|
+
fn: (db: SessionActivityDatabase) => Promise<T>,
|
|
753
|
+
): Promise<T> {
|
|
754
|
+
if (!isTransactionHandle(db)) {
|
|
755
|
+
throw new Error("Sequential workspace activity scopes require a caller-owned transaction");
|
|
756
|
+
}
|
|
757
|
+
const [prior] = await rawRows<{ state: string; workspace_id: string }>(
|
|
758
|
+
db,
|
|
759
|
+
sql`select
|
|
760
|
+
coalesce(current_setting('opengeni.session_activity_gate_state', true), '') as state,
|
|
761
|
+
coalesce(
|
|
762
|
+
current_setting('opengeni.session_activity_gate_workspace_id', true), ''
|
|
763
|
+
) as workspace_id`,
|
|
764
|
+
);
|
|
765
|
+
try {
|
|
766
|
+
return await withRlsContext(db, context, async (scopedDb) => {
|
|
767
|
+
const gate = await beginSessionActivityGate(scopedDb, context.workspaceId);
|
|
768
|
+
if (!gate.owner) {
|
|
769
|
+
throw new Error("Sequential workspace activity scope unexpectedly reused an open gate");
|
|
770
|
+
}
|
|
771
|
+
const value = await fn(gate.db);
|
|
772
|
+
await assertRlsContextApplied(gate.db, context);
|
|
773
|
+
await finalizeSessionActivityGate(gate.db, context.workspaceId);
|
|
774
|
+
return value;
|
|
775
|
+
});
|
|
776
|
+
} finally {
|
|
777
|
+
await db.execute(sql`select
|
|
778
|
+
set_config('opengeni.session_activity_gate_state', ${prior?.state ?? ""}, true),
|
|
779
|
+
set_config(
|
|
780
|
+
'opengeni.session_activity_gate_workspace_id', ${prior?.workspace_id ?? ""}, true
|
|
781
|
+
)`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
712
785
|
/**
|
|
713
786
|
* Open one nested savepoint without losing the gate brand. Drizzle correctly
|
|
714
787
|
* preserves the backend-local gate GUCs, but its structural transaction type
|