@opengeni/db 0.31.1 → 0.36.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 (122) hide show
  1. package/dist/browser-auth.d.ts +209 -1
  2. package/dist/browser-downloads.d.ts +51 -0
  3. package/dist/browser-identities.d.ts +11 -2
  4. package/dist/browser-sessions.d.ts +83 -1
  5. package/dist/browser-state-artifacts.d.ts +75 -1
  6. package/dist/canonical-human-identities.d.ts +48 -0
  7. package/dist/canonical-human-identities.js +28 -0
  8. package/dist/canonical-human-identities.js.map +1 -0
  9. package/dist/capability-integrations.d.ts +8 -6
  10. package/dist/{chunk-MWDVXQOL.js → chunk-5KTIEDX7.js} +5773 -4212
  11. package/dist/chunk-5KTIEDX7.js.map +1 -0
  12. package/dist/{chunk-NBE6CHRI.js → chunk-CQZ4QSP5.js} +49 -9
  13. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  14. package/dist/chunk-F32YT7MP.js +117 -0
  15. package/dist/chunk-F32YT7MP.js.map +1 -0
  16. package/dist/{chunk-ISI2TIUG.js → chunk-JC5QPLUM.js} +30 -3
  17. package/dist/chunk-JC5QPLUM.js.map +1 -0
  18. package/dist/{chunk-F5FT7BEZ.js → chunk-P3RE4D2B.js} +2 -2
  19. package/dist/chunk-TAUTWOS5.js +259 -0
  20. package/dist/chunk-TAUTWOS5.js.map +1 -0
  21. package/dist/{chunk-YHZXQ7HP.js → chunk-VKLB2ZGI.js} +446 -19
  22. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  23. package/dist/database.d.ts +5 -0
  24. package/dist/editable-artifact-durable-export.js +2 -2
  25. package/dist/editable-artifacts.js +3 -3
  26. package/dist/index.d.ts +438 -29
  27. package/dist/index.js +35640 -28969
  28. package/dist/index.js.map +1 -1
  29. package/dist/insights.d.ts +23 -7
  30. package/dist/integration-bindings.d.ts +24 -24
  31. package/dist/integration-facets.d.ts +99 -0
  32. package/dist/interaction-revisions.d.ts +11 -0
  33. package/dist/interaction-schema.d.ts +698 -34
  34. package/dist/provision-roles.js +1 -1
  35. package/dist/runtime-posture.d.ts +8 -7
  36. package/dist/schema.d.ts +9464 -5412
  37. package/dist/schema.js +51 -9
  38. package/dist/session-control.d.ts +29 -3
  39. package/dist/session-tenancy.d.ts +44 -0
  40. package/dist/session-tenancy.js +16 -0
  41. package/dist/session-tenancy.js.map +1 -0
  42. package/dist/slack-task-policy-schema.d.ts +847 -0
  43. package/dist/slack-task-policy.d.ts +65 -0
  44. package/dist/task-notes-schema.d.ts +924 -0
  45. package/dist/task-notes.d.ts +85 -0
  46. package/dist/video-generation.d.ts +9 -0
  47. package/dist/video-generation.js +5 -3
  48. package/dist/xai-subscription.d.ts +326 -0
  49. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  50. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  51. package/drizzle/0214_browser_download_saves.sql +34 -0
  52. package/drizzle/0215_browser_controller_host.sql +385 -0
  53. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  54. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  55. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  56. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  57. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  58. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  59. package/drizzle/0220_session_channels.sql +54 -0
  60. package/drizzle/0221_sessions_channel_index.sql +5 -0
  61. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  62. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  63. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  64. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  65. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  66. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  67. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  68. package/drizzle/0227_slack_native_actions.sql +145 -0
  69. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  70. package/drizzle/0228_slack_task_policy.sql +400 -0
  71. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  72. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  73. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  74. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  75. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  76. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  77. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  78. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  79. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  80. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  81. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  82. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  83. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  84. package/drizzle/0239_task_tree_notes.sql +990 -0
  85. package/package.json +13 -5
  86. package/src/browser-auth.ts +2600 -123
  87. package/src/browser-downloads.ts +517 -0
  88. package/src/browser-identities.ts +220 -3
  89. package/src/browser-sessions.ts +334 -45
  90. package/src/browser-state-artifacts.ts +402 -12
  91. package/src/canonical-human-identities.ts +317 -0
  92. package/src/capability-integrations.ts +121 -213
  93. package/src/database.ts +41 -1
  94. package/src/index.ts +4157 -394
  95. package/src/insights.ts +109 -28
  96. package/src/integration-bindings.ts +101 -101
  97. package/src/{integration-features.ts → integration-facets.ts} +187 -184
  98. package/src/interaction-revisions.ts +33 -3
  99. package/src/interaction-schema.ts +311 -8
  100. package/src/pack-components.ts +43 -43
  101. package/src/pack-installations.ts +0 -43
  102. package/src/plugin-packages.ts +9 -9
  103. package/src/provision-roles.ts +232 -0
  104. package/src/runtime-posture.ts +227 -14
  105. package/src/schema.ts +1188 -75
  106. package/src/session-control.ts +122 -37
  107. package/src/session-queue-commands.ts +78 -1
  108. package/src/session-tenancy.ts +188 -0
  109. package/src/session-tool-call-settlement.ts +9 -3
  110. package/src/slack-task-policy-schema.ts +130 -0
  111. package/src/slack-task-policy.ts +341 -0
  112. package/src/task-notes-schema.ts +135 -0
  113. package/src/task-notes.ts +178 -0
  114. package/src/video-generation.ts +90 -12
  115. package/src/workspace-artifacts.ts +1 -1
  116. package/src/xai-subscription.ts +1887 -0
  117. package/dist/chunk-ISI2TIUG.js.map +0 -1
  118. package/dist/chunk-MWDVXQOL.js.map +0 -1
  119. package/dist/chunk-NBE6CHRI.js.map +0 -1
  120. package/dist/chunk-YHZXQ7HP.js.map +0 -1
  121. package/dist/integration-features.d.ts +0 -99
  122. /package/dist/{chunk-F5FT7BEZ.js.map → chunk-P3RE4D2B.js.map} +0 -0
@@ -0,0 +1,920 @@
1
+ -- deployment-mode: rolling
2
+ -- Organization-independent canonical human identity and verified login-binding
3
+ -- authority. Identity never implies organization/workspace membership or
4
+ -- resource access. Mutations are lifecycle-only and revoke Better Auth sessions
5
+ -- in the same transaction that advances the monotonic auth revision.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '10min';
9
+
10
+ CREATE TABLE "canonical_human_identities" (
11
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ "display_name" text NOT NULL,
13
+ "status" text NOT NULL DEFAULT 'active',
14
+ "recovery_state" text NOT NULL DEFAULT 'ready',
15
+ "identity_revision" bigint NOT NULL DEFAULT 1,
16
+ "auth_revision" bigint NOT NULL DEFAULT 1,
17
+ "active_login_binding_id" uuid,
18
+ "created_at" timestamptz NOT NULL DEFAULT now(),
19
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
20
+ CONSTRAINT "canonical_human_identities_display_name_check" CHECK (
21
+ length(btrim("display_name")) BETWEEN 1 AND 256
22
+ ),
23
+ CONSTRAINT "canonical_human_identities_status_check" CHECK (
24
+ "status" IN ('active', 'recovery_required', 'disputed', 'disabled')
25
+ ),
26
+ CONSTRAINT "canonical_human_identities_recovery_state_check" CHECK (
27
+ "recovery_state" IN ('ready', 'recovery_required', 'lost_factor', 'disputed', 'disabled')
28
+ ),
29
+ CONSTRAINT "canonical_human_identities_revision_check" CHECK (
30
+ "identity_revision" > 0 AND "auth_revision" > 0
31
+ ),
32
+ CONSTRAINT "canonical_human_identities_state_check" CHECK (
33
+ ("status" = 'active' AND "recovery_state" = 'ready')
34
+ OR ("status" = 'recovery_required' AND "recovery_state" IN ('recovery_required', 'lost_factor'))
35
+ OR ("status" = 'disputed' AND "recovery_state" = 'disputed')
36
+ OR ("status" = 'disabled' AND "recovery_state" = 'disabled')
37
+ )
38
+ );
39
+
40
+ CREATE TABLE "canonical_human_identity_subjects" (
41
+ "auth_user_id" text PRIMARY KEY REFERENCES "auth_users"("id") ON DELETE RESTRICT,
42
+ "identity_id" uuid NOT NULL REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
43
+ "status" text NOT NULL DEFAULT 'active',
44
+ "revision" bigint NOT NULL DEFAULT 1,
45
+ "created_at" timestamptz NOT NULL DEFAULT now(),
46
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
47
+ CONSTRAINT "canonical_human_identity_subjects_user_check" CHECK (
48
+ length(btrim("auth_user_id")) BETWEEN 1 AND 1024
49
+ ),
50
+ CONSTRAINT "canonical_human_identity_subjects_status_check" CHECK (
51
+ "status" IN ('active', 'revoked')
52
+ ),
53
+ CONSTRAINT "canonical_human_identity_subjects_revision_check" CHECK ("revision" > 0)
54
+ );
55
+ CREATE INDEX "canonical_human_identity_subjects_identity_idx"
56
+ ON "canonical_human_identity_subjects" ("identity_id", "status");
57
+
58
+ CREATE TABLE "canonical_human_login_bindings" (
59
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
60
+ "identity_id" uuid NOT NULL REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
61
+ "provider_id" text NOT NULL,
62
+ "provider_account_id" text NOT NULL,
63
+ "status" text NOT NULL DEFAULT 'active',
64
+ "revision" bigint NOT NULL DEFAULT 1,
65
+ "verified_at" timestamptz NOT NULL DEFAULT now(),
66
+ "last_verified_at" timestamptz NOT NULL DEFAULT now(),
67
+ "revoked_at" timestamptz,
68
+ "created_at" timestamptz NOT NULL DEFAULT now(),
69
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
70
+ CONSTRAINT "canonical_human_login_bindings_provider_check" CHECK (
71
+ "provider_id" = lower(btrim("provider_id"))
72
+ AND length("provider_id") BETWEEN 1 AND 128
73
+ AND "provider_id" ~ '^[a-z0-9](?:[a-z0-9._:-]*[a-z0-9])?$'
74
+ ),
75
+ CONSTRAINT "canonical_human_login_bindings_account_check" CHECK (
76
+ length(btrim("provider_account_id")) BETWEEN 1 AND 1024
77
+ ),
78
+ CONSTRAINT "canonical_human_login_bindings_status_check" CHECK (
79
+ "status" IN ('active', 'recovery_pending', 'stale', 'disputed', 'revoked')
80
+ ),
81
+ CONSTRAINT "canonical_human_login_bindings_revision_check" CHECK ("revision" > 0),
82
+ CONSTRAINT "canonical_human_login_bindings_revocation_check" CHECK (
83
+ ("status" = 'revoked' AND "revoked_at" IS NOT NULL)
84
+ OR ("status" <> 'revoked' AND "revoked_at" IS NULL)
85
+ )
86
+ );
87
+ CREATE UNIQUE INDEX "canonical_human_login_bindings_provider_account_idx"
88
+ ON "canonical_human_login_bindings" ("provider_id", "provider_account_id");
89
+ CREATE INDEX "canonical_human_login_bindings_identity_status_idx"
90
+ ON "canonical_human_login_bindings" ("identity_id", "status", "created_at", "id");
91
+
92
+ ALTER TABLE "canonical_human_identities"
93
+ ADD CONSTRAINT "canonical_human_identities_active_binding_fk"
94
+ FOREIGN KEY ("active_login_binding_id")
95
+ REFERENCES "canonical_human_login_bindings"("id") ON DELETE RESTRICT;
96
+
97
+ CREATE TABLE "canonical_human_identity_operations" (
98
+ "operation_id" uuid PRIMARY KEY,
99
+ "identity_id" uuid NOT NULL REFERENCES "canonical_human_identities"("id") ON DELETE RESTRICT,
100
+ "actor_auth_user_id" text NOT NULL REFERENCES "auth_users"("id") ON DELETE RESTRICT,
101
+ "operation_type" text NOT NULL,
102
+ "binding_id" uuid REFERENCES "canonical_human_login_bindings"("id") ON DELETE RESTRICT,
103
+ "provider_id" text,
104
+ "provider_account_id" text,
105
+ "expected_identity_revision" bigint NOT NULL,
106
+ "result_identity_revision" bigint NOT NULL,
107
+ "result_auth_revision" bigint NOT NULL,
108
+ "outcome" text NOT NULL,
109
+ "reason" text NOT NULL,
110
+ "created_at" timestamptz NOT NULL DEFAULT now(),
111
+ CONSTRAINT "canonical_human_identity_operations_type_check" CHECK (
112
+ "operation_type" IN ('link', 'unlink', 'begin_recovery', 'recover')
113
+ ),
114
+ CONSTRAINT "canonical_human_identity_operations_outcome_check" CHECK (
115
+ "outcome" IN ('applied', 'lost_factor', 'disputed')
116
+ ),
117
+ CONSTRAINT "canonical_human_identity_operations_revision_check" CHECK (
118
+ "expected_identity_revision" > 0
119
+ AND "result_identity_revision" > 0
120
+ AND "result_auth_revision" > 0
121
+ ),
122
+ CONSTRAINT "canonical_human_identity_operations_reason_check" CHECK (
123
+ length(btrim("reason")) BETWEEN 1 AND 512
124
+ )
125
+ );
126
+ CREATE INDEX "canonical_human_identity_operations_identity_created_idx"
127
+ ON "canonical_human_identity_operations" ("identity_id", "created_at", "operation_id");
128
+
129
+ ALTER TABLE "auth_sessions"
130
+ ADD COLUMN "identity_id" uuid,
131
+ ADD COLUMN "identity_revision" bigint,
132
+ ADD COLUMN "auth_revision" bigint;
133
+
134
+ DO $canonical_human_backfill$
135
+ DECLARE
136
+ user_row record;
137
+ identity_id uuid;
138
+ BEGIN
139
+ FOR user_row IN
140
+ SELECT id, name, email, created_at, updated_at
141
+ FROM auth_users
142
+ ORDER BY id
143
+ LOOP
144
+ INSERT INTO canonical_human_identities (
145
+ display_name,
146
+ created_at,
147
+ updated_at
148
+ ) VALUES (
149
+ left(coalesce(nullif(btrim(user_row.name), ''), user_row.email), 256),
150
+ user_row.created_at,
151
+ user_row.updated_at
152
+ )
153
+ RETURNING id INTO identity_id;
154
+
155
+ INSERT INTO canonical_human_identity_subjects (
156
+ auth_user_id,
157
+ identity_id,
158
+ created_at,
159
+ updated_at
160
+ ) VALUES (
161
+ user_row.id,
162
+ identity_id,
163
+ user_row.created_at,
164
+ user_row.updated_at
165
+ );
166
+ END LOOP;
167
+
168
+ INSERT INTO canonical_human_login_bindings (
169
+ identity_id,
170
+ provider_id,
171
+ provider_account_id,
172
+ verified_at,
173
+ last_verified_at,
174
+ created_at,
175
+ updated_at
176
+ )
177
+ SELECT
178
+ subject_row.identity_id,
179
+ lower(identity_row.provider_id),
180
+ identity_row.account_id,
181
+ identity_row.created_at,
182
+ greatest(identity_row.created_at, identity_row.updated_at),
183
+ identity_row.created_at,
184
+ identity_row.updated_at
185
+ FROM auth_identities identity_row
186
+ INNER JOIN canonical_human_identity_subjects subject_row
187
+ ON subject_row.auth_user_id = identity_row.user_id
188
+ ORDER BY identity_row.created_at, identity_row.id;
189
+
190
+ UPDATE canonical_human_identities identity_row
191
+ SET active_login_binding_id = (
192
+ SELECT binding.id
193
+ FROM canonical_human_login_bindings binding
194
+ WHERE binding.identity_id = identity_row.id
195
+ AND binding.status = 'active'
196
+ ORDER BY binding.created_at, binding.id
197
+ LIMIT 1
198
+ );
199
+
200
+ UPDATE auth_sessions session_row
201
+ SET identity_id = subject_row.identity_id,
202
+ identity_revision = identity_row.identity_revision,
203
+ auth_revision = identity_row.auth_revision
204
+ FROM canonical_human_identity_subjects subject_row
205
+ INNER JOIN canonical_human_identities identity_row
206
+ ON identity_row.id = subject_row.identity_id
207
+ WHERE subject_row.auth_user_id = session_row.user_id
208
+ AND subject_row.status = 'active';
209
+ END
210
+ $canonical_human_backfill$;
211
+
212
+ ALTER TABLE "auth_sessions"
213
+ ALTER COLUMN "identity_id" SET NOT NULL,
214
+ ALTER COLUMN "identity_revision" SET NOT NULL,
215
+ ALTER COLUMN "auth_revision" SET NOT NULL,
216
+ ADD CONSTRAINT "auth_sessions_canonical_identity_fk"
217
+ FOREIGN KEY ("identity_id") REFERENCES "canonical_human_identities"("id") ON DELETE CASCADE,
218
+ ADD CONSTRAINT "auth_sessions_canonical_identity_revision_check" CHECK (
219
+ "identity_revision" > 0 AND "auth_revision" > 0
220
+ );
221
+ CREATE INDEX "auth_sessions_identity_revision_idx"
222
+ ON "auth_sessions" ("identity_id", "auth_revision", "identity_revision");
223
+
224
+ ALTER TABLE "canonical_human_identities" ENABLE ROW LEVEL SECURITY;
225
+ ALTER TABLE "canonical_human_identities" FORCE ROW LEVEL SECURITY;
226
+ ALTER TABLE "canonical_human_identity_subjects" ENABLE ROW LEVEL SECURITY;
227
+ ALTER TABLE "canonical_human_identity_subjects" FORCE ROW LEVEL SECURITY;
228
+ ALTER TABLE "canonical_human_login_bindings" ENABLE ROW LEVEL SECURITY;
229
+ ALTER TABLE "canonical_human_login_bindings" FORCE ROW LEVEL SECURITY;
230
+ ALTER TABLE "canonical_human_identity_operations" ENABLE ROW LEVEL SECURITY;
231
+ ALTER TABLE "canonical_human_identity_operations" FORCE ROW LEVEL SECURITY;
232
+
233
+ CREATE POLICY canonical_human_identity_lifecycle ON "canonical_human_identities"
234
+ USING (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active')
235
+ WITH CHECK (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active');
236
+ CREATE POLICY canonical_human_identity_lifecycle ON "canonical_human_identity_subjects"
237
+ USING (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active')
238
+ WITH CHECK (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active');
239
+ CREATE POLICY canonical_human_identity_lifecycle ON "canonical_human_login_bindings"
240
+ USING (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active')
241
+ WITH CHECK (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active');
242
+ CREATE POLICY canonical_human_identity_lifecycle ON "canonical_human_identity_operations"
243
+ USING (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active')
244
+ WITH CHECK (current_setting('opengeni.canonical_human_identity_lifecycle', true) = 'active');
245
+
246
+ DO $canonical_human_identity_routines$
247
+ DECLARE
248
+ data_schema text := current_schema();
249
+ BEGIN
250
+ EXECUTE format($ddl$
251
+ CREATE OR REPLACE FUNCTION %1$I.ensure_canonical_human_identity(
252
+ p_auth_user_id text,
253
+ p_display_name text
254
+ )
255
+ RETURNS TABLE (
256
+ identity_id uuid,
257
+ identity_revision bigint,
258
+ auth_revision bigint,
259
+ identity_status text
260
+ )
261
+ LANGUAGE plpgsql
262
+ SECURITY DEFINER
263
+ SET search_path = %1$I, pg_catalog
264
+ AS $body$
265
+ DECLARE
266
+ subject_row canonical_human_identity_subjects%%ROWTYPE;
267
+ identity_row canonical_human_identities%%ROWTYPE;
268
+ normalized_name text;
269
+ stale_count integer;
270
+ previous_marker text := pg_catalog.current_setting(
271
+ 'opengeni.canonical_human_identity_lifecycle', true
272
+ );
273
+ BEGIN
274
+ IF p_auth_user_id IS NULL
275
+ OR p_auth_user_id <> pg_catalog.btrim(p_auth_user_id)
276
+ OR pg_catalog.length(p_auth_user_id) NOT BETWEEN 1 AND 1024
277
+ THEN
278
+ RAISE EXCEPTION 'canonical human auth user is invalid' USING ERRCODE = '42501';
279
+ END IF;
280
+ normalized_name := pg_catalog.left(pg_catalog.btrim(p_display_name), 256);
281
+ IF normalized_name IS NULL OR pg_catalog.length(normalized_name) < 1 THEN
282
+ RAISE EXCEPTION 'canonical human display name is invalid' USING ERRCODE = '22023';
283
+ END IF;
284
+ IF NOT EXISTS (SELECT 1 FROM auth_users WHERE id = p_auth_user_id) THEN
285
+ RAISE EXCEPTION 'canonical human auth user was not found' USING ERRCODE = 'P0002';
286
+ END IF;
287
+
288
+ PERFORM pg_catalog.pg_advisory_xact_lock(
289
+ pg_catalog.hashtextextended('canonical-human-user:' || p_auth_user_id, 0)
290
+ );
291
+ PERFORM pg_catalog.set_config(
292
+ 'opengeni.canonical_human_identity_lifecycle', 'active', true
293
+ );
294
+
295
+ SELECT * INTO subject_row
296
+ FROM canonical_human_identity_subjects
297
+ WHERE auth_user_id = p_auth_user_id
298
+ FOR UPDATE;
299
+
300
+ IF NOT FOUND THEN
301
+ INSERT INTO canonical_human_identities (display_name)
302
+ VALUES (normalized_name)
303
+ RETURNING * INTO identity_row;
304
+ INSERT INTO canonical_human_identity_subjects (auth_user_id, identity_id)
305
+ VALUES (p_auth_user_id, identity_row.id)
306
+ RETURNING * INTO subject_row;
307
+ ELSE
308
+ IF subject_row.status <> 'active' THEN
309
+ RAISE EXCEPTION 'canonical human subject is not active' USING ERRCODE = '42501';
310
+ END IF;
311
+ SELECT * INTO identity_row
312
+ FROM canonical_human_identities
313
+ WHERE id = subject_row.identity_id
314
+ FOR UPDATE;
315
+ END IF;
316
+
317
+ IF identity_row.display_name IS DISTINCT FROM normalized_name THEN
318
+ UPDATE canonical_human_identities AS renamed_identity
319
+ SET display_name = normalized_name,
320
+ identity_revision = renamed_identity.identity_revision + 1,
321
+ updated_at = pg_catalog.clock_timestamp()
322
+ WHERE renamed_identity.id = identity_row.id
323
+ RETURNING * INTO identity_row;
324
+ END IF;
325
+
326
+ WITH stale AS (
327
+ UPDATE canonical_human_login_bindings binding
328
+ SET status = 'stale',
329
+ revision = binding.revision + 1,
330
+ updated_at = pg_catalog.clock_timestamp()
331
+ WHERE binding.identity_id = identity_row.id
332
+ AND binding.status = 'active'
333
+ AND NOT EXISTS (
334
+ SELECT 1
335
+ FROM auth_identities auth_identity
336
+ WHERE auth_identity.user_id = p_auth_user_id
337
+ AND lower(auth_identity.provider_id) = binding.provider_id
338
+ AND auth_identity.account_id = binding.provider_account_id
339
+ )
340
+ RETURNING 1
341
+ )
342
+ SELECT count(*)::integer INTO stale_count FROM stale;
343
+
344
+ IF stale_count > 0 THEN
345
+ UPDATE canonical_human_identities AS stale_identity
346
+ SET status = 'recovery_required',
347
+ recovery_state = 'recovery_required',
348
+ identity_revision = stale_identity.identity_revision + 1,
349
+ auth_revision = stale_identity.auth_revision + 1,
350
+ active_login_binding_id = (
351
+ SELECT binding.id
352
+ FROM canonical_human_login_bindings binding
353
+ WHERE binding.identity_id = identity_row.id
354
+ AND binding.status = 'active'
355
+ ORDER BY binding.created_at, binding.id
356
+ LIMIT 1
357
+ ),
358
+ updated_at = pg_catalog.clock_timestamp()
359
+ WHERE stale_identity.id = identity_row.id
360
+ RETURNING * INTO identity_row;
361
+ DELETE FROM auth_sessions WHERE identity_id = identity_row.id;
362
+ END IF;
363
+
364
+ PERFORM pg_catalog.set_config(
365
+ 'opengeni.canonical_human_identity_lifecycle',
366
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
367
+ true
368
+ );
369
+ identity_id := identity_row.id;
370
+ identity_revision := identity_row.identity_revision;
371
+ auth_revision := identity_row.auth_revision;
372
+ identity_status := identity_row.status;
373
+ RETURN NEXT;
374
+ EXCEPTION WHEN OTHERS THEN
375
+ PERFORM pg_catalog.set_config(
376
+ 'opengeni.canonical_human_identity_lifecycle',
377
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
378
+ true
379
+ );
380
+ RAISE;
381
+ END;
382
+ $body$;
383
+ $ddl$, data_schema);
384
+
385
+ EXECUTE format($ddl$
386
+ CREATE OR REPLACE FUNCTION %1$I.validate_canonical_human_session(
387
+ p_auth_session_id text,
388
+ p_auth_user_id text,
389
+ p_allow_recovery boolean DEFAULT false
390
+ )
391
+ RETURNS boolean
392
+ LANGUAGE plpgsql
393
+ SECURITY DEFINER
394
+ SET search_path = %1$I, pg_catalog
395
+ AS $body$
396
+ DECLARE
397
+ valid boolean;
398
+ revisions_match boolean;
399
+ current_status text;
400
+ previous_marker text := pg_catalog.current_setting(
401
+ 'opengeni.canonical_human_identity_lifecycle', true
402
+ );
403
+ BEGIN
404
+ PERFORM pg_catalog.set_config(
405
+ 'opengeni.canonical_human_identity_lifecycle', 'active', true
406
+ );
407
+ SELECT
408
+ session_row.identity_revision = identity_row.identity_revision
409
+ AND session_row.auth_revision = identity_row.auth_revision,
410
+ identity_row.status
411
+ INTO revisions_match, current_status
412
+ FROM auth_sessions session_row
413
+ INNER JOIN canonical_human_identity_subjects subject_row
414
+ ON subject_row.auth_user_id = session_row.user_id
415
+ AND subject_row.identity_id = session_row.identity_id
416
+ AND subject_row.status = 'active'
417
+ INNER JOIN canonical_human_identities identity_row
418
+ ON identity_row.id = session_row.identity_id
419
+ WHERE session_row.id = p_auth_session_id
420
+ AND session_row.user_id = p_auth_user_id
421
+ AND subject_row.status = 'active';
422
+ valid := coalesce(revisions_match, false) AND (
423
+ current_status = 'active'
424
+ OR (p_allow_recovery AND current_status = 'recovery_required')
425
+ );
426
+ IF NOT valid AND (
427
+ revisions_match IS DISTINCT FROM true
428
+ OR current_status IS NULL
429
+ OR current_status IN ('disputed', 'disabled')
430
+ ) THEN
431
+ DELETE FROM auth_sessions
432
+ WHERE id = p_auth_session_id AND user_id = p_auth_user_id;
433
+ END IF;
434
+ PERFORM pg_catalog.set_config(
435
+ 'opengeni.canonical_human_identity_lifecycle',
436
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
437
+ true
438
+ );
439
+ RETURN valid;
440
+ EXCEPTION WHEN OTHERS THEN
441
+ PERFORM pg_catalog.set_config(
442
+ 'opengeni.canonical_human_identity_lifecycle',
443
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
444
+ true
445
+ );
446
+ RAISE;
447
+ END;
448
+ $body$;
449
+ $ddl$, data_schema);
450
+
451
+ EXECUTE format($ddl$
452
+ CREATE OR REPLACE FUNCTION %1$I.get_canonical_human_identity_projection(
453
+ p_auth_user_id text
454
+ )
455
+ RETURNS jsonb
456
+ LANGUAGE plpgsql
457
+ SECURITY DEFINER
458
+ SET search_path = %1$I, pg_catalog
459
+ AS $body$
460
+ DECLARE
461
+ result jsonb;
462
+ previous_marker text := pg_catalog.current_setting(
463
+ 'opengeni.canonical_human_identity_lifecycle', true
464
+ );
465
+ BEGIN
466
+ PERFORM pg_catalog.set_config(
467
+ 'opengeni.canonical_human_identity_lifecycle', 'active', true
468
+ );
469
+ SELECT pg_catalog.jsonb_build_object(
470
+ 'activeIdentity', pg_catalog.jsonb_build_object(
471
+ 'id', identity_row.id,
472
+ 'displayName', identity_row.display_name,
473
+ 'status', identity_row.status,
474
+ 'identityRevision', identity_row.identity_revision,
475
+ 'authRevision', identity_row.auth_revision,
476
+ 'activeLoginBindingId', identity_row.active_login_binding_id,
477
+ 'recoveryState', identity_row.recovery_state,
478
+ 'createdAt', pg_catalog.to_char(
479
+ identity_row.created_at AT TIME ZONE 'UTC',
480
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
481
+ ),
482
+ 'updatedAt', pg_catalog.to_char(
483
+ identity_row.updated_at AT TIME ZONE 'UTC',
484
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
485
+ )
486
+ ),
487
+ 'loginBindings', coalesce((
488
+ SELECT pg_catalog.jsonb_agg(
489
+ pg_catalog.jsonb_build_object(
490
+ 'id', binding.id,
491
+ 'providerId', binding.provider_id,
492
+ 'providerAccountId', binding.provider_account_id,
493
+ 'status', binding.status,
494
+ 'revision', binding.revision,
495
+ 'verifiedAt', pg_catalog.to_char(
496
+ binding.verified_at AT TIME ZONE 'UTC',
497
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
498
+ ),
499
+ 'lastVerifiedAt', pg_catalog.to_char(
500
+ binding.last_verified_at AT TIME ZONE 'UTC',
501
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
502
+ ),
503
+ 'revokedAt', pg_catalog.to_char(
504
+ binding.revoked_at AT TIME ZONE 'UTC',
505
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
506
+ ),
507
+ 'createdAt', pg_catalog.to_char(
508
+ binding.created_at AT TIME ZONE 'UTC',
509
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
510
+ ),
511
+ 'updatedAt', pg_catalog.to_char(
512
+ binding.updated_at AT TIME ZONE 'UTC',
513
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
514
+ )
515
+ ) ORDER BY binding.created_at, binding.id
516
+ )
517
+ FROM canonical_human_login_bindings binding
518
+ WHERE binding.identity_id = identity_row.id
519
+ ), '[]'::jsonb)
520
+ ) INTO result
521
+ FROM canonical_human_identity_subjects subject_row
522
+ INNER JOIN canonical_human_identities identity_row
523
+ ON identity_row.id = subject_row.identity_id
524
+ WHERE subject_row.auth_user_id = p_auth_user_id
525
+ AND subject_row.status = 'active';
526
+ PERFORM pg_catalog.set_config(
527
+ 'opengeni.canonical_human_identity_lifecycle',
528
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
529
+ true
530
+ );
531
+ RETURN result;
532
+ EXCEPTION WHEN OTHERS THEN
533
+ PERFORM pg_catalog.set_config(
534
+ 'opengeni.canonical_human_identity_lifecycle',
535
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
536
+ true
537
+ );
538
+ RAISE;
539
+ END;
540
+ $body$;
541
+ $ddl$, data_schema);
542
+
543
+ EXECUTE format($ddl$
544
+ CREATE OR REPLACE FUNCTION %1$I.apply_canonical_human_identity_operation(
545
+ p_operation_id uuid,
546
+ p_auth_user_id text,
547
+ p_expected_identity_revision bigint,
548
+ p_operation_type text,
549
+ p_binding_id uuid,
550
+ p_provider_id text,
551
+ p_provider_account_id text,
552
+ p_reason text
553
+ )
554
+ RETURNS TABLE (
555
+ outcome text,
556
+ identity_id uuid,
557
+ identity_revision bigint,
558
+ auth_revision bigint
559
+ )
560
+ LANGUAGE plpgsql
561
+ SECURITY DEFINER
562
+ SET search_path = %1$I, pg_catalog
563
+ AS $body$
564
+ DECLARE
565
+ subject_row canonical_human_identity_subjects%%ROWTYPE;
566
+ identity_row canonical_human_identities%%ROWTYPE;
567
+ binding_row canonical_human_login_bindings%%ROWTYPE;
568
+ prior_operation canonical_human_identity_operations%%ROWTYPE;
569
+ normalized_provider text;
570
+ active_count integer;
571
+ replacement_binding_id uuid;
572
+ previous_marker text := pg_catalog.current_setting(
573
+ 'opengeni.canonical_human_identity_lifecycle', true
574
+ );
575
+ BEGIN
576
+ IF p_operation_id IS NULL
577
+ OR p_expected_identity_revision IS NULL
578
+ OR p_expected_identity_revision < 1
579
+ OR p_operation_type NOT IN ('link', 'unlink', 'begin_recovery', 'recover')
580
+ OR p_reason IS NULL
581
+ OR p_reason <> pg_catalog.btrim(p_reason)
582
+ OR pg_catalog.length(p_reason) NOT BETWEEN 1 AND 512
583
+ THEN
584
+ RAISE EXCEPTION 'canonical human identity operation is invalid' USING ERRCODE = '22023';
585
+ END IF;
586
+ normalized_provider := lower(pg_catalog.btrim(p_provider_id));
587
+ IF p_operation_type = 'link' AND (
588
+ normalized_provider IS NULL
589
+ OR pg_catalog.length(normalized_provider) NOT BETWEEN 1 AND 128
590
+ OR normalized_provider !~ '^[a-z0-9](?:[a-z0-9._:-]*[a-z0-9])?$'
591
+ OR p_provider_account_id IS NULL
592
+ OR p_provider_account_id <> pg_catalog.btrim(p_provider_account_id)
593
+ OR pg_catalog.length(p_provider_account_id) NOT BETWEEN 1 AND 1024
594
+ ) THEN
595
+ RAISE EXCEPTION 'canonical human login binding is invalid' USING ERRCODE = '22023';
596
+ END IF;
597
+ IF p_operation_type <> 'link' AND p_binding_id IS NULL THEN
598
+ RAISE EXCEPTION 'canonical human login binding id is required' USING ERRCODE = '22023';
599
+ END IF;
600
+
601
+ PERFORM pg_catalog.set_config(
602
+ 'opengeni.canonical_human_identity_lifecycle', 'active', true
603
+ );
604
+ SELECT * INTO prior_operation
605
+ FROM canonical_human_identity_operations
606
+ WHERE operation_id = p_operation_id;
607
+ IF FOUND THEN
608
+ IF prior_operation.actor_auth_user_id IS DISTINCT FROM p_auth_user_id
609
+ OR prior_operation.operation_type IS DISTINCT FROM p_operation_type
610
+ OR prior_operation.binding_id IS DISTINCT FROM p_binding_id
611
+ OR prior_operation.provider_id IS DISTINCT FROM normalized_provider
612
+ OR prior_operation.provider_account_id IS DISTINCT FROM p_provider_account_id
613
+ OR prior_operation.expected_identity_revision IS DISTINCT FROM p_expected_identity_revision
614
+ OR prior_operation.reason IS DISTINCT FROM p_reason
615
+ THEN
616
+ RAISE EXCEPTION 'canonical human operation id was reused' USING ERRCODE = '23505';
617
+ END IF;
618
+ outcome := prior_operation.outcome;
619
+ identity_id := prior_operation.identity_id;
620
+ identity_revision := prior_operation.result_identity_revision;
621
+ auth_revision := prior_operation.result_auth_revision;
622
+ PERFORM pg_catalog.set_config(
623
+ 'opengeni.canonical_human_identity_lifecycle',
624
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
625
+ true
626
+ );
627
+ RETURN NEXT;
628
+ RETURN;
629
+ END IF;
630
+
631
+ SELECT * INTO subject_row
632
+ FROM canonical_human_identity_subjects
633
+ WHERE auth_user_id = p_auth_user_id
634
+ AND status = 'active';
635
+ IF NOT FOUND THEN
636
+ RAISE EXCEPTION 'canonical human subject was not found' USING ERRCODE = 'P0002';
637
+ END IF;
638
+ SELECT * INTO identity_row
639
+ FROM canonical_human_identities
640
+ WHERE id = subject_row.identity_id
641
+ FOR UPDATE;
642
+ IF identity_row.identity_revision <> p_expected_identity_revision THEN
643
+ RAISE EXCEPTION 'canonical human identity revision conflict' USING ERRCODE = '40001';
644
+ END IF;
645
+ IF identity_row.status IN ('disputed', 'disabled') THEN
646
+ RAISE EXCEPTION 'canonical human identity is not mutable' USING ERRCODE = '42501';
647
+ END IF;
648
+ IF p_operation_type = 'link' AND identity_row.status = 'recovery_required' THEN
649
+ RAISE EXCEPTION 'canonical human identity requires explicit recovery completion'
650
+ USING ERRCODE = '42501';
651
+ END IF;
652
+
653
+ IF p_operation_type = 'link' THEN
654
+ IF NOT EXISTS (
655
+ SELECT 1 FROM auth_identities
656
+ WHERE user_id = p_auth_user_id
657
+ AND lower(provider_id) = normalized_provider
658
+ AND account_id = p_provider_account_id
659
+ ) THEN
660
+ RAISE EXCEPTION 'login binding is not verified by the authentication provider'
661
+ USING ERRCODE = '42501';
662
+ END IF;
663
+ PERFORM pg_catalog.pg_advisory_xact_lock(
664
+ pg_catalog.hashtextextended(
665
+ 'canonical-human-binding:' || normalized_provider || ':' || p_provider_account_id,
666
+ 0
667
+ )
668
+ );
669
+ SELECT * INTO binding_row
670
+ FROM canonical_human_login_bindings
671
+ WHERE provider_id = normalized_provider
672
+ AND provider_account_id = p_provider_account_id
673
+ FOR UPDATE;
674
+ IF FOUND AND binding_row.identity_id <> identity_row.id THEN
675
+ UPDATE canonical_human_login_bindings
676
+ SET status = 'disputed',
677
+ revision = revision + 1,
678
+ revoked_at = NULL,
679
+ updated_at = pg_catalog.clock_timestamp()
680
+ WHERE id = binding_row.id;
681
+ UPDATE canonical_human_identities AS disputed_identity
682
+ SET status = 'disputed',
683
+ recovery_state = 'disputed',
684
+ identity_revision = disputed_identity.identity_revision + 1,
685
+ auth_revision = disputed_identity.auth_revision + 1,
686
+ updated_at = pg_catalog.clock_timestamp()
687
+ WHERE disputed_identity.id IN (identity_row.id, binding_row.identity_id);
688
+ DELETE FROM auth_sessions AS disputed_session
689
+ WHERE disputed_session.identity_id IN (identity_row.id, binding_row.identity_id);
690
+ SELECT * INTO identity_row
691
+ FROM canonical_human_identities WHERE id = identity_row.id;
692
+ outcome := 'disputed';
693
+ ELSE
694
+ IF NOT FOUND THEN
695
+ INSERT INTO canonical_human_login_bindings (
696
+ identity_id, provider_id, provider_account_id
697
+ ) VALUES (
698
+ identity_row.id, normalized_provider, p_provider_account_id
699
+ ) RETURNING * INTO binding_row;
700
+ ELSE
701
+ UPDATE canonical_human_login_bindings
702
+ SET status = 'active',
703
+ revision = revision + 1,
704
+ last_verified_at = pg_catalog.clock_timestamp(),
705
+ revoked_at = NULL,
706
+ updated_at = pg_catalog.clock_timestamp()
707
+ WHERE id = binding_row.id
708
+ RETURNING * INTO binding_row;
709
+ END IF;
710
+ UPDATE canonical_human_identities AS linked_identity
711
+ SET status = 'active',
712
+ recovery_state = 'ready',
713
+ active_login_binding_id = binding_row.id,
714
+ identity_revision = linked_identity.identity_revision + 1,
715
+ auth_revision = linked_identity.auth_revision + 1,
716
+ updated_at = pg_catalog.clock_timestamp()
717
+ WHERE linked_identity.id = identity_row.id
718
+ RETURNING * INTO identity_row;
719
+ DELETE FROM auth_sessions AS linked_session
720
+ WHERE linked_session.identity_id = identity_row.id;
721
+ outcome := 'applied';
722
+ END IF;
723
+ ELSE
724
+ SELECT * INTO binding_row
725
+ FROM canonical_human_login_bindings AS requested_binding
726
+ WHERE requested_binding.id = p_binding_id
727
+ AND requested_binding.identity_id = identity_row.id
728
+ FOR UPDATE;
729
+ IF NOT FOUND THEN
730
+ RAISE EXCEPTION 'canonical human login binding was not found' USING ERRCODE = 'P0002';
731
+ END IF;
732
+
733
+ IF p_operation_type = 'unlink' THEN
734
+ SELECT count(*)::integer INTO active_count
735
+ FROM canonical_human_login_bindings AS remaining_binding
736
+ WHERE remaining_binding.identity_id = identity_row.id
737
+ AND remaining_binding.status = 'active'
738
+ AND remaining_binding.id <> binding_row.id;
739
+ IF active_count < 1 THEN
740
+ UPDATE canonical_human_login_bindings
741
+ SET status = 'recovery_pending',
742
+ revision = revision + 1,
743
+ updated_at = pg_catalog.clock_timestamp()
744
+ WHERE id = binding_row.id;
745
+ UPDATE canonical_human_identities AS lost_factor_identity
746
+ SET status = 'recovery_required',
747
+ recovery_state = 'lost_factor',
748
+ active_login_binding_id = NULL,
749
+ identity_revision = lost_factor_identity.identity_revision + 1,
750
+ auth_revision = lost_factor_identity.auth_revision + 1,
751
+ updated_at = pg_catalog.clock_timestamp()
752
+ WHERE lost_factor_identity.id = identity_row.id
753
+ RETURNING * INTO identity_row;
754
+ outcome := 'lost_factor';
755
+ ELSE
756
+ DELETE FROM auth_identities
757
+ WHERE user_id = p_auth_user_id
758
+ AND lower(provider_id) = binding_row.provider_id
759
+ AND account_id = binding_row.provider_account_id;
760
+ UPDATE canonical_human_login_bindings
761
+ SET status = 'revoked',
762
+ revision = revision + 1,
763
+ revoked_at = pg_catalog.clock_timestamp(),
764
+ updated_at = pg_catalog.clock_timestamp()
765
+ WHERE id = binding_row.id;
766
+ SELECT replacement_binding.id INTO replacement_binding_id
767
+ FROM canonical_human_login_bindings AS replacement_binding
768
+ WHERE replacement_binding.identity_id = identity_row.id
769
+ AND replacement_binding.status = 'active'
770
+ ORDER BY replacement_binding.created_at, replacement_binding.id LIMIT 1;
771
+ UPDATE canonical_human_identities AS unlinked_identity
772
+ SET active_login_binding_id = replacement_binding_id,
773
+ identity_revision = unlinked_identity.identity_revision + 1,
774
+ auth_revision = unlinked_identity.auth_revision + 1,
775
+ updated_at = pg_catalog.clock_timestamp()
776
+ WHERE unlinked_identity.id = identity_row.id
777
+ RETURNING * INTO identity_row;
778
+ outcome := 'applied';
779
+ END IF;
780
+ ELSIF p_operation_type = 'begin_recovery' THEN
781
+ UPDATE canonical_human_login_bindings
782
+ SET status = 'recovery_pending',
783
+ revision = revision + 1,
784
+ updated_at = pg_catalog.clock_timestamp()
785
+ WHERE id = binding_row.id;
786
+ UPDATE canonical_human_identities AS recovering_identity
787
+ SET status = 'recovery_required',
788
+ recovery_state = 'recovery_required',
789
+ active_login_binding_id = NULL,
790
+ identity_revision = recovering_identity.identity_revision + 1,
791
+ auth_revision = recovering_identity.auth_revision + 1,
792
+ updated_at = pg_catalog.clock_timestamp()
793
+ WHERE recovering_identity.id = identity_row.id
794
+ RETURNING * INTO identity_row;
795
+ outcome := 'applied';
796
+ ELSE
797
+ IF NOT EXISTS (
798
+ SELECT 1 FROM auth_identities
799
+ WHERE user_id = p_auth_user_id
800
+ AND lower(provider_id) = binding_row.provider_id
801
+ AND account_id = binding_row.provider_account_id
802
+ ) THEN
803
+ RAISE EXCEPTION 'login binding has not been reverified'
804
+ USING ERRCODE = '42501';
805
+ END IF;
806
+ UPDATE canonical_human_login_bindings
807
+ SET status = 'active',
808
+ revision = revision + 1,
809
+ last_verified_at = pg_catalog.clock_timestamp(),
810
+ revoked_at = NULL,
811
+ updated_at = pg_catalog.clock_timestamp()
812
+ WHERE id = binding_row.id;
813
+ UPDATE canonical_human_identities AS recovered_identity
814
+ SET status = 'active',
815
+ recovery_state = 'ready',
816
+ active_login_binding_id = binding_row.id,
817
+ identity_revision = recovered_identity.identity_revision + 1,
818
+ auth_revision = recovered_identity.auth_revision + 1,
819
+ updated_at = pg_catalog.clock_timestamp()
820
+ WHERE recovered_identity.id = identity_row.id
821
+ RETURNING * INTO identity_row;
822
+ outcome := 'applied';
823
+ END IF;
824
+ DELETE FROM auth_sessions AS invalidated_session
825
+ WHERE invalidated_session.identity_id = identity_row.id;
826
+ END IF;
827
+
828
+ INSERT INTO canonical_human_identity_operations (
829
+ operation_id,
830
+ identity_id,
831
+ actor_auth_user_id,
832
+ operation_type,
833
+ binding_id,
834
+ provider_id,
835
+ provider_account_id,
836
+ expected_identity_revision,
837
+ result_identity_revision,
838
+ result_auth_revision,
839
+ outcome,
840
+ reason
841
+ ) VALUES (
842
+ p_operation_id,
843
+ identity_row.id,
844
+ p_auth_user_id,
845
+ p_operation_type,
846
+ CASE WHEN p_operation_type = 'link' THEN binding_row.id ELSE p_binding_id END,
847
+ CASE WHEN p_operation_type = 'link' THEN normalized_provider ELSE NULL END,
848
+ CASE WHEN p_operation_type = 'link' THEN p_provider_account_id ELSE NULL END,
849
+ p_expected_identity_revision,
850
+ identity_row.identity_revision,
851
+ identity_row.auth_revision,
852
+ outcome,
853
+ p_reason
854
+ );
855
+
856
+ identity_id := identity_row.id;
857
+ identity_revision := identity_row.identity_revision;
858
+ auth_revision := identity_row.auth_revision;
859
+ PERFORM pg_catalog.set_config(
860
+ 'opengeni.canonical_human_identity_lifecycle',
861
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
862
+ true
863
+ );
864
+ RETURN NEXT;
865
+ EXCEPTION WHEN OTHERS THEN
866
+ PERFORM pg_catalog.set_config(
867
+ 'opengeni.canonical_human_identity_lifecycle',
868
+ CASE WHEN previous_marker IS NULL THEN '' ELSE previous_marker END,
869
+ true
870
+ );
871
+ RAISE;
872
+ END;
873
+ $body$;
874
+ $ddl$, data_schema);
875
+
876
+ EXECUTE format(
877
+ 'REVOKE ALL ON FUNCTION %I.ensure_canonical_human_identity(text,text) FROM PUBLIC',
878
+ data_schema
879
+ );
880
+ EXECUTE format(
881
+ 'REVOKE ALL ON FUNCTION %I.validate_canonical_human_session(text,text,boolean) FROM PUBLIC',
882
+ data_schema
883
+ );
884
+ EXECUTE format(
885
+ 'REVOKE ALL ON FUNCTION %I.get_canonical_human_identity_projection(text) FROM PUBLIC',
886
+ data_schema
887
+ );
888
+ EXECUTE format(
889
+ 'REVOKE ALL ON FUNCTION %I.apply_canonical_human_identity_operation(uuid,text,bigint,text,uuid,text,text,text) FROM PUBLIC',
890
+ data_schema
891
+ );
892
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
893
+ EXECUTE format(
894
+ 'GRANT EXECUTE ON FUNCTION %I.ensure_canonical_human_identity(text,text) TO opengeni_app',
895
+ data_schema
896
+ );
897
+ EXECUTE format(
898
+ 'GRANT EXECUTE ON FUNCTION %I.validate_canonical_human_session(text,text,boolean) TO opengeni_app',
899
+ data_schema
900
+ );
901
+ EXECUTE format(
902
+ 'GRANT EXECUTE ON FUNCTION %I.get_canonical_human_identity_projection(text) TO opengeni_app',
903
+ data_schema
904
+ );
905
+ EXECUTE format(
906
+ 'GRANT EXECUTE ON FUNCTION %I.apply_canonical_human_identity_operation(uuid,text,bigint,text,uuid,text,text,text) TO opengeni_app',
907
+ data_schema
908
+ );
909
+ END IF;
910
+ END
911
+ $canonical_human_identity_routines$;
912
+
913
+ COMMENT ON TABLE "canonical_human_identities" IS
914
+ 'Organization-independent canonical human identity; never organization or workspace authority.';
915
+ COMMENT ON TABLE "canonical_human_login_bindings" IS
916
+ 'Verified provider/account login bindings with deterministic recovery, dispute, stale, and revocation states.';
917
+ COMMENT ON TABLE "canonical_human_identity_operations" IS
918
+ 'Append-only identity lifecycle audit and idempotency ledger; contains no organization or resource metadata.';
919
+ COMMENT ON COLUMN "auth_sessions"."auth_revision" IS
920
+ 'Canonical-human authentication fence stamped at session creation; mismatch invalidates the session immediately.';