@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.
- package/dist/{chunk-3UCHDMKG.js → chunk-EX7CDSGH.js} +196 -2
- package/dist/chunk-EX7CDSGH.js.map +1 -0
- package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
- package/dist/chunk-IHPCI4GV.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +24 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +1457 -338
- package/dist/index.js.map +1 -1
- package/dist/preference-registry.d.ts +14 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +3 -3
- package/dist/schema.d.ts +262 -4
- package/dist/schema.js +5 -1
- package/dist/session-realtime-mirror.d.ts +22 -1
- package/dist/workspace-instruction-policies-schema.d.ts +449 -0
- package/dist/workspace-instruction-policies.d.ts +88 -8
- package/drizzle/0165_document_authority_foundation.sql +259 -0
- package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
- package/drizzle/0167_document_index_replay_authority.sql +61 -0
- package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
- package/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -0
- package/package.json +3 -3
- package/src/connection-token-resolver.ts +79 -16
- package/src/index.ts +419 -23
- package/src/preference-registry.ts +103 -0
- package/src/runtime-posture.ts +4 -0
- package/src/schema.ts +86 -0
- package/src/session-control.ts +48 -1
- package/src/session-queue-commands.ts +45 -11
- package/src/session-realtime-mirror.ts +117 -1
- package/src/session-realtime.ts +49 -1
- package/src/workspace-instruction-policies-schema.ts +116 -0
- package/src/workspace-instruction-policies.ts +819 -25
- package/dist/chunk-3UCHDMKG.js.map +0 -1
- 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.
|
|
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.
|
|
55
|
-
"@opengeni/contracts": "^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"
|