@opengeni/db 0.12.1 → 0.12.6

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/src/schema.ts CHANGED
@@ -756,6 +756,8 @@ export const sessions = pgTable(
756
756
  // mapSession exposes those rows as `legacy` instead of guessing omitted vs
757
757
  // explicit [].
758
758
  toolPolicy: jsonb("tool_policy").$type<SessionToolPolicy>(),
759
+ // Optimistic-concurrency fence for durable session tool-policy writes.
760
+ toolPolicyVersion: integer("tool_policy_version").notNull().default(1),
759
761
  // The manager session that spawned this one via session_create. Set only
760
762
  // when the creating grant carried a worker-signed sessionId claim (a session
761
763
  // spawning a worker); null for direct API creates and scheduled-task runs.
@@ -1152,6 +1154,9 @@ export const documentBases = pgTable(
1152
1154
  table.workspaceId,
1153
1155
  table.createdAt,
1154
1156
  ),
1157
+ defaultName: uniqueIndex("document_bases_workspace_default_name_uq")
1158
+ .on(table.workspaceId)
1159
+ .where(sql`lower(btrim(${table.name})) = 'default'`),
1155
1160
  }),
1156
1161
  );
1157
1162
 
@@ -1185,6 +1190,17 @@ export const documents = pgTable(
1185
1190
  sourceUpdatedAt: timestamp("source_updated_at", { withTimezone: true }),
1186
1191
  sourceVersion: text("source_version"),
1187
1192
  aclTags: jsonb("acl_tags").$type<string[]>().notNull().default([]),
1193
+ // Per-document access controls. visibility 'private' restricts human reads to
1194
+ // created_by (a grant subject id, not a uuid); agent_access=false hides the
1195
+ // document from agent retrieval surfaces (docs MCP) while humans keep REST.
1196
+ visibility: text("visibility").notNull().default("workspace"),
1197
+ createdBy: text("created_by"),
1198
+ agentAccess: boolean("agent_access").notNull().default(true),
1199
+ // Auto-curation output (knowledge drops).
1200
+ summary: text("summary"),
1201
+ topics: jsonb("topics").$type<string[]>().notNull().default([]),
1202
+ curationStatus: text("curation_status").notNull().default("none"),
1203
+ curation: jsonb("curation").$type<Record<string, unknown>>(),
1188
1204
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
1189
1205
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
1190
1206
  },
@@ -1207,6 +1223,27 @@ export const documents = pgTable(
1207
1223
  table.workspaceId,
1208
1224
  table.sourceExternalId,
1209
1225
  ),
1226
+ curationStatus: index("documents_workspace_curation_status_idx").on(
1227
+ table.workspaceId,
1228
+ table.curationStatus,
1229
+ ),
1230
+ visibilityState: check(
1231
+ "documents_visibility_chk",
1232
+ sql`${table.visibility} in ('workspace', 'private')`,
1233
+ ),
1234
+ curationState: check(
1235
+ "documents_curation_status_chk",
1236
+ sql`${table.curationStatus} in ('none', 'pending', 'suggested', 'auto_filed', 'failed')`,
1237
+ ),
1238
+ privateCreator: check(
1239
+ "documents_private_creator_chk",
1240
+ sql`${table.visibility} <> 'private' or nullif(btrim(${table.createdBy}), '') is not null`,
1241
+ ),
1242
+ topicsArray: check("documents_topics_array_chk", sql`jsonb_typeof(${table.topics}) = 'array'`),
1243
+ curationObject: check(
1244
+ "documents_curation_object_chk",
1245
+ sql`${table.curation} is null or jsonb_typeof(${table.curation}) = 'object'`,
1246
+ ),
1210
1247
  }),
1211
1248
  );
1212
1249
 
@@ -1485,7 +1522,8 @@ export const sessionTurnAttempts = pgTable(
1485
1522
  "session_turn_attempts_outcome_check",
1486
1523
  sql`${table.outcome} is null or ${table.outcome} in (
1487
1524
  'completed', 'failed', 'cancelled', 'superseded', 'requires_action',
1488
- 'interrupted_recoverable', 'lease_lost_recoverable', 'pre_cutover_closed'
1525
+ 'waiting_capacity', 'interrupted_recoverable', 'lease_lost_recoverable',
1526
+ 'pre_cutover_closed'
1489
1527
  )`,
1490
1528
  ),
1491
1529
  closedConsistent: check(
@@ -2014,12 +2052,13 @@ export const codexCapacityWaiters = pgTable(
2014
2052
  .notNull()
2015
2053
  .references(() => workspaces.id, { onDelete: "cascade" }),
2016
2054
  sessionId: uuid("session_id").notNull(),
2017
- goalId: uuid("goal_id").notNull(),
2055
+ goalId: uuid("goal_id"),
2018
2056
  blockedTurnId: uuid("blocked_turn_id").notNull(),
2057
+ blockedTurnGeneration: integer("blocked_turn_generation").notNull(),
2019
2058
  workflowId: text("workflow_id").notNull(),
2020
2059
  generation: integer("generation").notNull().default(1),
2021
2060
  status: text("status").notNull().default("waiting"), // waiting | resumed | superseded
2022
- goalVersion: integer("goal_version").notNull(),
2061
+ goalVersion: integer("goal_version"),
2023
2062
  policyHash: text("policy_hash"),
2024
2063
  earliestResetAt: timestamp("earliest_reset_at", { withTimezone: true }),
2025
2064
  nextCheckAt: timestamp("next_check_at", { withTimezone: true }).notNull(),
@@ -2118,6 +2157,7 @@ export const sessionEvents = pgTable(
2118
2157
  .where(
2119
2158
  sql`${table.type} not in ('agent.message.delta', 'agent.reasoning.delta', 'sandbox.command.output.delta', 'terminal.pty.output.delta')`,
2120
2159
  ),
2160
+ duplicateOfEvent: index("session_events_duplicate_of_event_idx").on(table.duplicateOfEventId),
2121
2161
  payloadBytes: check(
2122
2162
  "session_events_payload_bytes_check",
2123
2163
  sql`octet_length(${table.payload}::text) <= 65536`,
@@ -2499,6 +2539,9 @@ export const sandboxLeases = pgTable(
2499
2539
  reaperIdx: index("sandbox_leases_reaper_idx")
2500
2540
  .on(table.expiresAt)
2501
2541
  .where(sql`${table.liveness} in ('warming','warm','draining')`),
2542
+ expiredDrainingInventory: index("sandbox_leases_expired_draining_inventory_idx")
2543
+ .on(table.expiresAt, table.backend)
2544
+ .where(sql`${table.liveness} = 'draining'`),
2502
2545
  workspaceGenerationValid: check(
2503
2546
  "sandbox_leases_workspace_generation_check",
2504
2547
  sql`${table.workspaceGeneration} >= 0`,
@@ -2736,6 +2779,19 @@ export const sandboxRetainedProcesses = pgTable(
2736
2779
  settlementReason: text("settlement_reason"),
2737
2780
  startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
2738
2781
  settledAt: timestamp("settled_at", { withTimezone: true }),
2782
+ // Coordination state for bounded terminal-owner reconciliation. While a
2783
+ // claim is live, reconcileAfter is its expiry; otherwise it is the next
2784
+ // retry time. Becoming due only licenses an exact provider probe and is
2785
+ // never exit/loss proof.
2786
+ reconcileAfter: timestamp("reconcile_after", { withTimezone: true }).notNull().defaultNow(),
2787
+ reconcileClaimId: uuid("reconcile_claim_id"),
2788
+ reconcileClaimedAt: timestamp("reconcile_claimed_at", { withTimezone: true }),
2789
+ reconcileAttempts: integer("reconcile_attempts").notNull().default(0),
2790
+ lastReconcileOutcome: text("last_reconcile_outcome"),
2791
+ reconcileProofOutcome: text("reconcile_proof_outcome", { enum: ["exited", "lost"] }),
2792
+ reconcileProofExitCode: integer("reconcile_proof_exit_code"),
2793
+ reconcileProofReason: text("reconcile_proof_reason"),
2794
+ reconcileProofObservedAt: timestamp("reconcile_proof_observed_at", { withTimezone: true }),
2739
2795
  },
2740
2796
  (table) => ({
2741
2797
  workspaceAccount: foreignKey({
@@ -2788,6 +2844,12 @@ export const sandboxRetainedProcesses = pgTable(
2788
2844
  active: index("sandbox_retained_processes_active_idx")
2789
2845
  .on(table.workspaceId, table.sessionId, table.startedAt)
2790
2846
  .where(sql`${table.state} = 'active'`),
2847
+ reconcileDue: index("sandbox_retained_processes_reconcile_due_idx")
2848
+ .on(table.reconcileAfter, table.startedAt, table.id)
2849
+ .where(sql`${table.state} = 'active'`),
2850
+ activeInventory: index("sandbox_retained_processes_active_inventory_idx")
2851
+ .on(table.ownerActorKind, table.workspaceId, table.ownerTurnId, table.ownerAttemptId)
2852
+ .where(sql`${table.state} = 'active'`),
2791
2853
  identityValid: check(
2792
2854
  "sandbox_retained_processes_identity_check",
2793
2855
  sql`${table.leaseEpoch} >= 0
@@ -2824,6 +2886,41 @@ export const sandboxRetainedProcesses = pgTable(
2824
2886
  sql`${table.settlementReason} is null
2825
2887
  or octet_length(${table.settlementReason}) between 1 and 512`,
2826
2888
  ),
2889
+ reconcileClaimValid: check(
2890
+ "sandbox_retained_processes_reconcile_claim_check",
2891
+ sql`(${table.reconcileClaimId} is null and ${table.reconcileClaimedAt} is null)
2892
+ or (${table.reconcileClaimId} is not null and ${table.reconcileClaimedAt} is not null)`,
2893
+ ),
2894
+ reconcileAttemptsValid: check(
2895
+ "sandbox_retained_processes_reconcile_attempts_check",
2896
+ sql`${table.reconcileAttempts} >= 0`,
2897
+ ),
2898
+ reconcileOutcomeValid: check(
2899
+ "sandbox_retained_processes_reconcile_outcome_check",
2900
+ sql`${table.lastReconcileOutcome} is null
2901
+ or octet_length(${table.lastReconcileOutcome}) between 1 and 64`,
2902
+ ),
2903
+ reconcileProofValid: check(
2904
+ "sandbox_retained_processes_reconcile_proof_check",
2905
+ sql`(
2906
+ ${table.reconcileProofOutcome} is null
2907
+ and ${table.reconcileProofExitCode} is null
2908
+ and ${table.reconcileProofReason} is null
2909
+ and ${table.reconcileProofObservedAt} is null
2910
+ ) or (
2911
+ ${table.reconcileProofOutcome} = 'exited'
2912
+ and ${table.reconcileProofExitCode} is not null
2913
+ and ${table.reconcileProofReason} = 'provider_exit_banner'
2914
+ and ${table.reconcileProofObservedAt} is not null
2915
+ ) or (
2916
+ ${table.reconcileProofOutcome} = 'lost'
2917
+ and ${table.reconcileProofExitCode} is null
2918
+ and ${table.reconcileProofReason} in (
2919
+ 'provider_session_lost_banner', 'provider_instance_not_found'
2920
+ )
2921
+ and ${table.reconcileProofObservedAt} is not null
2922
+ )`,
2923
+ ),
2827
2924
  }),
2828
2925
  );
2829
2926
 
@@ -3410,10 +3507,17 @@ export const githubInstallations = pgTable(
3410
3507
  .notNull()
3411
3508
  .references(() => workspaces.id, { onDelete: "cascade" }),
3412
3509
  installationId: integer("installation_id").notNull(),
3510
+ githubAccountId: bigint("github_account_id", { mode: "number" }),
3413
3511
  accountLogin: text("account_login"),
3414
3512
  accountType: text("account_type"),
3415
3513
  repositoryScope: text("repository_scope").notNull().default("all"),
3416
3514
  linkedBySubjectId: text("linked_by_subject_id"),
3515
+ githubActorId: bigint("github_actor_id", { mode: "number" }),
3516
+ githubActorLogin: text("github_actor_login"),
3517
+ authorityKind: text("authority_kind"),
3518
+ authorityCheckedAt: timestamp("authority_checked_at", { withTimezone: true }),
3519
+ authorityExpiresAt: timestamp("authority_expires_at", { withTimezone: true }),
3520
+ authorityNonce: text("authority_nonce"),
3417
3521
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
3418
3522
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
3419
3523
  },
@@ -3428,6 +3532,55 @@ export const githubInstallations = pgTable(
3428
3532
  "github_installations_repository_scope_check",
3429
3533
  sql`${table.repositoryScope} in ('all', 'selected')`,
3430
3534
  ),
3535
+ authorityKindCheck: check(
3536
+ "github_installations_authority_kind_check",
3537
+ sql`
3538
+ (
3539
+ ${table.githubAccountId} is null
3540
+ and ${table.githubActorId} is null
3541
+ and ${table.githubActorLogin} is null
3542
+ and ${table.authorityKind} is null
3543
+ and ${table.authorityCheckedAt} is null
3544
+ and ${table.authorityExpiresAt} is null
3545
+ and ${table.authorityNonce} is null
3546
+ )
3547
+ or (
3548
+ ${table.githubAccountId} is not null
3549
+ and ${table.githubAccountId} > 0
3550
+ and ${table.githubActorId} is not null
3551
+ and ${table.githubActorId} > 0
3552
+ and ${table.githubActorLogin} is not null
3553
+ and length(${table.githubActorLogin}) > 0
3554
+ and ${table.accountLogin} is not null
3555
+ and length(${table.accountLogin}) > 0
3556
+ and ${table.accountType} is not null
3557
+ and ${table.linkedBySubjectId} is not null
3558
+ and length(${table.linkedBySubjectId}) > 0
3559
+ and ${table.authorityKind} is not null
3560
+ and ${table.authorityCheckedAt} is not null
3561
+ and ${table.authorityExpiresAt} is not null
3562
+ and ${table.authorityCheckedAt} < ${table.authorityExpiresAt}
3563
+ and ${table.authorityExpiresAt} <= ${table.authorityCheckedAt} + interval '10 minutes'
3564
+ and ${table.authorityNonce} is not null
3565
+ and length(${table.authorityNonce}) > 0
3566
+ and ${table.repositoryScope} = 'selected'
3567
+ and (
3568
+ (
3569
+ ${table.authorityKind} = 'personal_owner'
3570
+ and ${table.accountType} = 'User'
3571
+ and ${table.githubActorId} = ${table.githubAccountId}
3572
+ )
3573
+ or (
3574
+ ${table.authorityKind} = 'organization_owner'
3575
+ and ${table.accountType} = 'Organization'
3576
+ )
3577
+ )
3578
+ )
3579
+ `,
3580
+ ),
3581
+ authorityNonce: uniqueIndex("github_installations_authority_nonce_uq")
3582
+ .on(table.authorityNonce)
3583
+ .where(sql`${table.authorityNonce} is not null`),
3431
3584
  }),
3432
3585
  );
3433
3586
 
@@ -34,6 +34,7 @@ export type SessionTurnAttemptOutcome =
34
34
  | "cancelled"
35
35
  | "superseded"
36
36
  | "requires_action"
37
+ | "waiting_capacity"
37
38
  | "interrupted_recoverable"
38
39
  | "lease_lost_recoverable"
39
40
  | "pre_cutover_closed";
@@ -1,155 +0,0 @@
1
- // src/provision-roles.ts
2
- import postgres from "postgres";
3
- async function provisionRoles(adminConnection, options = {}) {
4
- const schema = validateIdentifier("targetSchema", options.targetSchema ?? "public");
5
- const rlsStrategy = options.rlsStrategy ?? "force";
6
- const appRole = validateIdentifier(
7
- "appRole",
8
- options.appRole ?? (process.env.OPENGENI_APP_DATABASE_USER?.trim() || "opengeni_app")
9
- );
10
- const appPassword = options.appPassword ?? process.env.OPENGENI_APP_DATABASE_PASSWORD;
11
- const hostExportRole = validateIdentifier(
12
- "hostExportRole",
13
- options.hostExportRole ?? (process.env.OPENGENI_HOST_EXPORT_DATABASE_USER?.trim() || "opengeni_host_exporter")
14
- );
15
- const hostExportPassword = options.hostExportPassword ?? process.env.OPENGENI_HOST_EXPORT_DATABASE_PASSWORD;
16
- const temporalRole = validateIdentifier(
17
- "temporalRole",
18
- options.temporalRole ?? (process.env.OPENGENI_TEMPORAL_DATABASE_USER?.trim() || "opengeni_temporal")
19
- );
20
- const temporalPassword = options.temporalPassword ?? process.env.OPENGENI_TEMPORAL_DATABASE_PASSWORD;
21
- const temporalDatabases = (options.temporalDatabases ?? commaSeparated(process.env.OPENGENI_TEMPORAL_DATABASES ?? "temporal,temporal_visibility")).map((name) => validateIdentifier("temporalDatabases", name));
22
- const sql = postgres(adminConnection, { max: 1 });
23
- try {
24
- let provisionedAppRole = null;
25
- if (rlsStrategy === "force") {
26
- if (!appPassword) {
27
- throw new Error(
28
- "OPENGENI_APP_DATABASE_PASSWORD (or appPassword) is required for rlsStrategy 'force'"
29
- );
30
- }
31
- await ensureLoginRole(sql, appRole, appPassword);
32
- provisionedAppRole = appRole;
33
- }
34
- if (temporalPassword) {
35
- await ensureLoginRole(sql, temporalRole, temporalPassword);
36
- for (const database of temporalDatabases) {
37
- await ensureDatabase(sql, database, temporalRole);
38
- await grantTemporalRoleInDatabase(adminConnection, database, temporalRole);
39
- }
40
- }
41
- if (hostExportPassword) {
42
- await ensureLoginRole(sql, hostExportRole, hostExportPassword);
43
- await grantHostExportRoleIfSchemaExists(sql, hostExportRole);
44
- }
45
- if (rlsStrategy === "force") {
46
- await grantAppRoleIfSchemaExists(sql, appRole, schema);
47
- }
48
- return {
49
- appRole: provisionedAppRole,
50
- hostExportRole: hostExportPassword ? hostExportRole : null,
51
- temporalRole: temporalPassword ? temporalRole : null,
52
- temporalDatabases: temporalPassword ? temporalDatabases : [],
53
- schema,
54
- rlsStrategy
55
- };
56
- } finally {
57
- await sql.end();
58
- }
59
- }
60
- async function grantHostExportRoleIfSchemaExists(sql, role) {
61
- await sql.unsafe(`
62
- DO $$
63
- BEGIN
64
- IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'opengeni_host_export') THEN
65
- EXECUTE format('GRANT USAGE ON SCHEMA opengeni_host_export TO %I', ${literal(role)});
66
- EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_host_export TO %I', ${literal(role)});
67
- EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA opengeni_host_export GRANT EXECUTE ON FUNCTIONS TO %I', ${literal(role)});
68
- END IF;
69
- END $$;
70
- `);
71
- }
72
- async function ensureLoginRole(sql, role, password) {
73
- await sql.unsafe(`
74
- DO $$
75
- BEGIN
76
- IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = ${literal(role)}) THEN
77
- EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', ${literal(role)}, ${literal(password)});
78
- ELSE
79
- EXECUTE format('ALTER ROLE %I LOGIN PASSWORD %L', ${literal(role)}, ${literal(password)});
80
- END IF;
81
- END $$;
82
- `);
83
- }
84
- async function ensureDatabase(sql, database, owner) {
85
- const existing = await sql`
86
- select exists(select 1 from pg_database where datname = ${database}) as exists
87
- `;
88
- if (!existing[0]?.exists) {
89
- await sql.unsafe(`CREATE DATABASE ${identifier(database)} OWNER ${identifier(owner)}`);
90
- }
91
- await sql.unsafe(
92
- `GRANT ALL PRIVILEGES ON DATABASE ${identifier(database)} TO ${identifier(owner)}`
93
- );
94
- }
95
- async function grantTemporalRoleInDatabase(adminConnection, database, role) {
96
- const databaseUrl = databaseUrlFor(adminConnection, database);
97
- const databaseSql = postgres(databaseUrl, { max: 1 });
98
- try {
99
- await databaseSql.unsafe(`GRANT USAGE, CREATE ON SCHEMA public TO ${identifier(role)}`);
100
- } finally {
101
- await databaseSql.end();
102
- }
103
- }
104
- async function grantAppRoleIfSchemaExists(sql, role, schema) {
105
- await sql.unsafe(`
106
- DO $$
107
- BEGIN
108
- IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ${literal(schema)}) THEN
109
- EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});
110
- EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});
111
- END IF;
112
- IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'opengeni_private') THEN
113
- EXECUTE format('GRANT USAGE ON SCHEMA opengeni_private TO %I', ${literal(role)});
114
- EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private TO %I', ${literal(role)});
115
- END IF;
116
- END $$;
117
- `);
118
- }
119
- function commaSeparated(value) {
120
- return value.split(",").map((item) => item.trim()).filter(Boolean);
121
- }
122
- function validateIdentifier(name, value) {
123
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
124
- throw new Error(`${name} contains an invalid Postgres identifier: ${value}`);
125
- }
126
- return value;
127
- }
128
- function identifier(value) {
129
- return `"${value.replace(/"/g, '""')}"`;
130
- }
131
- function literal(value) {
132
- return `'${value.replace(/'/g, "''")}'`;
133
- }
134
- function databaseUrlFor(value, database) {
135
- const url = new URL(value);
136
- url.pathname = `/${database}`;
137
- return url.toString();
138
- }
139
- if (import.meta.main) {
140
- const adminUrl = process.env.OPENGENI_MIGRATIONS_DATABASE_URL ?? process.env.OPENGENI_DATABASE_ADMIN_URL ?? process.env.OPENGENI_DATABASE_URL;
141
- if (!adminUrl) {
142
- throw new Error(
143
- "OPENGENI_MIGRATIONS_DATABASE_URL, OPENGENI_DATABASE_ADMIN_URL, or OPENGENI_DATABASE_URL is required"
144
- );
145
- }
146
- const result = await provisionRoles(adminUrl, {
147
- ...process.env.OPENGENI_DB_SCHEMA?.trim() ? { targetSchema: process.env.OPENGENI_DB_SCHEMA.trim() } : {}
148
- });
149
- console.log(JSON.stringify(result, null, 2));
150
- }
151
-
152
- export {
153
- provisionRoles
154
- };
155
- //# sourceMappingURL=chunk-BMFDXFPA.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/provision-roles.ts"],"sourcesContent":["import postgres from \"postgres\";\nimport type { RlsStrategy } from \"./index\";\n\nexport type ProvisionResult = {\n appRole: string | null;\n hostExportRole: string | null;\n temporalRole: string | null;\n temporalDatabases: string[];\n schema: string;\n rlsStrategy: RlsStrategy;\n};\n\nexport type ProvisionRolesOptions = {\n /**\n * The schema OpenGeni's tables live in. The app-role GRANTs target this\n * schema + `opengeni_private`. Defaults to `public` (standalone).\n */\n targetSchema?: string;\n /**\n * RLS posture (Step I). `\"force\"` (default) provisions the non-owner\n * `opengeni_app` login role and GRANTs it table DML in the target schema —\n * the role OpenGeni connects as under FORCE-RLS. `\"scoped\"` SKIPS the app-role\n * provisioning entirely: the embedded host runs OpenGeni's queries over a role\n * IT owns/manages (typically the schema owner), so OpenGeni neither creates\n * nor grants the `opengeni_app` role. Temporal-role provisioning is unaffected\n * by strategy.\n */\n rlsStrategy?: RlsStrategy;\n appRole?: string;\n appPassword?: string;\n /**\n * Optional cross-workspace projection role. It receives schema USAGE and\n * EXECUTE only on the host-export API; it receives no table privileges.\n * Provision it after the first migration run so the schema exists. The\n * provisioner also registers same-owner default privileges for future\n * host-export functions; shipped migrations preserve existing exporter ACLs\n * when a migration-only upgrade adds a function.\n */\n hostExportRole?: string;\n hostExportPassword?: string;\n temporalRole?: string;\n temporalPassword?: string;\n temporalDatabases?: string[];\n};\n\n/**\n * SDK entry point (Step I): provision the OpenGeni database roles + grants over\n * a host-supplied admin connection. This is the named, parameterized form of the\n * historical env-driven `provision-roles` script (which still works as a CLI via\n * the `import.meta.main` block at the bottom — it just reads env into these\n * options).\n *\n * STANDALONE (default): `provisionRoles(adminConnection)` with no options →\n * `targetSchema: \"public\"`, `rlsStrategy: \"force\"`, reads `opengeni_app` creds\n * from env. Byte-for-byte the historical script behavior.\n *\n * EMBEDDED: `provisionRoles(adminConnection, { targetSchema, rlsStrategy })` lets\n * a host provision the app role over a dedicated schema (force) OR skip the\n * app role entirely and own the connection role itself (scoped).\n */\nexport async function provisionRoles(\n adminConnection: string,\n options: ProvisionRolesOptions = {},\n): Promise<ProvisionResult> {\n const schema = validateIdentifier(\"targetSchema\", options.targetSchema ?? \"public\");\n const rlsStrategy: RlsStrategy = options.rlsStrategy ?? \"force\";\n\n const appRole = validateIdentifier(\n \"appRole\",\n options.appRole ?? (process.env.OPENGENI_APP_DATABASE_USER?.trim() || \"opengeni_app\"),\n );\n const appPassword = options.appPassword ?? process.env.OPENGENI_APP_DATABASE_PASSWORD;\n const hostExportRole = validateIdentifier(\n \"hostExportRole\",\n options.hostExportRole ??\n (process.env.OPENGENI_HOST_EXPORT_DATABASE_USER?.trim() || \"opengeni_host_exporter\"),\n );\n const hostExportPassword =\n options.hostExportPassword ?? process.env.OPENGENI_HOST_EXPORT_DATABASE_PASSWORD;\n const temporalRole = validateIdentifier(\n \"temporalRole\",\n options.temporalRole ??\n (process.env.OPENGENI_TEMPORAL_DATABASE_USER?.trim() || \"opengeni_temporal\"),\n );\n const temporalPassword =\n options.temporalPassword ?? process.env.OPENGENI_TEMPORAL_DATABASE_PASSWORD;\n const temporalDatabases = (\n options.temporalDatabases ??\n commaSeparated(process.env.OPENGENI_TEMPORAL_DATABASES ?? \"temporal,temporal_visibility\")\n ).map((name) => validateIdentifier(\"temporalDatabases\", name));\n\n const sql = postgres(adminConnection, { max: 1 });\n try {\n // FORCE strategy provisions the non-owner app role OpenGeni connects as.\n // SCOPED strategy: the host owns the connection role; OpenGeni provisions no\n // app role (skipped here), only the optional Temporal role.\n let provisionedAppRole: string | null = null;\n if (rlsStrategy === \"force\") {\n if (!appPassword) {\n throw new Error(\n \"OPENGENI_APP_DATABASE_PASSWORD (or appPassword) is required for rlsStrategy 'force'\",\n );\n }\n await ensureLoginRole(sql, appRole, appPassword);\n provisionedAppRole = appRole;\n }\n\n if (temporalPassword) {\n await ensureLoginRole(sql, temporalRole, temporalPassword);\n for (const database of temporalDatabases) {\n await ensureDatabase(sql, database, temporalRole);\n await grantTemporalRoleInDatabase(adminConnection, database, temporalRole);\n }\n }\n\n if (hostExportPassword) {\n await ensureLoginRole(sql, hostExportRole, hostExportPassword);\n await grantHostExportRoleIfSchemaExists(sql, hostExportRole);\n }\n\n if (rlsStrategy === \"force\") {\n await grantAppRoleIfSchemaExists(sql, appRole, schema);\n }\n\n return {\n appRole: provisionedAppRole,\n hostExportRole: hostExportPassword ? hostExportRole : null,\n temporalRole: temporalPassword ? temporalRole : null,\n temporalDatabases: temporalPassword ? temporalDatabases : [],\n schema,\n rlsStrategy,\n };\n } finally {\n await sql.end();\n }\n}\n\n/**\n * The exporter is intentionally separate from `opengeni_app`: its functions\n * project every workspace into a host-owned sink and therefore cannot be made\n * available to the tenant-scoped application role.\n */\nasync function grantHostExportRoleIfSchemaExists(sql: postgres.Sql, role: string): Promise<void> {\n await sql.unsafe(`\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'opengeni_host_export') THEN\n EXECUTE format('GRANT USAGE ON SCHEMA opengeni_host_export TO %I', ${literal(role)});\n EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_host_export TO %I', ${literal(role)});\n EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA opengeni_host_export GRANT EXECUTE ON FUNCTIONS TO %I', ${literal(role)});\n END IF;\nEND $$;\n`);\n}\n\nasync function ensureLoginRole(sql: postgres.Sql, role: string, password: string): Promise<void> {\n await sql.unsafe(`\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = ${literal(role)}) THEN\n EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', ${literal(role)}, ${literal(password)});\n ELSE\n EXECUTE format('ALTER ROLE %I LOGIN PASSWORD %L', ${literal(role)}, ${literal(password)});\n END IF;\nEND $$;\n`);\n}\n\nasync function ensureDatabase(sql: postgres.Sql, database: string, owner: string): Promise<void> {\n const existing = await sql<{ exists: boolean }[]>`\n select exists(select 1 from pg_database where datname = ${database}) as exists\n `;\n if (!existing[0]?.exists) {\n await sql.unsafe(`CREATE DATABASE ${identifier(database)} OWNER ${identifier(owner)}`);\n }\n await sql.unsafe(\n `GRANT ALL PRIVILEGES ON DATABASE ${identifier(database)} TO ${identifier(owner)}`,\n );\n}\n\nasync function grantTemporalRoleInDatabase(\n adminConnection: string,\n database: string,\n role: string,\n): Promise<void> {\n const databaseUrl = databaseUrlFor(adminConnection, database);\n const databaseSql = postgres(databaseUrl, { max: 1 });\n try {\n await databaseSql.unsafe(`GRANT USAGE, CREATE ON SCHEMA public TO ${identifier(role)}`);\n } finally {\n await databaseSql.end();\n }\n}\n\n/**\n * Grant the app role table DML in the OpenGeni data schema + EXECUTE on the\n * `opengeni_private` helper functions. Schema-parameterized (Step I): standalone\n * passes `public`; embedded passes the dedicated schema. The grants are guarded\n * on schema existence so provisioning before migrate is a safe no-op.\n */\nasync function grantAppRoleIfSchemaExists(\n sql: postgres.Sql,\n role: string,\n schema: string,\n): Promise<void> {\n await sql.unsafe(`\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ${literal(schema)}) THEN\n EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});\n END IF;\n IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'opengeni_private') THEN\n EXECUTE format('GRANT USAGE ON SCHEMA opengeni_private TO %I', ${literal(role)});\n EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private TO %I', ${literal(role)});\n END IF;\nEND $$;\n`);\n}\n\nfunction commaSeparated(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction validateIdentifier(name: string, value: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n throw new Error(`${name} contains an invalid Postgres identifier: ${value}`);\n }\n return value;\n}\n\nfunction identifier(value: string): string {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction literal(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nfunction databaseUrlFor(value: string, database: string): string {\n const url = new URL(value);\n url.pathname = `/${database}`;\n return url.toString();\n}\n\n// CLI form (unchanged behavior): read env into the SDK options and run. This is\n// what `bun src/provision-roles.ts` / the `provision-roles` package script\n// invokes — standalone byte-for-byte the historical script (public schema,\n// force strategy, env-driven creds).\nif (import.meta.main) {\n const adminUrl =\n process.env.OPENGENI_MIGRATIONS_DATABASE_URL ??\n process.env.OPENGENI_DATABASE_ADMIN_URL ??\n process.env.OPENGENI_DATABASE_URL;\n if (!adminUrl) {\n throw new Error(\n \"OPENGENI_MIGRATIONS_DATABASE_URL, OPENGENI_DATABASE_ADMIN_URL, or OPENGENI_DATABASE_URL is required\",\n );\n }\n const result = await provisionRoles(adminUrl, {\n ...(process.env.OPENGENI_DB_SCHEMA?.trim()\n ? { targetSchema: process.env.OPENGENI_DB_SCHEMA.trim() }\n : {}),\n });\n console.log(JSON.stringify(result, null, 2));\n}\n"],"mappings":"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","names":[]}