@memberjunction/server 5.41.0 → 5.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +5 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +4 -7
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +591 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +6832 -3610
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -16
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +108 -0
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
- package/dist/integration/CustomColumnPromoter.js +508 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -0
- package/dist/logging/StartupLogger.d.ts +57 -1
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +115 -6
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +78 -5
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +227 -15
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +0 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +5 -3
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/package.json +83 -78
- package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +8 -1
- package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
- package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
- package/src/agentSessions/index.ts +5 -0
- package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
- package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
- package/src/auth/initializeProviders.ts +4 -7
- package/src/config.ts +13 -2
- package/src/generated/generated.ts +2444 -205
- package/src/index.ts +33 -16
- package/src/integration/CustomColumnPromoter.ts +597 -0
- package/src/logging/StartupLogger.ts +130 -6
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
- package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +232 -17
- package/src/resolvers/RunAIAgentResolver.ts +1 -9
- package/src/rest/setupRESTEndpoints.ts +5 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { configInfo } from '../config.js';
|
|
2
2
|
import { LogStatus } from '@memberjunction/core';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import type { Ora } from 'ora';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Server log verbosity levels, ordered from least to most chatty.
|
|
@@ -46,6 +48,8 @@ type StartupSummaryData = {
|
|
|
46
48
|
DbConnection?: string;
|
|
47
49
|
/** Count of entities loaded into metadata. */
|
|
48
50
|
EntityCount?: number;
|
|
51
|
+
/** Absolute path to the resolved config file (omitted from the block if absent). */
|
|
52
|
+
ConfigPath?: string;
|
|
49
53
|
/** Names of registered auth providers. */
|
|
50
54
|
AuthProviders: string[];
|
|
51
55
|
/** Whether the REST API is enabled. */
|
|
@@ -82,6 +86,14 @@ export class StartupLogger {
|
|
|
82
86
|
private readonly resolvedLevel: ServerLogLevel;
|
|
83
87
|
private readonly phaseTimings: PhaseTiming[] = [];
|
|
84
88
|
private readonly summary: StartupSummaryData = { AuthProviders: [] };
|
|
89
|
+
/** Transient boot spinner — active only at `standard` level on a TTY (see {@link spinnerEnabled}). */
|
|
90
|
+
private spinner: Ora | null = null;
|
|
91
|
+
/** Interval that ticks the live elapsed counter on the spinner line. */
|
|
92
|
+
private bootTimer: ReturnType<typeof setInterval> | null = null;
|
|
93
|
+
/** Wall-clock start of the boot indicator, for the live elapsed counter. */
|
|
94
|
+
private bootStartMs = 0;
|
|
95
|
+
/** Most recent phase label, shown on the spinner line. */
|
|
96
|
+
private currentPhase?: string;
|
|
85
97
|
|
|
86
98
|
/**
|
|
87
99
|
* @param level Optional explicit level override (primarily for tests).
|
|
@@ -140,6 +152,85 @@ export class StartupLogger {
|
|
|
140
152
|
return performance.now();
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/**
|
|
156
|
+
* True when the transient boot spinner should animate: only at `standard`
|
|
157
|
+
* level AND when stdout is an interactive TTY. In non-TTY contexts (Docker,
|
|
158
|
+
* systemd, piped logs) a spinner emits escape codes that pollute log
|
|
159
|
+
* aggregation, and at `verbose`+ the inline phase lines would fight it — so
|
|
160
|
+
* both fall back to plain, line-oriented output.
|
|
161
|
+
*/
|
|
162
|
+
private get spinnerEnabled(): boolean {
|
|
163
|
+
return this.resolvedLevel === 'standard' && process.stdout.isTTY === true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Prefix for the transient boot indicator (no rocket — we haven't launched yet). */
|
|
167
|
+
private static readonly BOOT_LABEL = 'Bootstrapping MemberJunction Server';
|
|
168
|
+
|
|
169
|
+
/** Composes the boot label, plus the current phase when known (no elapsed counter). */
|
|
170
|
+
private bootText(phase?: string): string {
|
|
171
|
+
return phase ? `${StartupLogger.BOOT_LABEL} · ${phase}` : StartupLogger.BOOT_LABEL;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Repaints the spinner line with the current phase and a live elapsed counter. */
|
|
175
|
+
private refreshBootText(): void {
|
|
176
|
+
if (this.spinner) {
|
|
177
|
+
const elapsed = ((performance.now() - this.bootStartMs) / 1000).toFixed(1);
|
|
178
|
+
this.spinner.text = `${this.bootText(this.currentPhase)} ${elapsed}s`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Begins the transient "Bootstrapping…" indicator, replaced by the 🚀 summary
|
|
184
|
+
* block once {@link PrintSummary} runs (so the rocket appears only after launch).
|
|
185
|
+
*
|
|
186
|
+
* - `standard` + TTY: an animated, self-clearing `ora` spinner (its glyph is the
|
|
187
|
+
* leading "working" icon; phase status is appended inline).
|
|
188
|
+
* - `verbose`/`debug`, or non-TTY at `standard`: a single plain line with an
|
|
189
|
+
* hourglass icon (can't be cleared, but inline phase logs / the final block
|
|
190
|
+
* carry the detail). `minimal` stays silent.
|
|
191
|
+
*/
|
|
192
|
+
public StartBoot(): void {
|
|
193
|
+
if (this.spinner) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (this.spinnerEnabled) {
|
|
197
|
+
this.bootStartMs = performance.now();
|
|
198
|
+
this.spinner = ora({ text: this.bootText(), stream: process.stdout }).start();
|
|
199
|
+
// Tick the elapsed counter independently of ora's glyph animation. Unref'd so
|
|
200
|
+
// it never keeps the event loop alive; cleared by StopBoot before the block prints.
|
|
201
|
+
this.bootTimer = setInterval(() => this.refreshBootText(), 200);
|
|
202
|
+
this.bootTimer.unref();
|
|
203
|
+
} else if (this.resolvedLevel !== 'minimal') {
|
|
204
|
+
// eslint-disable-next-line no-console
|
|
205
|
+
console.log(`⏳ ${this.bootText()}…`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Marks the beginning of a named phase: updates the spinner status (when active)
|
|
211
|
+
* and returns a start timestamp to pass to {@link EndPhase}. Pure timing helper
|
|
212
|
+
* when the spinner is disabled — equivalent to {@link StartPhase}.
|
|
213
|
+
*
|
|
214
|
+
* @param phase Friendly label for the upcoming phase, shown after the boot label.
|
|
215
|
+
*/
|
|
216
|
+
public BeginPhase(phase: string): number {
|
|
217
|
+
this.currentPhase = phase;
|
|
218
|
+
this.refreshBootText();
|
|
219
|
+
return performance.now();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Stops and clears the boot spinner + its elapsed-counter interval. Safe to call when inactive. */
|
|
223
|
+
public StopBoot(): void {
|
|
224
|
+
if (this.bootTimer) {
|
|
225
|
+
clearInterval(this.bootTimer);
|
|
226
|
+
this.bootTimer = null;
|
|
227
|
+
}
|
|
228
|
+
if (this.spinner) {
|
|
229
|
+
this.spinner.stop();
|
|
230
|
+
this.spinner = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
143
234
|
/**
|
|
144
235
|
* Records the elapsed time for a phase since `startedAt`, buffering it for the
|
|
145
236
|
* summary. At `verbose`+ the phase is also printed immediately (matching the
|
|
@@ -163,6 +254,11 @@ export class StartupLogger {
|
|
|
163
254
|
this.summary.Version = version;
|
|
164
255
|
}
|
|
165
256
|
|
|
257
|
+
/** Records the resolved config-file path for the summary `Config` line. */
|
|
258
|
+
public SetConfigPath(path: string | undefined): void {
|
|
259
|
+
this.summary.ConfigPath = path;
|
|
260
|
+
}
|
|
261
|
+
|
|
166
262
|
/** Records DB platform + connection + entity count for the summary `DB` line. */
|
|
167
263
|
public SetDatabaseInfo(platform: string, connection: string, entityCount: number): void {
|
|
168
264
|
this.summary.DbPlatform = platform;
|
|
@@ -198,6 +294,9 @@ export class StartupLogger {
|
|
|
198
294
|
* all detail lines were already printed inline during boot).
|
|
199
295
|
*/
|
|
200
296
|
public PrintSummary(): void {
|
|
297
|
+
// Clear the transient spinner before any block output so the line it
|
|
298
|
+
// occupied is replaced cleanly by the summary.
|
|
299
|
+
this.StopBoot();
|
|
201
300
|
if (this.resolvedLevel === 'minimal') {
|
|
202
301
|
this.printReadyLineOnly();
|
|
203
302
|
return;
|
|
@@ -225,6 +324,7 @@ export class StartupLogger {
|
|
|
225
324
|
const lines: string[] = [];
|
|
226
325
|
lines.push(this.buildHeaderLine());
|
|
227
326
|
lines.push(this.buildDbLine());
|
|
327
|
+
lines.push(this.buildConfigLine());
|
|
228
328
|
lines.push(this.buildAuthLine());
|
|
229
329
|
lines.push(this.buildStartupLine());
|
|
230
330
|
lines.push(this.buildReadyLine());
|
|
@@ -252,6 +352,14 @@ export class StartupLogger {
|
|
|
252
352
|
return ` DB ${parts.join(' · ')}`;
|
|
253
353
|
}
|
|
254
354
|
|
|
355
|
+
/** ` Config <path>` (omitted when no config file was resolved). */
|
|
356
|
+
private buildConfigLine(): string {
|
|
357
|
+
if (!this.summary.ConfigPath) {
|
|
358
|
+
return '';
|
|
359
|
+
}
|
|
360
|
+
return ` Config ${this.summary.ConfigPath}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
255
363
|
/** ` Auth <providers> REST <on/off> · <N> scheduled jobs`. */
|
|
256
364
|
private buildAuthLine(): string {
|
|
257
365
|
const providers = this.summary.AuthProviders.length > 0
|
|
@@ -268,16 +376,32 @@ export class StartupLogger {
|
|
|
268
376
|
return ` Auth ${providers}${trailerPart}`;
|
|
269
377
|
}
|
|
270
378
|
|
|
271
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* ` Startup <total>s (<phase> <x>s · … · other <z>s)`.
|
|
381
|
+
*
|
|
382
|
+
* The headline `<total>` is the TRUE wall-clock since process start — in Node,
|
|
383
|
+
* `performance.now()` is measured from `performance.timeOrigin` (≈ process
|
|
384
|
+
* start), so it captures the entire bootstrap (module import, config load,
|
|
385
|
+
* ServerBootstrap, and `serve()`), not just the timed phases. The parenthetical
|
|
386
|
+
* lists each captured phase; `other` is the unattributed remainder (import +
|
|
387
|
+
* config + any untimed setup) shown only when it's non-trivial.
|
|
388
|
+
*/
|
|
272
389
|
private buildStartupLine(): string {
|
|
273
390
|
if (this.phaseTimings.length === 0) {
|
|
274
391
|
return '';
|
|
275
392
|
}
|
|
276
|
-
const totalMs =
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
393
|
+
const totalMs = performance.now();
|
|
394
|
+
const phaseSumMs = this.phaseTimings.reduce((sum, p) => sum + p.Ms, 0);
|
|
395
|
+
const otherMs = totalMs - phaseSumMs;
|
|
396
|
+
const parts = this.phaseTimings.map(
|
|
397
|
+
(p) => `${this.shortPhaseLabel(p.Label)} ${(p.Ms / 1000).toFixed(1)}s`,
|
|
398
|
+
);
|
|
399
|
+
// Surface unattributed boot time only when it's meaningful (>50ms), so the
|
|
400
|
+
// line stays clean when phases already account for ~all of startup.
|
|
401
|
+
if (otherMs > 50) {
|
|
402
|
+
parts.push(`other ${(otherMs / 1000).toFixed(1)}s`);
|
|
403
|
+
}
|
|
404
|
+
return ` Startup ${(totalMs / 1000).toFixed(1)}s (${parts.join(' · ')})`;
|
|
281
405
|
}
|
|
282
406
|
|
|
283
407
|
/**
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, Resolver } from 'type-graphql';
|
|
2
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
3
|
+
import { BaseRemotableOperation, RemoteOpInvokeMode } from '@memberjunction/core';
|
|
4
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
|
+
import { AppContext } from '../types.js';
|
|
6
|
+
import { GetReadWriteProvider } from '../util.js';
|
|
7
|
+
|
|
8
|
+
/** Input for the generic Remote Operation transport mutation. */
|
|
9
|
+
@InputType()
|
|
10
|
+
export class ExecuteRemoteOperationInput {
|
|
11
|
+
@Field()
|
|
12
|
+
operationKey: string;
|
|
13
|
+
|
|
14
|
+
@Field()
|
|
15
|
+
inputJSON: string;
|
|
16
|
+
|
|
17
|
+
@Field()
|
|
18
|
+
invokeMode: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Result of the generic Remote Operation transport mutation. */
|
|
22
|
+
@ObjectType()
|
|
23
|
+
export class ExecuteRemoteOperationResultType {
|
|
24
|
+
@Field(() => Boolean)
|
|
25
|
+
success: boolean;
|
|
26
|
+
|
|
27
|
+
@Field(() => String, { nullable: true })
|
|
28
|
+
resultCode?: string;
|
|
29
|
+
|
|
30
|
+
@Field(() => String, { nullable: true })
|
|
31
|
+
outputJSON?: string;
|
|
32
|
+
|
|
33
|
+
@Field(() => String, { nullable: true })
|
|
34
|
+
handle?: string;
|
|
35
|
+
|
|
36
|
+
@Field(() => String, { nullable: true })
|
|
37
|
+
errorMessage?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The single generic server entry point for Remote Operations. Resolves the operation by key,
|
|
42
|
+
* applies the framework authorization gates (API-key scope, system-user, then the operation's own
|
|
43
|
+
* `Authorize`), and routes the operation through the per-request provider — which executes it
|
|
44
|
+
* in-process. Only registered, active operations are reachable, so the public `RouteOperation`
|
|
45
|
+
* power-tool is safe by construction.
|
|
46
|
+
*/
|
|
47
|
+
@Resolver()
|
|
48
|
+
export class ExecuteRemoteOperationResolver extends ResolverBase {
|
|
49
|
+
@Mutation(() => ExecuteRemoteOperationResultType)
|
|
50
|
+
async ExecuteRemoteOperation(
|
|
51
|
+
@Arg('input') input: ExecuteRemoteOperationInput,
|
|
52
|
+
@Ctx() ctx: AppContext,
|
|
53
|
+
): Promise<ExecuteRemoteOperationResultType> {
|
|
54
|
+
try {
|
|
55
|
+
const key = input.operationKey?.trim();
|
|
56
|
+
if (!key) {
|
|
57
|
+
return fail('INVALID_OPERATION_KEY', 'operationKey is required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Resolve the operation's declaration so we can apply its scope / system-user gates.
|
|
61
|
+
const registration = MJGlobal.Instance.ClassFactory.GetRegistration(BaseRemotableOperation, key);
|
|
62
|
+
if (!registration) {
|
|
63
|
+
return fail('UNKNOWN_OPERATION', `Remote operation '${key}' is not registered`);
|
|
64
|
+
}
|
|
65
|
+
const opDecl = MJGlobal.Instance.ClassFactory.CreateInstance<BaseRemotableOperation>(BaseRemotableOperation, key);
|
|
66
|
+
|
|
67
|
+
// (1) API-key scope gate — a no-op for interactive OAuth/JWT users.
|
|
68
|
+
if (opDecl?.RequiredScope) {
|
|
69
|
+
await this.CheckAPIKeyScopeAuthorization(opDecl.RequiredScope, key, ctx.userPayload);
|
|
70
|
+
}
|
|
71
|
+
// (2) System-user gate.
|
|
72
|
+
if (opDecl?.RequiresSystemUser && !ctx.userPayload.isSystemUser) {
|
|
73
|
+
return fail('FORBIDDEN', `Operation '${key}' may only be invoked by the system user`);
|
|
74
|
+
}
|
|
75
|
+
// (3) Resolve the acting user (works for OAuth/JWT and API-key sessions alike).
|
|
76
|
+
const user = this.GetUserFromPayload(ctx.userPayload);
|
|
77
|
+
if (!user) {
|
|
78
|
+
return fail('NO_USER', 'Not authenticated');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let parsedInput: unknown;
|
|
82
|
+
try {
|
|
83
|
+
parsedInput = input.inputJSON ? JSON.parse(input.inputJSON) : {};
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return fail('INVALID_INPUT_JSON', `Invalid input JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Route through the per-request provider (server in-process path; runs the op's Authorize + InternalExecute).
|
|
89
|
+
const provider = GetReadWriteProvider(ctx.providers);
|
|
90
|
+
const result = await provider.RouteOperation(key, parsedInput, { user, mode: input.invokeMode as RemoteOpInvokeMode });
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: result.Success,
|
|
94
|
+
resultCode: result.ResultCode,
|
|
95
|
+
outputJSON: result.Output !== undefined ? JSON.stringify(result.Output) : undefined,
|
|
96
|
+
handle: result.Handle,
|
|
97
|
+
errorMessage: result.ErrorMessage,
|
|
98
|
+
};
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return fail('ERROR', e instanceof Error ? e.message : String(e));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Builds a failed result. */
|
|
106
|
+
function fail(resultCode: string, errorMessage: string): ExecuteRemoteOperationResultType {
|
|
107
|
+
return { success: false, resultCode, errorMessage };
|
|
108
|
+
}
|