@opengeni/db 1.0.2 → 1.2.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 (86) hide show
  1. package/dist/artifact-catalog.d.ts +12 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-reconciliation.js +2 -2
  4. package/dist/{chunk-KH3TYDGY.js → chunk-3GAR5UN3.js} +2 -2
  5. package/dist/{chunk-UEQQ2WWU.js → chunk-3TGYFFZR.js} +3 -26
  6. package/dist/chunk-3TGYFFZR.js.map +1 -0
  7. package/dist/{chunk-F3YMVRF4.js → chunk-6EIEGHPK.js} +4 -4
  8. package/dist/{chunk-OQZOGCLG.js → chunk-CONWNUSL.js} +2 -2
  9. package/dist/{chunk-7S53WLPI.js → chunk-CYP72JTQ.js} +11 -4
  10. package/dist/{chunk-7S53WLPI.js.map → chunk-CYP72JTQ.js.map} +1 -1
  11. package/dist/{chunk-LDTQGYD2.js → chunk-FWQNHNOU.js} +54 -2
  12. package/dist/chunk-FWQNHNOU.js.map +1 -0
  13. package/dist/{chunk-KEDWSSNP.js → chunk-GDN5TSZP.js} +2 -2
  14. package/dist/{chunk-PQGTWFRF.js → chunk-JICUP7ZR.js} +2 -2
  15. package/dist/{chunk-NLSG7GGA.js → chunk-JLVWBIEM.js} +3 -3
  16. package/dist/{chunk-Y77PVZZD.js → chunk-LXPFNG4L.js} +2 -2
  17. package/dist/{chunk-56MYWKQV.js → chunk-O2Y5E5M3.js} +68 -32
  18. package/dist/chunk-O2Y5E5M3.js.map +1 -0
  19. package/dist/{chunk-7POTLB24.js → chunk-QFJNEXFL.js} +2 -2
  20. package/dist/{chunk-MACELCXG.js → chunk-UCIMHNII.js} +101 -21
  21. package/dist/chunk-UCIMHNII.js.map +1 -0
  22. package/dist/{chunk-OMHBVHX7.js → chunk-VDPCIFKL.js} +5 -5
  23. package/dist/{chunk-N5QCKHYL.js → chunk-YOYILDLY.js} +87 -19
  24. package/dist/chunk-YOYILDLY.js.map +1 -0
  25. package/dist/connect-attempts.js +3 -3
  26. package/dist/credit-balances.d.ts +18 -0
  27. package/dist/credit-promotion-policy.d.ts +10 -0
  28. package/dist/editable-artifact-durable-export.js +2 -2
  29. package/dist/editable-artifacts.js +3 -3
  30. package/dist/index.d.ts +35 -10
  31. package/dist/index.js +2432 -2255
  32. package/dist/index.js.map +1 -1
  33. package/dist/managed-auth-session-sets.js +3 -3
  34. package/dist/managed-user-setup.d.ts +1 -0
  35. package/dist/mcp-operations.js +2 -2
  36. package/dist/organization-integration-policy.js +3 -3
  37. package/dist/organization-membership-lifecycle.d.ts +1 -0
  38. package/dist/persistence-errors.d.ts +9 -6
  39. package/dist/provision-roles.js +1 -1
  40. package/dist/retained-provider-commands.js +6 -6
  41. package/dist/runtime-posture.d.ts +7 -6
  42. package/dist/schema.d.ts +214 -2
  43. package/dist/schema.js +5 -1
  44. package/dist/session-background-commands.js +4 -4
  45. package/dist/session-command-output.js +5 -5
  46. package/dist/session-event-slices.js +3 -3
  47. package/dist/session-mcp-credential-rotation.js +5 -5
  48. package/dist/session-tenancy.d.ts +6 -1
  49. package/dist/session-tenancy.js +3 -5
  50. package/dist/video-generation.js +3 -3
  51. package/dist/workspace-tool-defaults.js +4 -4
  52. package/drizzle/0610_artifact_catalog_pins.sql +165 -0
  53. package/drizzle/0611_universal_session_tenancy_activation.sql +166 -0
  54. package/drizzle/0612_scheduled_model_unavailable_refusal.sql +21 -0
  55. package/drizzle/0613_model_scoped_promotional_credits.sql +238 -0
  56. package/drizzle/0614_quiescence_command_containment.sql +7 -0
  57. package/drizzle/0615_credit_promotion_policy_validation.sql +63 -0
  58. package/package.json +7 -7
  59. package/src/artifact-catalog.ts +31 -5
  60. package/src/credit-balances.ts +109 -0
  61. package/src/credit-promotion-policy.ts +12 -0
  62. package/src/database.ts +81 -42
  63. package/src/index.ts +295 -79
  64. package/src/managed-user-setup.ts +8 -0
  65. package/src/organization-membership-lifecycle.ts +8 -0
  66. package/src/persistence-errors.ts +17 -6
  67. package/src/provision-roles.ts +25 -0
  68. package/src/runtime-posture-cli.ts +0 -2
  69. package/src/runtime-posture.ts +86 -27
  70. package/src/schema.ts +57 -1
  71. package/src/session-tenancy.ts +9 -31
  72. package/src/video-generation.ts +2 -7
  73. package/dist/chunk-56MYWKQV.js.map +0 -1
  74. package/dist/chunk-LDTQGYD2.js.map +0 -1
  75. package/dist/chunk-MACELCXG.js.map +0 -1
  76. package/dist/chunk-N5QCKHYL.js.map +0 -1
  77. package/dist/chunk-UEQQ2WWU.js.map +0 -1
  78. /package/dist/{chunk-KH3TYDGY.js.map → chunk-3GAR5UN3.js.map} +0 -0
  79. /package/dist/{chunk-F3YMVRF4.js.map → chunk-6EIEGHPK.js.map} +0 -0
  80. /package/dist/{chunk-OQZOGCLG.js.map → chunk-CONWNUSL.js.map} +0 -0
  81. /package/dist/{chunk-KEDWSSNP.js.map → chunk-GDN5TSZP.js.map} +0 -0
  82. /package/dist/{chunk-PQGTWFRF.js.map → chunk-JICUP7ZR.js.map} +0 -0
  83. /package/dist/{chunk-NLSG7GGA.js.map → chunk-JLVWBIEM.js.map} +0 -0
  84. /package/dist/{chunk-Y77PVZZD.js.map → chunk-LXPFNG4L.js.map} +0 -0
  85. /package/dist/{chunk-7POTLB24.js.map → chunk-QFJNEXFL.js.map} +0 -0
  86. /package/dist/{chunk-OMHBVHX7.js.map → chunk-VDPCIFKL.js.map} +0 -0
@@ -0,0 +1,238 @@
1
+ -- deployment-mode: rolling
2
+ -- Deploy every reader and debit writer before configuring scoped offers.
3
+ -- Existing grants keep NULL eligibility and remain unrestricted.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '10min';
6
+
7
+ ALTER TABLE credit_ledger_entries ADD COLUMN eligible_model_ids text[];
8
+ ALTER TABLE credit_ledger_entries ADD CONSTRAINT credit_ledger_scope_valid CHECK (
9
+ eligible_model_ids IS NULL OR (type = 'grant' AND amount_micros > 0
10
+ AND cardinality(eligible_model_ids) BETWEEN 1 AND 40
11
+ AND array_position(eligible_model_ids, NULL) IS NULL
12
+ AND array_position(eligible_model_ids, '') IS NULL)
13
+ );
14
+ CREATE UNIQUE INDEX credit_ledger_entries_id_account_idx ON credit_ledger_entries (id, account_id);
15
+ CREATE INDEX credit_ledger_scoped_grants_idx ON credit_ledger_entries (account_id, created_at, id)
16
+ WHERE eligible_model_ids IS NOT NULL;
17
+ CREATE FUNCTION opengeni_private.preserve_credit_grant_scope() RETURNS trigger
18
+ LANGUAGE plpgsql SET search_path = pg_catalog AS $body$
19
+ BEGIN
20
+ IF NEW.eligible_model_ids IS DISTINCT FROM OLD.eligible_model_ids
21
+ OR (OLD.eligible_model_ids IS NOT NULL AND (
22
+ NEW.amount_micros IS DISTINCT FROM OLD.amount_micros
23
+ OR NEW.account_id IS DISTINCT FROM OLD.account_id
24
+ OR NEW.metadata->>'creditOfferLabel' IS DISTINCT FROM OLD.metadata->>'creditOfferLabel'
25
+ OR NEW.metadata->>'creditOfferId' IS DISTINCT FROM OLD.metadata->>'creditOfferId'
26
+ OR NEW.source_type IS DISTINCT FROM OLD.source_type
27
+ )) THEN
28
+ RAISE EXCEPTION 'Issued promotional credit terms are immutable' USING ERRCODE = '23514';
29
+ END IF;
30
+ RETURN NEW;
31
+ END $body$;
32
+ REVOKE ALL ON FUNCTION opengeni_private.preserve_credit_grant_scope() FROM PUBLIC;
33
+ CREATE TRIGGER preserve_credit_grant_scope BEFORE UPDATE ON credit_ledger_entries
34
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.preserve_credit_grant_scope();
35
+
36
+ CREATE TABLE credit_debit_allocations (
37
+ account_id uuid NOT NULL,
38
+ debit_entry_id uuid NOT NULL,
39
+ grant_entry_id uuid NOT NULL,
40
+ amount_micros bigint NOT NULL CONSTRAINT credit_debit_allocations_positive CHECK (amount_micros > 0),
41
+ PRIMARY KEY (debit_entry_id, grant_entry_id),
42
+ FOREIGN KEY (debit_entry_id, account_id) REFERENCES credit_ledger_entries (id, account_id) ON DELETE CASCADE,
43
+ FOREIGN KEY (grant_entry_id, account_id) REFERENCES credit_ledger_entries (id, account_id) ON DELETE CASCADE
44
+ );
45
+ CREATE INDEX credit_debit_allocations_grant_idx ON credit_debit_allocations (account_id, grant_entry_id);
46
+ ALTER TABLE credit_debit_allocations ENABLE ROW LEVEL SECURITY;
47
+ ALTER TABLE credit_debit_allocations FORCE ROW LEVEL SECURITY;
48
+ CREATE POLICY credit_debit_allocations_account_isolation ON credit_debit_allocations
49
+ USING (opengeni_private.account_rls_visible(account_id))
50
+ WITH CHECK (opengeni_private.account_rls_visible(account_id));
51
+ REVOKE ALL ON credit_debit_allocations FROM PUBLIC;
52
+ DO $grants$ BEGIN
53
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
54
+ GRANT SELECT, INSERT ON credit_debit_allocations TO opengeni_app;
55
+ END IF;
56
+ END $grants$;
57
+
58
+
59
+ -- Runtime policy: one deployment-wide default, with signup/coupon overrides.
60
+ -- Revisions stay available so an in-flight call can settle under its admitted policy.
61
+ CREATE TABLE opengeni_private.credit_promotion_policy_revisions (
62
+ revision bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
63
+ policy jsonb NOT NULL,
64
+ operator text NOT NULL CHECK (operator = btrim(operator) AND length(operator) BETWEEN 1 AND 200),
65
+ reason text NOT NULL CHECK (reason = btrim(reason) AND length(reason) BETWEEN 6 AND 1000),
66
+ database_role text NOT NULL DEFAULT session_user,
67
+ changed_at timestamptz NOT NULL DEFAULT clock_timestamp()
68
+ );
69
+ REVOKE ALL ON opengeni_private.credit_promotion_policy_revisions FROM PUBLIC;
70
+ CREATE TRIGGER credit_promotion_policy_revisions_immutable
71
+ BEFORE UPDATE OR DELETE ON opengeni_private.credit_promotion_policy_revisions
72
+ FOR EACH ROW EXECUTE FUNCTION reject_verified_signup_trial_switch_revision_mutation();
73
+ CREATE TRIGGER credit_promotion_policy_revisions_no_truncate
74
+ BEFORE TRUNCATE ON opengeni_private.credit_promotion_policy_revisions
75
+ FOR EACH STATEMENT EXECUTE FUNCTION reject_verified_signup_trial_switch_revision_mutation();
76
+
77
+ CREATE FUNCTION set_credit_promotion_policy(p_policy jsonb, p_operator text, p_reason text)
78
+ RETURNS bigint LANGUAGE plpgsql SET search_path = pg_catalog AS $body$
79
+ DECLARE
80
+ model_list jsonb;
81
+ offer jsonb;
82
+ written_revision bigint;
83
+ BEGIN
84
+ IF p_policy IS NULL OR jsonb_typeof(p_policy) <> 'object'
85
+ OR NOT p_policy ? 'defaultModelIds'
86
+ OR p_policy - ARRAY['defaultModelIds', 'signupModelIds', 'offers'] <> '{}'::jsonb
87
+ OR (p_policy ? 'offers' AND jsonb_typeof(p_policy->'offers') <> 'object') THEN
88
+ RAISE EXCEPTION 'Policy requires defaultModelIds and optional signupModelIds/offers' USING ERRCODE = '22023';
89
+ END IF;
90
+ FOR offer IN SELECT value FROM jsonb_each(coalesce(p_policy->'offers', '{}'::jsonb)) LOOP
91
+ IF jsonb_typeof(offer) <> 'object'
92
+ OR offer - ARRAY['label', 'eligibleModelIds'] <> '{}'::jsonb
93
+ OR jsonb_typeof(offer->'label') IS DISTINCT FROM 'string'
94
+ OR length(btrim(offer->>'label')) NOT BETWEEN 1 AND 120 THEN
95
+ RAISE EXCEPTION 'Invalid credit offer' USING ERRCODE = '22023';
96
+ END IF;
97
+ END LOOP;
98
+ FOR model_list IN
99
+ SELECT p_policy->'defaultModelIds'
100
+ UNION ALL SELECT p_policy->'signupModelIds' WHERE p_policy ? 'signupModelIds'
101
+ UNION ALL SELECT value->'eligibleModelIds'
102
+ FROM jsonb_each(coalesce(p_policy->'offers', '{}'::jsonb)) WHERE value ? 'eligibleModelIds'
103
+ LOOP
104
+ IF jsonb_typeof(model_list) IS DISTINCT FROM 'array' THEN
105
+ RAISE EXCEPTION 'Model list must be an array' USING ERRCODE = '22023';
106
+ END IF;
107
+ IF jsonb_array_length(model_list) NOT BETWEEN 1 AND 40 OR EXISTS (
108
+ SELECT 1 FROM jsonb_array_elements(model_list) id
109
+ WHERE jsonb_typeof(id) <> 'string' OR length(btrim(id #>> '{}')) NOT BETWEEN 1 AND 200
110
+ OR id #>> '{}' <> btrim(id #>> '{}')
111
+ ) THEN
112
+ RAISE EXCEPTION 'Model list requires 1-40 canonical model IDs' USING ERRCODE = '22023';
113
+ END IF;
114
+ END LOOP;
115
+ -- Serializes revision order across simultaneous operator updates.
116
+ PERFORM pg_advisory_xact_lock(hashtextextended('credit-promotion-policy', 0));
117
+ INSERT INTO opengeni_private.credit_promotion_policy_revisions (policy, operator, reason)
118
+ VALUES (p_policy, p_operator, p_reason) RETURNING revision INTO written_revision;
119
+ RETURN written_revision;
120
+ END $body$;
121
+ REVOKE ALL ON FUNCTION set_credit_promotion_policy(jsonb, text, text) FROM PUBLIC;
122
+ DO $policy_acl$
123
+ DECLARE grantee_name text;
124
+ BEGIN
125
+ FOR grantee_name IN
126
+ SELECT DISTINCT r.rolname FROM pg_class c
127
+ CROSS JOIN LATERAL aclexplode(coalesce(c.relacl, acldefault('r', c.relowner))) a
128
+ JOIN pg_roles r ON r.oid = a.grantee
129
+ WHERE c.oid = 'opengeni_private.credit_promotion_policy_revisions'::regclass AND a.grantee <> c.relowner
130
+ LOOP
131
+ EXECUTE format('REVOKE ALL ON opengeni_private.credit_promotion_policy_revisions FROM %I', grantee_name);
132
+ END LOOP;
133
+ FOR grantee_name IN
134
+ SELECT DISTINCT r.rolname FROM pg_proc p
135
+ CROSS JOIN LATERAL aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
136
+ JOIN pg_roles r ON r.oid = a.grantee
137
+ WHERE p.oid = 'set_credit_promotion_policy(jsonb,text,text)'::regprocedure AND a.grantee <> p.proowner
138
+ LOOP
139
+ EXECUTE format('REVOKE ALL ON FUNCTION set_credit_promotion_policy(jsonb,text,text) FROM %I', grantee_name);
140
+ END LOOP;
141
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
142
+ GRANT SELECT ON opengeni_private.credit_promotion_policy_revisions TO opengeni_app;
143
+ END IF;
144
+ END $policy_acl$;
145
+
146
+ CREATE OR REPLACE FUNCTION opengeni_private.grant_verified_signup_trial_credit()
147
+ RETURNS trigger
148
+ LANGUAGE plpgsql
149
+ SECURITY DEFINER
150
+ SET search_path FROM CURRENT
151
+ AS $body$
152
+ DECLARE
153
+ verified_owner boolean;
154
+ account_owner boolean;
155
+ runtime_enabled boolean;
156
+ inserted_count integer;
157
+ model_ids jsonb;
158
+ BEGIN
159
+ IF pg_catalog.current_setting('opengeni.verified_signup_trial_enabled', true)
160
+ IS DISTINCT FROM 'on' THEN
161
+ RETURN NEW;
162
+ END IF;
163
+
164
+ -- Shared against the setter's exclusive lock; a missing revision fails closed.
165
+ PERFORM pg_catalog.pg_advisory_xact_lock_shared(
166
+ pg_catalog.hashtextextended('verified-signup-trial-switch', 0)
167
+ );
168
+ SELECT revision.grants_enabled
169
+ INTO runtime_enabled
170
+ FROM opengeni_private.verified_signup_trial_switch_revisions revision
171
+ ORDER BY revision.revision DESC
172
+ LIMIT 1;
173
+ IF runtime_enabled IS DISTINCT FROM TRUE THEN
174
+ RETURN NEW;
175
+ END IF;
176
+
177
+ -- This is a defense in depth check: the sole receipt writer already checks
178
+ -- the canonical verified human and the absence of prior memberships.
179
+ SELECT candidate.email_verified IS TRUE
180
+ INTO verified_owner
181
+ FROM auth_users candidate
182
+ WHERE candidate.id = NEW.auth_user_id;
183
+ SELECT account.external_source = 'better-auth:user'
184
+ AND account.external_id = NEW.auth_user_id
185
+ INTO account_owner
186
+ FROM managed_accounts account
187
+ WHERE account.id = NEW.account_id;
188
+ IF verified_owner IS DISTINCT FROM TRUE
189
+ OR account_owner IS DISTINCT FROM TRUE
190
+ OR opengeni_private.current_subject_id() IS DISTINCT FROM 'user:' || NEW.auth_user_id
191
+ OR opengeni_private.current_account_id() IS DISTINCT FROM NEW.account_id
192
+ OR NEW.result ->> 'organizationId' IS DISTINCT FROM NEW.account_id::text
193
+ THEN
194
+ RAISE EXCEPTION 'verified signup trial requires an exact new owner'
195
+ USING ERRCODE = '42501';
196
+ END IF;
197
+
198
+ SELECT coalesce(policy->'signupModelIds', policy->'defaultModelIds') INTO model_ids
199
+ FROM opengeni_private.credit_promotion_policy_revisions ORDER BY revision DESC LIMIT 1;
200
+ model_ids := coalesce(model_ids, nullif(pg_catalog.current_setting('opengeni.signup_credit_model_ids', true), '')::jsonb);
201
+ INSERT INTO credit_ledger_entries (
202
+ account_id, workspace_id, type, amount_micros, source_type, source_id,
203
+ idempotency_key, metadata, eligible_model_ids
204
+ ) VALUES (
205
+ NEW.account_id, NULL, 'grant', 10000000, 'verified_signup_trial',
206
+ NEW.auth_user_id, 'verified-signup-trial:v1:' || NEW.auth_user_id,
207
+ pg_catalog.jsonb_build_object('campaign', 'verified_signup_trial_v1',
208
+ 'creditOfferLabel', 'Signup credits'),
209
+ CASE WHEN model_ids IS NULL THEN NULL
210
+ ELSE ARRAY(SELECT pg_catalog.jsonb_array_elements_text(model_ids)) END
211
+ ) ON CONFLICT (idempotency_key) DO NOTHING;
212
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
213
+ IF inserted_count <> 1 THEN
214
+ RAISE EXCEPTION 'verified signup trial credit idempotency collision'
215
+ USING ERRCODE = '23505';
216
+ END IF;
217
+ RETURN NEW;
218
+ END
219
+ $body$;
220
+
221
+ -- CREATE OR REPLACE resets SET search_path FROM CURRENT: re-pin both definers.
222
+ DO $posture$
223
+ DECLARE
224
+ data_schema text := pg_catalog.current_schema();
225
+ BEGIN
226
+ EXECUTE pg_catalog.format(
227
+ 'ALTER FUNCTION opengeni_private.grant_verified_signup_trial_credit() '
228
+ || 'SET search_path = pg_catalog, %I, opengeni_private, pg_temp',
229
+ data_schema
230
+ );
231
+ EXECUTE pg_catalog.format(
232
+ 'ALTER FUNCTION %I.set_verified_signup_trial_credits_enabled(boolean, text, text) '
233
+ || 'SET search_path = pg_catalog, %I, pg_temp',
234
+ data_schema,
235
+ data_schema
236
+ );
237
+ END
238
+ $posture$;
@@ -0,0 +1,7 @@
1
+ -- deployment-mode: rolling
2
+ -- Additive reason: existing capture/termination fences remain unchanged.
3
+ ALTER TABLE sandbox_leases
4
+ DROP CONSTRAINT sandbox_leases_command_containment_reason_check,
5
+ ADD CONSTRAINT sandbox_leases_command_containment_reason_check
6
+ CHECK (command_containment_reason IS NULL OR command_containment_reason IN
7
+ ('idle_containment', 'provider_deadline_containment', 'quiescence_containment'));
@@ -0,0 +1,63 @@
1
+ -- deployment-mode: rolling
2
+ SET LOCAL lock_timeout = '5s';
3
+ SET LOCAL statement_timeout = '10min';
4
+
5
+ -- Keep owner SQL updates readable by CreditPromotionPolicy. Replacing the
6
+ -- function preserves its owner-only ACL and the immutable revision history.
7
+ CREATE OR REPLACE FUNCTION set_credit_promotion_policy(p_policy jsonb, p_operator text, p_reason text)
8
+ RETURNS bigint LANGUAGE plpgsql SET search_path = pg_catalog AS $body$
9
+ DECLARE
10
+ -- ECMAScript String.trim whitespace; PostgreSQL btrim defaults to ASCII space.
11
+ trim_chars CONSTANT text := U&'\0009\000A\000B\000C\000D\0020\00A0\1680\2000\2001\2002\2003\2004\2005\2006\2007\2008\2009\200A\2028\2029\202F\205F\3000\FEFF';
12
+ model_list jsonb;
13
+ model_id text;
14
+ offer_id text;
15
+ offer jsonb;
16
+ offer_label text;
17
+ written_revision bigint;
18
+ BEGIN
19
+ IF p_policy IS NULL OR jsonb_typeof(p_policy) <> 'object'
20
+ OR NOT p_policy ? 'defaultModelIds'
21
+ OR p_policy - ARRAY['defaultModelIds', 'signupModelIds', 'offers'] <> '{}'::jsonb
22
+ OR (p_policy ? 'offers' AND jsonb_typeof(p_policy->'offers') <> 'object') THEN
23
+ RAISE EXCEPTION 'Policy requires defaultModelIds and optional signupModelIds/offers' USING ERRCODE = '22023';
24
+ END IF;
25
+ FOR offer_id, offer IN SELECT key, value FROM jsonb_each(coalesce(p_policy->'offers', '{}'::jsonb)) LOOP
26
+ IF offer_id = '' OR jsonb_typeof(offer) <> 'object'
27
+ OR offer - ARRAY['label', 'eligibleModelIds'] <> '{}'::jsonb
28
+ OR jsonb_typeof(offer->'label') IS DISTINCT FROM 'string' THEN
29
+ RAISE EXCEPTION 'Invalid credit offer' USING ERRCODE = '22023';
30
+ END IF;
31
+ offer_label := btrim(offer->>'label', trim_chars);
32
+ -- JavaScript string limits count UTF-16 units: supplementary characters use two.
33
+ IF length(regexp_replace(offer_label, U&'[\+010000-\+10FFFF]', 'xx', 'g')) NOT BETWEEN 1 AND 120 THEN
34
+ RAISE EXCEPTION 'Invalid credit offer label' USING ERRCODE = '22023';
35
+ END IF;
36
+ END LOOP;
37
+ FOR model_list IN
38
+ SELECT p_policy->'defaultModelIds'
39
+ UNION ALL SELECT p_policy->'signupModelIds' WHERE p_policy ? 'signupModelIds'
40
+ UNION ALL SELECT value->'eligibleModelIds'
41
+ FROM jsonb_each(coalesce(p_policy->'offers', '{}'::jsonb)) WHERE value ? 'eligibleModelIds'
42
+ LOOP
43
+ IF jsonb_typeof(model_list) IS DISTINCT FROM 'array' THEN
44
+ RAISE EXCEPTION 'Model list must be an array' USING ERRCODE = '22023';
45
+ END IF;
46
+ IF jsonb_array_length(model_list) NOT BETWEEN 1 AND 40 OR EXISTS (
47
+ SELECT 1 FROM jsonb_array_elements(model_list) id WHERE jsonb_typeof(id) <> 'string'
48
+ ) THEN
49
+ RAISE EXCEPTION 'Model list requires 1-40 canonical model IDs' USING ERRCODE = '22023';
50
+ END IF;
51
+ FOR model_id IN SELECT jsonb_array_elements_text(model_list) LOOP
52
+ IF model_id <> btrim(model_id, trim_chars)
53
+ OR length(regexp_replace(model_id, U&'[\+010000-\+10FFFF]', 'xx', 'g')) NOT BETWEEN 1 AND 200 THEN
54
+ RAISE EXCEPTION 'Model list requires 1-40 canonical model IDs' USING ERRCODE = '22023';
55
+ END IF;
56
+ END LOOP;
57
+ END LOOP;
58
+ -- Serializes revision order across simultaneous operator updates.
59
+ PERFORM pg_advisory_xact_lock(hashtextextended('credit-promotion-policy', 0));
60
+ INSERT INTO opengeni_private.credit_promotion_policy_revisions (policy, operator, reason)
61
+ VALUES (p_policy, p_operator, p_reason) RETURNING revision INTO written_revision;
62
+ RETURN written_revision;
63
+ END $body$;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "1.0.2",
3
+ "version": "1.2.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": {
@@ -115,12 +115,12 @@
115
115
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
116
116
  },
117
117
  "dependencies": {
118
- "@opengeni/codemode": "1.0.2",
119
- "@opengeni/codex": "1.0.2",
120
- "@opengeni/config": "1.0.2",
121
- "@opengeni/contracts": "1.0.2",
122
- "@opengeni/network": "1.0.2",
123
- "@opengeni/observability": "1.0.2",
118
+ "@opengeni/codemode": "1.2.0",
119
+ "@opengeni/codex": "1.2.0",
120
+ "@opengeni/config": "1.2.0",
121
+ "@opengeni/contracts": "1.2.0",
122
+ "@opengeni/network": "1.2.0",
123
+ "@opengeni/observability": "1.2.0",
124
124
  "drizzle-orm": "^0.45.2",
125
125
  "postgres": "^3.4.7"
126
126
  },
@@ -2,7 +2,13 @@ import type { ArtifactCatalogKind, ArtifactCatalogListQuery } from "@opengeni/co
2
2
  import { sql, type SQL } from "drizzle-orm";
3
3
  import { withRlsContext, type Database } from "./database";
4
4
 
5
- export type ArtifactCatalogPosition = { key: string; kind: ArtifactCatalogKind; id: string };
5
+ export type ArtifactCatalogPosition = {
6
+ key: string;
7
+ kind: ArtifactCatalogKind;
8
+ id: string;
9
+ /** Legacy cursor frontiers did not carry this bit. */
10
+ pinned?: boolean | undefined;
11
+ };
6
12
  /** Internal candidates are NOT an authorized public projection. */
7
13
  export type ArtifactCatalogCandidate = {
8
14
  id: string;
@@ -16,8 +22,22 @@ export type ArtifactCatalogCandidate = {
16
22
  source_session_id: string | null;
17
23
  version_id: string | null;
18
24
  sort_key: string;
25
+ pinned?: boolean;
19
26
  };
20
27
 
28
+ /** API callers must first verify publish permission and domain-specific read authority. */
29
+ export async function updateArtifactPin(
30
+ db: Database,
31
+ scope: { accountId: string; workspaceId: string },
32
+ input: { kind: ArtifactCatalogKind; artifactId: string; pinned: boolean },
33
+ ): Promise<void> {
34
+ await withRlsContext(db, scope, async (tx) => {
35
+ await tx.execute(sql`select opengeni_private.update_artifact_pin(
36
+ ${scope.accountId}::uuid, ${scope.workspaceId}::uuid,
37
+ ${input.kind}::text, ${input.artifactId}::text, ${input.pinned}::boolean)`);
38
+ });
39
+ }
40
+
21
41
  /** Called only after immutable bytes have been retained by explicit publication. */
22
42
  export async function recordSandboxFilePublication(
23
43
  db: Database,
@@ -84,7 +104,7 @@ export async function listArtifactCatalogCandidates(
84
104
  branches.push(sql`
85
105
  SELECT p.file_id::text AS id, p.kind, p.title, 'active'::text AS status,
86
106
  p.published_at AS created_at, p.published_at AS updated_at, p.source_session_id, NULL::text AS version_id, 'sandbox_file'::text AS origin
87
- FROM opengeni_private.list_sandbox_file_publications(${scope.accountId}::uuid, ${scope.workspaceId}::uuid,
107
+ FROM opengeni_private.list_sandbox_file_publications_pinned(${scope.accountId}::uuid, ${scope.workspaceId}::uuid,
88
108
  ${JSON.stringify({ sourceSessionId: source, q: query.q, kinds: publishedKinds, sort: query.sort, snapshotAt: input.snapshotAt, after: input.after, limit: input.limit })}::jsonb) p`);
89
109
  if (!branches.length) return [];
90
110
  const sort =
@@ -95,22 +115,28 @@ export async function listArtifactCatalogCandidates(
95
115
  : sql`to_char(updated_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US')`;
96
116
  const descending = query.sort !== "title";
97
117
  const position = input.after;
98
- const keyset = !position
118
+ const secondaryKeyset = !position
99
119
  ? sql`true`
100
120
  : sql`(
101
121
  sort_key COLLATE "C" ${descending ? sql`<` : sql`>`} ${position.key} COLLATE "C"
102
122
  OR (sort_key = ${position.key} AND (kind COLLATE "C", id COLLATE "C") > (${position.kind}, ${position.id})))`;
123
+ const keyset = !position
124
+ ? sql`true`
125
+ : sql`(pinned < ${position.pinned ?? false}::boolean OR
126
+ (pinned = ${position.pinned ?? false}::boolean AND ${secondaryKeyset}))`;
103
127
  return withRlsContext(
104
128
  db,
105
129
  scope,
106
130
  async (tx) => {
107
131
  const result = await tx.execute(sql`
108
132
  WITH candidates AS (${sql.join(branches, sql` UNION ALL `)}),
109
- ordered AS (SELECT *, ${sort} AS sort_key FROM candidates
133
+ ordered AS (SELECT candidates.*, ${sort} AS sort_key, (pins.artifact_id IS NOT NULL) AS pinned
134
+ FROM candidates LEFT JOIN opengeni_private.list_artifact_pins(${scope.accountId}::uuid, ${scope.workspaceId}::uuid) pins
135
+ ON pins.kind = candidates.kind AND pins.artifact_id = candidates.id
110
136
  WHERE status = ${query.status} AND created_at <= ${input.snapshotAt}::timestamptz
111
137
  ${query.q ? sql`AND strpos(lower(title), lower(${query.q})) > 0` : sql``})
112
138
  SELECT * FROM ordered WHERE ${keyset}
113
- ORDER BY sort_key COLLATE "C" ${descending ? sql`DESC` : sql`ASC`}, kind COLLATE "C", id COLLATE "C"
139
+ ORDER BY pinned DESC, sort_key COLLATE "C" ${descending ? sql`DESC` : sql`ASC`}, kind COLLATE "C", id COLLATE "C"
114
140
  LIMIT ${input.limit}`);
115
141
  return (
116
142
  Array.isArray(result) ? result : (result as { rows: ArtifactCatalogCandidate[] }).rows
@@ -0,0 +1,109 @@
1
+ import type { BillingBalance, PromotionalCreditBalance } from "@opengeni/contracts";
2
+ import { sql } from "drizzle-orm";
3
+ import type { Database } from "./database";
4
+ import { rawRows, withAccountRls } from "./database";
5
+ import * as schema from "./schema";
6
+
7
+ /** Read totals and grant remainders from one database snapshot. */
8
+ type CreditBalanceSnapshot = BillingBalance & { creditPolicyRevision?: number };
9
+
10
+ export async function getBillingBalance(
11
+ db: Database,
12
+ accountId: string,
13
+ policyRevision?: number,
14
+ ): Promise<CreditBalanceSnapshot> {
15
+ return await withAccountRls(db, accountId, async (tx) => {
16
+ const [row] = await rawRows<{
17
+ balance: string;
18
+ grants: PromotionalCreditBalance[];
19
+ revision: number;
20
+ }>(
21
+ tx,
22
+ sql`
23
+ with current_policy as (
24
+ select revision, policy from opengeni_private.credit_promotion_policy_revisions
25
+ where (${policyRevision ?? null}::bigint is null or revision <= ${policyRevision ?? null}::bigint)
26
+ order by revision desc limit 1
27
+ )
28
+ select
29
+ coalesce((select revision from current_policy), 0)::int as revision,
30
+ (select coalesce(sum(amount_micros), 0)::text
31
+ from ${schema.creditLedgerEntries} where account_id = ${accountId}) as balance,
32
+ coalesce((select jsonb_agg(jsonb_build_object(
33
+ 'grantId', g.id,
34
+ 'label', coalesce(g.metadata->>'creditOfferLabel', 'Promotional credits'),
35
+ 'eligibleModelIds', coalesce(
36
+ (select coalesce(
37
+ case when g.source_type = 'verified_signup_trial' then policy->'signupModelIds'
38
+ else policy->'offers'->(g.metadata->>'creditOfferId')->'eligibleModelIds' end,
39
+ policy->'defaultModelIds'
40
+ ) from current_policy), to_jsonb(g.eligible_model_ids)),
41
+ 'remainingMicros', g.amount_micros - coalesce(used.amount, 0)
42
+ ) order by g.created_at, g.id)
43
+ from ${schema.creditLedgerEntries} g
44
+ left join (
45
+ select grant_entry_id, sum(amount_micros) as amount
46
+ from ${schema.creditDebitAllocations} where account_id = ${accountId}
47
+ group by grant_entry_id
48
+ ) used on used.grant_entry_id = g.id
49
+ where g.account_id = ${accountId} and g.eligible_model_ids is not null
50
+ and g.amount_micros > coalesce(used.amount, 0)
51
+ ), '[]'::jsonb) as grants
52
+ `,
53
+ );
54
+ const balanceMicros = Number(row?.balance ?? 0);
55
+ const promotionalCredits = row?.grants ?? [];
56
+ return {
57
+ accountId,
58
+ creditPolicyRevision: row?.revision ?? 0,
59
+ balanceMicros,
60
+ generalBalanceMicros:
61
+ balanceMicros - promotionalCredits.reduce((sum, grant) => sum + grant.remainingMicros, 0),
62
+ promotionalCredits,
63
+ currency: "usd",
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ });
67
+ }
68
+
69
+ /** Missing model means a non-model resource, which can only use general credits. */
70
+ export function spendableCreditMicros(balance: BillingBalance, modelId?: string): number {
71
+ return (
72
+ Math.max(0, balance.generalBalanceMicros ?? balance.balanceMicros) +
73
+ (balance.promotionalCredits ?? []).reduce(
74
+ (sum, grant) =>
75
+ sum + (modelId && grant.eligibleModelIds.includes(modelId) ? grant.remainingMicros : 0),
76
+ 0,
77
+ )
78
+ );
79
+ }
80
+
81
+ export async function getSpendableCreditBalance(
82
+ db: Database,
83
+ accountId: string,
84
+ modelId?: string,
85
+ policyRevision?: number,
86
+ ): Promise<CreditBalanceSnapshot> {
87
+ const balance = await getBillingBalance(db, accountId, policyRevision);
88
+ return { ...balance, balanceMicros: spendableCreditMicros(balance, modelId) };
89
+ }
90
+
91
+ export function planCreditDebit(
92
+ balance: BillingBalance,
93
+ requestedMicros: number,
94
+ modelId?: string,
95
+ ) {
96
+ if (!Number.isSafeInteger(requestedMicros) || requestedMicros <= 0) {
97
+ throw new Error("credit debit requires a positive, safe integer micro amount");
98
+ }
99
+ const debitedMicros = Math.min(requestedMicros, spendableCreditMicros(balance, modelId));
100
+ let remaining = debitedMicros;
101
+ const allocations: { grantEntryId: string; amountMicros: number }[] = [];
102
+ for (const grant of balance.promotionalCredits ?? []) {
103
+ if (!remaining || !modelId || !grant.eligibleModelIds.includes(modelId)) continue;
104
+ const amountMicros = Math.min(remaining, grant.remainingMicros);
105
+ allocations.push({ grantEntryId: grant.grantId, amountMicros });
106
+ remaining -= amountMicros;
107
+ }
108
+ return { debitedMicros, allocations };
109
+ }
@@ -0,0 +1,12 @@
1
+ import { CreditPromotionPolicy } from "@opengeni/config";
2
+ import { sql } from "drizzle-orm";
3
+ import { rawRows, type Database } from "./database";
4
+
5
+ /** Runtime policy supersedes deployment bootstrap configuration. No process cache. */
6
+ export async function readCreditPromotionPolicy(db: Database) {
7
+ const [row] = await rawRows<{ policy: unknown }>(
8
+ db,
9
+ sql`select policy from opengeni_private.credit_promotion_policy_revisions order by revision desc limit 1`,
10
+ );
11
+ return row ? CreditPromotionPolicy.parse(row.policy) : null;
12
+ }