@opengeni/db 0.36.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-GYO7ED2Q.js → chunk-EULAN2NP.js} +2 -2
- package/dist/{chunk-VTPRIXKG.js → chunk-NJVZPWXY.js} +3 -3
- package/dist/{chunk-HSOWSYOX.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-5KTIEDX7.js → chunk-PR2WMLOQ.js} +7714 -4443
- package/dist/chunk-PR2WMLOQ.js.map +1 -0
- package/dist/{chunk-Z7VYMNPZ.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.d.ts +10 -0
- package/dist/editable-artifact-durable-export.js +18 -3
- package/dist/editable-artifact-durable-export.js.map +1 -1
- 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 +1633 -87
- package/dist/index.js +22004 -8109
- 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/knowledge-source-sync-schema.d.ts +730 -0
- package/dist/knowledge-source-sync.d.ts +60 -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 +23 -9
- package/dist/schema.d.ts +24925 -13697
- package/dist/schema.js +103 -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 +42 -5
- package/dist/session-realtime-ledger.d.ts +3 -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/0240_model_context_user_messages.sql +204 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0243_google_drive_object_acl_authority.sql +554 -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-artifact-durable-export.ts +27 -0
- 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 +12001 -1699
- 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/knowledge-source-sync-schema.ts +83 -0
- package/src/knowledge-source-sync.ts +530 -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 +931 -24
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1315 -52
- package/src/schema.ts +3336 -438
- 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 +450 -98
- package/src/session-realtime-context.ts +19 -3
- package/src/session-realtime-ledger.ts +75 -12
- 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-5KTIEDX7.js.map +0 -1
- package/dist/chunk-FPVAD4CO.js +0 -117
- package/dist/chunk-FPVAD4CO.js.map +0 -1
- package/dist/chunk-HSOWSYOX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-VKLB2ZGI.js +0 -2389
- package/dist/chunk-VKLB2ZGI.js.map +0 -1
- package/dist/chunk-Z7VYMNPZ.js.map +0 -1
- /package/dist/{chunk-GYO7ED2Q.js.map → chunk-EULAN2NP.js.map} +0 -0
- /package/dist/{chunk-VTPRIXKG.js.map → chunk-NJVZPWXY.js.map} +0 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateOrganizationResponse,
|
|
3
|
+
ListOrganizationInvitationsResponse,
|
|
4
|
+
ListOrganizationInvitationsPageResponse,
|
|
5
|
+
ListOrganizationMembersResponse,
|
|
6
|
+
ListSelfOrganizationMembershipsResponse,
|
|
7
|
+
OrganizationInvitation,
|
|
8
|
+
OrganizationAdministrationOverview,
|
|
9
|
+
OrganizationPrivateSessionSettings,
|
|
10
|
+
OrganizationMember,
|
|
11
|
+
OrganizationRetentionDeletionClaim,
|
|
12
|
+
OrganizationRetentionDatabaseFinalization,
|
|
13
|
+
OrganizationRetentionDeletionObject,
|
|
14
|
+
OrganizationRetentionDeletionPreview,
|
|
15
|
+
OrganizationRetentionDeletionResult,
|
|
16
|
+
OrganizationRetentionPolicy,
|
|
17
|
+
OrganizationSummary,
|
|
18
|
+
type CreateOrganizationResponse as CreateOrganizationResponseType,
|
|
19
|
+
type OrganizationAdministrationOverview as OrganizationAdministrationOverviewType,
|
|
20
|
+
type OrganizationInvitation as OrganizationInvitationType,
|
|
21
|
+
type OrganizationMember as OrganizationMemberType,
|
|
22
|
+
type OrganizationPrivateSessionSettings as OrganizationPrivateSessionSettingsType,
|
|
23
|
+
type OrganizationMembershipRole,
|
|
24
|
+
type OrganizationRetentionDeletionClaim as OrganizationRetentionDeletionClaimType,
|
|
25
|
+
type OrganizationRetentionDatabaseFinalization as OrganizationRetentionDatabaseFinalizationType,
|
|
26
|
+
type OrganizationRetentionDeletionObject as OrganizationRetentionDeletionObjectType,
|
|
27
|
+
type OrganizationRetentionDeletionPreview as OrganizationRetentionDeletionPreviewType,
|
|
28
|
+
type OrganizationRetentionDeletionResult as OrganizationRetentionDeletionResultType,
|
|
29
|
+
type OrganizationRetentionPolicy as OrganizationRetentionPolicyType,
|
|
30
|
+
type OrganizationSummary as OrganizationSummaryType,
|
|
31
|
+
type UpdateOrganizationMemberRequest,
|
|
32
|
+
} from "@opengeni/contracts";
|
|
33
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
34
|
+
import type { Database } from "./database";
|
|
35
|
+
import {
|
|
36
|
+
rawRows,
|
|
37
|
+
setSubjectRlsContext,
|
|
38
|
+
withRestoredSessionActivityRlsContext,
|
|
39
|
+
withRlsContext,
|
|
40
|
+
} from "./database";
|
|
41
|
+
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
42
|
+
import { lockSessionEventWriteRows } from "./session-control";
|
|
43
|
+
import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
|
|
44
|
+
import * as schema from "./schema";
|
|
45
|
+
|
|
46
|
+
export async function createManagedOrganization(
|
|
47
|
+
db: Database,
|
|
48
|
+
input: {
|
|
49
|
+
subjectId: string;
|
|
50
|
+
subjectLabel: string;
|
|
51
|
+
name: string;
|
|
52
|
+
operationId: string;
|
|
53
|
+
},
|
|
54
|
+
): Promise<CreateOrganizationResponseType> {
|
|
55
|
+
return await db.transaction(async (tx) => {
|
|
56
|
+
await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
|
|
57
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
58
|
+
tx,
|
|
59
|
+
sql`select create_managed_organization(
|
|
60
|
+
${input.subjectId},
|
|
61
|
+
${input.subjectLabel},
|
|
62
|
+
${input.name},
|
|
63
|
+
${input.operationId}::uuid
|
|
64
|
+
) as result`,
|
|
65
|
+
);
|
|
66
|
+
return CreateOrganizationResponse.parse(row?.result);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Bounded replay of one exact organization-lifecycle transaction after a
|
|
72
|
+
* PostgreSQL deadlock abort.
|
|
73
|
+
*
|
|
74
|
+
* A lifecycle command spans an entire organization and therefore touches many
|
|
75
|
+
* rows that ordinary workspace writers touch too, so it can land in a lock
|
|
76
|
+
* cycle with one. PostgreSQL resolves a cycle by aborting one of the two
|
|
77
|
+
* transactions with `40P01`.
|
|
78
|
+
*
|
|
79
|
+
* Replay is exact rather than approximate: the whole lifecycle command runs in
|
|
80
|
+
* one transaction keyed by its caller-supplied operation id
|
|
81
|
+
* (`organization_membership_operation_receipts`) plus its CAS revisions, and a
|
|
82
|
+
* deadlock abort rolls back every durable effect, so re-running the identical
|
|
83
|
+
* command either applies it once or observes the newer authoritative state.
|
|
84
|
+
*
|
|
85
|
+
* Only `40P01` is replayed. `40001` is the lifecycle's own authoritative
|
|
86
|
+
* stale-revision / stale-epoch conflict and must reach the caller unchanged,
|
|
87
|
+
* and the original failure is rethrown untouched once the budget is spent.
|
|
88
|
+
*
|
|
89
|
+
* SCOPE - read this before assuming a deadlock cannot escape. PostgreSQL picks
|
|
90
|
+
* ONE of the two transactions in the cycle as the victim, and it may pick the
|
|
91
|
+
* ordinary workspace writer instead of the lifecycle command. That writer is a
|
|
92
|
+
* plain caller of an unrelated module (`transitionSessionVisibility`, a session
|
|
93
|
+
* event/goal/system-update insert, ...); nothing here can replay it. So this
|
|
94
|
+
* covers exactly the lifecycle side of a cycle: it is a caller-side safety net,
|
|
95
|
+
* NOT a lock-order fix. The known organization/workspace lock-order inversion
|
|
96
|
+
* was removed in SQL by migration
|
|
97
|
+
* `0299_organization_membership_lock_order.sql` (advisory-lock mutual exclusion
|
|
98
|
+
* plus a downgraded `managed_accounts FOR KEY SHARE` row lock), and its
|
|
99
|
+
* parallel-load probe reads `pg_stat_database.deadlocks` directly so this
|
|
100
|
+
* replay cannot mask a regression. Do not treat this wrapper as licence to
|
|
101
|
+
* reintroduce a conflicting organization row lock.
|
|
102
|
+
*
|
|
103
|
+
* Every lifecycle entry point whose command acquires workspace rows - and so
|
|
104
|
+
* can be inside the cycle at all - is wrapped: `accept` (it inserts the
|
|
105
|
+
* personal workspace) and `suspend`/`offboard` (they take the account's
|
|
106
|
+
* `workspaces` rows `FOR KEY SHARE`). The remaining actions never reach a
|
|
107
|
+
* workspace row, so they can block an ordinary writer but cannot close a cycle
|
|
108
|
+
* with one.
|
|
109
|
+
*
|
|
110
|
+
* The wrapper must stay OUTSIDE any transaction it replays: retrying inside an
|
|
111
|
+
* already-aborted transaction is not a retry.
|
|
112
|
+
*/
|
|
113
|
+
async function withOrganizationLifecycleDeadlockReplay<T>(operation: () => Promise<T>): Promise<T> {
|
|
114
|
+
const maxAttempts = 3;
|
|
115
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
116
|
+
try {
|
|
117
|
+
return await operation();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (attempt >= maxAttempts || nestedPostgresSqlState(error) !== "40P01") throw error;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 5 + Math.random() * 20));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type CommandBase = {
|
|
126
|
+
organizationId: string;
|
|
127
|
+
actorSubjectId: string;
|
|
128
|
+
operationId: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type OrganizationMembershipProtocolSettlement = {
|
|
132
|
+
accountId: string;
|
|
133
|
+
workspaceId: string;
|
|
134
|
+
sessionId: string;
|
|
135
|
+
turnId: string;
|
|
136
|
+
executionGeneration: number;
|
|
137
|
+
turnAssociation: "current" | null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
141
|
+
|
|
142
|
+
function parseOrganizationMembershipProtocolSettlements(
|
|
143
|
+
value: unknown,
|
|
144
|
+
): OrganizationMembershipProtocolSettlement[] {
|
|
145
|
+
if (!Array.isArray(value)) {
|
|
146
|
+
throw new Error("Organization membership protocol preparation returned a non-array result");
|
|
147
|
+
}
|
|
148
|
+
return value.map((candidate) => {
|
|
149
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
150
|
+
throw new Error("Organization membership protocol preparation returned an invalid item");
|
|
151
|
+
}
|
|
152
|
+
const item = candidate as Record<string, unknown>;
|
|
153
|
+
if (
|
|
154
|
+
typeof item.accountId !== "string" ||
|
|
155
|
+
!uuidPattern.test(item.accountId) ||
|
|
156
|
+
typeof item.workspaceId !== "string" ||
|
|
157
|
+
!uuidPattern.test(item.workspaceId) ||
|
|
158
|
+
typeof item.sessionId !== "string" ||
|
|
159
|
+
!uuidPattern.test(item.sessionId) ||
|
|
160
|
+
typeof item.turnId !== "string" ||
|
|
161
|
+
!uuidPattern.test(item.turnId) ||
|
|
162
|
+
typeof item.executionGeneration !== "number" ||
|
|
163
|
+
!Number.isSafeInteger(item.executionGeneration) ||
|
|
164
|
+
item.executionGeneration <= 0 ||
|
|
165
|
+
(item.turnAssociation !== "current" && item.turnAssociation !== null)
|
|
166
|
+
) {
|
|
167
|
+
throw new Error("Organization membership protocol preparation returned an invalid item");
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
accountId: item.accountId,
|
|
171
|
+
workspaceId: item.workspaceId,
|
|
172
|
+
sessionId: item.sessionId,
|
|
173
|
+
turnId: item.turnId,
|
|
174
|
+
executionGeneration: item.executionGeneration,
|
|
175
|
+
turnAssociation: item.turnAssociation,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function prepareOrganizationMembershipProtocolSettlements(
|
|
181
|
+
db: Database,
|
|
182
|
+
command: CommandBase & Record<string, unknown>,
|
|
183
|
+
): Promise<OrganizationMembershipProtocolSettlement[]> {
|
|
184
|
+
return await withRlsContext(
|
|
185
|
+
db,
|
|
186
|
+
{ accountId: command.organizationId, workspaceId: null },
|
|
187
|
+
async (scopedDb) => {
|
|
188
|
+
await setSubjectRlsContext(scopedDb, command.actorSubjectId);
|
|
189
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
190
|
+
scopedDb,
|
|
191
|
+
sql`select prepare_organization_membership_protocol_settlements(
|
|
192
|
+
${JSON.stringify(command)}::jsonb
|
|
193
|
+
) as result`,
|
|
194
|
+
);
|
|
195
|
+
return parseOrganizationMembershipProtocolSettlements(row?.result ?? []);
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function settleOrganizationMembershipProtocols(
|
|
201
|
+
db: Database,
|
|
202
|
+
settlements: OrganizationMembershipProtocolSettlement[],
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const [prior] = await rawRows<{ subject_id: string; initiating_human_subject_id: string }>(
|
|
205
|
+
db,
|
|
206
|
+
sql`select
|
|
207
|
+
coalesce(current_setting('opengeni.subject_id', true), '') as subject_id,
|
|
208
|
+
coalesce(
|
|
209
|
+
current_setting('opengeni.initiating_human_subject_id', true), ''
|
|
210
|
+
) as initiating_human_subject_id`,
|
|
211
|
+
);
|
|
212
|
+
try {
|
|
213
|
+
// The SECURITY DEFINER preparation has already authorized and locked each
|
|
214
|
+
// exact target turn. Canonical settlement needs the established internal
|
|
215
|
+
// service RLS view because an administrator cannot otherwise see another
|
|
216
|
+
// member's user-private session rows.
|
|
217
|
+
await db.execute(sql`select
|
|
218
|
+
set_config('opengeni.subject_id', '', true),
|
|
219
|
+
set_config('opengeni.initiating_human_subject_id', '', true)`);
|
|
220
|
+
const workspaceIds = [
|
|
221
|
+
...new Set(settlements.map((settlement) => settlement.workspaceId)),
|
|
222
|
+
].sort();
|
|
223
|
+
for (const workspaceId of workspaceIds) {
|
|
224
|
+
const workspaceSettlements = settlements
|
|
225
|
+
.filter((settlement) => settlement.workspaceId === workspaceId)
|
|
226
|
+
.sort(
|
|
227
|
+
(left, right) =>
|
|
228
|
+
left.sessionId.localeCompare(right.sessionId) ||
|
|
229
|
+
left.turnId.localeCompare(right.turnId),
|
|
230
|
+
);
|
|
231
|
+
const accountId = workspaceSettlements[0]?.accountId;
|
|
232
|
+
if (!accountId) continue;
|
|
233
|
+
await withRestoredSessionActivityRlsContext(
|
|
234
|
+
db,
|
|
235
|
+
{ accountId, workspaceId },
|
|
236
|
+
async (scopedDb) => {
|
|
237
|
+
for (const settlement of workspaceSettlements) {
|
|
238
|
+
const locks = await lockSessionEventWriteRows(scopedDb, {
|
|
239
|
+
workspaceId,
|
|
240
|
+
controlLock: "already_locked",
|
|
241
|
+
workspaceLock: "already_locked",
|
|
242
|
+
sessionIds: [settlement.sessionId],
|
|
243
|
+
turnIds: [settlement.turnId],
|
|
244
|
+
});
|
|
245
|
+
const session = locks.sessions[0];
|
|
246
|
+
const turn = locks.turns[0];
|
|
247
|
+
if (
|
|
248
|
+
!session ||
|
|
249
|
+
!turn ||
|
|
250
|
+
session.accountId !== settlement.accountId ||
|
|
251
|
+
turn.accountId !== settlement.accountId ||
|
|
252
|
+
turn.sessionId !== settlement.sessionId ||
|
|
253
|
+
turn.executionGeneration !== settlement.executionGeneration ||
|
|
254
|
+
!["queued", "running", "requires_action", "recovering", "waiting_capacity"].includes(
|
|
255
|
+
turn.status,
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
258
|
+
throw new Error("Organization membership protocol settlement lost its locked turn");
|
|
259
|
+
}
|
|
260
|
+
const now = new Date();
|
|
261
|
+
const closedTools = await closePendingSessionToolCallsInTransaction(scopedDb, {
|
|
262
|
+
accountId: settlement.accountId,
|
|
263
|
+
workspaceId,
|
|
264
|
+
sessionId: settlement.sessionId,
|
|
265
|
+
turnId: settlement.turnId,
|
|
266
|
+
reason: "authority_changed",
|
|
267
|
+
sequence: session.lastSequence,
|
|
268
|
+
now,
|
|
269
|
+
turnAssociation: settlement.turnAssociation,
|
|
270
|
+
});
|
|
271
|
+
if (closedTools.sequence !== session.lastSequence) {
|
|
272
|
+
const [updated] = await scopedDb
|
|
273
|
+
.update(schema.sessions)
|
|
274
|
+
.set({ lastSequence: closedTools.sequence, updatedAt: now })
|
|
275
|
+
.where(
|
|
276
|
+
and(
|
|
277
|
+
eq(schema.sessions.accountId, settlement.accountId),
|
|
278
|
+
eq(schema.sessions.workspaceId, workspaceId),
|
|
279
|
+
eq(schema.sessions.id, settlement.sessionId),
|
|
280
|
+
eq(schema.sessions.lastSequence, session.lastSequence),
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
.returning({ id: schema.sessions.id });
|
|
284
|
+
if (!updated) {
|
|
285
|
+
throw new Error("Organization membership protocol sequence changed under its lock");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
} finally {
|
|
293
|
+
await db.execute(sql`select
|
|
294
|
+
set_config('opengeni.subject_id', ${prior?.subject_id ?? ""}, true),
|
|
295
|
+
set_config(
|
|
296
|
+
'opengeni.initiating_human_subject_id',
|
|
297
|
+
${prior?.initiating_human_subject_id ?? ""}, true
|
|
298
|
+
)`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Remove one workspace membership through the fenced SECURITY DEFINER
|
|
304
|
+
* teardown (migration 0278): the same prepare/settle/command protocol as
|
|
305
|
+
* organization offboarding, scoped to exactly one workspace and one subject.
|
|
306
|
+
* Cancels the removed member's queued/live turns in the workspace, interrupts
|
|
307
|
+
* live attempts, ends their realtime modes, advances their private sessions'
|
|
308
|
+
* authority epochs, registers workflow wakes, deletes their per-workspace
|
|
309
|
+
* personal rows, and deletes the membership - in one transaction. Returns
|
|
310
|
+
* false when no membership row exists (idempotent retry).
|
|
311
|
+
*/
|
|
312
|
+
export async function removeWorkspaceMember(
|
|
313
|
+
db: Database,
|
|
314
|
+
input: {
|
|
315
|
+
accountId: string;
|
|
316
|
+
workspaceId: string;
|
|
317
|
+
actorSubjectId: string;
|
|
318
|
+
targetSubjectId: string;
|
|
319
|
+
requireOrganizationSharedWorkspaceAdministration?: boolean;
|
|
320
|
+
},
|
|
321
|
+
): Promise<boolean> {
|
|
322
|
+
const command = {
|
|
323
|
+
action: "remove",
|
|
324
|
+
organizationId: input.accountId,
|
|
325
|
+
workspaceId: input.workspaceId,
|
|
326
|
+
actorSubjectId: input.actorSubjectId,
|
|
327
|
+
targetSubjectId: input.targetSubjectId,
|
|
328
|
+
operationId: crypto.randomUUID(),
|
|
329
|
+
};
|
|
330
|
+
return await db.transaction(async (tx) => {
|
|
331
|
+
const txDb = tx as unknown as Database;
|
|
332
|
+
let organizationAdministrationCapabilityId: string | null = null;
|
|
333
|
+
if (input.requireOrganizationSharedWorkspaceAdministration) {
|
|
334
|
+
organizationAdministrationCapabilityId = await withRlsContext(
|
|
335
|
+
txDb,
|
|
336
|
+
{ accountId: input.accountId, workspaceId: null },
|
|
337
|
+
async (scopedDb) => {
|
|
338
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
339
|
+
const [row] = await rawRows<{ capabilityId: string }>(
|
|
340
|
+
scopedDb,
|
|
341
|
+
sql`select open_organization_shared_workspace_administration_capability(
|
|
342
|
+
${input.accountId}::uuid,
|
|
343
|
+
${input.workspaceId}::uuid,
|
|
344
|
+
${input.actorSubjectId}
|
|
345
|
+
) as "capabilityId"`,
|
|
346
|
+
);
|
|
347
|
+
if (!row) throw new Error("Organization administration capability was not opened");
|
|
348
|
+
return row.capabilityId;
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
// The personal-state fence first, exactly as the pre-0278 delete path took
|
|
353
|
+
// it: session listing takes the shared counterpart and pin mutation the
|
|
354
|
+
// same exclusive one. The removal command re-acquires it (reentrant within
|
|
355
|
+
// this transaction), but taking it here keeps the wait observable and the
|
|
356
|
+
// fence ordered before every other lock this transaction takes.
|
|
357
|
+
await txDb.execute(
|
|
358
|
+
sql`select pg_advisory_xact_lock(hashtextextended(
|
|
359
|
+
${`session-personal-state:${input.workspaceId}:${input.targetSubjectId}`}, 0))`,
|
|
360
|
+
);
|
|
361
|
+
const settlements = await withRlsContext(
|
|
362
|
+
txDb,
|
|
363
|
+
{ accountId: input.accountId, workspaceId: null },
|
|
364
|
+
async (scopedDb) => {
|
|
365
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
366
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
367
|
+
scopedDb,
|
|
368
|
+
sql`select prepare_workspace_membership_removal_settlements(
|
|
369
|
+
${JSON.stringify(command)}::jsonb
|
|
370
|
+
) as result`,
|
|
371
|
+
);
|
|
372
|
+
return parseOrganizationMembershipProtocolSettlements(row?.result ?? []);
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
await settleOrganizationMembershipProtocols(txDb, settlements);
|
|
376
|
+
const removed = await withRlsContext(
|
|
377
|
+
txDb,
|
|
378
|
+
{ accountId: input.accountId, workspaceId: null },
|
|
379
|
+
async (scopedDb) => {
|
|
380
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
381
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
382
|
+
scopedDb,
|
|
383
|
+
sql`select workspace_membership_removal_command(
|
|
384
|
+
${JSON.stringify(command)}::jsonb
|
|
385
|
+
) as result`,
|
|
386
|
+
);
|
|
387
|
+
if (!row) throw new Error("Workspace membership removal returned no result");
|
|
388
|
+
return (row.result as { removed?: unknown } | null)?.removed === true;
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
if (organizationAdministrationCapabilityId) {
|
|
392
|
+
await txDb.execute(
|
|
393
|
+
sql`select close_organization_shared_workspace_administration_capability(
|
|
394
|
+
${organizationAdministrationCapabilityId}::uuid
|
|
395
|
+
)`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return removed;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function assertActiveManagedHumanOrganizationMembership(
|
|
403
|
+
db: Database,
|
|
404
|
+
input: { accountId: string; subjectId: string },
|
|
405
|
+
): Promise<number | null> {
|
|
406
|
+
const [row] = await rawRows<{ authorization_revision: number | string | null }>(
|
|
407
|
+
db,
|
|
408
|
+
sql`select assert_active_managed_human_organization_membership(
|
|
409
|
+
${input.accountId}::uuid,
|
|
410
|
+
${input.subjectId}
|
|
411
|
+
) as authorization_revision`,
|
|
412
|
+
);
|
|
413
|
+
return row?.authorization_revision === null || row?.authorization_revision === undefined
|
|
414
|
+
? null
|
|
415
|
+
: Number(row.authorization_revision);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function runCommand(
|
|
419
|
+
db: Database,
|
|
420
|
+
command: CommandBase & Record<string, unknown>,
|
|
421
|
+
): Promise<unknown> {
|
|
422
|
+
return await withRlsContext(
|
|
423
|
+
db,
|
|
424
|
+
{ accountId: command.organizationId, workspaceId: null },
|
|
425
|
+
async (scopedDb) => {
|
|
426
|
+
await setSubjectRlsContext(scopedDb, command.actorSubjectId);
|
|
427
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
428
|
+
scopedDb,
|
|
429
|
+
sql`select organization_membership_command(${JSON.stringify(command)}::jsonb) as result`,
|
|
430
|
+
);
|
|
431
|
+
if (!row) throw new Error("Organization membership command returned no result");
|
|
432
|
+
return row.result;
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function listSelfOrganizationMemberships(
|
|
438
|
+
db: Database,
|
|
439
|
+
subjectId: string,
|
|
440
|
+
): Promise<OrganizationMemberType[]> {
|
|
441
|
+
return await db.transaction(async (tx) => {
|
|
442
|
+
await setSubjectRlsContext(tx as unknown as Database, subjectId);
|
|
443
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
444
|
+
tx,
|
|
445
|
+
sql`select list_self_organization_memberships(${subjectId}) as result`,
|
|
446
|
+
);
|
|
447
|
+
return ListSelfOrganizationMembershipsResponse.parse({
|
|
448
|
+
memberships: row?.result ?? [],
|
|
449
|
+
}).memberships;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function listSelfOrganizationInvitations(
|
|
454
|
+
db: Database,
|
|
455
|
+
input: { subjectId: string; cursor?: string; limit: number },
|
|
456
|
+
): Promise<{
|
|
457
|
+
invitations: OrganizationInvitationType[];
|
|
458
|
+
nextCursor: string | null;
|
|
459
|
+
}> {
|
|
460
|
+
return await db.transaction(async (tx) => {
|
|
461
|
+
await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
|
|
462
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
463
|
+
tx,
|
|
464
|
+
sql`select list_self_organization_invitations(
|
|
465
|
+
${input.subjectId},
|
|
466
|
+
${input.cursor ?? null}::uuid,
|
|
467
|
+
${input.limit}
|
|
468
|
+
) as result`,
|
|
469
|
+
);
|
|
470
|
+
const candidates = ListOrganizationInvitationsResponse.parse({
|
|
471
|
+
invitations: row?.result ?? [],
|
|
472
|
+
}).invitations;
|
|
473
|
+
const hasMore = candidates.length > input.limit;
|
|
474
|
+
const invitations = candidates.slice(0, input.limit);
|
|
475
|
+
return ListOrganizationInvitationsPageResponse.parse({
|
|
476
|
+
invitations,
|
|
477
|
+
nextCursor: hasMore ? (invitations.at(-1)?.id ?? null) : null,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function getSelfOrganizationInvitation(
|
|
483
|
+
db: Database,
|
|
484
|
+
input: { subjectId: string; invitationId: string },
|
|
485
|
+
): Promise<OrganizationInvitationType> {
|
|
486
|
+
return await db.transaction(async (tx) => {
|
|
487
|
+
await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
|
|
488
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
489
|
+
tx,
|
|
490
|
+
sql`select get_self_organization_invitation(
|
|
491
|
+
${input.subjectId},
|
|
492
|
+
${input.invitationId}::uuid
|
|
493
|
+
) as result`,
|
|
494
|
+
);
|
|
495
|
+
if (!row) throw new Error("Self organization invitation lookup returned no result");
|
|
496
|
+
return OrganizationInvitation.parse(row.result);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function listOrganizationMembers(
|
|
501
|
+
db: Database,
|
|
502
|
+
input: { organizationId: string; actorSubjectId: string },
|
|
503
|
+
): Promise<OrganizationMemberType[]> {
|
|
504
|
+
return await withRlsContext(
|
|
505
|
+
db,
|
|
506
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
507
|
+
async (scopedDb) => {
|
|
508
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
509
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
510
|
+
scopedDb,
|
|
511
|
+
sql`select list_organization_members(
|
|
512
|
+
${input.organizationId}::uuid,
|
|
513
|
+
${input.actorSubjectId}
|
|
514
|
+
) as result`,
|
|
515
|
+
);
|
|
516
|
+
return ListOrganizationMembersResponse.parse({
|
|
517
|
+
members: row?.result ?? [],
|
|
518
|
+
}).members;
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export async function getOrganizationAdministrationOverview(
|
|
524
|
+
db: Database,
|
|
525
|
+
input: { organizationId: string; actorSubjectId: string },
|
|
526
|
+
): Promise<OrganizationAdministrationOverviewType> {
|
|
527
|
+
return await withRlsContext(
|
|
528
|
+
db,
|
|
529
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
530
|
+
async (scopedDb) => {
|
|
531
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
532
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
533
|
+
scopedDb,
|
|
534
|
+
sql`select get_organization_administration_overview(
|
|
535
|
+
${input.organizationId}::uuid,
|
|
536
|
+
${input.actorSubjectId}
|
|
537
|
+
) as result`,
|
|
538
|
+
);
|
|
539
|
+
return OrganizationAdministrationOverview.parse(row?.result);
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function updateOrganizationName(
|
|
545
|
+
db: Database,
|
|
546
|
+
input: {
|
|
547
|
+
organizationId: string;
|
|
548
|
+
actorSubjectId: string;
|
|
549
|
+
name: string;
|
|
550
|
+
expectedUpdatedAt: string;
|
|
551
|
+
operationId: string;
|
|
552
|
+
},
|
|
553
|
+
): Promise<OrganizationSummaryType> {
|
|
554
|
+
return await withRlsContext(
|
|
555
|
+
db,
|
|
556
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
557
|
+
async (scopedDb) => {
|
|
558
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
559
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
560
|
+
scopedDb,
|
|
561
|
+
sql`select update_organization_name(
|
|
562
|
+
${input.organizationId}::uuid,
|
|
563
|
+
${input.actorSubjectId},
|
|
564
|
+
${input.name},
|
|
565
|
+
${input.expectedUpdatedAt}::timestamptz,
|
|
566
|
+
${input.operationId}::uuid
|
|
567
|
+
) as result`,
|
|
568
|
+
);
|
|
569
|
+
return OrganizationSummary.parse(row?.result);
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export async function getOrganizationPrivateSessionSettings(
|
|
575
|
+
db: Database,
|
|
576
|
+
input: { organizationId: string; actorSubjectId: string },
|
|
577
|
+
): Promise<OrganizationPrivateSessionSettingsType> {
|
|
578
|
+
return await withRlsContext(
|
|
579
|
+
db,
|
|
580
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
581
|
+
async (scopedDb) => {
|
|
582
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
583
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
584
|
+
scopedDb,
|
|
585
|
+
sql`select get_organization_private_session_settings(
|
|
586
|
+
${input.organizationId}::uuid,
|
|
587
|
+
${input.actorSubjectId}
|
|
588
|
+
) as result`,
|
|
589
|
+
);
|
|
590
|
+
return OrganizationPrivateSessionSettings.parse(row?.result);
|
|
591
|
+
},
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function updateOrganizationPrivateSessionSettings(
|
|
596
|
+
db: Database,
|
|
597
|
+
input: {
|
|
598
|
+
organizationId: string;
|
|
599
|
+
actorSubjectId: string;
|
|
600
|
+
enabled: boolean;
|
|
601
|
+
expectedVersion: number;
|
|
602
|
+
operationId: string;
|
|
603
|
+
},
|
|
604
|
+
): Promise<OrganizationPrivateSessionSettingsType> {
|
|
605
|
+
return await withRlsContext(
|
|
606
|
+
db,
|
|
607
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
608
|
+
async (scopedDb) => {
|
|
609
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
610
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
611
|
+
scopedDb,
|
|
612
|
+
sql`select update_organization_private_session_settings(
|
|
613
|
+
${input.organizationId}::uuid,
|
|
614
|
+
${input.actorSubjectId},
|
|
615
|
+
${input.enabled},
|
|
616
|
+
${input.expectedVersion}::bigint,
|
|
617
|
+
${input.operationId}::uuid
|
|
618
|
+
) as result`,
|
|
619
|
+
);
|
|
620
|
+
return OrganizationPrivateSessionSettings.parse(row?.result);
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function listOrganizationInvitations(
|
|
626
|
+
db: Database,
|
|
627
|
+
input: {
|
|
628
|
+
organizationId: string;
|
|
629
|
+
actorSubjectId: string;
|
|
630
|
+
cursor?: string;
|
|
631
|
+
limit: number;
|
|
632
|
+
},
|
|
633
|
+
): Promise<{
|
|
634
|
+
invitations: OrganizationInvitationType[];
|
|
635
|
+
nextCursor: string | null;
|
|
636
|
+
}> {
|
|
637
|
+
return await withRlsContext(
|
|
638
|
+
db,
|
|
639
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
640
|
+
async (scopedDb) => {
|
|
641
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
642
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
643
|
+
scopedDb,
|
|
644
|
+
sql`select list_organization_invitations(
|
|
645
|
+
${input.organizationId}::uuid,
|
|
646
|
+
${input.actorSubjectId},
|
|
647
|
+
${input.cursor ?? null}::uuid,
|
|
648
|
+
${input.limit}
|
|
649
|
+
) as result`,
|
|
650
|
+
);
|
|
651
|
+
const candidates = ListOrganizationInvitationsResponse.parse({
|
|
652
|
+
invitations: row?.result ?? [],
|
|
653
|
+
}).invitations;
|
|
654
|
+
const hasMore = candidates.length > input.limit;
|
|
655
|
+
const invitations = candidates.slice(0, input.limit);
|
|
656
|
+
return ListOrganizationInvitationsPageResponse.parse({
|
|
657
|
+
invitations,
|
|
658
|
+
nextCursor: hasMore ? (invitations.at(-1)?.id ?? null) : null,
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function createOrganizationInvitation(
|
|
665
|
+
db: Database,
|
|
666
|
+
input: CommandBase & {
|
|
667
|
+
targetSubjectId: string | null;
|
|
668
|
+
targetEmail: string;
|
|
669
|
+
targetName?: string;
|
|
670
|
+
initialWorkspaceIds?: string[];
|
|
671
|
+
role: OrganizationMembershipRole;
|
|
672
|
+
expiresAt: string;
|
|
673
|
+
},
|
|
674
|
+
): Promise<OrganizationInvitationType> {
|
|
675
|
+
const command = { action: "invite", ...input };
|
|
676
|
+
const result = await withRlsContext(
|
|
677
|
+
db,
|
|
678
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
679
|
+
async (scopedDb) => {
|
|
680
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
681
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
682
|
+
scopedDb,
|
|
683
|
+
sql`select create_organization_invitation_v2(
|
|
684
|
+
${JSON.stringify(command)}::jsonb
|
|
685
|
+
) as result`,
|
|
686
|
+
);
|
|
687
|
+
if (!row) throw new Error("Organization invitation command returned no result");
|
|
688
|
+
return row.result;
|
|
689
|
+
},
|
|
690
|
+
);
|
|
691
|
+
return OrganizationInvitation.parse(result);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export async function bindPendingOrganizationInvitationsForVerifiedEmail(
|
|
695
|
+
db: Database,
|
|
696
|
+
input: { subjectId: string; email: string },
|
|
697
|
+
): Promise<number> {
|
|
698
|
+
return await db.transaction(async (tx) => {
|
|
699
|
+
const txDb = tx as unknown as Database;
|
|
700
|
+
await setSubjectRlsContext(txDb, input.subjectId);
|
|
701
|
+
const [row] = await rawRows<{ result: number | string }>(
|
|
702
|
+
txDb,
|
|
703
|
+
sql`select bind_pending_organization_invitations_for_verified_email(
|
|
704
|
+
${input.subjectId}::text, ${input.email}::text
|
|
705
|
+
) as result`,
|
|
706
|
+
);
|
|
707
|
+
return Number(row?.result ?? 0);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export async function acceptOrganizationInvitation(
|
|
712
|
+
db: Database,
|
|
713
|
+
input: CommandBase & { invitationId: string; expectedRevision: number },
|
|
714
|
+
): Promise<{
|
|
715
|
+
invitation: OrganizationInvitationType;
|
|
716
|
+
membership: OrganizationMemberType;
|
|
717
|
+
}> {
|
|
718
|
+
// `accept` inserts the invited human's personal workspace, so like
|
|
719
|
+
// suspend/offboard it acquires workspace rows and can be inside a lock cycle
|
|
720
|
+
// with an ordinary workspace writer. It needs the identical replay.
|
|
721
|
+
const command = { action: "accept", ...input };
|
|
722
|
+
const result = await withOrganizationLifecycleDeadlockReplay(async () =>
|
|
723
|
+
withRlsContext(db, { accountId: input.organizationId, workspaceId: null }, async (scopedDb) => {
|
|
724
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
725
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
726
|
+
scopedDb,
|
|
727
|
+
sql`select accept_organization_invitation_v2(
|
|
728
|
+
${JSON.stringify(command)}::jsonb
|
|
729
|
+
) as result`,
|
|
730
|
+
);
|
|
731
|
+
if (!row) throw new Error("Organization invitation acceptance returned no result");
|
|
732
|
+
return row.result;
|
|
733
|
+
}),
|
|
734
|
+
);
|
|
735
|
+
return {
|
|
736
|
+
invitation: OrganizationInvitation.parse((result as { invitation?: unknown }).invitation),
|
|
737
|
+
membership: OrganizationMember.parse((result as { membership?: unknown }).membership),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export async function revokeOrganizationInvitation(
|
|
742
|
+
db: Database,
|
|
743
|
+
input: CommandBase & { invitationId: string; expectedRevision: number },
|
|
744
|
+
): Promise<OrganizationInvitationType> {
|
|
745
|
+
return OrganizationInvitation.parse(
|
|
746
|
+
await runCommand(db, { action: "revoke_invitation", ...input }),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export async function updateOrganizationMember(
|
|
751
|
+
db: Database,
|
|
752
|
+
input: CommandBase & {
|
|
753
|
+
membershipId: string;
|
|
754
|
+
transition: UpdateOrganizationMemberRequest;
|
|
755
|
+
},
|
|
756
|
+
): Promise<OrganizationMemberType> {
|
|
757
|
+
const { transition, ...base } = input;
|
|
758
|
+
const command = {
|
|
759
|
+
action: transition.kind,
|
|
760
|
+
...base,
|
|
761
|
+
expectedAuthorizationRevision: transition.expectedAuthorizationRevision,
|
|
762
|
+
...(transition.role === undefined ? {} : { role: transition.role }),
|
|
763
|
+
...(transition.reason === undefined ? {} : { reason: transition.reason }),
|
|
764
|
+
};
|
|
765
|
+
return await withOrganizationLifecycleDeadlockReplay(async () =>
|
|
766
|
+
db.transaction(async (tx) => {
|
|
767
|
+
if (transition.kind === "suspend" || transition.kind === "offboard") {
|
|
768
|
+
const settlements = await prepareOrganizationMembershipProtocolSettlements(
|
|
769
|
+
tx as unknown as Database,
|
|
770
|
+
command,
|
|
771
|
+
);
|
|
772
|
+
await settleOrganizationMembershipProtocols(tx as unknown as Database, settlements);
|
|
773
|
+
}
|
|
774
|
+
return OrganizationMember.parse(await runCommand(tx as unknown as Database, command));
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export async function getOrganizationRetentionPolicy(
|
|
780
|
+
db: Database,
|
|
781
|
+
input: { organizationId: string; actorSubjectId: string },
|
|
782
|
+
): Promise<OrganizationRetentionPolicyType> {
|
|
783
|
+
return await withRlsContext(
|
|
784
|
+
db,
|
|
785
|
+
{ accountId: input.organizationId, workspaceId: null },
|
|
786
|
+
async (scopedDb) => {
|
|
787
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
788
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
789
|
+
scopedDb,
|
|
790
|
+
sql`select get_organization_retention_policy(
|
|
791
|
+
${input.organizationId}::uuid,
|
|
792
|
+
${input.actorSubjectId}
|
|
793
|
+
) as result`,
|
|
794
|
+
);
|
|
795
|
+
return OrganizationRetentionPolicy.parse(row?.result);
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export async function updateOrganizationRetentionPolicy(
|
|
801
|
+
db: Database,
|
|
802
|
+
input: CommandBase & {
|
|
803
|
+
mode: "retain" | "delete_after";
|
|
804
|
+
retentionDays: number | null;
|
|
805
|
+
expectedVersion: number;
|
|
806
|
+
},
|
|
807
|
+
): Promise<OrganizationRetentionPolicyType> {
|
|
808
|
+
return OrganizationRetentionPolicy.parse(await runCommand(db, { action: "retention", ...input }));
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function runRetentionCapability<T>(
|
|
812
|
+
db: Database,
|
|
813
|
+
organizationId: string,
|
|
814
|
+
query: (scopedDb: Database) => Promise<T>,
|
|
815
|
+
): Promise<T> {
|
|
816
|
+
return await withRlsContext(
|
|
817
|
+
db,
|
|
818
|
+
{ accountId: organizationId, workspaceId: null },
|
|
819
|
+
async (scopedDb) => await query(scopedDb),
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export async function previewOrganizationRetentionDeletions(
|
|
824
|
+
db: Database,
|
|
825
|
+
input: { organizationId: string; limit?: number },
|
|
826
|
+
): Promise<OrganizationRetentionDeletionPreviewType[]> {
|
|
827
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
828
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
829
|
+
scopedDb,
|
|
830
|
+
sql`select preview_organization_retention_deletions(
|
|
831
|
+
${input.organizationId}::uuid, ${input.limit ?? 25}
|
|
832
|
+
) as result`,
|
|
833
|
+
);
|
|
834
|
+
return OrganizationRetentionDeletionPreview.array().parse(row?.result ?? []);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export async function claimOrganizationRetentionDeletion(
|
|
839
|
+
db: Database,
|
|
840
|
+
input: { organizationId: string; operationId: string; excludedMembershipIds?: string[] },
|
|
841
|
+
): Promise<OrganizationRetentionDeletionClaimType | null> {
|
|
842
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
843
|
+
const excludedMembershipIds = input.excludedMembershipIds ?? [];
|
|
844
|
+
const excludedMembershipArray =
|
|
845
|
+
excludedMembershipIds.length === 0
|
|
846
|
+
? sql`ARRAY[]::uuid[]`
|
|
847
|
+
: sql`ARRAY[${sql.join(
|
|
848
|
+
excludedMembershipIds.map((membershipId) => sql`${membershipId}::uuid`),
|
|
849
|
+
sql`, `,
|
|
850
|
+
)}]::uuid[]`;
|
|
851
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
852
|
+
scopedDb,
|
|
853
|
+
sql`select claim_organization_retention_deletion(
|
|
854
|
+
${input.organizationId}::uuid, ${input.operationId}::uuid,
|
|
855
|
+
${excludedMembershipArray}
|
|
856
|
+
) as result`,
|
|
857
|
+
);
|
|
858
|
+
return row?.result === null || row?.result === undefined
|
|
859
|
+
? null
|
|
860
|
+
: OrganizationRetentionDeletionClaim.parse(row.result);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export async function listOrganizationRetentionDeletionObjects(
|
|
865
|
+
db: Database,
|
|
866
|
+
input: {
|
|
867
|
+
organizationId: string;
|
|
868
|
+
membershipId: string;
|
|
869
|
+
operationId: string;
|
|
870
|
+
objectBucket: string;
|
|
871
|
+
limit?: number;
|
|
872
|
+
},
|
|
873
|
+
): Promise<OrganizationRetentionDeletionObjectType[]> {
|
|
874
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
875
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
876
|
+
scopedDb,
|
|
877
|
+
sql`select list_organization_retention_deletion_objects(
|
|
878
|
+
${input.organizationId}::uuid,
|
|
879
|
+
${input.membershipId}::uuid,
|
|
880
|
+
${input.operationId}::uuid,
|
|
881
|
+
${input.objectBucket},
|
|
882
|
+
${input.limit ?? 100}
|
|
883
|
+
) as result`,
|
|
884
|
+
);
|
|
885
|
+
return OrganizationRetentionDeletionObject.array().parse(row?.result ?? []);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export async function recordOrganizationRetentionObjectDeleted(
|
|
890
|
+
db: Database,
|
|
891
|
+
input: {
|
|
892
|
+
organizationId: string;
|
|
893
|
+
membershipId: string;
|
|
894
|
+
operationId: string;
|
|
895
|
+
objectKind: OrganizationRetentionDeletionObjectType["objectKind"];
|
|
896
|
+
sourceId: string;
|
|
897
|
+
objectBucket: string;
|
|
898
|
+
objectKey: string;
|
|
899
|
+
},
|
|
900
|
+
): Promise<boolean> {
|
|
901
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
902
|
+
const [row] = await rawRows<{ result: boolean }>(
|
|
903
|
+
scopedDb,
|
|
904
|
+
sql`select record_organization_retention_object_deleted(
|
|
905
|
+
${input.organizationId}::uuid,
|
|
906
|
+
${input.membershipId}::uuid,
|
|
907
|
+
${input.operationId}::uuid,
|
|
908
|
+
${input.objectKind},
|
|
909
|
+
${input.sourceId},
|
|
910
|
+
${input.objectBucket},
|
|
911
|
+
${input.objectKey}
|
|
912
|
+
) as result`,
|
|
913
|
+
);
|
|
914
|
+
return row?.result === true;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export async function failOrganizationRetentionDeletion(
|
|
919
|
+
db: Database,
|
|
920
|
+
input: {
|
|
921
|
+
organizationId: string;
|
|
922
|
+
membershipId: string;
|
|
923
|
+
operationId: string;
|
|
924
|
+
reasonCode: string;
|
|
925
|
+
},
|
|
926
|
+
): Promise<boolean> {
|
|
927
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
928
|
+
const [row] = await rawRows<{ result: boolean }>(
|
|
929
|
+
scopedDb,
|
|
930
|
+
sql`select fail_organization_retention_deletion(
|
|
931
|
+
${input.organizationId}::uuid,
|
|
932
|
+
${input.membershipId}::uuid,
|
|
933
|
+
${input.operationId}::uuid,
|
|
934
|
+
${input.reasonCode}
|
|
935
|
+
) as result`,
|
|
936
|
+
);
|
|
937
|
+
return row?.result === true;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export async function finalizeOrganizationRetentionDeletion(
|
|
942
|
+
db: Database,
|
|
943
|
+
input: {
|
|
944
|
+
organizationId: string;
|
|
945
|
+
membershipId: string;
|
|
946
|
+
operationId: string;
|
|
947
|
+
objectBucket: string;
|
|
948
|
+
},
|
|
949
|
+
): Promise<OrganizationRetentionDatabaseFinalizationType> {
|
|
950
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
951
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
952
|
+
scopedDb,
|
|
953
|
+
sql`select finalize_organization_retention_deletion(
|
|
954
|
+
${input.organizationId}::uuid,
|
|
955
|
+
${input.membershipId}::uuid,
|
|
956
|
+
${input.operationId}::uuid,
|
|
957
|
+
${input.objectBucket}
|
|
958
|
+
) as result`,
|
|
959
|
+
);
|
|
960
|
+
return OrganizationRetentionDatabaseFinalization.parse(row?.result);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export async function completeOrganizationRetentionDeletion(
|
|
965
|
+
db: Database,
|
|
966
|
+
input: {
|
|
967
|
+
organizationId: string;
|
|
968
|
+
membershipId: string;
|
|
969
|
+
operationId: string;
|
|
970
|
+
objectBucket: string;
|
|
971
|
+
},
|
|
972
|
+
): Promise<OrganizationRetentionDeletionResultType> {
|
|
973
|
+
return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
|
|
974
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
975
|
+
scopedDb,
|
|
976
|
+
sql`select complete_organization_retention_deletion(
|
|
977
|
+
${input.organizationId}::uuid,
|
|
978
|
+
${input.membershipId}::uuid,
|
|
979
|
+
${input.operationId}::uuid,
|
|
980
|
+
${input.objectBucket}
|
|
981
|
+
) as result`,
|
|
982
|
+
);
|
|
983
|
+
return OrganizationRetentionDeletionResult.parse(row?.result);
|
|
984
|
+
});
|
|
985
|
+
}
|