@opengeni/db 0.23.0 → 0.27.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 (35) hide show
  1. package/dist/{chunk-3UCHDMKG.js → chunk-EX7CDSGH.js} +196 -2
  2. package/dist/chunk-EX7CDSGH.js.map +1 -0
  3. package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
  4. package/dist/chunk-IHPCI4GV.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +68 -2
  7. package/dist/index.js +1457 -338
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +262 -4
  13. package/dist/schema.js +5 -1
  14. package/dist/session-realtime-mirror.d.ts +22 -1
  15. package/dist/workspace-instruction-policies-schema.d.ts +449 -0
  16. package/dist/workspace-instruction-policies.d.ts +88 -8
  17. package/drizzle/0165_document_authority_foundation.sql +259 -0
  18. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  19. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  20. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  21. package/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -0
  22. package/package.json +3 -3
  23. package/src/connection-token-resolver.ts +79 -16
  24. package/src/index.ts +419 -23
  25. package/src/preference-registry.ts +103 -0
  26. package/src/runtime-posture.ts +4 -0
  27. package/src/schema.ts +86 -0
  28. package/src/session-control.ts +48 -1
  29. package/src/session-queue-commands.ts +45 -11
  30. package/src/session-realtime-mirror.ts +117 -1
  31. package/src/session-realtime.ts +49 -1
  32. package/src/workspace-instruction-policies-schema.ts +116 -0
  33. package/src/workspace-instruction-policies.ts +819 -25
  34. package/dist/chunk-3UCHDMKG.js.map +0 -1
  35. package/dist/chunk-L6ADMZHE.js.map +0 -1
@@ -0,0 +1,259 @@
1
+ -- deployment-mode: maintenance
2
+ -- Durable organization/workspace/personal authority for Documents and chunks.
3
+ -- Existing workspace-visible rows remain workspace authority. Existing private
4
+ -- rows remain personal authority, anchored to their original workspace and
5
+ -- immutable creating subject. Collections/bases are not authority boundaries.
6
+
7
+ SET lock_timeout = '5s';
8
+ SET statement_timeout = '10min';
9
+
10
+ -- Reject a mixed-version cutover before taking table locks. Repeat after the
11
+ -- locks to close the connect-before-lock race. Deployment still owns the
12
+ -- external stop/no-restart protocol after this migration commits.
13
+ DO $document_writer_drain_before_lock$
14
+ BEGIN
15
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
16
+ AND EXISTS (
17
+ SELECT 1
18
+ FROM pg_stat_activity
19
+ WHERE datname = current_database()
20
+ AND usename = 'opengeni_app'
21
+ AND pid <> pg_backend_pid()
22
+ )
23
+ THEN
24
+ RAISE EXCEPTION 'document authority activation requires all opengeni_app sessions to be stopped'
25
+ USING ERRCODE = '55000';
26
+ END IF;
27
+ END
28
+ $document_writer_drain_before_lock$;
29
+
30
+ -- Both tables already use FORCE RLS. The migration role owns them but may not
31
+ -- be a superuser, so temporarily restore the ordinary owner bypass inside this
32
+ -- transaction. Runtime roles remain subject to RLS throughout the cutover.
33
+ -- The final policy block restores FORCE before this migration can commit.
34
+ ALTER TABLE "documents" NO FORCE ROW LEVEL SECURITY;
35
+ ALTER TABLE "document_chunks" NO FORCE ROW LEVEL SECURITY;
36
+
37
+ DO $document_writer_drain_after_lock$
38
+ BEGIN
39
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app')
40
+ AND EXISTS (
41
+ SELECT 1
42
+ FROM pg_stat_activity
43
+ WHERE datname = current_database()
44
+ AND usename = 'opengeni_app'
45
+ AND pid <> pg_backend_pid()
46
+ )
47
+ THEN
48
+ RAISE EXCEPTION 'document authority activation requires all opengeni_app sessions to be stopped'
49
+ USING ERRCODE = '55000';
50
+ END IF;
51
+ END
52
+ $document_writer_drain_after_lock$;
53
+
54
+ -- This is a protocol cutover, not a replay adapter. Stop the API and every
55
+ -- worker, wait for all document-index Temporal workflows to close, and settle
56
+ -- every queued/indexing document before applying the migration. The row check
57
+ -- is the database-verifiable half of that drain and prevents a legacy
58
+ -- three-field activity payload from being resumed under the new authority
59
+ -- contract.
60
+ DO $document_index_drain$
61
+ BEGIN
62
+ IF EXISTS (
63
+ SELECT 1 FROM "documents" WHERE "status" IN ('queued', 'indexing')
64
+ ) THEN
65
+ RAISE EXCEPTION 'migration 0165 requires every queued/indexing document to settle before cutover'
66
+ USING ERRCODE = '55000',
67
+ HINT = 'Stop API/workers, close document-index workflows, and retry after documents are ready or failed.';
68
+ END IF;
69
+ END
70
+ $document_index_drain$;
71
+
72
+ ALTER TABLE "documents" ADD COLUMN "authority_kind" text;
73
+ ALTER TABLE "documents" ADD COLUMN "authority_workspace_id" uuid;
74
+ ALTER TABLE "documents" ADD COLUMN "authority_subject_id" text;
75
+
76
+ ALTER TABLE "document_chunks" ADD COLUMN "authority_kind" text;
77
+ ALTER TABLE "document_chunks" ADD COLUMN "authority_workspace_id" uuid;
78
+ ALTER TABLE "document_chunks" ADD COLUMN "authority_subject_id" text;
79
+
80
+ -- Deterministic legacy backfill. Migration 0126 already guarantees every
81
+ -- private row has a non-empty creator, so this never widens an ambiguous row.
82
+ UPDATE "documents"
83
+ SET "authority_kind" = CASE WHEN "visibility" = 'private' THEN 'personal' ELSE 'workspace' END,
84
+ "authority_workspace_id" = "workspace_id",
85
+ "authority_subject_id" = CASE WHEN "visibility" = 'private' THEN "created_by" ELSE NULL END;
86
+
87
+ -- The document is the canonical parent for every chunk identity field. Repair
88
+ -- any legacy drift before freezing future writes with the trigger below.
89
+ UPDATE "document_chunks" AS chunk
90
+ SET "account_id" = document."account_id",
91
+ "workspace_id" = document."workspace_id",
92
+ "base_id" = document."base_id",
93
+ "file_id" = document."file_id"
94
+ FROM "documents" AS document
95
+ WHERE document."id" = chunk."document_id"
96
+ AND (
97
+ chunk."account_id" IS DISTINCT FROM document."account_id"
98
+ OR chunk."workspace_id" IS DISTINCT FROM document."workspace_id"
99
+ OR chunk."base_id" IS DISTINCT FROM document."base_id"
100
+ OR chunk."file_id" IS DISTINCT FROM document."file_id"
101
+ );
102
+
103
+ UPDATE "document_chunks" AS chunk
104
+ SET "authority_kind" = document."authority_kind",
105
+ "authority_workspace_id" = document."authority_workspace_id",
106
+ "authority_subject_id" = document."authority_subject_id"
107
+ FROM "documents" AS document
108
+ WHERE document."id" = chunk."document_id";
109
+
110
+ ALTER TABLE "documents" ALTER COLUMN "authority_kind" SET NOT NULL;
111
+ ALTER TABLE "documents" ALTER COLUMN "authority_kind" SET DEFAULT 'workspace';
112
+ ALTER TABLE "document_chunks" ALTER COLUMN "authority_kind" SET NOT NULL;
113
+ ALTER TABLE "document_chunks" ALTER COLUMN "authority_kind" SET DEFAULT 'workspace';
114
+
115
+ ALTER TABLE "documents"
116
+ ADD CONSTRAINT "documents_authority_workspace_fk"
117
+ FOREIGN KEY ("authority_workspace_id", "account_id")
118
+ REFERENCES "workspaces"("id", "account_id") ON DELETE RESTRICT;
119
+ ALTER TABLE "document_chunks"
120
+ ADD CONSTRAINT "document_chunks_authority_workspace_fk"
121
+ FOREIGN KEY ("authority_workspace_id", "account_id")
122
+ REFERENCES "workspaces"("id", "account_id") ON DELETE RESTRICT;
123
+
124
+ ALTER TABLE "documents" ADD CONSTRAINT "documents_authority_chk" CHECK (
125
+ ("authority_kind" = 'organization' AND "authority_workspace_id" IS NULL AND "authority_subject_id" IS NULL)
126
+ OR ("authority_kind" = 'workspace' AND "authority_workspace_id" = "workspace_id" AND "authority_subject_id" IS NULL)
127
+ OR (
128
+ "authority_kind" = 'personal'
129
+ AND "authority_workspace_id" = "workspace_id"
130
+ AND NULLIF(btrim("authority_subject_id"), '') IS NOT NULL
131
+ AND octet_length(convert_to("authority_subject_id", 'UTF8')) <= 1024
132
+ AND "authority_subject_id" = "created_by"
133
+ )
134
+ );
135
+ ALTER TABLE "documents" ADD CONSTRAINT "documents_authority_visibility_chk" CHECK (
136
+ ("authority_kind" = 'personal') = ("visibility" = 'private')
137
+ );
138
+ ALTER TABLE "document_chunks" ADD CONSTRAINT "document_chunks_authority_chk" CHECK (
139
+ ("authority_kind" = 'organization' AND "authority_workspace_id" IS NULL AND "authority_subject_id" IS NULL)
140
+ OR ("authority_kind" = 'workspace' AND "authority_workspace_id" = "workspace_id" AND "authority_subject_id" IS NULL)
141
+ OR (
142
+ "authority_kind" = 'personal'
143
+ AND "authority_workspace_id" = "workspace_id"
144
+ AND NULLIF(btrim("authority_subject_id"), '') IS NOT NULL
145
+ AND octet_length(convert_to("authority_subject_id", 'UTF8')) <= 1024
146
+ )
147
+ );
148
+
149
+ CREATE INDEX "documents_authority_idx" ON "documents" (
150
+ "account_id", "authority_kind", "authority_workspace_id", "authority_subject_id", "status"
151
+ );
152
+ CREATE INDEX "document_chunks_authority_idx" ON "document_chunks" (
153
+ "account_id", "authority_kind", "authority_workspace_id", "authority_subject_id"
154
+ );
155
+
156
+ CREATE OR REPLACE FUNCTION opengeni_private.apply_document_authority()
157
+ RETURNS trigger
158
+ LANGUAGE plpgsql
159
+ AS $$
160
+ BEGIN
161
+ IF TG_OP = 'UPDATE' AND (
162
+ NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
163
+ OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
164
+ OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
165
+ OR NEW.created_by IS DISTINCT FROM OLD.created_by
166
+ OR NEW.visibility IS DISTINCT FROM OLD.visibility
167
+ ) THEN
168
+ RAISE EXCEPTION 'document authority is immutable';
169
+ END IF;
170
+
171
+ IF NEW.authority_kind IS NULL OR (
172
+ NEW.authority_kind = 'workspace'
173
+ AND NEW.authority_workspace_id IS NULL
174
+ AND NEW.visibility = 'private'
175
+ ) THEN
176
+ NEW.authority_kind := CASE WHEN NEW.visibility = 'private' THEN 'personal' ELSE 'workspace' END;
177
+ END IF;
178
+ CASE NEW.authority_kind
179
+ WHEN 'organization' THEN
180
+ NEW.authority_workspace_id := NULL;
181
+ NEW.authority_subject_id := NULL;
182
+ NEW.visibility := 'workspace';
183
+ WHEN 'workspace' THEN
184
+ NEW.authority_workspace_id := NEW.workspace_id;
185
+ NEW.authority_subject_id := NULL;
186
+ NEW.visibility := 'workspace';
187
+ WHEN 'personal' THEN
188
+ NEW.authority_workspace_id := NEW.workspace_id;
189
+ NEW.authority_subject_id := coalesce(NULLIF(btrim(NEW.authority_subject_id), ''), NULLIF(btrim(NEW.created_by), ''));
190
+ NEW.visibility := 'private';
191
+ ELSE
192
+ RAISE EXCEPTION 'invalid document authority kind: %', NEW.authority_kind;
193
+ END CASE;
194
+ RETURN NEW;
195
+ END
196
+ $$;
197
+
198
+ CREATE TRIGGER documents_authority_guard
199
+ BEFORE INSERT OR UPDATE ON "documents"
200
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.apply_document_authority();
201
+
202
+ CREATE OR REPLACE FUNCTION opengeni_private.apply_document_chunk_authority()
203
+ RETURNS trigger
204
+ LANGUAGE plpgsql
205
+ AS $$
206
+ DECLARE parent "documents"%ROWTYPE;
207
+ BEGIN
208
+ IF TG_OP = 'UPDATE' AND (
209
+ NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
210
+ OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
211
+ OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
212
+ OR NEW.document_id IS DISTINCT FROM OLD.document_id
213
+ ) THEN
214
+ RAISE EXCEPTION 'document chunk authority is immutable';
215
+ END IF;
216
+ SELECT * INTO parent FROM "documents" WHERE "id" = NEW.document_id;
217
+ IF NOT FOUND
218
+ OR parent.account_id IS DISTINCT FROM NEW.account_id
219
+ OR parent.workspace_id IS DISTINCT FROM NEW.workspace_id
220
+ OR parent.base_id IS DISTINCT FROM NEW.base_id
221
+ OR parent.file_id IS DISTINCT FROM NEW.file_id
222
+ THEN
223
+ RAISE EXCEPTION 'document chunk parent identity mismatch';
224
+ END IF;
225
+ NEW.authority_kind := parent.authority_kind;
226
+ NEW.authority_workspace_id := parent.authority_workspace_id;
227
+ NEW.authority_subject_id := parent.authority_subject_id;
228
+ RETURN NEW;
229
+ END
230
+ $$;
231
+
232
+ CREATE TRIGGER document_chunks_authority_guard
233
+ BEFORE INSERT OR UPDATE ON "document_chunks"
234
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.apply_document_chunk_authority();
235
+
236
+ DO $$
237
+ DECLARE table_name text;
238
+ BEGIN
239
+ FOREACH table_name IN ARRAY ARRAY['documents', 'document_chunks'] LOOP
240
+ IF EXISTS (
241
+ SELECT 1 FROM pg_policies
242
+ WHERE schemaname = current_schema() AND tablename = table_name
243
+ AND policyname = 'workspace_isolation'
244
+ ) THEN
245
+ EXECUTE format('DROP POLICY workspace_isolation ON %I', table_name);
246
+ END IF;
247
+ EXECUTE format(
248
+ 'CREATE POLICY document_authority_isolation ON %I '
249
+ || 'USING (opengeni_private.scoped_knowledge_scope_visible('
250
+ || 'account_id, authority_kind, authority_workspace_id, authority_subject_id)) '
251
+ || 'WITH CHECK (opengeni_private.scoped_knowledge_scope_visible('
252
+ || 'account_id, authority_kind, authority_workspace_id, authority_subject_id))',
253
+ table_name
254
+ );
255
+ EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
256
+ EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
257
+ END LOOP;
258
+ END
259
+ $$;
@@ -0,0 +1,49 @@
1
+ -- deployment-mode: rolling
2
+ -- Bind a subject-owned connection disconnect to one caller-frozen generation
3
+ -- and operation key. Exact retries converge only while the produced generation
4
+ -- remains current; reconnecting permanently fences delayed old requests.
5
+
6
+ CREATE TABLE "connection_disconnect_operations" (
7
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
9
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
10
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
11
+ "subject_id" text NOT NULL,
12
+ "idempotency_key" text NOT NULL,
13
+ "expected_version" integer NOT NULL,
14
+ "result_version" integer NOT NULL,
15
+ "created_at" timestamptz NOT NULL DEFAULT now(),
16
+ CONSTRAINT "connection_disconnect_operations_subject_key_uq"
17
+ UNIQUE ("workspace_id", "subject_id", "idempotency_key"),
18
+ CONSTRAINT "connection_disconnect_operations_connection_generation_uq"
19
+ UNIQUE ("workspace_id", "connection_id", "expected_version"),
20
+ CONSTRAINT "connection_disconnect_operations_identity_check"
21
+ CHECK (
22
+ length("subject_id") BETWEEN 1 AND 512
23
+ AND length("idempotency_key") BETWEEN 1 AND 200
24
+ AND "idempotency_key" = btrim("idempotency_key")
25
+ AND "expected_version" > 0
26
+ AND "result_version" = "expected_version" + 1
27
+ )
28
+ );
29
+
30
+ ALTER TABLE "connection_disconnect_operations" ENABLE ROW LEVEL SECURITY;
31
+ ALTER TABLE "connection_disconnect_operations" FORCE ROW LEVEL SECURITY;
32
+ CREATE POLICY workspace_subject_isolation ON "connection_disconnect_operations"
33
+ USING (
34
+ opengeni_private.workspace_rls_visible("account_id", "workspace_id")
35
+ AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
36
+ )
37
+ WITH CHECK (
38
+ opengeni_private.workspace_rls_visible("account_id", "workspace_id")
39
+ AND "subject_id" = nullif(current_setting('opengeni.subject_id', true), '')
40
+ );
41
+
42
+ DO $$
43
+ BEGIN
44
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
45
+ REVOKE ALL PRIVILEGES ON TABLE "connection_disconnect_operations" FROM opengeni_app;
46
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE "connection_disconnect_operations" TO opengeni_app;
47
+ END IF;
48
+ END
49
+ $$;
@@ -0,0 +1,61 @@
1
+ -- deployment-mode: rolling
2
+ -- Resolve only the immutable document authority tuple for historical
3
+ -- three-field document-index Temporal payloads. The ordinary documents policy
4
+ -- remains unchanged: personal content is still invisible without its exact
5
+ -- subject. This SECURITY DEFINER capability returns no content and manually
6
+ -- enforces the caller's already-applied account/workspace RLS context before
7
+ -- reading the exact document identity.
8
+
9
+ DO $document_index_authority_resolver$
10
+ DECLARE data_schema text := current_schema();
11
+ BEGIN
12
+ EXECUTE format($ddl$
13
+ CREATE OR REPLACE FUNCTION opengeni_private.resolve_document_index_authority(
14
+ p_account_id uuid,
15
+ p_workspace_id uuid,
16
+ p_document_id uuid
17
+ )
18
+ RETURNS TABLE (
19
+ authority_kind text,
20
+ authority_workspace_id uuid,
21
+ authority_subject_id text
22
+ )
23
+ LANGUAGE plpgsql
24
+ SECURITY DEFINER
25
+ SET search_path = pg_catalog
26
+ AS $body$
27
+ BEGIN
28
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
29
+ OR p_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
30
+ THEN
31
+ RAISE EXCEPTION 'document index authority lookup requires exact account/workspace RLS context'
32
+ USING ERRCODE = '42501';
33
+ END IF;
34
+
35
+ RETURN QUERY
36
+ SELECT
37
+ document.authority_kind,
38
+ document.authority_workspace_id,
39
+ document.authority_subject_id
40
+ FROM %1$I.documents document
41
+ WHERE document.account_id = p_account_id
42
+ AND document.workspace_id = p_workspace_id
43
+ AND document.id = p_document_id
44
+ LIMIT 1;
45
+ END;
46
+ $body$;
47
+ $ddl$, data_schema);
48
+ END
49
+ $document_index_authority_resolver$;
50
+
51
+ REVOKE ALL ON FUNCTION opengeni_private.resolve_document_index_authority(uuid, uuid, uuid)
52
+ FROM PUBLIC;
53
+
54
+ DO $runtime_grant$
55
+ BEGIN
56
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
57
+ GRANT EXECUTE ON FUNCTION opengeni_private.resolve_document_index_authority(uuid, uuid, uuid)
58
+ TO opengeni_app;
59
+ END IF;
60
+ END
61
+ $runtime_grant$;
@@ -0,0 +1,44 @@
1
+ -- deployment-mode: rolling
2
+
3
+ -- Immutable revision and activation rows are also the durable receipts for
4
+ -- policy-administration mutations. Legacy rows remain untouched and project
5
+ -- their immutable row id as a compatibility operation identity; new writers
6
+ -- persist stable UUIDs plus a secret-safe fingerprint of the complete canonical
7
+ -- request so only byte-equivalent semantics can replay the original result.
8
+ ALTER TABLE "workspace_instruction_policy_revisions"
9
+ ADD COLUMN IF NOT EXISTS "operation_id" uuid,
10
+ ADD COLUMN IF NOT EXISTS "request_fingerprint" text;
11
+
12
+ ALTER TABLE "workspace_instruction_policy_revisions"
13
+ ADD CONSTRAINT "workspace_instruction_policy_revisions_operation_receipt_chk"
14
+ CHECK (
15
+ ("operation_id" IS NULL AND "request_fingerprint" IS NULL)
16
+ OR (
17
+ "operation_id" IS NOT NULL
18
+ AND "request_fingerprint" ~ '^[0-9a-f]{64}$'
19
+ )
20
+ );
21
+
22
+ CREATE UNIQUE INDEX IF NOT EXISTS
23
+ "workspace_instruction_policy_revisions_workspace_operation_uq"
24
+ ON "workspace_instruction_policy_revisions" ("workspace_id", "operation_id")
25
+ WHERE "operation_id" IS NOT NULL;
26
+
27
+ ALTER TABLE "workspace_instruction_policy_activation_events"
28
+ ADD COLUMN IF NOT EXISTS "operation_id" uuid,
29
+ ADD COLUMN IF NOT EXISTS "request_fingerprint" text;
30
+
31
+ ALTER TABLE "workspace_instruction_policy_activation_events"
32
+ ADD CONSTRAINT "workspace_instruction_policy_events_operation_receipt_chk"
33
+ CHECK (
34
+ ("operation_id" IS NULL AND "request_fingerprint" IS NULL)
35
+ OR (
36
+ "operation_id" IS NOT NULL
37
+ AND "request_fingerprint" ~ '^[0-9a-f]{64}$'
38
+ )
39
+ );
40
+
41
+ CREATE UNIQUE INDEX IF NOT EXISTS
42
+ "workspace_instruction_policy_events_workspace_operation_uq"
43
+ ON "workspace_instruction_policy_activation_events" ("workspace_id", "operation_id")
44
+ WHERE "operation_id" IS NOT NULL;
@@ -0,0 +1,202 @@
1
+ -- deployment-mode: rolling
2
+
3
+ -- Onboarding inputs create immutable proposal receipts plus ordinary inactive
4
+ -- instruction-policy revisions. This table is provenance/audit evidence only:
5
+ -- it never writes or owns an active policy head.
6
+ CREATE TABLE "workspace_instruction_policy_onboarding_proposals" (
7
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ "operation_id" uuid NOT NULL,
9
+ "request_fingerprint" text NOT NULL,
10
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
11
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
12
+ "kind" text NOT NULL,
13
+ "scope" text NOT NULL,
14
+ "role_key" text,
15
+ "source_id" text NOT NULL,
16
+ "source_version" text NOT NULL,
17
+ "confidence_bps" integer NOT NULL,
18
+ "baseline_revision_id" uuid REFERENCES "workspace_instruction_policy_revisions"("id")
19
+ ON DELETE RESTRICT,
20
+ "baseline_revision" bigint,
21
+ "baseline_content_hash" text,
22
+ "baseline_activation_version" bigint NOT NULL,
23
+ "baseline_activated_at" timestamptz,
24
+ "draft_revision_id" uuid NOT NULL REFERENCES "workspace_instruction_policy_revisions"("id")
25
+ ON DELETE RESTRICT,
26
+ "draft_revision" bigint NOT NULL,
27
+ "draft_content_hash" text NOT NULL,
28
+ "status" text NOT NULL DEFAULT 'proposed',
29
+ "created_by_subject_id" text NOT NULL,
30
+ "created_at" timestamptz NOT NULL DEFAULT now(),
31
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_operation_receipt_chk" CHECK (
32
+ "request_fingerprint" ~ '^[0-9a-f]{64}$'
33
+ ),
34
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_target_chk" CHECK (
35
+ ("kind" = 'charter' AND "scope" = 'global' AND "role_key" IS NULL)
36
+ OR ("kind" = 'policy' AND "scope" = 'global' AND "role_key" IS NULL)
37
+ OR ("kind" = 'policy' AND "scope" = 'role' AND "role_key" IS NOT NULL)
38
+ ),
39
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_role_key_chk" CHECK (
40
+ "role_key" IS NULL
41
+ OR (
42
+ "role_key" = lower(btrim("role_key"))
43
+ AND length("role_key") BETWEEN 1 AND 64
44
+ AND "role_key" ~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
45
+ AND "role_key" !~ '--'
46
+ )
47
+ ),
48
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_source_chk" CHECK (
49
+ length(btrim("source_id")) BETWEEN 1 AND 512
50
+ AND length(btrim("source_version")) BETWEEN 1 AND 256
51
+ AND "confidence_bps" BETWEEN 0 AND 10000
52
+ ),
53
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_baseline_chk" CHECK (
54
+ (
55
+ "baseline_revision_id" IS NULL
56
+ AND "baseline_revision" IS NULL
57
+ AND "baseline_content_hash" IS NULL
58
+ AND "baseline_activation_version" = 0
59
+ AND "baseline_activated_at" IS NULL
60
+ ) OR (
61
+ "baseline_revision_id" IS NOT NULL
62
+ AND "baseline_revision" > 0
63
+ AND "baseline_content_hash" ~ '^[0-9a-f]{64}$'
64
+ AND "baseline_activation_version" > 0
65
+ AND "baseline_activated_at" IS NOT NULL
66
+ )
67
+ ),
68
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_draft_chk" CHECK (
69
+ "draft_revision" > 0
70
+ AND "draft_content_hash" ~ '^[0-9a-f]{64}$'
71
+ AND "status" = 'proposed'
72
+ ),
73
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_actor_chk" CHECK (
74
+ length(btrim("created_by_subject_id")) BETWEEN 1 AND 1024
75
+ ),
76
+ CONSTRAINT "workspace_instruction_policy_onboarding_proposals_workspace_operation_uq"
77
+ UNIQUE ("workspace_id", "operation_id")
78
+ );
79
+
80
+ CREATE UNIQUE INDEX "workspace_instruction_policy_onboarding_proposals_source_version_target_uq"
81
+ ON "workspace_instruction_policy_onboarding_proposals"
82
+ (
83
+ "workspace_id",
84
+ "kind",
85
+ "scope",
86
+ coalesce("role_key", ''),
87
+ "source_id",
88
+ "source_version"
89
+ );
90
+
91
+ CREATE INDEX "workspace_instruction_policy_onboarding_proposals_workspace_time_idx"
92
+ ON "workspace_instruction_policy_onboarding_proposals"
93
+ ("workspace_id", "created_at" DESC, "id" DESC);
94
+
95
+ CREATE OR REPLACE FUNCTION workspace_instruction_policy_validate_onboarding_proposal()
96
+ RETURNS trigger
97
+ LANGUAGE plpgsql
98
+ AS $$
99
+ BEGIN
100
+ IF NEW."baseline_revision_id" IS NULL THEN
101
+ IF EXISTS (
102
+ SELECT 1
103
+ FROM "workspace_instruction_policy_heads" head
104
+ WHERE head."account_id" = NEW."account_id"
105
+ AND head."workspace_id" = NEW."workspace_id"
106
+ AND head."kind" = NEW."kind"
107
+ AND head."scope" = NEW."scope"
108
+ AND head."role_key" IS NOT DISTINCT FROM NEW."role_key"
109
+ ) THEN
110
+ RAISE EXCEPTION 'instruction-policy onboarding proposal must capture the exact active head baseline'
111
+ USING ERRCODE = '23514';
112
+ END IF;
113
+ ELSIF NOT EXISTS (
114
+ SELECT 1
115
+ FROM "workspace_instruction_policy_heads" head
116
+ WHERE head."account_id" = NEW."account_id"
117
+ AND head."workspace_id" = NEW."workspace_id"
118
+ AND head."kind" = NEW."kind"
119
+ AND head."scope" = NEW."scope"
120
+ AND head."role_key" IS NOT DISTINCT FROM NEW."role_key"
121
+ AND head."revision_id" = NEW."baseline_revision_id"
122
+ AND head."revision" = NEW."baseline_revision"
123
+ AND head."content_hash" = NEW."baseline_content_hash"
124
+ AND head."activation_version" = NEW."baseline_activation_version"
125
+ AND head."activated_at" = NEW."baseline_activated_at"
126
+ ) THEN
127
+ RAISE EXCEPTION 'instruction-policy onboarding proposal must capture the exact active head baseline'
128
+ USING ERRCODE = '23514';
129
+ END IF;
130
+
131
+ IF NEW."baseline_revision_id" IS NOT NULL AND NOT EXISTS (
132
+ SELECT 1
133
+ FROM "workspace_instruction_policy_revisions" revision
134
+ WHERE revision."id" = NEW."baseline_revision_id"
135
+ AND revision."account_id" = NEW."account_id"
136
+ AND revision."workspace_id" = NEW."workspace_id"
137
+ AND revision."kind" = NEW."kind"
138
+ AND revision."scope" = NEW."scope"
139
+ AND revision."role_key" IS NOT DISTINCT FROM NEW."role_key"
140
+ AND revision."revision" = NEW."baseline_revision"
141
+ AND revision."content_hash" = NEW."baseline_content_hash"
142
+ ) THEN
143
+ RAISE EXCEPTION 'instruction-policy onboarding proposal has an invalid baseline revision'
144
+ USING ERRCODE = '23514';
145
+ END IF;
146
+
147
+ IF NOT EXISTS (
148
+ SELECT 1
149
+ FROM "workspace_instruction_policy_revisions" revision
150
+ WHERE revision."id" = NEW."draft_revision_id"
151
+ AND revision."operation_id" = NEW."operation_id"
152
+ AND revision."request_fingerprint" = NEW."request_fingerprint"
153
+ AND revision."account_id" = NEW."account_id"
154
+ AND revision."workspace_id" = NEW."workspace_id"
155
+ AND revision."kind" = NEW."kind"
156
+ AND revision."scope" = NEW."scope"
157
+ AND revision."role_key" IS NOT DISTINCT FROM NEW."role_key"
158
+ AND revision."revision" = NEW."draft_revision"
159
+ AND revision."content_hash" = NEW."draft_content_hash"
160
+ AND revision."provenance_source" = 'onboarding'
161
+ AND revision."provenance_source_id" = NEW."id"::text
162
+ AND revision."supersedes_revision_id" IS NOT DISTINCT FROM NEW."baseline_revision_id"
163
+ AND revision."created_by_subject_id" = NEW."created_by_subject_id"
164
+ ) THEN
165
+ RAISE EXCEPTION 'instruction-policy onboarding proposal must identify its exact inactive draft'
166
+ USING ERRCODE = '23514';
167
+ END IF;
168
+
169
+ IF EXISTS (
170
+ SELECT 1
171
+ FROM "workspace_instruction_policy_heads" head
172
+ WHERE head."account_id" = NEW."account_id"
173
+ AND head."workspace_id" = NEW."workspace_id"
174
+ AND head."revision_id" = NEW."draft_revision_id"
175
+ ) OR EXISTS (
176
+ SELECT 1
177
+ FROM "workspace_instruction_policy_activation_events" event
178
+ WHERE event."account_id" = NEW."account_id"
179
+ AND event."workspace_id" = NEW."workspace_id"
180
+ AND event."new_revision_id" = NEW."draft_revision_id"
181
+ ) THEN
182
+ RAISE EXCEPTION 'instruction-policy onboarding proposal must identify a never-activated inactive draft'
183
+ USING ERRCODE = '23514';
184
+ END IF;
185
+ RETURN NEW;
186
+ END;
187
+ $$;
188
+
189
+ CREATE TRIGGER workspace_instruction_policy_onboarding_proposals_validate_draft
190
+ BEFORE INSERT OR UPDATE ON "workspace_instruction_policy_onboarding_proposals"
191
+ FOR EACH ROW EXECUTE FUNCTION workspace_instruction_policy_validate_onboarding_proposal();
192
+
193
+ CREATE TRIGGER workspace_instruction_policy_onboarding_proposals_immutable
194
+ BEFORE UPDATE OR DELETE ON "workspace_instruction_policy_onboarding_proposals"
195
+ FOR EACH ROW EXECUTE FUNCTION workspace_instruction_policy_reject_mutation();
196
+
197
+ ALTER TABLE "workspace_instruction_policy_onboarding_proposals" ENABLE ROW LEVEL SECURITY;
198
+ ALTER TABLE "workspace_instruction_policy_onboarding_proposals" FORCE ROW LEVEL SECURITY;
199
+
200
+ CREATE POLICY workspace_isolation ON "workspace_instruction_policy_onboarding_proposals"
201
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
202
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.23.0",
3
+ "version": "0.27.0",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@opengeni/codex": "^0.2.10",
54
- "@opengeni/config": "^0.10.3",
55
- "@opengeni/contracts": "^0.32.0",
54
+ "@opengeni/config": "^0.10.7",
55
+ "@opengeni/contracts": "^0.36.0",
56
56
  "@opengeni/network": "^0.1.1",
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "postgres": "^3.4.7"