
Security News
How Enterprise Security Is Adapting to AI-Accelerated Threats
Socket CTO Ahmad Nassri discusses why supply chain attacks now target developer machines and what AI means for the future of enterprise security.
@magento/upward-spec
Advanced tools
Unified Progressive Web App Response Definitions are simple files describing how a web server delivers and supports a Progressive Web Application. They denote server behavior in a platform-independent way, so that a PWA client application expecting certain behavior from HTTP endpoints can be deployed on any type of tech stack that implements the UPWARD specification.
See RATIONALE.md for a detailed explanation of the context that led to the introduction of UPWARD, and the problems it intends to solve.
See EXECUTION_SCHEDULING_STRATEGIES.md for a deep dive into how the core logic of an UPWARD-compliant server might be implemented with a state machine.
See UPWARD_MAGENTO.md for context on how UPWARD fills a need in Magento PWA Studio and Magento 2 frontend development.
This repository is a test suite for UPWARD compliance, testing several scenarios and features on a live web server. It requires NodeJS v8 LTS or later. To test an UPWARD server:
Write or obtain a POSIX shell script which:
UPWARD_PATHUse npx to run upward-spec on your shell script
npx upward-spec ./test_upward_server.sh
The shell script will run for each test suite with the environment variable UPWARD_YAML set to the path of a fixture YAML file for configuring a server instance. The script should launch a server (on a local port or a remote port, but resolvable to the local system) and print its host to standard out, staying in the foreground.
The tests may run in parallel, so the server or launch script should seek open ports to bind to. When each test suite is over, the script will receive a SIGTERM or SIGKILL.
By default, the test runner will print human-readable results to stdout; the argument --xunit will make it print XUnit-compatible (and therefore JUnit-compatible) test result XML. The argument --tap will make it print Test Anything Protocol-compatible text. Under the hood, this uses tape and it can be piped to any number of open-source TAP reporters.
:information_source: (The npx tool above is not required; it's a convenience script to avoid installing global NPM dependencies. You can also install upward-spec permanently using npm install -g upward-spec, and then simply invoke upward-spec ./test_upward_server.sh from that point forward.)
UPWARD definitions are YAML files which declare the behavior of an application shell server. An application shell server implements a strict subset of HTTP functionality: it proxies to API backends, it serves static files, and it handles an HTTP GET request for a resource by delivering a minimal app shell: the code and data to bootstrap a Progressive Web App which displays that resource.
An App Shell is purposefully minimal, and so is an UPWARD server. It is meant to initialize or refresh sessions, deliver small HTML documents with enough server-side rendering for initial display and SEO, and then hand off subsequent request handling to the PWA running in the client.
The declarative format of UPWARD means that an UPWARD-compliant server may be written in any programming language and run on any tech stack; therefore, a PWA can declare the URIs and behavior of the network endpoints it depends on by including an UPWARD file.
This example definition file echoes request data as text back to the client.
status:
resolver: inline
inline: 200
headers:
resolver: inline
inline:
content-type: text/plain
body:
resolver: template
engine: mustache
provide:
- request
template:
resolver: inline
inline: |
{{#request}}
Headers:
{{#headerEntries}}
{{name}}: {{value}}
{{/headerEntries}}
URL:
{{#url}}{{#?protocol}}protocol: {{protocol}}
{{/?protocol}}{{#?host}}host: {{host}}
{{/?host}}{{#?hostname}}hostname: {{hostname}}
{{/?hostname}}{{#?port}}port: {{port}}
{{/?port}}pathname: {{pathname}}
{{/url}}
URL Query:
{{#queryEntries}}
{{name}}: {{value}}
{{/queryEntries}}
{{/request}}
This example demonstrates the initial properties of the context object, populated by the originating HTTP request. describes a server which always returns status 200 with a single header, content-type, and a text body which is a plaintext summary of the GET request properties. An example request to such a server results in:
$ curl 'http://localhost:54422/head/shoulders?and=knees&and=toes'
Headers:
host: localhost:54422
user-agent: curl/7.54.0
accept: */*
URL:
host: localhost:54422
hostname: localhost
port: 54422
pathname: /head/shoulders
URL Query:
and: knees,toes
A spec-compliant UPWARD server should be configurable with a runtime parameter: the location of the UPWARD definition YML file. The file references external resources using Resolvers.
An UPWARD definition file is an instruction manual for building an HTTP response. It links values together in a global namespace hereafter called the context. Each request handling cycle begins with with a new context object, with the incoming Request assigned to the context value request, and current environment variables assigned to the context value env. Root properties of the definition file represent other named values in the context, which Resolvers populate. Resolvers can use other context properties as input, and they can also use other Resolvers directly; in this way, the definition itself can be considered an abstract decision tree, from which code could be statically generated.
Resolvers must execute only when needed. If a request handling cycle moves through ConditionalResolver branches in a way that never requires a particular context value, then that context value must never be resolved during that execution cycle.
Resolvers must execute as concurrently as possible. The maximum concurrency is left to the implementation. A compliant server detects when a Resolver uses a context value, and delays its execution until that context value becomes available, via topological sorting of resolver execution.
See EXECUTION_SCHEDULING_STRATEGIES.md for an example of how this could be implemented.
An UPWARD server must detect cyclic dependencies, and should detect them as soon as possible. Only bare string literals can be used to reference context objects in a definition file, so it should be possible to detect potential cyclic dependencies upon first processing the configuration file, before any requests are made. If a cyclic dependency is detected in this manner, the server should raise an error on startup. If a cyclic dependency occurs at runtime, the server should return a 500 error.
The root context must always eventually have non-null status, headers, and body properties. Once the resolution path has assigned these three values fo context, the UPWARD-compliant server should immediately use those three values to create an HTTP response and flush it to the client. No streaming or buffering interface should be provided; UPWARD servers should not deal in data large enough to require streaming. If all resolvers finish executing and the response is lacking any of the status, headers, or body properties, the server should emit a 500 error. If it is possible at startup time to trace a path through the decision tree where this occurs, the implementation may use static analysis to do so and raise an error on startup.
:information_source: _In real-world scenarios, the generation of status, headers, and body will share a lot of logic. The recommended best practice is to use an InlineResolver to create a top-level object, called something like "response", with those properties, and then define the top-level status, headers, and body properties to refer to that object in context, e.g. status: response.status.
The context is a global namespace created for each request. It is a dictionary of values, like a JSON object. A typical response cycle may append intermediate values to the context, like a query result or a template string. Those values do not emit as part of the response. Only the status, headers, and body properties of the context will be flushed to the client. Since the context is the global namespace and the means of sharing values between resolvers, it may become a large object at some points in the cycle, but this should not affect performance on the client.
Context values cannot be overwritten. A resolver which attempts to overwrite an already-set context value must raise a context conflict error. UPWARD-compliant servers should be able to identify potential context conflict errors during static analysis of the definition file and raise an error on launch. UPWARD-compliant servers must be able to identify a context conflict during runtime execution and respond with a 500 error.
When an UPWARD-compliant server receives an HTTP GET request, it must populate an initial context object with the following values:
request: an object representing the incoming HTTP request. It must have these properties:
headers: An object representing HTTP headers. Header names are lower-cased, and multiple values are joined with commas.
headerEntries: An iterable array version of the headers object, suitable for use in a Mustache template (which cannot iterate over plain JSON objects). For this headers object:
{
"accept": "text/html",
"host": "example.com"
}
the headerEntries array would be:
[
{ "name": "accept", "value": "text/html" },
{ "name": "host", "value": "example.com" }
]
queryEntries: An iterable array version of the URL query object, suitable for Mustache like the headerEntries property explained above.
url: A subset of a URL record as specified by WHATWG. The following properties should at least be populated; the Host header can be used to infer the origin.
| Attribute | Example |
|---|---|
host | example.com:8080 |
hostname | example.com |
port | 8080 |
pathname | /deep/blue/sea |
search | ?baby=beluga |
query | { "baby": "beluga" } |
Because HTTP servers are sometimes unable to ascertain their own domain names or origins, it is acceptable for one or more of the href, origin, protocol, username, password, host, hostname, and port properties to be undefined. However, a compliant server MUST provide pathname, search, and query.
The query property is not part of the WHATWG specification, but it must be included in the url object nevertheless. Much like the headers and headerEntries properties, these objects exist for property lookup and iteration in logic-less templates, respectively.
env: an object containing the environment variables set when the server was launched. For instance, if a Dockerfile launches the server through Apache, with an environment variable MAGENTO_GRAPHQL_ENDPOINT:
ENV MAGENTO_GRAPHQL_ENDPOINT https://m2host.com/graphql
CMD ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
then the context value env.MAGENTO_GRAPHQL_ENDPOINT would equal "https://m2host.com/graphql".
For convenience and concision, frequently used strings should be registered as self-referential top-level context objects. Examples include:
GET: the string 'GET' must be included in the global context for convenience, to reduce boilerplate
POST: the string 'POST' must be included in the global context for convenience, to reduce boilerplate
mustache: the string 'mustache', included in the global context for convenience, to reduce boilerplate
Other strings that must be preset as literals in the context include:
text/htmltext/plainapplication/jsonutf-8latin-1base64hex200, 404, 500, and all others.In UPWARD configuration, a bare string is treated as a context lookup by default. Some resolver configuration properties must treat bare strings as literal strings, and the default contains many constants for common strings, as mentioned earlier, so a definition file may appear to use many literal strings, but almost all string values are context lookups.
A context lookup resembles "dot lookup" notation in JavaSript or Python, though it has simpler rules and cannot be dynamically generated.
Rules:
characters.0.name, will perform array access by index. Array indices must be positive integers; they must be specified literally, just like the rest of a context lookup string, and cannot be dynamically evaluated.foo is assigned, but it is not an object or it does not contain the property bar, then the context lookup foo.bar should wait until foo is assigned, and then yield the empty string.Parts of a context lookup:
string property names
basename | |
| | array index |
| | | |
uxbridges.characters.0.name
| |
named property lookups
The above context lookup behaves at runtime as an instruction to wait until the uxbridges basename has been assigned. There must be a definition elsewhere in the definition file which assigns this property, e.g.
uxbridges:
resolver: service
method: POST
resolver: inline
inline: false
url:
resolver: inline
inline: http://stapi.co/api/v1/rest/character/search
headers:
resolver: inline
inline:
content-type: application/x-www-form-urlencoded
query:
resolver: inline
inline: '{
characters @rest(type: "Character", path: )
}'
The above definition would assign an HttpResponse to the uxbridges basename once it has run. An HTTP response has no characters property, so the example context lookup would resolve to the empty string. However, an HTTP response does have a body property, so the lookup uxbridges.body.characters.0.name would resolve to Kevin Uxbridge.
:information_source: (Array and list handling is intentionally rudimentary in UPWARD, because of the potential for scope confusion, performance and security issues in iteration. The only recommended use case for list lookup is when a web service returns a list of items expected to have only one result, so that the result may be lifted out into a scalar value.)
Since arbitrary property lookups do not through exceptions and instead return a default empty string, use pattern matching to test the success or failure of requests and responses:
matches: uxbridges.body.characters.0..name
pattern: '.'
The regex dot character will pass if the string is non-empty, and fail if it is empty. This pattern should be common in UPWARD definitions.
Anywhere a in the definition file where a resolver is allowed, you may substitute a context lookup instead. A context path indicates a dependency on another root context value, which causes the current branch to wait until that value is resolved. In this example:
body:
resolver: conditional
when:
- matches: request.url.query.shipmentId
pattern: '\d+'
use:
resolver: template
engine: mustache
template:
resolver: file
file:
resolver: inline
inline: './renderShipment.mst'
provide:
shipment:
resolver: service
query:
resolver: file
file:
resolver: inline
inline: './getShipment.graphql'
variables:
id: request.url.query.shipmentId
- matches: request.url.query.shipmentName
pattern: '\w+'
use:
resolver: template
engine: mustache
template:
resolver: file
file:
resolver: inline
inline: './renderShipment.mst'
provide:
shipment:
resolver: service
url: env.SHIPMENTS_SVC
query:
resolver: file
file:
resolver: inline
inline: './getShipment.graphql'
variables:
name: request.url.query.shipmentName
- matches: request.url.query.shipmentTrackingNumber
pattern: '[\w\d\.]+'
use:
resolver: template
engine: mustache
template:
resolver: file
file:
resolver: inline
inline: './renderShipment.mst'
provide:
shipment:
resolver: service
url: env.SHIPMENTS_SVC
query:
resolver: file
file:
resolver: inline
inline: './getShipment.graphql'
variables:
trackingNumber: request.url.query.shipmentTrackingNumber
default:
inline: 'Please provide a query'
The matchers use the context lookups request.url.query.shipmentId, request.url.query.shipmentName, and request.url.query.shipmentTrackingNumber to asynchronously obtain values for comparison, as soon as they have been derived. The request object is part of the initial context, but you can also declare dependencies on other context values you have defined. The above definition could be more concisely expressed using intermediate values and more context lookups:
body:
resolver: conditional
when:
- matches: gqlVariables
pattern: null
use:
inline: 'Please provide a query'
default:
resolver: template
engine: mustache
template:
resolver: file
file:
resolver: inline
inline: './renderShipment.mst'
provide:
shipment:
resolver: service
url: env.SHIPMENTS_SVC
query:
resolver: file
file:
resolver: inline
inline: './getShipment.graphql'
variables: gqlVariables
gqlVariables:
resolver: conditional
when:
- matches: request.url.query.shipmentId
pattern: '\d+'
use:
resolver: inline
inline:
id: request.url.query.shipmentId
- matches: request.url.query.shipmentName
pattern: '\w+'
use:
resolver: inline
inline:
name: request.url.query.shipmentName
- matches: request.url.query.shipmentTrackingNumber
pattern: '[\w\d\.]+'
use:
resolver: inline
inline:
trackingNumber: request.url.query.trackingNumber
default: null
The above definition produces a server with identical behavior to the previous query, in 30% fewer lines. This is not the only way to use context values to reduce the first, repetitive definition, but it demonstrates the idea. If the GraphQL service set an order of precedence for the shipment query which used the same priority logic as the ConditionalResolver, the definition could be further reduced to use the same variable set in all cases:
body:
resolver: conditional
when:
- matches: request.url.query.shipmentId
pattern: '\d+'
use: getShipment
- matches: request.url.query.shipmentName
pattern: '\w+'
use: getShipment
- matches: request.url.query.shipmentTrackingNumber
pattern: '[\w\d\.]+'
use: getShipment
default:
inline: 'Please provide a query'
getShipment:
resolver: template
engine: mustache
template:
resolver: file
file:
resolver: inline
inline: './renderShipment.mst'
provide:
shipment:
resolver: service
url: env.SHIPMENTS_SVC
query:
resolver: file
file:
resolver: inline
inline: './getShipment.graphql'
variables:
id: request.url.query.shipmentId
name: request.url.query.shipmentName
trackingNumber: request.url.query.trackingNumber
Setting top-level context properties for reusable values is an important way to reduce verbosity and code duplication in the definition file. See Reducing boilerplate for more practices to reduce verbosity in a definition file.
When writing UPWARD definitions, no distinction needs to be made between "persistent" context values that should be the same for each request, and other values that may differ for each request. An UPWARD server must do topological sorting to determine the order in which to run Resolvers, so an efficient implementation can identify the parts of the root context that are not dependent on the incoming request and cache or discard them.
A Resolver is an object which describes how a value is obtained. There are five kinds of Resolver:
InlineResolver adds hardcoded valuesFileResolver loads files from a filesystemServiceResolver places GraphQL queries and loads the result setTemplateResolver renders a template string against the contextConditionalResolver does branch logic using pattern matching on context valuesProxyResolver delegates request/response handling to a proxyDirectoryResolver delegates request/response handling to a static file directoryEach Resolver takes different configuration parameters. Like a context lookup string, a resolver represents an operation which will execute and then deliver its results upward in the tree, until all dependencies of the top-level status, headers, and body definitions are resolved.
There is sometimes a need to put literal values in the context. However, a string property value performs a context lookup:
body: 'Hello world!'
The above expression uses Hello world! as a property name and tries to substitute the value of the context property named Hello world!. This is likely not the intent of the user. To set the body to the literal string 'Hello world!', use an InlineResolver:
body:
resolver: inline
inline: 'Hello world!'
The inline of an InlineResolver may be of any type; it may be a primitive value, a list, or an object of arbitrary depth. List values and Object properties may be context lookups or resolvers themselves, so when building lists or objects, make sure to use InlineResolvers again at every other level of depth. (A consequence of this is that it is difficult and verbose to set a literal value resembling a resolver configuration, such as { "resolver": "inline" }, but this seems to be a minor inconvenience.)
| Property | Type | Default | Description |
|---|---|---|---|
inline | any | Required. The value to be assigned to context. |
Queries and templates are often large, and often reused by other systems. They can be inline strings, as in the first example, but they should usually be separate files, referenced with a FileResolver.
query:
resolver: file
file:
resolver: inline
inline: './productDetail.graphql'
encoding:
resolver: inline
inline: 'utf-8'
The above expression loads the content of the file ./productDetail.graphql and sets it as the property query. The file path is resolved relative to the location of the definition file. The encoding property is optional.
| Property | Type | Default | Description |
|---|---|---|---|
file | Resolved<string> | Required. Path to the file to be read. Resolved relative to the definition file. | |
encoding | Resolved<string> | utf-8 | Character set to use when reading the file as text. Can be utf-8, latin-1, or binary. |
parse | Resolved<string> | auto | Attempt to parse the file as a given file type. The default of auto should attempt to determine the file type from its extension. The value text will effectively disable parsing. |
An UPWARD server must support pre-parsing of graphql, json, and mustache files according to their respective specifications, but should support as many filetypes as necessary and may support custom filetypes.
If the file cannot be found or there were any other failures reading the file, the resolver must resolve an object with a single errors property instead of a string or a parsed value. Errors must be formatted like GraphQL errors.
:information_source: (File contents are not expected to change during server runtime, so an UPWARD-compliant server should cache file contents on startup.)
Filenames are usually not variable; they will usually be specified as literals, rather than dynamically resolved out of context. For readability and convenience, a "shorthand syntax" must be available to imply a FileResolver that loads and parses a UTF-8 encoded file from a string filesystem path. Instead of explicitly resolving a filepath as an inline string:
query:
resolver: file
file:
resolver: inline
inline: './path/to/file.graphql'
The shorthand syntax should allow the following to be equivalent:
query: './path/to/file.graphql'
An UPWARD-compliant server must treat a barestring as a literal filepath instead of a context lookup when:
The string is being argued to a configuration parameter that accepts a FileResolver for the implied type of the file after parsing
AND
The string begins with one of the following prefixes:
./ and/or one or more ..// or C:\ or any other Windows drive letterfile://AND
Upon checking the filesystem, the string resolves to a regular file (not a symlink, device, or directory)
In the latter case, if the shorthand string neither resolves to a legal file, nor exists as a resolvable context value, an UPWARD-compliant server should raise a detailed error message explaining this.
An UPWARD server uses a ServiceResolver to obtain live data from a GraphQL backing service. URL, method, and headers can be specified manually; the query itself is constructed using query and variables parameters.
documentResult:
resolver: service
url:
resolver: inline
inline: 'https://example.com/graphql'
method:
resolver: inline
inline: POST
headers:
resolver: inline
inline:
'content-type': 'application/json'
accept: 'application/json'
# A small inline template renders the full syntax of the OAuth Bearer
# token header, instead of just the plain token.
authorization:
resolver: template
engine: 'mustache'
template:
resolver: 'inline'
inline: 'Bearer {{env.BEARER_TOKEN}}'
query:
resolver: inline
inline: 'query getDocument($id: String!) {
document(id: $id) {
title
contents
}
}'
variables:
resolver: inline
inline:
# This is a barestring indicating a context lookup. It resolves to the
# `id` value in the URL query string of the request, using the builtin
# `request` context object.
id: request.url.query.id
:information_source: (For the purposes of demonstration, the query here is resolved inline. The best practice is to store queries in files and use FileResolvers to obtain them.)
| Property | Type | Default | Description |
|---|---|---|---|
url | Resolved<string> | https://localhost/graphql | Required. The URL of the service endpoint to call. |
method | Resolved<string> | POST | The HTTP method to use. While GraphQL queries are typically POSTS, some services expose GraphQL over GET instead. |
headers | Resolved<Object<string,string>> | Additional HTTP headers to send with the GraphQL request. Some headers are set automatically, but the headers configuration can append to headers that can have multiple values. | |
query | `Resolved<Query | string>` | |
variables | Resolved<Object<any>> | {} | Variables to use with the GraphQL query. Must resolve to an object with keys and values, almost always with an InlineResolver. |
ServiceResolvers always use GraphQL. To obtain data from a non-GraphQL service, an UPWARD server may implement client-side directives which change the behavior of a GraphQL query, such as apollo-link-rest, and place the directives in the query itself. This should be transparent to the UPWARD server itself, which delegates the service call to a GraphQL client. If an UPWARD server's GraphQL client has no implementation for such a directive, then it must pass the query unmodified to the backing service to handle the directive.
The below behavior is not standard or normative: it is an example of how one might query a REST service using an UPWARD server whose GraphQL client implementation has a @rest directive like apollo-link-rest.
documentResult:
resolver: service
url: env.REST_API_ENDPOINT
method:
resolver: inline
inline: GET
headers:
# In contrast to the template above, this assumes that en environment
# variable is already set with the "Bearer <token>" format.
inline:
authorization: env.BEARER_TOKEN_STRING
query: 'query getDocument($id: String!) {
document(id: $id) @rest(type: "Document", path: "/documents/{args.id}") {
title
contents
}
links(documentId: $id) {
name
url
}
}'
variables:
inline:
id: request.url.query.id
The GraphQL specification requires that a successful response to a GraphQL query have a root "data" property, an object whose properties correspond to the entities returned, and an that a failed response have a root "errors" property. An UPWARD server must assign the entire root object to the named context value. The query above would, if successful, result in a context with documentResult.data.document and documentResult.data.links properties. If the query failed, the context would contain documentResult.errors.
The GraphQL specification [defines error behavior][graphql spec errors] clearly, and UPWARD servers should pass the errors collection from a GraphQL response as described above. Other errors can occur during resolution, such as:
query could not be parsedAn UPWARD server should format these errors the same way that GraphQL services do; it should return an object with an errors array containing all errors encountered.
Once the UPWARD server assembles the data for a response, it must turn that data into a response body. The TemplateResolver renders a template into a string, executing it with the context object as its root value by default. UPWARD servers must provide a Mustache template renderer that adheres to the Mustache specification. UPWARD servers can also provide other template renderers, which must evaluate context objects into strings.
status:
resolver: inline
inline: 200
headers:
resolver: inline
inline:
'content-type':
resolver: inline
inline: 'text/html'
body:
resolver: template
engine:
resolver: inline
inline: mustache
root: documentResult.data.document
template:
resolver: inline
inline: |
{{> headtag}}
{{> header}}
<div class="document">
<h1>{{title}}</h1>
<div class="document-body">
{{& contents}}
</div>
</div>
{{> footer}}
The above configuration resolves into an HTML document displaying content from the documentResult.data.document context value. Its use of Mustache partials implies that additional files called headtag.mst, header.mst, and footer.mst exist in the directory containing the definition file. Attempting to include a missing partial should raise an error as soon as the template is resolved, ideally at server startup time.
:information_source: (For illustrative purposes, the above uses an InlineResolver where it would be more appropriate to use a FileResolver to obtain the template, as with the query in the ServiceResolver example.)
| Property | Type | Default | Description |
|---|---|---|---|
engine | Resolved<string> | Required. The label of the template engine to use. | |
provide | `Resolved<string[] | object>` | |
template | `Resolved<Template | string>` |
The entire context cannot be available for template render; that would cause an immediate circular dependency, since the template's output is added to context! Instead, use the provide argument to select what values the template actually needs. They will be available, at root, inside the template.
The provide argument can be a list:
provide:
- env
- articleResult
The resulting template eval context might look like this:
"env": {
"envVars": "here"
},
"articleResult": {
"data": {
"article": {
articleContents: 'here'
}
}
}
Lists may only inject "base" context properties. The above articleResult could not be articleResult.data.article when using the list format.
The other, more powerful option for the provide argument is to provide a mapping, as a simple object. A mapping must resolve to a plain object of string keys and context values. It might appear as:
provide:
inline:
article: articleResult.data.article
This would give the template a single root property "article", thus flatting out the template tree and making templates more readable.
"article": {
articleContents: 'here'
}
The engine property must resolve to a string labeling a supported template engine. The only required template engine is Mustache, and its label must be mustache. An UPWARD server may support additional template engines. For instance, an UPWARD server may support ReactJS server-side rendering.
body:
resolver: template
engine:
resolver: inline
inline: react
provide:
inline:
document: documentResult.data.document
query: request.url.query
template:
resolver: file
file:
resolver: inline
inline: './build/RootComponents/Document.js'
The above configuration assumes support for a template engine labeled react. The underlying template engine could be a simple Node module:
const { createElement } = require('react');
const { renderToString } = require('react-dom/server');
module.exports = (props, component) =>
renderToString(createElement(require(component), props));
Attaching such a template engine to a Resolver would be trivial.
If the engine is unknown or not supported, the UPWARD server should detect this as soon as possible and send a 500 error.
If there were any errors parsing the template, evaluating, or serializing the data, the resolver must resolve an object with an errors array instead of a rendered string. Errors must be formatted like GraphQL errors.
A ConditionalResolver tests a context value for a particular pattern, and then yields to another Resolver depending on the match results.
The ConditionalResolver is the only branch logic operation available in the UPWARD specification. It performs pattern matching on a context value using Perl compatible regular expressions. It can only test a single context value for each potential match, and it has no Boolean operators such as AND, OR, or NOT. All of these logical operations can be achieved through pattern matching, however. It takes two configuration values: when must be a list of Matcher<Resolver<T>>>, and default must be a resolver to use if none of the when conditions are true.
monkey:
resolver: conditional
when:
# All values are coerced to strings for regex matching.
- matches: request.url.query.grab
pattern: '^(true|1)$'
use:
resolver: inline
inline: "do anyway"
- matches: status
pattern: '^403$'
use:
resolver: inline
inline: "see"
default:
resolver: inline
inline: "do"
status:
resolver: inline
inline: 403
body:
resolver: template
engine:
resolver: inline
inline: mustache
template:
resolver: inline
inline: "<p>monkey <b>{{ monkey }}</b>.</p>"
The above configuration uses a ConditionalResolver to create a context value monkey. The when list contains two matchers. They are executed in order. The first matcher tests if the request query string has a value grab matching the regular expression ^(true|1)$, If the request query string contains grab=true or grab=1, this matcher succeeds and the ConditionalResolver yields to the resolver specified in the matcher's use property. The context now matches the object:
{
"status": 403,
"monkey": "do anyway",
"body": "<p>monkey <b>do anyway</b></p>"
}
If the request query string does not contain a qualifying grab property, the second matcher runs. This matcher tests if the status context value matches the regular expression ^403$, effectively checking if the response status code has already been set to HTTP 403 Forbidden. The status resolver runs before the monkey resolver, though it is defined later in the document (see Concurrency), so the matcher tests against the value 403 (cast to a string) and succeeds. The ConditionalResolver uses the matcher's resolver, and the context now matches the object:
{
"status": 403,
"monkey": "see",
"body": "<p>monkey <b>see</b></p>"
}
If some other configuration provides a different status, such as 200, then neither matcher in the when list succeeds, and the ConditionalResolver uses its default resolver. The context now matches the object:
{
"status": 200,
"monkey": "do",
"body": "<p>monkey <b>do</b></p>"
}
| Property | Type | Default | Description |
|---|---|---|---|
when | Matcher[] | ||
default | Resolved<any> | Required. The default resolver to use if no matcher succeeds. |
A Matcher is an object which can only be used as an item in a ConditionalResolver's when list. It must have the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
matches | string | Required. The context value to match. Must be a bare string context lookup. | |
pattern | string | Required. PCRE regular expression to use to test against the value in matches. | |
use | Resolved<any> | Required. Resolver to use if the match succeeds. |
During the resolution of a matcher's use resolver, properties from the match object are temporarily added to the context. Using these temporary context values, resolvers can extract matching text, or capture groups, from the match itself.
$match.$0 - The full text of the last matched value.$match.$1 - The string captured in the first backreference the regex declared.The $match object must have additional numbered properties for each backreference.
matches value.default resolver is required.The ProxyResolver acts as a "passthrough" to another service. It is guaranteed to resolve into an object with status, headers, and body properties from a logical point of view. In implementation, it may handle request objects more directly, for performance purposes.
A ProxyResolver is an important part of the UPWARD philosophy: a PWA's UPWARD file must describe all of the network behavior that a PWA expects at runtime, and that includes proxies to various backing APIs.
proxy:
target: env.MAGENTO_BACKEND_URL
ignoreSSLErrors: true
| Property | Type | Default | Description |
|---|---|---|---|
target | Resolved<string> | ||
ignoreSSLErrors | Resolved<boolean> | false | Ignore remnote SSL certificate errors (useful for internal communication among containers). |
ProxyResolvers are special targets for static analysis. Using simple techniques, it should be possible for an analysis system to determine proxying rules by walking the UPWARD tree. This is effectively a declarative proxy config, and deployment tools can be enhanced to create proxy servers in front of UPWARD where appropriate.
Like the ProxyResolver, the DirectoryResolver delegates request and response handling to a static server. Unlike the ProxyResolver, it needs access to a local directory, which it will then sere as a public assets folder. Much like the ProxyResolver, this resolver exists to bolster the notion that an UPWARD file can describe the expected behavior of a PWA server-side site, in detail.
static:
directory:
inline: './dist'
| Property | Type | Default | Description |
|---|---|---|---|
directory | Resolved<string> |
The above examples are fairly verbose, to make the workings of UPWARD configuration especially clear. The UPWARD specification is intentionally verbose in its canonical format, to enable maximal static analysis. However, An UPWARD-compliant server must also include features to reduce boilerplate.
Many resolver configuration parameters have default values, so they can be omitted from configuration if the default is appropriate.
In the first example of a ServiceResolver above, some config parameters are actually unnecessary. The default URL for a service call is http://localhost/graphql, and the default method is POST. MIME headers are automatically generated, so the following configuration would be equivalent:
documentResult:
resolver: service
headers:
resolver: inline
inline:
Authorization: env.BEARER_TOKEN
query:
resolver: inline
inline: 'query getDocument($id: String!) {
document(id: $id) {
title
contents
}
}'
variables:
id: request.url.query.id
Using the best practice of storing queries in separate files and storing reusable objects as separate context values, the configuration would likely look like this:
documentQuery: './queries/document.graphql'
oauthHeaders:
resolver: inline
inline:
authorization: env.BEARER_TOKEN
documentResult:
resolver: service
headers: oauthHeaders
query: documentQuery
variables:
id: request.url.query.id
The default context must contain some builtin constants for common strings, to avoid repetitive use of InlineResolvers.
In the first example of a TemplateResolver above, some inline resolvers are unnecessary. HTTP response codes, common MIME types, common template engine labels, and other useful values must be preset in the initial context, so the following, shorter configuration is equivalent to the TemplateResolver example:
status: 200
headers:
resolver: inline
inline:
'content-type': 'text/html'
body:
resolver: template
engine: 'mustache'
provide:
model: documentResult.data.document
template:
resolver: inline
inline: |
{{> headtag}}
{{> header}}
<div class="document">
<h1>{{model.title}}</h1>
<div class="document-body">
{{& contents}}
</div>
</div>
{{> footer}}
For any value which must be a Resolver or a context lookup, an UPWARD-compatible server should attempt to infer resolver types from a supplied resolver configuration object, rather than requiring a resolver name to be specified. Each resolver has required parameters that are mutually exclusive with one another, so a resolver type can be inferred from the presence of those parameters.
| If parameter exists... | Then infer resolver type: |
|---|---|
inline | InlineResolver |
file | FileResolver |
query | ServiceResolver |
engine | TemplateResolver |
when | ConditionalResolver |
target | ProxyResolver |
directory | DirectoryResolver |
Resolver type inference allows configuration to omit resolver: parameters, which makes it possible to be far more terse. The example optimized configuration in Builtin constants could be further reduced:
status: 200
headers:
inline:
'content-type': 'text/html'
body:
engine: 'mustache'
root: documentResult.data.document
template:
inline: |
{{> headtag}}
{{> header}}
<div class="document">
<h1>{{title}}</h1>
<div class="document-body">
{{& contents}}
</div>
</div>
{{> footer}}
And the example optimized configuration in Default parameters could be further reduced:
documentResult:
headers:
inline:
authorization: env.BEARER_TOKEN
query: './queries/document.graphql'
variables:
id: request.url.query.id
:information_source: (Note that the variables object cannot be a resolver, as specified in ServiceResolver configuration options — it must be an object whose keys are expected query variable names and whose values are resolvers, so there is no ambiguity if a GraphQL query expects a variable named, for example, file.)
The YAML specificationsupports an anchor and reference syntax which can also be used to shorten configuration files. While this can be used as part of legal YAML parsing, its use is discouraged by UPWARD files, since context resolution is clearer to the reader. Additionally, not all parsers support references in the same way, so the feature should be used with caution.
FAQs
UPWARD specification, guide, and test suite.
We found that @magento/upward-spec demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 12 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CTO Ahmad Nassri discusses why supply chain attacks now target developer machines and what AI means for the future of enterprise security.

Security News
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.

Security News
Experts push back on new claims about AI-driven ransomware, warning that hype and sponsored research are distorting how the threat is understood.