- ims
Wrapper function to easily perform adobe IMS authentication
Usage:
const wrap = require('@adobe/helix-shared-wrap'); const bodyData = require('@adobe/helix-shared-body-data'); const ims = require('@adobe/helix-shared-ims');
async main(req, context) { // …my action code… if (context.ims.profile) { // do authenticated stuff } }
module.exports.main = wrap(main) .with(ims, { clientId: 'my-client' }) .with(bodyData) .with(logger);
- wrap
Helper function to easily chain functions.
Usage:
const { wrap } = require('@adobe/helix-shared');
async main(params) { // …my action code… }
module.exports.main = wrap(main) .with(epsagon) .with(status) .with(logger);
- ModifiersConfig
The modifiers class help manage the metadata and headers modifiers.
- SchemaDerivedConfig
A Helix Config that is based on a (number of) JSON Schema(s).
- GitUrl
Represents a GIT url.
- nextTick ⇒
promise
Await the next tick;
NOTE: Internally this uses setImmediate, not process.nextTick. This is because process.nextTick and setImmediate are horribly named and their names should be swapped.
const mAsyncFn = () => { const page1 = await request('https://example.com/1'); await nextTick(); const page2 = await request('https://example.com/2'); ... };
- isNodeType
Check whether the given type is the type of a dom node. Note that, in order to support various dom implementations, this function uses a heuristic and there might be some false positives.
- assertNode
Ensure that the given node is a domNode. Checks with isNode()
- nodeName
Determine the name of a node. The result is always in lower case.
- ancestryNodes ⇒
Array.<DomNode>
Retrieve all the parent nodes of a dom node.
- equalizeNode ⇒
DomNode
Removes comments and redundant whitespace from dom trees and moves meaningful white space to a standardized location.
Adjacent text nodes are also merged, as https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize does.
This function predominantly serves as a way to preprocess nodes given to
nodeIsEquivalent
, so these nodes can be compared usingisEqualNode
without insignificant whitespace changes standing in the way of equivalence.normalizeDomWhitespace
is supposed to turn equivalent dom treesturn equivalent dom trees into equal dom trees.The concept of equivalence is a bit fuzzy unfortunately. Some html minifiers like kangax's html-minifier even leave whitespace alone by default, because what transformations are permitted is so unclear. Going by isEqualNode, any two dom trees just differentiated by their whitespace content are unequal.
This function's ultimate goal is to introduce an equivalence concept which
- closely matches the mental model developers would have
- does not affect rendering
For instance, indenting dom nodes for improved readability should usually not affect equivalence, neither should inserting newline characters/replacing spaces with newlines because a line is growing too long or because dom elements should be one per line.
Whitespace in
elements however should affect equivalence.
The given examples also adhere to the 'do not affect rendering' rules unless exotic javascript or CSS is added after the fact.
The following rules are used by this function:
- Whitespace in
tags and contained tags is left alone. In more precise terms, whitespace in any elements whose computed
white-space
style property starts withpre
is left alone. - Whitespace in other elements is compacted, meaning any combination of whitespace characters (newlines, spaces, tabs, etc) is replaced by a single space.
- Any whitespace before/after closing/opening tags is removed, unless
the tag in question is inline. A tag is inline if it's computed
style property
display
starts withinline
or is set tocontent
. This is the default behaviour for . - Whitespace next to opening/closing tags is also collapsed; all space between text nodes across a tree of purely inline elements are collapsed into a single space character. The space character is placed in the closest common ancestor, between the ancestors of both text nodes.
Rule 3 and 4 are a bit verbose. Please take a look at the examples below.
See also: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM https://drafts.csswg.org/css-text-3/#propdef-white-space
<div> </div>
-><div></div>
Rule 3 - div is not inline:
Hello <div> world </div> friend
->Hello<div>world</div>friend
Rule 4 - span is inline:
Hello <span> world </span> friend
->Hello <span>world</span> friend
Rule 4 – the whitespace between multiple inline elements is placed int the lowest common ancestor.
<a>Hello </a> \n <a> World</a>
-><a>Hello</a> <a>World</a>
<a>Hello</a><a> World</a>
-><a>Hello</a> <a>World</a>
<span><a>Hello</a></span><a> World</a>
-><span><a>Hello</a></span> <a>World</a>
Note that this function does not manually check for dom nodes like
or differentiate between and
. Instead the `display` and `white-space` computed css properties are used to determine how space should be compacted.Since the computedStyle is used to determine how space compaction should be applied, the function can deal with css affecting rendering of white space: e.g. if
white-space
is set topre
, this will be detected by this function just as if aelement had been used. The same is true for the
display
property.The only requirement for this to work is that the CSS in question is present in the dom tree.
So when JSDOM is used to provide the DOM, then the entire html document should be loaded (not just fragments) and loading external stylesheets should be enabled...
- nodeIsEquivalent ⇒
Boolean
Test whether two nodes are equivalent.
equals()
over two dom nodes is an alias for this.Invokes equalizeNode() on both given elements before invoking .isEqualNode. This means the equivalence model described in
equalizeNode()
is employed. Please refer to it's documentation to learn more- nodeMatches ⇒
Boolean
Node equivalence testing with wildcard support.
This is mostly like nodeIsEquivalent, except that the pattern may contain wildcard nodes. Wildcard nodes are nodes with the name
match:any
.Wildcards in the pattern will lazily (meaning non greedily) match zero, one or many dom nodes in the given node to test.
<match:any></match:any>
matches anything ``foo
<div></div>
<match:any></match:any>Hello<match:any></match:any>
matches any node that containsHello
as a child:HelloHello
Foo Hello Foo
<div></div> Foo Hello
but not this example, because here hello is in a subnode.<div>Hello</div>
<div class='xxx' id='Borg'><matches:any></matches:any>Foo</div>
matches:<div class='xxx' id='Borg'>Foo</div>
<div class='xxx' id='Borg'>Hello Foo</div>
<div id='Borg' class='xxx'>borg Foo</div>
but notFoo
<div id='Borg' class='xxx'></div>
- assertEquivalentNode
Assert that two dom nodes are equivalent. The implementation mostly defers to .isEqualNode, but provides better error messages.
- dumpDOM
prints dom in order for changes to be more discernible.
- getData(request, [opts]) ⇒
Promise.<object>
Extracts the data from the given request. The data can be provided either as request parameters, url-encoded form data body, or a json body.
Note that for post body requests, the body is consumed from the request and is no longer available.
- toMetaName(text) ⇒
string
Converts all non-valid characters to
-
.- stripQuery(m, ...specialparams) ⇒
object
Cleans up the URL by removing parameters that are deemed special. These special parameters will be returned in the return object instead.
- getData(request, ...names) ⇒
object
Exported only for testisg
- match(globs, path, defaultValue) ⇒
boolean
Return a flag indicating whether a particular path is matches all given glob patterns.
- contains(cfg, path) ⇒
boolean
Return a flag indicating whether a particular path is contained in the indexing configuration (include or exclude element). This is true if a path is included and not excluded.
- getDOMValue(elements, expression, log, vars)
Return a value in the DOM by evaluating an expression
- indexResource(path, response, config, log) ⇒
object
Given a response, extract a value and evaluate an expression on it. The index contains the CSS selector that will select the value(s) to process. If we get multiple values, we return an array.
- dequeue(queue) ⇒
Generator.<*, void, *>
Simple dequeing iterator.
- _request(target, input) ⇒
any
Pass a request to the AWS secrets manager
- reset()
reset the cache - for testing only
- loadSecrets(ctx, [opts]) ⇒
Promise.<object>
Loads the secrets from the respective secrets manager.
- multiline()
This is a helper for declaring multiline strings.
const s = multiline(` Foo Bar Baz
Hello Bang
`);
The function basically just takes a string and then strips the first & last lines if they are empty.
In order to remove indentation, we determine the common whitespace prefix length (number of space 0x20 characters at the start of the line). This prefix is simply removed from each line...
- lookupBackendResponses(status) ⇒
Object
A glorified lookup table that translates backend errors into the appropriate HTTP status codes and log levels for your service.
- computeSurrogateKey(url) ⇒
Promise.<string>
Computes the caching Surrogate-Key for the given url. The computation uses a hmac_sha256 with a fixed key: {@code "helix"}. the result is base64 encoded and truncated to 16 characters. This algorithm is chosen, because similar functionality exists in Fastly's VCL api:
declare local var.key STRING; set var.key = digest.hmac_sha256_base64("helix", "input"); set var.key = regsub(var.key, "(.{16}).*", "\1");
- propagateStatusCode(status) ⇒
int
What is the appropriate status code to use in your service when your backend responds with
status
? This function provides a standardized lookup function to map backend responses to gateway responses, assuming you are implementing the gateway.- logLevelForStatusCode(status) ⇒
string
What is the appropriate log level for logging HTTP responses you are getting from a backend when the backend responds with
status
? This function provides a standardized lookup function of backend status codes to log levels.You can use it like this:
logger[logLevelForStatusCode(response.status)](response.message);
- cleanupHeaderValue(value) ⇒
Cleans up a header value by stripping invalid characters and truncating to 1024 chars
- hashContentBusId(value) ⇒
Compute an SHA digest from some string value.
Wrapper function to easily perform adobe IMS authentication
Usage:
const wrap = require('@adobe/helix-shared-wrap');
const bodyData = require('@adobe/helix-shared-body-data');
const ims = require('@adobe/helix-shared-ims');
async main(req, context) {
// …my action code…
if (context.ims.profile) {
// do authenticated stuff
}
}
module.exports.main = wrap(main)
.with(ims, { clientId: 'my-client' })
.with(bodyData)
.with(logger);
- ims
- module.exports(func, [options]) ⇒
UniversalFunction
⏏- ~redirectToLogin(ctx, noPrompt) ⇒
Response
- ~fetchProfile(ctx) ⇒
Promise.<(IMSProfile|null)>
- ~logout(ctx) ⇒
Promise.<Response>
- ~redirectToLogin(ctx, noPrompt) ⇒
- module.exports(func, [options]) ⇒
Wraps a function with an ims authorization middle ware. If the request is authenticated, the
context.ims
will contain a profile
object, representing the authenticated user profile.
The wrapper claims several routes:
The IMSConfig.routeLogin
(default '/login') is used
to respond with a redirect to the IMS login page in 'no-prompt' mode. i.e. the IMS page will
not provide username/password fields to login the user, but tries instead to silently login.
After authentication the IMS login page redirects back to IMSConfig.routeLoginRedirect
.
The IMSConfig.routeLoginRedirect
(default '/login/ack') route handles the response from the
first, silent login attempt. The the login was successful, it will respond with a redirect to
the root /
.
if not successful, it will respond with a redirect again to the IMS login page in
normal mode, i.e. where the IMS page provides means to login. After login, the IMS login
page redirects back to IMSConfig.routeLoginRedirectPrompt
.
The IMSConfig.routeLoginRedirectPrompt
(default '/login/ack2') route handles the response from
the second login attempt.
The login was successful, it will respond with a redirect to the root /
,
otherwise the request remains unauthenticated.
After a successful login, a ims_access_token
cookie is set on the response, which is
then used for subsequent requests.
The IMSConfig.routeLogout
(default '/logout') is used to logout the user. It sends a
request to the IMS logout endpoint and subsequently clears the ims_access_token
cookie.
The response is always be a 200.
The IMS access token can either be provided via the ims_access_token
cookie, or a
request parameter with the same name.
Kind: Exported function
Returns: UniversalFunction
- an universal function with the added middleware.
Param | Type | Description |
---|---|---|
func | UniversalFunction |
the universal function |
[options] | IMSConfig |
Options |
Calculates the login redirect response
Kind: inner method of module.exports
Returns: Response
- redirect response
Param | Type | Description |
---|---|---|
ctx | UniversalContextWithIMS |
universal context |
noPrompt | boolean |
flag indicating if the login should be silent |
Fetches the ims profile
Kind: inner method of module.exports
Param | Type | Description |
---|---|---|
ctx | UniversalContextWithIMS |
the context of the universal serverless function |
Sends the logout request to IMS and clears the access token cookie.
Kind: inner method of module.exports
Param | Type | Description |
---|---|---|
ctx | UniversalContextWithIMS |
the context of the universal serverless function |
Helper function to easily chain functions.
Usage:
const { wrap } = require('@adobe/helix-shared');
async main(params) {
// …my action code…
}
module.exports.main = wrap(main)
.with(epsagon)
.with(status)
.with(logger);
A function that makes your function (i.e. main
) wrappable,
so that using with
a number of wrappers can be applied. This allows
you to export the result as a new function.
Usage:
async main(req, context) {
//…my action code…
}
module.exports.main = wrap(main)
.with(epsagon)
.with(status)
.with(logger);
Note: the execution order is that the last wrapper added will be executed first.
Kind: Exported function
Returns: WrappableFunction
- the same main function, now including a with
method
Param | Type | Description |
---|---|---|
fn | function |
the function to prepare for wrapping |
Await the next tick;
NOTE: Internally this uses setImmediate, not process.nextTick. This is because process.nextTick and setImmediate are horribly named and their names should be swapped.
const mAsyncFn = () => {
const page1 = await request('https://example.com/1');
await nextTick();
const page2 = await request('https://example.com/2');
...
};
Kind: global constant
Returns: promise
- A promise that will resolve during the next tick.
Check whether the given type is the type of a dom node. Note that, in order to support various dom implementations, this function uses a heuristic and there might be some false positives.
Ensure that the given node is a domNode. Checks with isNode()
Determine the name of a node. The result is always in lower case.
Retrieve all the parent nodes of a dom node.
Kind: global constant
Returns: Array.<DomNode>
- All the ancestor dom nodes to the given
dom node, starting with the most distant dom node.
Param | Type |
---|---|
node | DomNode |
Removes comments and redundant whitespace from dom trees and moves meaningful white space to a standardized location.
Adjacent text nodes are also merged, as https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize does.
This function predominantly serves as a way to preprocess nodes
given to nodeIsEquivalent
, so these nodes can be compared
using isEqualNode
without insignificant whitespace changes standing
in the way of equivalence.
normalizeDomWhitespace
is supposed to turn equivalent dom treesturn equivalent dom trees
into equal dom trees.
The concept of equivalence is a bit fuzzy unfortunately. Some html minifiers like kangax's html-minifier even leave whitespace alone by default, because what transformations are permitted is so unclear. Going by isEqualNode, any two dom trees just differentiated by their whitespace content are unequal.
This function's ultimate goal is to introduce an equivalence concept which
- closely matches the mental model developers would have
- does not affect rendering
For instance, indenting dom nodes for improved readability should usually not affect equivalence, neither should inserting newline characters/replacing spaces with newlines because a line is growing too long or because dom elements should be one per line.
Whitespace in
elements however should affect equivalence.The given examples also adhere to the 'do not affect rendering' rules unless exotic javascript or CSS is added after the fact.
The following rules are used by this function:
- Whitespace in
tags and contained tags is left alone. In more precise terms, whitespace in any elements whose computedwhite-space
style property starts withpre
is left alone.- Whitespace in other elements is compacted, meaning any combination of whitespace characters (newlines, spaces, tabs, etc) is replaced by a single space.
- Any whitespace before/after closing/opening tags is removed, unless the tag in question is inline. A tag is inline if it's computed style property
display
starts withinline
or is set tocontent
. This is the default behaviour for .- Whitespace next to opening/closing tags is also collapsed; all space between text nodes across a tree of purely inline elements are collapsed into a single space character. The space character is placed in the closest common ancestor, between the ancestors of both text nodes.
Rule 3 and 4 are a bit verbose. Please take a look at the examples below.
See also: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM https://drafts.csswg.org/css-text-3/#propdef-white-space
<div> </div>
-><div></div>
Rule 3 - div is not inline:
Hello <div> world </div> friend
->Hello<div>world</div>friend
Rule 4 - span is inline:
Hello <span> world </span> friend
->Hello <span>world</span> friend
Rule 4 – the whitespace between multiple inline elements is placed int the lowest common ancestor.
<a>Hello </a> \n <a> World</a>
-><a>Hello</a> <a>World</a>
<a>Hello</a><a> World</a>
-><a>Hello</a> <a>World</a>
<span><a>Hello</a></span><a> World</a>
-><span><a>Hello</a></span> <a>World</a>
Note that this function does not manually check for dom nodes like
or differentiate between and
. Instead the `display` and `white-space` computed css properties are used to determine how space should be compacted. Since the computedStyle is used to determine how space compaction should be applied, the function can deal with css affecting rendering of white space: e.g. if `white-space` is set to `pre`, this will be detected by this function just as if aelement had been used. The same is true for the `display` property. The only requirement for this to work is that the CSS in question is present in the dom tree. So when JSDOM is used to provide the DOM, then the entire html document should be loaded (not just fragments) and loading external stylesheets should be enabled... **Kind**: global constant **Returns**:DomNode
- The node parameter; the node parameter was mutated by this function; a reference to it is returned in order to facilitate function chaining. | Param | Type | Description | | --- | --- | --- | | node |DomNode
| The node to equalize; this value will be mutated! | ## nodeIsEquivalent ⇒Boolean
Test whether two nodes are equivalent. `equals()` over two dom nodes is an alias for this. Invokes equalizeNode() on both given elements before invoking .isEqualNode. This means the equivalence model described in `equalizeNode()` is employed. Please refer to it's documentation to learn more **Kind**: global constant | Param | Type | | --- | --- | | a |DomNode
| | b |DomNode
| ## nodeMatches ⇒Boolean
Node equivalence testing with wildcard support. This is mostly like nodeIsEquivalent, except that the pattern may contain wildcard nodes. Wildcard nodes are nodes with the name `match:any`. Wildcards in the pattern will lazily (meaning non greedily) match zero, one or many dom nodes in the given node to test. `` matches anything `` `foo` `` `Hello` matches any node that contains `Hello` as a child: `HelloHello` `Foo Hello Foo` ` Foo Hello` but not this example, because here hello is in a subnode. `Hello` `Foo` matches: `Foo` `Hello Foo` `borg Foo` but not `Foo` `` **Kind**: global constant | Param | Type | | --- | --- | | node |DomNode
| | pattern |DomNode
| ## assertEquivalentNode Assert that two dom nodes are equivalent. The implementation mostly defers to .isEqualNode, but provides better error messages. **Kind**: global constant ## dumpDOM prints dom in order for changes to be more discernible. **Kind**: global constant | Param | Type | Description | | --- | --- | --- | | actual |object
| node from original page | | expected |object
| node from test domain page | | level |number
| current level in recursion tree return dump of dom that is indented at every level by level*2 spaces | ## getData(request, [opts]) ⇒Promise.<object>
Extracts the _data_ from the given request. The data can be provided either as request parameters, url-encoded form data body, or a json body. Note that for post body requests, the body is consumed from the request and is no longer available. **Kind**: global function **Returns**:Promise.<object>
- the parsed data object. | Param | Type | Description | | --- | --- | --- | | request |Request
| The universal request | | [opts] |BodyDataOptions
| Options | ## toMetaName(text) ⇒string
Converts all non-valid characters to `-`. **Kind**: global function **Returns**:string
- the meta name | Param | Type | Description | | --- | --- | --- | | text |string
| input text | ## stripQuery(m, ...specialparams) ⇒object
Cleans up the URL by removing parameters that are deemed special. These special parameters will be returned in the return object instead. **Kind**: global function **Returns**:object
- an object with a clean URL and extracted parameters | Param | Type | Description | | --- | --- | --- | | m |object
| the mount point | | m.url |string
| mount point URL | | ...specialparams |string
| list of special parameters that should be removed from the URL and returned in the object | ## getData(request, ...names) ⇒object
Exported only for testisg **Kind**: global function **Returns**:object
- an object with the provided parameter names as keys | Param | Type | Description | | --- | --- | --- | | request |Request
| a fetch-API Request | | ...names |string
| the parameter names to extract | ## match(globs, path, defaultValue) ⇒boolean
Return a flag indicating whether a particular path is matches all given glob patterns. **Kind**: global function **Returns**:boolean
- whether path matches the globs | Param | Type | Description | | --- | --- | --- | | globs |Array.<string>
| globbing patterns | | path |string
| path to check | | defaultValue |boolean
| what to return if `globs` is undefined | ## contains(cfg, path) ⇒boolean
Return a flag indicating whether a particular path is contained in the indexing configuration (include or exclude element). This is true if a path is included and *not* excluded. **Kind**: global function **Returns**:boolean
- whether path is included in configuration | Param | Type | Description | | --- | --- | --- | | cfg |Index
| indexing configuration's | | path |string
| path to check | ## getDOMValue(elements, expression, log, vars) Return a value in the DOM by evaluating an expression **Kind**: global function | Param | Type | | --- | --- | | elements |Array.<HTMLElement>
| | expression |string
| | log |Logger
| | vars |object
| ## indexResource(path, response, config, log) ⇒object
Given a response, extract a value and evaluate an expression on it. The index contains the CSS selector that will select the value(s) to process. If we get multiple values, we return an array. **Kind**: global function **Returns**:object
- extracted properties | Param | Type | Description | | --- | --- | --- | | path |string
| Path of document retrieved | | response |object
| response containing body and headers | | config |Index
| indexing configuration | | log |Logger
| logger | ## dequeue(queue) ⇒Generator.<\*, void, \*>
Simple dequeing iterator. **Kind**: global function | Param | | --- | | queue | ## \_request(target, input) ⇒any
Pass a request to the AWS secrets manager **Kind**: global function **Returns**:any
- response object | Param | Type | Description | | --- | --- | --- | | target |string
| target method to invoke | | input |any
| input that will be passed as JSON | ## reset() reset the cache - for testing only **Kind**: global function ## loadSecrets(ctx, [opts]) ⇒Promise.<object>
Loads the secrets from the respective secrets manager. **Kind**: global function **Returns**:Promise.<object>
- the secrets or {@code null}. | Param | Type | Description | | --- | --- | --- | | ctx |UniversalContext
| the context | | [opts] |SecretsOptions
| Options | ## multiline() This is a helper for declaring multiline strings. ``` const s = multiline(` Foo Bar Baz Hello Bang `); ``` The function basically just takes a string and then strips the first & last lines if they are empty. In order to remove indentation, we determine the common whitespace prefix length (number of space 0x20 characters at the start of the line). This prefix is simply removed from each line... **Kind**: global function ## lookupBackendResponses(status) ⇒Object
A glorified lookup table that translates backend errors into the appropriate HTTP status codes and log levels for your service. **Kind**: global function **Returns**:Object
- a pair of status code to return and log level to use in your code | Param | Type | Description | | --- | --- | --- | | status |int
| the HTTP status code you've been getting from the backend | ## computeSurrogateKey(url) ⇒Promise.<string>
Computes the caching Surrogate-Key for the given url. The computation uses a hmac_sha256 with a fixed key: {@code "helix"}. the result is base64 encoded and truncated to 16 characters. This algorithm is chosen, because similar functionality exists in Fastly's VCL api: ``` declare local var.key STRING; set var.key = digest.hmac_sha256_base64("helix", "input"); set var.key = regsub(var.key, "(.{16}).*", "\1"); ``` **Kind**: global function **Returns**:Promise.<string>
- A promise with the computed key. | Param | Type | Description | | --- | --- | --- | | url |\*
| The input url. | ## propagateStatusCode(status) ⇒int
What is the appropriate status code to use in your service when your backend responds with `status`? This function provides a standardized lookup function to map backend responses to gateway responses, assuming you are implementing the gateway. **Kind**: global function **Returns**:int
- the appropriate HTTP status code for your app | Param | Type | Description | | --- | --- | --- | | status |int
| the backend HTTP status code | ## logLevelForStatusCode(status) ⇒string
What is the appropriate log level for logging HTTP responses you are getting from a backend when the backend responds with `status`? This function provides a standardized lookup function of backend status codes to log levels. You can use it like this: ```javascript logger[logLevelForStatusCode(response.status)](response.message); ``` **Kind**: global function **Returns**:string
- the correct log level | Param | Type | Description | | --- | --- | --- | | status |int
| the HTTP status code from your backend | ## cleanupHeaderValue(value) ⇒ Cleans up a header value by stripping invalid characters and truncating to 1024 chars **Kind**: global function **Returns**: a valid header value | Param | Type | Description | | --- | --- | --- | | value |string
| a header value | ## hashContentBusId(value) ⇒ Compute an SHA digest from some string value. **Kind**: global function **Returns**: SHA256 digest of value, shortened to 59 characters | Param | Type | Description | | --- | --- | --- | | value |string
| value to create digest for |