@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
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assertOrganizationIntegrationAllowed,
|
|
5
|
+
stableJson,
|
|
6
|
+
type IntegrationSource,
|
|
7
|
+
type McpServerConnectionRef,
|
|
8
|
+
} from "@opengeni/contracts";
|
|
4
9
|
import { and, asc, eq, inArray, ne, or, sql } from "drizzle-orm";
|
|
5
10
|
|
|
6
11
|
import {
|
|
@@ -16,6 +21,7 @@ import {
|
|
|
16
21
|
type Database,
|
|
17
22
|
} from "./database";
|
|
18
23
|
import { connectionScopeKey } from "./connection-scopes";
|
|
24
|
+
import { withOrganizationIntegrationPolicyFence } from "./organization-integration-policy";
|
|
19
25
|
import {
|
|
20
26
|
ensureIntegrationFacetDefinition,
|
|
21
27
|
integrationBindingKey,
|
|
@@ -184,7 +190,7 @@ export type ApiIntegrationRuntime = {
|
|
|
184
190
|
};
|
|
185
191
|
|
|
186
192
|
export type ApiIntegrationOwner = {
|
|
187
|
-
kind: "direct" | "plugin" | "
|
|
193
|
+
kind: "direct" | "plugin" | "migration";
|
|
188
194
|
id: string;
|
|
189
195
|
removable: boolean;
|
|
190
196
|
};
|
|
@@ -227,6 +233,25 @@ export class ApiIntegrationInstallationVersionConflictError extends Error {
|
|
|
227
233
|
export async function installApiIntegration(
|
|
228
234
|
db: Database,
|
|
229
235
|
input: InstallApiIntegrationInput,
|
|
236
|
+
): Promise<InstalledApiIntegration> {
|
|
237
|
+
// Inputs here are server-resolved immutable previews, not deserialized client
|
|
238
|
+
// provenance. Custom documents never gain curated permission by their URL.
|
|
239
|
+
const snapshot = structuredClone(input);
|
|
240
|
+
const key =
|
|
241
|
+
snapshot.definitionProvenance === "curated"
|
|
242
|
+
? snapshot.definitionId
|
|
243
|
+
: `custom:${snapshot.protocol}`;
|
|
244
|
+
return withOrganizationIntegrationPolicyFence(db, snapshot, (tx, policy) =>
|
|
245
|
+
installApiIntegrationInScope(tx, snapshot, () =>
|
|
246
|
+
assertOrganizationIntegrationAllowed(policy, key),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function installApiIntegrationInScope(
|
|
252
|
+
db: Database,
|
|
253
|
+
input: InstallApiIntegrationInput,
|
|
254
|
+
assertAcquisition: () => void,
|
|
230
255
|
): Promise<InstalledApiIntegration> {
|
|
231
256
|
assertInstallInput(input);
|
|
232
257
|
return await withRlsContext(
|
|
@@ -407,6 +432,12 @@ export async function installApiIntegration(
|
|
|
407
432
|
const displayName =
|
|
408
433
|
input.displayName?.trim() ||
|
|
409
434
|
(instanceKey === "default" ? input.name : `${input.name} — connected account`);
|
|
435
|
+
// Capture immutable definition identity before any ensure helper can
|
|
436
|
+
// repair it. A repair/addition is acquisition, not unchanged replay.
|
|
437
|
+
const previousDefinitions = await tx
|
|
438
|
+
.select()
|
|
439
|
+
.from(schema.integrationFacetDefinitions)
|
|
440
|
+
.where(eq(schema.integrationFacetDefinitions.integrationFacetId, integrationFacet.id));
|
|
410
441
|
const toolsFacet = await ensureIntegrationFacetDefinition(tx as unknown as Database, {
|
|
411
442
|
integrationFacetId: integrationFacet.id,
|
|
412
443
|
facetKey: "tools",
|
|
@@ -536,6 +567,116 @@ export async function installApiIntegration(
|
|
|
536
567
|
const approvalRequiredTools = input.revision.tools
|
|
537
568
|
.filter((tool) => selectedTools.includes(tool.id) && tool.approvalMode === "ask")
|
|
538
569
|
.map((tool) => tool.id);
|
|
570
|
+
const nextConfig = {
|
|
571
|
+
baseServerId: input.serverId,
|
|
572
|
+
allowedTools: selectedTools,
|
|
573
|
+
requireApproval: approvalRequiredTools,
|
|
574
|
+
connectionKind: connection?.kind ?? null,
|
|
575
|
+
subjectScope: connection?.subjectId ? "subject" : connection ? "workspace" : "none",
|
|
576
|
+
};
|
|
577
|
+
// This is the same advisory -> row prefix used by the binding upsert.
|
|
578
|
+
// All preceding ensure writes are local to this transaction and roll
|
|
579
|
+
// back if admission below denies a new acquisition.
|
|
580
|
+
await tx.execute(
|
|
581
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`integration-binding:${input.workspaceId}:${runtimeServerId}`}, 0))`,
|
|
582
|
+
);
|
|
583
|
+
const [previousBinding] = await tx
|
|
584
|
+
.select()
|
|
585
|
+
.from(schema.integrationFacetBindings)
|
|
586
|
+
.where(
|
|
587
|
+
and(
|
|
588
|
+
eq(schema.integrationFacetBindings.workspaceId, input.workspaceId),
|
|
589
|
+
eq(schema.integrationFacetBindings.runtimeKey, runtimeServerId),
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
.for("update")
|
|
593
|
+
.limit(1);
|
|
594
|
+
const bindingOwners = previousBinding
|
|
595
|
+
? await listIntegrationFacetBindingOwners(tx as unknown as Database, previousBinding.id)
|
|
596
|
+
: [];
|
|
597
|
+
const componentOwners = await tx
|
|
598
|
+
.select()
|
|
599
|
+
.from(schema.capabilityComponentOwners)
|
|
600
|
+
.where(
|
|
601
|
+
inArray(schema.capabilityComponentOwners.facetInstallationId, [
|
|
602
|
+
integrationFacetInstallation.id,
|
|
603
|
+
apiFacetInstallation.id,
|
|
604
|
+
]),
|
|
605
|
+
);
|
|
606
|
+
const [storedSpec] = await tx
|
|
607
|
+
.select()
|
|
608
|
+
.from(schema.integrationSpecRevisions)
|
|
609
|
+
.where(
|
|
610
|
+
and(
|
|
611
|
+
eq(schema.integrationSpecRevisions.apiFacetId, apiFacet.id),
|
|
612
|
+
eq(schema.integrationSpecRevisions.status, "active"),
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
.limit(1);
|
|
616
|
+
const [storedAuthority] = await tx
|
|
617
|
+
.select()
|
|
618
|
+
.from(schema.capabilityIntegrationFacets)
|
|
619
|
+
.where(eq(schema.capabilityIntegrationFacets.facetId, integrationFacet.id))
|
|
620
|
+
.limit(1);
|
|
621
|
+
const [storedApi] = await tx
|
|
622
|
+
.select()
|
|
623
|
+
.from(schema.capabilityApiFacets)
|
|
624
|
+
.where(eq(schema.capabilityApiFacets.facetId, apiFacet.id))
|
|
625
|
+
.limit(1);
|
|
626
|
+
const sameOwner = (candidate: { kind: string; id: string; removable: boolean }) =>
|
|
627
|
+
candidate.kind === owner.kind &&
|
|
628
|
+
candidate.id === owner.id &&
|
|
629
|
+
candidate.removable === owner.removable;
|
|
630
|
+
const unchangedAuthority =
|
|
631
|
+
existingPluginInstallation?.status === "active" &&
|
|
632
|
+
existingPluginInstallation.pluginVersionId === pluginVersion.id &&
|
|
633
|
+
!integrationFacetInstallationResult.changed &&
|
|
634
|
+
!apiFacetInstallationResult.changed &&
|
|
635
|
+
previousBinding?.status === "active" &&
|
|
636
|
+
previousBinding.bindingKey === instanceKey &&
|
|
637
|
+
previousBinding.integrationFacetInstallationId === integrationFacetInstallation.id &&
|
|
638
|
+
previousBinding.facetDefinitionId === toolsFacet.id &&
|
|
639
|
+
previousBinding.connectionId === (input.connectionId ?? null) &&
|
|
640
|
+
isApiIntegrationPermissionReduction(previousBinding.config, nextConfig) &&
|
|
641
|
+
bindingOwners.some(sameOwner) &&
|
|
642
|
+
[integrationFacetInstallation.id, apiFacetInstallation.id].every((id) =>
|
|
643
|
+
componentOwners.some(
|
|
644
|
+
(candidate) =>
|
|
645
|
+
candidate.facetInstallationId === id &&
|
|
646
|
+
sameOwner({
|
|
647
|
+
kind: candidate.ownerKind,
|
|
648
|
+
id: candidate.ownerId,
|
|
649
|
+
removable: candidate.removable,
|
|
650
|
+
}),
|
|
651
|
+
),
|
|
652
|
+
) &&
|
|
653
|
+
stableJson(storedSpec?.spec) === stableJson(input.revision) &&
|
|
654
|
+
storedApi?.protocol === input.protocol &&
|
|
655
|
+
storedApi.baseUrl === input.baseUrl &&
|
|
656
|
+
storedApi.specSourceUrl === (input.sourceUrl ?? null) &&
|
|
657
|
+
storedAuthority?.providerDomain === input.providerDomain &&
|
|
658
|
+
stableJson(storedApi?.authScheme) === stableJson(input.authScheme ?? {}) &&
|
|
659
|
+
stableJson(storedAuthority?.requiredScopes) ===
|
|
660
|
+
stableJson(normalizedStrings(input.requiredScopes ?? [], 256)) &&
|
|
661
|
+
storedAuthority?.ownership ===
|
|
662
|
+
(input.ownership ?? (connection?.subjectId ? "subject" : "workspace")) &&
|
|
663
|
+
previousDefinitions.some(
|
|
664
|
+
(definition) =>
|
|
665
|
+
definition.id === toolsFacet.id &&
|
|
666
|
+
definition.kind === toolsFacet.kind &&
|
|
667
|
+
stableJson(definition.configSchema) === stableJson(toolsFacet.configSchema) &&
|
|
668
|
+
stableJson(definition.capabilities) === stableJson(toolsFacet.capabilities),
|
|
669
|
+
) &&
|
|
670
|
+
(input.facetDefinitions ?? []).every((definition) =>
|
|
671
|
+
previousDefinitions.some(
|
|
672
|
+
(old) =>
|
|
673
|
+
old.facetKey === definition.facetKey &&
|
|
674
|
+
old.kind === definition.kind &&
|
|
675
|
+
stableJson(old.configSchema) === stableJson(definition.configSchema) &&
|
|
676
|
+
stableJson(old.capabilities) === stableJson(definition.capabilities),
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
if (!unchangedAuthority) assertAcquisition();
|
|
539
680
|
const bindingResult = await upsertIntegrationFacetBinding(tx as unknown as Database, {
|
|
540
681
|
accountId: input.accountId,
|
|
541
682
|
workspaceId: input.workspaceId,
|
|
@@ -545,13 +686,7 @@ export async function installApiIntegration(
|
|
|
545
686
|
displayName,
|
|
546
687
|
runtimeKey: runtimeServerId,
|
|
547
688
|
connectionId: input.connectionId ?? null,
|
|
548
|
-
config:
|
|
549
|
-
baseServerId: input.serverId,
|
|
550
|
-
allowedTools: selectedTools,
|
|
551
|
-
requireApproval: approvalRequiredTools,
|
|
552
|
-
connectionKind: connection?.kind ?? null,
|
|
553
|
-
subjectScope: connection?.subjectId ? "subject" : connection ? "workspace" : "none",
|
|
554
|
-
},
|
|
689
|
+
config: nextConfig,
|
|
555
690
|
createdBySubjectId: input.subjectId,
|
|
556
691
|
owner,
|
|
557
692
|
...(input.expectedInstanceVersion !== undefined
|
|
@@ -812,12 +947,6 @@ export async function listInstalledApiIntegrationServerIdsForDelegations(
|
|
|
812
947
|
return [...byServerId];
|
|
813
948
|
}
|
|
814
949
|
|
|
815
|
-
/**
|
|
816
|
-
* Read installed API Integrations from a transaction that already carries the
|
|
817
|
-
* workspace RLS context. This is intentionally separate from the public
|
|
818
|
-
* wrapper so compound Plugin/Pack lifecycle transactions never open a nested
|
|
819
|
-
* transaction or lose their advisory locks.
|
|
820
|
-
*/
|
|
821
950
|
export async function listInstalledApiIntegrationsInRlsContext(
|
|
822
951
|
scopedDb: Database,
|
|
823
952
|
workspaceId: string,
|
|
@@ -1078,6 +1207,133 @@ export async function getApiIntegrationUninstallPreview(
|
|
|
1078
1207
|
});
|
|
1079
1208
|
}
|
|
1080
1209
|
|
|
1210
|
+
/** No discovery or new authority: recover only an exact, currently visible and
|
|
1211
|
+
* directly owned installed source. The installer must still lock and recheck
|
|
1212
|
+
* the snapshot before committing; this read is not installation admission.
|
|
1213
|
+
*/
|
|
1214
|
+
export async function getApiIntegrationReconciliationSnapshot(
|
|
1215
|
+
db: Database,
|
|
1216
|
+
input: {
|
|
1217
|
+
accountId: string;
|
|
1218
|
+
workspaceId: string;
|
|
1219
|
+
subjectId: string;
|
|
1220
|
+
source: IntegrationSource;
|
|
1221
|
+
connectionId?: string;
|
|
1222
|
+
instanceKey?: string;
|
|
1223
|
+
expectedRevisionId: string;
|
|
1224
|
+
expectedContentSha256: string;
|
|
1225
|
+
allowedTools?: string[];
|
|
1226
|
+
},
|
|
1227
|
+
): Promise<{
|
|
1228
|
+
runtime: ApiIntegrationRuntime;
|
|
1229
|
+
baseServerId: string;
|
|
1230
|
+
name: string;
|
|
1231
|
+
provider: string | null;
|
|
1232
|
+
requiredScopes: string[];
|
|
1233
|
+
} | null> {
|
|
1234
|
+
return withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (tx) => {
|
|
1235
|
+
const instanceKey = integrationBindingKey(input.connectionId, input.instanceKey);
|
|
1236
|
+
const candidates = (
|
|
1237
|
+
await listInstalledApiIntegrationsInRlsContext(tx, input.workspaceId, input.subjectId)
|
|
1238
|
+
).filter((runtime) => {
|
|
1239
|
+
if (
|
|
1240
|
+
runtime.instanceKey !== instanceKey ||
|
|
1241
|
+
(runtime.connectionRef?.connectionId ?? null) !== (input.connectionId ?? null) ||
|
|
1242
|
+
runtime.revision.id !== input.expectedRevisionId ||
|
|
1243
|
+
runtime.revision.contentSha256 !== input.expectedContentSha256 ||
|
|
1244
|
+
!(input.allowedTools ?? runtime.revision.tools.map((tool) => tool.id)).every((tool) =>
|
|
1245
|
+
runtime.allowedTools.includes(tool),
|
|
1246
|
+
)
|
|
1247
|
+
)
|
|
1248
|
+
return false;
|
|
1249
|
+
const source = input.source;
|
|
1250
|
+
if (source.kind === "definition")
|
|
1251
|
+
return (
|
|
1252
|
+
runtime.definitionProvenance === "curated" && runtime.definitionId === source.definitionId
|
|
1253
|
+
);
|
|
1254
|
+
if (runtime.definitionProvenance !== "workspace") return false;
|
|
1255
|
+
if (source.kind === "graphql")
|
|
1256
|
+
return runtime.protocol === "graphql" && source.endpoint === runtime.sourceUrl;
|
|
1257
|
+
return (
|
|
1258
|
+
(source.kind === "auto" || runtime.protocol === "openapi") &&
|
|
1259
|
+
source.url === runtime.sourceUrl &&
|
|
1260
|
+
(source.baseUrl === undefined || source.baseUrl === runtime.baseUrl)
|
|
1261
|
+
);
|
|
1262
|
+
});
|
|
1263
|
+
if (candidates.length !== 1) return null;
|
|
1264
|
+
const runtime = candidates[0]!;
|
|
1265
|
+
const owners = await listIntegrationFacetBindingOwners(tx, runtime.instanceId);
|
|
1266
|
+
if (
|
|
1267
|
+
!owners.some(
|
|
1268
|
+
(owner) => owner.kind === "direct" && owner.id === runtime.capabilityId && owner.removable,
|
|
1269
|
+
)
|
|
1270
|
+
)
|
|
1271
|
+
return null;
|
|
1272
|
+
const [stored] = await tx
|
|
1273
|
+
.select({
|
|
1274
|
+
manifest: schema.capabilityPluginVersions.manifest,
|
|
1275
|
+
name: schema.capabilityPlugins.name,
|
|
1276
|
+
requiredScopes: schema.capabilityIntegrationFacets.requiredScopes,
|
|
1277
|
+
})
|
|
1278
|
+
.from(schema.capabilityPluginInstallations)
|
|
1279
|
+
.innerJoin(
|
|
1280
|
+
schema.capabilityPlugins,
|
|
1281
|
+
eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
|
|
1282
|
+
)
|
|
1283
|
+
.innerJoin(
|
|
1284
|
+
schema.capabilityPluginVersions,
|
|
1285
|
+
eq(
|
|
1286
|
+
schema.capabilityPluginVersions.id,
|
|
1287
|
+
schema.capabilityPluginInstallations.pluginVersionId,
|
|
1288
|
+
),
|
|
1289
|
+
)
|
|
1290
|
+
.innerJoin(
|
|
1291
|
+
schema.integrationFacetBindings,
|
|
1292
|
+
eq(schema.integrationFacetBindings.id, runtime.instanceId),
|
|
1293
|
+
)
|
|
1294
|
+
.innerJoin(
|
|
1295
|
+
schema.integrationFacetDefinitions,
|
|
1296
|
+
eq(
|
|
1297
|
+
schema.integrationFacetDefinitions.id,
|
|
1298
|
+
schema.integrationFacetBindings.facetDefinitionId,
|
|
1299
|
+
),
|
|
1300
|
+
)
|
|
1301
|
+
.innerJoin(
|
|
1302
|
+
schema.capabilityIntegrationFacets,
|
|
1303
|
+
eq(
|
|
1304
|
+
schema.capabilityIntegrationFacets.facetId,
|
|
1305
|
+
schema.integrationFacetDefinitions.integrationFacetId,
|
|
1306
|
+
),
|
|
1307
|
+
)
|
|
1308
|
+
.where(
|
|
1309
|
+
and(
|
|
1310
|
+
eq(schema.capabilityPluginInstallations.id, runtime.pluginInstallationId),
|
|
1311
|
+
eq(schema.capabilityPluginInstallations.accountId, input.accountId),
|
|
1312
|
+
eq(schema.capabilityPluginInstallations.workspaceId, input.workspaceId),
|
|
1313
|
+
eq(schema.capabilityPluginInstallations.status, "active"),
|
|
1314
|
+
),
|
|
1315
|
+
)
|
|
1316
|
+
.limit(1);
|
|
1317
|
+
const manifest = objectValue(stored?.manifest);
|
|
1318
|
+
const baseServerId = stringValue(manifest.serverId);
|
|
1319
|
+
if (
|
|
1320
|
+
!stored ||
|
|
1321
|
+
!baseServerId ||
|
|
1322
|
+
manifest.revisionId !== runtime.revision.id ||
|
|
1323
|
+
manifest.contentSha256 !== runtime.revision.contentSha256 ||
|
|
1324
|
+
manifest.capabilityId !== runtime.capabilityId
|
|
1325
|
+
)
|
|
1326
|
+
return null;
|
|
1327
|
+
return {
|
|
1328
|
+
runtime,
|
|
1329
|
+
baseServerId,
|
|
1330
|
+
name: stored.name,
|
|
1331
|
+
provider: stringValue(manifest.provider) ?? null,
|
|
1332
|
+
requiredScopes: stored.requiredScopes,
|
|
1333
|
+
};
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1081
1337
|
export async function uninstallApiIntegration(
|
|
1082
1338
|
db: Database,
|
|
1083
1339
|
input: {
|
|
@@ -1631,12 +1887,7 @@ async function integrationOwners(
|
|
|
1631
1887
|
);
|
|
1632
1888
|
const owners = new Map<string, ApiIntegrationOwner>();
|
|
1633
1889
|
for (const row of rows) {
|
|
1634
|
-
if (
|
|
1635
|
-
row.kind !== "direct" &&
|
|
1636
|
-
row.kind !== "plugin" &&
|
|
1637
|
-
row.kind !== "pack" &&
|
|
1638
|
-
row.kind !== "migration"
|
|
1639
|
-
) {
|
|
1890
|
+
if (row.kind !== "direct" && row.kind !== "plugin" && row.kind !== "migration") {
|
|
1640
1891
|
throw new Error(`Unknown API Integration owner kind: ${row.kind}`);
|
|
1641
1892
|
}
|
|
1642
1893
|
const key = `${row.kind}\0${row.id}`;
|
|
@@ -1703,6 +1954,35 @@ function selectedToolIds(input: InstallApiIntegrationInput): string[] {
|
|
|
1703
1954
|
return selected;
|
|
1704
1955
|
}
|
|
1705
1956
|
|
|
1957
|
+
function isApiIntegrationPermissionReduction(
|
|
1958
|
+
previous: Record<string, unknown>,
|
|
1959
|
+
next: Record<string, unknown>,
|
|
1960
|
+
): boolean {
|
|
1961
|
+
const {
|
|
1962
|
+
allowedTools: previousTools,
|
|
1963
|
+
requireApproval: previousApproval,
|
|
1964
|
+
...previousIdentity
|
|
1965
|
+
} = previous;
|
|
1966
|
+
const { allowedTools: nextTools, requireApproval: nextApproval, ...nextIdentity } = next;
|
|
1967
|
+
const before = stringArray(previousTools);
|
|
1968
|
+
const after = stringArray(nextTools);
|
|
1969
|
+
const requiredBefore = stringArray(previousApproval);
|
|
1970
|
+
const requiredAfter = stringArray(nextApproval);
|
|
1971
|
+
return (
|
|
1972
|
+
before !== undefined &&
|
|
1973
|
+
after !== undefined &&
|
|
1974
|
+
requiredAfter !== undefined &&
|
|
1975
|
+
(previousApproval === true || requiredBefore !== undefined) &&
|
|
1976
|
+
stableJson(previousIdentity) === stableJson(nextIdentity) &&
|
|
1977
|
+
after.every(
|
|
1978
|
+
(tool) =>
|
|
1979
|
+
before.includes(tool) &&
|
|
1980
|
+
(!(previousApproval === true || requiredBefore?.includes(tool)) ||
|
|
1981
|
+
requiredAfter.includes(tool)),
|
|
1982
|
+
)
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1706
1986
|
function storedRevision(value: Record<string, unknown>): StoredApiIntegrationRevision {
|
|
1707
1987
|
const protocol = value.protocol;
|
|
1708
1988
|
const tools = value.tools;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { withWorkspaceRls, type Database } from "./database";
|
|
3
|
+
|
|
4
|
+
/** Retain the installation row through the caller's outer transaction. This is
|
|
5
|
+
* a no-write reconciliation read; it never activates or alters an installation.
|
|
6
|
+
* Acquisition callers must already hold the organization policy fence. */
|
|
7
|
+
export async function withLockedCapabilityInstallation<T>(
|
|
8
|
+
db: Database,
|
|
9
|
+
workspaceId: string,
|
|
10
|
+
capabilityId: string,
|
|
11
|
+
read: (tx: Database) => Promise<T>,
|
|
12
|
+
) {
|
|
13
|
+
return withWorkspaceRls(db, workspaceId, async (tx) => {
|
|
14
|
+
await tx.execute(sql`select id from capability_installations
|
|
15
|
+
where workspace_id = ${workspaceId}::uuid and capability_id = ${capabilityId}
|
|
16
|
+
for update`);
|
|
17
|
+
return read(tx);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Durable logical-turn state; checks and quota/account signals never spend this budget. */
|
|
2
|
+
export const CODEX_CAPACITY_RECOVERY_KEY = "codexCapacityRecoveryV1";
|
|
3
|
+
export const CODEX_CAPACITY_FALSE_RESUMPTION_LIMIT = 10;
|
|
4
|
+
|
|
5
|
+
export type CodexCapacityRecovery = {
|
|
6
|
+
falseResumptions: number;
|
|
7
|
+
resumeGeneration: number | null;
|
|
8
|
+
retryNotBefore: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function readCodexCapacityRecovery(
|
|
12
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
13
|
+
): CodexCapacityRecovery {
|
|
14
|
+
const value = metadata?.[CODEX_CAPACITY_RECOVERY_KEY];
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return { falseResumptions: 0, resumeGeneration: null, retryNotBefore: null };
|
|
17
|
+
}
|
|
18
|
+
const state = value as CodexCapacityRecovery | null;
|
|
19
|
+
if (
|
|
20
|
+
!state ||
|
|
21
|
+
!Number.isSafeInteger(state.falseResumptions) ||
|
|
22
|
+
state.falseResumptions < 0 ||
|
|
23
|
+
state.falseResumptions > CODEX_CAPACITY_FALSE_RESUMPTION_LIMIT ||
|
|
24
|
+
(state.resumeGeneration !== null &&
|
|
25
|
+
(!Number.isSafeInteger(state.resumeGeneration) || state.resumeGeneration < 1)) ||
|
|
26
|
+
(state.retryNotBefore !== null &&
|
|
27
|
+
(typeof state.retryNotBefore !== "string" ||
|
|
28
|
+
!Number.isFinite(Date.parse(state.retryNotBefore))))
|
|
29
|
+
) {
|
|
30
|
+
throw new Error("Invalid persisted Codex capacity recovery state");
|
|
31
|
+
}
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Equal jitter, 30–60 seconds initially, capped at 15 minutes. Persist the chosen deadline. */
|
|
36
|
+
export function codexFalseResumptionBackoffMs(count: number, random = Math.random()): number {
|
|
37
|
+
const cap = Math.min(60_000 * 2 ** Math.max(0, Math.min(count - 1, 4)), 900_000);
|
|
38
|
+
return Math.floor(cap * (0.5 + Math.max(0, Math.min(random, 1)) / 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearCodexCapacityRecovery(
|
|
42
|
+
metadata: Record<string, unknown>,
|
|
43
|
+
): Record<string, unknown> {
|
|
44
|
+
const next = { ...metadata };
|
|
45
|
+
delete next[CODEX_CAPACITY_RECOVERY_KEY];
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Refusal-specific, ordered recovery evidence; never erase the failure ledger. */
|
|
2
|
+
export function unresolvedCodexCredentialFailures(
|
|
3
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
4
|
+
accounts: readonly {
|
|
5
|
+
id: string;
|
|
6
|
+
status: string;
|
|
7
|
+
exhaustedUntil: Date | null;
|
|
8
|
+
exhaustedKind: string | null;
|
|
9
|
+
exhaustedRevision?: number;
|
|
10
|
+
credentialVersion?: number;
|
|
11
|
+
}[],
|
|
12
|
+
now = new Date(),
|
|
13
|
+
): string[] {
|
|
14
|
+
const failedIds = Array.isArray(metadata?.codexCredentialFailedIds)
|
|
15
|
+
? metadata.codexCredentialFailedIds.filter(
|
|
16
|
+
(id): id is string => typeof id === "string" && id.length > 0,
|
|
17
|
+
)
|
|
18
|
+
: [];
|
|
19
|
+
const rawRevisions = metadata?.codexCredentialFailureCooldownRevisions;
|
|
20
|
+
const revisions =
|
|
21
|
+
rawRevisions && typeof rawRevisions === "object" && !Array.isArray(rawRevisions)
|
|
22
|
+
? (rawRevisions as Record<string, unknown>)
|
|
23
|
+
: null;
|
|
24
|
+
const rawEvidence = metadata?.codexCredentialFailureEvidenceV1;
|
|
25
|
+
const evidence =
|
|
26
|
+
rawEvidence && typeof rawEvidence === "object" && !Array.isArray(rawEvidence)
|
|
27
|
+
? (rawEvidence as Record<string, unknown>)
|
|
28
|
+
: null;
|
|
29
|
+
return [...new Set(failedIds)].filter((id) => {
|
|
30
|
+
const account = accounts.find((candidate) => candidate.id === id);
|
|
31
|
+
const revision = account?.exhaustedRevision;
|
|
32
|
+
const baseline = revisions?.[id];
|
|
33
|
+
const rawReceipt = evidence?.[id];
|
|
34
|
+
const receipt =
|
|
35
|
+
rawReceipt && typeof rawReceipt === "object" && !Array.isArray(rawReceipt)
|
|
36
|
+
? (rawReceipt as Record<string, unknown>)
|
|
37
|
+
: null;
|
|
38
|
+
if (
|
|
39
|
+
rawReceipt !== undefined &&
|
|
40
|
+
(!receipt || !["quota", "rate_limit", "status"].includes(String(receipt.kind)))
|
|
41
|
+
)
|
|
42
|
+
return true;
|
|
43
|
+
// The earlier atomic quarantine writer used explicit null only for status
|
|
44
|
+
// refusals (it wrote needs_relogin/error in the same transaction). Seeing
|
|
45
|
+
// active again therefore proves a later repair. Missing ID-only evidence
|
|
46
|
+
// is different and remains excluded. New receipts additionally fence the
|
|
47
|
+
// repaired credential version below.
|
|
48
|
+
if (!receipt && baseline === null && account?.status === "active") return false;
|
|
49
|
+
if (account?.status === "active" && receipt?.kind === "status") {
|
|
50
|
+
return !(
|
|
51
|
+
typeof receipt.credentialVersion === "number" &&
|
|
52
|
+
Number.isSafeInteger(receipt.credentialVersion) &&
|
|
53
|
+
receipt.credentialVersion >= 1 &&
|
|
54
|
+
typeof account.credentialVersion === "number" &&
|
|
55
|
+
Number.isSafeInteger(account.credentialVersion) &&
|
|
56
|
+
account.credentialVersion > receipt.credentialVersion
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
account?.status === "active" &&
|
|
61
|
+
((receipt?.kind === "rate_limit" &&
|
|
62
|
+
typeof receipt.cooldownRevision === "number" &&
|
|
63
|
+
Number.isSafeInteger(receipt.cooldownRevision) &&
|
|
64
|
+
receipt.cooldownRevision >= 1 &&
|
|
65
|
+
typeof revision === "number" &&
|
|
66
|
+
revision >= receipt.cooldownRevision) ||
|
|
67
|
+
(!receipt &&
|
|
68
|
+
typeof baseline === "number" &&
|
|
69
|
+
Number.isSafeInteger(baseline) &&
|
|
70
|
+
baseline >= 1 &&
|
|
71
|
+
revision === baseline)) &&
|
|
72
|
+
typeof revision === "number" &&
|
|
73
|
+
Number.isSafeInteger(revision) &&
|
|
74
|
+
account.exhaustedKind === "rate_limit" &&
|
|
75
|
+
account.exhaustedUntil !== null &&
|
|
76
|
+
account.exhaustedUntil.getTime() <= now.getTime()
|
|
77
|
+
) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// Quota recovery needs an ordered explicit clear, including older numeric receipts.
|
|
81
|
+
return !(
|
|
82
|
+
account?.status === "active" &&
|
|
83
|
+
account.exhaustedUntil === null &&
|
|
84
|
+
account.exhaustedKind === null &&
|
|
85
|
+
typeof baseline === "number" &&
|
|
86
|
+
Number.isSafeInteger(baseline) &&
|
|
87
|
+
baseline >= 1 &&
|
|
88
|
+
typeof revision === "number" &&
|
|
89
|
+
Number.isSafeInteger(revision) &&
|
|
90
|
+
revision > baseline
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
}
|
package/src/connect-attempts.ts
CHANGED
|
@@ -98,6 +98,45 @@ function validateOperation(id: string, digest: string): void {
|
|
|
98
98
|
|
|
99
99
|
/** Caller must establish live workspace/actor authority before this storage
|
|
100
100
|
* seam. RLS scope is a consistency boundary, not proof of authentication. */
|
|
101
|
+
type ConnectBeginIdentity = {
|
|
102
|
+
idempotencyKey: string;
|
|
103
|
+
requestDigest: string;
|
|
104
|
+
returnUrl: string;
|
|
105
|
+
externalContinuation?: ExternalActorContinuation;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Recover only an exact saved begin request, under the caller's live actor
|
|
109
|
+
* admission. This performs no provider work or acquisition. */
|
|
110
|
+
export async function getConnectBeginReplay(
|
|
111
|
+
db: Database,
|
|
112
|
+
scope: ConnectActorScope,
|
|
113
|
+
input: ConnectBeginIdentity,
|
|
114
|
+
): Promise<Attempt | null> {
|
|
115
|
+
validateOperation(input.idempotencyKey, input.requestDigest);
|
|
116
|
+
const origin = input.externalContinuation
|
|
117
|
+
? ExternalActorContinuation.parse(input.externalContinuation)
|
|
118
|
+
: null;
|
|
119
|
+
const key = createHash("sha256").update(input.idempotencyKey).digest("hex");
|
|
120
|
+
return scoped(db, scope, async (tx) => {
|
|
121
|
+
const [existing] = await rawRows<Row>(
|
|
122
|
+
tx,
|
|
123
|
+
sql`select * from connect_attempts
|
|
124
|
+
where workspace_id = ${scope.workspaceId}::uuid and subject_id = ${scope.subjectId}
|
|
125
|
+
and idempotency_key_hash = ${key} and account_id = ${scope.accountId}::uuid`,
|
|
126
|
+
);
|
|
127
|
+
if (!existing) return null;
|
|
128
|
+
if (
|
|
129
|
+
existing.request_digest !== input.requestDigest ||
|
|
130
|
+
existing.return_url !== input.returnUrl ||
|
|
131
|
+
stableJson(existing.external_continuation ?? null) !== stableJson(origin)
|
|
132
|
+
)
|
|
133
|
+
throw new ConnectAttemptConflictError(
|
|
134
|
+
"Connect idempotency key was reused with different input",
|
|
135
|
+
);
|
|
136
|
+
return projection(existing, scope);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
101
140
|
export async function beginConnectAttempt(
|
|
102
141
|
db: Database,
|
|
103
142
|
scope: ConnectActorScope,
|
|
@@ -107,6 +146,7 @@ export async function beginConnectAttempt(
|
|
|
107
146
|
returnUrl: string;
|
|
108
147
|
attempt: Attempt;
|
|
109
148
|
externalContinuation?: ExternalActorContinuation;
|
|
149
|
+
authorizeAcquisition?: (tx: Database) => Promise<void>;
|
|
110
150
|
},
|
|
111
151
|
): Promise<Attempt> {
|
|
112
152
|
const value = ConnectAttempt.parse(input.attempt);
|
|
@@ -128,24 +168,9 @@ export async function beginConnectAttempt(
|
|
|
128
168
|
and expires_at < now() - interval '30 days'
|
|
129
169
|
order by expires_at, id limit 100 for update skip locked
|
|
130
170
|
) delete from connect_attempts where id in (select id from expired)`);
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
where workspace_id = ${scope.workspaceId}::uuid and subject_id = ${scope.subjectId}
|
|
135
|
-
and idempotency_key_hash = ${key} and account_id = ${scope.accountId}::uuid`,
|
|
136
|
-
);
|
|
137
|
-
if (existing) {
|
|
138
|
-
if (
|
|
139
|
-
existing.request_digest !== input.requestDigest ||
|
|
140
|
-
existing.return_url !== input.returnUrl ||
|
|
141
|
-
stableJson(existing.external_continuation ?? null) !== stableJson(origin)
|
|
142
|
-
) {
|
|
143
|
-
throw new ConnectAttemptConflictError(
|
|
144
|
-
"Connect idempotency key was reused with different input",
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
return projection(existing, scope);
|
|
148
|
-
}
|
|
171
|
+
const existing = await getConnectBeginReplay(tx, scope, input);
|
|
172
|
+
if (existing) return existing;
|
|
173
|
+
await input.authorizeAcquisition?.(tx);
|
|
149
174
|
const [count] = await rawRows<{ count: number }>(
|
|
150
175
|
tx,
|
|
151
176
|
sql`select count(*)::int as count from connect_attempts
|
|
@@ -227,6 +252,8 @@ export async function claimConnectOperation(
|
|
|
227
252
|
operationId: string;
|
|
228
253
|
inputDigest: string;
|
|
229
254
|
authorize?: ConnectOperationAuthorization;
|
|
255
|
+
/** Additional acquisition admission, only after exact no-effect receipt replay. */
|
|
256
|
+
authorizeAcquisition?: (tx: Database, attempt: Attempt) => Promise<void>;
|
|
230
257
|
},
|
|
231
258
|
): Promise<{ status: "claimed"; attempt: Attempt } | { status: "replayed"; attempt: Attempt }> {
|
|
232
259
|
validateOperation(input.operationId, input.inputDigest);
|
|
@@ -242,6 +269,7 @@ export async function claimConnectOperation(
|
|
|
242
269
|
if (receipt) {
|
|
243
270
|
return { status: "replayed", attempt: receipt };
|
|
244
271
|
}
|
|
272
|
+
await input.authorizeAcquisition?.(tx, structuredClone(current));
|
|
245
273
|
if (row.operation_id)
|
|
246
274
|
throw new ConnectAttemptConflictError(
|
|
247
275
|
"Connect operation is in flight or uncertain; reconcile before retrying",
|
|
@@ -272,6 +300,8 @@ export async function finishConnectOperation(
|
|
|
272
300
|
inputDigest: string;
|
|
273
301
|
commit: (tx: Database, current: Attempt) => Promise<Attempt>;
|
|
274
302
|
authorize?: ConnectOperationAuthorization;
|
|
303
|
+
/** Additional acquisition admission, only after exact no-effect receipt replay. */
|
|
304
|
+
authorizeAcquisition?: (tx: Database, attempt: Attempt) => Promise<void>;
|
|
275
305
|
},
|
|
276
306
|
): Promise<Attempt> {
|
|
277
307
|
validateOperation(input.operationId, input.inputDigest);
|
|
@@ -287,6 +317,7 @@ export async function finishConnectOperation(
|
|
|
287
317
|
if (receipt) {
|
|
288
318
|
return receipt;
|
|
289
319
|
}
|
|
320
|
+
await input.authorizeAcquisition?.(tx, structuredClone(current));
|
|
290
321
|
if (row.operation_id !== input.operationId || row.operation_digest !== input.inputDigest)
|
|
291
322
|
throw new ConnectAttemptConflictError();
|
|
292
323
|
const next = ConnectAttempt.parse(await input.commit(tx, structuredClone(current)));
|