@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3
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 +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +24441 -14433
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +16 -2
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +38 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +84 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +56 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +191 -74
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -50
- package/dist/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/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +23 -153
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.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/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -92
- 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 +3 -3
- package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +24183 -17182
- package/src/generic/FireAndForgetHeartbeat.ts +16 -2
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +97 -1
- package/src/generic/ResolverBase.ts +220 -76
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +228 -51
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
- package/src/resolvers/RunAIAgentResolver.ts +28 -174
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- 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 -67
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL pools read the same databaseSettings values as SQL Server, with the same defaults.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
BuildPostgreSQLConnectionConfig,
|
|
7
|
+
CODEGEN_STATEMENT_TIMEOUT_MS,
|
|
8
|
+
ResolvePostgreSQLEndpoint,
|
|
9
|
+
ResolvePostgreSQLReadOnlyCredentials,
|
|
10
|
+
ToPGPoolConfig
|
|
11
|
+
} from '../postgresqlPoolSettings.js';
|
|
12
|
+
import type { DatabaseSettingsInfo } from '../config.js';
|
|
13
|
+
import { DescribeReadOnlyLoginOverreach } from '../postgresqlPoolSettings.js';
|
|
14
|
+
import type { ReadOnlyLoginCheckClient, ReadOnlyLoginGrants } from '../postgresqlPoolSettings.js';
|
|
15
|
+
|
|
16
|
+
const defaults: DatabaseSettingsInfo = {
|
|
17
|
+
connectionTimeout: 45000,
|
|
18
|
+
requestTimeout: 30000,
|
|
19
|
+
metadataCacheRefreshInterval: 180000,
|
|
20
|
+
connectionPool: { max: 50, min: 5, idleTimeoutMillis: 30000, acquireTimeoutMillis: 30000 }
|
|
21
|
+
};
|
|
22
|
+
const endpoint = { Host: 'db', Port: 5432, Database: 'mj', User: 'api', Password: 'secret' };
|
|
23
|
+
|
|
24
|
+
describe('BuildPostgreSQLConnectionConfig', () => {
|
|
25
|
+
it('gives API pools requestTimeout as the statement timeout and the pool settings SQL Server uses', () => {
|
|
26
|
+
const config = BuildPostgreSQLConnectionConfig(endpoint, defaults, 'api');
|
|
27
|
+
expect(config).toMatchObject({
|
|
28
|
+
...endpoint,
|
|
29
|
+
MaxConnections: 50,
|
|
30
|
+
MinConnections: 5,
|
|
31
|
+
IdleTimeoutMillis: 30000,
|
|
32
|
+
ConnectionTimeoutMillis: 30000,
|
|
33
|
+
StatementTimeoutMs: 30000,
|
|
34
|
+
IdleInTransactionSessionTimeoutMs: 60000
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('follows a configured requestTimeout and pool', () => {
|
|
39
|
+
const config = BuildPostgreSQLConnectionConfig(endpoint, {
|
|
40
|
+
...defaults,
|
|
41
|
+
requestTimeout: 5000,
|
|
42
|
+
connectionPool: { max: 12, min: 1, idleTimeoutMillis: 7000, acquireTimeoutMillis: 9000 }
|
|
43
|
+
}, 'api');
|
|
44
|
+
expect(config).toMatchObject({ MaxConnections: 12, MinConnections: 1, IdleTimeoutMillis: 7000, ConnectionTimeoutMillis: 9000, StatementTimeoutMs: 5000, IdleInTransactionSessionTimeoutMs: 10000 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('sets no timeouts when requestTimeout is 0', () => {
|
|
48
|
+
const config = BuildPostgreSQLConnectionConfig(endpoint, { ...defaults, requestTimeout: 0 }, 'api');
|
|
49
|
+
expect(config.StatementTimeoutMs).toBe(0);
|
|
50
|
+
expect(config.IdleInTransactionSessionTimeoutMs).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps the read-only pool small but gives it the API timeouts', () => {
|
|
54
|
+
const config = BuildPostgreSQLConnectionConfig(endpoint, defaults, 'read-only');
|
|
55
|
+
expect(config).toMatchObject({ MaxConnections: 10, MinConnections: 0, StatementTimeoutMs: 30000, IdleInTransactionSessionTimeoutMs: 60000 });
|
|
56
|
+
expect(BuildPostgreSQLConnectionConfig(endpoint, { ...defaults, connectionPool: { max: 4, min: 1, idleTimeoutMillis: 1, acquireTimeoutMillis: 1 } }, 'read-only').MaxConnections).toBe(4);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('gives the CodeGen pool the long timeout the SQL Server CodeGen pool has, and no idle-in-transaction limit', () => {
|
|
60
|
+
const config = BuildPostgreSQLConnectionConfig(endpoint, defaults, 'codegen');
|
|
61
|
+
expect(config.StatementTimeoutMs).toBe(CODEGEN_STATEMENT_TIMEOUT_MS);
|
|
62
|
+
expect(CODEGEN_STATEMENT_TIMEOUT_MS).toBe(600000);
|
|
63
|
+
expect(config.IdleInTransactionSessionTimeoutMs).toBe(0);
|
|
64
|
+
expect(config.MaxConnections).toBe(10);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('ToPGPoolConfig', () => {
|
|
69
|
+
it('maps the connection config onto pg.Pool option names', () => {
|
|
70
|
+
const pool = ToPGPoolConfig(BuildPostgreSQLConnectionConfig(endpoint, defaults, 'api'));
|
|
71
|
+
expect(pool).toEqual({
|
|
72
|
+
host: 'db', port: 5432, database: 'mj', user: 'api', password: 'secret',
|
|
73
|
+
max: 50, min: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 30000,
|
|
74
|
+
statement_timeout: 30000, idle_in_transaction_session_timeout: 60000
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('leaves the timeouts out when they are 0', () => {
|
|
79
|
+
const pool = ToPGPoolConfig(BuildPostgreSQLConnectionConfig(endpoint, { ...defaults, requestTimeout: 0 }, 'api'));
|
|
80
|
+
expect(pool).not.toHaveProperty('statement_timeout');
|
|
81
|
+
expect(pool).not.toHaveProperty('idle_in_transaction_session_timeout');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('ResolvePostgreSQLEndpoint', () => {
|
|
86
|
+
it('prefers PG_* variables over DB_* ones', () => {
|
|
87
|
+
expect(ResolvePostgreSQLEndpoint({ PG_HOST: 'pg', DB_HOST: 'db', DB_PORT: '6000', DB_DATABASE: 'd', DB_USERNAME: 'u', DB_PASSWORD: 'p' }))
|
|
88
|
+
.toEqual({ Host: 'pg', Port: 6000, Database: 'd', User: 'u', Password: 'p' });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('falls back to localhost:5432 and the postgres user', () => {
|
|
92
|
+
expect(ResolvePostgreSQLEndpoint({})).toEqual({ Host: 'localhost', Port: 5432, Database: '', User: 'postgres', Password: '' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('ResolvePostgreSQLReadOnlyCredentials', () => {
|
|
97
|
+
it('uses the dbReadOnlyUsername / dbReadOnlyPassword settings SQL Server uses', () => {
|
|
98
|
+
expect(ResolvePostgreSQLReadOnlyCredentials({ dbReadOnlyUsername: 'ro', dbReadOnlyPassword: 'pw' }, {}))
|
|
99
|
+
.toEqual({ User: 'ro', Password: 'pw' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('falls back to PG_READ_ONLY_* when the settings are absent', () => {
|
|
103
|
+
expect(ResolvePostgreSQLReadOnlyCredentials({}, { PG_READ_ONLY_USERNAME: 'pgro', PG_READ_ONLY_PASSWORD: 'x' }))
|
|
104
|
+
.toEqual({ User: 'pgro', Password: 'x' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns null when no complete pair is configured', () => {
|
|
108
|
+
expect(ResolvePostgreSQLReadOnlyCredentials({ dbReadOnlyUsername: 'ro' }, { PG_READ_ONLY_USERNAME: 'pgro' })).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('DescribeReadOnlyLoginOverreach', () => {
|
|
113
|
+
const clientAnswering = (row: ReadOnlyLoginGrants): ReadOnlyLoginCheckClient => ({
|
|
114
|
+
query: async () => ({ rows: [row] }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('says nothing for a login that reads only views', async () => {
|
|
118
|
+
expect(await DescribeReadOnlyLoginOverreach(clientAnswering({ ReadsServerFiles: false, ReadableBaseTables: 0 }), '__mj')).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('warns when the login can read base tables', async () => {
|
|
122
|
+
const warnings = await DescribeReadOnlyLoginOverreach(clientAnswering({ ReadsServerFiles: false, ReadableBaseTables: 412 }), '__mj');
|
|
123
|
+
expect(warnings).toHaveLength(1);
|
|
124
|
+
expect(warnings[0]).toMatch(/can read 412 base table\(s\) in schema "__mj"/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('warns when the login can read server files', async () => {
|
|
128
|
+
const warnings = await DescribeReadOnlyLoginOverreach(clientAnswering({ ReadsServerFiles: true, ReadableBaseTables: 0 }), '__mj');
|
|
129
|
+
expect(warnings[0]).toMatch(/pg_read_server_files/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cross-instance fan-out of push-status updates (MJ #4222).
|
|
3
|
+
*
|
|
4
|
+
* Behind a load balancer the browser's WebSocket lives on one replica while the mutation driving
|
|
5
|
+
* the agent may be handled by another, so a completion can be published to a topic that has no
|
|
6
|
+
* subscriber. Replicating progress and completion closes that.
|
|
7
|
+
*
|
|
8
|
+
* Three properties are pinned here because each one, if wrong, produces a bug that looks like
|
|
9
|
+
* something else entirely:
|
|
10
|
+
*
|
|
11
|
+
* - **Streaming content is not replicated.** It is hundreds of messages per second, each carrying
|
|
12
|
+
* a full serialized agent run. Replicating it would look like a network problem, not a bug here.
|
|
13
|
+
* - **Every publish carries `SourceServerId`.** Without it, echo suppression on the receiving side
|
|
14
|
+
* has nothing to match and every push is delivered twice.
|
|
15
|
+
* - **A hook that throws does not break local delivery.** The message bus is an optimization over
|
|
16
|
+
* the durable tail query; the local path must work when it is down.
|
|
17
|
+
*/
|
|
18
|
+
import 'reflect-metadata';
|
|
19
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
20
|
+
import {
|
|
21
|
+
publishStatusUpdate,
|
|
22
|
+
ParseReplicatedStatusUpdate,
|
|
23
|
+
ShouldReplicateStatusUpdate,
|
|
24
|
+
SetPushStatusPublishHook,
|
|
25
|
+
type PushStatusNotificationPayload,
|
|
26
|
+
} from '../generic/PushStatusResolver.js';
|
|
27
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
28
|
+
import type { PubSubEngine } from 'type-graphql';
|
|
29
|
+
|
|
30
|
+
const SESSION = 'session-1';
|
|
31
|
+
const OWNER = 'U0000000-0000-0000-0000-000000000000';
|
|
32
|
+
|
|
33
|
+
function fakePubSub() {
|
|
34
|
+
const published: Array<{ topic: string; payload: PushStatusNotificationPayload }> = [];
|
|
35
|
+
const engine = {
|
|
36
|
+
publish: (topic: string, payload: PushStatusNotificationPayload) => {
|
|
37
|
+
published.push({ topic, payload });
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
},
|
|
40
|
+
} as unknown as PubSubEngine;
|
|
41
|
+
return { engine, published };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const envelope = (type: string) => JSON.stringify({ resolver: 'RunAIAgentResolver', type, status: 'ok', data: {} });
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
SetPushStatusPublishHook(undefined);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('ShouldReplicateStatusUpdate', () => {
|
|
51
|
+
it('replicates execution progress', () => {
|
|
52
|
+
expect(ShouldReplicateStatusUpdate(envelope('ExecutionProgress'))).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not replicate streaming content', () => {
|
|
56
|
+
expect(ShouldReplicateStatusUpdate(envelope('StreamingContent'))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('replicates a plain-string message, which is rare and low-rate', () => {
|
|
60
|
+
expect(ShouldReplicateStatusUpdate('agent finished')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not replicate an empty message', () => {
|
|
64
|
+
expect(ShouldReplicateStatusUpdate(undefined)).toBe(false);
|
|
65
|
+
expect(ShouldReplicateStatusUpdate('')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('replicates JSON that is not the resolver envelope', () => {
|
|
69
|
+
expect(ShouldReplicateStatusUpdate(JSON.stringify({ hello: 'world' }))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('publishStatusUpdate fan-out', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
SetPushStatusPublishHook(undefined);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('stamps the publishing instance on every payload', () => {
|
|
79
|
+
const { engine, published } = fakePubSub();
|
|
80
|
+
|
|
81
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') });
|
|
82
|
+
|
|
83
|
+
expect(published).toHaveLength(1);
|
|
84
|
+
expect(published[0].payload.SourceServerId).toBe(MJGlobal.Instance.ProcessUUID);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('still publishes locally when no hook is registered', () => {
|
|
88
|
+
const { engine, published } = fakePubSub();
|
|
89
|
+
|
|
90
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') });
|
|
91
|
+
|
|
92
|
+
expect(published[0].payload.ownerUserId).toBe(OWNER);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('forwards a replicable update to the hook', () => {
|
|
96
|
+
const hook = vi.fn();
|
|
97
|
+
SetPushStatusPublishHook(hook);
|
|
98
|
+
const { engine } = fakePubSub();
|
|
99
|
+
|
|
100
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') });
|
|
101
|
+
|
|
102
|
+
expect(hook).toHaveBeenCalledTimes(1);
|
|
103
|
+
const forwarded = hook.mock.calls[0][0] as PushStatusNotificationPayload;
|
|
104
|
+
expect(forwarded.sessionId).toBe(SESSION);
|
|
105
|
+
expect(forwarded.ownerUserId).toBe(OWNER);
|
|
106
|
+
expect(forwarded.SourceServerId).toBe(MJGlobal.Instance.ProcessUUID);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not forward streaming content, but still delivers it locally', () => {
|
|
110
|
+
const hook = vi.fn();
|
|
111
|
+
SetPushStatusPublishHook(hook);
|
|
112
|
+
const { engine, published } = fakePubSub();
|
|
113
|
+
|
|
114
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('StreamingContent') });
|
|
115
|
+
|
|
116
|
+
expect(hook).not.toHaveBeenCalled();
|
|
117
|
+
expect(published).toHaveLength(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('delivers locally even when the hook throws', () => {
|
|
121
|
+
SetPushStatusPublishHook(() => {
|
|
122
|
+
throw new Error('redis is down');
|
|
123
|
+
});
|
|
124
|
+
const { engine, published } = fakePubSub();
|
|
125
|
+
|
|
126
|
+
expect(() =>
|
|
127
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') })
|
|
128
|
+
).not.toThrow();
|
|
129
|
+
expect(published).toHaveLength(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('publishes locally before attempting fan-out', () => {
|
|
133
|
+
const order: string[] = [];
|
|
134
|
+
SetPushStatusPublishHook(() => order.push('hook'));
|
|
135
|
+
const engine = {
|
|
136
|
+
publish: () => {
|
|
137
|
+
order.push('local');
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
},
|
|
140
|
+
} as unknown as PubSubEngine;
|
|
141
|
+
|
|
142
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') });
|
|
143
|
+
|
|
144
|
+
expect(order).toEqual(['local', 'hook']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('clears the hook when set with no argument', () => {
|
|
148
|
+
const hook = vi.fn();
|
|
149
|
+
SetPushStatusPublishHook(hook);
|
|
150
|
+
SetPushStatusPublishHook(undefined);
|
|
151
|
+
const { engine } = fakePubSub();
|
|
152
|
+
|
|
153
|
+
publishStatusUpdate(engine, { sessionId: SESSION, ownerUserId: OWNER, message: envelope('ExecutionProgress') });
|
|
154
|
+
|
|
155
|
+
expect(hook).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('ParseReplicatedStatusUpdate', () => {
|
|
160
|
+
/**
|
|
161
|
+
* Messages on the shared channel come from other processes, so every field is checked before
|
|
162
|
+
* the update is republished onto the local topic, where the subscription filter compares
|
|
163
|
+
* `ownerUserId` against the connection's user.
|
|
164
|
+
*/
|
|
165
|
+
const LOCAL = 'LOCAL-PROCESS';
|
|
166
|
+
const raw = (over: Record<string, unknown> = {}) =>
|
|
167
|
+
JSON.stringify({ sessionId: SESSION, ownerUserId: OWNER, message: 'hello', SourceServerId: 'OTHER', ...over });
|
|
168
|
+
|
|
169
|
+
it('returns the update from another instance', () => {
|
|
170
|
+
expect(ParseReplicatedStatusUpdate(raw(), LOCAL)).toEqual({
|
|
171
|
+
sessionId: SESSION,
|
|
172
|
+
ownerUserId: OWNER,
|
|
173
|
+
message: 'hello',
|
|
174
|
+
SourceServerId: 'OTHER',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('drops our own message echoed back by the bus', () => {
|
|
179
|
+
expect(ParseReplicatedStatusUpdate(raw({ SourceServerId: LOCAL }), LOCAL)).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('drops an update with no identity', () => {
|
|
183
|
+
expect(ParseReplicatedStatusUpdate(raw({ ownerUserId: '' }), LOCAL)).toBeNull();
|
|
184
|
+
expect(ParseReplicatedStatusUpdate(raw({ sessionId: undefined }), LOCAL)).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('drops identity fields that are not strings', () => {
|
|
188
|
+
// A truthy object would pass a presence check and reach the filter as the wrong type.
|
|
189
|
+
expect(ParseReplicatedStatusUpdate(raw({ ownerUserId: { ID: OWNER } }), LOCAL)).toBeNull();
|
|
190
|
+
expect(ParseReplicatedStatusUpdate(raw({ sessionId: 42 }), LOCAL)).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('drops a message body that is not a string', () => {
|
|
194
|
+
expect(ParseReplicatedStatusUpdate(raw({ message: { type: 'x' } }), LOCAL)).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('accepts an update with no message body', () => {
|
|
198
|
+
expect(ParseReplicatedStatusUpdate(raw({ message: undefined }), LOCAL)?.message).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('drops malformed JSON and values that are not objects', () => {
|
|
202
|
+
expect(ParseReplicatedStatusUpdate('{not json', LOCAL)).toBeNull();
|
|
203
|
+
expect(ParseReplicatedStatusUpdate('null', LOCAL)).toBeNull();
|
|
204
|
+
expect(ParseReplicatedStatusUpdate('42', LOCAL)).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated resolvers emit SQL Server bracket-quoted identifiers; on PostgreSQL they become
|
|
3
|
+
* double-quoted. Only identifiers change: brackets inside a string literal are data.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { TranslateBracketsToPG } from '../postgresqlCompat.js';
|
|
7
|
+
|
|
8
|
+
describe('TranslateBracketsToPG', () => {
|
|
9
|
+
it('turns bracket-quoted identifiers into double-quoted ones', () => {
|
|
10
|
+
expect(TranslateBracketsToPG('SELECT [ID], [Name] FROM [__mj].[vwUsers]')).toBe('SELECT "ID", "Name" FROM "__mj"."vwUsers"');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('leaves brackets inside a string literal alone', () => {
|
|
14
|
+
expect(TranslateBracketsToPG("SELECT [ID] FROM [t] WHERE [Note] = '[draft] copy'")).toBe(`SELECT "ID" FROM "t" WHERE "Note" = '[draft] copy'`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('unescapes ]] and escapes a double quote in the name', () => {
|
|
18
|
+
expect(TranslateBracketsToPG('SELECT [a]]b], [say "hi"] FROM t')).toBe('SELECT "a]b", "say ""hi""" FROM t');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -21,6 +21,7 @@ describe('Authentication Provider Backward Compatibility', () => {
|
|
|
21
21
|
let factory: AuthProviderFactory;
|
|
22
22
|
|
|
23
23
|
beforeEach(() => {
|
|
24
|
+
vi.spyOn(AuthProviderFactory, 'DiscoverFromEnvironment').mockReturnValue([]);
|
|
24
25
|
factory = AuthProviderFactory.Instance;
|
|
25
26
|
factory.clear();
|
|
26
27
|
});
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { DEFAULT_WORK_QUEUE_CONFIG, WorkQueueSchema } from './services/workQueueConfig.js';
|
|
2
3
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
4
|
import { LogError, LogStatus, LogStatusEx } from '@memberjunction/core';
|
|
4
5
|
import { mergeConfigs, parseBooleanEnv } from '@memberjunction/config';
|
|
@@ -470,10 +471,20 @@ const twilioTelephonySchema = z.object({
|
|
|
470
471
|
apiKeySecret: z.string().optional(),
|
|
471
472
|
/** The publicly reachable `wss://…/telephony/twilio/media` URL Twilio's <Connect><Stream> connects to. */
|
|
472
473
|
streamPublicUrl: z.string(),
|
|
473
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* Reserved; currently unused. The Media-Streams websocket is authenticated by a per-call token MJ mints and
|
|
476
|
+
* embeds in the TwiML (`<Parameter name="mjToken">`), and webhooks by `X-Twilio-Signature`.
|
|
477
|
+
*/
|
|
474
478
|
webhookSigningSecret: z.string().optional(),
|
|
475
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* URL Twilio posts outbound-call lifecycle events to. Defaults to `<public URL>/telephony/twilio/status`, the
|
|
481
|
+
* route that ends the bridge session when a call is busy / unanswered / failed / completed.
|
|
482
|
+
*/
|
|
476
483
|
statusCallbackUrl: z.string().optional(),
|
|
484
|
+
/** URL Twilio posts the async answering-machine verdict to. Defaults to `<public URL>/telephony/twilio/amd`. */
|
|
485
|
+
amdStatusCallbackUrl: z.string().optional(),
|
|
486
|
+
/** What to do when a machine or fax answers an outbound call: `hangup` (default) ends it, `continue` only logs the verdict. */
|
|
487
|
+
onMachine: z.enum(['hangup', 'continue']).optional(),
|
|
477
488
|
}).passthrough();
|
|
478
489
|
|
|
479
490
|
/**
|
|
@@ -495,8 +506,14 @@ const vonageTelephonySchema = z.object({
|
|
|
495
506
|
mediaPublicUrl: z.string(),
|
|
496
507
|
/** Vonage account signature secret — HMAC key for signed-request `sig` AND HS256 webhook-JWT verification. */
|
|
497
508
|
signatureSecret: z.string().optional(),
|
|
498
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* Event-webhook URL Vonage posts call lifecycle events to (passed on outbound createCall). Defaults to
|
|
511
|
+
* `<public URL>/telephony/vonage/event`, the route that ends the bridge session when a call is busy /
|
|
512
|
+
* unanswered / failed / completed.
|
|
513
|
+
*/
|
|
499
514
|
eventUrl: z.string().optional(),
|
|
515
|
+
/** What to do when a machine answers an outbound call — Vonage's `machine_detection`: `hangup` (default) or `continue`. */
|
|
516
|
+
onMachine: z.enum(['hangup', 'continue']).optional(),
|
|
500
517
|
}).passthrough();
|
|
501
518
|
|
|
502
519
|
/**
|
|
@@ -550,15 +567,119 @@ const teamsMeetingsSchema = z.object({
|
|
|
550
567
|
modelSampleRate: z.coerce.number().optional().default(16000),
|
|
551
568
|
}).passthrough();
|
|
552
569
|
|
|
570
|
+
/**
|
|
571
|
+
* LiveKit SIP binding: phone calls carried by a SIP trunk and landed in a LiveKit room (every conversation lives in a room;
|
|
572
|
+
* the carrier is only the pipe). LiveKit credentials default to the `LIVEKIT_URL` / `LIVEKIT_API_KEY` /
|
|
573
|
+
* `LIVEKIT_API_SECRET` env vars the Meet room already uses. When `livekitSip` is omitted, the LiveKit SIP webhook is not mounted.
|
|
574
|
+
* Point a LiveKit project webhook at `<public URL>/telephony/livekit-sip/webhook` (the signature is verified with the API secret).
|
|
575
|
+
*/
|
|
576
|
+
const livekitSipTelephonySchema = z.object({
|
|
577
|
+
/** LiveKit server URL (`wss://…`). Defaults to `LIVEKIT_URL`. */
|
|
578
|
+
serverUrl: z.string().optional(),
|
|
579
|
+
/** LiveKit API key. Defaults to `LIVEKIT_API_KEY`. */
|
|
580
|
+
apiKey: z.string().optional(),
|
|
581
|
+
/** LiveKit API secret (resolved upstream — never inlined). Defaults to `LIVEKIT_API_SECRET`. */
|
|
582
|
+
apiSecret: z.string().optional(),
|
|
583
|
+
/** Inbound calls land in a room whose name starts with this. Defaults to `call-`. */
|
|
584
|
+
roomPrefix: z.string().optional(),
|
|
585
|
+
/** The numbers (E.164) this deployment answers. Each routes to the agent identity registered for it (on the LiveKit provider). */
|
|
586
|
+
numbers: z.array(z.string()).optional(),
|
|
587
|
+
/** The LiveKit inbound trunk id, when created by hand. */
|
|
588
|
+
inboundTrunkId: z.string().optional(),
|
|
589
|
+
/** The LiveKit OUTBOUND trunk used to dial out (outbound calls, fallback legs, transfers to a number). Dialing out is off without it. */
|
|
590
|
+
outboundTrunkId: z.string().optional(),
|
|
591
|
+
/** The caller ID presented on calls dialed out through the outbound trunk. */
|
|
592
|
+
outboundFromNumber: z.string().optional(),
|
|
593
|
+
/** Create the inbound trunk and dispatch rule at startup when missing (idempotent). Defaults to false. */
|
|
594
|
+
autoProvision: zodBooleanWithTransforms().optional().default(false),
|
|
595
|
+
/** Source addresses the inbound trunk accepts calls from (the carrier's SIP signalling addresses). Used when provisioning. */
|
|
596
|
+
allowedAddresses: z.array(z.string()).optional(),
|
|
597
|
+
/** The carrier behind the trunk, for configuration checks only (no carrier API is called). */
|
|
598
|
+
carrier: z
|
|
599
|
+
.object({
|
|
600
|
+
type: z.literal('twilio-elastic-sip'),
|
|
601
|
+
/** The LiveKit SIP URI the Twilio trunk's origination URI points at, e.g. `sip:<project>.sip.livekit.cloud`. */
|
|
602
|
+
originationUri: z.string().optional(),
|
|
603
|
+
/** The trunk's Termination SIP URI on Twilio (`<name>.pstn.twilio.com`). Needed only when dialing out. */
|
|
604
|
+
terminationUri: z.string().optional(),
|
|
605
|
+
/** How Twilio authenticates LiveKit's outbound requests. */
|
|
606
|
+
terminationAuth: z.enum(['credential-list', 'ip-acl']).optional(),
|
|
607
|
+
/** The Twilio numbers associated with the trunk (E.164). */
|
|
608
|
+
numbers: z.array(z.string()).optional(),
|
|
609
|
+
})
|
|
610
|
+
.optional(),
|
|
611
|
+
}).passthrough();
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Outbound-call policy applied to every `PlaceTwilioCall` / `PlaceVonageCall` / `PlaceRingCentralCall`
|
|
615
|
+
* mutation, on top of the caller's right to run the agent. Defaults are deliberately conservative.
|
|
616
|
+
* The rate limiter is in-memory and therefore PER PROCESS: with N MJAPI instances a user can place up to
|
|
617
|
+
* N × `maxCallsPerUserPerHour` calls.
|
|
618
|
+
*/
|
|
619
|
+
const outboundTelephonySchema = z.object({
|
|
620
|
+
/** Destination prefixes a call may go to (E.164, e.g. `+1`). An empty list refuses every destination. Defaults to `['+1']`. */
|
|
621
|
+
allowedPrefixes: z.array(z.string()).optional().default(['+1']),
|
|
622
|
+
/**
|
|
623
|
+
* Destination prefixes that are always refused, even when an allowed prefix matches. Deliberately has NO default
|
|
624
|
+
* here: when omitted, `@memberjunction/telephony-adapters` applies its own `DEFAULT_BLOCKED_PREFIXES` (NANP
|
|
625
|
+
* premium-rate plus the Caribbean +1 countries abused for toll fraud), so the list lives in exactly one place.
|
|
626
|
+
* Setting this REPLACES that list.
|
|
627
|
+
*/
|
|
628
|
+
blockedPrefixes: z.array(z.string()).optional(),
|
|
629
|
+
/** Max outbound calls one user may place per rolling hour (per process). Defaults to 20. */
|
|
630
|
+
maxCallsPerUserPerHour: z.coerce.number().int().positive().optional().default(20),
|
|
631
|
+
}).passthrough();
|
|
632
|
+
|
|
553
633
|
const telephonySchema = z.object({
|
|
554
634
|
/** Master switch. When false (or when no vendor block is present), telephony routes are not mounted. */
|
|
555
635
|
enabled: zodBooleanWithTransforms().default(false),
|
|
636
|
+
/**
|
|
637
|
+
* Email of the user INBOUND calls run as. A caller is an anonymous member of the public, so the call needs a
|
|
638
|
+
* principal to create its agent session and run the agent. Point this at a DEDICATED LEAST-PRIVILEGE user.
|
|
639
|
+
* There is deliberately no fallback: if unset, unknown, inactive, or the system user, inbound calls are
|
|
640
|
+
* rejected with a polite message and the rejection is logged. Applies to Twilio, Vonage and RingCentral.
|
|
641
|
+
*/
|
|
642
|
+
inboundRunAsUserEmail: z.string().optional(),
|
|
643
|
+
/** Maximum length of one phone call, in seconds. The bridge session is stopped (and the call hung up) at the cap. Defaults to 1800. */
|
|
644
|
+
maxCallSeconds: z.coerce.number().int().positive().optional().default(1800),
|
|
645
|
+
/**
|
|
646
|
+
* Most phone calls (every carrier together, both directions) the server carries at once. Past it an inbound
|
|
647
|
+
* caller hears "all agents are busy" and an outbound request is refused. Keep it at or below the realtime
|
|
648
|
+
* model plan's concurrent-session limit. Defaults to 25.
|
|
649
|
+
*/
|
|
650
|
+
maxConcurrentCalls: z.coerce.number().int().positive().optional().default(25),
|
|
651
|
+
/** Outbound destination policy + per-user rate limit (see {@link outboundTelephonySchema}). */
|
|
652
|
+
outbound: outboundTelephonySchema.optional().default({}),
|
|
653
|
+
/**
|
|
654
|
+
* The places the agent may hand a live call to, by name (the agent names a target, never a number, an email or an agent
|
|
655
|
+
* id). `kind` is `number` (the default; a phone number), `user` (a person at an Explorer console, who may accept or
|
|
656
|
+
* decline; `userEmail`, optional `fallbackNumber` the room dials if they do not take it) or `agent` (another MJ AI agent,
|
|
657
|
+
* by `agentName`). `user` and `agent` targets work on calls that live in a LiveKit room; a carrier call can only transfer to
|
|
658
|
+
* a `number`. Every number is validated at startup against the outbound allow/block lists; an invalid entry is dropped
|
|
659
|
+
* and logged. Empty (the default) means the agent cannot hand calls over at all.
|
|
660
|
+
*/
|
|
661
|
+
transferTargets: z
|
|
662
|
+
.array(
|
|
663
|
+
z.object({
|
|
664
|
+
name: z.string().min(1),
|
|
665
|
+
kind: z.enum(['number', 'user', 'agent']).optional(),
|
|
666
|
+
number: z.string().optional(),
|
|
667
|
+
userEmail: z.string().optional(),
|
|
668
|
+
fallbackNumber: z.string().optional(),
|
|
669
|
+
agentName: z.string().optional(),
|
|
670
|
+
description: z.string().optional(),
|
|
671
|
+
}),
|
|
672
|
+
)
|
|
673
|
+
.optional()
|
|
674
|
+
.default([]),
|
|
556
675
|
/** Twilio Programmable Voice + Media Streams binding. */
|
|
557
676
|
twilio: twilioTelephonySchema.optional(),
|
|
558
677
|
/** Vonage Voice + WebSocket-media binding. */
|
|
559
678
|
vonage: vonageTelephonySchema.optional(),
|
|
560
679
|
/** RingCentral Call Control + media-stream binding. */
|
|
561
680
|
ringcentral: ringcentralTelephonySchema.optional(),
|
|
681
|
+
/** LiveKit SIP binding: phone calls carried by a SIP trunk, landed in a LiveKit room. */
|
|
682
|
+
livekitSip: livekitSipTelephonySchema.optional(),
|
|
562
683
|
/** Microsoft Teams meetings (Graph cloud-communications + ACS application-hosted media) binding. */
|
|
563
684
|
teams: teamsMeetingsSchema.optional(),
|
|
564
685
|
}).passthrough();
|
|
@@ -582,6 +703,7 @@ const configInfoSchema = z.object({
|
|
|
582
703
|
componentRegistries: z.array(componentRegistrySchema).optional(),
|
|
583
704
|
scheduledJobs: scheduledJobsSchema.optional().default({}),
|
|
584
705
|
integrationSyncWorker: integrationSyncWorkerSchema.optional().default({}),
|
|
706
|
+
workQueue: WorkQueueSchema.optional().default({}),
|
|
585
707
|
telemetry: telemetrySchema.optional().default({}),
|
|
586
708
|
queryDialects: queryDialectSchema.optional().default({}),
|
|
587
709
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
@@ -632,6 +754,7 @@ export type RealtimeConfig = z.infer<typeof realtimeSchema>;
|
|
|
632
754
|
export type TwilioTelephonyConfig = z.infer<typeof twilioTelephonySchema>;
|
|
633
755
|
export type VonageTelephonyConfig = z.infer<typeof vonageTelephonySchema>;
|
|
634
756
|
export type RingCentralTelephonyConfig = z.infer<typeof ringcentralTelephonySchema>;
|
|
757
|
+
export type LiveKitSipTelephonyConfig = z.infer<typeof livekitSipTelephonySchema>;
|
|
635
758
|
export type TeamsMeetingsConfig = z.infer<typeof teamsMeetingsSchema>;
|
|
636
759
|
export type DatabaseSettingsInfo = z.infer<typeof databaseSettingsInfoSchema>;
|
|
637
760
|
export type ViewingSystemSettingsInfo = z.infer<typeof viewingSystemInfoSchema>;
|
|
@@ -642,6 +765,7 @@ export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
|
|
|
642
765
|
export type ComponentRegistryConfig = z.infer<typeof componentRegistrySchema>;
|
|
643
766
|
export type ScheduledJobsConfig = z.infer<typeof scheduledJobsSchema>;
|
|
644
767
|
export type IntegrationSyncWorkerConfig = z.infer<typeof integrationSyncWorkerSchema>;
|
|
768
|
+
export type { WorkQueueConfig } from './services/workQueueConfig.js';
|
|
645
769
|
export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
646
770
|
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
647
771
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
@@ -775,6 +899,9 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
775
899
|
maxConcurrentRuns: 3
|
|
776
900
|
},
|
|
777
901
|
|
|
902
|
+
// Work queue host defaults (off until an instance opts in)
|
|
903
|
+
workQueue: DEFAULT_WORK_QUEUE_CONFIG,
|
|
904
|
+
|
|
778
905
|
// Realtime WebRTC SDP broker defaults (on by default; can be disabled via MJ_REALTIME_ENABLED=false)
|
|
779
906
|
realtime: {
|
|
780
907
|
enabled: RealtimeEnabledDefault(process.env.MJ_REALTIME_ENABLED),
|
package/src/context.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { CloneUserForSessionContext } from './auth/sessionUserClone.js';
|
|
|
10
10
|
import { GetAPIKeyActingContextResolver } from './auth/actingContextResolver.js';
|
|
11
11
|
import { TokenExpiredError, AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
12
12
|
import { AuthCache } from './cache.js';
|
|
13
|
-
import { userEmailMap, apiKey, mj_core_schema } from './config.js';
|
|
13
|
+
import { userEmailMap, apiKey, mj_core_schema, configInfo } from './config.js';
|
|
14
|
+
import { BuildPostgreSQLConnectionConfig, PostgreSQLReadOnlyPool, ResolvePostgreSQLEndpoint, ResolvePostgreSQLReadOnlyCredentials } from './postgresqlPoolSettings.js';
|
|
14
15
|
import { BuildBoundaryLogPayload } from './logging/boundaryLogPayload.js';
|
|
15
16
|
import { StartupLogger } from './logging/StartupLogger.js';
|
|
16
17
|
import { DataSourceInfo, UserPayload } from './types.js';
|
|
@@ -831,15 +832,10 @@ async function createPerRequestProviders(
|
|
|
831
832
|
*/
|
|
832
833
|
async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
833
834
|
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
834
|
-
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
835
|
-
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
836
|
-
const pgUser = process.env.PG_USERNAME || process.env.DB_USERNAME || 'postgres';
|
|
837
|
-
const pgPass = process.env.PG_PASSWORD || process.env.DB_PASSWORD || '';
|
|
838
|
-
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
839
835
|
|
|
840
836
|
const pgProvider = new PostgreSQLDataProvider();
|
|
841
837
|
const pgConfig = new PostgreSQLProviderConfigData(
|
|
842
|
-
|
|
838
|
+
BuildPostgreSQLConnectionConfig(ResolvePostgreSQLEndpoint(), configInfo.databaseSettings, 'api'),
|
|
843
839
|
mj_core_schema,
|
|
844
840
|
0,
|
|
845
841
|
undefined,
|
|
@@ -859,39 +855,30 @@ async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
|
859
855
|
}
|
|
860
856
|
|
|
861
857
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
858
|
+
* Creates a read-only PostgreSQL per-request provider on the read-only pool MJAPI opened at
|
|
859
|
+
* startup with the read-only login. Returns null when no read-only login is configured, so the
|
|
860
|
+
* caller never falls back to the read-write pool.
|
|
864
861
|
*/
|
|
865
862
|
async function tryCreateReadOnlyPostgresProvider(): Promise<DatabaseProviderBase | null> {
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
if (!
|
|
863
|
+
const readOnlyPool = PostgreSQLReadOnlyPool.Instance.Pool;
|
|
864
|
+
const credentials = ResolvePostgreSQLReadOnlyCredentials(configInfo);
|
|
865
|
+
if (!readOnlyPool || !credentials) {
|
|
869
866
|
return null;
|
|
870
867
|
}
|
|
871
868
|
|
|
872
869
|
try {
|
|
873
870
|
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
874
|
-
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
875
|
-
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
876
|
-
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
877
871
|
|
|
878
872
|
const roProvider = new PostgreSQLDataProvider();
|
|
879
873
|
const roConfig = new PostgreSQLProviderConfigData(
|
|
880
|
-
{
|
|
874
|
+
BuildPostgreSQLConnectionConfig({ ...ResolvePostgreSQLEndpoint(), ...credentials }, configInfo.databaseSettings, 'read-only'),
|
|
881
875
|
mj_core_schema,
|
|
882
876
|
0,
|
|
883
877
|
undefined,
|
|
884
878
|
undefined,
|
|
885
879
|
false,
|
|
886
880
|
);
|
|
887
|
-
|
|
888
|
-
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)
|
|
889
|
-
if (primaryProvider?.DatabaseConnection) {
|
|
890
|
-
await roProvider.ConfigWithSharedPool(roConfig, primaryProvider.DatabaseConnection);
|
|
891
|
-
} else {
|
|
892
|
-
await roProvider.Config(roConfig);
|
|
893
|
-
}
|
|
894
|
-
|
|
881
|
+
await roProvider.ConfigWithSharedPool(roConfig, readOnlyPool);
|
|
895
882
|
return roProvider;
|
|
896
883
|
} catch (_err) {
|
|
897
884
|
return null;
|