@opengeni/db 5.0.1 → 6.0.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/automations.d.ts +0 -2
- package/dist/canonical-human-identities.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-components.d.ts +5 -13
- package/dist/capability-integrations.d.ts +23 -8
- package/dist/capability-reconciliation.d.ts +5 -0
- package/dist/capability-reconciliation.js +21 -0
- package/dist/capability-reconciliation.js.map +1 -0
- package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
- package/dist/chunk-4UJCXBR2.js.map +1 -0
- package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
- package/dist/chunk-AJA2ZIUW.js +219 -0
- package/dist/chunk-AJA2ZIUW.js.map +1 -0
- package/dist/chunk-CDAT6EKW.js +247 -0
- package/dist/chunk-CDAT6EKW.js.map +1 -0
- package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
- package/dist/chunk-CJI6G254.js.map +1 -0
- package/dist/chunk-G32NVMRJ.js +276 -0
- package/dist/chunk-G32NVMRJ.js.map +1 -0
- package/dist/chunk-GABXCG3N.js +5811 -0
- package/dist/chunk-GABXCG3N.js.map +1 -0
- package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
- package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
- package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
- package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
- package/dist/chunk-MN5PZMEK.js.map +1 -0
- package/dist/chunk-PJI4MUZ7.js +131 -0
- package/dist/chunk-PJI4MUZ7.js.map +1 -0
- package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
- package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
- package/dist/chunk-RL2MUXM3.js.map +1 -0
- package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
- package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
- package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
- package/dist/chunk-XIXVVBW2.js.map +1 -0
- package/dist/chunk-ZPJ6V2VG.js +156 -0
- package/dist/chunk-ZPJ6V2VG.js.map +1 -0
- package/dist/codex-capacity-recovery.d.ts +12 -0
- package/dist/codex-failure-eligibility.d.ts +9 -0
- package/dist/company-profile-schema.d.ts +4 -2
- package/dist/connect-attempts.d.ts +15 -0
- package/dist/connect-attempts.js +25 -0
- package/dist/connect-attempts.js.map +1 -0
- package/dist/connection-authority.d.ts +7 -63
- package/dist/connection-refresh-lock.d.ts +4 -0
- package/dist/connection-token-resolver.d.ts +8 -43
- package/dist/database-timing.d.ts +10 -0
- package/dist/database.d.ts +4 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +9 -0
- package/dist/editable-artifacts.js +7 -5
- package/dist/index.d.ts +447 -78
- package/dist/index.js +12705 -12872
- package/dist/index.js.map +1 -1
- package/dist/insights-usage-bundle.d.ts +3 -0
- package/dist/integration-bindings.d.ts +1 -1
- package/dist/integration-facets.d.ts +18 -1
- package/dist/knowledge-entries-schema.d.ts +2 -0
- package/dist/knowledge-entries.d.ts +1 -0
- package/dist/knowledge-indexing.d.ts +1 -0
- package/dist/live-session-attempt.d.ts +35 -137
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/mcp-account-bindings.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +11 -0
- package/dist/mcp-operations.js +2 -2
- package/dist/migrate.js +1 -1
- package/dist/organization-integration-policy.d.ts +51 -0
- package/dist/organization-integration-policy.js +18 -0
- package/dist/organization-integration-policy.js.map +1 -0
- package/dist/organization-usage.d.ts +20 -0
- package/dist/plugin-packages.d.ts +9 -12
- package/dist/pr-review.d.ts +0 -11
- package/dist/preference-registry-schema.d.ts +17 -0
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.d.ts +124 -6
- package/dist/retained-provider-commands.js +29 -97
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/retired-session-create-metadata.d.ts +4 -0
- package/dist/runtime-posture.d.ts +7 -5
- package/dist/schema.d.ts +876 -656
- package/dist/schema.js +5 -7
- package/dist/session-admission-block.d.ts +32 -0
- package/dist/session-attempt-writers.d.ts +4 -0
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -126
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-control.d.ts +5 -1
- package/dist/session-event-slices.d.ts +10 -0
- package/dist/session-event-slices.js +8 -222
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-file-attachments.d.ts +28 -0
- package/dist/session-goal-reports.d.ts +43 -0
- package/dist/session-mcp-approval.d.ts +4 -0
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-message-search.d.ts +22 -0
- package/dist/session-queue-commands.d.ts +2 -6
- package/dist/session-retry.d.ts +36 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/skill-source-release-impact.d.ts +18 -0
- package/dist/skill-source-release.d.ts +9 -0
- package/dist/skills.d.ts +7 -1
- package/dist/user-resource-authority.d.ts +0 -8
- package/dist/video-generation.js +3 -3
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
- package/drizzle/0469_knowledge_source_discovery.sql +85 -0
- package/drizzle/0470_organization_integration_policy.sql +151 -0
- package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
- package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
- package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
- package/drizzle/0474_goal_report_requirements.sql +156 -0
- package/drizzle/0475_session_event_history_statistics.sql +15 -0
- package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
- package/drizzle/0477_managed_sign_in_methods.sql +303 -0
- package/drizzle/0478_sender_owned_connections.sql +2772 -0
- package/drizzle/0479_connection_create_idempotency.sql +16 -0
- package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
- package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
- package/drizzle/0482_remove_packs.sql +417 -0
- package/drizzle/0483_preclaim_admission_block.sql +85 -0
- package/drizzle/0484_insights_usage_projection.sql +211 -0
- package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
- package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
- package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
- package/drizzle/0488_permanent_skill_removal.sql +207 -0
- package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
- package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
- package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
- package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
- package/drizzle/0493_stopping_command_error_containment.sql +58 -0
- package/drizzle/0494_mcp_account_bindings.sql +334 -0
- package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
- package/drizzle/0496_supervised_command_settlement.sql +386 -0
- package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
- package/drizzle/0498_enrollment_membership_fence.sql +87 -0
- package/drizzle/0499_session_attachment_access.sql +224 -0
- package/drizzle/0500_session_execution_epoch.sql +37 -0
- package/drizzle/0501_session_sharing_execution.sql +591 -0
- package/drizzle/0502_active_message_boundary_forks.sql +30 -0
- package/package.json +17 -5
- package/src/automations.ts +17 -73
- package/src/canonical-human-identities.ts +17 -5
- package/src/capability-components.ts +7 -24
- package/src/capability-integrations.ts +301 -21
- package/src/capability-reconciliation.ts +19 -0
- package/src/codex-capacity-recovery.ts +47 -0
- package/src/codex-failure-eligibility.ts +93 -0
- package/src/connect-attempts.ts +49 -18
- package/src/connection-authority.ts +12 -158
- package/src/connection-refresh-lock.ts +22 -0
- package/src/connection-token-resolver.ts +58 -530
- package/src/database-timing.ts +43 -0
- package/src/database.ts +61 -26
- package/src/editable-artifacts.ts +1 -1
- package/src/index.ts +3578 -1273
- package/src/insights-model-bundle.ts +19 -7
- package/src/insights-usage-bundle.ts +16 -19
- package/src/integration-bindings.ts +2 -7
- package/src/integration-facets.ts +73 -6
- package/src/knowledge-entries.ts +8 -1
- package/src/live-session-attempt.ts +0 -22
- package/src/mcp-account-bindings.ts +12 -0
- package/src/mcp-oauth.ts +26 -0
- package/src/organization-integration-policy.ts +235 -0
- package/src/organization-usage.ts +83 -0
- package/src/plugin-packages.ts +226 -42
- package/src/pr-review.ts +7 -69
- package/src/preference-registry-schema.ts +1 -0
- package/src/provision-roles.ts +50 -37
- package/src/retained-provider-commands.ts +286 -9
- package/src/retired-session-create-metadata.ts +16 -0
- package/src/runtime-posture.ts +217 -13
- package/src/schema.ts +104 -120
- package/src/session-admission-block.ts +29 -0
- package/src/session-attempt-writers.ts +48 -0
- package/src/session-control.ts +21 -5
- package/src/session-event-slices.ts +76 -26
- package/src/session-file-attachments.ts +66 -0
- package/src/session-goal-reports.ts +255 -0
- package/src/session-mcp-approval.ts +30 -0
- package/src/session-message-search.ts +348 -0
- package/src/session-queue-commands.ts +60 -36
- package/src/session-retry.ts +319 -0
- package/src/skill-config-migration.ts +11 -2
- package/src/skill-source-release-impact.ts +33 -0
- package/src/skill-source-release.ts +67 -26
- package/src/skills.ts +44 -6
- package/src/user-resource-authority.ts +0 -56
- package/dist/chunk-6T2GGYWE.js.map +0 -1
- package/dist/chunk-7HMK3B45.js.map +0 -1
- package/dist/chunk-DPONPYJD.js.map +0 -1
- package/dist/chunk-F73UKJY6.js.map +0 -1
- package/dist/chunk-T6YLHJLS.js.map +0 -1
- package/dist/host-mcp-binding-match.d.ts +0 -5
- package/dist/host-mcp-bindings.d.ts +0 -79
- package/dist/host-mcp-resolvers.d.ts +0 -47
- package/dist/host-mcp-task-authority.d.ts +0 -43
- package/dist/host-selection-identity.d.ts +0 -6
- package/dist/pack-components.d.ts +0 -83
- package/dist/pack-installations.d.ts +0 -86
- package/src/host-mcp-binding-match.ts +0 -34
- package/src/host-mcp-bindings.ts +0 -777
- package/src/host-mcp-resolvers.ts +0 -266
- package/src/host-mcp-task-authority.ts +0 -384
- package/src/host-selection-identity.ts +0 -29
- package/src/pack-components.ts +0 -980
- package/src/pack-installations.ts +0 -950
- /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
- /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
- /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
|
@@ -540,20 +540,32 @@ export async function readWorkspaceInsightsModelBundle(
|
|
|
540
540
|
from current_visible fact
|
|
541
541
|
), contribution_source_rows as (
|
|
542
542
|
select
|
|
543
|
-
|
|
544
|
-
contribution->>'
|
|
545
|
-
(contribution->>'
|
|
546
|
-
(contribution->>'
|
|
547
|
-
|
|
543
|
+
contribution.entry->>'source' as source,
|
|
544
|
+
(contribution.entry->>'items')::bigint as items,
|
|
545
|
+
(contribution.entry->>'utf8Bytes')::bigint as utf8_bytes,
|
|
546
|
+
(contribution.entry->>'estimatedTokens')::bigint as estimated_tokens,
|
|
547
|
+
-- The validated CHECK rejects duplicate named sources. Its SQL NULL
|
|
548
|
+
-- semantics still permit repeated null sources; count those only once
|
|
549
|
+
-- per fact without sorting all contribution rows by raw fact ID.
|
|
550
|
+
case when contribution.entry->>'source' is not null then true
|
|
551
|
+
else not exists (
|
|
552
|
+
select 1
|
|
553
|
+
from jsonb_array_elements(fact.context_contributions)
|
|
554
|
+
with ordinality earlier(entry, ordinal)
|
|
555
|
+
where earlier.ordinal < contribution.ordinal
|
|
556
|
+
and earlier.entry->>'source' is null
|
|
557
|
+
)
|
|
558
|
+
end as first_source
|
|
548
559
|
from current_visible fact
|
|
549
|
-
cross join lateral jsonb_array_elements(fact.context_contributions)
|
|
560
|
+
cross join lateral jsonb_array_elements(fact.context_contributions)
|
|
561
|
+
with ordinality contribution(entry, ordinal)
|
|
550
562
|
), contribution_rows as (
|
|
551
563
|
select
|
|
552
564
|
source,
|
|
553
565
|
sum(items)::bigint as items,
|
|
554
566
|
sum(utf8_bytes)::bigint as utf8_bytes,
|
|
555
567
|
sum(estimated_tokens)::bigint as estimated_tokens,
|
|
556
|
-
count(
|
|
568
|
+
count(*) filter (where first_source)::bigint as calls
|
|
557
569
|
from contribution_source_rows
|
|
558
570
|
group by source
|
|
559
571
|
)
|
|
@@ -86,6 +86,9 @@ function stringValue(row: JsonRecord, key: string): string {
|
|
|
86
86
|
* One read for the three existing usage windows. The event-type arrays are
|
|
87
87
|
* deliberately closed so the authority function can keep selective predicates
|
|
88
88
|
* inside its scan while all rollup math remains outside the billing path.
|
|
89
|
+
* Only the reused current window needs a second materialization: aggregate the
|
|
90
|
+
* single-use prior/month SRFs directly, since their volatile calls prevent CTE
|
|
91
|
+
* inlining even without an explicit MATERIALIZED keyword.
|
|
89
92
|
*/
|
|
90
93
|
export async function readWorkspaceInsightsUsageBundle(
|
|
91
94
|
db: Database,
|
|
@@ -103,28 +106,12 @@ export async function readWorkspaceInsightsUsageBundle(
|
|
|
103
106
|
with current_visible as materialized (
|
|
104
107
|
select usage_row.event_type, usage_row.quantity, usage_row.occurred_at,
|
|
105
108
|
usage_row.source_resource_id
|
|
106
|
-
from opengeni_private.
|
|
109
|
+
from opengeni_private.visible_workspace_insights_usage_projection(
|
|
107
110
|
${input.workspaceId},
|
|
108
111
|
${input.since.toISOString()}::timestamp with time zone,
|
|
109
112
|
${input.until.toISOString()}::timestamp with time zone,
|
|
110
113
|
array['model.cost', 'sandbox.warm_seconds']::text[]
|
|
111
114
|
) usage_row
|
|
112
|
-
), prior_visible as materialized (
|
|
113
|
-
select usage_row.event_type, usage_row.quantity
|
|
114
|
-
from opengeni_private.visible_workspace_insights_usage_events(
|
|
115
|
-
${input.workspaceId},
|
|
116
|
-
${input.priorSince.toISOString()}::timestamp with time zone,
|
|
117
|
-
${input.priorUntil.toISOString()}::timestamp with time zone,
|
|
118
|
-
array['model.cost', 'sandbox.warm_seconds']::text[]
|
|
119
|
-
) usage_row
|
|
120
|
-
), month_visible as materialized (
|
|
121
|
-
select usage_row.event_type, usage_row.quantity, usage_row.occurred_at
|
|
122
|
-
from opengeni_private.visible_workspace_insights_usage_events(
|
|
123
|
-
${input.workspaceId},
|
|
124
|
-
${input.monthSince.toISOString()}::timestamp with time zone,
|
|
125
|
-
'infinity'::timestamp with time zone,
|
|
126
|
-
array['model.tokens', 'agent_run.created']::text[]
|
|
127
|
-
) usage_row
|
|
128
115
|
), current_totals as (
|
|
129
116
|
select
|
|
130
117
|
coalesce(sum(usage_row.quantity) filter (
|
|
@@ -142,7 +129,12 @@ export async function readWorkspaceInsightsUsageBundle(
|
|
|
142
129
|
coalesce(sum(usage_row.quantity) filter (
|
|
143
130
|
where usage_row.event_type = 'sandbox.warm_seconds'
|
|
144
131
|
), 0) as prior_warm_seconds
|
|
145
|
-
from
|
|
132
|
+
from opengeni_private.visible_workspace_insights_usage_projection(
|
|
133
|
+
${input.workspaceId},
|
|
134
|
+
${input.priorSince.toISOString()}::timestamp with time zone,
|
|
135
|
+
${input.priorUntil.toISOString()}::timestamp with time zone,
|
|
136
|
+
array['model.cost', 'sandbox.warm_seconds']::text[]
|
|
137
|
+
) usage_row
|
|
146
138
|
), month_totals as (
|
|
147
139
|
select
|
|
148
140
|
coalesce(sum(usage_row.quantity) filter (
|
|
@@ -153,7 +145,12 @@ export async function readWorkspaceInsightsUsageBundle(
|
|
|
153
145
|
where usage_row.event_type = 'agent_run.created'
|
|
154
146
|
and usage_row.occurred_at > ${input.monthSince.toISOString()}::timestamp with time zone
|
|
155
147
|
), 0) as agent_runs_used
|
|
156
|
-
from
|
|
148
|
+
from opengeni_private.visible_workspace_insights_usage_projection(
|
|
149
|
+
${input.workspaceId},
|
|
150
|
+
${input.monthSince.toISOString()}::timestamp with time zone,
|
|
151
|
+
'infinity'::timestamp with time zone,
|
|
152
|
+
array['model.tokens', 'agent_run.created']::text[]
|
|
153
|
+
) usage_row
|
|
157
154
|
), bucket_rows as (
|
|
158
155
|
select
|
|
159
156
|
${bucket} as bucket,
|
|
@@ -8,7 +8,7 @@ import { effectiveCapabilityOwnerSql } from "./capability-components";
|
|
|
8
8
|
import * as schema from "./schema";
|
|
9
9
|
|
|
10
10
|
export type IntegrationFacetBindingOwner = {
|
|
11
|
-
kind: "direct" | "plugin" | "
|
|
11
|
+
kind: "direct" | "plugin" | "migration";
|
|
12
12
|
id: string;
|
|
13
13
|
removable: boolean;
|
|
14
14
|
};
|
|
@@ -358,12 +358,7 @@ async function loadIntegrationFacetBindingOwnersForBindings(
|
|
|
358
358
|
asc(schema.integrationFacetBindingOwners.ownerId),
|
|
359
359
|
);
|
|
360
360
|
for (const row of rows) {
|
|
361
|
-
if (
|
|
362
|
-
row.kind !== "direct" &&
|
|
363
|
-
row.kind !== "plugin" &&
|
|
364
|
-
row.kind !== "pack" &&
|
|
365
|
-
row.kind !== "migration"
|
|
366
|
-
) {
|
|
361
|
+
if (row.kind !== "direct" && row.kind !== "plugin" && row.kind !== "migration") {
|
|
367
362
|
throw new Error(`Unknown Integration instance owner kind: ${row.kind}`);
|
|
368
363
|
}
|
|
369
364
|
ownersByBindingId
|
|
@@ -78,7 +78,7 @@ export type IntegrationFacetRemovalResult = {
|
|
|
78
78
|
status: "not_configured" | "removed" | "retained_by_other_owners";
|
|
79
79
|
binding: IntegrationFacetBindingSummary | null;
|
|
80
80
|
remainingOwners: {
|
|
81
|
-
kind: "direct" | "plugin" | "
|
|
81
|
+
kind: "direct" | "plugin" | "migration";
|
|
82
82
|
id: string;
|
|
83
83
|
removable: boolean;
|
|
84
84
|
}[];
|
|
@@ -172,8 +172,20 @@ type IntegrationInstanceContext = {
|
|
|
172
172
|
integrationFacetId: string;
|
|
173
173
|
providerDomain: string;
|
|
174
174
|
connectionId: string | null;
|
|
175
|
+
apiProtocol: string | null;
|
|
176
|
+
definitionId: string | null;
|
|
177
|
+
definitionProvenance: string | null;
|
|
175
178
|
};
|
|
176
179
|
|
|
180
|
+
/** Trusted server callback, never part of a serialized mutation request. The
|
|
181
|
+
* caller must acquire its policy fence before invoking the facet operation.
|
|
182
|
+
* IDs/domains in this context identify installed records, not curated provenance.
|
|
183
|
+
*/
|
|
184
|
+
export type IntegrationFacetAcquisitionAuthorizer = (
|
|
185
|
+
tx: Database,
|
|
186
|
+
context: Readonly<IntegrationInstanceContext>,
|
|
187
|
+
) => Promise<void>;
|
|
188
|
+
|
|
177
189
|
export async function listIntegrationInstanceFacets(
|
|
178
190
|
db: Database,
|
|
179
191
|
workspaceId: string,
|
|
@@ -290,6 +302,7 @@ export async function configureIntegrationFacet(
|
|
|
290
302
|
config: Record<string, unknown>;
|
|
291
303
|
expectedVersion?: number;
|
|
292
304
|
idempotencyKey: string;
|
|
305
|
+
beforeAcquire?: IntegrationFacetAcquisitionAuthorizer;
|
|
293
306
|
},
|
|
294
307
|
): Promise<IntegrationFacetMutationResult> {
|
|
295
308
|
const requestDigest = integrationFacetConfigureRequestDigest(input);
|
|
@@ -298,6 +311,26 @@ export async function configureIntegrationFacet(
|
|
|
298
311
|
const definition = await requireFacetDefinition(tx, context, input.facetKey);
|
|
299
312
|
assertFacetConfig(input.config, definition.configSchema);
|
|
300
313
|
await requireFacetConnection(tx, input, context, definition.capabilities);
|
|
314
|
+
if (input.beforeAcquire) {
|
|
315
|
+
// Match upsertIntegrationFacetBinding's advisory -> row prefix before
|
|
316
|
+
// deciding whether this request adds authority. The upsert reuses both
|
|
317
|
+
// transaction locks, including the absent-row insertion fence.
|
|
318
|
+
await tx.execute(
|
|
319
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`integration-binding:${input.workspaceId}:${definition.id}:${input.instanceKey}`}, 0))`,
|
|
320
|
+
);
|
|
321
|
+
const existing = await loadFacetBinding(tx, context, input.instanceKey, definition.id, true);
|
|
322
|
+
const owner = facetOwner(input.capabilityId, input.instanceKey, input.facetKey);
|
|
323
|
+
const owners = existing ? await listIntegrationFacetBindingOwners(tx, existing.id) : [];
|
|
324
|
+
if (
|
|
325
|
+
!existing ||
|
|
326
|
+
existing.status !== "active" ||
|
|
327
|
+
existing.connectionId !== context.connectionId ||
|
|
328
|
+
stableJson(existing.config) !== stableJson(input.config) ||
|
|
329
|
+
!owners.some((current) => current.kind === owner.kind && current.id === owner.id)
|
|
330
|
+
) {
|
|
331
|
+
await input.beforeAcquire(tx, context);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
301
334
|
const result = await upsertIntegrationFacetBinding(tx, {
|
|
302
335
|
accountId: input.accountId,
|
|
303
336
|
workspaceId: input.workspaceId,
|
|
@@ -343,6 +376,7 @@ export async function setIntegrationFacetLifecycle(
|
|
|
343
376
|
action: "pause" | "resume";
|
|
344
377
|
expectedVersion: number;
|
|
345
378
|
idempotencyKey: string;
|
|
379
|
+
beforeAcquire?: IntegrationFacetAcquisitionAuthorizer;
|
|
346
380
|
},
|
|
347
381
|
): Promise<IntegrationFacetMutationResult> {
|
|
348
382
|
const requestDigest = sha256(
|
|
@@ -352,6 +386,7 @@ export async function setIntegrationFacetLifecycle(
|
|
|
352
386
|
workspaceId: undefined,
|
|
353
387
|
subjectId: undefined,
|
|
354
388
|
idempotencyKey: undefined,
|
|
389
|
+
beforeAcquire: undefined,
|
|
355
390
|
}),
|
|
356
391
|
);
|
|
357
392
|
return await withFacetOperation(db, input, requestDigest, "update", async (tx) => {
|
|
@@ -370,6 +405,9 @@ export async function setIntegrationFacetLifecycle(
|
|
|
370
405
|
}
|
|
371
406
|
await assertDirectFacetOwnership(tx, binding.id, input);
|
|
372
407
|
const targetStatus = input.action === "pause" ? "paused" : "active";
|
|
408
|
+
if (input.action === "resume" && binding.status !== "active") {
|
|
409
|
+
await input.beforeAcquire?.(tx, context);
|
|
410
|
+
}
|
|
373
411
|
let row = binding;
|
|
374
412
|
if (binding.status !== targetStatus) {
|
|
375
413
|
const [updated] = await tx
|
|
@@ -653,10 +691,7 @@ function capabilityComponentOwners(value: unknown): IntegrationFacetBindingOwner
|
|
|
653
691
|
const owner = recordValue(entry);
|
|
654
692
|
if (
|
|
655
693
|
!owner ||
|
|
656
|
-
(owner.kind !== "direct" &&
|
|
657
|
-
owner.kind !== "plugin" &&
|
|
658
|
-
owner.kind !== "pack" &&
|
|
659
|
-
owner.kind !== "migration") ||
|
|
694
|
+
(owner.kind !== "direct" && owner.kind !== "plugin" && owner.kind !== "migration") ||
|
|
660
695
|
typeof owner.id !== "string" ||
|
|
661
696
|
typeof owner.removable !== "boolean"
|
|
662
697
|
) {
|
|
@@ -709,6 +744,13 @@ async function loadIntegrationInstanceContext(
|
|
|
709
744
|
integrationFacetId: schema.integrationFacetDefinitions.integrationFacetId,
|
|
710
745
|
providerDomain: schema.capabilityIntegrationFacets.providerDomain,
|
|
711
746
|
connectionId: schema.integrationFacetBindings.connectionId,
|
|
747
|
+
apiProtocol: schema.capabilityApiFacets.protocol,
|
|
748
|
+
definitionId: sql<
|
|
749
|
+
string | null
|
|
750
|
+
>`${schema.capabilityPluginVersions.manifest} ->> 'definitionId'`,
|
|
751
|
+
definitionProvenance: sql<
|
|
752
|
+
string | null
|
|
753
|
+
>`${schema.capabilityPluginVersions.manifest} ->> 'definitionProvenance'`,
|
|
712
754
|
})
|
|
713
755
|
.from(schema.integrationFacetBindings)
|
|
714
756
|
.innerJoin(
|
|
@@ -740,6 +782,21 @@ async function loadIntegrationInstanceContext(
|
|
|
740
782
|
schema.capabilityPluginVersions,
|
|
741
783
|
eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
|
|
742
784
|
)
|
|
785
|
+
.leftJoin(
|
|
786
|
+
schema.capabilityApiFacets,
|
|
787
|
+
and(
|
|
788
|
+
eq(
|
|
789
|
+
schema.capabilityApiFacets.integrationFacetId,
|
|
790
|
+
schema.capabilityIntegrationFacets.facetId,
|
|
791
|
+
),
|
|
792
|
+
sql`exists (
|
|
793
|
+
select 1 from ${schema.capabilityFacetInstallations} api_installation
|
|
794
|
+
where api_installation.plugin_installation_id = ${schema.capabilityPluginInstallations.id}
|
|
795
|
+
and api_installation.facet_id = ${schema.capabilityApiFacets.facetId}
|
|
796
|
+
and api_installation.status = 'active'
|
|
797
|
+
)`,
|
|
798
|
+
),
|
|
799
|
+
)
|
|
743
800
|
.where(
|
|
744
801
|
and(
|
|
745
802
|
eq(schema.integrationFacetBindings.workspaceId, workspaceId),
|
|
@@ -759,7 +816,17 @@ async function loadIntegrationInstanceContext(
|
|
|
759
816
|
),
|
|
760
817
|
)
|
|
761
818
|
.limit(2);
|
|
762
|
-
if (lock)
|
|
819
|
+
if (lock)
|
|
820
|
+
query = query.for("update", {
|
|
821
|
+
of: [
|
|
822
|
+
schema.integrationFacetBindings,
|
|
823
|
+
schema.integrationFacetDefinitions,
|
|
824
|
+
schema.capabilityIntegrationFacets,
|
|
825
|
+
schema.capabilityFacetInstallations,
|
|
826
|
+
schema.capabilityPluginInstallations,
|
|
827
|
+
schema.capabilityPluginVersions,
|
|
828
|
+
],
|
|
829
|
+
}) as typeof query;
|
|
763
830
|
const rows = await query;
|
|
764
831
|
if (rows.length === 0) return null;
|
|
765
832
|
if (rows.length !== 1) throw new Error(`API Integration ${capabilityId} instance is ambiguous`);
|
package/src/knowledge-entries.ts
CHANGED
|
@@ -644,12 +644,19 @@ export async function inspectKnowledgeFilePreparation(
|
|
|
644
644
|
export async function completeKnowledgeFilePreparation(
|
|
645
645
|
db: Database,
|
|
646
646
|
context: KnowledgeContext,
|
|
647
|
-
input: {
|
|
647
|
+
input: {
|
|
648
|
+
fileId: string;
|
|
649
|
+
title: string;
|
|
650
|
+
content: string;
|
|
651
|
+
sourceVersion?: string;
|
|
652
|
+
purpose?: "evidence" | "reference";
|
|
653
|
+
},
|
|
648
654
|
) {
|
|
649
655
|
return KnowledgeFilePreparationResult.parse(
|
|
650
656
|
await knowledgeFilePreparationOperation(db, context, {
|
|
651
657
|
operation: "complete",
|
|
652
658
|
fileId: z.uuid().parse(input.fileId),
|
|
659
|
+
purpose: z.enum(["evidence", "reference"]).parse(input.purpose ?? "evidence"),
|
|
653
660
|
...(input.sourceVersion
|
|
654
661
|
? {
|
|
655
662
|
sourceVersion: z
|
|
@@ -2,7 +2,6 @@ import { and, eq, inArray, sql } from "drizzle-orm";
|
|
|
2
2
|
import { withWorkspaceRls, type Database } from "./database";
|
|
3
3
|
import * as schema from "./schema";
|
|
4
4
|
import { getExternalLinkTurnAuthorization } from "./external-link-work";
|
|
5
|
-
import { initiatorFromStorage } from "./turn-initiator";
|
|
6
5
|
|
|
7
6
|
/** Shared physical-attempt fence. Does not mutate goal/execution snapshots. */
|
|
8
7
|
export async function getLiveSessionAttemptTurn(
|
|
@@ -62,24 +61,3 @@ export async function getLiveSessionAttemptTurn(
|
|
|
62
61
|
return row.turn;
|
|
63
62
|
});
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
/** Host credential reads need provenance and live authority, not a runnable
|
|
67
|
-
* execution projection or a write to the turn's frozen goal snapshot. */
|
|
68
|
-
export async function getHostMcpLiveAttempt(
|
|
69
|
-
db: Database,
|
|
70
|
-
workspaceId: string,
|
|
71
|
-
sessionId: string,
|
|
72
|
-
attemptId: string,
|
|
73
|
-
) {
|
|
74
|
-
const row = await getLiveSessionAttemptTurn(db, workspaceId, sessionId, attemptId);
|
|
75
|
-
if (!row) return null;
|
|
76
|
-
return {
|
|
77
|
-
...row,
|
|
78
|
-
initiator: initiatorFromStorage(
|
|
79
|
-
row.initiatorKind,
|
|
80
|
-
row.initiatorSubjectId,
|
|
81
|
-
row.initiatorContext ?? {},
|
|
82
|
-
),
|
|
83
|
-
initiatorContext: row.initiatorContext ?? {},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
McpConnectionAccountBindings,
|
|
3
|
+
type McpConnectionAccountBinding,
|
|
4
|
+
} from "@opengeni/contracts";
|
|
5
|
+
|
|
6
|
+
/** NULL is a pre-account-routing receipt, not an empty accepted selection.
|
|
7
|
+
* Never default it to [] (or default [] to NULL) during replay/inheritance. */
|
|
8
|
+
export function parseAcceptedMcpAccountBindings(
|
|
9
|
+
value: unknown,
|
|
10
|
+
): McpConnectionAccountBinding[] | null {
|
|
11
|
+
return value == null ? null : McpConnectionAccountBindings.parse(value);
|
|
12
|
+
}
|
package/src/mcp-oauth.ts
CHANGED
|
@@ -177,6 +177,32 @@ export async function deleteMcpOAuthAuthorizationRequest(
|
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
export async function rebindMcpOAuthAuthorizationRequest(
|
|
181
|
+
db: Database,
|
|
182
|
+
input: {
|
|
183
|
+
requestHash: string;
|
|
184
|
+
subjectId: string;
|
|
185
|
+
accountId: string;
|
|
186
|
+
workspaceId: string;
|
|
187
|
+
permissions: AccessGrant["permissions"];
|
|
188
|
+
toolIdentities: Array<{ serverId: string; toolName: string }>;
|
|
189
|
+
},
|
|
190
|
+
): Promise<boolean> {
|
|
191
|
+
const [row] = await rawRows<{ request_hash: string }>(
|
|
192
|
+
db,
|
|
193
|
+
sql`update mcp_oauth_authorization_requests
|
|
194
|
+
set account_id = ${input.accountId},
|
|
195
|
+
workspace_id = ${input.workspaceId},
|
|
196
|
+
permissions = ${JSON.stringify(input.permissions)}::jsonb,
|
|
197
|
+
tool_identities = ${JSON.stringify(input.toolIdentities)}::jsonb
|
|
198
|
+
where request_hash = ${input.requestHash}
|
|
199
|
+
and subject_id = ${input.subjectId}
|
|
200
|
+
and expires_at > clock_timestamp()
|
|
201
|
+
returning request_hash`,
|
|
202
|
+
);
|
|
203
|
+
return Boolean(row);
|
|
204
|
+
}
|
|
205
|
+
|
|
180
206
|
export async function consumeMcpOAuthAuthorizationRequest(
|
|
181
207
|
db: Database,
|
|
182
208
|
input: {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OrganizationIntegrationPolicy,
|
|
3
|
+
UpdateOrganizationIntegrationPolicyRequest,
|
|
4
|
+
assertOrganizationIntegrationAllowed,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
6
|
+
import { sql } from "drizzle-orm";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import {
|
|
9
|
+
rawRows,
|
|
10
|
+
rlsContextForWorkspace,
|
|
11
|
+
setSubjectRlsContext,
|
|
12
|
+
withRlsContext,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "./database";
|
|
15
|
+
|
|
16
|
+
export type OrganizationIntegrationPolicyScope = { accountId: string };
|
|
17
|
+
export type OrganizationIntegrationAcquisitionScope = { accountId: string; workspaceId: string };
|
|
18
|
+
const policyScope = z.object({
|
|
19
|
+
accountId: z
|
|
20
|
+
.string()
|
|
21
|
+
.uuid()
|
|
22
|
+
.transform((value) => value.toLowerCase()),
|
|
23
|
+
});
|
|
24
|
+
const acquisitionScope = policyScope.extend({
|
|
25
|
+
workspaceId: z
|
|
26
|
+
.string()
|
|
27
|
+
.uuid()
|
|
28
|
+
.transform((value) => value.toLowerCase()),
|
|
29
|
+
});
|
|
30
|
+
/** Trusted application adapter MUST verify canonical human/local organization-admin
|
|
31
|
+
* provenance or accountScopedApiKeyWorkspaceAuthority (exact account + workspace:admin).
|
|
32
|
+
* This callback is a server dependency, never deserialized request data. The returned
|
|
33
|
+
* subject alone is NOT authentication. DB checks below are live authority fences only.
|
|
34
|
+
*/
|
|
35
|
+
export type AuthorizeOrganizationIntegrationPolicyAdministration = () => Promise<{
|
|
36
|
+
accountId: string;
|
|
37
|
+
subjectId: string;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
async function requireReadCommitted(tx: Database): Promise<void> {
|
|
41
|
+
const [isolation] = await rawRows<{ level: string }>(
|
|
42
|
+
tx,
|
|
43
|
+
sql`select current_setting('transaction_isolation') as level`,
|
|
44
|
+
);
|
|
45
|
+
if (isolation?.level !== "read committed") {
|
|
46
|
+
throw new Error("Organization integration policy requires read committed isolation");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function read(
|
|
51
|
+
tx: Database,
|
|
52
|
+
scope: OrganizationIntegrationPolicyScope,
|
|
53
|
+
): Promise<OrganizationIntegrationPolicy> {
|
|
54
|
+
const [row] = await rawRows<{ policy: unknown }>(
|
|
55
|
+
tx,
|
|
56
|
+
sql`select jsonb_build_object('mode', mode, 'allowedIntegrationKeys', allowed_integration_keys, 'revision', revision) as policy from organization_integration_policies where account_id = ${scope.accountId}::uuid`,
|
|
57
|
+
);
|
|
58
|
+
return OrganizationIntegrationPolicy.parse(row?.policy ?? {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Own only the subject setting this policy operation changes. The enclosing
|
|
62
|
+
* withRlsContext transaction/savepoint restores all settings on failure; on
|
|
63
|
+
* success it restores account/workspace only, so restore our subject explicitly.
|
|
64
|
+
* Do not run restoration after a failed SQL statement in an aborted transaction.
|
|
65
|
+
*/
|
|
66
|
+
async function withAdministratorSubject<T>(
|
|
67
|
+
tx: Database,
|
|
68
|
+
subjectId: string,
|
|
69
|
+
use: () => Promise<T>,
|
|
70
|
+
): Promise<T> {
|
|
71
|
+
const [previous] = await rawRows<{ subject: string | null }>(
|
|
72
|
+
tx,
|
|
73
|
+
sql`select current_setting('opengeni.subject_id', true) as subject`,
|
|
74
|
+
);
|
|
75
|
+
await setSubjectRlsContext(tx, subjectId);
|
|
76
|
+
const result = await use();
|
|
77
|
+
const expected = previous?.subject ?? "";
|
|
78
|
+
const [restored] = await rawRows<{ subject: string }>(
|
|
79
|
+
tx,
|
|
80
|
+
sql`select set_config('opengeni.subject_id', ${expected}, true) as subject`,
|
|
81
|
+
);
|
|
82
|
+
if (restored?.subject !== expected)
|
|
83
|
+
throw new Error("Integration policy subject scope was not restored");
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function getOrganizationIntegrationPolicy(
|
|
88
|
+
db: Database,
|
|
89
|
+
inputScope: OrganizationIntegrationPolicyScope,
|
|
90
|
+
authorize: AuthorizeOrganizationIntegrationPolicyAdministration,
|
|
91
|
+
): Promise<OrganizationIntegrationPolicy> {
|
|
92
|
+
const scope = policyScope.parse(inputScope);
|
|
93
|
+
const actor = await authorize();
|
|
94
|
+
if (actor.accountId.toLowerCase() !== scope.accountId || !actor.subjectId)
|
|
95
|
+
throw new Error("Canonical organization administrator required");
|
|
96
|
+
return withRlsContext(
|
|
97
|
+
db,
|
|
98
|
+
{ ...scope, workspaceId: null },
|
|
99
|
+
async (tx) => {
|
|
100
|
+
await requireReadCommitted(tx);
|
|
101
|
+
return withAdministratorSubject(tx, actor.subjectId, async () => {
|
|
102
|
+
// Live authority only: no policy or organization advisory lock on reads.
|
|
103
|
+
await tx.execute(
|
|
104
|
+
sql`select opengeni_private.assert_organization_integration_policy_administrator(${scope.accountId}::uuid, ${actor.subjectId})`,
|
|
105
|
+
);
|
|
106
|
+
return read(tx, scope);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
undefined,
|
|
110
|
+
"none",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Call before acquiring membership/workspace/credential locks in any enclosing
|
|
115
|
+
* transaction. The SQL operation owns policy-exclusive -> membership -> admin
|
|
116
|
+
* row order; it never locks a workspace and rejects acquisition-lock upgrades.
|
|
117
|
+
*/
|
|
118
|
+
export async function updateOrganizationIntegrationPolicy(
|
|
119
|
+
db: Database,
|
|
120
|
+
inputScope: OrganizationIntegrationPolicyScope,
|
|
121
|
+
input: UpdateOrganizationIntegrationPolicyRequest,
|
|
122
|
+
authorize: AuthorizeOrganizationIntegrationPolicyAdministration,
|
|
123
|
+
): Promise<OrganizationIntegrationPolicy> {
|
|
124
|
+
const scope = policyScope.parse(inputScope);
|
|
125
|
+
const request = UpdateOrganizationIntegrationPolicyRequest.parse(input);
|
|
126
|
+
const actor = await authorize();
|
|
127
|
+
if (actor.accountId.toLowerCase() !== scope.accountId || !actor.subjectId)
|
|
128
|
+
throw new Error("Canonical organization administrator required");
|
|
129
|
+
return withRlsContext(
|
|
130
|
+
db,
|
|
131
|
+
{ ...scope, workspaceId: null },
|
|
132
|
+
async (tx) => {
|
|
133
|
+
await requireReadCommitted(tx);
|
|
134
|
+
return withAdministratorSubject(tx, actor.subjectId, async () => {
|
|
135
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
136
|
+
tx,
|
|
137
|
+
sql`select opengeni_private.update_organization_integration_policy(${scope.accountId}::uuid, ${actor.subjectId}, ${JSON.stringify(request)}::jsonb) as result`,
|
|
138
|
+
);
|
|
139
|
+
return OrganizationIntegrationPolicy.parse(row?.result);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
undefined,
|
|
143
|
+
"none",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Internal serialization primitive, NOT acquisition authorization. Authenticate
|
|
148
|
+
* the caller normally. A callback may first return an exact completed receipt;
|
|
149
|
+
* before any new claim, uncommitted progress, or commit it MUST assert the trusted
|
|
150
|
+
* integration classification against the supplied runtime-frozen policy snapshot.
|
|
151
|
+
* Do not put network operations or retries inside this transaction.
|
|
152
|
+
*
|
|
153
|
+
* Requires read committed isolation. The shared policy fence precedes EVERY
|
|
154
|
+
* organization-membership, workspace/tenancy, and credential lock. In particular,
|
|
155
|
+
* persistProviderOAuthConnection may take the membership fence inside acquire.
|
|
156
|
+
* Policy writers take exclusive policy -> membership -> live administrator rows,
|
|
157
|
+
* and never workspace rows. Ordinary connection lifecycle writers do not take
|
|
158
|
+
* the policy fence. A caller passing an existing transaction must honor this
|
|
159
|
+
* prefix. Never administer policy inside or after an acquisition in that transaction:
|
|
160
|
+
* the SQL writer rejects shared-to-exclusive upgrades rather than risking deadlock.
|
|
161
|
+
* All final persistence must use tx, never a separate connection. No automatic
|
|
162
|
+
* retries: external effects cannot be rolled back. For slow external preparation,
|
|
163
|
+
* preflight separately and use this guard for final persistence. Existing execution,
|
|
164
|
+
* refresh, credential lifecycle, and cleanup do not belong behind this guard.
|
|
165
|
+
*/
|
|
166
|
+
export async function withOrganizationIntegrationPolicyFence<T>(
|
|
167
|
+
db: Database,
|
|
168
|
+
inputScope: OrganizationIntegrationAcquisitionScope,
|
|
169
|
+
run: (tx: Database, policy: OrganizationIntegrationPolicy) => Promise<T>,
|
|
170
|
+
): Promise<T> {
|
|
171
|
+
const requested = acquisitionScope.parse(inputScope);
|
|
172
|
+
// Non-locking authoritative resolution before any new lock; repeat under the
|
|
173
|
+
// policy fence below so a moved/deleted workspace cannot retain stale scope.
|
|
174
|
+
const resolved = await rlsContextForWorkspace(db, requested.workspaceId);
|
|
175
|
+
if (resolved.accountId !== requested.accountId)
|
|
176
|
+
throw new Error("Organization integration policy workspace scope invalid");
|
|
177
|
+
const scope = { accountId: resolved.accountId, workspaceId: requested.workspaceId };
|
|
178
|
+
return withRlsContext(
|
|
179
|
+
db,
|
|
180
|
+
scope,
|
|
181
|
+
async (tx) => {
|
|
182
|
+
await requireReadCommitted(tx);
|
|
183
|
+
await tx.execute(
|
|
184
|
+
sql`select pg_advisory_xact_lock_shared(hashtextextended(${`organization-integration-policy:${scope.accountId}`}, 0))`,
|
|
185
|
+
);
|
|
186
|
+
// No row lock here: the existing writer owns its canonical membership ->
|
|
187
|
+
// workspace prefix. All policy checks and callback writes share this tx.
|
|
188
|
+
const [workspace] = await rawRows(
|
|
189
|
+
tx,
|
|
190
|
+
sql`select id from workspaces where id = ${scope.workspaceId}::uuid and account_id = ${scope.accountId}::uuid`,
|
|
191
|
+
);
|
|
192
|
+
if (!workspace) throw new Error("Organization integration policy workspace scope invalid");
|
|
193
|
+
const policy = await read(tx, scope);
|
|
194
|
+
Object.freeze(policy.allowedIntegrationKeys);
|
|
195
|
+
Object.freeze(policy);
|
|
196
|
+
const result = await run(tx, policy);
|
|
197
|
+
const [finalWorkspace] = await rawRows(
|
|
198
|
+
tx,
|
|
199
|
+
sql`select id from workspaces where id = ${scope.workspaceId}::uuid and account_id = ${scope.accountId}::uuid for key share`,
|
|
200
|
+
);
|
|
201
|
+
if (!finalWorkspace)
|
|
202
|
+
throw new Error("Organization integration policy workspace scope changed before commit");
|
|
203
|
+
return result;
|
|
204
|
+
},
|
|
205
|
+
undefined,
|
|
206
|
+
"none",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** New acquisitions always assert before effects and before returning to commit.
|
|
211
|
+
* The immutable snapshot stays authoritative through commit: the shared fence
|
|
212
|
+
* excludes policy writers and the SQL writer rejects same-transaction upgrades.
|
|
213
|
+
* Receipt-aware callers instead use withOrganizationIntegrationPolicyFence and
|
|
214
|
+
* assert only after distinguishing completed replay from new workflow progress.
|
|
215
|
+
*/
|
|
216
|
+
export async function withOrganizationIntegrationAcquisition<T>(
|
|
217
|
+
db: Database,
|
|
218
|
+
inputScope: OrganizationIntegrationAcquisitionScope,
|
|
219
|
+
integrationKeys: readonly (string | null)[],
|
|
220
|
+
acquire: (tx: Database) => Promise<T>,
|
|
221
|
+
): Promise<T> {
|
|
222
|
+
// Snapshot classifications before the first await, including workspace lookup.
|
|
223
|
+
const keys = [...integrationKeys];
|
|
224
|
+
if (!keys.length)
|
|
225
|
+
throw new Error("Acquisition requires at least one trusted integration classification");
|
|
226
|
+
return withOrganizationIntegrationPolicyFence(db, inputScope, async (tx, policy) => {
|
|
227
|
+
const assert = () => {
|
|
228
|
+
for (const key of keys) assertOrganizationIntegrationAllowed(policy, key);
|
|
229
|
+
};
|
|
230
|
+
assert();
|
|
231
|
+
const result = await acquire(tx);
|
|
232
|
+
assert();
|
|
233
|
+
return result;
|
|
234
|
+
});
|
|
235
|
+
}
|