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
Optionalprovider: IMetadataProviderProtectedArrayArray form of MapFieldNamesToCodeNames. Returns a NEW array of NEW objects; neither the input array nor its rows are modified. Both matter: the cache freezes the array as well as the rows it contains, 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 never hit this.
RLS is applied by RunView as an outer WHERE around the entity being queried, not compiled into the view. Subqueries against another entity's BaseView therefore do not inherit that entity's RLS; they are still restricted to the view (not the table).
Optionalprovider: IMetadataProviderProtectedBeforeProtectedBeforeProtectedBeforeProtectedBuildThe CodeNames this user may read on this entity, for the field-security transport key — or null when there is nothing to state (no denials, so every field is readable and the client needs no help distinguishing a withheld null from a genuine one).
Carries entity field NAMES, not CodeNames and not the _mj__ transport shape. The client
consumes this after it has already reversed the transport mapping, and it matches names
against EntityFieldInfo.Name — the same key the denied set itself is built from. Sending
the one shape both sides already agree on avoids a second mapping that could drift.
Cancel in-flight relayed tool delegations for a session — the CLIENT-DIRECT cancel channel.
The browser calls this on an EXPLICIT user cancel (the call overlay's per-card ✕ — a
deliberate host policy: true barge-in alone never aborts delegations, since the narration
design expects the user to keep talking while delegated work runs). The service-side
in-flight registry aborts the matching AbortController(s), which fires the delegated
run's cancellationToken; the original ExecuteRealtimeSessionTool mutation then resolves
with the run's cancelled/failed result through its normal path.
Ownership-gated like the sibling mutations. A Closed session is accepted — a cancel can legitimately race teardown, and aborting stragglers is exactly what the caller wants then.
OptionalcallId: stringWhen supplied, only the delegation for that provider call id is aborted; omit to abort ALL in-flight delegations for the session.
A structured CancelRealtimeSessionToolResult. Tolerant: an unknown call id
or a session with nothing in flight is Success: true, AbortedCount: 0 (the work may
simply have finished already); an unexpected registry error is a structured failure.
ProtectedCheckChecks 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 a single tool call the browser relayed from its provider socket and return the serialized result for the browser to relay back to the model.
Ownership-gated; the target agent id is read from the session config (authoritative), never from the client. Heartbeats the session on success.
The serialized tool result JSON.
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: IMetadataProviderThe filtered data object
ProtectedfindProtectedgetProtectedGetProtectedGetProtectedListenProtectedLoadLoads 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.
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, and
LocalCacheManager deep-freezes them. Renaming in place therefore threw
Cannot add property _mj__CreatedAt, object is not extensible on every UserByEmail /
UserByID / UserByEmployeeID call and on every generated single-record resolver whose
entity has caching enabled. (Before the freeze it did something worse but quieter: it
rewrote the cached row's keys, so later readers were served transport-shaped rows that
BaseEntity.SetMany rejects.) 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: IMetadataProviderOptionaldeniedReadFields: Set<string>OptionalrecordChangeProjector: RecordChangeFieldSecurityProjectorA new object in transport shape, or null when there is nothing to map
ProtectedMustWhether UpdateRecord must hydrate the entity from the DATABASE rather than from the
client's OldValues___.
The OldValues___ path exists as an optimization: when nothing needs the true prior state,
the client already holds it and a round trip is wasted. Each condition below is a reason
that assumption fails.
EnableFieldLevelSecurity is the security-critical one, and it is NOT redundant with
hasDeniedReadFields. That flag reports READ denials, and the canonical FLS configuration is
Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
OldValues___, and a value it supplies there for an update-denied field arrives through
LoadFromData, which the EntityField setter records as that field's INITIAL value. The
field is therefore not dirty, BaseEntity.CheckFieldLevelUpdatePermissions only rejects
field.Dirty && denied, and GenerateSaveSQL sends every IsSPParameter field regardless of
dirtiness (it skips only NotLoaded). The fabricated value reached spUpdate having passed
every check: pinning a value in OldValues___ was a write to a field the caller may not write.
Forcing the truth-load closes that rather than relocating it. The entity is hydrated by
InnerLoad from the real row, and TestAndSetClientOldValuesToDBValues reads the client's
OldValues only to detect concurrent-edit overlap before ending in SetMany(clientNewValues) —
it never applies them to the entity. An update-denied field can then only become dirty by being
named in the mutation input itself, which is precisely the case that check does catch.
Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have the feature switched on, which is almost none of them.
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).
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 a co-agent CHANNEL tool-call (browser_ / Whiteboard_ etc.) onto the session's co-agent AIPromptRun.Messages and records the tool execution facts as a hidden ConversationDetail turn. HiddenToUser=true ensures chat components stay speech-only while allowing session review to reconstruct action cards dynamically on load (Option 1). Ownership-gated; best-effort (a missing prompt run / save failure simply returns false).
OptionalargsJson: stringOptionalresultJson: stringtrue when the tool turn was recorded on the run.
Persist a transcript turn (user or assistant) as a Conversation Detail on the session's
conversation. Ownership-gated; heartbeats the session on success.
The co-agent observability AIAgentRun/AIPromptRun are created at session start (see
RealtimeClientSessionResolver.StartRealtimeClientSession) and finalized on close;
incremental usage telemetry lands on the AIPromptRun via
RealtimeClientSessionResolver.RelayRealtimeUsage. Each persisted turn is ALSO appended
to the co-agent AIPromptRun.Messages (best-effort), so the run captures the full conversation
— observability parity with every other MJ agent run, not just token totals.
OptionalreplacesPrevious: booleanOptionalutteranceStartMs: numberOptionalutteranceEndMs: numbertrue when the transcript turn was persisted.
Relay incremental usage telemetry from the browser's provider socket onto the co-agent's
observability AIPromptRun (the run created at session start).
The browser accumulates the realtime client's OnUsage token DELTAS and flushes them here
debounced (~10s) plus once at teardown; this mutation ACCUMULATES the deltas onto the
prompt run's TokensPrompt / TokensCompletion (and recomputes TokensUsed). Status is
deliberately untouched — FinalizeCoAgentRun keeps stamping Success/Completed at close,
and a post-close flush still lands (accumulation has no Status gate).
Ownership-gated like the sibling mutations; a Closed session is accepted (the final
teardown flush can land after CloseAgentSession). Everything PAST the ownership gate is
best-effort and tolerant: a missing/malformed session config, an absent promptRunID
(observability creation was skipped), a failed load, or a failed save all log and return
false — usage relay must never break a live call.
Input-token DELTA to add (negative/non-finite values are clamped to 0).
Output-token DELTA to add (negative/non-finite values are clamped to 0).
true when the accumulated usage was persisted; false on any tolerated failure.
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: booleanOptionaldataSource: "Live" | "Materialized"ProtectedRunOptimized implementation that:
ProtectedsafePersist an interactive channel's current state (e.g. the live whiteboard's board JSON) as a durable, user-owned artifact — distinct from SaveSessionChannelState, which only stores the session-scoped state of record. The artifact survives the session and shows up in the user's artifact library (and, when possible, in the conversation's history).
Flow:
UserID === contextUser.ID). A Closed
session is accepted: "save my board" legitimately arrives as/after the call ends.contentJson beyond MAX_CHANNEL_STATE_CHARS is a structured failure.MJ: Artifacts header (user-owned, Visibility Always) + its
MJ: Artifact Versions v1 row carrying contentJson.Conversation Detail stamped with this
session id exists (the transcript relay stamps AgentSessionID), link the version into
history via a MJ: Conversation Detail Artifacts junction row (Direction Output);LastActiveAt on the session-channel row when one exists.A structured SaveSessionChannelArtifactResult — graceful failures (missing
type seed, oversized content, save failure) come back as Success: false, while
authn/ownership violations throw like the sibling mutations.
Persist an interactive channel's state of record (e.g. the live whiteboard's serialized
scene) onto the session's MJ: AI Agent Session Channels row.
Flow:
UserID must equal the
caller's. Unlike Execute/Relay, a Closed session is accepted: the client's final
on-end flush legitimately lands after CloseAgentSession has run.MJ: AI Agent Channels) by channelName. When no
active definition row exists (the deployment hasn't synced the channel seed yet),
return false gracefully and log — never throw for a missing definition.ServerPluginClass, held per-session by RealtimeChannelServerHost) for
validation/normalization — strictly best-effort: no plugin, a plugin failure, or an
oversized replacement all fall back to persisting the original payload.Connected) when missing, store
the (possibly normalized) state in its Config field, and stamp LastActiveAt.true when the state was persisted; false on any tolerated failure (missing
channel definition, oversized state, save failure) — all logged.
ProtectedscreenApplies 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.
Optionalprovider: IMetadataProviderStart a client-direct realtime voice session targeting targetAgentId.
Flow:
CanRun on the target agent; denial throws and no
session is created.coAgentId →
agent's DefaultCoAgentID → type-level AIAgentCoAgent default row → global Realtime Co-Agent) —
see RealtimeClientSessionResolver.resolveCoAgentID for the full contract.AIAgentSession (run by the co-agent), storing targetAgentID in its
config server-side — this is the authoritative target for all later relays.SECURITY NOTE — clientToolsJson: these are CLIENT-EXECUTED UI tools (e.g. the live
whiteboard's Whiteboard.* surface). The server only declares them to the realtime model
so it can call them — the server NEVER executes them, and a relayed call for one of these
names falls into the standard "not available" path on the server side. They grant no
server-side capability whatsoever; server-executed tools remain exclusively server-declared
(invoke-target-agent + future action wiring). The declarations are still validated
(count cap, size cap, per-tool shape) so a hostile client can't bloat the session config.
The agent the co-agent voices. OPTIONAL when the resolved co-agent
has pairing rows with an IsDefault target (MJ: AI Agent Co Agents) — the default
pairing stands in. Required for a universal co-agent (zero pairing rows). When the
co-agent HAS pairing rows, a supplied target must be in that list (clear error otherwise).
OptionalconversationId: stringOptionallastSessionId: stringOptionalpreferredModelId: stringOptionalclientToolsJson: stringOptionalcoAgentId: stringOptional EXPLICIT co-agent choice (MJ: AI Agents.ID of an Active,
Realtime-type agent). When set, the server uses exactly that co-agent and FAILS with a
clear reason if it can't (no silent fallback — mirroring preferredModelId's contract).
Omit to let metadata drive the choice: the target agent's DefaultCoAgentID, then the
type-level AIAgentCoAgent default row for its agent type, then the global Realtime Co-Agent.
OptionalconfigOverridesJson: stringOptional RUNTIME configuration-override layer (the most-specific
layer of the effective-config merge: type DefaultConfiguration ← agent
TypeConfiguration ← this). Authorization-gated: requires the
Realtime: Advanced Session Controls authorization — unauthorized callers receive a
structured rejection (never a silent ignore). Must be a JSON object.
OptionalrecordingStartedAt: stringOptionalrecordingConsent: booleanOptionalmediaCollectionId: stringOptionalapplicationId: stringOptionalappContextJson: stringThe ephemeral config + session linkage the browser needs to open its socket.
ProtectedStripField-level security guard for the update path.
Every denied-read field is stripped — new values AND OldValues. A field the user cannot read was absent from every payload that client ever received, so any value coming back for it is the transport's invention rather than user intent, and applying it would silently overwrite the real column. Stripping is what makes "load a record, edit an unrelated field, save" safe for a restricted user.
There is no split by update permission: Read is required for Update, so a user denied read is denied update too and denied-read ∩ denied-update is just denied-read.
Silent narrowing, not rejection — consistent with the output projection the client already experiences, and with the ambiguous-error rule (naming the field would confirm it exists and is restricted).
Returns true when the user has a non-empty denied-read set on this entity. The caller must then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold true values that an omitted key leaves untouched.
ProtectedStripRefuses a client-sent MJ: Record Changes payload column from a caller who carries field
denials, and forces the update onto the load-truth-from-DB branch.
This is the write half of the audit-trail projection, and without it that projection would
itself destroy audit history. A restricted caller is served a NARROWED ChangesJSON /
FullRecordJSON and no ChangesDescription. Those values hydrate a client-side entity as
ordinary loaded values, and GenerateSaveSQL writes EVERY IsSPParameter field rather than
only dirty ones — so a user who edits Comments on that record and saves would silently
overwrite the stored payload with the narrowed one they were shown. The row would keep looking
like a complete audit entry while the pruned fields were gone for everyone, permanently.
The rule is the one field security already applies elsewhere: a value a client could not have seen in full is not user intent, it is a transport artifact, and it is ignored. Here that is widened from "fields the caller cannot read" to "the audit payload of a caller who carries any denial", because the column is readable — it is its CONTENTS that were narrowed, and which row's target entity did the narrowing is not knowable from the input alone.
Returning true routes the caller through InnerLoad, so the stored values are restored from
the database before the save. Legitimate payload writes are unaffected: they happen
server-side (SnapshotBuilder, replay) and never through this resolver.
Optionalprovider: IMetadataProviderProtectedTestThis 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.
ProtectedUpdateUpload a CLIENT-DIRECT session audio recording, store it in MJStorage, link it to the owning
AIAgentSession, and stamp the session's recording fields. The browser records locally during
the call and uploads the assembled blob (base64) once the session ends.
Hard gates (in order):
loadOwnedSession enforces UserID === contextUser.ID (throws on violation).consent !== true is a hard refusal: no audio is ever stored without it.RecordingStorageProviderID, else AttachmentStorageProviderID).Storage failures (and any unexpected throw) come back as Success: false with a reason — they
NEVER throw to the browser, mirroring the shared storeRealtimeRecording contract.
The session the recording belongs to (ownership-gated).
The base64-encoded recording bytes (client-direct: seekable WAV/PCM).
The audio MIME type (audio/wav for client-direct; legacy audio/webm/ogg/mp4).
OptionaldurationMs: numberOptional client-measured recording duration (ms) — informational.
Optionalconsent: booleanWhether the user consented to recording. MUST be true or the upload is refused.
Optionalpeaks: number[]Optional capture-time waveform peaks (max-abs per bucket, normalized 0..1). When
present, persisted as a peaks.json sidecar beside the recording for fast waveform rendering
without re-decoding the audio.
A structured UploadRealtimeRecordingResult with the created MJ: Files id.
Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
folder (realtime-recordings/<sessionId>/seg-NNNN.<ext>) as a raw storage object. Durability
insurance during a live call so a browser/tab death loses at most the last window; the shards are
deleted once the canonical consolidated file lands via UploadRealtimeRecording. Ownership-
gated; consent was already established at session start. Best-effort — returns false (never
throws) on any problem so a failed shard never disrupts the live call.
The in-progress session (ownership-gated).
0-based shard index within the session.
The base64-encoded shard bytes.
The audio MIME type.
true when the shard was stored.
Resolver for the client-direct realtime voice topology. A single SessionManager and a single RealtimeClientSessionService are shared across requests — neither holds per-user or per-provider state; every method is passed the request
contextUser+ provider explicitly.