@opengeni/db 4.2.2 → 4.3.2-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.
Files changed (103) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-S4Y42VSX.js → chunk-AI4NC6XW.js} +212 -7
  3. package/dist/chunk-AI4NC6XW.js.map +1 -0
  4. package/dist/{chunk-SNLIKELL.js → chunk-DVSLOUEI.js} +2 -2
  5. package/dist/{chunk-6GAPAYZR.js → chunk-F4BIHJ7J.js} +562 -86
  6. package/dist/chunk-F4BIHJ7J.js.map +1 -0
  7. package/dist/{chunk-B2KR3TEM.js → chunk-I4ITNMX2.js} +2 -2
  8. package/dist/{chunk-MGVOYVPS.js → chunk-PFBUHOI5.js} +3 -3
  9. package/dist/{chunk-6Y7ZDVJ5.js → chunk-UUPY6IGP.js} +2 -2
  10. package/dist/{chunk-FPTI4NP4.js → chunk-UVBPOGA7.js} +2 -2
  11. package/dist/{chunk-7WX3R3YL.js → chunk-XBXANJQS.js} +7 -2
  12. package/dist/chunk-XBXANJQS.js.map +1 -0
  13. package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
  14. package/dist/chunk-YVYBAWBU.js.map +1 -0
  15. package/dist/connect-attempts.d.ts +60 -0
  16. package/dist/connection-setup-authority.d.ts +10 -0
  17. package/dist/connection-token-resolver.d.ts +11 -2
  18. package/dist/editable-artifact-durable-export.js +2 -2
  19. package/dist/editable-artifacts.js +3 -3
  20. package/dist/external-identities.d.ts +16 -0
  21. package/dist/external-identity-links.d.ts +74 -0
  22. package/dist/external-link-work.d.ts +51 -0
  23. package/dist/host-mcp-binding-match.d.ts +5 -0
  24. package/dist/host-mcp-bindings.d.ts +73 -0
  25. package/dist/host-mcp-task-authority.d.ts +47 -0
  26. package/dist/host-selection-identity.d.ts +6 -0
  27. package/dist/index.d.ts +60 -35
  28. package/dist/index.js +15247 -13793
  29. package/dist/index.js.map +1 -1
  30. package/dist/live-session-attempt.d.ts +275 -0
  31. package/dist/managed-auth-session-sets.js +3 -3
  32. package/dist/memory-domain.d.ts +3 -3
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/retained-provider-commands.js +2 -2
  35. package/dist/runtime-posture.d.ts +6 -6
  36. package/dist/scheduled-task-revision-authority.d.ts +11 -0
  37. package/dist/schema.d.ts +1279 -9
  38. package/dist/schema.js +11 -1
  39. package/dist/session-background-commands.js +4 -4
  40. package/dist/session-command-output.js +4 -4
  41. package/dist/session-event-slices.js +2 -2
  42. package/dist/session-queue-commands.d.ts +7 -0
  43. package/dist/session-tenancy.d.ts +2 -0
  44. package/dist/session-tenancy.js +3 -3
  45. package/dist/video-generation.js +3 -3
  46. package/dist/workspace-authority.d.ts +4 -4
  47. package/dist/workspace-tool-defaults.js +4 -4
  48. package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
  49. package/drizzle/0438_durable_connect_attempts.sql +95 -0
  50. package/drizzle/0439_external_identity_provisioning.sql +128 -0
  51. package/drizzle/0440_external_workspace_member_removal.sql +59 -0
  52. package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
  53. package/drizzle/0442_external_owning_user_authority.sql +81 -0
  54. package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
  55. package/drizzle/0444_host_mcp_delegations.sql +129 -0
  56. package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
  57. package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
  58. package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
  59. package/drizzle/0448_host_mcp_child_authority.sql +115 -0
  60. package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
  61. package/drizzle/0450_external_identity_link_work.sql +179 -0
  62. package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
  63. package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
  64. package/drizzle/0453_host_mcp_native_owner.sql +35 -0
  65. package/drizzle/0454_connect_origin_authority.sql +42 -0
  66. package/drizzle/0455_external_link_inventory_labels.sql +44 -0
  67. package/drizzle/0456_social_connection_versions.sql +14 -0
  68. package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
  69. package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
  70. package/package.json +6 -6
  71. package/src/connect-attempts.ts +317 -0
  72. package/src/connection-authority.ts +9 -0
  73. package/src/connection-setup-authority.ts +35 -0
  74. package/src/connection-token-resolver.ts +177 -48
  75. package/src/external-identities.ts +70 -0
  76. package/src/external-identity-links.ts +364 -0
  77. package/src/external-link-work.ts +228 -0
  78. package/src/host-mcp-binding-match.ts +33 -0
  79. package/src/host-mcp-bindings.ts +741 -0
  80. package/src/host-mcp-task-authority.ts +383 -0
  81. package/src/host-selection-identity.ts +29 -0
  82. package/src/index.ts +481 -151
  83. package/src/live-session-attempt.ts +85 -0
  84. package/src/managed-human-provisioning.ts +6 -1
  85. package/src/memory-domain.ts +6 -11
  86. package/src/provision-roles.ts +3 -0
  87. package/src/runtime-posture.ts +47 -0
  88. package/src/scheduled-task-revision-authority.ts +30 -0
  89. package/src/schema.ts +233 -7
  90. package/src/session-control.ts +14 -0
  91. package/src/session-queue-commands.ts +11 -0
  92. package/src/session-tenancy.ts +7 -0
  93. package/src/workspace-artifacts.ts +10 -4
  94. package/src/workspace-authority.ts +5 -5
  95. package/dist/chunk-6GAPAYZR.js.map +0 -1
  96. package/dist/chunk-6IQ4X35Y.js.map +0 -1
  97. package/dist/chunk-7WX3R3YL.js.map +0 -1
  98. package/dist/chunk-S4Y42VSX.js.map +0 -1
  99. /package/dist/{chunk-SNLIKELL.js.map → chunk-DVSLOUEI.js.map} +0 -0
  100. /package/dist/{chunk-B2KR3TEM.js.map → chunk-I4ITNMX2.js.map} +0 -0
  101. /package/dist/{chunk-MGVOYVPS.js.map → chunk-PFBUHOI5.js.map} +0 -0
  102. /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-UUPY6IGP.js.map} +0 -0
  103. /package/dist/{chunk-FPTI4NP4.js.map → chunk-UVBPOGA7.js.map} +0 -0
package/dist/schema.js CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  composerDrafts,
56
56
  computerSessionAssociations,
57
57
  computerSessions,
58
+ connectAttempts,
58
59
  connectionDisconnectOperations,
59
60
  connectionUseAuditFacts,
60
61
  connectionUseOnceConsumptionReceipts,
@@ -87,6 +88,7 @@ import {
87
88
  enrollmentOsValues,
88
89
  enrollmentStatusValues,
89
90
  enrollments,
91
+ externalIdentities,
90
92
  feedbackSubmissions,
91
93
  fileUploads,
92
94
  files,
@@ -104,6 +106,9 @@ import {
104
106
  hostExportCursorState,
105
107
  hostExportDeadLetters,
106
108
  hostExportOutbox,
109
+ hostMcpBindings,
110
+ hostMcpDelegations,
111
+ hostMcpTurnAuthorities,
107
112
  imageGenerationOperations,
108
113
  importBatches,
109
114
  integrationFacetBindingOwners,
@@ -354,7 +359,7 @@ import {
354
359
  xaiRotationSettings,
355
360
  xaiSessionAccountPins,
356
361
  xaiSubscriptionCredentials
357
- } from "./chunk-S4Y42VSX.js";
362
+ } from "./chunk-AI4NC6XW.js";
358
363
  import "./chunk-PZ5AY32C.js";
359
364
  export {
360
365
  agentRunStates,
@@ -413,6 +418,7 @@ export {
413
418
  composerDrafts,
414
419
  computerSessionAssociations,
415
420
  computerSessions,
421
+ connectAttempts,
416
422
  connectionDisconnectOperations,
417
423
  connectionUseAuditFacts,
418
424
  connectionUseOnceConsumptionReceipts,
@@ -445,6 +451,7 @@ export {
445
451
  enrollmentOsValues,
446
452
  enrollmentStatusValues,
447
453
  enrollments,
454
+ externalIdentities,
448
455
  feedbackSubmissions,
449
456
  fileUploads,
450
457
  files,
@@ -462,6 +469,9 @@ export {
462
469
  hostExportCursorState,
463
470
  hostExportDeadLetters,
464
471
  hostExportOutbox,
472
+ hostMcpBindings,
473
+ hostMcpDelegations,
474
+ hostMcpTurnAuthorities,
465
475
  imageGenerationOperations,
466
476
  importBatches,
467
477
  integrationFacetBindingOwners,
@@ -22,10 +22,10 @@ import {
22
22
  settleClaimedConnectedMachineBackgroundCommandWithMutation,
23
23
  settleConnectedMachineSessionBackgroundCommandWithMutation,
24
24
  settleSessionBackgroundCommandForRetainedProcessInTransaction
25
- } from "./chunk-6GAPAYZR.js";
26
- import "./chunk-MGVOYVPS.js";
27
- import "./chunk-B2KR3TEM.js";
28
- import "./chunk-S4Y42VSX.js";
25
+ } from "./chunk-F4BIHJ7J.js";
26
+ import "./chunk-PFBUHOI5.js";
27
+ import "./chunk-I4ITNMX2.js";
28
+ import "./chunk-AI4NC6XW.js";
29
29
  import "./chunk-PZ5AY32C.js";
30
30
  export {
31
31
  COMMAND_OUTPUT_DEFAULT_BYTES,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  lockSessionEventWriteRows
3
- } from "./chunk-6GAPAYZR.js";
4
- import "./chunk-MGVOYVPS.js";
3
+ } from "./chunk-F4BIHJ7J.js";
4
+ import "./chunk-PFBUHOI5.js";
5
5
  import {
6
6
  withSessionActivityRlsContext
7
- } from "./chunk-B2KR3TEM.js";
7
+ } from "./chunk-I4ITNMX2.js";
8
8
  import {
9
9
  fromPostgresLosslessJson,
10
10
  sandboxRetainedProcesses,
@@ -12,7 +12,7 @@ import {
12
12
  sessionEvents,
13
13
  sessions,
14
14
  withLosslessContentWriteVersion
15
- } from "./chunk-S4Y42VSX.js";
15
+ } from "./chunk-AI4NC6XW.js";
16
16
  import "./chunk-PZ5AY32C.js";
17
17
 
18
18
  // src/session-command-output.ts
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  rawRows,
3
3
  withWorkspaceRls
4
- } from "./chunk-B2KR3TEM.js";
4
+ } from "./chunk-I4ITNMX2.js";
5
5
  import {
6
6
  LOSSLESS_JSON_STRING_PREFIX,
7
7
  fromPostgresLosslessJson,
8
8
  sessionEvents,
9
9
  sessionTurns
10
- } from "./chunk-S4Y42VSX.js";
10
+ } from "./chunk-AI4NC6XW.js";
11
11
  import "./chunk-PZ5AY32C.js";
12
12
 
13
13
  // src/session-event-slices.ts
@@ -230,6 +230,13 @@ export declare function submitHumanPromptInTransaction(db: SessionActivityDataba
230
230
  /** Trusted database-only admission seam. It runs only after a completed
231
231
  * operation replay has been ruled out; throwing rolls the prompt back. */
232
232
  beforeFreshPromptCommit?: (tx: Database) => Promise<void>;
233
+ /** Backend-only authority capture, atomic with the newly accepted turn. */
234
+ captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
235
+ selectedHostMcpDelegations?: Array<{
236
+ serverId: string;
237
+ delegationId: string;
238
+ generation: number;
239
+ }>;
233
240
  /** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
234
241
  controlLockTimeoutMs?: number;
235
242
  }): Promise<SubmitHumanPromptResult>;
@@ -33,6 +33,7 @@ export type TransitionSessionVisibilityInput = {
33
33
  targetVisibility: SessionTenancyVisibility;
34
34
  expectedAuthorityEpoch: number;
35
35
  operationKey: string;
36
+ beforeCommit?: (tx: Database) => Promise<void>;
36
37
  };
37
38
  export type TransitionSessionVisibilityResult = {
38
39
  operationId: string;
@@ -60,6 +61,7 @@ export type ForkSessionContentInput = {
60
61
  operationKey: string;
61
62
  runtimeRequest?: ForkSessionRuntimeRequest;
62
63
  runtimeConfiguration?: ForkSessionRuntimeConfiguration;
64
+ beforeCommit?: (tx: Database) => Promise<void>;
63
65
  };
64
66
  export type ForkSessionRuntimeRequest = {
65
67
  variableSetIds: string[];
@@ -14,9 +14,9 @@ import {
14
14
  replayAppliedSessionFork,
15
15
  sessionTenancyProductActivated,
16
16
  transitionSessionVisibility
17
- } from "./chunk-7WX3R3YL.js";
18
- import "./chunk-B2KR3TEM.js";
19
- import "./chunk-S4Y42VSX.js";
17
+ } from "./chunk-XBXANJQS.js";
18
+ import "./chunk-I4ITNMX2.js";
19
+ import "./chunk-AI4NC6XW.js";
20
20
  import "./chunk-PZ5AY32C.js";
21
21
  export {
22
22
  SessionTenancyAccessError,
@@ -29,9 +29,9 @@ import {
29
29
  settleVideoGenerationFailure,
30
30
  settleVideoGenerationReady,
31
31
  updateWorkspaceVideoGenerationPolicy
32
- } from "./chunk-MGVOYVPS.js";
33
- import "./chunk-B2KR3TEM.js";
34
- import "./chunk-S4Y42VSX.js";
32
+ } from "./chunk-PFBUHOI5.js";
33
+ import "./chunk-I4ITNMX2.js";
34
+ import "./chunk-AI4NC6XW.js";
35
35
  import "./chunk-PZ5AY32C.js";
36
36
  export {
37
37
  ACTIVE_VIDEO_GENERATION_STATUSES,
@@ -51,10 +51,10 @@ import { type Database } from "./database.js";
51
51
  * adding a caller, that stamp is the part you have to get right; this function
52
52
  * will happily answer `true` for any subject you scope a transaction to.
53
53
  *
54
- * Non-`user:` subjects (API keys, delegated service principals, configured and
55
- * local principals) can never own an organization membership, so they
56
- * short-circuit to the plain membership answer instead of tripping
57
- * `list_self_organization_memberships`' `42501`.
54
+ * Persisted `external_user:` subjects also have an organization membership and
55
+ * an exact Personal pointer. They use the same consistency check only after the
56
+ * API's dedicated verified external proof, never a native-cookie stamp. Other
57
+ * service/configured/local subjects retain the plain membership answer.
58
58
  */
59
59
  export declare function subjectHasLiveWorkspaceAuthorityInScope(scopedDb: Database, input: {
60
60
  accountId: string;
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  lockWorkspaceInferenceControl
3
- } from "./chunk-6GAPAYZR.js";
4
- import "./chunk-MGVOYVPS.js";
3
+ } from "./chunk-F4BIHJ7J.js";
4
+ import "./chunk-PFBUHOI5.js";
5
5
  import {
6
6
  withWorkspaceRls
7
- } from "./chunk-B2KR3TEM.js";
8
- import "./chunk-S4Y42VSX.js";
7
+ } from "./chunk-I4ITNMX2.js";
8
+ import "./chunk-AI4NC6XW.js";
9
9
  import "./chunk-PZ5AY32C.js";
10
10
 
11
11
  // src/workspace-tool-defaults.ts
@@ -0,0 +1,71 @@
1
+ -- deployment-mode: maintenance
2
+ -- Stop every old API and worker before activation. Old binaries use a global
3
+ -- conflict target and must not restart after this organization-scoped cutover.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '5min';
6
+
7
+ DO $drain$
8
+ DECLARE
9
+ roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
10
+ BEGIN
11
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
12
+ RAISE EXCEPTION '0437 requires an explicit application database role list' USING ERRCODE = '55000';
13
+ END IF;
14
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
15
+ SELECT 1 FROM jsonb_array_elements(roles) item
16
+ WHERE jsonb_typeof(item) <> 'string' OR btrim(item #>> '{}') = ''
17
+ OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63
18
+ ) THEN
19
+ RAISE EXCEPTION '0437 received invalid application database roles' USING ERRCODE = '55000';
20
+ END IF;
21
+ IF EXISTS (
22
+ SELECT 1 FROM pg_stat_activity activity
23
+ JOIN jsonb_array_elements_text(roles) role_name ON role_name = activity.usename
24
+ WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
25
+ ) THEN
26
+ RAISE EXCEPTION '0437 requires all application database sessions stopped' USING ERRCODE = '55000';
27
+ END IF;
28
+ END
29
+ $drain$;
30
+
31
+ LOCK TABLE workspaces IN ACCESS EXCLUSIVE MODE;
32
+
33
+ DO $cutover$
34
+ DECLARE
35
+ definition text;
36
+ old_target constant text := 'ON CONFLICT (external_source, external_id) DO UPDATE';
37
+ new_target constant text := 'ON CONFLICT (account_id, external_source, external_id) DO UPDATE';
38
+ old_personal_guard constant text := 'WHERE conflicting_workspace.external_source = ''opengeni:organization-membership''';
39
+ new_personal_guard constant text := 'WHERE conflicting_workspace.account_id = p_account_id AND conflicting_workspace.external_source = ''opengeni:organization-membership''';
40
+ BEGIN
41
+ IF EXISTS (
42
+ SELECT 1 FROM pg_stat_activity activity
43
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) role_name
44
+ ON role_name = activity.usename
45
+ WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
46
+ ) THEN
47
+ RAISE EXCEPTION '0437 requires all application database sessions stopped' USING ERRCODE = '55000';
48
+ END IF;
49
+ -- Preserve the deployed lifecycle function, including its security attributes
50
+ -- and all intervening fixes. Fail closed if its one reviewed target drifted.
51
+ SELECT pg_get_functiondef('organization_membership_command(jsonb)'::regprocedure) INTO definition;
52
+ IF strpos(definition, 'RETURN organization_membership_command_0263(p_command);') = 0 THEN
53
+ RAISE EXCEPTION '0437 organization membership wrapper drifted' USING ERRCODE = '55000';
54
+ END IF;
55
+ -- The live wrapper retains the newer offboarding/session-tenancy fences;
56
+ -- invitation acceptance delegates to this separately named implementation.
57
+ SELECT pg_get_functiondef('organization_membership_command_0263(jsonb)'::regprocedure) INTO definition;
58
+ IF (length(definition) - length(replace(definition, old_target, ''))) / length(old_target) <> 1 THEN
59
+ RAISE EXCEPTION '0437 organization membership conflict target drifted' USING ERRCODE = '55000';
60
+ END IF;
61
+ EXECUTE replace(definition, old_target, new_target);
62
+ SELECT pg_get_functiondef('ensure_managed_human_personal_workspace(uuid,text,uuid)'::regprocedure) INTO definition;
63
+ IF (length(definition) - length(replace(definition, old_personal_guard, ''))) / length(old_personal_guard) <> 1 THEN
64
+ RAISE EXCEPTION '0437 personal workspace conflict guard drifted' USING ERRCODE = '55000';
65
+ END IF;
66
+ EXECUTE replace(definition, old_personal_guard, new_personal_guard);
67
+ END
68
+ $cutover$;
69
+
70
+ DROP INDEX workspaces_external_idx;
71
+ CREATE UNIQUE INDEX workspaces_external_idx ON workspaces (account_id, external_source, external_id);
@@ -0,0 +1,95 @@
1
+ -- deployment-mode: maintenance
2
+ -- The exact runtime table/RLS/grant contract changes. Drain all old API and
3
+ -- worker logins and provision only the matching binary's role after migration.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '5min';
6
+
7
+ DO $drain$
8
+ DECLARE
9
+ roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
10
+ BEGIN
11
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
12
+ RAISE EXCEPTION '0438 requires an explicit application database role list' USING ERRCODE = '55000';
13
+ END IF;
14
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
15
+ SELECT 1 FROM jsonb_array_elements(roles) item
16
+ WHERE jsonb_typeof(item) <> 'string' OR btrim(item #>> '{}') = ''
17
+ OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63
18
+ ) THEN
19
+ RAISE EXCEPTION '0438 received invalid application database roles' USING ERRCODE = '55000';
20
+ END IF;
21
+ IF EXISTS (
22
+ SELECT 1 FROM pg_stat_activity activity
23
+ JOIN jsonb_array_elements_text(roles) role_name ON role_name = activity.usename
24
+ WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
25
+ ) THEN
26
+ RAISE EXCEPTION '0438 requires all application database sessions stopped' USING ERRCODE = '55000';
27
+ END IF;
28
+ END
29
+ $drain$;
30
+
31
+ CREATE TABLE connect_attempts (
32
+ id uuid PRIMARY KEY,
33
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
34
+ workspace_id uuid NOT NULL,
35
+ subject_id text NOT NULL CHECK (octet_length(subject_id) BETWEEN 1 AND 1024),
36
+ idempotency_key_hash text NOT NULL CHECK (idempotency_key_hash ~ '^[0-9a-f]{64}$'),
37
+ request_digest text NOT NULL CHECK (request_digest ~ '^[0-9a-f]{64}$'),
38
+ return_url text NOT NULL CHECK (octet_length(return_url) BETWEEN 1 AND 4096),
39
+ projection jsonb NOT NULL CHECK (jsonb_typeof(projection) = 'object' AND octet_length(projection::text) <= 1048576),
40
+ operation_id text CHECK (octet_length(operation_id) BETWEEN 1 AND 512),
41
+ operation_digest text CHECK (operation_digest ~ '^[0-9a-f]{64}$'),
42
+ receipts jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(receipts) = 'object' AND octet_length(receipts::text) <= 4194304),
43
+ expires_at timestamptz NOT NULL,
44
+ created_at timestamptz NOT NULL DEFAULT now(),
45
+ updated_at timestamptz NOT NULL DEFAULT now(),
46
+ CONSTRAINT connect_attempts_workspace_account_fk FOREIGN KEY (workspace_id, account_id)
47
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
48
+ CONSTRAINT connect_attempts_operation_pair_check CHECK ((operation_id IS NULL) = (operation_digest IS NULL)),
49
+ CONSTRAINT connect_attempts_projection_identity_check CHECK (
50
+ projection ?& ARRAY['id', 'workspaceId', 'revision', 'state', 'expiresAt']
51
+ AND projection->>'id' = id::text AND projection->>'workspaceId' = workspace_id::text
52
+ AND (projection->>'revision')::bigint > 0
53
+ AND (projection->>'expiresAt')::timestamptz = expires_at
54
+ )
55
+ );
56
+ CREATE UNIQUE INDEX connect_attempts_actor_idempotency_idx ON connect_attempts
57
+ (workspace_id, subject_id, idempotency_key_hash);
58
+ CREATE INDEX connect_attempts_actor_pending_idx ON connect_attempts
59
+ (workspace_id, subject_id, created_at DESC, id DESC)
60
+ WHERE projection->>'state' NOT IN ('complete','cancelled','expired');
61
+ CREATE INDEX connect_attempts_expiry_idx ON connect_attempts (expires_at, id);
62
+
63
+ ALTER TABLE connect_attempts ENABLE ROW LEVEL SECURITY;
64
+ ALTER TABLE connect_attempts FORCE ROW LEVEL SECURITY;
65
+ CREATE POLICY connect_attempts_actor_scope ON connect_attempts
66
+ USING (account_id = opengeni_private.current_account_id()
67
+ AND workspace_id = opengeni_private.current_workspace_id()
68
+ AND subject_id = opengeni_private.current_subject_id())
69
+ WITH CHECK (account_id = opengeni_private.current_account_id()
70
+ AND workspace_id = opengeni_private.current_workspace_id()
71
+ AND subject_id = opengeni_private.current_subject_id());
72
+
73
+ -- New-table defaults must not leave privileges for old or unrelated logins.
74
+ REVOKE ALL ON TABLE connect_attempts FROM PUBLIC;
75
+ DO $acl$
76
+ DECLARE target_role record;
77
+ BEGIN
78
+ FOR target_role IN
79
+ SELECT DISTINCT role.rolname FROM pg_class relation
80
+ CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
81
+ JOIN pg_roles role ON role.oid = privilege.grantee
82
+ WHERE relation.oid = 'connect_attempts'::regclass AND privilege.grantee <> relation.relowner
83
+ LOOP
84
+ EXECUTE format('REVOKE ALL ON TABLE connect_attempts FROM %I', target_role.rolname);
85
+ END LOOP;
86
+ IF EXISTS (
87
+ SELECT 1 FROM pg_stat_activity activity
88
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) role_name
89
+ ON role_name = activity.usename
90
+ WHERE activity.datname = current_database() AND activity.pid <> pg_backend_pid()
91
+ ) THEN
92
+ RAISE EXCEPTION '0438 requires all application database sessions stopped' USING ERRCODE = '55000';
93
+ END IF;
94
+ END
95
+ $acl$;
@@ -0,0 +1,128 @@
1
+ -- deployment-mode: maintenance
2
+ -- External identity storage/provisioning only. Does not grant workspace access,
3
+ -- activate private sessions, link native users, or change authentication.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '5min';
6
+ DO $drain$
7
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
8
+ BEGIN
9
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
10
+ RAISE EXCEPTION '0439 requires application database roles' USING ERRCODE = '55000';
11
+ END IF;
12
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
13
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
14
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
15
+ OR octet_length(item #>> '{}') > 63
16
+ ) THEN RAISE EXCEPTION '0439 invalid application roles' USING ERRCODE = '55000'; END IF;
17
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
18
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
19
+ RAISE EXCEPTION '0439 requires stopped application sessions' USING ERRCODE = '55000';
20
+ END IF;
21
+ END
22
+ $drain$;
23
+
24
+ CREATE TABLE external_identities (
25
+ id uuid PRIMARY KEY,
26
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
27
+ source text NOT NULL CHECK (char_length(source) BETWEEN 1 AND 200),
28
+ external_id text NOT NULL CHECK (char_length(external_id) BETWEEN 1 AND 1024),
29
+ subject_id text NOT NULL CHECK (subject_id = 'external_user:' || id::text),
30
+ organization_membership_id uuid NOT NULL,
31
+ personal_workspace_id uuid NOT NULL,
32
+ status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','revoked')),
33
+ authorization_revision bigint NOT NULL DEFAULT 1 CHECK (authorization_revision > 0),
34
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
35
+ updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
36
+ UNIQUE (account_id, source, external_id),
37
+ UNIQUE (account_id, subject_id),
38
+ FOREIGN KEY (organization_membership_id, account_id) REFERENCES organization_memberships(id, account_id),
39
+ FOREIGN KEY (personal_workspace_id, account_id) REFERENCES workspaces(id, account_id)
40
+ );
41
+ ALTER TABLE external_identities ENABLE ROW LEVEL SECURITY;
42
+ ALTER TABLE external_identities FORCE ROW LEVEL SECURITY;
43
+ CREATE POLICY external_identity_lifecycle ON external_identities
44
+ USING (account_id = opengeni_private.current_account_id()
45
+ AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning')
46
+ WITH CHECK (account_id = opengeni_private.current_account_id()
47
+ AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning');
48
+ CREATE POLICY external_identity_provisioning ON organization_memberships
49
+ USING (account_id = opengeni_private.current_account_id()
50
+ AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning')
51
+ WITH CHECK (account_id = opengeni_private.current_account_id()
52
+ AND current_setting('opengeni.organization_tenancy_lifecycle', true) = 'external_identity_provisioning');
53
+ REVOKE ALL ON external_identities FROM PUBLIC;
54
+
55
+ -- Account scope is established by the authenticated API before this call.
56
+ -- Like other lifecycle functions, this is not itself an HTTP authenticator.
57
+ CREATE FUNCTION ensure_external_identity(p_account_id uuid, p_source text, p_external_id text)
58
+ RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT
59
+ AS $body$
60
+ DECLARE
61
+ identity_row external_identities%ROWTYPE;
62
+ membership_row organization_memberships%ROWTYPE;
63
+ identity_id uuid;
64
+ personal_id uuid;
65
+ membership_id uuid;
66
+ subject text;
67
+ previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
68
+ previous_workspace text := current_setting('opengeni.workspace_id', true);
69
+ BEGIN
70
+ IF p_account_id IS NULL OR p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
71
+ OR p_source IS NULL OR char_length(p_source) NOT BETWEEN 1 AND 200
72
+ OR p_external_id IS NULL OR char_length(p_external_id) NOT BETWEEN 1 AND 1024
73
+ THEN RAISE EXCEPTION 'invalid external identity scope' USING ERRCODE = '42501'; END IF;
74
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'external_identity_provisioning', true);
75
+ -- Serialize a tuple without relying on delimiters in opaque host identifiers.
76
+ PERFORM pg_advisory_xact_lock(hashtextextended(jsonb_build_array(p_account_id,p_source,p_external_id)::text, 0));
77
+ SELECT * INTO identity_row FROM external_identities
78
+ WHERE account_id = p_account_id AND source = p_source AND external_id = p_external_id FOR UPDATE;
79
+ IF NOT FOUND THEN
80
+ identity_id := gen_random_uuid();
81
+ subject := 'external_user:' || identity_id::text;
82
+ PERFORM set_config('opengeni.workspace_id', '', true);
83
+ INSERT INTO workspaces (account_id, name, external_source, external_id)
84
+ VALUES (p_account_id, 'Personal workspace', 'opengeni:organization-membership', p_account_id::text || ':' || subject)
85
+ RETURNING id INTO personal_id;
86
+ PERFORM set_config('opengeni.workspace_id', personal_id::text, true);
87
+ INSERT INTO workspace_inference_controls (workspace_id, account_id) VALUES (personal_id, p_account_id);
88
+ INSERT INTO organization_memberships (account_id, subject_id, role, status, personal_workspace_id)
89
+ VALUES (p_account_id, subject, 'member', 'active', personal_id) RETURNING id INTO membership_id;
90
+ INSERT INTO external_identities (id, account_id, source, external_id, subject_id, organization_membership_id, personal_workspace_id)
91
+ VALUES (identity_id, p_account_id, p_source, p_external_id, subject, membership_id, personal_id)
92
+ RETURNING * INTO identity_row;
93
+ END IF;
94
+ SELECT * INTO membership_row FROM organization_memberships
95
+ WHERE id = identity_row.organization_membership_id AND account_id = p_account_id FOR UPDATE;
96
+ IF NOT FOUND OR identity_row.status <> 'active' OR membership_row.status <> 'active'
97
+ OR membership_row.subject_id IS DISTINCT FROM identity_row.subject_id
98
+ OR membership_row.personal_workspace_id IS DISTINCT FROM identity_row.personal_workspace_id
99
+ THEN RAISE EXCEPTION 'external identity is unavailable' USING ERRCODE = '42501'; END IF;
100
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
101
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace,''), true);
102
+ RETURN jsonb_build_object('id', identity_row.id, 'accountId', identity_row.account_id,
103
+ 'source', identity_row.source, 'externalId', identity_row.external_id,
104
+ 'subjectId', identity_row.subject_id, 'organizationMembershipId', identity_row.organization_membership_id,
105
+ 'personalWorkspaceId', identity_row.personal_workspace_id, 'status', identity_row.status,
106
+ 'authorizationRevision', identity_row.authorization_revision);
107
+ EXCEPTION WHEN OTHERS THEN
108
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
109
+ PERFORM set_config('opengeni.workspace_id', coalesce(previous_workspace,''), true);
110
+ RAISE;
111
+ END
112
+ $body$;
113
+ REVOKE ALL ON FUNCTION ensure_external_identity(uuid,text,text) FROM PUBLIC;
114
+ DO $acl$
115
+ DECLARE target_role record;
116
+ BEGIN
117
+ FOR target_role IN SELECT DISTINCT role.rolname FROM pg_class relation
118
+ CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
119
+ JOIN pg_roles role ON role.oid = privilege.grantee
120
+ WHERE relation.oid = 'external_identities'::regclass AND privilege.grantee <> relation.relowner
121
+ LOOP EXECUTE format('REVOKE ALL ON external_identities FROM %I', target_role.rolname); END LOOP;
122
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a
123
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
124
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
125
+ RAISE EXCEPTION '0439 requires stopped application sessions' USING ERRCODE = '55000';
126
+ END IF;
127
+ END
128
+ $acl$;
@@ -0,0 +1,59 @@
1
+ -- deployment-mode: rolling
2
+ -- Extend the existing fenced teardown, never replace its settlement or lock
3
+ -- protocol. Service authority is limited to external members in shared workspaces.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '5min';
6
+
7
+ DO $patch$
8
+ DECLARE
9
+ definition text;
10
+ anchor text;
11
+ replacement text;
12
+ BEGIN
13
+ definition := pg_get_functiondef(
14
+ 'opengeni_private.assert_workspace_membership_removal_actor(uuid,uuid,text,text)'::regprocedure);
15
+ anchor := ' actor_can_administer boolean;';
16
+ IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1 THEN
17
+ RAISE EXCEPTION '0440 removal declaration drift' USING ERRCODE = '55000';
18
+ END IF;
19
+ definition := replace(definition, anchor, anchor || E'\n service_permissions jsonb;\n service_can_administer boolean := false;');
20
+ anchor := ' SELECT actor_is_organization_administrator OR EXISTS (';
21
+ replacement := $service$
22
+ IF p_actor_subject LIKE 'api_key:%' THEN
23
+ -- The API authenticates the key before supplying this subject. This is a
24
+ -- live consistency fence, not authentication from an arbitrary UUID.
25
+ SELECT credential.permissions INTO service_permissions
26
+ FROM api_keys credential
27
+ WHERE credential.account_id = p_account_id
28
+ AND 'api_key:' || credential.id::text = p_actor_subject
29
+ AND credential.workspace_id IS NULL
30
+ AND credential.credential_kind = 'organization'
31
+ AND credential.revoked_at IS NULL
32
+ AND (credential.expires_at IS NULL OR credential.expires_at > clock_timestamp())
33
+ FOR SHARE;
34
+ service_can_administer := coalesce(
35
+ service_permissions ?| ARRAY['workspace:admin', 'members:manage'], false
36
+ ) AND EXISTS (
37
+ SELECT 1 FROM organization_memberships target
38
+ WHERE target.account_id = p_account_id
39
+ AND target.subject_id = p_target_subject
40
+ AND target.subject_id ~ '^external_user:[0-9a-f-]{36}$'
41
+ );
42
+ END IF;
43
+ SELECT actor_is_organization_administrator OR service_can_administer OR EXISTS (
44
+ $service$;
45
+ IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1 THEN
46
+ RAISE EXCEPTION '0440 removal authority drift' USING ERRCODE = '55000';
47
+ END IF;
48
+ EXECUTE replace(definition, anchor, replacement);
49
+
50
+ definition := pg_get_functiondef('workspace_membership_removal_command(jsonb)'::regprocedure);
51
+ anchor := '''human'', actor_subject, ''workspace.membership.remove''';
52
+ IF (length(definition) - length(replace(definition, anchor, ''))) / length(anchor) <> 1
53
+ OR strpos(definition, 'acquire_session_tenancy_fence') = 0 THEN
54
+ RAISE EXCEPTION '0440 removal receipt or fence drift' USING ERRCODE = '55000';
55
+ END IF;
56
+ EXECUTE replace(definition, anchor,
57
+ 'CASE WHEN actor_subject LIKE ''api_key:%'' THEN ''service'' ELSE ''human'' END, actor_subject, ''workspace.membership.remove''');
58
+ END
59
+ $patch$;