@opengeni/db 0.12.0 → 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/dist/{chunk-VUKRIBO5.js → chunk-CYA6BHV6.js} +150 -7
- package/dist/chunk-CYA6BHV6.js.map +1 -0
- package/dist/chunk-XRVNI2GB.js +896 -0
- package/dist/chunk-XRVNI2GB.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1166 -265
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +324 -38
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-CnpD6BcX.d.ts → schema-CeXWlYcG.d.ts} +441 -12
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/drizzle/0122_codex_capacity_same_turn.sql +59 -0
- package/drizzle/0123_session_tool_policy_version.sql +14 -0
- package/drizzle/0124_session_event_duplicate_lookup.sql +4 -0
- package/drizzle/0125_document_drops_visibility.sql +29 -0
- package/drizzle/0126_document_access_constraints.sql +138 -0
- package/drizzle/0127_document_default_base_index.sql +5 -0
- package/drizzle/0128_github_installation_authority.sql +69 -0
- package/drizzle/0129_retained_process_reconciliation.sql +356 -0
- package/package.json +4 -3
- package/src/event-payload-sanitizer.ts +167 -55
- package/src/index.ts +1494 -258
- package/src/new-session-drafts.ts +127 -6
- package/src/provision-roles.ts +184 -2
- package/src/runtime-posture-cli.ts +57 -0
- package/src/runtime-posture.ts +760 -0
- package/src/schema.ts +156 -3
- package/src/session-control.ts +1 -0
- package/dist/chunk-BMFDXFPA.js +0 -155
- package/dist/chunk-BMFDXFPA.js.map +0 -1
- package/dist/chunk-VUKRIBO5.js.map +0 -1
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
|
-
'
|
|
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")
|
|
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")
|
|
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
|
|
package/src/session-control.ts
CHANGED
package/dist/chunk-BMFDXFPA.js
DELETED
|
@@ -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":[]}
|