@memberjunction/server 5.51.3 → 5.51.4
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/README.md +34 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +17 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +136 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +1 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3262 -3273
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +124 -4
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +170 -15
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +148 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +287 -30
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -48
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +2 -2
- package/dist/realtimeWidget/widgetGuestElevation.js +2 -2
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +39 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +97 -157
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +14 -4
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +18 -6
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +13 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +26 -14
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +63 -46
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +24 -27
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +29 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +90 -89
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +136 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +318 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +66 -27
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +134 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +221 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +173 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +116 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +351 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +164 -0
- package/src/__tests__/SessionManager.test.ts +88 -13
- package/src/__tests__/SessionRunIDVerifier.test.ts +255 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +155 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +241 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +180 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/resolverBase.rls.test.ts +2 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +24 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +179 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +34 -1
- package/src/context.ts +25 -34
- package/src/generated/generated.ts +3262 -3273
- package/src/generic/CacheInvalidationResolver.ts +206 -15
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +339 -31
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +63 -50
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +2 -2
- package/src/resolvers/ActionResolver.ts +85 -22
- package/src/resolvers/AdhocQueryResolver.ts +110 -177
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +24 -7
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +82 -50
- package/src/resolvers/RemoteBrowserActionResolver.ts +30 -26
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/__tests__/ReportResolver.test.ts +2 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskOrchestrator.ts +17 -17
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -42
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -50
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Checks the run ids a realtime session keeps in its `Config` before server code
|
|
3
|
+
* acts on them.
|
|
4
|
+
*
|
|
5
|
+
* @module @memberjunction/server
|
|
6
|
+
*/
|
|
7
|
+
import { IMetadataProvider, LogError, RunView, RunViewParams, UserInfo } from '@memberjunction/core';
|
|
8
|
+
import { IsValidUUID, UUIDsEqual } from '@memberjunction/global';
|
|
9
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
10
|
+
|
|
11
|
+
const AGENT_RUN_ENTITY = 'MJ: AI Agent Runs';
|
|
12
|
+
const AGENT_RUN_STEP_ENTITY = 'MJ: AI Agent Run Steps';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escapes a value for a single-quoted SQL literal: drops null bytes and doubles quotes. A copy of
|
|
16
|
+
* `EscapeSQLString` from `@memberjunction/global` on next, which lts/5 does not have.
|
|
17
|
+
*/
|
|
18
|
+
function EscapeSQLString(value: string): string {
|
|
19
|
+
return value.replace(/\0/g, '').replace(/'/g, "''");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The run ids a realtime voice session keeps in its `Config` JSON. Server code reads and writes the
|
|
24
|
+
* records they point at, often as an elevated user.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionRunIDs {
|
|
27
|
+
/** The co-agent observability run (`MJ: AI Agent Runs`). */
|
|
28
|
+
CoAgentRunID?: string;
|
|
29
|
+
/** The co-agent prompt run (`MJ: AI Prompt Runs`), linked through the co-agent run's prompt step. */
|
|
30
|
+
PromptRunID?: string;
|
|
31
|
+
/** The co-agent run's system-prompt step (`MJ: AI Agent Run Steps`). */
|
|
32
|
+
CoAgentRunStepID?: string;
|
|
33
|
+
/** A delegated run (`MJ: AI Agent Runs`) that paused for the user's answer. */
|
|
34
|
+
PendingFeedbackRunID?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A row of the run-step lookup. */
|
|
38
|
+
interface RunStepRow {
|
|
39
|
+
ID: string;
|
|
40
|
+
StepType: string;
|
|
41
|
+
TargetLogID: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A row of either lookup; the run lookup returns only `ID`. */
|
|
45
|
+
type LookupRow = Partial<RunStepRow> & { ID: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keeps the run ids from a session's `Config` that are records of that session.
|
|
49
|
+
*
|
|
50
|
+
* The session owner can edit `Config`, so its run ids are claims. A run id is kept when it is the
|
|
51
|
+
* UUID of an `MJ: AI Agent Runs` row whose `AgentSessionID` is the session. The step id and the
|
|
52
|
+
* prompt run id are kept when they belong to that co-agent run: the step's `AgentRunID` is the
|
|
53
|
+
* co-agent run, and the prompt run is the `TargetLogID` of one of its `Prompt` steps.
|
|
54
|
+
*
|
|
55
|
+
* The lookups run as the system user when one is cached, else as the acting user. They are pinned
|
|
56
|
+
* to the session's id and return nothing to the caller, and row-level security can hide a session's
|
|
57
|
+
* own runs from its owner (a web-widget guest's delegated runs carry no `ConversationID`, which the
|
|
58
|
+
* guest's run filter keys on).
|
|
59
|
+
*
|
|
60
|
+
* Each call reads the database again (`BypassCache`) and keeps no result, because the links can
|
|
61
|
+
* change after a lookup: a run's `AgentSessionID` and a step's `AgentRunID` and `TargetLogID` are
|
|
62
|
+
* updatable.
|
|
63
|
+
*/
|
|
64
|
+
export class SessionRunIDVerifier {
|
|
65
|
+
/**
|
|
66
|
+
* Returns the subset of `claimed` that belongs to the session now. Never throws: when a lookup
|
|
67
|
+
* fails, nothing is kept.
|
|
68
|
+
*
|
|
69
|
+
* @param sessionID The `MJ: AI Agent Sessions` id the claims were read from. The caller must have
|
|
70
|
+
* checked that the session is the caller's own.
|
|
71
|
+
* @param claimed The run ids read from the session's `Config`.
|
|
72
|
+
* @param contextUser The acting user; the lookups run as it only when no system user is cached.
|
|
73
|
+
* @param provider The request-scoped metadata provider.
|
|
74
|
+
* @returns The claims that are records of the session.
|
|
75
|
+
*/
|
|
76
|
+
public async Verify(
|
|
77
|
+
sessionID: string,
|
|
78
|
+
claimed: SessionRunIDs,
|
|
79
|
+
contextUser: UserInfo,
|
|
80
|
+
provider: IMetadataProvider,
|
|
81
|
+
): Promise<SessionRunIDs> {
|
|
82
|
+
const candidates = this.keepUUIDs(claimed);
|
|
83
|
+
if (!candidates.CoAgentRunID && !candidates.PendingFeedbackRunID) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
return (await this.lookUpOwned(sessionID.trim(), candidates, contextUser, provider)) ?? {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Drops every claim that is not a UUID. The prompt run and step need a co-agent run to hang off. */
|
|
90
|
+
private keepUUIDs(claimed: SessionRunIDs): SessionRunIDs {
|
|
91
|
+
const coAgentRunID = this.uuidOrUndefined(claimed.CoAgentRunID);
|
|
92
|
+
return {
|
|
93
|
+
CoAgentRunID: coAgentRunID,
|
|
94
|
+
PromptRunID: coAgentRunID ? this.uuidOrUndefined(claimed.PromptRunID) : undefined,
|
|
95
|
+
CoAgentRunStepID: coAgentRunID ? this.uuidOrUndefined(claimed.CoAgentRunStepID) : undefined,
|
|
96
|
+
PendingFeedbackRunID: this.uuidOrUndefined(claimed.PendingFeedbackRunID),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private uuidOrUndefined(value: string | undefined): string | undefined {
|
|
101
|
+
return value && IsValidUUID(value) ? value.trim() : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Runs the lookups; `null` when one of them fails. */
|
|
105
|
+
private async lookUpOwned(
|
|
106
|
+
sessionID: string,
|
|
107
|
+
candidates: SessionRunIDs,
|
|
108
|
+
contextUser: UserInfo,
|
|
109
|
+
provider: IMetadataProvider,
|
|
110
|
+
): Promise<SessionRunIDs | null> {
|
|
111
|
+
const stepsQuery = this.stepsQuery(candidates);
|
|
112
|
+
const queries = stepsQuery ? [this.runsQuery(sessionID, candidates), stepsQuery] : [this.runsQuery(sessionID, candidates)];
|
|
113
|
+
try {
|
|
114
|
+
const lookupUser = UserCache.Instance.GetSystemUser() ?? contextUser;
|
|
115
|
+
const [runs, steps] = await RunView.FromMetadataProvider(provider).RunViews<LookupRow>(queries, lookupUser);
|
|
116
|
+
const failed = [runs, steps].find((result) => result && !result.Success);
|
|
117
|
+
if (!runs || failed) {
|
|
118
|
+
LogError(`SessionRunIDVerifier: run lookup failed for session ${sessionID}: ${failed?.ErrorMessage ?? 'no result'}`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return this.pickOwned(candidates, runs.Results, steps?.Results ?? []);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
LogError(`SessionRunIDVerifier: run lookup failed for session ${sessionID}: ${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The claimed runs that carry this session's id. */
|
|
129
|
+
private runsQuery(sessionID: string, candidates: SessionRunIDs): RunViewParams {
|
|
130
|
+
const ids = [candidates.CoAgentRunID, candidates.PendingFeedbackRunID]
|
|
131
|
+
.filter((id): id is string => !!id)
|
|
132
|
+
.map((id) => `'${EscapeSQLString(id)}'`);
|
|
133
|
+
return {
|
|
134
|
+
EntityName: AGENT_RUN_ENTITY,
|
|
135
|
+
ExtraFilter: `AgentSessionID='${EscapeSQLString(sessionID)}' AND ID IN (${ids.join(', ')})`,
|
|
136
|
+
Fields: ['ID'],
|
|
137
|
+
ResultType: 'simple',
|
|
138
|
+
BypassCache: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The claimed co-agent run's steps that match the claimed step or prompt run; `null` when neither is claimed. */
|
|
143
|
+
private stepsQuery(candidates: SessionRunIDs): RunViewParams | null {
|
|
144
|
+
const matches: string[] = [];
|
|
145
|
+
if (candidates.CoAgentRunStepID) {
|
|
146
|
+
matches.push(`ID='${EscapeSQLString(candidates.CoAgentRunStepID)}'`);
|
|
147
|
+
}
|
|
148
|
+
if (candidates.PromptRunID) {
|
|
149
|
+
matches.push(`TargetLogID='${EscapeSQLString(candidates.PromptRunID)}'`);
|
|
150
|
+
}
|
|
151
|
+
if (!candidates.CoAgentRunID || matches.length === 0) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
EntityName: AGENT_RUN_STEP_ENTITY,
|
|
156
|
+
ExtraFilter: `AgentRunID='${EscapeSQLString(candidates.CoAgentRunID)}' AND (${matches.join(' OR ')})`,
|
|
157
|
+
Fields: ['ID', 'StepType', 'TargetLogID'],
|
|
158
|
+
ResultType: 'simple',
|
|
159
|
+
BypassCache: true,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private pickOwned(candidates: SessionRunIDs, runs: LookupRow[], steps: LookupRow[]): SessionRunIDs {
|
|
164
|
+
const isOwnedRun = (id: string | undefined): boolean => !!id && runs.some((row) => UUIDsEqual(row.ID, id));
|
|
165
|
+
const coAgentRunID = isOwnedRun(candidates.CoAgentRunID) ? candidates.CoAgentRunID : undefined;
|
|
166
|
+
const ownedSteps = coAgentRunID ? steps : [];
|
|
167
|
+
const stepID = candidates.CoAgentRunStepID;
|
|
168
|
+
const promptRunID = candidates.PromptRunID;
|
|
169
|
+
return {
|
|
170
|
+
CoAgentRunID: coAgentRunID,
|
|
171
|
+
CoAgentRunStepID: stepID && ownedSteps.some((row) => UUIDsEqual(row.ID, stepID)) ? stepID : undefined,
|
|
172
|
+
PromptRunID:
|
|
173
|
+
promptRunID && ownedSteps.some((row) => row.StepType === 'Prompt' && UUIDsEqual(row.TargetLogID, promptRunID))
|
|
174
|
+
? promptRunID
|
|
175
|
+
: undefined,
|
|
176
|
+
PendingFeedbackRunID: isOwnedRun(candidates.PendingFeedbackRunID) ? candidates.PendingFeedbackRunID : undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AuthorizationInfo, AuthorizationRoleInfo, AuthorizationRoleType, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { NormalizeUUID, UUIDsEqual } from '@memberjunction/global';
|
|
3
|
+
|
|
4
|
+
/** Authorizations keyed by their normalized ID. */
|
|
5
|
+
type AuthorizationsByID = Map<string, AuthorizationInfo>;
|
|
6
|
+
|
|
7
|
+
/** The outcome of {@link EvaluateActionRunAuthorization}. */
|
|
8
|
+
export interface ActionRunAuthorization {
|
|
9
|
+
/** True when the user may run the action. */
|
|
10
|
+
Allowed: boolean;
|
|
11
|
+
/** Why the run is allowed or refused. */
|
|
12
|
+
Reason: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One provider's authorization tree and the role grants on it. */
|
|
16
|
+
export type AuthorizationMetadata = Pick<IMetadataProvider, 'Authorizations' | 'AuthorizationRoles'>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when the user is an Owner, MJ's superuser type. The column is nchar, so the stored value is
|
|
20
|
+
* padded.
|
|
21
|
+
*/
|
|
22
|
+
export function IsOwner(user: UserInfo): boolean {
|
|
23
|
+
return user.Type?.trim().toLowerCase() === 'owner';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decides whether a user may run an action through a public entry point such as the `RunAction`
|
|
28
|
+
* mutation. It fails closed:
|
|
29
|
+
*
|
|
30
|
+
* - An Owner may run any action.
|
|
31
|
+
* - An action with `MJ: Action Authorizations` rows runs for a user who holds at least one of the
|
|
32
|
+
* linked authorizations. A linked authorization counts only when it is active and none of the
|
|
33
|
+
* user's roles is denied it; the user may then hold it directly or through an ancestor.
|
|
34
|
+
* - An action with no such rows runs for any user when its class authorizes its caller itself
|
|
35
|
+
* (`BaseAction.AuthorizesCaller`), so the action's own check decides. Otherwise it runs for
|
|
36
|
+
* Owners only.
|
|
37
|
+
*
|
|
38
|
+
* The authorizations, their parents and their role grants are read only from `metadata`, never
|
|
39
|
+
* from the global provider. Pass the provider the `MJ: Action Authorizations` rows were read from.
|
|
40
|
+
*
|
|
41
|
+
* Refuse scope-limited sessions before calling this; see `IsScopeLimitedPrincipal`.
|
|
42
|
+
*
|
|
43
|
+
* @param user the caller
|
|
44
|
+
* @param actionAuthorizationIDs the AuthorizationID of each `MJ: Action Authorizations` row for the action
|
|
45
|
+
* @param metadata the provider whose authorizations and role grants decide
|
|
46
|
+
* @param actionAuthorizesCaller true when the action's class authorizes its caller itself
|
|
47
|
+
* @returns whether the run is allowed, and why
|
|
48
|
+
*/
|
|
49
|
+
export function EvaluateActionRunAuthorization(
|
|
50
|
+
user: UserInfo,
|
|
51
|
+
actionAuthorizationIDs: readonly string[],
|
|
52
|
+
metadata: AuthorizationMetadata,
|
|
53
|
+
actionAuthorizesCaller: boolean
|
|
54
|
+
): ActionRunAuthorization {
|
|
55
|
+
if (IsOwner(user)) {
|
|
56
|
+
return { Allowed: true, Reason: 'The user is an Owner.' };
|
|
57
|
+
}
|
|
58
|
+
if (actionAuthorizationIDs.length === 0) {
|
|
59
|
+
return actionAuthorizesCaller
|
|
60
|
+
? { Allowed: true, Reason: 'The action authorizes its caller itself.' }
|
|
61
|
+
: { Allowed: false, Reason: 'No Action Authorization grants this action to any role, and the action does not authorize its caller itself; only an Owner may run it.' };
|
|
62
|
+
}
|
|
63
|
+
const authorizationsByID: AuthorizationsByID = new Map(metadata.Authorizations.map((auth) => [NormalizeUUID(auth.ID), auth]));
|
|
64
|
+
const userGrants = grantsToUserRoles(user, metadata.AuthorizationRoles);
|
|
65
|
+
const granted = actionAuthorizationIDs
|
|
66
|
+
.map((id) => authorizationsByID.get(NormalizeUUID(id)))
|
|
67
|
+
.filter((auth): auth is AuthorizationInfo => auth !== undefined)
|
|
68
|
+
.find((auth) => holdsLinkedAuthorization(auth, authorizationsByID, userGrants));
|
|
69
|
+
return granted
|
|
70
|
+
? { Allowed: true, Reason: `The user holds the "${granted.Name}" authorization.` }
|
|
71
|
+
: { Allowed: false, Reason: 'None of your roles holds an authorization this action requires.' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The role grants that name one of the user's roles. */
|
|
75
|
+
function grantsToUserRoles(user: UserInfo, grants: readonly AuthorizationRoleInfo[]): AuthorizationRoleInfo[] {
|
|
76
|
+
const userRoleIDs = new Set((user.UserRoles ?? []).map((userRole) => NormalizeUUID(userRole.RoleID)));
|
|
77
|
+
return grants.filter((grant) => userRoleIDs.has(NormalizeUUID(grant.RoleID)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True when the user holds a linked authorization: it is active, none of the user's roles is denied
|
|
82
|
+
* it, and the user holds it directly or through an ancestor. The first two are checked on the linked
|
|
83
|
+
* authorization itself, because the ancestor walk would otherwise grant it through the parent.
|
|
84
|
+
*/
|
|
85
|
+
function holdsLinkedAuthorization(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
86
|
+
if (!auth.IsActive || grantTypes(auth, userGrants).includes(AuthorizationRoleType.Deny)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return holdsDirectlyOrThroughAncestor(auth, authorizationsByID, userGrants);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* True when the user holds the authorization or one of its ancestors, following `ParentID` within
|
|
94
|
+
* `authorizationsByID`. This is the walk `AuthorizationEvaluator.UserCanExecuteWithAncestors` does,
|
|
95
|
+
* with the role grants taken from the same provider as the authorizations.
|
|
96
|
+
*/
|
|
97
|
+
function holdsDirectlyOrThroughAncestor(auth: AuthorizationInfo, authorizationsByID: AuthorizationsByID, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
98
|
+
const visited = new Set<string>();
|
|
99
|
+
let node: AuthorizationInfo | undefined = auth;
|
|
100
|
+
while (node && !visited.has(NormalizeUUID(node.ID))) {
|
|
101
|
+
if (holdsDirectly(node, userGrants)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
visited.add(NormalizeUUID(node.ID));
|
|
105
|
+
node = node.ParentID ? authorizationsByID.get(NormalizeUUID(node.ParentID)) : undefined;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True when the authorization is active and the user's roles have an Allow and no Deny on it. */
|
|
111
|
+
function holdsDirectly(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): boolean {
|
|
112
|
+
const types = grantTypes(auth, userGrants);
|
|
113
|
+
return auth.IsActive && types.includes(AuthorizationRoleType.Allow) && !types.includes(AuthorizationRoleType.Deny);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Allow or Deny, for each of the user's role grants on the authorization. */
|
|
117
|
+
function grantTypes(auth: AuthorizationInfo, userGrants: readonly AuthorizationRoleInfo[]): AuthorizationRoleType[] {
|
|
118
|
+
return userGrants.filter((grant) => UUIDsEqual(grant.AuthorizationID, auth.ID)).map((grant) => grant.AuthorizationType());
|
|
119
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
|
10
10
|
import { initializeAuthProviders } from './initializeProviders.js';
|
|
11
11
|
|
|
12
12
|
export * from './APIKeyScopeAuth.js';
|
|
13
|
+
export * from './scopeLimitedPrincipal.js';
|
|
13
14
|
|
|
14
15
|
// This is a hard-coded forever constant due to internal migrations
|
|
15
16
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { UserInfo } from '@memberjunction/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when a session's read authority is deliberately narrower than the role grant it
|
|
5
|
+
* authenticates with — i.e. the principal is confined by per-session scope rather than by
|
|
6
|
+
* its roles alone.
|
|
7
|
+
*
|
|
8
|
+
* Two such principals exist today, both from the magic-link surface:
|
|
9
|
+
*
|
|
10
|
+
* - **Anonymous magic-link guests** (`IsMagicLinkAnonymous`) authenticate as the *shared*
|
|
11
|
+
* Anonymous principal, so their role grant is common to every guest. What separates one
|
|
12
|
+
* guest from another is not the role — it is the per-session scope.
|
|
13
|
+
* - **Resource-scoped magic-link sessions** (`MagicLinkScope`) hold a normal role, pinned to
|
|
14
|
+
* one shared resource (and its FK-reachable dependents) for the life of the session.
|
|
15
|
+
*
|
|
16
|
+
* In both cases the confinement is expressed as row-level-security filter tokens
|
|
17
|
+
* (`{{ScopeResourceID}}` / `{{ScopeResourceType}}`) that the entity-read path substitutes.
|
|
18
|
+
* That means the confinement only exists on code paths that go **through** entity
|
|
19
|
+
* permissions and RLS — `RunView`, `BaseEntity`, the generated resolvers. Any path that
|
|
20
|
+
* reaches the database *around* those layers (raw SQL on the read-only pool, for instance)
|
|
21
|
+
* sees no scope at all, so such a path must refuse these principals outright rather than
|
|
22
|
+
* assume a filter it never applies.
|
|
23
|
+
*
|
|
24
|
+
* Callers should treat a `true` result as "not permitted here", never as "apply a narrower
|
|
25
|
+
* filter" — a caller that could narrow correctly would not need this predicate.
|
|
26
|
+
*
|
|
27
|
+
* @param user the principal to classify. A missing principal returns `true`: an
|
|
28
|
+
* unidentifiable session is the one case where the scope certainly cannot be
|
|
29
|
+
* established, so it fails closed.
|
|
30
|
+
* @returns `true` when the principal is scope-limited (or unidentifiable), else `false`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* if (IsScopeLimitedPrincipal(context.userPayload?.userRecord)) {
|
|
35
|
+
* return this.buildErrorResult('Not permitted for scope-limited sessions.');
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function IsScopeLimitedPrincipal(user: UserInfo | undefined | null): boolean {
|
|
40
|
+
if (!user) {
|
|
41
|
+
return true; // no identifiable principal — fail closed
|
|
42
|
+
}
|
|
43
|
+
if (user.IsMagicLinkAnonymous) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const scope = user.MagicLinkScope;
|
|
47
|
+
// An empty scope object carries no confinement; only a populated one does. (Truthiness
|
|
48
|
+
// of the object alone would classify `{}` as scoped and lock out a session that is not.)
|
|
49
|
+
return !!(scope?.ResourceID || scope?.ResourceType);
|
|
50
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -198,6 +198,29 @@ const cacheSettingsSchema = z.object({
|
|
|
198
198
|
evictionSweepIntervalSeconds: z.number().optional().default(300),
|
|
199
199
|
/** Enable verbose cache logging (hits, misses, evictions). Default: false. */
|
|
200
200
|
verboseLogging: z.boolean().optional().default(false),
|
|
201
|
+
/**
|
|
202
|
+
* Entity names whose FULL ROW may ride along with a cache-invalidation broadcast.
|
|
203
|
+
*
|
|
204
|
+
* The cache-invalidation subscription is delivered to EVERY connected client with no per-user
|
|
205
|
+
* filter (see CacheInvalidationResolver), so any row named here is disclosed to every signed-in
|
|
206
|
+
* session, whatever row-level security or tenant scoping would otherwise apply to reading it.
|
|
207
|
+
*
|
|
208
|
+
* Defaults to `[]`: invalidation still carries the entity name and primary key, which is all a
|
|
209
|
+
* client needs to evict, and the client re-fetches through the normal read path where access
|
|
210
|
+
* control applies. Listing an entity re-enables the apply-in-place optimisation for it — correct
|
|
211
|
+
* only for reference data every signed-in user is allowed to read.
|
|
212
|
+
*
|
|
213
|
+
* What the re-fetch costs depends on the consumer. `ConversationEngine` re-reads the ONE record
|
|
214
|
+
* by primary key. `BaseEngine` (every engine subclass with `AutoRefresh`, the default) applies a
|
|
215
|
+
* remote save in place only when the row is present, so without it a remote save falls through to
|
|
216
|
+
* a full reload of each matching config — a `RunView` of that entity, not a keyed read. Remote
|
|
217
|
+
* deletes still apply in place from the primary key. Engine-cached reference entities that every
|
|
218
|
+
* signed-in user may read are the ones worth listing here.
|
|
219
|
+
*
|
|
220
|
+
* `['*']` opts every entity in, restoring the previous behaviour. Only safe on a deployment where
|
|
221
|
+
* every signed-in user may read every row of every entity.
|
|
222
|
+
*/
|
|
223
|
+
recordDataBroadcastEntities: z.array(z.string()).optional().default([]),
|
|
201
224
|
});
|
|
202
225
|
|
|
203
226
|
const loggingSettingsSchema = z.object({
|
|
@@ -623,7 +646,17 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
623
646
|
autoCreateNewUsers: true,
|
|
624
647
|
newUserLimitedToAuthorizedDomains: false,
|
|
625
648
|
newUserAuthorizedDomains: [],
|
|
626
|
-
|
|
649
|
+
// 'UI' ONLY, deliberately (issue #4260). Auto-provisioning is on by default above, with no
|
|
650
|
+
// domain restriction, so this list is the standing authority of anyone the configured IdP will
|
|
651
|
+
// issue a token for. On the baseline seed 'Developer' and 'Integration' hold unfiltered
|
|
652
|
+
// CanUpdate on ~439 of the database's ~446 entities, so defaulting every such identity into
|
|
653
|
+
// either grants broad data-plane access no host should hand out by default. (The MJ: Users
|
|
654
|
+
// escalation this list also used to guard against — writing your own Type to 'Owner' — is now
|
|
655
|
+
// closed at the entity layer regardless of role: see MJUserEntityServer in
|
|
656
|
+
// @memberjunction/core-entities-server.) 'UI' carries the end-user surface (conversations,
|
|
657
|
+
// views, dashboards, settings) and no write on MJ: Users. Hosts that need more grant it
|
|
658
|
+
// per-deployment.
|
|
659
|
+
newUserRoles: ['UI'],
|
|
627
660
|
updateCacheWhenNotFound: true,
|
|
628
661
|
updateCacheWhenNotFoundDelay: 5000,
|
|
629
662
|
contextUserForNewUserCreation: 'not.set@nowhere.com',
|
package/src/context.ts
CHANGED
|
@@ -8,7 +8,8 @@ import sql from 'mssql';
|
|
|
8
8
|
import { getSigningKeys, getSystemUser, getValidationOptions, verifyUserRecord, extractUserInfoFromPayload } from './auth/index.js';
|
|
9
9
|
import { TokenExpiredError, AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
10
10
|
import { authCache } from './cache.js';
|
|
11
|
-
import { userEmailMap, apiKey, mj_core_schema } from './config.js';
|
|
11
|
+
import { userEmailMap, apiKey, mj_core_schema, configInfo } from './config.js';
|
|
12
|
+
import { BuildPostgreSQLConnectionConfig, PostgreSQLReadOnlyPool, ResolvePostgreSQLEndpoint, ResolvePostgreSQLReadOnlyCredentials } from './postgresqlPoolSettings.js';
|
|
12
13
|
import { buildBoundaryLogPayload } from './logging/boundaryLogPayload.js';
|
|
13
14
|
import { StartupLogger } from './logging/StartupLogger.js';
|
|
14
15
|
import { DataSourceInfo, UserPayload } from './types.js';
|
|
@@ -748,36 +749,35 @@ async function createPerRequestProviders(
|
|
|
748
749
|
{ provider: p, type: 'Read-Write' }
|
|
749
750
|
];
|
|
750
751
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
providers.push({ provider: rp, type: 'Read-Only' });
|
|
755
|
-
}
|
|
756
|
-
} else {
|
|
757
|
-
const rp = await tryCreateReadOnlyProvider(dataSources);
|
|
758
|
-
if (rp) {
|
|
759
|
-
providers.push({ provider: rp, type: 'Read-Only' });
|
|
760
|
-
}
|
|
752
|
+
const rp = await CreateReadOnlyProvider(dataSources);
|
|
753
|
+
if (rp) {
|
|
754
|
+
providers.push({ provider: rp, type: 'Read-Only' });
|
|
761
755
|
}
|
|
762
756
|
|
|
763
757
|
return providers;
|
|
764
758
|
}
|
|
765
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Opens a provider on the read-only database login, for the current platform. Resolves null when no
|
|
762
|
+
* read-only login is configured; it never falls back to the read-write pool.
|
|
763
|
+
* @param dataSources the data sources MJAPI opened at startup
|
|
764
|
+
*/
|
|
765
|
+
export async function CreateReadOnlyProvider(dataSources: DataSourceInfo[]): Promise<DatabaseProviderBase | null> {
|
|
766
|
+
return resolveDbPlatformFromEnv() === 'postgresql'
|
|
767
|
+
? tryCreateReadOnlyPostgresProvider()
|
|
768
|
+
: tryCreateReadOnlyProvider(dataSources);
|
|
769
|
+
}
|
|
770
|
+
|
|
766
771
|
/**
|
|
767
772
|
* Creates a PostgreSQL per-request provider, sharing the connection pool
|
|
768
773
|
* from the primary provider to avoid pool exhaustion.
|
|
769
774
|
*/
|
|
770
775
|
async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
771
776
|
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
772
|
-
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
773
|
-
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
774
|
-
const pgUser = process.env.PG_USERNAME || process.env.DB_USERNAME || 'postgres';
|
|
775
|
-
const pgPass = process.env.PG_PASSWORD || process.env.DB_PASSWORD || '';
|
|
776
|
-
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
777
777
|
|
|
778
778
|
const pgProvider = new PostgreSQLDataProvider();
|
|
779
779
|
const pgConfig = new PostgreSQLProviderConfigData(
|
|
780
|
-
|
|
780
|
+
BuildPostgreSQLConnectionConfig(ResolvePostgreSQLEndpoint(), configInfo.databaseSettings, 'api'),
|
|
781
781
|
mj_core_schema,
|
|
782
782
|
0,
|
|
783
783
|
undefined,
|
|
@@ -797,39 +797,30 @@ async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
800
|
+
* Creates a read-only PostgreSQL per-request provider on the read-only pool MJAPI opened at
|
|
801
|
+
* startup with the read-only login. Returns null when no read-only login is configured, so the
|
|
802
|
+
* caller never falls back to the read-write pool.
|
|
802
803
|
*/
|
|
803
804
|
async function tryCreateReadOnlyPostgresProvider(): Promise<DatabaseProviderBase | null> {
|
|
804
|
-
const
|
|
805
|
-
const
|
|
806
|
-
if (!
|
|
805
|
+
const readOnlyPool = PostgreSQLReadOnlyPool.Instance.Pool;
|
|
806
|
+
const credentials = ResolvePostgreSQLReadOnlyCredentials(configInfo);
|
|
807
|
+
if (!readOnlyPool || !credentials) {
|
|
807
808
|
return null;
|
|
808
809
|
}
|
|
809
810
|
|
|
810
811
|
try {
|
|
811
812
|
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
812
|
-
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
813
|
-
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
814
|
-
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
815
813
|
|
|
816
814
|
const roProvider = new PostgreSQLDataProvider();
|
|
817
815
|
const roConfig = new PostgreSQLProviderConfigData(
|
|
818
|
-
{
|
|
816
|
+
BuildPostgreSQLConnectionConfig({ ...ResolvePostgreSQLEndpoint(), ...credentials }, configInfo.databaseSettings, 'read-only'),
|
|
819
817
|
mj_core_schema,
|
|
820
818
|
0,
|
|
821
819
|
undefined,
|
|
822
820
|
undefined,
|
|
823
821
|
false,
|
|
824
822
|
);
|
|
825
|
-
|
|
826
|
-
const primaryProvider = Metadata.Provider as unknown as { DatabaseConnection?: import('pg').Pool }; // global-provider-ok: bootstrap (share primary provider's PG pool with the read-only provider)
|
|
827
|
-
if (primaryProvider?.DatabaseConnection) {
|
|
828
|
-
await roProvider.ConfigWithSharedPool(roConfig, primaryProvider.DatabaseConnection);
|
|
829
|
-
} else {
|
|
830
|
-
await roProvider.Config(roConfig);
|
|
831
|
-
}
|
|
832
|
-
|
|
823
|
+
await roProvider.ConfigWithSharedPool(roConfig, readOnlyPool);
|
|
833
824
|
return roProvider;
|
|
834
825
|
} catch (_err) {
|
|
835
826
|
return null;
|