Resolves InboundAPI-026/027/028/029 (+ newly-surfaced -030). - 026: authorize the scoped Database helper in the design doc; SQL-injection protection is parameter binding (values never concatenated); allow writes via ExecuteAsync; drop the false 'read-only' claim. Named connections only. - 027: async ADO.NET end-to-end (no .GetAwaiter().GetResult()); honour the method deadline token on ExecuteScalarAsync/ExecuteReaderAsync/ExecuteNonQueryAsync + a CommandTimeout backstop derived from the method timeout. - 028: negative-path tests (null-gateway, deadline cancellation, parameterization) + e2e Database + WaitForAttribute cases through the real endpoint. - 029: WaitForAttribute is bounded by its WAIT timeout (per-wait CTS + client-abort + explicit token), NOT the method deadline (spec §6) — a long wait may outlive the method timeout; WithRequestAborted threads the raw client-abort token separately. - 030: Central UI compile-surface mirrors (InboundScriptHost / SandboxInboundScriptHost) gained the Database member (drifted since the runtime helper was added) so the authorized async API type-checks at the design-time gate.
18 KiB
Component: Inbound API
Purpose
The Inbound API exposes a web API on the central cluster that external systems can call into. This is the reverse of the External System Gateway — where that component handles the SCADA system calling out to external systems, this component handles external systems calling in. It provides API key authentication, method-level authorization, and script-based method implementations.
Location
Central cluster only (active node). Not available at site clusters.
Responsibilities
- Host a web API endpoint on the central cluster.
- Authenticate inbound requests via API keys.
- Route requests to the appropriate API method definition.
- Enforce per-method API key authorization (only approved keys can call a given method).
- Execute the C# script implementation for the called method.
- Return structured responses to the caller.
- Failover: API becomes available on the new active node after central failover.
API Key Management
Storage
- API keys are stored in the configuration database (MS SQL).
Key Properties
- Name/Label: Human-readable identifier for the key (e.g., "MES-Production", "RecipeManager-Dev").
- Key Value: The secret key string used for authentication.
- Enabled/Disabled Flag: Keys can be disabled without deletion.
Management
- Managed by users with the Administrator role via the Central UI.
- All key changes (create, enable/disable, delete) are audit logged.
API Method Definition
Properties
Each API method definition includes:
- Method Name: Unique identifier and URL path segment for the endpoint.
- Approved API Keys: List of API keys authorized to invoke this method. Requests from non-approved keys are rejected.
- Parameter Definitions: Ordered list of input parameters, each with:
- Parameter name.
- Data type — the extended type system (Boolean, Integer, Float, String, plus the nestable Object and List; see Extended Type System).
- Whether the parameter is required.
- Return Value Definition: Structure of the response, with:
- Field names and (extended-system) data types. Supports returning lists of objects and arbitrarily nested structures.
- Implementation Script: C# script that executes when the method is called. Stored inline in the method definition. Follows standard C# authoring patterns but has no template inheritance — it is a standalone script tied to this method.
- Timeout: Configurable per method. Defines the maximum time the method is allowed to execute (including any routed calls to sites) before returning a timeout error to the caller.
Management
- Managed by users with the Designer role via the Central UI.
- All method definition changes are audit logged.
HTTP Contract
URL Structure
- All API calls use
POST /api/{methodName}. - Method names map directly to URL path segments (e.g., method "GetProductionReport" →
POST /api/GetProductionReport). - All calls are POST — these are RPC-style script invocations, not RESTful resource operations.
Authentication Header
- The API key token (
sbk_<keyId>_<secret>) is accepted from either of two headers:Authorization: Bearer sbk_<keyId>_<secret>— the preferred form.X-API-Key: sbk_<keyId>_<secret>— alternate form for callers that cannot set the Authorization header.
- When both headers are present,
Authorizationtakes precedence. - Both forms are processed by the same
IApiKeyVerifierfrom the sharedZB.MOM.WW.Auth.ApiKeyslibrary; the verifier strips the optionalBearerprefix and performs the same peppered-HMAC constant-time verification regardless of which header was used.
Request Format
- Content-Type:
application/json. - Parameters are top-level JSON fields in the request body matching the method's parameter definitions:
{
"siteId": "SiteA",
"startDate": "2026-03-01",
"endDate": "2026-03-16"
}
Response Format
- Success (200): The response body is the method's return value as JSON, with fields matching the return value definition:
{
"siteName": "Site Alpha",
"totalUnits": 14250,
"lines": [
{ "lineName": "Line-1", "units": 8200, "efficiency": 92.5 },
{ "lineName": "Line-2", "units": 6050, "efficiency": 88.1 }
]
}
- Failure (4xx/5xx): The response body is an error object:
{
"error": "Site unreachable",
"code": "SITE_UNREACHABLE"
}
- HTTP status codes distinguish success from failure — no envelope wrapper.
Extended Type System
- API method parameter and return type definitions support an extended type system beyond the four template attribute types (Boolean, Integer, Float, String):
- Object: A named structure with typed fields. Supports nesting.
- List: An ordered collection of objects or primitive types.
- This allows complex request/response structures (e.g., an object containing properties and a list of nested objects).
- Template attributes retain the simpler four-type system. The extended types apply only to Inbound API method definitions and External System Gateway method definitions.
Type Definition Format & Nested Validation
- Parameter and return type definitions are persisted as JSON Schema (the canonical format produced by the Central UI schema builder; see the
MigrateParametersToJsonSchemamigration). An object declares its fields viaproperties(+ arequiredarray); a list declares its element type viaitems. The legacy flat-array form ([{name,type,required,itemType?}]) is still accepted on read for transition safety. - Validation is recursive and type-aware for the extended types (request parameters and script return values alike, via a single shared engine so the two cannot drift):
- Object: each declared field's value is validated against its declared (possibly nested) type; a missing required field and a present-but-wrong type are both reported.
- List: every element is validated against the declared element type (recursing into nested objects/lists). A list whose element type is left undeclared (
arraywithoutitems) is shape-checked only. - Scalars at any depth are checked against the extended type.
- Errors are path-qualified (e.g.
order.items[2].quantity) so the caller can locate the offending field.
- Undeclared fields are rejected at every level (consistent with the top-level "unexpected parameter" rejection): an object that declares its fields rejects any field not in its
properties, so a typo'd field name surfaces as a400/error rather than being silently ignored. A bare object schema with no declared fields ({"type":"object"}) stays shape-only and accepts any fields. - A JSON
nullvalue satisfies any declared type (a present-but-null field is allowed); only the absence of a required field is an error.
Script Compilation & Hot-Reload
API method scripts are compiled at central startup — all method definitions are loaded from the configuration database and compiled into in-memory delegates.
Update Workflow
- Updating a method via the CLI (
api-method update --id <N> --code '...') or Management API triggers immediate recompilation (CompileAndRegister). The updated script takes effect on the next API call — no node restart is required. - Creating a new method after startup: if the method is created but not yet compiled, the first invocation triggers lazy (on-demand) compilation.
Direct SQL Warning
Do not edit API method scripts via direct SQL. The in-memory compiled script will not be updated until the next node restart. Always use the CLI, Management API, or Central UI to modify API method scripts.
API Call Logging
- Every request — success or failure — emits one
ApiInbound.Completedrow toICentralAuditWriterfrom request middleware before the HTTP response is flushed. The row captures the API key name (never the key material), remote IP, user-agent, response status, duration, and the request/response bodies. Bodies are captured in full up toAuditLog:InboundMaxBytes(default 1 MiB);PayloadTruncated = 1only when that ceiling is hit. Header redaction and per-target body redactors still apply (see Component-AuditLog.md, Payload Capture Policy). This supersedes the earlier failures-only stance: operational API traffic is now part of the centralized audit log, so configuration changes and call activity share a single retention/query surface. - Script execution errors (500 responses) remain captured on the same
ApiInbound.Completedrow (response status + error fields) rather than emitting a separate failure-only event. - Fail-soft semantics (fire-and-forget). The audit write is fire-and-forget (
InboundAPI-018):WriteAsyncis called withoutawaitso the user-facing HTTP response is never blocked or delayed by the audit path. Asynchronous faults are observed via aContinueWithcontinuation that logs at Warning and incrementsCentralAuditWriteFailures(see Health Monitoring #11) rather than being silently dropped. A failed audit append never turns a successful API call into an error returned to the caller. - No rate limiting — this is a private API in a controlled industrial environment with a known set of callers. Misbehaving callers are handled operationally (disable the API key).
Request Flow
%%{init: {'theme':'base', 'themeVariables': {'textColor':'#111111','lineColor':'#555555','edgeLabelBackground':'#ffffff','fontSize':'15px'}}}%%
flowchart TD
ext(["External System"])
api["Inbound API (Central)"]
s1["1. Extract API key from request"]
s2["2. Validate key exists and is enabled"]
s3["3. Resolve method by name"]
s4["4. Check API key is in method's approved list"]
s5["5. Validate and deserialize parameters"]
s6["6. Execute implementation script<br/>(subject to method timeout)"]
s7["7. Serialize return value"]
s8["8. Return response"]
ext --> api
api --> s1
s1 --> s2
s2 --> s3
s3 --> s4
s4 --> s5
s5 --> s6
s6 --> s7
s7 --> s8
classDef start fill:#d5e8d4,stroke:#82b366,color:#111111;
classDef proc fill:#dae8fc,stroke:#6c8ebf,color:#111111;
classDef dec fill:#fff2cc,stroke:#d6b656,color:#111111;
classDef warn fill:#ffe6cc,stroke:#d79b00,color:#111111;
classDef alt fill:#e1d5e7,stroke:#9673a6,color:#111111;
class ext start
class api proc
class s1,s2,s3,s4,s5,s7 dec
class s6 alt
class s8 warn
Implementation Script Capabilities
The C# script that implements an API method executes on the central cluster. Unlike instance scripts at sites, inbound API scripts run on central and can interact with any instance at any site through a routing API.
Inbound API scripts cannot call shared scripts directly — shared scripts are deployed to sites only and execute inline in Script Actors. To execute logic on a site, use Route.To().Call().
Script Runtime API
Instance Routing
Route.To("instanceUniqueCode").Call("scriptName", parameters)— Invoke a script on a specific instance at any site. Central routes the call to the appropriate site via the Communication Layer. The call reaches the target Instance Actor's Script Actor, which spawns a Script Execution Actor to execute the script. The return value flows back to the calling API script.Route.To("instanceUniqueCode").GetAttribute("attributeName")— Read a single attribute value from a specific instance at any site.Route.To("instanceUniqueCode").GetAttributes("attr1", "attr2", ...)— Read multiple attribute values in a single call, returned as a dictionary of name-value pairs.Route.To("instanceUniqueCode").SetAttribute("attributeName", value)— Write a single attribute value on a specific instance at any site.Route.To("instanceUniqueCode").SetAttributes(dictionary)— Write multiple attribute values in a single call, accepting a dictionary of name-value pairs.Route.To("instanceUniqueCode").WaitForAttribute("attributeName", targetValue, timeout)— Wait, event-driven, until an attribute on a specific instance at any site reachestargetValue(value-equality only across the wire), bounded bytimeout. Returnstrueif matched within the timeout,falseif it timed out. The wait is bounded by its owntimeout, not the generic method-level timeout — this is the one routed call that may legitimately outlive the method timeout (the site enforcestimeoutand returnsfalsewhen it elapses). A client disconnect still cancels the wait. This is the deliberate exception to the rule below that routed calls inherit the method-level timeout (see "Routing Behavior"): a long event-driven wait is the explicit reasontimeoutgoverns here.
Input/Output
- Input parameters are available as defined in the method definition.
- Return value construction matching the defined return structure.
Parameter Access
Parameters["key"]— Raw dictionary access.Parameters.Get<T>("key")— Typed access (same API as site runtime scripts). See Site Runtime component for full type support.
Database Access
Inbound API scripts may read from and write to the configuration / machine-data
databases through the curated, scoped Database helper — InboundDatabaseHelper,
exposed as InboundScriptContext.Database. This is the "dedicated, scoped helper added
as an explicit design change" that the script trust model requires: scripts are never
handed a raw SqlConnection, and never reference System.Data (the ForbiddenApiChecker,
delegating to the shared ScriptAnalysis ScriptTrustValidator (#25), still statically
bans System.IO, Process, Threading, Reflection, and raw network access — that is
defence-in-depth static enforcement, not a true runtime sandbox).
The helper API (all asynchronous — scripts await them; bounded by the method timeout):
await Database.QuerySingleAsync<T>("connectionName", sql, parameters)— first column of the first row asT(default if no rows).await Database.QueryAsync("connectionName", sql, parameters)— all rows as case-insensitive column→value dictionaries.await Database.ExecuteAsync("connectionName", sql, parameters)— run a write (INSERT/UPDATE/DELETE/DDL); returns rows affected. Writes are permitted — the move-in integration records results, not just reads them.
Containment rules (enforced, not advisory):
- Named connections only.
connectionNameselects one of the connections configured on the central database gateway (IDatabaseGateway); a script cannot supply an arbitrary connection string or reach a database the gateway is not configured for. - SQL-injection protection. Statement text is authored by the (design-time) method
script, but every request-derived value is passed via
parametersand bound as a named@-prefixed SQL parameter — never string-concatenated into the command text. Request input therefore reaches the database only through parameter binding. - Deadline-bound. Calls use the async ADO.NET path end-to-end (no pool-thread blocking)
and honour the method deadline token, with a
CommandTimeoutbackstop derived from the method timeout, so a slow query is bounded by the method timeout.
For everything else, scripts interact with the system through the curated Route and
Parameters surfaces above.
Routing Behavior
- The
Route.To()helper resolves the instance's site assignment from the configuration database and routes the request to the correct site cluster via the Communication Layer. - The call is synchronous from the API caller's perspective — the API method blocks until the site responds or the method-level timeout is reached.
- If the target site is unreachable or the call times out, the call fails and the API returns an error to the caller. No store-and-forward buffering is used for inbound API calls.
Authentication Details
- The API key token is accepted from either the
Authorization: Bearer <token>header (preferred) or theX-API-Key: <token>header (alternate).Authorizationtakes precedence when both are present. - The token format is
sbk_<keyId>_<secret>. Both header forms are verified by the sameIApiKeyVerifierfromZB.MOM.WW.Auth.ApiKeys. - The system validates:
- The token is well-formed and the key ID exists in the key store.
- The key is enabled (not revoked).
- The key's provisioned scopes include the requested method name (case-sensitive).
- Failed authentication returns an appropriate HTTP error (401 Unauthorized for invalid/missing credentials; 403 Forbidden for a valid key not approved for the method). For enumeration-safety, both "method not found" and "key not in scope" return 403 with an identical body.
Error Handling
- Invalid API key → 401 Unauthorized.
- Valid key but not approved for method → 403 Forbidden.
- Invalid parameters → 400 Bad Request.
- Script execution failure → 500 Internal Server Error (with safe error message, no internal details exposed).
- Script errors are logged in the central audit/event system.
Dependencies
- Configuration Database (MS SQL): Stores API keys and method definitions.
- Communication Layer: Routes requests to sites when method implementations need site data.
- Security & Auth: API key validation (separate from LDAP/AD — API uses key-based auth).
- Configuration Database (via IAuditService): All API key and method definition changes are audit logged.
- Script Analysis (#25):
ForbiddenApiCheckerdelegates toScriptTrustValidator.FindViolationsfor the authoritative forbidden-API verdict during script compilation and validation. - Audit Log (#23): Every inbound API request emits an
ApiInbound.Completedrow viaICentralAuditWriterfrom request middleware (non-blocking for the HTTP response). Request and response bodies are captured in full up toAuditLog:InboundMaxBytes(default 1 MiB) per the Audit Log Payload Capture Policy; redaction (headers + per-target body redactors) still applies before persistence. - Cluster Infrastructure: API is hosted on the active central node and fails over with it.
Interactions
- External Systems: Call the API with API keys.
- Communication Layer: API method scripts use this to reach sites.
- Site Runtime (Instance Actors, Script Actors): Routed calls execute on site Instance Actors via their Script Actors.
- Central UI: Administrator manages API keys; Designer manages method definitions.
- Configuration Database (via IAuditService): Configuration changes are audited.