ProtectedAfterProtectedAfterProtectedAfterProtectedapplyApplies an inbound RestoreContext___ blob to a server-side BaseEntity.
Mirrors the OldValues___ pattern — the client-side BaseEntity's
_restoreContext doesn't traverse the network, so the server must
reconstruct it from the mutation input before calling Save().
Returns true when context was applied; false when no context was on the input.
ProtectedArrayFilters encrypted fields for an array of data objects. fieldKey is as for
FilterEncryptedFieldsForAPI.
Optionalprovider: IMetadataProviderProtectedArrayArray form of MapFieldNamesToCodeNames. Returns a NEW array of NEW objects; neither the input array nor its rows are modified. Both matter: a reference-sharing cache hands back the stored array itself, not a copy of it, so collecting the mapped copies (rather than mapping in place and returning the original) is what makes this safe on cache-served input.
OptionalcontextUser: UserInfoOptionalprovider: IMetadataProviderProtectedassertSECURITY — GraphQL-boundary screen for one client-supplied SQL clause fragment.
ValidateUserProvidedSQLClause still blocks stacked statements, DML, comments, UNION
and WAITFOR, and still permits SELECT (server-internal engines pass richer filters).
A keyword ban on SELECT/EXISTS at this boundary (#4253) broke first-party clients that
use IN (SELECT … FROM <entity base view>) — that is a legitimate ExtraFilter.
This screen uses @memberjunction/sql-parser (same wrap as EDS assertReadOnlyClause):
wrap the fragment as a single SELECT, fail closed if it does not parse as a read, then
allow a FROM only when it is an entity BaseView. Base tables (Meeting, __mj.User)
and catalogs are rejected. Server-internal RunView callers get only the provider's
statement-separator check, which cannot refuse a legitimate filter.
The screen is ClientClauseScreen in @memberjunction/generic-database-provider, shared
with the REST view routes, ad-hoc list sources, and the provider's saved-view checks.
Before that parse, the fragment must be balanced on its own (IsBalancedSQLFragment). The
provider and the tenant hook splice it inside parentheses and AND their own filters after
it, so a fragment that closes those parentheses (1=1) OR (1=1) would leave row-level
security and tenant scope on one side of an OR the caller controls.
The parse wraps the fragment in parentheses too, which is why it cannot catch this alone.
A backslash inside a quoted string or identifier is refused as well: the parser reads \'
inside a plain literal as an escaped quote, but SQL Server and PostgreSQL end the literal
there, so the text after it could be code that the parse-based checks never saw.
RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled
into the view, and RunView removes denied fields from its own result only. A subquery reads a
base view directly with neither, so the screen refuses one over any entity the user reads under
a row filter or with denied fields (ClientClauseScreen.AssertUnscopedRead), the queried
entity included.
Optionalprovider: IMetadataProviderOptionaluser: UserInfoOptionaloptions: { LeaveUnparseableToProvider?: boolean }ProtectedassertSECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
to the SELECT-only validation the caller performs, every table reference must resolve to an
entity BaseView the acting user can read UNSCOPED. A reference is excluded only when it reads a
CTE that is in scope there (StatementRelation.IsCTEReference): unqualified, declared by its
own SELECT or an enclosing one, and matched by the dialect's identifier rules. A name reserved
for the SQL parser is refused before parsing (ClientClauseScreen.AssertNoIdentifierAliasNames).
Fails closed: no user, or an unresolvable reference, refuses.
The references come from a walk of the whole parse tree
(ClientClauseScreen.CollectStatementReads). The statement is refused when that walk does not
account for every SELECT in the text, or when a FROM item names no table (a table function or
a row list).
Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with row-level security, and raw SQL does not apply it. Rather than try to rewrite arbitrary SQL (joins, CTEs) to compose that policy, this refuses any entity that carries a row filter for the caller — those reads belong on RunView. For the remaining entities CanRead genuinely means "every row and field of the view".
ProtectedBeforeProtectedBeforeProtectedBeforeProtectedCheckChecks API key scope authorization. Only performs check if request was authenticated via API key (apiKeyHash present in userPayload). For OAuth/JWT auth, this is a no-op.
The scope path (e.g., 'entity:read', 'agent:execute')
The resource name (e.g., entity name, agent name)
The user payload from context
ProtectedCheckOptionalprovider: IMetadataProviderProtectedcreateProtectedCreateProtectedcreateProtectedDeleteProtectedEmitExecute ONE browser action relayed from the client-direct realtime session, returning the outcome + resulting URL.
Flow:
UserID must equal the caller's (throws otherwise).TypeConfiguration ({ remoteBrowser: { provider } }), else
the single Active provider.kind + the supplied fields.The AIAgentSession id the browser is bound to.
The action kind ('navigate' | 'click' | 'type' | 'key' | 'scroll' | 'back' | 'forward' | 'wait').
Optionalurl: stringOptionalselector: stringOptionalx: numberOptionaly: numberOptionaltext: stringOptionalkey: stringOptionaldeltaX: numberOptionaldeltaY: numberOptionalms: numberThe action result (success + resulting URL + detail).
Execute a high-level GOAL against the session's browser — the agent sets an intent ("log in and open the latest invoice") and the resolved control strategy (computer-use loop or backend native AI) plans
OnProgress callback).The AIAgentSession id the browser is bound to.
The natural-language goal.
OptionalstartUrl: stringOptionalmaxSteps: numberOptionalpreferredStrategy: stringThe goal outcome (success, strategy, status, step count, url, detail).
ProtectedFilterFilters encrypted field values before sending to the API client.
For each encrypted field in the entity:
Name of the entity
The data object containing field values
User context for encryption operations
Optionalprovider: IMetadataProviderWhat the object's keys are: field CodeNames (GraphQL transport rows, the default) or field Names (raw provider rows, such as dataset item results)
The filtered data object
ProtectedfindPoll the outcome of a goal STARTED by RemoteBrowserActionResolver.ExecuteRemoteBrowserGoal.
Returns Status: 'Running' while the loop is in flight, then the terminal outcome (success,
strategy, status, step count, url, detail) once it finishes. Ownership-gated. A Status: 'Unknown'
result means the run id is unrecognized — it expired (results are retained briefly) or never existed.
The AIAgentSession id the goal runs against.
The handle returned by ExecuteRemoteBrowserGoal.
The current/terminal goal outcome.
Returns the remote page's CURRENT text selection — the copy-out half of human clipboard support. The
viewer captures a local copy / Cmd+C, calls this to read what the human selected on the live page, and
writes the result to the LOCAL clipboard (sidestepping the isolated remote clipboard, the mirror of the
'text' paste-in path). Ownership-gated.
Gracefully best-effort, never throws past the ownership gate (mirrors RemoteBrowserSnapshot):
{ Text: '' }.HumanTakeover (RemoteBrowserCapabilityNotSupportedError) → { Text: '' }.{ Text: '' } (logged).The AIAgentSession id the browser is bound to.
The selection text, or { Text: '' } when none is readable.
ProtectedgetProtectedGetProtectedGetVISION-QUERY path (separate from the navigate/click RemoteBrowserActionResolver.ExecuteRemoteBrowserAction
path — this OBSERVES the page, it never drives it). Captures the session's current screenshot and runs
a fast vision AI Prompt over it so a non-omnimodal voice agent (audio/text-only) can effectively SEE the
page: a concise text description ("kinda see") and, when query names a target, the pixel centroid(s) the
agent can then feed to browser_Click(x, y).
Flow:
UserID must equal the caller's (throws otherwise).CaptureScreenshot(). No live session / no screenshot → { Description: null, Elements: [], Detail: 'no live browser' }.Remote Browser Visual Interpreter prompt with the screenshot as an image_url content block
plus an instruction derived from query (empty/"describe" → describe; a named target → locate it).Best-effort + tolerant by contract: any vision/prompt/parse failure is logged and returned as
{ Description: null, Elements: [], Detail: <message> }. This mutation NEVER throws past the ownership gate.
The AIAgentSession id the browser is bound to.
Optionalquery: stringOptional request — empty/"describe" for a page description, else a visual target to localize.
The interpretation (description + localized elements + optional detail note).
ProtectedListenProtectedLoadLoads a single external-data-source-backed entity record by primary key and returns it in GraphQL field-name (CodeName) shape, or null if not found.
External entities (Entity.ExternalDataSourceID set) have no MJ base view or sproc — their
data is proxied live from a remote system — so the generated single-record resolver cannot run
SELECT * FROM <baseView>. Instead it loads through a BaseEntity object, whose InnerLoad
the data provider dispatches to the external read router's LoadExternalRecord (a composite-key
aware, quoted, parameter-bound single-record lookup), applying the same RLS gate and field
post-processing (decryption / datetime normalization) as the MJ-DB path. The caller is
responsible for the CheckUserReadPermissions gate beforehand.
ProtectedloadLoads the agent session and enforces inbound ownership: the session's UserID must equal the
caller's. Throws when the session is missing or owned by another user.
The AIAgentSession id.
The loaded, owned session entity.
ProtectedMapMaps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
For encrypted fields coming from raw SQL queries (not entity objects):
Returns a COPY — dataObject is never written to. Callers routinely pass rows straight
from findBy/RunView, which are the server cache's own objects held by reference under a
reference-sharing storage provider. Renaming in place therefore rewrote the cached row's
keys, so later readers were served transport-shaped rows that BaseEntity.SetMany rejects —
and because that cache is process-wide, one response corrupted every subsequent request
across all workers. It bit every UserByEmail / UserByID / UserByEmployeeID call and
every generated single-record resolver whose entity has caching enabled. Copying here fixes
every call site at once and makes the hazard unreachable for future ones.
The entity name
The data object with field values. Not modified.
OptionalcontextUser: UserInfoOptional user context for decryption (required for encrypted fields)
Optionalprovider: IMetadataProviderA new object in transport shape, or null when there is nothing to map
ProtectedpackageProtectedPublishPublishes a CACHE_INVALIDATION event to connected browser clients after a successful entity save or delete. Includes the originSessionId so the originating browser can skip redundant re-fetches (it already handled the event locally).
The row itself rides along ONLY for entities opted in via
cacheSettings.recordDataBroadcastEntities — this event reaches every connected client
unfiltered, so the row would otherwise be readable by sessions that could not read the record.
ProtectedPublishPublishes a push-status update to the client on PUSH_STATUS_UPDATES_TOPIC, stamping the
authenticated owner's user ID from userPayload so the subscription filter can bind delivery
to identity (see B49 / statusUpdatesFilter). The ergonomic wrapper every resolver should use
instead of calling pubSub.publish on the topic directly — it makes omitting identity
impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared
publishStatusUpdate() function directly with an explicit ownerUserId.
Relays ONE human-takeover input (pointer move/click or key press) from the user watching the live screencast into the session's server-hosted browser — the "grab the wheel" path. Ownership-gated. The surface captures the event on the live-view canvas, maps display→viewport coordinates, and calls this; the resolver builds a strongly-typed RemoteBrowserHumanInput and routes it over CDP via IRemoteBrowserSession.RouteHumanInput.
Gracefully best-effort, never throws past the ownership gate:
false.buildHumanInput returns null) → false.HumanTakeover (RemoteBrowserCapabilityNotSupportedError) → false (the
live view stays view-only for that backend).false.NOTE: this prototype does NOT reject by control mode here — the engine's floor arbiter governs the
agent⇄human floor, so we just relay. Finer floor / AgentOnly gating is a follow-up.
The AIAgentSession id the browser is bound to.
The input kind ('pointer-move' | 'pointer-click' | 'pointer-down' | 'pointer-up' | 'key' | 'text' | 'scroll').
Optionalx: numberOptionaly: numberOptionalbutton: stringOptionalkey: stringOptionaltext: stringThe pasted text (the 'text' paste-in kind only) — inserted into the page's focused element.
OptionaldeltaX: numberOptionaldeltaY: numberOptionalmodifiers: stringOptional comma-separated modifier keys held during the input ('Shift', 'Control',
'Alt', 'Meta') — carries Shift-click selection and Ctrl/Cmd+key chords faithfully.
true when the input was routed, else false.
Return the current viewport screenshot + URL for the session's live browser — the client's live view. Ownership-gated. When the session holds no live browser (never started, or already torn down), the result's fields are null rather than an error.
The AIAgentSession id.
The current screenshot + URL, or an empty snapshot when no live browser exists.
ProtectedrequireResolve the request user + read-write provider, throwing a clear error if unauthenticated.
ProtectedresolveResolves the remote-browser backend name for the session from the session's agent's
TypeConfiguration JSON ({ remoteBrowser: { provider } }). Best-effort: a missing agent, absent
config, or unparseable JSON all yield undefined, letting the engine fall back to the single Active
provider.
The owned session entity (supplies the agent id).
The configured backend name, or undefined to let the engine auto-select.
ProtectedReverseReverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object.
For example, _mj__integration_SyncStatus is mapped back to __mj_integration_SyncStatus.
Also reverse-maps keys inside the OldValues___ array if present.
This is the inverse of MapFieldNamesToCodeNames and must be called before passing
GraphQL input to entity SetMany() or field lookups.
ProtectedRunOptimized RunViewGenericInternal implementation with:
Optionalaggregates: AggregateExpression[]OptionalafterKey: CompositeKeyOptionalbypassCache: booleanProtectedRunOptimized implementation that:
ProtectedsafeProtectedscreenApplies assertClientClauseUsesEntityBaseViews to every client-supplied clause a view request can carry that is actually a SQL fragment. GraphQL entry points (RunViewByName, RunViewByID, RunDynamicView, RunViews) funnel through RunViewGenericInternal / RunViewsGenericInternal.
🚨 UserSearchString is deliberately NOT screened here (#4392). It is not a clause — it is
the free text a person typed into a search box, and it never reaches SQL as a fragment.
GenericDatabaseProvider.createViewUserSearchSQL builds the predicate itself from
IncludeInUserSearchAPI metadata and lands the text only as a literal, doubling single
quotes and escaping LIKE metacharacters with an explicit ESCAPE. Running it through a
screen that parses its argument as SQL and fails closed rejected every term that is not
coincidentally valid SQL: Marcus Chen parses as nothing, O'Leary as an unterminated
literal — so essentially every real name search returned 0 rows.
The provider-level denylist does NOT back this up — it was removed from UserSearchString
in the same change, for the same reason (it refused Union Pacific). What protects the value
is that it is never SQL: it reaches the database only as a quoted, quote-doubled literal.
The single place that is not true is a field carrying UserSearchParamFormatAPI, whose
admin-authored format may splice the term in unquoted; createViewUserSearchSQL re-applies
ValidateUserProvidedSQLClause for exactly those entities.
Each Aggregates[].expression gets the same screen, except that text the parser cannot read
goes on to the provider, which refuses it per aggregate. The provider never runs the caller's
aggregate text: it runs the SQL that CheckAggregateExpression rebuilds from a single
aggregate call over the entity's columns, and refuses everything else per aggregate.
Optionalprovider: IMetadataProviderOptionaluser: UserInfoStarts streaming the session browser's TAB AUDIO and PUSHES each encoded chunk to the calling user's
push-status topic — so a co-agent demoing a video/audio site is HEARD, not just seen. Ownership-gated.
Idempotent: a re-call for an already-streaming session is a no-op that reports Streaming: true.
Capability gating (v1 = by backend implementation): IRemoteBrowserSession.StartAudioStream
throws RemoteBrowserCapabilityNotSupportedError on a backend with no audio-capture mechanism —
caught here and reported as Streaming: false (the client simply plays no audio). Any other failure is
logged and likewise reported as Streaming: false.
The AIAgentSession id the browser is bound to.
{ Streaming: true } when audio chunks are now being pushed, else { Streaming: false }.
Starts a live CDP screencast on the session's browser and PUSHES each encoded frame to the calling
user's push-status topic — replacing the client's 700ms snapshot poll with low-latency pushed frames.
Ownership-gated. Idempotent: a re-call for an already-streaming session is a no-op that reports
Streaming: true.
Capability gating: IRemoteBrowserSession.StartScreencast throws
RemoteBrowserCapabilityNotSupportedError on a backend without ScreenStreaming — caught here
and reported as Streaming: false, leaving the client on its polling fallback. Any other failure is
logged and likewise reported as Streaming: false (the poll keeps the view alive).
The AIAgentSession id the browser is bound to.
{ Streaming: true } when frames are now being pushed, else { Streaming: false }.
Stops a tab-audio stream previously started by RemoteBrowserActionResolver.StartRemoteBrowserAudioStream.
Ownership-gated and best-effort: when no live browser exists, or StopAudioStream rejects, the call
still resolves true (the client's teardown should never depend on this succeeding).
The AIAgentSession id.
true (always) once the stop has been attempted.
Stops a screencast previously started by RemoteBrowserActionResolver.StartRemoteBrowserScreencast.
Ownership-gated and best-effort: when no live browser exists, or StopScreencast rejects, the call
still resolves true (the client's teardown should never depend on this succeeding).
The AIAgentSession id.
true (always) once the stop has been attempted.
ProtectedTestThis routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.
ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
ProtectedUpdate
Resolver for the Remote Browser native realtime channel. A single RemoteBrowserEngine instance (the process-wide singleton) backs every request; ownership is enforced per call against the session's
UserID.