# sfcrm AppService

This module is the independently runnable L1 owner of the migrated CRM
record-access, record-view-read, query, mutation, related-list-count,
recycle-bin-read, effective-permission, search-scope-order, and
notification-read HTTP projections. Notification reads reuse the existing
neutral query capability;
the L1 service supplies only CRM route and response semantics while Runtime
resolves the current principal from trusted request context. The production
command composes the capabilities declared by the latest immutable CRM
AppRelease and installs their matching Runtime ports. CRM AppRelease `0.14.0`
preserves the effective-permission and scope-order behavior from `0.13.0`
and moves the launch global/lookup Search routes onto the private Runtime
Search/Query capabilities. Search object/field shape comes only from the
effective immutable AppMetadata view; Salesforce labels, URLs, and response
grammar remain L1-owned projections of that same revision. The prepared 0.74.0
release consolidates scope-order under Search and reads installed L1/L2 scope
from V3; see [installed search scope](../../docs/search-scope-installed-metadata.md)
for the paired publication requirements.
The production command installs all three Runtime Files ports together and
installs the permission, metadata, scope-order, and global/lookup Search ports
as one fail-closed composition.
Salesforce-specific request and response semantics remain in this L1 module.
The record-view family owns only `GET /sobjects/:object/:id` and
`GET /sobjects/:object/:field/:value`: one neutral read plus at most one
Catalog-bound attachment-source resolution call. CRUD/RLS/FLS, compound-field
visibility, lookup authorization, metadata revision, and attachment access are
enforced by Runtime; this module adds only CRM URL, error, relationship, and
file-link response grammar.

The module deliberately does not own HTTP process lifecycle, Runtime TLS,
workload authentication, IdentityContext exchange, delegation, or gRPC policy.
Those app-blind mechanisms come from the released `nexuscrm-api-host` module.
The serving process depends on the neutral API and declared capabilities only;
it never selects or validates a Core, Runtime, repository, or Go module release
version. When an AppRelease does not declare an explicit API version, no extra
version gate is inferred and the neutral API's baseline compatibility contract
applies. Every neutral Runtime capability actually consumed by a handler is
still mandatory in that handler's AppRelease declaration.
Adding a handler here does not switch route ownership. L2 may activate a route
family only after the exact released AppService and Runtime images
pass the real-TiDB parity, isolation, failure, and no-fallback test2 gate.

## Query response limits

The Runtime query contract owns the encoded response budget; the transport
module separately owns the gRPC message limit. Increasing a semantic budget
does not increase transport capacity. The query response-budget change from
1 MiB to 4 MiB fits the existing 4 MiB protobuf envelope and leaves the
2,000-record batch bound unchanged. Its local acceptance test uses the real
Runtime query client and uncompressed gRPC before the public HTTP projection,
covering a seven-column 2,000-row page, an exact-boundary response, and a
one-byte overflow. The database producer is a deterministic fixture.

Query response-size failures return HTTP 400 / `LIMIT_EXCEEDED`, using the
serving peer's typed limit and maximum in the existing error array. A byte-limit
failure says that the encoded response is too large and suggests fewer fields
or a smaller `Sforce-Query-Options` batch. History instead suggests a smaller
SOQL `LIMIT`, since its current route does not read that header. Concurrent
admission exhaustion retains HTTP 429 and tells the caller to retry later; it is not reported as a
payload-size failure. Internal causes and query text are never disclosed.

Salesforce query batch sizes range from 200 to 2,000 and are not guaranteed to
equal the number actually returned:
<https://developer.salesforce.com/docs/platform/mobile-sdk/guide/ref-rest-apis-soql-query.html>.
The encoded byte ceiling is a NexusCRM Runtime bound, not a Salesforce limit.

The larger budget requires published semantic/wire dependency updates on both
Runtime and AppService. Upgrade query readers first, then producers; old
readers still reject responses above 1 MiB. Until those releases are pinned,
the source-level error improvement works with the peer's existing maximum.

The existing grouped-query overflow probe still requests one extra group.
Runtime admission and aggregate-response validation allow 2,001 rows so the
unchanged public codec can detect overflow and return `EXCEEDED_ID_LIMIT`.
An explicit `LIMIT 2000` still returns only the requested 2,000 groups. This
does not enlarge ordinary pages or add grouped query continuation support.

## REST discovery and query URL compatibility

`GET /services/data` and `/services/data/` are public version-discovery
endpoints. Their `version`, `label`, and `url` rows preserve the legacy
Salesforce API version labels, using the query codec's supported versions.

`GET /services/data/{version}` (with or without a trailing slash) validates
the Runtime session and returns the migrated legacy resource document,
including its resource-family links and the authenticated user's identity URL.
This preserves the old document shape; a listed family is not proof that every
legacy operation in that family has been migrated to the standalone service.
The identity URL uses the configured public origin and organization identity.

`GET /services/data/{version}/limits` (with or without a trailing slash)
validates the Runtime session and returns the legacy `Max`/`Remaining` rows. These are compatibility values, not live quota measurements.
For example, `DailyApiRequests` is `{ "Max": 100000, "Remaining": 99000 }`.
Golden fixtures are copied from the legacy dialect's default documents.

Both authenticated documents request an explicit document-policy projection
through the neutral Runtime session-read contract. Runtime evaluates current
custom-scope ownership and route coverage through the existing scope registry,
and derives configured hourly/daily limits through the existing rate-subject
rule (Connected App or personal access token). AppService applies the resulting
allowed-link set and quota overrides to the migrated documents. Identity route
constraints remain L1-owned. No token claims are decoded in AppService.
A Runtime without this projection returns a service-unavailable response; a
missing decision never means unrestricted visibility. Deploy the matching
Runtime before this AppService. This policy describes the document only;
operation authorization remains with the corresponding Runtime operation.

The legacy dialect resource-directory and limits handlers, their registrations,
and their handler-only tests have been removed. AppService owns these documents.

`GET /query/` and `/queryAll/` return the legacy Gin-style `301` redirect to
the canonical path without the slash. The Location retains the original raw
query string. Redirects occur before session validation, as in Gin; following
the redirect executes the unchanged query handler and its authentication.
Only these two collection aliases are added. Invalid versions and invalid
pagination paths remain rejected. The outer HTTP server or ingress may
normalize repeated slashes before the AppService handler sees the path. The
released query codec and its pins are unchanged.

Validation: `GOWORK=off go test ./...` from this module. Tests cover all
supported versions, golden default resource/limits documents, invalid sessions,
bounded session failures, redirect status and Location, query result parity,
deleted-record visibility, nextRecordsUrl pagination, restricted and empty
resource documents, Runtime quota overrides, and missing-policy failures.
These tests do not claim production rollout.

## Related-list editor candidate ownership

`GET /services/data/{version}/ui-api/related-list-candidates/{objectApiName}`
is the layout-editor extension owned by this AppService's existing metadata
projection handler. It has no legacy dialect registration or fallback. D2A
records it separately from the legacy manifest with activation pending until
the coordinated distribution walking slice passes.

The handler's already-declared neutral dependencies are AppMetadata V3,
effective permissions, and page-layout resolution (the live object-list-fields
operation). Catalog provides the effective object shape, installed presentation
rules and explicit relationship columns; Runtime provides current object list
fields and user permissions. L1 assembles recommendation buckets, labels,
ordering and column precedence. A matching rule with an omitted bucket defaults
to `recommended`; a relationship with no matching rule remains `additional`. It does not read Runtime tables or use the
legacy process-global candidate registry, and checks authority receipts across
the reads. A missing projection or failed Runtime read is an error, not an
empty candidate list. Candidates never implicitly populate a saved Layout.

Candidate eligibility follows the existing Layout save/read contract: the
display object and lookup field must exist, and the field's `ReferenceTo` must
include the parent. Relationship presentation and `appendIfMissing` rules do
not create schema. An inapplicable rule is ignored without failing unrelated
candidates. This includes optional Files rules when their object or relationship
is not installed; a valid installed relationship retains its L1 presentation.

A Resolution may redirect a real display relationship to another read target,
but its query, detail and list references must remain current. Candidate columns
must be direct fields accepted by both the display object's authoring schema
and the resolved query schema; dotted paths and query-only fields cannot be
saved as related-list columns. Runtime list defaults still use one deduplicated
batch. Layout/record-UI object and field authorization remains in its existing
owner and continues to filter lists the end user cannot read.

The old dialect candidate handler and its classification are removed here.
The paired Core change removes the direct backend route, metadata candidate
interface and structural seeding call. Controlplane's installation/backfill
rule consumers are distinct from this serving path and are retained.

## D3 OAuth client-credentials parity boundary

`POST /services/oauth2/token` is an L1 Salesforce-wire projection, not a
second OAuth or Connected App authority. `client_credentials` is a restored D3
parity projection through `runtime.authorization.lifecycle.v1`, not a new side
route: L1 parses the Salesforce-compatible form, puts the client secret only in
the private Runtime credential channel, and renders the access-token response.
It never turns the request into a password grant, proxies to the legacy
backend, decodes the returned bearer, or inspects Connected App records.

The Runtime owner atomically authenticates a confidential Connected App,
requires its allowed grant and configured Run As User, rechecks active app/user
state plus requested scope and access policy, and issues the Run As user's
durable app-bound session without a browser control proof or refresh token. If
HTTP credential publication fails after issuance, the AppService invokes the
idempotent neutral Runtime revoke path with bounded retries and records only a
sanitized failure category if Runtime remains unavailable. A credential is
never added to a later error response or log. Grant-specific response
validation in the released Runtime client rejects refresh-token issuance and
malformed successes before they can become an L1 token response. Browser
session-control remains exclusive to browser requests and its admission/cookie
contract is unchanged.

Salesforce wire parity also preserves the legacy client-credentials error
split: a Connected App access-policy `unauthorized_client` denial is rendered
as HTTP 403 `access_denied`; invalid clients, disallowed grants, and invalid
scopes retain their distinct OAuth token-endpoint errors.

This repair does not widen source-backed Connected App authoring. The current
source-backed component contract still forbids `client_credentials` and
`runAsUserId`; admitting those pack-authored facts requires its separate owned
payload/apply/verify change. This route therefore consumes only a Connected App
already admitted by the durable L0 authority. The matching AppRelease must keep
the shared neutral session-lifecycle capability declared; the versioned
authorization and federated-login contracts deliberately alias that one gate.
Any deployed L0/L1
tuple must pass parity, rollback, isolation, and no-fallback gates before route
activation.

### Query field and calendar validation

Runtime selection-validation failures map to HTTP 400 `INVALID_FIELD` via the
existing typed invalid-component discriminator. Root, child and aggregate
field existence is owned by the core metadata/query engine, before projection
or storage; the AppService does not maintain a second field allowlist.

SOQL `TODAY`, `THIS_YEAR`, and `LAST_N_DAYS:n` are resolved in the shared L1
query codec. AppService reads `User.TimeZoneSidKey` with a Runtime query bound
to the trusted principal ID, as the acting user. It uses the existing effective
object metadata view for Date/DateTime and formula return types, requests that
catalog revision and validates execution receipts against it. Continuations
retain the original calendar anchor and skip the user preference reread.

Release the querycodec module, then pin the same version in AppService and both
dialect consumers. Deploy the core field-validation change together with the
L1 error projection to get `INVALID_FIELD` end to end. Tests cover the actual
Runtime client and protobuf/HTTP boundaries with a fixture replacing database
production; deployment acceptance must rerun the saved test2 queries.

Query dependency errors are classified at each execution adapter before the
shared codec projects the public response. Unknown errors are internal failures,
never evidence of invalid user input. The transitional dialect preserves context
cancellation/deadline and explicit HTTP status categories without exposing raw
error text; only its structured domain-wire contract supplies public detail.
Regression coverage spans query/queryAll/queryMore, calendar reads, root and
relationship metadata, semijoins, and record execution. AppService tests exercise
the production gRPC client as well as metadata and receipt failures.
