+ When asked to send a wallet address request, given a walletAddress perform the following steps.
+
+
+ Let |walletAddressUrl:URL| be the result of running [=URL parser=] with |walletAddress|.
+
+
+ If |walletAddressUrl| [=url/scheme=] is not `https`, return failure.
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/URL=]
+
|walletAddressUrl|
+
[=request/method=]
+
"GET"
+
[=request/body=]
+
null
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to `Accept` and value set to `application/json`.
+
[=request/mode=]
+
"cors"
+
+
+
+ Let |details| be null.
+
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody|
+ set to the following steps, given a [=response=] |response| and |response|'s [=response/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|:
+
+
+ Convert |json| to a {{ WalletAddressDetails }},
+ |walletAddressDetails: WalletAddressDetails|, and
+ include |walletAddressUrl| as the
+ {{WalletAddressDetails/url}} property.
+
+
+ If one of the previous two steps threw an exception, return failure.
+
+ When asked to send an outgoing payment and quote grant request,
+ given |walletAddressDetails:WalletAddressDetails|, |budget:number|
+ and |interval:DOMString| perform the following steps.
+
+
+ Let |authServer:URL| be the result of running [=URL parser=]
+ with |walletAddressDetails|'s {{WalletAddressDetails/authServer}} property.
+
+
+
Construct a {{ GrantRequest }} |grantRequest: GrantRequest| as follows:
+
+
+ Set |grantRequest|'s {{GrantRequest/client}} to |walletAddressDetails|'s {{WalletAddressDetails/id}} property.
+
+
+ Set |grantRequest|'s {{GrantRequest/interact}} to a new {{ Interact }} with {{Interact/start}} set to `["redirect"]`.
+
+
+ Let |quoteAccess| be a new {{ Access }} object with:
+
+
{{Access/type}} set to `"quote"`
+
{{Access/actions}} set to `["create"]`
+
+
+
+ Let |assetScale| be |walletAddressDetails|'s {{WalletAddressDetails/assetScale}} property.
+
+
+ Let |scaledValue| be Math.floor(parseFloat(|budget|) * Math.pow(10, |assetScale|)).
+
+
+ Let |debitAmount| be a new {{ Amount }} object with:
+
+
{{Amount/value}} set to the result of calling {{Number/toString()}} on |scaledValue|
+
{{Amount/assetScale}} set to |assetScale|
+
{{Amount/assetCode}} set to |walletAddressDetails|'s {{WalletAddressDetails/assetCode}} property
+
+
+
+ Let |accessLimits| be a new {{ AccessLimits }} object with:
+
+
{{AccessLimits/debitAmount}} set to |debitAmount|
+
{{AccessLimits/interval}} set to |interval|
+
+
+
+ Let |paymentAccess| be a new {{ Access }} object with:
+
+
{{Access/type}} set to `"outgoing-payment"`
+
{{Access/actions}} set to `["create", "read"]`
+
{{Access/identifier}} set to walletAddressDetails's {{WalletAddressDetails/id}} property
+
{{Access/limits}} set to |accessLimits|
+
+
+
+ Set |grantRequest|'s {{GrantRequest/access_token}} to a new {{ RequestAccessToken }} with:
+
+
{{RequestAccessToken/access}} set to a list containing |quoteAccess| and |paymentAccess|
+
+
+
+
+
+
Let request be a new [=request=] as follows:
+
+
[=request/URL=]
+
|authServer|
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
the result of running [=serialize a JavaScript value to a JSON string=] on |grantRequest|
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to `Content-Type` and value set to `application/json`.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request|.
+
+
+ Let |grant| be null.
+
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to
+ the following steps, given a [=response=] and |response|'s [=request/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|.
+
+
+ Convert |json| to a {{ PendingGrant }}, |grantResponse:PendingGrant|.
+
+
+ If one of the previous two steps threw an exception, return failure.
+
+
+ Set |grant| to |grantResponse|.
+
+
+
+
+
+
+
+
+
+
+
+
Incoming payment grant request
+
The incoming payment grant request refers to
+ the Open Payments grant request.
+ This request is non-interactive: implementations MUST NOT include an {{GrantRequest.interact}} and
+ the authorization server will return a {{Grant}} directly if successful.
+
+ When asked to send an incoming payment grant request, given |websiteWalletAddressDetails:WalletAddressDetails|
+ perform the following steps.
+
+
+ Let |userWalletDetails:WalletAddressDetails?| be the result of [=get user wallet=].
+
+
If |userWalletDetails| is null, then return failure.
+
+ Let |authServer:URL| be the result of running [=URL parser=] with |websiteWalletAddressDetails|'s {{WalletAddressDetails/authServer}} property.
+
+
+
Construct a {{ GrantRequest }} |grantRequest: GrantRequest| as follows:
+
+
+ Set |grantRequest|'s {{GrantRequest/client}} to |userWalletDetails|.{{WalletAddressDetails/id}}.
+
+
+ Let |incomingPaymentAccess| be a new {{ Access }} object with:
+
+
{{Access/type}} set to `"incoming-payment"`
+
{{Access/actions}} set to `["create"]`
+
{{Access/identifier}} set to |websiteWalletAddressDetails|'s {{WalletAddressDetails/id}} property
+
+
+
+ Set |grantRequest|'s {{GrantRequest/access_token}} to a new {{ RequestAccessToken }} with:
+
+
{{RequestAccessToken/access}} set to a list containing |incomingPaymentAccess|
+
+
+
+
+
+
Let request be a new [=request=] as follows:
+
+
[=request/URL=]
+
authServer
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
the result of running [=serialize a JavaScript value to a JSON string=] on |grantRequest|
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to `Content-Type` and value set to `application/json`.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request|.
+
+
Let |grant| be null.
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to the following steps,
+ given a [=response=] and |response|'s [=request/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|.
+
+
+ Convert |json| to a {{ Grant }}, |grantResponse:Grant|.
+
+
+ If one of the previous two steps threw an exception, return failure.
+
+
+ Set |grant| to |grantResponse|.
+
+
+
+
Return |grant|.
+
+
+
+
The access token returned in the {{Grant}} is used to authorize the subsequent
+ create incoming payment request.
+ When asked to send a continue grant request, given |continueUri:DOMString|, |accessToken:DOMString|, perform the following steps.
+
+
+ Let |uri:URL| be the result of running [=URL parser=] with |continueUri|.
+
+
+ If |uri| [=url/scheme=] is not `https`, return failure.
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/URL=]
+
|uri|
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
"none"
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to `Content-Type` and value set to `application/json`.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request| with |accessToken|.
+
+
Let |grant| be null.
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to the following steps,
+ given a [=response=] |response| with [=response/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|.
+
+
+ Convert |json| to a {{ PendingGrant }} or {{ Grant }}, |grantResponse|.
+
+ The continuation request can return either a {{ PendingGrant }} or a {{ Grant }}.
+
+
+
+ If one of the previous two steps threw an exception, return failure.
+
+ When asked to send a cancel grant request, given |continueUri|, |accessToken| perform the following steps.
+
+
+ Let |uri| be the result of running [=URL parser=] with |continueUri|.
+
+
+ If |uri| [=url/scheme=] is not `https`, return failure.
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/url=]
+
|uri|
+
[=request/method=]
+
"DELETE"
+
[=request/body=]
+
"none"
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to `Content-Type` and value set to `application/json`.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request| with |accessToken|.
+
+
+ Perform a [=fetch request=] with |request|, given a [=response=]:
+
+
+ If {{"NetworkError"}} is thrown, notify the user that the browser could not cancel the grant.
+
+
+ If the [=status=] is `204`, the grant was successfully canceled and the storage can be cleared.
+
+ When clearing the storage all user data should be removed. The only properties
+ that should not be cleared are: {{Storage/privateKey}}, {{Storage/publicKey}}, {{Storage/kid}}.
+
+ When asked to send a rotate access token request, given {{Grant}} |userWalletGrant:Grant| perform the following steps.
+
+
+ Let |manageUrl:URL| be the result of running [=URL parser=] with
+ |userWalletGrant|.{{Grant/access_token}}.{{AccessToken/manage}}.
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/URL=]
+
|manageUrl|
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
null
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
an empty list
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request|
+ with |userWalletGrant|.{{Grant/access_token}}.{{AccessToken/value}}.
+
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to the following steps,
+ given a [=response=] |response| and |response|'s [=response/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|.
+
+
+ If |json| does not have an `access_token` property that is an object, return failure.
+
+
+ Convert |json|.access_token to an {{ AccessToken }}, |rotatedAccessToken|.
+
+ When asked to send a create incoming payment request,
+ given |websiteWalletAddressDetails:WalletAddressDetails|,
+ |incomingPaymentGrant:Grant|, perform the following steps.
+
+
+ Let |resourceServer:URL| be the result of running [=URL parser=] with
+ |websiteWalletAddressDetails|'s {{WalletAddressDetails/resourceServer}} property.
+
+
+ Let |incomingPaymentsUrl:URL| be the result of running [=URL parser=] with the string formed by
+ concatenating |resourceServer| and "/incoming-payments".
+
+
+ Let |expiresAt:DOMString| be the current date-time plus 10 minutes, formatted as an ISO 8601 string.
+
The `expiresAt` field is set to the current date plus 10 minutes.
+ We assume users typically spend about 5 minutes on a website, making it unnecessary to keep the session active longer.
+ Ideally, the browser should terminate the session when the user navigates away from the site.
+ However, if that doesn’t happen for any reason, the expiration date will ensure the session is ended.
+ If the session expires before the user leaves the website, the browser should reinitialize the session.
+
+
+
+
Let |body| be a new JavaScript object constructed as follows:
+
+
Set |body|.walletAddress to |websiteWalletAddressDetails|'s {{WalletAddressDetails/id}}.
+
Set |body|.expiresAt to |expiresAt|.
+
+
`incomingAmount` is not set, as the amount is not fixed
+ and the same incoming payment is reused for the session duration.
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/URL=]
+
|incomingPaymentsUrl|
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
the result of running [=serialize a JavaScript value to a JSON string=] on |body|
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to Content-Type and value set to application/json.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request| with |incomingPaymentGrant|.{{Grant/access_token}}.{{AccessToken/value}}.
+
+
+ Let |incomingPaymentId:DOMString| be null.
+
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to the following steps,
+ given a [=response=] and |response|'s [=request/body=] |responseBody|:
+
+
+ Let |json| be the result of [=extract the JSON fetch response=] from |response| and |responseBody|.
+
+
+ If |json| does not have an id property whose value is a string, return failure.
+
+ When asked to send a create outgoing payment request,
+ given |userWalletAddressDetails:WalletAddressDetails|,
+ |incomingPaymentId:DOMString|,
+ |debitAmountValue:DOMString|,
+ and |outgoingPaymentGrant:Grant|, perform the following steps.
+
+
+ Let |resourceServer:URL| be the result of running [=URL parser=] with
+ |userWalletAddressDetails|'s {{WalletAddressDetails/resourceServer}} property.
+
+
+ Let |outgoingPaymentsUrl:URL| be the result of running [=URL parser=] with the string formed by
+ concatenating |resourceServer| and "/outgoing-payments".
+
+
+
Let |body| be a new JavaScript object constructed as follows:
+
+
Set |body|.walletAddress to |userWalletAddressDetails|'s {{WalletAddressDetails/id}}.
+
Set |body|.incomingPayment to |incomingPaymentId|.
+
+ Set |body|.debitAmount to a new object with:
+
+
{{Amount/value}} set to |debitAmountValue|
+
{{Amount/assetCode}} set to |userWalletAddressDetails|'s {{WalletAddressDetails/assetCode}}
+
{{Amount/assetScale}} set to |userWalletAddressDetails|'s {{WalletAddressDetails/assetScale}}
+
+
+
+
+
+
Let |request| be a new [=request=] as follows:
+
+
[=request/URL=]
+
|outgoingPaymentsUrl|
+
[=request/method=]
+
"POST"
+
[=request/body=]
+
the result of running [=serialize a JavaScript value to a JSON string=] on |body|
+
[=request/redirect mode=]
+
"follow"
+
[=request/client=]
+
null
+
{{RequestInit/window}}
+
null
+
[=request/service-workers mode=]
+
"none"
+
[=request/destination=]
+
"monetization"
+
[=request/header list=]
+
a list containing a single header with name set to Content-Type and value set to application/json.
+
[=request/mode=]
+
"cors"
+
+
+
+ Run the [=generate an HTTP message signature=] algorithm on |request|
+ with |outgoingPaymentGrant|.{{Grant/access_token}}.{{AccessToken/value}}.
+
+
+ Let |result:OutgoingPaymentResult| be {{OutgoingPaymentResult/"error"}}.
+
+
+ Perform a [=fetch request=] with |request| and with |processResponseConsumeBody| set to the following steps,
+ given a [=response=] |response| and |response|'s [=response/body=] |responseBody|:
+
+
+ If |response|'s [=status=] is `201`, then set |result| to {{OutgoingPaymentResult/"success"}}.
+
+
+ If |response|'s [=status=] is `403`, then set |result| to {{OutgoingPaymentResult/"token-invalid"}}.
+
+
+
+
+ Return |result|.
+
+
+
+
+
+
+
+
+
+
Helper algorithms
+
+
Fetch request
+
+ To perform a fetch request given a [=request=] request and
+ an algorithm |processResponseConsumeBody|, execute the following steps:
+
+
+ [=Queue a global task=] on the [=networking task source=] to [=fetch=] |request|
+ with [=fetch/processResponseEndOfBody=] set to |processResponseConsumeBody|.
+
+
+
+
+
+
+
Extract JSON from fetch response
+
+
To extract the JSON fetch response a [=response=] response:
+
+
+ If |response| is a [=network error=] or its [=status=] is not an [=ok status=], throw a new {{"NetworkError"}}.
+
+
+ Let |mimeType| be the result of [=header list/extract a MIME type=] from |response|'s [=header list=].
+
+
+ If |mimeType| is failure or is not a [=JSON MIME type=], throw a new {{"NetworkError"}}.
+
+
+ Let |json| be the result of [=parse JSON bytes to an infra value=] passing |responseBody|.
+
+
+ If |json| is a parsing exception, throw a new {{"NetworkError"}}.
+
+
+ Return |json|.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/specification/flows/section/authentication.html b/src/pages/specification/flows/section/authentication.html
new file mode 100644
index 00000000..8405c0f8
--- /dev/null
+++ b/src/pages/specification/flows/section/authentication.html
@@ -0,0 +1,333 @@
+
+
Authentication
+
This section defines the key material and requirements for generating and managing cryptographic keys used to
+ create HTTP message signatures for Open Payments.
+
+
All authenticated Open Payments protocol operations initiated by the user agent and sent to either
+ the user's wallet provider or a website's wallet provider
+ (e.g. grant requests, quote creation, outgoing payment creation) MUST be signed.
+
+
The validity of signatures is established because the user, during setup or an authorization procedure,
+ registers the [=encoded public key JWK=] with their wallet provider. The wallet provider then uses
+ that public key to verify subsequent signed requests (for example, grant creation, quote requests,
+ and outgoing payment creation).
+
+
+ Reusing a single long-lived signing key across
+ multiple origins or wallet providers can enable cross-site tracking.
+ Future revisions may define per-session or per-origin derived authentication
+ keys to further reduce tracking surfaces. See the
+ directed identity proposal for background.
+
+
+
User agents MUST use an [=Ed25519 key pair=] to sign HTTP requests that
+ require proof-of-possession as described in this specification.
+
+
A generated key pair is persisted in {{Storage}}.
+
+
The user agent SHOULD display the [=encoded public key JWK=] on its [=setup page=] to help
+ the user register it with their wallet provider.
+
+
+ Regenerating the key pair invalidates the association the user previously
+ made between their public key and their wallet. The user would have to
+ re-upload or re-register the new public key and repeat any interactive
+ authorization flows.
+
+
+
+
Ed25519 key pair
+
The Ed25519 key id (kid) is an opaque identifier that MUST be
+ unique among the active signing keys for a single wallet address.
+
+
An Ed25519 key pair is
+ the ordered pair of a [=private key=] (seed) and its corresponding [=public key=] as defined
+ in [[RFC8032]]. It is identified by a [=key id=].
+
+ The Ed25519
+ private key is the 32‑octet seed. It is
+ conceptually an opaque 32‑element octet array (e.g.
+ std::array<uint8_t, 32>) and SHOULD be stored and persisted
+ only in that seed form. Any expanded form needed for signing MAY be
+ derived transiently in memory and SHOULD be zeroized after use. The seed MUST
+ remain local to the user agent and MUST NOT be exposed to web content or
+ transmitted.
+
+
+ The Ed25519
+ public key is the 32-octet encoded public group element. In other words,
+ it is the canonical public key bytes produced from the corresponding [=private key=].
+ The public key is represented as a [=public key JWK=].
+
+
+
+
+
Public key representations
+
+ A public key JWK is a [[[RFC7517]]] with OKP key type.
+
+
+
+
+
+ An encoded public key JWK is the base64 encoding (per [[RFC4648]])
+ of the UTF-8 JSON serialization of a [=public key JWK=]. This representation is what user agents persist and display.
+
+
+
+
+
The [=encoded public key JWK=] is intended to be shared with the user's wallet provider so the wallet
+ can validate signatures on Open Payments requests.
+
+
A wallet provider exposes the registered signing public key(s) via
+ an endpoint such as
+ wallet-address/{address}/keys endpoint.
+ This allows verifiers to retrieve the current public key material associated with a wallet address.
+
+
+
+
+
Constructing the encoded public key JWK
+
+ This algorithm constructs the [=encoded public key JWK=] representing a [=public key=]
+ given the raw 32‑octet public key bytes and a [=key id=].
+
+
+ To construct an encoded public key JWK,
+ given |rawPublicKeyBytes:byte sequence| (32 octets) and |keyId:DOMString| (the [=key id=]):
+
+
Let |x:DOMString| be |rawPublicKeyBytes| base64url encoded (no padding) per [[RFC4648]].
+
+ Let |jwk| be a new JSON object.
+
+
Set |jwk|.kty to "OKP".
+
Set |jwk|.crv to "Ed25519".
+
Set |jwk|.alg to "EdDSA".
+
Set |jwk|.x to |x|.
+
Set |jwk|.kid to |keyId|.
+
+
+
Let |json:DOMString| be the UTF-8 JSON serialization of |jwk|.
+
Let |encodedPublicKeyJwk:DOMString| be |json| base64 encoded per [[RFC4648]].
+
Return |encodedPublicKeyJwk|.
+
+
+
The `x` parameter MUST omit base64 padding characters (`=`).
+
Member order in JSON is not significant; implementations MUST accept any ordering when parsing.
+
+
+
+
The `AuthKeys` dictionary
+
The {{AuthKeys}} dictionary stores the [=Ed25519 key pair=] used for HTTP message signatures.
The [=key id=] associated with the key pair, unique per user agent instance.
+
publicKey
+
The [=encoded public key JWK=].
+
privateKey
+
+ Contains the 32‑octet [=private key=] seed as a PrivateKeySeed
+ (a Uint8Array length 32).
+
+
+
+
+
+
Generating an Ed25519 key pair
+
+ This algorithm creates a fresh Ed25519 key pair and persists it
+ as {{Storage/keys}} in {{Storage}}.
+
+
+ To generate an Ed25519 key pair:
+
+
Let |keyId:DOMString| be a newly generated UUID v4 ([[RFC9562]]).
+
Generate a new [=Ed25519 private key=] and corresponding [=Ed25519 public key=] per [[RFC8032]].
+
Let |rawPublicKeyBytes| be the raw public key bytes.
+
Let |encodedPublicKeyJwk:DOMString| be the result of running the [=construct an encoded public key JWK=] algorithm
+ given |rawPublicKeyBytes| and |keyId|.
+
Let |privateKeySeed:PrivateKeySeed| be the 32-octet seed bytes.
+
+ Let |authKeys:AuthKeys| be a new {{AuthKeys}} instance with the following members:
+
+
{{AuthKeys/keyId}} field set to |keyId|
+
{{AuthKeys/privateKey}} field set to |privateKeySeed|
+
{{AuthKeys/publicKey}} field set to |encodedPublicKeyJwk|
+
+
+
+ Run the [=store auth keys=] algorithm with |authKeys|, overwriting
+ any existing value.
+
+
+
+
+
+
+
HTTP message signatures
+
User agents MUST create HTTP message signatures for authenticated Open Payments requests using the Ed25519 key pair.
+ The following algorithm defines signature construction.
+
+ To generate an HTTP message signature, given an HTTP request
+ and an optional GNAP authorization |accessToken:DOMString|:
+
+
Let |authKeys:AuthKeys?| be the result of the [=get auth keys=] algorithm.
+
If |authKeys| is null, then fail.
+
If request's [=request/body=] is not `null`, then:
+
+
+
Handle the `Content-Type` header as follows:
+
+
Let mediaType be the result of [=header list/extract a MIME type=] from request's headers.
+
If mediaType is not null, [=header list/append=] `Content-Type`
+ header name with value mediaType.
+
+
+
+
Set the `Content-Digest` header as follows:
+
+
Let |digestValue| be the result of computing a `sha-512` digest over the exact octets
+ that will be transmitted as request's body.
+
[=Set a structured header=] with headerName `Content-Digest`, key `sha-512`, and |digestValue| as value.
+
+
The value is serialized as a structured byte sequence (base64-encoded)
+ and wrapped in colons per [[RFC9651]].
+
+
+
Handle the `Content-Length` header as follows:
+
+
Let length be the byte length of the transmitted body.
+
If the environment permits setting `Content-Length`,
+ [=header list/append=] `Content-Length` header name with value length.
+
+
+
+
+ In implementations where the request is sent using the [=fetch=] mechanism, the `Content-Length`
+ header is treated as a [=forbidden request-header=], and manually setting it is not allowed.
+ Attempting to do so results in a `net::ERR_INVALID_ARGUMENT` error.
+ The {{WindowOrWorkerGlobalScope/fetch()}} automatically computes and appends it before sending
+ the request.
+
+
That said, since `Content-Length` is part of the signature, its value MUST still be computed and
+ included in the signature input.
+
+
+
+
Let |algorithm:DOMString| be `ed25519`.
+
Let |privateKey| be |authKeys|'s {{AuthKeys/privateKey}}.
+
Let |keyId:DOMString| be |authKeys|'s {{AuthKeys/keyId}}.
+
Let |createdTimestamp:integer| be the current UNIX timestamp in whole seconds.
+ Sub-second precision is not included.
Let |components| initial value be `<<"@method", "@target-uri">>`.
+
If |accessToken| is given, then:
+
+
[=header list/Append=] an `Authorization` header whose value is the concatenation of
+ the literal string `GNAP`, a single U+0020 SPACE character, and |accessToken|
+ (that is, `GNAP` + " " + |accessToken|).
+
Append `"authorization"` to |components|.
+
+
+
If request's body is not `null`, append `"content-length"` and `"content-digest"`; if a `Content-Type` header will be sent, also append `"content-type"`.
+
Header field names in |components| MUST be lowercase.
+
Set |components|'s parameters to include
+ `keyid`=|keyId| and `created`=|createdTimestamp|. Implementations MAY include `expires` and `nonce` when available.
+ The `alg`=`"ed25519"` parameter may also be included but not required.
+
Let |signatureBase:byte sequence| be the serialization of |components|
+ (the covered components list; this corresponds to the |signature base| defined by [[RFC9421]]).
+
+
+
+
+
+ Compute the signature |signatureValue|:
+
+
Use the Ed25519 |privateKey| to sign |signatureBase| directly
+
Let |signatureValue| be the resulting signature byte sequence.
+
+
+
+
Set the `Signature-Input` header as follows:
+
+
Let |signatureInputValue:DOMString| be the serialization of |components| per [[RFC9421]] for a `Signature-Input` field value.
+
Append a `Signature-Input` header with value sig1= followed by |signatureInputValue|. If a `sig1` item already exists, its value MUST be replaced.
+
+
+
+
Set the `Signature` header as follows:
+
+
[=Set a structured header=] with headerName `Signature`, key `sig1`, and |signatureValue| as value.
+
+
+
+
+
+
+
+
+ To set a structured header, given a string |headerName|, a string |key|, and |value:byte sequence| for a |request:Request|:
+
+
Let |headerValue:structured field dictionary| be a structured field dictionary with a single key |key| and corresponding value |value|.
+
Let |header| be a [=tuple=] of |headerName| and |headerValue|.
+
[=header list/Set a structured field value=] given |header| in the [=request/header list=] of the |request|. The |headerValue| MUST be serialized as described in [[RFC9651]].
+ The original URL that the wallet address information was
+ requested at. Keeping a reference to this URL
+ is useful as users may use proxied wallet addresses.
+
+
+
`id`
+
The wallet address identifier.
+
+
`authServer`
+
corresponds to the endpoint for the [=AS=] APIs.
+
+
`resourceServer`
+
corresponds to the endpoint for the [=RS=] APIs.
+
+
`assetCode`
+
corresponds to [=asset code=].
+
+
`assetScale`
+
corresponds to [=asset scale=].
+
+
`publicName`
+
The public name associated with the wallet address, if available.
+
+
+
+
+
+
Grant Dictionaries
+
These dictionaries define the structure for grant requests and responses used in the Open Payments authorization flow.
+
+
The `Grant` dictionary
+
The {{Grant}} dictionary represents a completed and authorized grant with access tokens.
The access token value for authenticating continuation requests.
+
+
+
The `Continue` dictionary
+
The {{Continue}} dictionary represents continuation information returned from grant requests.
+
+ dictionary Continue {
+ required ContinueAccessToken access_token;
+ required DOMString uri;
+ unsigned short wait; // `wait` is only defined when receiving a pending grant
+ };
+
+
+
`access_token`
+
The access token to use for continuation requests.
+
`uri`
+
The URI to send continuation requests to.
+
`wait`
+
The amount of time in integer seconds the client instance SHOULD wait after receiving this continuation handle and calling the URI.
+
+
+
+
The `Interact` dictionary
+
The {{Interact}} dictionary specifies how user interaction SHOULD be initiated for a pending grant.
The interaction information needed to complete the grant authorization.
+
`continue`
+
The continuation information for polling or continuing the grant request.
+
+
+
+
+
Supporting Dictionaries
+
These supporting dictionaries and enums define reusable value objects and enumerations referenced by the grant-related dictionaries above.
+
+
+
The `Amount` dictionary
+
The {{Amount}} dictionary represents a monetary quantity denominated in the wallet address's
+ asset. The numeric value is expressed as a string of the minor units determined by
+ {{Amount/assetScale}}.
Stringified non‑negative integer of minor units. Scaled by `Math.pow(10, assetScale)`).
+
`assetCode`
+
The ISO code or other identifier for the asset (e.g. "USD").
+
`assetScale`
+
Number of fractional decimal places; defines the power-of-ten scale for value.
+
+
+
The `AccessLimits` dictionary
+
The {{AccessLimits}} dictionary constrains how an access token may be used. Implementations MUST
+ use only one of {{AccessLimits/debitAmount}} or {{AccessLimits/receiveAmount}} per limits object.
+ If both are supplied the request is invalid.
+
+ dictionary AccessLimits {
+ DOMString receiver;
+ DOMString interval;
+ required Amount debitAmount; // only one of the amounts should be used
+ required Amount receiveAmount;
+ };
+
+
+
`receiver`
+
A receiver (destination) address or identifier the access is limited to (optional).
+
`interval`
+
+ Time window that bounds the applicable spend or receive total. Uses an
+ ISO 8601 repeating
+ interval of the form R/start-date-time/period. The period (e.g.
+ P1M) defines the recurrence length; omit fractional repetitions.
+
R/2025-01-28T02:12:16.254Z/P1M
+
+
`debitAmount`
+
Maximum amount that may be debited (spent) under this authorization.
+
`receiveAmount`
+
Maximum amount that may be received under this authorization.
+
+
+
The `Access` dictionary
+
The {{Access}} dictionary specifies a single requested permission scope. Multiple {{Access}}
+ entries MAY appear in a {{RequestAccessToken/access}} list.
+
+ dictionary Access {
+ required AccessType type;
+ required sequence<AccessAction> actions;
+ DOMString identifier;
+ AccessLimits limits; // required only when type is equal to "outgoing-payment"
+ };
+
+
+
`type`
+
The resource or operation category (e.g. "outgoing-payment").
+
`actions`
+
Sequence of allowed actions for this access (e.g. ["create","read"]).
+
`identifier`
+
Target identifier (such as a wallet address id) the access applies to (optional).
+
`limits`
+
Constraints applied when {{Access/type}} is "outgoing-payment"; otherwise omitted.
These dictionaries define internal session records used by the user agent to track active monetization sessions.
+
+
+
The `Session` dictionary
+
+ The {{Session}} dictionary represents an active monetization session. This is an internal user agent
+ structure maintained in the [=active sessions list=] and is not exposed to JavaScript.
+
+ The specification defines the browser implementation for setting up a
+ wallet and managing the payment session flow. To start using web
+ monetization in a browser, the user must add a wallet to enable web
+ monetization payments while browsing the web. When a user visits a [=web monetized website=],
+ the payment session begins, facilitated through a series of
+ [[[openpayments]]] (OP) calls.
+ This document details the procedures and protocols involved.
+
+
+
+ [Ongoing discussions] The specification assumes that the parties are using the same [=asset code=] and [=asset scale=]. Cross currency scenarios will be defined later on.
+
+
+
+ [Ongoing discussions] The specification assumes that a payment sessions will start without user interaction.
+
+ This section describes the payment session flow that occurs when a user visits a [=web monetized website=].
+ The session flow involves establishing a payment session, managing ongoing payments, and handling session lifecycle events.
+
+
+
+
Session lifecycle
+
+ This section defines when sessions are created and ended. It specifies the triggers that invoke the
+ [[[#session-initiation]]] and [[[#session-termination]]] algorithms defined later in this section.
+
+
+
+ When a {{Document}} becomes [=Document/fully active=], the user agent MUST
+ [=initiate document-wide sessions=].
+ If a {{Document}} becomes not [=Document/fully active=], the user agent MUST
+ [=end sessions for document=] for it and all of its [=child navigables=].
+
+
+
+ When the user starts interacting with a [=media element=], the user agent SHOULD
+ [=initiate media-scoped sessions=] given that element; when the user stops interacting, the user agent
+ SHOULD [=end sessions scoped to=] that element.
+
+
+
+ If any of the conditions defined in the
+ link fetch and processing timing
+ cause a monetization link element to be reprocessed, the user agent MUST [=end session=] for the session
+ associated with that element (if any) and then [=initiate a session=] for that element.
+
+
+
+ The user agent maintains an [=active sessions list=] to track all currently running monetization sessions.
+ Each session in this list corresponds to a monetization link element and contains the information needed
+ to distribute payments to the website's wallet.
+
+
+
+
active sessions list
+
+ A user agent maintained ordered list of {{Session}} records created by [=initiate a session=].
+
+
current session iterator
+
+ A user agent maintained iterator that points to the next {{Session}} in the [=active sessions list=] to be processed for payment.
+ This iterator is used to implement round-robin session processing,
+ ensuring fair distribution of payment attempts across all active sessions.
+ When the [=active sessions list=] is empty, the iterator is set to a special end iterator value
+ that represents a position past the last element in the list.
+
+
+
+
+
+
Session initiation
+
+ This section defines the algorithms for initiating payment sessions at various scopes—document-wide,
+ media-scoped, or for individual monetization link elements. Session initiation discovers website wallet details,
+ negotiates authorization, creates an incoming payment, and registers a session record in the [=active sessions list=].
+
+
+
+
To initiate document-wide sessions given a |document:Document|, run these steps:
+
+
If the result of [=get user wallet=] is null, then return.
+
[=initiate sessions scoped to=] the [^head^] element of |document|.
+
For each [=child navigable=] of |document|, recursively [=initiate document-wide sessions=].
+
+
+
+
+
To initiate media-scoped sessions given a |media:HTMLElement|, run these steps:
+
+
If the result of [=get user wallet=] is null, then return.
+
[=initiate sessions scoped to=] |media|.
+
+
+
+
+
To initiate sessions scoped to the |root:HTMLElement|, run these steps:
+
+
Let |document:Document| be the {{Node/ownerDocument}} of |root|.
+
If [=document monetization disabled=] given |document|, then return.
+
Let |linkElements:NodeList| be all [^link^] elements with `rel="monetization"` that are descendants of |root|.
+
For each |linkElement:HTMLLinkElement| in |linkElements|, [=initiate a session=] given |linkElement|.
+
+
+
+
+
To initiate a session, given |linkElement:HTMLLinkElement|, perform the following steps:
+
+
If |linkElement| has [^link/disabled^] attribute, then return.
+
+ Let |websiteWalletAddress:DOMString| be the value of the {{HTMLLinkElement/href}} of |linkElement|.
+
+
+ If [=wallet monetization restricted=] given |document| and |websiteWalletAddress|, then return.
+
+
+ Let |websiteWalletDetails:WalletAddressDetails?| be the result of [=send a wallet address request=] with |websiteWalletAddress|.
+
+
+ If |websiteWalletDetails| is null, fire an `error` event on |linkElement| and return.
+
+
+ Fire a `load` event on |linkElement|.
+
+
+ Let |incomingPaymentGrant:Grant?| be the result of [=send an incoming payment grant request=] with |websiteWalletDetails|.
+
+
+ If |incomingPaymentGrant| is null, return.
+
+
+ Let |incomingPaymentId:DOMString?| be the result of [=send a create incoming payment request=] with |websiteWalletDetails|, and |incomingPaymentGrant|.
+
+
+ If |incomingPaymentId| is null, return.
+
+
+ Let |continueUri:DOMString| be |incomingPaymentGrant|.{{Grant/continue}}.{{Continue/uri}}.
+
+
+ Let |accessToken:DOMString| be |incomingPaymentGrant|.{{Grant/continue}}.{{Continue/access_token}}.{{ContinueAccessToken/value}}.
+
+
+ [=Send a cancel grant request=] with |continueUri| and |accessToken|.
+
The incoming payment grant is no longer needed after the payment bucket has been created. Canceling it ensures proper cleanup and revokes the authorization. Implementations should handle cancellation failures gracefully but can proceed with the session even if cancellation fails, as the grant will expire naturally.
+
+
+ Let |session:Session| be a new {{Session}} with:
+
+
{{Session/linkElement}} set to |linkElement|
+
{{Session/walletDetails}} set to |websiteWalletDetails|
+
{{Session/incomingPaymentId}} set to |incomingPaymentId|
+
+
+
+ Append |session| to the [=active sessions list=].
+
+
+ If this is the first active session:
+
+
Set the [=current session iterator=] to point to |session|.
+
[=Schedule payment streaming tick=].
+
+
+
+
+
+
+
+
+
+
Session termination
+
+ This section defines the algorithms for ending payment sessions at various scopes—document-wide,
+ media-scoped, or for individual monetization link elements. Session termination removes session records
+ from the [=active sessions list=] and cancels scheduled payment work.
+
+
+
+
To end sessions for document, given a |document:Document|, run these steps:
+
+
[=End sessions scoped to=] the [^head^] element of |document|.
+
For each [=child navigable=] of |document|, recursively [=end sessions for document=].
+
+
+
+
+
To end sessions scoped to the |root:HTMLElement|, run these steps:
+
+
Let |linkElements:NodeList| be all [^link^] elements with `rel="monetization"` that are descendants of |root|.
+
For each |linkElement:HTMLLinkElement| in |linkElements|:
+
+
Let |session:Session| be the session record in the [=active sessions list=] whose {{HTMLLinkElement}} is |linkElement|, or null if none exists.
+
If |session| is not null, [=end session=] given |session|.
+
+
+
+
+
+
+
To end session, given a session record |session:Session|, run these steps:
+
+
If |session| is the only session in the [=active sessions list=]:
+
+
Remove |session| from the [=active sessions list=].
+
Set the [=current session iterator=] to the [=end iterator=].
+
+
+
Otherwise:
+
+
If the [=current session iterator=] points to |session|, [=select next session for payment=].
+
Remove |session| from the [=active sessions list=].
+
+
+
The user agent MUST cancel any scheduled payment work for |session|.
+
+
+
+
+
+
Payment streaming
+
+ Once a session is established, the browser manages the ongoing payment stream according to the configured budget and rate limits.
+
+
+
+
To schedule payment streaming tick, run these steps:
+
+
Let |userWallet:UserWallet| be the result of [=get user wallet=].
+
If |userWallet| is null, then return.
+
If the [=active sessions list=] is empty, then return.
+
Let |session:Session| be the session pointed to by the [=current session iterator=].
+
[=Select next session for payment=].
+
Let (|paymentAmount:long long|, |delay:DOMHighResTimeStamp|) be the result of [=compute payment amount and delay=].
+
If |paymentAmount| is 0, then return.
+
Let |amount:PaymentCurrencyAmount| be a new {{PaymentCurrencyAmount}} with
+ {{PaymentCurrencyAmount/value}} set to |paymentAmount| serialized as a string.
+
Schedule a task that, after |delay| has elapsed, will [=process session for payment=] given |session| and |amount|.
+
+
+
+
+
To select next session for payment, run these steps:
+
+
If the [=active sessions list=] is empty, then set the [=current session iterator=] to the [=end iterator=].
+
Otherwise, if the [=current session iterator=] is not at the [=end iterator=],
+ then advance the [=current session iterator=] to the next session in the [=active sessions list=].
+
If the [=current session iterator=] is at the [=end iterator=],
+ then set the [=current session iterator=] to point to the first session in the [=active sessions list=].
+
+
+
+
+
To process session for payment, given |session:Session| and |amount:PaymentCurrencyAmount|, run these steps:
+
+
Let |userWallet:UserWallet| be the result of [=get user wallet=].
+
If |userWallet| is null, then return.
+
If |session| is not in the [=active sessions list=], then return.
+
Let |amountValue:DOMString| be |amount|'s {{PaymentCurrencyAmount/value}}.
+
Let |userWalletGrant:Grant| be |userWallet|'s {{UserWallet/outgoingPaymentGrant}}.
+
Let |result:OutgoingPaymentResult| be the result of [=send a create outgoing payment request=]
+ with |userWallet|'s {{UserWallet/walletAddressDetails}}, |session|'s {{Session/incomingPaymentId}},
+ |amountValue|, and |userWalletGrant|.
+
If |result| is {{OutgoingPaymentResult/"token-invalid"}}:
+
+
Let |rotatedAccessToken:AccessToken| be the result of [=send a rotate access token request=]
+ with |userWalletGrant|.
+
If |rotatedAccessToken| is failure:
+
+
[=Revoke user wallet credentials=].
+
Return.
+
+
+
Set |userWalletGrant|'s {{Grant/access_token}} to |rotatedAccessToken|.
+
Run [=update grant access token=] with |rotatedAccessToken|.
+
Set |result| to the result of [=send a create outgoing payment request=]
+ with |userWallet|'s {{UserWallet/walletAddressDetails}}, |session|'s {{Session/incomingPaymentId}},
+ |amountValue|, and |userWalletGrant|.
+
+
+
If |result| is {{OutgoingPaymentResult/"success"}}:
+
+
[=Fire monetization event=] given |session|, |amount|, and |userWallet|.
+
[=Schedule payment streaming tick=].
+
Return.
+
+
+
[=Revoke user wallet credentials=].
+
+
+
+
+
To fire monetization event, given |session:Session|, |amount:PaymentCurrencyAmount|, and |userWallet:UserWallet|, run these steps:
+
+
Let |assetScale:long| be |userWallet|.{{UserWallet/walletAddressDetails}}.{{WalletAddressDetails/assetScale}}.
+
Let |assetCode:DOMString| be |userWallet|.{{UserWallet/walletAddressDetails}}.{{WalletAddressDetails/assetCode}}.
+
Let |decimalAmount:DOMString| be the result of [=convert scaled amount to decimal=] given |amount|.{{PaymentCurrencyAmount/value}} and |assetScale|.
+
Fire a [=monetization event=] on |session|.{{Session/linkElement}} with:
+
+
{{MonetizationEvent/amountSent}}.{{MonetizationCurrencyAmount/currency}} set to |assetCode|
+
{{MonetizationEvent/amountSent}}.{{MonetizationCurrencyAmount/value}} set to |decimalAmount|
+
{{MonetizationEvent/paymentPointer}} set to |session|.{{Session/walletDetails}}.{{WalletAddressDetails/id}}
+
{{MonetizationEvent/incomingPayment}} set to |session|.{{Session/incomingPaymentId}}
+
+
+
+
+
+
+
To compute payment amount and delay, run these steps:
+
+
Let |storage:Storage| be the result of [=get storage=].
+
+ Let |maxRateOfPayPerMonth:long long| be the result of parsing
+ |storage|.{{Storage/maxRateOfPay}} as a non-negative integer.
+
+
+ If |maxRateOfPayPerMonth| is 0, then return the tuple (0, 1000).
+
A monthly budget of 0 disables payment attempts.
+
+
Let |secondsInMonth:double| be `30 * 24 * 60 * 60`.
+
A "month" is treated as a fixed 30-day period for payment pacing calculations.
+
Let |defaultPaymentAmountPerSecond:double| be `|maxRateOfPayPerMonth| / |secondsInMonth|`.
+
Let |minSendableAmount:long long| be 1.
+
Let |paymentAmount:long long| be 0.
+
Let |paymentDelay:DOMHighResTimeStamp| be 1000.
+
If |defaultPaymentAmountPerSecond| is greater than or equal to |minSendableAmount|,
+ then set |paymentAmount| to `Math.floor(|defaultPaymentAmountPerSecond|)`.
+
+ Otherwise:
+
+
Set |paymentAmount| to |minSendableAmount|.
+
Set |paymentDelay| to `Math.ceil(1000 * (|paymentAmount| / |defaultPaymentAmountPerSecond|))`.
+
When using the minimum sendable amount, |defaultPaymentAmountPerSecond| is less than 1, so this computation yields a delay greater than 1000ms (ensuring the long-run average spend does not exceed {{Storage/maxRateOfPay}}).
+
+
+
Return the tuple (|paymentAmount|, |paymentDelay|).
+
+
+
+
+
To convert scaled amount to decimal, given |scaledAmount:long long| and |assetScale:long|, run these steps:
+
+
Let |divisor:double| be `Math.pow(10, |assetScale|)`.
+
Let |decimalValue:double| be |scaledAmount| divided by |divisor|.
+
Return a string representation of |decimalValue| formatted with |assetScale| decimal places.
+
+
For example, `convert scaled amount to decimal(100, 2)` returns `"1.00"`.
+
+
+
+
To revoke user wallet credentials, run these steps:
+
+
Run [=update grant access token=] with the empty string.
+
For each |session| in the [=active sessions list=]:
+
+
Remove |session| from the [=active sessions list=].
+
+
+
Set the [=current session iterator=] to the [=end iterator=].
+ User agents SHOULD provide controls to disable Web Monetization completely and on a per-[=origin=]
+ basis via a wallet management interface. User agents SHOULD provide a user interface that allows
+ users to view and adjust these preferences.
+
+
+
+ A website can disable Web Monetization for itself and/or its subdocuments by setting
+ the `monetization`
+ [=policy-controlled feature=].
+
+
+
+ A website can also restrict Web Monetization to wallet addresses of certain [=origin=]s
+ by using the `monetization-src`
+ directive in the [=content security policy=].
+
+
+
+
Document monetization restrictions
+
+
+ Document monetization disabled
+ given a |document:Document| if either of the following conditions apply:
+
+
+
+
+ The user preferences specify that Web Monetization is either completely disabled or disabled specifically
+ for the [=Document/origin=] of the |document|.
+
+
+ The |document| is not [=allowed to use=]
+ the `monetization`
+ [=policy-controlled feature=].
+
+
+
+
+
+
Wallet address restrictions
+
+
+ Wallet monetization restricted
+ given a |document:Document| and a [=wallet address=] |walletAddress:DOMString|
+ if the `monetization-src` directive
+ in the [=content security policy=] of the |document| restricts the |walletAddress|'s [=origin=].
+
+
+
+
+ These mechanisms provide multiple layers of control over Web Monetization: the user controls whether
+ monetization is enabled in their browser, the website can control whether monetization is allowed
+ via permissions policy, and the website can restrict which wallet addresses are allowed
+ via content security policy. All three mechanisms work together to ensure monetization only occurs when all parties consent.
+
This section defines the browser storage requirements for persisting Web Monetization user data and session state.
+
+
The browser MUST securely store an instance of {{Storage}} to maintain Web Monetization state across page loads and browser restarts. This storage MUST be:
+
+
+
Persistent across browser sessions
+
Encrypted when stored on disk
+
+
+
+ Implementations MUST use secure storage mechanisms such as the browser's credential store, encrypted
+ local storage, or platform-specific secure storage APIs.
+ Private keys and access tokens are particularly sensitive and require the highest level of protection.
+
+
+
+
Storage
+
+
The {{Storage}} dictionary represents the complete user state for Web Monetization, containing wallet information, authentication credentials, active grants, and payment preferences.
+
+
+ dictionary Storage {
+ // Wallet Information
+ WalletAddressDetails? wallet;
+
+ // Authentication Keys
+ AuthKeys? keys;
+
+ // Active Grant Information
+ Grant? grant;
+
+ // Payment Limit
+ DOMString? maxRateOfPay;
+ };
+
+
+
+
wallet
+
Contains the user's wallet address information and server endpoints.
+ Set when user wallet is connected.
+
+
+
keys
+
Contains the cryptographic key pair used for HTTP message signatures.
+ Set when user wallet is connected.
+
+
+
grant
+
Contains the active grant tokens and management URLs.
+ Set when a grant request is successfully completed and approved.
+
+
+
maxRateOfPay
+
+ The user's configured Web Monetization spending budget, expressed as a non-negative integer string in
+ [=scaled amount=] units per month of the user wallet asset.
+ The user agent uses this value to compute the payment amount and delay for streaming payments.
+
+ Configured by the user during wallet connection.
+
+
+
+ Example: if the user sets a monthly spending budget of 10.00 USD and the wallet asset scale is 2,
+ then 10.00 * Math.pow(10, 2) = 1000, so {{Storage/maxRateOfPay}} is stored as "1000".
+
+
+
+ The user's wallet enforces spending limits and renewal via the outgoing payment grant.
+ The user agent's role is to pace outgoing payment attempts based on the configured monthly budget.
+ For pacing calculations, a "month" is treated as a fixed 30-day period.
+
+
+
+
+
+
+
Storage operations
+
+
+
To get storage:
+
+
Let |storage| be the {{Storage}} instance from secure browser storage.
+
If no stored storage exists, return a new {{Storage}} with all properties set to null.
+
Return |storage|.
+
+
+
+
+
To store wallet credentials, given |walletAddressDetails:WalletAddressDetails|:
+
+
Let |storage| be the result of [=get storage=].
+
Set |storage|'s {{Storage/wallet}} to |walletAddressDetails|.
+
[=Persist storage=] with |storage|.
+
+
+
+
+
To store grant credentials, given |grant:Grant|:
+
+
Let |storage| be the result of [=get storage=].
+
Set |storage|'s {{Storage/grant}} to |grant|.
+
[=Persist storage=] with |storage|.
+
+
+
+
+
To store max rate of pay, given |maxRateOfPay:DOMString|:
+
+
Let |storage| be the result of [=get storage=].
+
Set |storage|'s {{Storage/maxRateOfPay}} to |maxRateOfPay|.
+
[=Persist storage=] with |storage|.
+
+
+
+
+
To update grant access token, given |accessToken:DOMString|:
+
+
Let |storage| be the result of [=get storage=].
+
If |storage|'s {{Storage/grant}} is null, return.
+
Set |storage|'s {{Storage/grant}}'s {{Grant/access_token}} to |accessToken|.
+
[=Persist storage=] with |storage|.
+
+
This algorithm updates only the access token within an existing grant,
+ preserving other grant properties. Used during token rotation when the authorization
+ server issues a new access token. When |accessToken| is the empty string, the grant is
+ invalidated but wallet information is preserved for reconnection.
+
+
+
+
To store auth keys, given |authKeys:AuthKeys|:
+
+
Let |storage| be the result of [=get storage=].
+
Set |storage|'s {{Storage/keys}} to |authKeys|.
+
[=Persist storage=] with |storage|.
+
+
+
+
+
To get auth keys:
+
+
Let |storage| be the result of [=get storage=].
+
If |storage|'s {{Storage/keys}} is null, return null.
+
Return |storage|'s {{Storage/keys}}.
+
+
+
+
+
To get user wallet:
+
+
Let |storage| be the result of [=get storage=].
+
If |storage|'s {{Storage/wallet}} is null, return null.
+
Return |storage|'s {{Storage/wallet}}.
+
+
+
+
+
To clear wallet data:
+
+
Let |storage| be the result of [=get storage=].
+
Set |storage|'s {{Storage/grant}} to null.
+
Set |storage|'s {{Storage/keys}} to null.
+
Set |storage|'s {{Storage/wallet}} to null.
+
[=Persist storage=] with |storage|.
+
+
Removes all wallet-related state (grant tokens, authentication keys, wallet address details). Budget limits MAY be retained; implementations can also clear {{Storage/maxRateOfPay}} if the user requests a full reset.
GNAP (Grant Negotitation and Authorization Protocol)
+
+ GNAP defines a mechanism for delegating authorization to a piece of software (client), and conveying the results and artifacts of that delegation to the software. This delegation can include access to a set of APIs as well as subject information passed directly to the software. For more information, refer to [[[RFC9635]]].
+
+
+
AS (Authorization Server)
+
+ A server that grants delegated authorization and privileges, via [=GNAP=], to a particular instance of client software in the form of access tokens, allowing the client to call the Open Payments APIs. We’ve provided an opinionated version of a GNAP authorization server via the auth service, meaning that we’ve made certain decisions regarding the implementation and configuration of the server that may limit customization but ensure consistency and adherence to preferred practices.
+
+
+
RS (Resource Server)
+
A server that hosts and manages access to protected Open Payments resources for incoming payments, quotes, and outgoing payments.
+
+
Grants
+
+ A delegation of authorization from a resource owner to a client, allowing the client to access protected resources or perform actions on the owner’s behalf. In Rafiki, this process is managed by the authorization server, which issues grants as access tokens. These grants permit clients to interact with Open Payments APIs to, for example, create payments and retrieve account information, based on the permissions granted by the resource owner.
+
+
Web monetized website
+
Describes a page/site that has implemented Web Monetization.
+
+ Web Monetization is configured at the page level. A web monetized page is a page that contains a well-formed monetization `link` element. For an entire site to be web monetized, each page of the site must have a `link` element.
+
+
+
ASE (Account Servicing Entity)
+
+ An entity that provides and maintains a payment account for a payer and/or payee. An ASE is a regulated entity within the country or countries it operates. Examples include digital wallets, banks, crypto wallets, and mobile money providers. The ASE can provide Open Payments-enabled accounts for its customers.
+
+
+
IdP (Identity provider)
+
A system or service that stores and manages user identity information, authentication, and consent.
+
+
Web Monetization provider
+
+ An entity or collection of entities that together enable Web Monetization payments to be sent on behalf of an individual when the individual visits a web monetized page. At minimum, a Web Monetization provider is an [=ASE=] that supplies a funded Open Payments-enabled account from which to send payments.
+
+
+
Web Monetization receiver
+
+ An entity or collection of entities that together enable Web Monetization payments to be received and held on behalf of an individual. At minimum, a Web Monetization receiver is an [=ASE=] that supplies an Open Payments-enabled account capable of receiving payments.
+
+
+
Wallet
+
An account within the [=web monetization provider=] or [=web monetization receiver=].
+
+
Wallet address
+
+ A secure, unique URL that identifies an Open Payments-enabled account. It acts as an entry point to the Open Payments APIs, facilitating interactions like sending and receiving payments.
+
+
+ A wallet address is publicly shareable and used to interact with the underlying payment account without compromising its security. Wallet address URLs are treated as case-insensitive, meaning that both lowercase and uppercase variations of the same address will be recognized as identical.
+
+
+
Asset code
+
A string representing the currency of the [=wallet=].
+
+
Asset scale
+
An integer that defines the scale (number of decimal places) for the currency of the [=wallet=].
+
+
Scaled amount
+
+ An integer amount expressed in a [=wallet=]'s scaled units.
+ One scaled unit corresponds to 1 / Math.pow(10, assetScale) of the asset, so a scaled amount equals the decimal amount multiplied by Math.pow(10, assetScale).
+
This section specifies how the user can set their [=wallet address=] for Web Monetization in the user agent.
+
+
+
Terms
+
+
Web Monetization page
+
+ Refers to the browser's UI for managing Web Monetization settings, accessible via a browser specific URI.
+ In Chromium based browser the page can be located at `chrome://webmonetization`.
+
+
Web Monetization setup page
+
+ Refers to the browser's UI for setting up Web Monetization on the [=Web Monetization page=]. The UI lets user specify the [=wallet address=], and the budget.
+ An example of such a UI could be an "Add a Wallet" button that opens a dialog for entering this information, with an "Add" button that starts the wallet connection process when clicked.
+
+
Interactive grant
+
+ Open Payments requires outgoing payment [=grant=] requests to be interactive. When a grant request is interactive, it means explicit interaction by an individual (typically the client’s end user) is a required step in the delegation process.
+
+
+ After the grant request is made, the client must poll for a Grant Continuation request so the [=AS=] knows to issue an access token.
+
+
Redirect URI
+
+ The redirect URI is an URL the user is redirected to in order to accept or decline the outgoing payment grant ([=interactive grant=]) when conneting their [=wallet=] to the browser.
+
+
+
+
+
+
+
+
Connecting a wallet
+
+
The user can initiate the wallet setup from the [=setup page=].
+
+ The [=setup page=] UI allows the user to enter: (1) a [=wallet address=]; (2) a spending budget; and
+ (3) a renewal monthly flag.
+
+
+
For every new wallet connection, the browser MUST generate a fresh key pair by
+ running the [=generate an Ed25519 key pair=] algorithm, replacing any previously stored key pair, and MUST display
+ the newly generated [=encoded public key JWK=] (representing the [=Ed25519 public key=]) so the user can copy or
+ upload it to their wallet provider. This key generation and public key
+ exposure happen before invoking the algorithm below; if no key pair exists at invocation time the algorithm fails.
+
+
To process the wallet setup, given a string |walletAddress:DOMString|
+ (the [=wallet address=] provided by the user),
+ a number |budget:number| (a spending budget requested from the wallet for outgoing payments, expressed in the wallet asset), and
+ a boolean |renewMonthly:boolean| (whether the budget is renewed monthly by the wallet):
+
+
If the result of the [=get auth keys=] algorithm is null, return failure.
+
Let |walletAddressDetails:WalletAddressDetails| be the result of the [=send a wallet address request=] algorithm on |walletAddress|.
+
+ Let |assetScale:long| be |walletAddressDetails|'s {{WalletAddressDetails/assetScale}}.
+
+
+ Let |maxRateOfPay:long long| be Math.round(|budget| * Math.pow(10, |assetScale|)).
+
+ This converts the user-entered monthly budget in the wallet asset into a [=scaled amount=] integer.
+ The resulting value is stored as {{Storage/maxRateOfPay}} and used to compute the payment amount and delay.
+
+
+
+ Run the [=store max rate of pay=] algorithm with |maxRateOfPay| serialized as a string.
+
+
+ Let |interval:DOMString| be an ISO 8601 repeating interval
+ if |renewMonthly| is `true`.
+
+ The starting date for the repeating interval needs to be the users current date (ISO String).
+
+
+
+
+ Let |pendingGrant:PendingGrant| ({{PendingGrant}}) be the result of the [=send an outgoing payment and quote grant request=] algorithm with |walletAddressDetails|, |budget| and |interval|.
+
+ Since we ask for access to outgoing payments, the user will have to interact with the grant (approve or decline). Currently in Open Payments, only the grants that request access to outgoing payments are interactive.
+
+
+
+ Let |continueUri:DOMString| be |pendingGrant|'s {{PendingGrant/continue}}.{{Continue/uri}} property at which the client instance can make continuation requests.
+
+
+ Let |continueAccessToken:DOMString| be |pendingGrant|'s {{PendingGrant/continue}}.{{Continue/access_token}}.{{AccessToken/value}} property.
+
+
+ (TODO: Look into browsing/navigation context, instead of specifying "open in a new tab") Open the |pendingGrant|'s {{PendingGrant/interact}}.{{PendingInteract/redirect}} property ([=Redirect URI=]) in a new tab to redirect the user to their wallet's [=IdP=], where the user can accept or decline the grant.
+
+
+ Let |wait:number| be |pendingGrant|'s {{PendingGrant/continue}}.{{Continue/wait}} property.
+
+
We cannot use the {{PendingInteract/finish}} redirect method as browser's internal URLs can not be opened from an external source.
+ In the Open Payments scenario, the [=AS=] will have to redirect the user to the internal browser page for Web Monetization to continue the flow.
+
Since we cannot use Open Payments' redirect {{PendingInteract/finish}} method,
+ we will have to check every |wait| seconds (polling)
+ to retrieve information about the grant (whether user accepted or rejected the grant).
+
+
+
Let |finalizedGrant| be `null`.
+
+
Start polling to get the information about the grant state every |wait| seconds.
+
+
Let |interval:number| be result of |wait| multiplied by 1000, as |wait| is defined in seconds.
+
+ Let |intervalId| be the result of calling {{ WindowOrWorkerGlobalScope/setInterval() }} with |interval| and the following steps as callback:
+
+
Let |grant| be the result of running the [=send a continue grant request=] algorithm given |continueUri| and |continueAccessToken|.
+
+
If |grant| is failure (not an [=ok status=]), call {{ WindowOrWorkerGlobalScope/clearInterval(id)|clearInterval(intervalId) }}.
+
If |grant| is of type {{ PendingGrant }}, continue polling.
+
If |grant| is of type {{ Grant }}, {{ WindowOrWorkerGlobalScope/clearInterval(id)|clearInterval(intervalId) }} and set |finalizedGrant| to |grant|.
+
+
+
+
+
If |finalizedGrant| is null:
+
+
Run the [=clear wallet data=] algorithm.
+
Return failure.
+
+
+
Run the [=store wallet credentials=] algorithm with |walletAddressDetails|.
+
Run the [=store grant credentials=] algorithm with |finalizedGrant|.
+
+
+
+
+
Disconnecting a wallet
+
The user can disconnect their previously connected [=wallet=] via the [=setup page=] using a UI control
+ (e.g. a "Disconnect Wallet" button) that requires explicit user confirmation (such as a confirmation dialog).
+
+
Disconnecting a wallet revokes the active grant (if any) and clears state from {{Storage}}.
+
+
To disconnect a wallet:
+
+
Let |storage:Storage| be the result of the [=get storage=] algorithm.
+
If |storage|'s {{Storage/grant}} is not null, then:
+
+
Let |grant:Grant| be |storage|'s {{Storage/grant}}.
+
Let |continueUri:DOMString| be |grant|'s {{Grant/continue}}.{{Continue/uri}}.
+
Let |continueAccessToken:DOMString| be |grant|'s {{Grant/continue}}.{{Continue/access_token}}.{{ContinueAccessToken/value}}.
+
Run the [=send a cancel grant request=] algorithm with |continueUri| and |continueAccessToken|.
+
Failure to cancel (e.g. network error) SHOULD surface a non-blocking warning to the user;
+ the browser MAY still proceed with local cleanup.
+
+
+
Run the [=clear wallet data=] algorithm.
+
All grant data, authentication keys, and wallet details are removed.
+ Reconnecting will generate a new key pair requiring re-registration of the public key.