thinkwork-cli 0.13.1 → 0.13.2

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 (130) hide show
  1. package/dist/{api-client-4DZ266J5.js → api-client-JYZXYQJR.js} +3 -1
  2. package/dist/bowser-6Z6JFAIR.js +2822 -0
  3. package/dist/chunk-DV3MRBR5.js +43 -0
  4. package/dist/chunk-DVPBI5HM.js +1728 -0
  5. package/dist/chunk-EI5ZX7CN.js +522 -0
  6. package/dist/chunk-EUJ7X6YD.js +126 -0
  7. package/dist/chunk-GG5WKBOX.js +436 -0
  8. package/dist/chunk-I64EZSU5.js +358 -0
  9. package/dist/chunk-ILZKG5WV.js +6151 -0
  10. package/dist/chunk-KMIJ5KGT.js +128 -0
  11. package/dist/chunk-OCPHU34C.js +996 -0
  12. package/dist/chunk-QOERT44A.js +3286 -0
  13. package/dist/chunk-XYM7PE2A.js +2264 -0
  14. package/dist/{chunk-Y2WHJECR.js → chunk-Z2F6RJFR.js} +1 -39
  15. package/dist/cli.js +43080 -13150
  16. package/dist/dist-es-CRV46D5B.js +315 -0
  17. package/dist/dist-es-DCY4IC3Y.js +2000 -0
  18. package/dist/dist-es-ENBUC3JP.js +14726 -0
  19. package/dist/dist-es-G54LBMRN.js +48 -0
  20. package/dist/dist-es-G7PTG75M.js +176 -0
  21. package/dist/dist-es-GKDBE7E6.js +88 -0
  22. package/dist/dist-es-HY3535OI.js +70 -0
  23. package/dist/dist-es-KL7Y5NB2.js +377 -0
  24. package/dist/dist-es-LDE437AM.js +170 -0
  25. package/dist/dist-es-P26FWFLT.js +485 -0
  26. package/dist/dist-es-Q57SZXZ7.js +70 -0
  27. package/dist/dist-es-R7FGL2IB.js +88 -0
  28. package/dist/dist-es-USORBXZ5.js +318 -0
  29. package/dist/dist-es-ZKC2QKD2.js +377 -0
  30. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  31. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  32. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  33. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  34. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  35. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  36. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  37. package/dist/drizzle/0227_analyst_reader_role.sql +6 -0
  38. package/dist/drizzle/0230_analyst_rls.sql +9 -0
  39. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  40. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  41. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  42. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  43. package/dist/drizzle/0261_managed_harness_multiplayer.sql +313 -0
  44. package/dist/drizzle/0261_native_auth_control_plane.sql +328 -0
  45. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  46. package/dist/drizzle/0262_parallel_harness_threads.sql +9 -0
  47. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  48. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  49. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  50. package/dist/drizzle/0264_harness_tool_execution_ledger.sql +113 -0
  51. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  52. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  53. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  54. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  55. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  56. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  57. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  58. package/dist/event-streams-7JNLJEEL.js +894 -0
  59. package/dist/event-streams-KDF5RWR6.js +1515 -0
  60. package/dist/loadSso-DJVB7BXE.js +576 -0
  61. package/dist/loadSso-JAMNA3WY.js +583 -0
  62. package/dist/plugins/catalog/package.json +1 -2
  63. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +0 -1
  64. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +0 -69
  65. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +0 -2
  66. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -5
  67. package/dist/plugins/catalog/src/contracts.ts +2 -179
  68. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -6
  69. package/dist/signin-UGDWTGS4.js +821 -0
  70. package/dist/sso-oidc-Q4VXANXY.js +837 -0
  71. package/dist/sts-CWLSE3GH.js +1241 -0
  72. package/dist/sts-DNZLNVOV.js +1245 -0
  73. package/dist/terraform/examples/greenfield/main.tf +223 -66
  74. package/dist/terraform/examples/greenfield/terraform.tfvars.example +15 -1
  75. package/dist/terraform/modules/app/agentcore-gateway/main.tf +305 -0
  76. package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +44 -0
  77. package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +93 -0
  78. package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +33 -0
  79. package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +543 -0
  80. package/dist/terraform/modules/app/agentcore-gateway/variables.tf +70 -0
  81. package/dist/terraform/modules/app/agentcore-harness/main.tf +237 -8
  82. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +63 -0
  83. package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +5 -0
  84. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +430 -0
  85. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +180 -0
  86. package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +193 -0
  87. package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +5 -0
  88. package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +5 -0
  89. package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +5 -0
  90. package/dist/terraform/modules/app/agentcore-harness/variables.tf +72 -1
  91. package/dist/terraform/modules/app/agentcore-identity/main.tf +173 -0
  92. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  93. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +122 -0
  94. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +25 -0
  95. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  96. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +40 -0
  97. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  98. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +74 -0
  99. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +61 -0
  100. package/dist/terraform/modules/app/agentcore-identity/variables.tf +54 -0
  101. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  102. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  103. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  104. package/dist/terraform/modules/app/deployment-control-plane/runner.py +966 -27
  105. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +581 -22
  106. package/dist/terraform/modules/app/lambda-api/agentcore-harness-alarms.tf +88 -0
  107. package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +91 -0
  108. package/dist/terraform/modules/app/lambda-api/handlers.tf +298 -74
  109. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +99 -54
  110. package/dist/terraform/modules/app/lambda-api/main.tf +24 -0
  111. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +181 -4
  112. package/dist/terraform/modules/app/lambda-api/outputs.tf +35 -0
  113. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  114. package/dist/terraform/modules/app/lambda-api/variables.tf +109 -16
  115. package/dist/terraform/modules/foundation/cognito/main.tf +294 -72
  116. package/dist/terraform/modules/foundation/cognito/outputs.tf +42 -0
  117. package/dist/terraform/modules/foundation/cognito/variables.tf +114 -4
  118. package/dist/terraform/modules/thinkwork/main.tf +269 -46
  119. package/dist/terraform/modules/thinkwork/outputs.tf +136 -8
  120. package/dist/terraform/modules/thinkwork/variables.tf +124 -17
  121. package/dist/terraform/schema.graphql +69 -36
  122. package/dist/workspace-defaults/files/USER.md +1 -0
  123. package/package.json +3 -2
  124. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  125. package/dist/plugins/workos-auth/package.json +0 -22
  126. package/dist/plugins/workos-auth/src/index.ts +0 -30
  127. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  128. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  129. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  130. package/dist/plugins/workos-auth/tsconfig.json +0 -8
@@ -0,0 +1,313 @@
1
+ -- creates: public.harness_managed_thread_enrollments
2
+ -- creates: public.thread_public_events
3
+ -- creates: public.harness_participant_sessions
4
+ -- creates: public.harness_participant_session_events
5
+ -- creates: public.harness_governed_tool_executions
6
+ -- creates: public.harness_disclosure_decisions
7
+ -- creates-function: public.capture_harness_message_public_event
8
+ -- creates-function: public.capture_harness_artifact_public_event
9
+ -- creates-trigger: public.messages.trg_capture_harness_message_public_event
10
+ -- creates-trigger: public.message_artifacts.trg_capture_harness_artifact_public_event
11
+ -- Managed multiplayer Harness proof ledger. Hand-rolled because this migration
12
+ -- also installs admission triggers over existing canonical message tables.
13
+
14
+ CREATE TABLE IF NOT EXISTS public.harness_managed_thread_enrollments (
15
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
16
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
17
+ thread_id uuid NOT NULL REFERENCES public.threads(id) ON DELETE CASCADE,
18
+ logical_agent_id uuid NOT NULL REFERENCES public.agents(id),
19
+ trust_profile text NOT NULL,
20
+ harness_arn text NOT NULL,
21
+ qualifier text NOT NULL,
22
+ resolved_version text NOT NULL,
23
+ session_strategy text NOT NULL DEFAULT 'fresh'
24
+ CHECK (session_strategy = 'fresh'),
25
+ prior_runtime text NOT NULL,
26
+ status text NOT NULL DEFAULT 'active'
27
+ CHECK (status IN ('active', 'restoring', 'restored', 'failed')),
28
+ enrolled_by_user_id uuid NOT NULL REFERENCES public.users(id),
29
+ created_at timestamptz NOT NULL DEFAULT now(),
30
+ restored_at timestamptz,
31
+ UNIQUE (tenant_id, thread_id)
32
+ );
33
+
34
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_harness_enrollment_active_profile
35
+ ON public.harness_managed_thread_enrollments (tenant_id, trust_profile)
36
+ WHERE status = 'active';
37
+
38
+ CREATE TABLE IF NOT EXISTS public.thread_public_events (
39
+ id bigserial PRIMARY KEY,
40
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
41
+ thread_id uuid NOT NULL REFERENCES public.threads(id) ON DELETE CASCADE,
42
+ source_kind text NOT NULL CHECK (source_kind IN ('message', 'message_artifact')),
43
+ source_id uuid NOT NULL,
44
+ source_version text NOT NULL,
45
+ event_kind text NOT NULL CHECK (event_kind IN ('insert', 'invalidate')),
46
+ canonical_digest text NOT NULL,
47
+ created_at timestamptz NOT NULL DEFAULT now(),
48
+ UNIQUE (tenant_id, thread_id, source_kind, source_id, source_version)
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_thread_public_events_prefix
52
+ ON public.thread_public_events (tenant_id, thread_id, id);
53
+
54
+ CREATE TABLE IF NOT EXISTS public.harness_participant_sessions (
55
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
56
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
57
+ enrollment_id uuid NOT NULL REFERENCES public.harness_managed_thread_enrollments(id) ON DELETE CASCADE,
58
+ thread_id uuid NOT NULL REFERENCES public.threads(id) ON DELETE CASCADE,
59
+ participant_user_id uuid NOT NULL REFERENCES public.users(id),
60
+ turn_id uuid NOT NULL REFERENCES public.thread_turns(id) ON DELETE CASCADE,
61
+ runtime_session_id text NOT NULL UNIQUE,
62
+ generation integer NOT NULL DEFAULT 1 CHECK (generation = 1),
63
+ captured_high_water bigint NOT NULL,
64
+ applied_high_water bigint,
65
+ qualifier text NOT NULL,
66
+ resolved_version text NOT NULL,
67
+ base_fingerprint text NOT NULL,
68
+ participant_fingerprint text NOT NULL,
69
+ state text NOT NULL DEFAULT 'allocated'
70
+ CHECK (state IN ('allocated', 'running', 'finalizing', 'completed', 'abandoned')),
71
+ failure_reason text,
72
+ created_at timestamptz NOT NULL DEFAULT now(),
73
+ started_at timestamptz,
74
+ finished_at timestamptz,
75
+ UNIQUE (tenant_id, turn_id),
76
+ CHECK (
77
+ (state IN ('completed', 'abandoned') AND finished_at IS NOT NULL)
78
+ OR (state NOT IN ('completed', 'abandoned') AND finished_at IS NULL)
79
+ )
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_harness_participant_sessions_thread
83
+ ON public.harness_participant_sessions
84
+ (tenant_id, thread_id, participant_user_id, created_at);
85
+
86
+ CREATE TABLE IF NOT EXISTS public.harness_participant_session_events (
87
+ id bigserial PRIMARY KEY,
88
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
89
+ session_id uuid NOT NULL REFERENCES public.harness_participant_sessions(id) ON DELETE CASCADE,
90
+ turn_id uuid NOT NULL REFERENCES public.thread_turns(id) ON DELETE CASCADE,
91
+ event_type text NOT NULL,
92
+ from_state text,
93
+ to_state text NOT NULL
94
+ CHECK (to_state IN ('allocated', 'running', 'finalizing', 'completed', 'abandoned')),
95
+ reason_code text,
96
+ applied_high_water bigint,
97
+ evidence jsonb,
98
+ created_at timestamptz NOT NULL DEFAULT now()
99
+ );
100
+
101
+ CREATE INDEX IF NOT EXISTS idx_harness_session_events_session
102
+ ON public.harness_participant_session_events (session_id, id);
103
+
104
+ CREATE TABLE IF NOT EXISTS public.harness_governed_tool_executions (
105
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
106
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
107
+ turn_id uuid NOT NULL REFERENCES public.thread_turns(id) ON DELETE CASCADE,
108
+ participant_user_id uuid NOT NULL REFERENCES public.users(id),
109
+ session_id uuid NOT NULL REFERENCES public.harness_participant_sessions(id) ON DELETE CASCADE,
110
+ idempotency_key text NOT NULL,
111
+ audience text NOT NULL,
112
+ operation text NOT NULL,
113
+ tool_use_id text NOT NULL,
114
+ input_digest text NOT NULL,
115
+ state text NOT NULL DEFAULT 'claimed'
116
+ CHECK (state IN ('claimed', 'completed', 'failed', 'ambiguous')),
117
+ policy_decision_id text,
118
+ credential_owner_alias text,
119
+ sanitized_result jsonb,
120
+ failure_reason text,
121
+ claimed_at timestamptz NOT NULL DEFAULT now(),
122
+ completed_at timestamptz,
123
+ UNIQUE (tenant_id, idempotency_key)
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_harness_governed_tool_turn
127
+ ON public.harness_governed_tool_executions (turn_id);
128
+
129
+ CREATE TABLE IF NOT EXISTS public.harness_disclosure_decisions (
130
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
131
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
132
+ participant_user_id uuid NOT NULL REFERENCES public.users(id),
133
+ thread_id uuid NOT NULL REFERENCES public.threads(id) ON DELETE CASCADE,
134
+ turn_id uuid NOT NULL REFERENCES public.thread_turns(id) ON DELETE CASCADE,
135
+ session_id uuid NOT NULL REFERENCES public.harness_participant_sessions(id) ON DELETE CASCADE,
136
+ operation text NOT NULL,
137
+ projection_digest text NOT NULL,
138
+ status text NOT NULL
139
+ CHECK (status IN ('published', 'withheld', 'confirmation_required')),
140
+ reason_code text NOT NULL,
141
+ expires_at timestamptz NOT NULL,
142
+ created_at timestamptz NOT NULL DEFAULT now()
143
+ );
144
+
145
+ CREATE INDEX IF NOT EXISTS idx_harness_disclosure_turn
146
+ ON public.harness_disclosure_decisions (turn_id);
147
+
148
+ CREATE OR REPLACE FUNCTION public.capture_harness_message_public_event()
149
+ RETURNS trigger
150
+ LANGUAGE plpgsql
151
+ AS $$
152
+ DECLARE
153
+ digest_value text;
154
+ version_value text;
155
+ event_value text;
156
+ admitted_before boolean;
157
+ admitted_now boolean;
158
+ BEGIN
159
+ IF TG_OP = 'DELETE' THEN
160
+ IF NOT EXISTS (
161
+ SELECT 1 FROM public.thread_public_events e
162
+ WHERE e.tenant_id = OLD.tenant_id
163
+ AND e.thread_id = OLD.thread_id
164
+ AND e.source_kind = 'message'
165
+ AND e.source_id = OLD.id
166
+ ) THEN
167
+ RETURN OLD;
168
+ END IF;
169
+ digest_value := encode(digest(concat_ws('|', OLD.role, coalesce(OLD.content, ''), coalesce(OLD.parts::text, ''), coalesce(OLD.metadata::text, '')), 'sha256'), 'hex');
170
+ version_value := 'delete:' || digest_value;
171
+ event_value := 'invalidate';
172
+ INSERT INTO public.thread_public_events (
173
+ tenant_id, thread_id, source_kind, source_id, source_version,
174
+ event_kind, canonical_digest
175
+ ) VALUES (
176
+ OLD.tenant_id, OLD.thread_id, 'message', OLD.id,
177
+ version_value, event_value, digest_value
178
+ ) ON CONFLICT DO NOTHING;
179
+ RETURN OLD;
180
+ END IF;
181
+
182
+ admitted_now := NEW.role IN ('user', 'assistant')
183
+ AND coalesce(NEW.metadata->>'visibility', 'public') = 'public'
184
+ AND coalesce(NEW.metadata->>'disclosure_status', 'published') NOT IN ('withheld', 'confirmation_required')
185
+ AND EXISTS (
186
+ SELECT 1 FROM public.harness_managed_thread_enrollments e
187
+ WHERE e.tenant_id = NEW.tenant_id
188
+ AND e.thread_id = NEW.thread_id
189
+ AND e.status = 'active'
190
+ );
191
+
192
+ IF TG_OP = 'UPDATE' THEN
193
+ admitted_before := EXISTS (
194
+ SELECT 1 FROM public.thread_public_events e
195
+ WHERE e.tenant_id = OLD.tenant_id
196
+ AND e.thread_id = OLD.thread_id
197
+ AND e.source_kind = 'message'
198
+ AND e.source_id = OLD.id
199
+ );
200
+ IF admitted_before THEN
201
+ digest_value := encode(digest(concat_ws('|', OLD.role, coalesce(OLD.content, ''), coalesce(OLD.parts::text, ''), coalesce(OLD.metadata::text, '')), 'sha256'), 'hex');
202
+ version_value := 'invalidate:update:' || encode(digest(concat_ws('|', NEW.role, coalesce(NEW.content, ''), coalesce(NEW.parts::text, ''), coalesce(NEW.metadata::text, '')), 'sha256'), 'hex');
203
+ INSERT INTO public.thread_public_events (
204
+ tenant_id, thread_id, source_kind, source_id, source_version,
205
+ event_kind, canonical_digest
206
+ ) VALUES (
207
+ OLD.tenant_id, OLD.thread_id, 'message', OLD.id,
208
+ version_value, 'invalidate', digest_value
209
+ ) ON CONFLICT DO NOTHING;
210
+ END IF;
211
+ END IF;
212
+
213
+ IF NOT admitted_now THEN
214
+ RETURN NEW;
215
+ END IF;
216
+
217
+ digest_value := encode(digest(concat_ws('|', NEW.role, coalesce(NEW.content, ''), coalesce(NEW.parts::text, '')), 'sha256'), 'hex');
218
+ version_value := lower(TG_OP) || ':' || digest_value || ':' || encode(digest(coalesce(NEW.metadata::text, ''), 'sha256'), 'hex');
219
+ INSERT INTO public.thread_public_events (
220
+ tenant_id, thread_id, source_kind, source_id, source_version,
221
+ event_kind, canonical_digest
222
+ ) VALUES (
223
+ NEW.tenant_id, NEW.thread_id, 'message', NEW.id,
224
+ version_value, 'insert', digest_value
225
+ ) ON CONFLICT DO NOTHING;
226
+ RETURN NEW;
227
+ END;
228
+ $$;
229
+
230
+ DROP TRIGGER IF EXISTS trg_capture_harness_message_public_event ON public.messages;
231
+ CREATE TRIGGER trg_capture_harness_message_public_event
232
+ AFTER INSERT OR UPDATE OR DELETE ON public.messages
233
+ FOR EACH ROW EXECUTE FUNCTION public.capture_harness_message_public_event();
234
+
235
+ CREATE OR REPLACE FUNCTION public.capture_harness_artifact_public_event()
236
+ RETURNS trigger
237
+ LANGUAGE plpgsql
238
+ AS $$
239
+ DECLARE
240
+ digest_value text;
241
+ version_value text;
242
+ admitted_before boolean;
243
+ admitted_now boolean;
244
+ BEGIN
245
+ IF TG_OP = 'DELETE' THEN
246
+ IF NOT EXISTS (
247
+ SELECT 1 FROM public.thread_public_events e
248
+ WHERE e.tenant_id = OLD.tenant_id
249
+ AND e.thread_id = OLD.thread_id
250
+ AND e.source_kind = 'message_artifact'
251
+ AND e.source_id = OLD.id
252
+ ) THEN
253
+ RETURN OLD;
254
+ END IF;
255
+ digest_value := encode(digest(concat_ws('|', OLD.artifact_type, coalesce(OLD.name, ''), coalesce(OLD.artifact_id::text, ''), coalesce(OLD.metadata::text, '')), 'sha256'), 'hex');
256
+ INSERT INTO public.thread_public_events (
257
+ tenant_id, thread_id, source_kind, source_id, source_version,
258
+ event_kind, canonical_digest
259
+ ) VALUES (
260
+ OLD.tenant_id, OLD.thread_id, 'message_artifact', OLD.id,
261
+ 'delete:' || digest_value, 'invalidate', digest_value
262
+ ) ON CONFLICT DO NOTHING;
263
+ RETURN OLD;
264
+ END IF;
265
+
266
+ admitted_now := EXISTS (
267
+ SELECT 1 FROM public.harness_managed_thread_enrollments e
268
+ WHERE e.tenant_id = NEW.tenant_id
269
+ AND e.thread_id = NEW.thread_id
270
+ AND e.status = 'active'
271
+ ) AND coalesce(NEW.metadata->>'access_state', 'public') = 'public';
272
+
273
+ IF TG_OP = 'UPDATE' THEN
274
+ admitted_before := EXISTS (
275
+ SELECT 1 FROM public.thread_public_events e
276
+ WHERE e.tenant_id = OLD.tenant_id
277
+ AND e.thread_id = OLD.thread_id
278
+ AND e.source_kind = 'message_artifact'
279
+ AND e.source_id = OLD.id
280
+ );
281
+ IF admitted_before THEN
282
+ digest_value := encode(digest(concat_ws('|', OLD.artifact_type, coalesce(OLD.name, ''), coalesce(OLD.artifact_id::text, ''), coalesce(OLD.metadata::text, '')), 'sha256'), 'hex');
283
+ version_value := 'invalidate:update:' || encode(digest(concat_ws('|', NEW.artifact_type, coalesce(NEW.name, ''), coalesce(NEW.artifact_id::text, ''), coalesce(NEW.metadata::text, '')), 'sha256'), 'hex');
284
+ INSERT INTO public.thread_public_events (
285
+ tenant_id, thread_id, source_kind, source_id, source_version,
286
+ event_kind, canonical_digest
287
+ ) VALUES (
288
+ OLD.tenant_id, OLD.thread_id, 'message_artifact', OLD.id,
289
+ version_value, 'invalidate', digest_value
290
+ ) ON CONFLICT DO NOTHING;
291
+ END IF;
292
+ END IF;
293
+
294
+ IF NOT admitted_now THEN
295
+ RETURN NEW;
296
+ END IF;
297
+
298
+ digest_value := encode(digest(concat_ws('|', NEW.artifact_type, coalesce(NEW.name, ''), coalesce(NEW.artifact_id::text, ''), coalesce(NEW.metadata::text, '')), 'sha256'), 'hex');
299
+ INSERT INTO public.thread_public_events (
300
+ tenant_id, thread_id, source_kind, source_id, source_version,
301
+ event_kind, canonical_digest
302
+ ) VALUES (
303
+ NEW.tenant_id, NEW.thread_id, 'message_artifact', NEW.id,
304
+ lower(TG_OP) || ':' || digest_value, 'insert', digest_value
305
+ ) ON CONFLICT DO NOTHING;
306
+ RETURN NEW;
307
+ END;
308
+ $$;
309
+
310
+ DROP TRIGGER IF EXISTS trg_capture_harness_artifact_public_event ON public.message_artifacts;
311
+ CREATE TRIGGER trg_capture_harness_artifact_public_event
312
+ AFTER INSERT OR UPDATE OR DELETE ON public.message_artifacts
313
+ FOR EACH ROW EXECUTE FUNCTION public.capture_harness_artifact_public_event();
@@ -0,0 +1,328 @@
1
+ -- Provider-neutral authentication control plane (plan 2026-07-18-001 U11).
2
+ -- creates-column: public.auth_provider_resources.connection_key
3
+ -- creates-column: public.auth_provider_resources.provider_kind
4
+ -- creates-column: public.auth_provider_resources.lifecycle_state
5
+ -- creates-column: public.auth_provider_resources.resource_arn
6
+ -- creates-column: public.auth_provider_resources.aws_account_id
7
+ -- creates-column: public.auth_provider_resources.aws_region
8
+ -- creates-column: public.auth_provider_resources.desired_revision
9
+ -- creates-column: public.tenant_auth_provider_references.desired_revision
10
+ -- creates: public.uq_auth_provider_resources_connection_key
11
+ -- creates: public.uq_tenant_auth_provider_references_tenant_resource
12
+ -- creates: public.tenant_auth_policies
13
+ -- creates: public.uq_tenant_auth_policies_tenant
14
+ -- creates-function: public.ensure_default_tenant_auth_policy
15
+ -- creates-trigger: public.tenants.trg_tenants_default_auth_policy
16
+ -- creates: public.tenant_auth_hosts
17
+ -- creates: public.uq_tenant_auth_hosts_hostname
18
+ -- creates: public.idx_tenant_auth_hosts_tenant_status
19
+ -- creates: public.auth_route_clients
20
+ -- creates: public.uq_auth_route_clients_route_family
21
+ -- creates: public.uq_auth_route_clients_app_client
22
+ -- creates: public.idx_auth_route_clients_lifecycle_status
23
+ -- creates: public.user_auth_identities
24
+ -- creates: public.uq_user_auth_identities_cognito_sub
25
+ -- creates: public.uq_user_auth_identities_provider_subject
26
+ -- creates: public.idx_user_auth_identities_user_status
27
+ -- creates: public.idx_user_auth_identities_tenant_status
28
+ -- creates: public.auth_identity_enrollments
29
+ -- creates: public.uq_auth_identity_enrollments_nonce
30
+ -- creates: public.uq_auth_identity_enrollments_challenge
31
+ -- creates: public.idx_auth_identity_enrollments_pending
32
+ -- creates: public.auth_reconciliation_sets
33
+ -- creates: public.uq_auth_reconciliation_sets_stage_revision
34
+ -- creates: public.uq_auth_reconciliation_sets_idempotency
35
+ -- creates: public.auth_cutover_runs
36
+ -- creates: public.uq_auth_cutover_runs_stage_inventory
37
+ -- creates: public.idx_auth_cutover_runs_status
38
+ -- creates: public.auth_identity_proofs
39
+ -- creates: public.uq_auth_identity_proofs_digest
40
+ -- creates: public.idx_auth_identity_proofs_identity
41
+ -- drops: public.uq_tenant_auth_provider_references_install_resource
42
+
43
+ ALTER TABLE public.auth_provider_resources
44
+ ADD COLUMN IF NOT EXISTS connection_key text,
45
+ ADD COLUMN IF NOT EXISTS provider_kind text,
46
+ ADD COLUMN IF NOT EXISTS lifecycle_state text,
47
+ ADD COLUMN IF NOT EXISTS resource_arn text,
48
+ ADD COLUMN IF NOT EXISTS aws_account_id text,
49
+ ADD COLUMN IF NOT EXISTS aws_region text,
50
+ ADD COLUMN IF NOT EXISTS desired_revision integer;
51
+
52
+ UPDATE public.auth_provider_resources
53
+ SET connection_key = COALESCE(connection_key, provider_key || ':' || id::text),
54
+ provider_kind = COALESCE(provider_kind, CASE WHEN provider_key = 'workos' THEN 'legacy_workos' ELSE provider_key END),
55
+ lifecycle_state = COALESCE(lifecycle_state, 'coexistence'),
56
+ desired_revision = COALESCE(desired_revision, 1);
57
+
58
+ ALTER TABLE public.auth_provider_resources
59
+ ALTER COLUMN connection_key SET DEFAULT 'legacy',
60
+ ALTER COLUMN connection_key SET NOT NULL,
61
+ ALTER COLUMN provider_kind SET DEFAULT 'legacy_workos',
62
+ ALTER COLUMN provider_kind SET NOT NULL,
63
+ ALTER COLUMN lifecycle_state SET DEFAULT 'coexistence',
64
+ ALTER COLUMN lifecycle_state SET NOT NULL,
65
+ ALTER COLUMN desired_revision SET DEFAULT 1,
66
+ ALTER COLUMN desired_revision SET NOT NULL,
67
+ ALTER COLUMN issuer_url DROP NOT NULL,
68
+ ALTER COLUMN client_id DROP NOT NULL,
69
+ ALTER COLUMN client_secret_ref DROP NOT NULL;
70
+
71
+ ALTER TABLE public.auth_provider_resources
72
+ DROP CONSTRAINT IF EXISTS auth_provider_resources_no_public_without_valid,
73
+ DROP CONSTRAINT IF EXISTS auth_provider_resources_lifecycle_state_allowed,
74
+ ADD CONSTRAINT auth_provider_resources_lifecycle_state_allowed
75
+ CHECK (lifecycle_state IN ('coexistence', 'native', 'denied')),
76
+ ADD CONSTRAINT auth_provider_resources_no_public_without_valid
77
+ CHECK (
78
+ public_options_published = false OR
79
+ (validation_status IN ('valid', 'partially_valid') AND lifecycle_state <> 'denied')
80
+ );
81
+
82
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_provider_resources_connection_key
83
+ ON public.auth_provider_resources (cognito_user_pool_id, connection_key);
84
+
85
+ ALTER TABLE public.tenant_auth_provider_references
86
+ ADD COLUMN IF NOT EXISTS desired_revision integer;
87
+
88
+ UPDATE public.tenant_auth_provider_references
89
+ SET desired_revision = COALESCE(desired_revision, 1);
90
+
91
+ ALTER TABLE public.tenant_auth_provider_references
92
+ ALTER COLUMN desired_revision SET DEFAULT 1,
93
+ ALTER COLUMN desired_revision SET NOT NULL,
94
+ ALTER COLUMN plugin_install_id DROP NOT NULL,
95
+ DROP CONSTRAINT IF EXISTS tenant_auth_provider_references_tenant_id_fkey,
96
+ DROP CONSTRAINT IF EXISTS tenant_auth_provider_references_plugin_install_id_fkey,
97
+ DROP CONSTRAINT IF EXISTS tenant_auth_provider_references_auth_provider_resource_id_fkey,
98
+ ADD CONSTRAINT tenant_auth_provider_references_tenant_id_fkey
99
+ FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE RESTRICT,
100
+ ADD CONSTRAINT tenant_auth_provider_references_plugin_install_id_fkey
101
+ FOREIGN KEY (plugin_install_id) REFERENCES public.plugin_installs(id) ON DELETE SET NULL,
102
+ ADD CONSTRAINT tenant_auth_provider_references_auth_provider_resource_id_fkey
103
+ FOREIGN KEY (auth_provider_resource_id) REFERENCES public.auth_provider_resources(id) ON DELETE RESTRICT;
104
+
105
+ DROP INDEX IF EXISTS public.uq_tenant_auth_provider_references_install_resource;
106
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_tenant_auth_provider_references_tenant_resource
107
+ ON public.tenant_auth_provider_references (tenant_id, auth_provider_resource_id);
108
+
109
+ CREATE TABLE IF NOT EXISTS public.tenant_auth_policies (
110
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
111
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE RESTRICT,
112
+ local_password_enabled boolean NOT NULL DEFAULT true,
113
+ status text NOT NULL DEFAULT 'draft',
114
+ revision integer NOT NULL DEFAULT 1,
115
+ catalog_revision text,
116
+ updated_by uuid REFERENCES public.users(id) ON DELETE SET NULL,
117
+ created_at timestamptz NOT NULL DEFAULT now(),
118
+ updated_at timestamptz NOT NULL DEFAULT now(),
119
+ CONSTRAINT tenant_auth_policies_status_allowed
120
+ CHECK (status IN ('draft', 'active', 'disabled'))
121
+ );
122
+
123
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_tenant_auth_policies_tenant
124
+ ON public.tenant_auth_policies (tenant_id);
125
+
126
+ -- Preserve the pre-native-auth behavior for existing tenants explicitly:
127
+ -- local password is enabled and validated global Google/Microsoft routes are
128
+ -- permitted by an active policy. Tenant-specific Entra references can then
129
+ -- replace general Microsoft without leaving older tenants policy-less.
130
+ INSERT INTO public.tenant_auth_policies (
131
+ tenant_id,
132
+ local_password_enabled,
133
+ status,
134
+ revision,
135
+ created_at,
136
+ updated_at
137
+ )
138
+ SELECT id, true, 'active', 1, now(), now()
139
+ FROM public.tenants
140
+ ON CONFLICT (tenant_id) DO NOTHING;
141
+
142
+ CREATE OR REPLACE FUNCTION public.ensure_default_tenant_auth_policy()
143
+ RETURNS trigger
144
+ LANGUAGE plpgsql
145
+ AS $$
146
+ BEGIN
147
+ INSERT INTO public.tenant_auth_policies (
148
+ tenant_id,
149
+ local_password_enabled,
150
+ status,
151
+ revision,
152
+ created_at,
153
+ updated_at
154
+ ) VALUES (NEW.id, true, 'active', 1, now(), now())
155
+ ON CONFLICT (tenant_id) DO NOTHING;
156
+ RETURN NEW;
157
+ END;
158
+ $$;
159
+
160
+ DROP TRIGGER IF EXISTS trg_tenants_default_auth_policy ON public.tenants;
161
+ CREATE TRIGGER trg_tenants_default_auth_policy
162
+ AFTER INSERT ON public.tenants
163
+ FOR EACH ROW
164
+ EXECUTE FUNCTION public.ensure_default_tenant_auth_policy();
165
+
166
+ CREATE TABLE IF NOT EXISTS public.tenant_auth_hosts (
167
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
168
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE RESTRICT,
169
+ hostname text NOT NULL,
170
+ status text NOT NULL DEFAULT 'pending',
171
+ verified_at timestamptz,
172
+ created_at timestamptz NOT NULL DEFAULT now(),
173
+ updated_at timestamptz NOT NULL DEFAULT now(),
174
+ CONSTRAINT tenant_auth_hosts_status_allowed
175
+ CHECK (status IN ('pending', 'verified', 'disabled'))
176
+ );
177
+
178
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_tenant_auth_hosts_hostname
179
+ ON public.tenant_auth_hosts (hostname);
180
+ CREATE INDEX IF NOT EXISTS idx_tenant_auth_hosts_tenant_status
181
+ ON public.tenant_auth_hosts (tenant_id, status);
182
+
183
+ CREATE TABLE IF NOT EXISTS public.auth_route_clients (
184
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
185
+ route_key text NOT NULL,
186
+ client_family text NOT NULL,
187
+ cognito_user_pool_id text NOT NULL,
188
+ cognito_app_client_id text NOT NULL,
189
+ provider_names jsonb NOT NULL DEFAULT '[]'::jsonb,
190
+ explicit_auth_flows jsonb NOT NULL DEFAULT '[]'::jsonb,
191
+ redirect_uris jsonb NOT NULL DEFAULT '[]'::jsonb,
192
+ logout_uris jsonb NOT NULL DEFAULT '[]'::jsonb,
193
+ lifecycle_state text NOT NULL DEFAULT 'native',
194
+ validation_status text NOT NULL DEFAULT 'unconfigured',
195
+ desired_revision integer NOT NULL DEFAULT 1,
196
+ resource_arn text,
197
+ diagnostics jsonb NOT NULL DEFAULT '{}'::jsonb,
198
+ created_at timestamptz NOT NULL DEFAULT now(),
199
+ updated_at timestamptz NOT NULL DEFAULT now(),
200
+ CONSTRAINT auth_route_clients_lifecycle_allowed
201
+ CHECK (lifecycle_state IN ('coexistence', 'native', 'denied')),
202
+ CONSTRAINT auth_route_clients_validation_allowed
203
+ CHECK (validation_status IN ('unconfigured', 'validating', 'valid', 'partially_valid', 'invalid', 'rotating_secret', 'disabled'))
204
+ );
205
+
206
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_route_clients_route_family
207
+ ON public.auth_route_clients (route_key, client_family);
208
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_route_clients_app_client
209
+ ON public.auth_route_clients (cognito_app_client_id);
210
+ CREATE INDEX IF NOT EXISTS idx_auth_route_clients_lifecycle_status
211
+ ON public.auth_route_clients (lifecycle_state, validation_status);
212
+
213
+ CREATE TABLE IF NOT EXISTS public.user_auth_identities (
214
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
215
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE RESTRICT,
216
+ user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT,
217
+ auth_provider_resource_id uuid REFERENCES public.auth_provider_resources(id) ON DELETE RESTRICT,
218
+ cognito_issuer text NOT NULL,
219
+ cognito_sub text NOT NULL,
220
+ provider_issuer text NOT NULL,
221
+ provider_subject text NOT NULL,
222
+ status text NOT NULL DEFAULT 'pending_proof',
223
+ proof_kind text NOT NULL,
224
+ evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
225
+ activated_at timestamptz,
226
+ quarantined_at timestamptz,
227
+ revoked_at timestamptz,
228
+ created_at timestamptz NOT NULL DEFAULT now(),
229
+ updated_at timestamptz NOT NULL DEFAULT now(),
230
+ CONSTRAINT user_auth_identities_status_allowed
231
+ CHECK (status IN ('pending_proof', 'active', 'quarantined', 'revoked')),
232
+ CONSTRAINT user_auth_identities_active_has_resource
233
+ CHECK (status = 'quarantined' OR auth_provider_resource_id IS NOT NULL)
234
+ );
235
+
236
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_user_auth_identities_cognito_sub
237
+ ON public.user_auth_identities (cognito_issuer, cognito_sub);
238
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_user_auth_identities_provider_subject
239
+ ON public.user_auth_identities (auth_provider_resource_id, provider_issuer, provider_subject);
240
+ CREATE INDEX IF NOT EXISTS idx_user_auth_identities_user_status
241
+ ON public.user_auth_identities (user_id, status);
242
+ CREATE INDEX IF NOT EXISTS idx_user_auth_identities_tenant_status
243
+ ON public.user_auth_identities (tenant_id, status);
244
+
245
+ CREATE TABLE IF NOT EXISTS public.auth_identity_enrollments (
246
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
247
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE RESTRICT,
248
+ intended_user_id uuid REFERENCES public.users(id) ON DELETE RESTRICT,
249
+ recipient_grant_kind text NOT NULL,
250
+ recipient_grant_id uuid NOT NULL,
251
+ auth_provider_resource_id uuid NOT NULL REFERENCES public.auth_provider_resources(id) ON DELETE RESTRICT,
252
+ auth_route_client_id uuid NOT NULL REFERENCES public.auth_route_clients(id) ON DELETE RESTRICT,
253
+ redirect_uri text NOT NULL,
254
+ nonce_digest text NOT NULL,
255
+ recipient_challenge_digest text NOT NULL,
256
+ status text NOT NULL DEFAULT 'pending',
257
+ expires_at timestamptz NOT NULL,
258
+ consumed_at timestamptz,
259
+ proof jsonb NOT NULL DEFAULT '{}'::jsonb,
260
+ created_at timestamptz NOT NULL DEFAULT now(),
261
+ updated_at timestamptz NOT NULL DEFAULT now(),
262
+ CONSTRAINT auth_identity_enrollments_status_allowed
263
+ CHECK (status IN ('pending', 'consumed', 'expired', 'revoked')),
264
+ CONSTRAINT auth_identity_enrollments_grant_kind_allowed
265
+ CHECK (recipient_grant_kind IN ('membership', 'pending_owner'))
266
+ );
267
+
268
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_identity_enrollments_nonce
269
+ ON public.auth_identity_enrollments (nonce_digest);
270
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_identity_enrollments_challenge
271
+ ON public.auth_identity_enrollments (recipient_challenge_digest);
272
+ CREATE INDEX IF NOT EXISTS idx_auth_identity_enrollments_pending
273
+ ON public.auth_identity_enrollments (tenant_id, status, expires_at);
274
+
275
+ CREATE TABLE IF NOT EXISTS public.auth_reconciliation_sets (
276
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
277
+ stage text NOT NULL,
278
+ revision integer NOT NULL,
279
+ idempotency_key text NOT NULL,
280
+ manifest_fingerprint text NOT NULL,
281
+ desired_connections jsonb NOT NULL,
282
+ status text NOT NULL DEFAULT 'pending',
283
+ applied_at timestamptz,
284
+ created_at timestamptz NOT NULL DEFAULT now(),
285
+ CONSTRAINT auth_reconciliation_sets_status_allowed
286
+ CHECK (status IN ('pending', 'applied', 'rejected'))
287
+ );
288
+
289
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_reconciliation_sets_stage_revision
290
+ ON public.auth_reconciliation_sets (stage, revision);
291
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_reconciliation_sets_idempotency
292
+ ON public.auth_reconciliation_sets (idempotency_key);
293
+
294
+ CREATE TABLE IF NOT EXISTS public.auth_cutover_runs (
295
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
296
+ stage text NOT NULL,
297
+ tenant_id uuid,
298
+ inventory_fingerprint text NOT NULL,
299
+ status text NOT NULL DEFAULT 'inventory',
300
+ terminal_dispositions jsonb NOT NULL DEFAULT '{}'::jsonb,
301
+ client_shutdown_evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
302
+ drain_evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
303
+ started_at timestamptz NOT NULL DEFAULT now(),
304
+ completed_at timestamptz,
305
+ created_at timestamptz NOT NULL DEFAULT now(),
306
+ CONSTRAINT auth_cutover_runs_status_allowed
307
+ CHECK (status IN ('inventory', 'ready', 'cutting_over', 'soaking', 'rollback_required', 'complete', 'failed'))
308
+ );
309
+
310
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_cutover_runs_stage_inventory
311
+ ON public.auth_cutover_runs (stage, inventory_fingerprint);
312
+ CREATE INDEX IF NOT EXISTS idx_auth_cutover_runs_status
313
+ ON public.auth_cutover_runs (stage, status);
314
+
315
+ CREATE TABLE IF NOT EXISTS public.auth_identity_proofs (
316
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
317
+ user_auth_identity_id uuid NOT NULL REFERENCES public.user_auth_identities(id) ON DELETE RESTRICT,
318
+ proof_digest text NOT NULL,
319
+ proof_kind text NOT NULL,
320
+ evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
321
+ observed_at timestamptz NOT NULL DEFAULT now(),
322
+ created_at timestamptz NOT NULL DEFAULT now()
323
+ );
324
+
325
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_auth_identity_proofs_digest
326
+ ON public.auth_identity_proofs (proof_digest);
327
+ CREATE INDEX IF NOT EXISTS idx_auth_identity_proofs_identity
328
+ ON public.auth_identity_proofs (user_auth_identity_id);