@memberjunction/server 6.2.0-edge.2 → 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.
Files changed (150) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +2189 -71
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +123 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -23
  9. package/dist/context.js.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
  11. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  12. package/dist/generated/graphql-schemas/__mj.js +8482 -3649
  13. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  14. package/dist/generic/ResolverBase.d.ts +43 -3
  15. package/dist/generic/ResolverBase.d.ts.map +1 -1
  16. package/dist/generic/ResolverBase.js +142 -27
  17. package/dist/generic/ResolverBase.js.map +1 -1
  18. package/dist/generic/RunViewResolver.d.ts +4 -0
  19. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  20. package/dist/generic/RunViewResolver.js +33 -1
  21. package/dist/generic/RunViewResolver.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +115 -49
  25. package/dist/index.js.map +1 -1
  26. package/dist/postgresqlCompat.d.ts +8 -0
  27. package/dist/postgresqlCompat.d.ts.map +1 -0
  28. package/dist/postgresqlCompat.js +19 -0
  29. package/dist/postgresqlCompat.js.map +1 -0
  30. package/dist/postgresqlPoolSettings.d.ts +108 -0
  31. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  32. package/dist/postgresqlPoolSettings.js +134 -0
  33. package/dist/postgresqlPoolSettings.js.map +1 -0
  34. package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
  35. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  36. package/dist/resolvers/AdhocQueryResolver.js +77 -167
  37. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  38. package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
  39. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  40. package/dist/resolvers/EntityRecordNameResolver.js +9 -47
  41. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  42. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  43. package/dist/resolvers/GetDataResolver.js +9 -0
  44. package/dist/resolvers/GetDataResolver.js.map +1 -1
  45. package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
  46. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
  47. package/dist/resolvers/HumanHandoffResolver.js +324 -0
  48. package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
  49. package/dist/resolvers/MeetingResolver.d.ts +69 -0
  50. package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
  51. package/dist/resolvers/MeetingResolver.js +381 -0
  52. package/dist/resolvers/MeetingResolver.js.map +1 -0
  53. package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
  54. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  55. package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
  56. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  57. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
  58. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  59. package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
  60. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  61. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  62. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  63. package/dist/resolvers/adhocTimeout.js +12 -0
  64. package/dist/resolvers/adhocTimeout.js.map +1 -0
  65. package/dist/resolvers/meetings/index.d.ts +5 -0
  66. package/dist/resolvers/meetings/index.d.ts.map +1 -0
  67. package/dist/resolvers/meetings/index.js +5 -0
  68. package/dist/resolvers/meetings/index.js.map +1 -0
  69. package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
  70. package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
  71. package/dist/resolvers/meetings/meetingHelpers.js +212 -0
  72. package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
  73. package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
  74. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
  75. package/dist/resolvers/meetings/meetingMutations.js +418 -0
  76. package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
  77. package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
  78. package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
  79. package/dist/resolvers/meetings/meetingQueries.js +113 -0
  80. package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
  81. package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
  82. package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
  83. package/dist/resolvers/meetings/meetingTypes.js +345 -0
  84. package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
  85. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  86. package/dist/rest/ViewOperationsHandler.js +8 -5
  87. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  88. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  89. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  90. package/dist/services/TaskGraphActionRunner.js +27 -1
  91. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  92. package/dist/services/WorkQueueHostService.d.ts +36 -0
  93. package/dist/services/WorkQueueHostService.d.ts.map +1 -0
  94. package/dist/services/WorkQueueHostService.js +97 -0
  95. package/dist/services/WorkQueueHostService.js.map +1 -0
  96. package/dist/services/workQueueConfig.d.ts +103 -0
  97. package/dist/services/workQueueConfig.d.ts.map +1 -0
  98. package/dist/services/workQueueConfig.js +48 -0
  99. package/dist/services/workQueueConfig.js.map +1 -0
  100. package/dist/types.d.ts +5 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/package.json +95 -94
  104. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  105. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  106. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  107. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  108. package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
  109. package/src/__tests__/MeetingResolver.test.ts +374 -0
  110. package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
  111. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
  112. package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
  113. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
  114. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
  115. package/src/__tests__/WorkQueueHostService.test.ts +133 -0
  116. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  117. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  118. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  119. package/src/config.ts +130 -3
  120. package/src/context.ts +11 -24
  121. package/src/generated/graphql-schemas/__mj.ts +22138 -18770
  122. package/src/generic/ResolverBase.ts +171 -27
  123. package/src/generic/RunViewResolver.ts +33 -2
  124. package/src/index.ts +127 -50
  125. package/src/postgresqlCompat.ts +21 -0
  126. package/src/postgresqlPoolSettings.ts +213 -0
  127. package/src/resolvers/AdhocQueryResolver.ts +88 -188
  128. package/src/resolvers/EntityRecordNameResolver.ts +9 -51
  129. package/src/resolvers/GetDataResolver.ts +10 -0
  130. package/src/resolvers/HumanHandoffResolver.ts +304 -0
  131. package/src/resolvers/MeetingResolver.ts +438 -0
  132. package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
  133. package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
  134. package/src/resolvers/adhocTimeout.ts +12 -0
  135. package/src/resolvers/meetings/index.ts +4 -0
  136. package/src/resolvers/meetings/meetingHelpers.ts +307 -0
  137. package/src/resolvers/meetings/meetingMutations.ts +535 -0
  138. package/src/resolvers/meetings/meetingQueries.ts +172 -0
  139. package/src/resolvers/meetings/meetingTypes.ts +238 -0
  140. package/src/rest/ViewOperationsHandler.ts +8 -5
  141. package/src/services/TaskGraphActionRunner.ts +29 -1
  142. package/src/services/WorkQueueHostService.ts +129 -0
  143. package/src/services/workQueueConfig.ts +52 -0
  144. package/src/types.ts +5 -0
  145. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
  146. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  147. package/dist/resolvers/adhoc-query-helpers.js +0 -50
  148. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  149. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  150. 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,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
+ });
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
- /** Optional shared secret gating the public webhook/WSS endpoints (defense-in-depth beyond signature verification). */
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
- /** Optional status-callback URL Twilio posts call lifecycle events to. */
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
- /** Optional event-webhook URL Vonage posts call lifecycle events to (passed on outbound createCall). */
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
- { Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgUser, Password: pgPass },
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
- * Attempts to create a read-only PostgreSQL provider using DB_READ_ONLY_USERNAME/PASSWORD.
863
- * Shares the connection pool from the primary provider. Returns null if no read-only credentials are configured.
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 roUser = process.env.PG_READ_ONLY_USERNAME || process.env.DB_READ_ONLY_USERNAME;
867
- const roPass = process.env.PG_READ_ONLY_PASSWORD || process.env.DB_READ_ONLY_PASSWORD;
868
- if (!roUser || !roPass) {
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
- { Host: pgHost, Port: pgPort, Database: pgDatabase, User: roUser, Password: roPass },
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;