@opengeni/db 1.0.1 → 3.2.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-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
- package/dist/chunk-2EH4WRHL.js.map +1 -0
- package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
- package/dist/chunk-6HMM2R5T.js +390 -0
- package/dist/chunk-6HMM2R5T.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
- package/dist/chunk-AZHW7R5V.js.map +1 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
- package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
- package/dist/chunk-KRLPEMLH.js.map +1 -0
- package/dist/chunk-NAB6FKFN.js +516 -0
- package/dist/chunk-NAB6FKFN.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-UZNXG44K.js +4508 -0
- package/dist/chunk-UZNXG44K.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 +85 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +23 -4
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1606 -81
- package/dist/index.js +21898 -8147
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +29 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +43 -4
- package/dist/session-realtime-ledger.d.ts +2 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +268 -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 +28 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
- package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
- package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
- package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
- package/drizzle/0346_document_migration_audit_surface.sql +454 -0
- package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -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 +167 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +87 -2
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +11676 -1550
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +995 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +905 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1317 -72
- package/src/schema.ts +3316 -428
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +441 -85
- package/src/session-realtime-context.ts +4 -6
- package/src/session-realtime-ledger.ts +28 -14
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +410 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +867 -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 +138 -11
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PersonalResourceAttachmentSummary,
|
|
3
|
+
type PersonalResourceAttachmentIntent,
|
|
4
|
+
USER_RESOURCE_ACTION_BY_KIND,
|
|
5
|
+
type UserResourceDelegation,
|
|
6
|
+
type UserResourceKind,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import { sql } from "drizzle-orm";
|
|
9
|
+
import { rawRows, setSubjectRlsContext, withRlsContext, type Database } from "./database";
|
|
10
|
+
import { nestedPostgresSqlState } from "./persistence-errors";
|
|
11
|
+
|
|
12
|
+
export type UserResourceAuthorityGrant = {
|
|
13
|
+
grantId: string;
|
|
14
|
+
targetWorkspaceId: string;
|
|
15
|
+
targetSessionId: string | null;
|
|
16
|
+
action: (typeof USER_RESOURCE_ACTION_BY_KIND)[UserResourceKind];
|
|
17
|
+
mode: "once" | "session" | "always";
|
|
18
|
+
context: "user_private" | "workspace_shared";
|
|
19
|
+
authorityEpoch: number | null;
|
|
20
|
+
generation: number;
|
|
21
|
+
status: "active" | "consumed" | "revoked" | "expired";
|
|
22
|
+
expiresAt: string | null;
|
|
23
|
+
delegation: UserResourceDelegation;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type UserResourceAuthoritySummary = {
|
|
27
|
+
authorityId: string;
|
|
28
|
+
resourceKind: UserResourceKind;
|
|
29
|
+
resourceId: string;
|
|
30
|
+
originWorkspaceId: string | null;
|
|
31
|
+
generation: number;
|
|
32
|
+
status: "active" | "retained" | "revoked";
|
|
33
|
+
grants: UserResourceAuthorityGrant[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type AcceptedTurnPersonalResourceAttachment = {
|
|
37
|
+
summary: PersonalResourceAttachmentSummary;
|
|
38
|
+
replay: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class PersonalResourceAttachmentAcceptanceError extends Error {
|
|
42
|
+
readonly name = "PersonalResourceAttachmentAcceptanceError";
|
|
43
|
+
constructor(
|
|
44
|
+
readonly kind: "invalid" | "forbidden" | "conflict",
|
|
45
|
+
options?: ErrorOptions,
|
|
46
|
+
) {
|
|
47
|
+
super(
|
|
48
|
+
kind === "invalid"
|
|
49
|
+
? "The personal-resource attachment request is invalid"
|
|
50
|
+
: kind === "forbidden"
|
|
51
|
+
? "The personal-resource attachment is not authorized"
|
|
52
|
+
: "The personal-resource attachment conflicts with accepted work",
|
|
53
|
+
options,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Issue and freeze the locked session's personal Variable Set/Rig/Connected
|
|
60
|
+
* Machine closure on
|
|
61
|
+
* one already-inserted logical turn. The caller owns the surrounding accepted-
|
|
62
|
+
* work transaction; this helper must never be called as a standalone grant
|
|
63
|
+
* mutation.
|
|
64
|
+
*/
|
|
65
|
+
export async function acceptTurnPersonalResourceAttachmentInTransaction(
|
|
66
|
+
db: Database,
|
|
67
|
+
input: {
|
|
68
|
+
accountId: string;
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
turnId: string;
|
|
72
|
+
subjectId: string;
|
|
73
|
+
intent: PersonalResourceAttachmentIntent & { expectedAuthorityEpoch: number };
|
|
74
|
+
},
|
|
75
|
+
): Promise<AcceptedTurnPersonalResourceAttachment> {
|
|
76
|
+
await setSubjectRlsContext(db, input.subjectId);
|
|
77
|
+
let row:
|
|
78
|
+
| {
|
|
79
|
+
grantMode: "once" | "session" | "always";
|
|
80
|
+
grantContext: "user_private" | "workspace_shared";
|
|
81
|
+
resourceCount: number;
|
|
82
|
+
resourceKinds: Array<"variable_set" | "rig" | "connected_machine">;
|
|
83
|
+
sharedOutputWarningVersion: 1;
|
|
84
|
+
replay: boolean;
|
|
85
|
+
}
|
|
86
|
+
| undefined;
|
|
87
|
+
try {
|
|
88
|
+
[row] = await rawRows(
|
|
89
|
+
db,
|
|
90
|
+
sql`
|
|
91
|
+
select grant_mode as "grantMode", grant_context as "grantContext",
|
|
92
|
+
resource_count::int as "resourceCount", resource_kinds as "resourceKinds",
|
|
93
|
+
shared_output_warning_version::int as "sharedOutputWarningVersion", replay
|
|
94
|
+
from accept_turn_personal_resource_attachment(
|
|
95
|
+
${input.accountId}::uuid, ${input.workspaceId}::uuid,
|
|
96
|
+
${input.sessionId}::uuid, ${input.turnId}::uuid, ${input.intent.mode},
|
|
97
|
+
${input.intent.expectedAuthorityEpoch},
|
|
98
|
+
${input.intent.workspaceSharedAcknowledged},
|
|
99
|
+
${input.intent.sharedOutputWarningVersion}
|
|
100
|
+
)
|
|
101
|
+
`,
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const sqlState = nestedPostgresSqlState(error);
|
|
105
|
+
if (sqlState === "22023") {
|
|
106
|
+
throw new PersonalResourceAttachmentAcceptanceError("invalid", { cause: error });
|
|
107
|
+
}
|
|
108
|
+
if (sqlState === "42501") {
|
|
109
|
+
throw new PersonalResourceAttachmentAcceptanceError("forbidden", { cause: error });
|
|
110
|
+
}
|
|
111
|
+
if (sqlState === "23505") {
|
|
112
|
+
throw new PersonalResourceAttachmentAcceptanceError("conflict", { cause: error });
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (!row) throw new Error("atomic personal-resource attachment was not returned");
|
|
117
|
+
return {
|
|
118
|
+
summary: PersonalResourceAttachmentSummary.parse({
|
|
119
|
+
mode: row.grantMode,
|
|
120
|
+
context: row.grantContext,
|
|
121
|
+
resourceCount: row.resourceCount,
|
|
122
|
+
resourceKinds: row.resourceKinds,
|
|
123
|
+
sharedOutputWarningVersion: row.sharedOutputWarningVersion,
|
|
124
|
+
}),
|
|
125
|
+
replay: row.replay,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type AuthorityRow = Omit<
|
|
130
|
+
UserResourceAuthorityGrant,
|
|
131
|
+
"grantId" | "targetWorkspaceId" | "delegation"
|
|
132
|
+
> & {
|
|
133
|
+
authorityId: string;
|
|
134
|
+
organizationId: string;
|
|
135
|
+
resourceKind: UserResourceKind;
|
|
136
|
+
resourceId: string;
|
|
137
|
+
originWorkspaceId: string | null;
|
|
138
|
+
authorityGeneration: number;
|
|
139
|
+
authorityStatus: UserResourceAuthoritySummary["status"];
|
|
140
|
+
grantId: string | null;
|
|
141
|
+
targetWorkspaceId: string | null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function rfc3339(value: string | null): string | null {
|
|
145
|
+
if (value === null) return null;
|
|
146
|
+
const date = new Date(value);
|
|
147
|
+
if (Number.isNaN(date.getTime())) throw new Error("invalid database timestamptz projection");
|
|
148
|
+
return date.toISOString();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function withOwnerContext<T>(
|
|
152
|
+
db: Database,
|
|
153
|
+
input: { accountId: string; workspaceId: string; subjectId: string },
|
|
154
|
+
fn: (db: Database) => Promise<T>,
|
|
155
|
+
): Promise<T> {
|
|
156
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
157
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
158
|
+
return await fn(scopedDb);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function listSelfUserResourceAuthorities(
|
|
163
|
+
db: Database,
|
|
164
|
+
input: {
|
|
165
|
+
accountId: string;
|
|
166
|
+
workspaceId: string;
|
|
167
|
+
subjectId: string;
|
|
168
|
+
resourceKind: UserResourceKind;
|
|
169
|
+
cursor?: string | undefined;
|
|
170
|
+
limit: number;
|
|
171
|
+
},
|
|
172
|
+
): Promise<{ authorities: UserResourceAuthoritySummary[]; nextCursor: string | null }> {
|
|
173
|
+
return await withOwnerContext(db, input, async (scopedDb) => {
|
|
174
|
+
const rows = await rawRows<AuthorityRow>(
|
|
175
|
+
scopedDb,
|
|
176
|
+
sql`
|
|
177
|
+
select authority_id as "authorityId", organization_id as "organizationId",
|
|
178
|
+
resource_kind as "resourceKind", resource_id as "resourceId",
|
|
179
|
+
origin_workspace_id as "originWorkspaceId",
|
|
180
|
+
authority_generation::int as "authorityGeneration",
|
|
181
|
+
authority_status as "authorityStatus", grant_id as "grantId",
|
|
182
|
+
target_workspace_id as "targetWorkspaceId", target_session_id as "targetSessionId",
|
|
183
|
+
action, grant_mode as mode, grant_context as context,
|
|
184
|
+
authority_epoch::int as "authorityEpoch",
|
|
185
|
+
grant_generation::int as generation, grant_status as status,
|
|
186
|
+
expires_at::text as "expiresAt"
|
|
187
|
+
from list_self_user_resource_authorities(
|
|
188
|
+
${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.resourceKind},
|
|
189
|
+
${input.cursor ?? null}::uuid, ${input.limit + 1}
|
|
190
|
+
)
|
|
191
|
+
`,
|
|
192
|
+
);
|
|
193
|
+
const grouped = new Map<string, UserResourceAuthoritySummary>();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const authority = grouped.get(row.authorityId) ?? {
|
|
196
|
+
authorityId: row.authorityId,
|
|
197
|
+
resourceKind: row.resourceKind,
|
|
198
|
+
resourceId: row.resourceId,
|
|
199
|
+
originWorkspaceId: row.originWorkspaceId,
|
|
200
|
+
generation: row.authorityGeneration,
|
|
201
|
+
status: row.authorityStatus,
|
|
202
|
+
grants: [],
|
|
203
|
+
};
|
|
204
|
+
if (row.grantId && row.targetWorkspaceId) {
|
|
205
|
+
authority.grants.push({
|
|
206
|
+
grantId: row.grantId,
|
|
207
|
+
targetWorkspaceId: row.targetWorkspaceId,
|
|
208
|
+
targetSessionId: row.targetSessionId,
|
|
209
|
+
action: row.action,
|
|
210
|
+
mode: row.mode,
|
|
211
|
+
context: row.context,
|
|
212
|
+
authorityEpoch: row.authorityEpoch,
|
|
213
|
+
generation: row.generation,
|
|
214
|
+
status: row.status,
|
|
215
|
+
expiresAt: rfc3339(row.expiresAt),
|
|
216
|
+
delegation: {
|
|
217
|
+
authorityId: row.authorityId,
|
|
218
|
+
grantId: row.grantId,
|
|
219
|
+
organizationId: row.organizationId,
|
|
220
|
+
workspaceId: row.targetWorkspaceId,
|
|
221
|
+
sessionId: row.targetSessionId,
|
|
222
|
+
action: row.action,
|
|
223
|
+
mode: row.mode,
|
|
224
|
+
context: row.context,
|
|
225
|
+
authorityEpoch: row.authorityEpoch,
|
|
226
|
+
authorityGeneration: row.authorityGeneration,
|
|
227
|
+
grantGeneration: row.generation,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
grouped.set(row.authorityId, authority);
|
|
232
|
+
}
|
|
233
|
+
const values = [...grouped.values()];
|
|
234
|
+
const hasMore = values.length > input.limit;
|
|
235
|
+
const authorities = hasMore ? values.slice(0, input.limit) : values;
|
|
236
|
+
return {
|
|
237
|
+
authorities,
|
|
238
|
+
nextCursor: hasMore ? (authorities.at(-1)?.authorityId ?? null) : null,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function issueSelfUserResourceGrant(
|
|
244
|
+
db: Database,
|
|
245
|
+
input: {
|
|
246
|
+
accountId: string;
|
|
247
|
+
workspaceId: string;
|
|
248
|
+
subjectId: string;
|
|
249
|
+
authorityId: string;
|
|
250
|
+
resourceKind: UserResourceKind;
|
|
251
|
+
mode: "session" | "always";
|
|
252
|
+
context: "user_private" | "workspace_shared";
|
|
253
|
+
sessionId?: string | null;
|
|
254
|
+
expectedAuthorityEpoch?: number | null;
|
|
255
|
+
workspaceSharedAcknowledged: boolean;
|
|
256
|
+
},
|
|
257
|
+
): Promise<UserResourceAuthorityGrant> {
|
|
258
|
+
return await withOwnerContext(db, input, async (scopedDb) => {
|
|
259
|
+
const [row] = await rawRows<
|
|
260
|
+
Omit<UserResourceAuthorityGrant, "delegation"> & {
|
|
261
|
+
organizationId: string;
|
|
262
|
+
authorityGeneration: number;
|
|
263
|
+
}
|
|
264
|
+
>(
|
|
265
|
+
scopedDb,
|
|
266
|
+
sql`
|
|
267
|
+
select grant_id as "grantId", organization_id as "organizationId",
|
|
268
|
+
authority_generation::int as "authorityGeneration",
|
|
269
|
+
target_workspace_id as "targetWorkspaceId",
|
|
270
|
+
target_session_id as "targetSessionId", action, grant_mode as mode,
|
|
271
|
+
grant_context as context, authority_epoch::int as "authorityEpoch",
|
|
272
|
+
grant_generation::int as generation,
|
|
273
|
+
grant_status as status, expires_at::text as "expiresAt"
|
|
274
|
+
from issue_self_user_resource_grant(
|
|
275
|
+
${input.accountId}::uuid, ${input.authorityId}::uuid, ${input.workspaceId}::uuid,
|
|
276
|
+
${input.resourceKind}, ${input.mode}, ${input.context},
|
|
277
|
+
${input.sessionId ?? null}::uuid, ${input.expectedAuthorityEpoch ?? null}::integer,
|
|
278
|
+
${input.workspaceSharedAcknowledged}
|
|
279
|
+
)
|
|
280
|
+
`,
|
|
281
|
+
);
|
|
282
|
+
if (!row) throw new Error("user-resource grant was not returned");
|
|
283
|
+
const { organizationId, authorityGeneration, ...grant } = row;
|
|
284
|
+
return {
|
|
285
|
+
...grant,
|
|
286
|
+
expiresAt: rfc3339(grant.expiresAt),
|
|
287
|
+
delegation: {
|
|
288
|
+
authorityId: input.authorityId,
|
|
289
|
+
grantId: grant.grantId,
|
|
290
|
+
organizationId,
|
|
291
|
+
workspaceId: grant.targetWorkspaceId,
|
|
292
|
+
sessionId: grant.targetSessionId,
|
|
293
|
+
action: grant.action,
|
|
294
|
+
mode: grant.mode,
|
|
295
|
+
context: grant.context,
|
|
296
|
+
authorityEpoch: grant.authorityEpoch,
|
|
297
|
+
authorityGeneration,
|
|
298
|
+
grantGeneration: grant.generation,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function revokeSelfUserResourceGrant(
|
|
305
|
+
db: Database,
|
|
306
|
+
input: { accountId: string; workspaceId: string; subjectId: string; grantId: string },
|
|
307
|
+
): Promise<{ grantId: string; generation: number; status: "revoked"; revokedAt: string }> {
|
|
308
|
+
return await withOwnerContext(db, input, async (scopedDb) => {
|
|
309
|
+
const [row] = await rawRows<{
|
|
310
|
+
grantId: string;
|
|
311
|
+
generation: number;
|
|
312
|
+
status: "revoked";
|
|
313
|
+
revokedAt: string;
|
|
314
|
+
}>(
|
|
315
|
+
scopedDb,
|
|
316
|
+
sql`
|
|
317
|
+
select grant_id as "grantId", grant_generation::int as generation,
|
|
318
|
+
grant_status as status, revoked_at::text as "revokedAt"
|
|
319
|
+
from revoke_self_user_resource_grant(
|
|
320
|
+
${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.grantId}::uuid
|
|
321
|
+
)
|
|
322
|
+
`,
|
|
323
|
+
);
|
|
324
|
+
if (!row) throw new Error("user-resource grant was not returned");
|
|
325
|
+
return { ...row, revokedAt: rfc3339(row.revokedAt)! };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function resolveSessionAttemptPersonalResources(
|
|
330
|
+
db: Database,
|
|
331
|
+
input: { accountId: string; workspaceId: string; subjectId: string | null; attemptId: string },
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
await withRlsContext(db, input, async (scopedDb) => {
|
|
334
|
+
if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
335
|
+
await scopedDb.execute(sql`select authorize_session_attempt_personal_resource_reads(
|
|
336
|
+
${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.attemptId}::uuid
|
|
337
|
+
)`);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export type PersonalDocumentAuthority = {
|
|
342
|
+
authorityId: string;
|
|
343
|
+
ownerOrganizationMembershipId: string;
|
|
344
|
+
authorityGeneration: number;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create the common organization-user authority before inserting a personal
|
|
349
|
+
* Document. Returns null for a configured/local or rolling-legacy subject that
|
|
350
|
+
* has no eligible active organization membership, preserving the existing
|
|
351
|
+
* workspace-anchored personal lane. Call this inside the same database
|
|
352
|
+
* transaction as the Document insert so a failed write cannot leave an orphan
|
|
353
|
+
* authority.
|
|
354
|
+
*/
|
|
355
|
+
export async function createPersonalDocumentAuthority(
|
|
356
|
+
db: Database,
|
|
357
|
+
input: {
|
|
358
|
+
accountId: string;
|
|
359
|
+
workspaceId: string;
|
|
360
|
+
subjectId: string;
|
|
361
|
+
documentId: string;
|
|
362
|
+
},
|
|
363
|
+
): Promise<PersonalDocumentAuthority | null> {
|
|
364
|
+
await setSubjectRlsContext(db, input.subjectId);
|
|
365
|
+
const [row] = await rawRows<PersonalDocumentAuthority>(
|
|
366
|
+
db,
|
|
367
|
+
sql`
|
|
368
|
+
select authority_id as "authorityId",
|
|
369
|
+
owner_organization_membership_id as "ownerOrganizationMembershipId",
|
|
370
|
+
authority_generation::int as "authorityGeneration"
|
|
371
|
+
from create_personal_document_authority(
|
|
372
|
+
${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.documentId}::uuid
|
|
373
|
+
)
|
|
374
|
+
`,
|
|
375
|
+
);
|
|
376
|
+
return row ?? null;
|
|
377
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { OrganizationMember } from "@opengeni/contracts";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { rawRows, type Database } from "./database";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE canonical live-workspace-authority resolver for one exact subject, run on
|
|
7
|
+
* a handle that is ALREADY inside the caller's transaction with that subject's
|
|
8
|
+
* RLS context applied.
|
|
9
|
+
*
|
|
10
|
+
* `getWorkspaceGrant` is a bare `workspace_memberships` join and is therefore
|
|
11
|
+
* NOT a complete authority answer: a managed human's personal workspace
|
|
12
|
+
* deliberately has no row in that table (migration 0219 raises on one), so its
|
|
13
|
+
* owner's access is the `organization_memberships.personal_workspace_id`
|
|
14
|
+
* pointer instead. Any seam that re-derives "does this subject still hold
|
|
15
|
+
* workspace authority here" from that bare join denies the one human who always
|
|
16
|
+
* belongs. This module exists so that rule has exactly one implementation.
|
|
17
|
+
*
|
|
18
|
+
* Seams that must serialize the answer with their own writes — session listing,
|
|
19
|
+
* session pins, the composer draft — call this variant so the read and the write
|
|
20
|
+
* share one transaction snapshot and one advisory fence. Seams that need to ask
|
|
21
|
+
* about a subject that is NOT the caller call
|
|
22
|
+
* {@link namedSubjectHasLiveWorkspaceAuthority} (`@opengeni/db`), which is a thin
|
|
23
|
+
* scope wrapper over exactly this body.
|
|
24
|
+
*
|
|
25
|
+
* True when the subject currently holds workspace authority the same way the
|
|
26
|
+
* route-time access builder derives it: a `workspace_memberships` row whose
|
|
27
|
+
* owning organization membership (when one exists) is active, or an active
|
|
28
|
+
* organization membership whose personal-workspace pointer is exactly this
|
|
29
|
+
* workspace. Never infers authority any other way; a deleted membership row and
|
|
30
|
+
* a suspended/revoked organization membership are both false. Only the
|
|
31
|
+
* membership's own `personal_workspace_id` counts as stated authority — nothing
|
|
32
|
+
* is read from `created_by`, a default workspace, a resource name, or current
|
|
33
|
+
* access.
|
|
34
|
+
*
|
|
35
|
+
* # This function is NOT an authorization. Read this before relying on it.
|
|
36
|
+
*
|
|
37
|
+
* It answers "does subject X hold authority over workspace W". It does NOT
|
|
38
|
+
* establish that the caller is X. Nothing here authenticates anybody.
|
|
39
|
+
*
|
|
40
|
+
* The scope assertion below is a **consistency check, not an authorization**.
|
|
41
|
+
* It prevents one thing: a caller naming a third-party subject while its
|
|
42
|
+
* transaction is scoped to someone else. It cannot detect a caller that scoped
|
|
43
|
+
* the transaction to an attacker-chosen subject in the first place, because
|
|
44
|
+
* `withWorkspaceSubjectRls(db, workspaceId, subjectId, ...)` takes that subject
|
|
45
|
+
* from its argument too. At today's three call sites both sides come from the
|
|
46
|
+
* same variable, so the check is a tautology there and fires only if a future
|
|
47
|
+
* refactor makes them diverge. Treat it as a tripwire against that refactor —
|
|
48
|
+
* never as evidence that the subject was authenticated.
|
|
49
|
+
*
|
|
50
|
+
* **The actual authorization for the personal-workspace exception lives in the
|
|
51
|
+
* API layer**: `AccessGrantAuthorization.canonicalManagedHumanSession`
|
|
52
|
+
* (`@opengeni/core`), stamped only where a Better Auth cookie was verified. A
|
|
53
|
+
* caller that has not proven that must not enable the exception. If you are
|
|
54
|
+
* adding a caller, that stamp is the part you have to get right; this function
|
|
55
|
+
* will happily answer `true` for any subject you scope a transaction to.
|
|
56
|
+
*
|
|
57
|
+
* Non-`user:` subjects (API keys, delegated service principals, configured and
|
|
58
|
+
* local principals) can never own an organization membership, so they
|
|
59
|
+
* short-circuit to the plain membership answer instead of tripping
|
|
60
|
+
* `list_self_organization_memberships`' `42501`.
|
|
61
|
+
*/
|
|
62
|
+
export async function subjectHasLiveWorkspaceAuthorityInScope(
|
|
63
|
+
scopedDb: Database,
|
|
64
|
+
input: { accountId: string; workspaceId: string; subjectId: string },
|
|
65
|
+
): Promise<boolean> {
|
|
66
|
+
// CONSISTENCY CHECK, NOT AN AUTHORIZATION. This function deliberately does not
|
|
67
|
+
// set `opengeni.subject_id`, so it cannot be handed a scope of its own — that
|
|
68
|
+
// makes the oracle shape unrepresentable here. But the scope it inherits was
|
|
69
|
+
// set by `withWorkspaceSubjectRls` from ITS caller's argument, so a match
|
|
70
|
+
// proves only that one caller used one subject consistently; it proves
|
|
71
|
+
// nothing about who that subject is. Authorization is the API layer's
|
|
72
|
+
// canonical-managed-cookie stamp. See the doc comment above.
|
|
73
|
+
const scopedSubjectId = await subjectIdInRlsScope(scopedDb);
|
|
74
|
+
if (scopedSubjectId !== input.subjectId) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"subjectHasLiveWorkspaceAuthorityInScope: subject does not match the applied RLS scope",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const [membershipRow] = await rawRows<{ present: number }>(
|
|
80
|
+
scopedDb,
|
|
81
|
+
sql`select 1 as present from workspace_memberships
|
|
82
|
+
where subject_id = ${input.subjectId}
|
|
83
|
+
and workspace_id = ${input.workspaceId}
|
|
84
|
+
limit 1`,
|
|
85
|
+
);
|
|
86
|
+
if (!input.subjectId.startsWith("user:")) {
|
|
87
|
+
return Boolean(membershipRow);
|
|
88
|
+
}
|
|
89
|
+
const [organizationMembershipResult] = await rawRows<{ result: unknown }>(
|
|
90
|
+
scopedDb,
|
|
91
|
+
sql`select list_self_organization_memberships(${input.subjectId}) as result`,
|
|
92
|
+
);
|
|
93
|
+
const organizationMemberships = OrganizationMember.array().parse(
|
|
94
|
+
organizationMembershipResult?.result ?? [],
|
|
95
|
+
);
|
|
96
|
+
const selfOrganizationMembership = organizationMemberships.find(
|
|
97
|
+
(organizationMembership) => organizationMembership.organizationId === input.accountId,
|
|
98
|
+
);
|
|
99
|
+
if (membershipRow) {
|
|
100
|
+
// Mirror the access builder's inactive-organization filter: a persisted
|
|
101
|
+
// membership row is dead while its organization membership is
|
|
102
|
+
// suspended/revoked. A subject with no organization membership at all
|
|
103
|
+
// (legacy/standalone) keeps its persisted row's authority.
|
|
104
|
+
return !selfOrganizationMembership || selfOrganizationMembership.status === "active";
|
|
105
|
+
}
|
|
106
|
+
return (
|
|
107
|
+
selfOrganizationMembership?.status === "active" &&
|
|
108
|
+
selfOrganizationMembership.personalWorkspaceId === input.workspaceId
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The applied subject GUC, or `""` when none is set. `""` is the canonical
|
|
114
|
+
* "unset" for this GUC: `current_subject_id()` is
|
|
115
|
+
* `nullif(current_setting('opengeni.subject_id', true), '')` (0239 precedent), so
|
|
116
|
+
* a caller restoring `""` genuinely clears it rather than pinning an empty
|
|
117
|
+
* subject. Never throws, so it is safe for capturing a prior value that may
|
|
118
|
+
* legitimately be absent.
|
|
119
|
+
*/
|
|
120
|
+
export async function rlsSubjectIdOrEmpty(scopedDb: Database): Promise<string> {
|
|
121
|
+
const [row] = await rawRows<{ subjectId: string | null }>(
|
|
122
|
+
scopedDb,
|
|
123
|
+
sql`select current_setting('opengeni.subject_id', true) as "subjectId"`,
|
|
124
|
+
);
|
|
125
|
+
return row?.subjectId ?? "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The subject of the caller's ALREADY-applied RLS scope, required to be present.
|
|
130
|
+
* Used by the in-scope resolver to check that a caller is asking about its own
|
|
131
|
+
* scope rather than naming a third party — a consistency check, not proof that
|
|
132
|
+
* anything authenticated that subject.
|
|
133
|
+
*/
|
|
134
|
+
async function subjectIdInRlsScope(scopedDb: Database): Promise<string> {
|
|
135
|
+
const subjectId = await rlsSubjectIdOrEmpty(scopedDb);
|
|
136
|
+
if (!subjectId) {
|
|
137
|
+
throw new Error("RLS subject context is not applied on the active backend");
|
|
138
|
+
}
|
|
139
|
+
return subjectId;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The tenant account of the caller's ALREADY-applied RLS scope. `withRlsContext`
|
|
144
|
+
* sets and read-back-verifies this GUC on the pinned backend before running the
|
|
145
|
+
* scoped body, so it is exact rather than a re-derivation — which lets a seam
|
|
146
|
+
* that only received a workspace id consult
|
|
147
|
+
* {@link subjectHasLiveWorkspaceAuthorityInScope} without a second round trip to
|
|
148
|
+
* resolve the account.
|
|
149
|
+
*/
|
|
150
|
+
export async function accountIdInRlsScope(scopedDb: Database): Promise<string> {
|
|
151
|
+
const [row] = await rawRows<{ accountId: string | null }>(
|
|
152
|
+
scopedDb,
|
|
153
|
+
sql`select current_setting('opengeni.account_id', true) as "accountId"`,
|
|
154
|
+
);
|
|
155
|
+
const accountId = row?.accountId ?? "";
|
|
156
|
+
if (!accountId) {
|
|
157
|
+
throw new Error("RLS account context is not applied on the active backend");
|
|
158
|
+
}
|
|
159
|
+
return accountId;
|
|
160
|
+
}
|
|
@@ -172,6 +172,55 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
|
|
|
172
172
|
}),
|
|
173
173
|
);
|
|
174
174
|
|
|
175
|
+
export const workspaceInstructionPolicyDeactivationEvents = pgTable(
|
|
176
|
+
"workspace_instruction_policy_deactivation_events",
|
|
177
|
+
{
|
|
178
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
179
|
+
operationId: uuid("operation_id").notNull(),
|
|
180
|
+
requestFingerprint: text("request_fingerprint").notNull(),
|
|
181
|
+
accountId: uuid("account_id").notNull(),
|
|
182
|
+
workspaceId: uuid("workspace_id").notNull(),
|
|
183
|
+
kind: text("kind").notNull(),
|
|
184
|
+
scope: text("scope").notNull(),
|
|
185
|
+
roleKey: text("role_key"),
|
|
186
|
+
type: text("type").notNull(),
|
|
187
|
+
activationVersion: bigint("activation_version", { mode: "number" }).notNull(),
|
|
188
|
+
oldRevisionId: uuid("old_revision_id").notNull(),
|
|
189
|
+
oldRevision: bigint("old_revision", { mode: "number" }).notNull(),
|
|
190
|
+
oldContentHash: text("old_content_hash").notNull(),
|
|
191
|
+
actorSubjectId: text("actor_subject_id").notNull(),
|
|
192
|
+
reason: text("reason").notNull(),
|
|
193
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
194
|
+
},
|
|
195
|
+
(table) => ({
|
|
196
|
+
workspaceOperation: uniqueIndex(
|
|
197
|
+
"workspace_instruction_policy_deactivate_workspace_operation_uq",
|
|
198
|
+
).on(table.workspaceId, table.operationId),
|
|
199
|
+
workspaceActivationVersion: uniqueIndex(
|
|
200
|
+
"workspace_instruction_policy_deactivations_target_version_uq",
|
|
201
|
+
).on(
|
|
202
|
+
table.workspaceId,
|
|
203
|
+
table.kind,
|
|
204
|
+
table.scope,
|
|
205
|
+
sql`coalesce(${table.roleKey}, '')`,
|
|
206
|
+
table.activationVersion,
|
|
207
|
+
),
|
|
208
|
+
workspaceTimeline: index("workspace_instruction_policy_deactivations_workspace_time_idx").on(
|
|
209
|
+
table.workspaceId,
|
|
210
|
+
table.createdAt,
|
|
211
|
+
table.id,
|
|
212
|
+
),
|
|
213
|
+
target: check(
|
|
214
|
+
"workspace_instruction_policy_deactivations_target_chk",
|
|
215
|
+
sql`(
|
|
216
|
+
(${table.kind} = 'charter' and ${table.scope} = 'global' and ${table.roleKey} is null)
|
|
217
|
+
or (${table.kind} = 'policy' and ${table.scope} = 'global' and ${table.roleKey} is null)
|
|
218
|
+
or (${table.kind} = 'policy' and ${table.scope} = 'role' and ${table.roleKey} is not null)
|
|
219
|
+
)`,
|
|
220
|
+
),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
|
|
175
224
|
export const workspaceInstructionPolicyOnboardingProposals = pgTable(
|
|
176
225
|
"workspace_instruction_policy_onboarding_proposals",
|
|
177
226
|
{
|
|
@@ -211,6 +260,9 @@ export const workspaceInstructionPolicyOnboardingProposals = pgTable(
|
|
|
211
260
|
sql`coalesce(${table.roleKey}, '')`,
|
|
212
261
|
table.sourceId,
|
|
213
262
|
table.sourceVersion,
|
|
263
|
+
// One proposal per source per baseline: a rebaselined successor is
|
|
264
|
+
// admissible, a duplicate against the same baseline is not.
|
|
265
|
+
table.baselineActivationVersion,
|
|
214
266
|
),
|
|
215
267
|
workspaceTimeline: index(
|
|
216
268
|
"workspace_instruction_policy_onboarding_proposals_workspace_time_idx",
|
|
@@ -239,7 +291,7 @@ export const workspaceInstructionPolicyOnboardingProposals = pgTable(
|
|
|
239
291
|
${table.baselineRevisionId} is null
|
|
240
292
|
and ${table.baselineRevision} is null
|
|
241
293
|
and ${table.baselineContentHash} is null
|
|
242
|
-
and ${table.baselineActivationVersion}
|
|
294
|
+
and ${table.baselineActivationVersion} >= 0
|
|
243
295
|
and ${table.baselineActivatedAt} is null
|
|
244
296
|
) or (
|
|
245
297
|
${table.baselineRevisionId} is not null
|