@opengeni/db 3.2.0-canary.0 → 3.4.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-SIBYBA2D.js → chunk-6DPJA36C.js} +30 -11
  3. package/dist/chunk-6DPJA36C.js.map +1 -0
  4. package/dist/{chunk-KRLPEMLH.js → chunk-C2AFP7SB.js} +184 -6
  5. package/dist/chunk-C2AFP7SB.js.map +1 -0
  6. package/dist/{chunk-AZHW7R5V.js → chunk-H7MXYG7R.js} +12 -8
  7. package/dist/chunk-H7MXYG7R.js.map +1 -0
  8. package/dist/{chunk-2PRS4J5Q.js → chunk-HY3F5Q4E.js} +3 -3
  9. package/dist/{chunk-NAB6FKFN.js → chunk-O6FTELKH.js} +3 -3
  10. package/dist/{chunk-UZNXG44K.js → chunk-PYMY3AJW.js} +324 -8
  11. package/dist/chunk-PYMY3AJW.js.map +1 -0
  12. package/dist/{chunk-6HMM2R5T.js → chunk-TRBOWUG3.js} +98 -4
  13. package/dist/chunk-TRBOWUG3.js.map +1 -0
  14. package/dist/{chunk-E6CLMKEE.js → chunk-V3TRUXFJ.js} +2 -2
  15. package/dist/{chunk-2EH4WRHL.js → chunk-ZWRIPCTR.js} +2 -2
  16. package/dist/editable-artifact-durable-export.js +2 -2
  17. package/dist/editable-artifacts.js +3 -3
  18. package/dist/governed-learning-activation.d.ts +16 -0
  19. package/dist/index.d.ts +108 -7
  20. package/dist/index.js +3605 -1417
  21. package/dist/index.js.map +1 -1
  22. package/dist/insights-model-bundle.d.ts +30 -0
  23. package/dist/insights-usage-bundle.d.ts +32 -0
  24. package/dist/insights.d.ts +10 -2
  25. package/dist/lossless-json.d.ts +1 -1
  26. package/dist/managed-user-setup.d.ts +82 -0
  27. package/dist/memory-domain.d.ts +2 -2
  28. package/dist/migrate.d.ts +3 -1
  29. package/dist/migrate.js +1 -1
  30. package/dist/new-session-drafts.d.ts +4 -4
  31. package/dist/organization-membership-lifecycle.d.ts +60 -1
  32. package/dist/pr-review.d.ts +44 -3
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/runtime-posture.d.ts +12 -7
  35. package/dist/schema.d.ts +839 -101
  36. package/dist/schema.js +9 -1
  37. package/dist/session-background-commands.js +3 -3
  38. package/dist/session-tenancy.d.ts +12 -1
  39. package/dist/session-tenancy.js +3 -3
  40. package/dist/video-generation.js +3 -3
  41. package/drizzle/0348_named_signup_and_user_setup.sql +838 -0
  42. package/drizzle/0349_greenfield_session_tenancy_activation.sql +792 -0
  43. package/drizzle/0350_organization_shared_workspace_administration.sql +889 -0
  44. package/drizzle/0351_organization_user_setup_delivery.sql +978 -0
  45. package/drizzle/0352_session_variable_set_attachments.sql +1661 -0
  46. package/drizzle/0353_automatic_session_title_policy_fence.sql +476 -0
  47. package/drizzle/0354_automatic_session_title_quarantine_index.sql +9 -0
  48. package/drizzle/0355_automatic_session_title_quarantine.sql +92 -0
  49. package/drizzle/0356_set_based_insights_session_visibility.sql +237 -0
  50. package/drizzle/0357_rig_platform_base_only.sql +44 -0
  51. package/drizzle/0358_pr_review_managed_github_app.sql +108 -0
  52. package/drizzle/0359_insights_force_rls_read_capability.sql +936 -0
  53. package/drizzle/0360_organization_identity_confirmation_prompt.sql +103 -0
  54. package/drizzle/0361_remember_knowledge_memory_materialization.sql +249 -0
  55. package/package.json +6 -6
  56. package/src/database.ts +12 -8
  57. package/src/governed-learning-activation.ts +74 -2
  58. package/src/index.ts +879 -92
  59. package/src/insights-model-bundle.ts +681 -0
  60. package/src/insights-usage-bundle.ts +240 -0
  61. package/src/insights.ts +265 -152
  62. package/src/lossless-json.ts +2 -1
  63. package/src/managed-user-setup.ts +316 -0
  64. package/src/memory-domain.ts +3 -3
  65. package/src/migrate.ts +38 -14
  66. package/src/new-session-drafts.ts +22 -15
  67. package/src/organization-membership-lifecycle.ts +253 -1
  68. package/src/pr-review.ts +449 -6
  69. package/src/provision-roles.ts +92 -0
  70. package/src/runtime-posture.ts +247 -4
  71. package/src/schema.ts +210 -4
  72. package/src/session-tenancy.ts +120 -2
  73. package/dist/chunk-6HMM2R5T.js.map +0 -1
  74. package/dist/chunk-AZHW7R5V.js.map +0 -1
  75. package/dist/chunk-KRLPEMLH.js.map +0 -1
  76. package/dist/chunk-SIBYBA2D.js.map +0 -1
  77. package/dist/chunk-UZNXG44K.js.map +0 -1
  78. /package/dist/{chunk-2PRS4J5Q.js.map → chunk-HY3F5Q4E.js.map} +0 -0
  79. /package/dist/{chunk-NAB6FKFN.js.map → chunk-O6FTELKH.js.map} +0 -0
  80. /package/dist/{chunk-E6CLMKEE.js.map → chunk-V3TRUXFJ.js.map} +0 -0
  81. /package/dist/{chunk-2EH4WRHL.js.map → chunk-ZWRIPCTR.js.map} +0 -0
@@ -0,0 +1,237 @@
1
+ -- deployment-mode: rolling
2
+ -- Insights scans model_call_facts and usage_events across thousands of rows.
3
+ -- Their ordinary restrictive policies deliberately call
4
+ -- session_reference_visible() for each row, which is correct for point reads
5
+ -- but makes analytical scans execute one nested sessions lookup per fact. Keep
6
+ -- those table policies unchanged for every ordinary caller. These two bounded
7
+ -- read-only functions reproduce the same account/workspace/subject visibility
8
+ -- rule inside one set-based join and expose no data outside the existing fact
9
+ -- shapes. Only roles that already SELECT both fact tables receive EXECUTE.
10
+ DO $set_based_insights_session_visibility_functions$
11
+ DECLARE
12
+ data_schema text := pg_catalog.current_schema();
13
+ BEGIN
14
+ EXECUTE pg_catalog.format($ddl$
15
+ CREATE OR REPLACE FUNCTION opengeni_private.visible_workspace_insights_model_call_facts(
16
+ p_workspace_id uuid,
17
+ p_since timestamp with time zone,
18
+ p_until timestamp with time zone
19
+ ) RETURNS SETOF %1$I.model_call_facts
20
+ LANGUAGE plpgsql
21
+ STABLE
22
+ SECURITY DEFINER
23
+ SET search_path = pg_catalog, %1$I, opengeni_private, pg_temp
24
+ AS $function$
25
+ DECLARE
26
+ context_account_id uuid;
27
+ context_workspace_id uuid;
28
+ context_subject_id text;
29
+ BEGIN
30
+ PERFORM opengeni_private.session_variable_set_attachments_protocol_v1_active();
31
+ BEGIN
32
+ context_account_id := nullif(
33
+ pg_catalog.current_setting('opengeni.account_id', true), ''
34
+ )::uuid;
35
+ context_workspace_id := nullif(
36
+ pg_catalog.current_setting('opengeni.workspace_id', true), ''
37
+ )::uuid;
38
+ EXCEPTION WHEN invalid_text_representation THEN
39
+ RAISE EXCEPTION 'Insights RLS context is malformed'
40
+ USING ERRCODE = '42501';
41
+ END;
42
+ context_subject_id := nullif(
43
+ pg_catalog.current_setting('opengeni.subject_id', true), ''
44
+ );
45
+ IF context_account_id IS NULL
46
+ OR context_workspace_id IS NULL
47
+ OR context_workspace_id <> p_workspace_id
48
+ THEN
49
+ RAISE EXCEPTION 'Insights RLS context does not match the requested workspace'
50
+ USING ERRCODE = '42501';
51
+ END IF;
52
+ IF p_since IS NULL
53
+ OR p_until IS NULL
54
+ OR p_until <= p_since
55
+ OR (
56
+ p_until <> 'infinity'::timestamp with time zone
57
+ AND p_until - p_since > interval '370 days'
58
+ )
59
+ OR (
60
+ p_until = 'infinity'::timestamp with time zone
61
+ AND p_since < CURRENT_TIMESTAMP - interval '370 days'
62
+ )
63
+ THEN
64
+ RAISE EXCEPTION 'Insights fact window must be positive and at most 370 days'
65
+ USING ERRCODE = '22023';
66
+ END IF;
67
+
68
+ RETURN QUERY
69
+ SELECT fact.*
70
+ FROM %1$I.model_call_facts fact
71
+ INNER JOIN %1$I.sessions session_row
72
+ ON session_row.account_id = fact.account_id
73
+ AND session_row.workspace_id = fact.workspace_id
74
+ AND session_row.id = fact.session_id
75
+ WHERE fact.account_id = context_account_id
76
+ AND fact.workspace_id = p_workspace_id
77
+ AND fact.occurred_at >= p_since
78
+ AND fact.occurred_at < p_until
79
+ AND (
80
+ context_subject_id IS NULL
81
+ OR session_row.visibility = 'workspace_shared'
82
+ OR %1$I.session_private_actor_visible(
83
+ session_row.account_id,
84
+ session_row.workspace_id,
85
+ session_row.owner_organization_membership_id,
86
+ session_row.owner_subject_id
87
+ )
88
+ );
89
+ END
90
+ $function$;
91
+ $ddl$, data_schema);
92
+
93
+ EXECUTE pg_catalog.format($ddl$
94
+ CREATE OR REPLACE FUNCTION opengeni_private.visible_workspace_insights_usage_events(
95
+ p_workspace_id uuid,
96
+ p_since timestamp with time zone,
97
+ p_until timestamp with time zone
98
+ ) RETURNS SETOF %1$I.usage_events
99
+ LANGUAGE plpgsql
100
+ STABLE
101
+ SECURITY DEFINER
102
+ SET search_path = pg_catalog, %1$I, opengeni_private, pg_temp
103
+ AS $function$
104
+ DECLARE
105
+ context_account_id uuid;
106
+ context_workspace_id uuid;
107
+ context_subject_id text;
108
+ BEGIN
109
+ PERFORM opengeni_private.session_variable_set_attachments_protocol_v1_active();
110
+ BEGIN
111
+ context_account_id := nullif(
112
+ pg_catalog.current_setting('opengeni.account_id', true), ''
113
+ )::uuid;
114
+ context_workspace_id := nullif(
115
+ pg_catalog.current_setting('opengeni.workspace_id', true), ''
116
+ )::uuid;
117
+ EXCEPTION WHEN invalid_text_representation THEN
118
+ RAISE EXCEPTION 'Insights RLS context is malformed'
119
+ USING ERRCODE = '42501';
120
+ END;
121
+ context_subject_id := nullif(
122
+ pg_catalog.current_setting('opengeni.subject_id', true), ''
123
+ );
124
+ IF context_account_id IS NULL
125
+ OR context_workspace_id IS NULL
126
+ OR context_workspace_id <> p_workspace_id
127
+ THEN
128
+ RAISE EXCEPTION 'Insights RLS context does not match the requested workspace'
129
+ USING ERRCODE = '42501';
130
+ END IF;
131
+ IF p_since IS NULL
132
+ OR p_until IS NULL
133
+ OR p_until <= p_since
134
+ OR (
135
+ p_until <> 'infinity'::timestamp with time zone
136
+ AND p_until - p_since > interval '370 days'
137
+ )
138
+ OR (
139
+ p_until = 'infinity'::timestamp with time zone
140
+ AND p_since < CURRENT_TIMESTAMP - interval '370 days'
141
+ )
142
+ THEN
143
+ RAISE EXCEPTION 'Insights fact window must be positive and at most 370 days'
144
+ USING ERRCODE = '22023';
145
+ END IF;
146
+
147
+ RETURN QUERY
148
+ SELECT usage_row.*
149
+ FROM %1$I.usage_events usage_row
150
+ LEFT JOIN %1$I.sessions session_row
151
+ ON session_row.account_id = usage_row.account_id
152
+ AND session_row.workspace_id = usage_row.workspace_id
153
+ AND session_row.id = usage_row.session_id
154
+ WHERE usage_row.account_id = context_account_id
155
+ AND usage_row.workspace_id = p_workspace_id
156
+ AND usage_row.occurred_at >= p_since
157
+ AND usage_row.occurred_at < p_until
158
+ AND (
159
+ usage_row.session_id IS NULL
160
+ OR (
161
+ session_row.id IS NOT NULL
162
+ AND (
163
+ context_subject_id IS NULL
164
+ OR session_row.visibility = 'workspace_shared'
165
+ OR %1$I.session_private_actor_visible(
166
+ session_row.account_id,
167
+ session_row.workspace_id,
168
+ session_row.owner_organization_membership_id,
169
+ session_row.owner_subject_id
170
+ )
171
+ )
172
+ )
173
+ );
174
+ END
175
+ $function$;
176
+ $ddl$, data_schema);
177
+ END
178
+ $set_based_insights_session_visibility_functions$;
179
+
180
+ REVOKE ALL ON FUNCTION
181
+ opengeni_private.visible_workspace_insights_model_call_facts(
182
+ uuid, timestamp with time zone, timestamp with time zone
183
+ )
184
+ FROM PUBLIC;
185
+ REVOKE ALL ON FUNCTION
186
+ opengeni_private.visible_workspace_insights_usage_events(
187
+ uuid, timestamp with time zone, timestamp with time zone
188
+ )
189
+ FROM PUBLIC;
190
+
191
+ -- Rolling compatibility is limited to the explicitly configured application
192
+ -- roles. Do not infer application authority from fact-table SELECT: operator,
193
+ -- export, or reporting roles may intentionally lack sessions/private-schema
194
+ -- access even when they can inspect these fact tables through ordinary RLS.
195
+ DO $set_based_insights_session_visibility_grants$
196
+ DECLARE
197
+ data_schema text := pg_catalog.current_schema();
198
+ role_name text;
199
+ BEGIN
200
+ FOR role_name IN
201
+ SELECT role_row.rolname
202
+ FROM pg_catalog.jsonb_array_elements_text(
203
+ pg_catalog.current_setting('opengeni.migration_application_roles')::jsonb
204
+ ) configured(value)
205
+ INNER JOIN pg_catalog.pg_roles role_row
206
+ ON role_row.rolname = configured.value
207
+ WHERE pg_catalog.has_table_privilege(
208
+ role_row.rolname,
209
+ pg_catalog.format('%I.model_call_facts', data_schema),
210
+ 'SELECT'
211
+ )
212
+ AND pg_catalog.has_table_privilege(
213
+ role_row.rolname,
214
+ pg_catalog.format('%I.usage_events', data_schema),
215
+ 'SELECT'
216
+ )
217
+ ORDER BY role_row.rolname COLLATE "C"
218
+ LOOP
219
+ EXECUTE pg_catalog.format(
220
+ 'GRANT USAGE ON SCHEMA opengeni_private TO %I',
221
+ role_name
222
+ );
223
+ EXECUTE pg_catalog.format(
224
+ 'GRANT EXECUTE ON FUNCTION '
225
+ || 'opengeni_private.visible_workspace_insights_model_call_facts('
226
+ || 'uuid, timestamp with time zone, timestamp with time zone) TO %I',
227
+ role_name
228
+ );
229
+ EXECUTE pg_catalog.format(
230
+ 'GRANT EXECUTE ON FUNCTION '
231
+ || 'opengeni_private.visible_workspace_insights_usage_events('
232
+ || 'uuid, timestamp with time zone, timestamp with time zone) TO %I',
233
+ role_name
234
+ );
235
+ END LOOP;
236
+ END
237
+ $set_based_insights_session_visibility_grants$;
@@ -0,0 +1,44 @@
1
+ -- deployment-mode: maintenance
2
+ -- Rig definitions may customize setup and checks, but the deployment owns the
3
+ -- sandbox base image. Preserve historical image-bearing rows for audit and
4
+ -- harmless active/provider metadata updates while rejecting every new image
5
+ -- override at the storage boundary.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+
9
+ CREATE OR REPLACE FUNCTION opengeni_private.enforce_rig_platform_base_only_v1()
10
+ RETURNS trigger
11
+ LANGUAGE plpgsql
12
+ SET search_path = pg_catalog
13
+ AS $function$
14
+ BEGIN
15
+ IF TG_OP = 'INSERT' AND NEW.image IS NOT NULL THEN
16
+ RAISE EXCEPTION
17
+ 'Rig image overrides are unsupported; Rigs use the deployment platform sandbox image'
18
+ USING ERRCODE = '23514',
19
+ CONSTRAINT = 'rig_versions_platform_base_only';
20
+ END IF;
21
+
22
+ IF TG_OP = 'UPDATE'
23
+ AND NEW.image IS NOT NULL
24
+ AND NEW.image IS DISTINCT FROM OLD.image
25
+ THEN
26
+ RAISE EXCEPTION
27
+ 'Rig image overrides are unsupported; Rigs use the deployment platform sandbox image'
28
+ USING ERRCODE = '23514',
29
+ CONSTRAINT = 'rig_versions_platform_base_only';
30
+ END IF;
31
+
32
+ RETURN NEW;
33
+ END
34
+ $function$;
35
+
36
+ REVOKE ALL ON FUNCTION opengeni_private.enforce_rig_platform_base_only_v1() FROM PUBLIC;
37
+
38
+ CREATE TRIGGER rig_versions_platform_base_only
39
+ BEFORE INSERT OR UPDATE OF image ON rig_versions
40
+ FOR EACH ROW
41
+ EXECUTE FUNCTION opengeni_private.enforce_rig_platform_base_only_v1();
42
+
43
+ COMMENT ON COLUMN rig_versions.image IS
44
+ 'Legacy audit field only. Runtime ignores it and new non-null values are rejected.';
@@ -0,0 +1,108 @@
1
+ -- deployment-mode: rolling
2
+ -- A deployment-owned, least-privilege GitHub App can be installed through the
3
+ -- same owner-proof browser flow as the platform App. Workspace registrations
4
+ -- reference that managed identity rather than copying its private key. The
5
+ -- credential-free route table resolves an already-authenticated GitHub
6
+ -- delivery to one exact Pack source and repository binding.
7
+
8
+ SET LOCAL lock_timeout = '5s';
9
+ SET LOCAL statement_timeout = '10min';
10
+
11
+ ALTER TABLE pr_review_app_registrations
12
+ ADD COLUMN installation_id text,
13
+ ADD COLUMN provider_account_login text,
14
+ ADD COLUMN provider_account_type text,
15
+ ADD COLUMN github_actor_id text,
16
+ ADD COLUMN authority_kind text,
17
+ ADD COLUMN authority_checked_at timestamptz,
18
+ ADD COLUMN authority_expires_at timestamptz,
19
+ ADD COLUMN authority_nonce text;
20
+
21
+ ALTER TABLE pr_review_app_registrations
22
+ DROP CONSTRAINT pr_review_app_registrations_credential_chk;
23
+
24
+ ALTER TABLE pr_review_app_registrations
25
+ ADD CONSTRAINT pr_review_app_registrations_credential_chk CHECK (
26
+ (provider = 'github' AND credential_kind = 'github_app'
27
+ AND credential_encrypted IS NOT NULL AND app_id IS NOT NULL
28
+ AND installation_id IS NULL)
29
+ OR
30
+ (provider = 'github' AND credential_kind = 'managed_github_app'
31
+ AND credential_encrypted IS NULL AND app_id IS NOT NULL
32
+ AND installation_id IS NOT NULL
33
+ AND provider_account_type IN ('User', 'Organization')
34
+ AND github_actor_id ~ '^[1-9][0-9]*$'
35
+ AND authority_kind IN ('personal_owner', 'organization_owner')
36
+ AND authority_checked_at IS NOT NULL
37
+ AND authority_expires_at IS NOT NULL
38
+ AND authority_expires_at > authority_checked_at
39
+ AND octet_length(authority_nonce) BETWEEN 16 AND 256)
40
+ OR
41
+ (provider IN ('gitlab', 'azure_devops') AND credential_kind = 'provider_token'
42
+ AND credential_encrypted IS NOT NULL AND installation_id IS NULL)
43
+ );
44
+
45
+ CREATE UNIQUE INDEX pr_review_managed_github_workspace_installation_uq
46
+ ON pr_review_app_registrations (workspace_id, installation_id)
47
+ WHERE credential_kind = 'managed_github_app';
48
+
49
+ CREATE UNIQUE INDEX pr_review_managed_github_authority_nonce_uq
50
+ ON pr_review_app_registrations (authority_nonce)
51
+ WHERE authority_nonce IS NOT NULL;
52
+
53
+ -- Append-only consumption receipts keep every successful OAuth state
54
+ -- single-use even after the registration advances to a newer authority proof.
55
+ CREATE TABLE pr_review_managed_github_authority_nonces (
56
+ authority_nonce text PRIMARY KEY,
57
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
58
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
59
+ installation_id text NOT NULL,
60
+ authority_expires_at timestamptz NOT NULL,
61
+ consumed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
62
+ CONSTRAINT pr_review_managed_github_authority_nonces_workspace_account_fk
63
+ FOREIGN KEY (workspace_id, account_id)
64
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
65
+ CONSTRAINT pr_review_managed_github_authority_nonces_identity_chk CHECK (
66
+ octet_length(authority_nonce) BETWEEN 16 AND 256
67
+ AND installation_id ~ '^[1-9][0-9]*$'
68
+ AND authority_expires_at > consumed_at
69
+ )
70
+ );
71
+
72
+ ALTER TABLE pr_review_managed_github_authority_nonces ENABLE ROW LEVEL SECURITY;
73
+ ALTER TABLE pr_review_managed_github_authority_nonces FORCE ROW LEVEL SECURITY;
74
+ CREATE POLICY pr_review_managed_github_authority_nonces_tenant
75
+ ON pr_review_managed_github_authority_nonces
76
+ USING (opengeni_private.workspace_rls_visible(account_id, workspace_id))
77
+ WITH CHECK (opengeni_private.workspace_rls_visible(account_id, workspace_id));
78
+
79
+ -- Credential-free global routing only. The shared webhook secret is verified
80
+ -- over the bounded raw body before these provider ids are parsed or resolved;
81
+ -- the FORCE-RLS source, trigger, Pack installation, and binding remain the
82
+ -- acceptance and dispatch authorities.
83
+ CREATE TABLE pr_review_managed_github_routes (
84
+ binding_id uuid PRIMARY KEY REFERENCES pr_review_repository_bindings(id) ON DELETE CASCADE,
85
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
86
+ workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
87
+ registration_id uuid NOT NULL,
88
+ source_id uuid NOT NULL,
89
+ installation_id text NOT NULL,
90
+ provider_repository_id text NOT NULL,
91
+ created_at timestamptz NOT NULL DEFAULT now(),
92
+ CONSTRAINT pr_review_managed_github_routes_workspace_account_fk
93
+ FOREIGN KEY (workspace_id, account_id)
94
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
95
+ CONSTRAINT pr_review_managed_github_routes_registration_fk
96
+ FOREIGN KEY (workspace_id, registration_id)
97
+ REFERENCES pr_review_app_registrations(workspace_id, id) ON DELETE CASCADE,
98
+ CONSTRAINT pr_review_managed_github_routes_source_fk
99
+ FOREIGN KEY (workspace_id, source_id)
100
+ REFERENCES automation_sources(workspace_id, id) ON DELETE CASCADE,
101
+ CONSTRAINT pr_review_managed_github_routes_identity_chk CHECK (
102
+ installation_id ~ '^[1-9][0-9]*$'
103
+ AND provider_repository_id ~ '^[1-9][0-9]*$'
104
+ )
105
+ );
106
+
107
+ CREATE UNIQUE INDEX pr_review_managed_github_route_identity_uq
108
+ ON pr_review_managed_github_routes (installation_id, provider_repository_id);