FEP-e3e9: Actor-Relative URLs
Summary
"All problems in computer science can be solved by another level of indirection" (the "fundamental theorem of software engineering")
-- Attributed to: Butler Lampson (src)
This FEP introduces an ID scheme for ActivityPub objects and collections that has the following properties:
- IDs remains stable across domain migrations. That is, allows the controller of the objects to change object hosting providers without changing the object IDs.
- IDs are regular HTTP(S) URLs that are resolvable via an HTTP
GET
request (provided the client allows following302
redirects).
The proposed mechanism identifies objects by adding query parameters to existing
Actor profile URLs. ActivityPub clients wishing to fetch the objects make an
HTTP GET
request to this URL, as usual, carrying whatever authentication
mechanism is required currently, and then follow the HTTP 302
status code
redirect in the response to the current storage location of the object.
Example Actor-Relative URL:
https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567
An AP client, encountering an Object ID with this URL makes an HTTP GET
request
just as it would with any other Object ID:
GET /actor?service=storage&relativeRef=/AP/objects/567 HTTP/1.1
Host: alice-personal-site.example
The server responds with a 302
redirect (which all HTTP clients are able
to automatically follow) pointing to the current storage location of the object.
For example:
HTTP/1.1 302 Found
Location: https://storage-provider.example/users/1234/AP/objects/567
This redirection mechanism is enabled in all existing HTTP clients by default (see https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect), and requires no additional re-tooling of ActivityPub client code.
Actor-Relative URLs for Objects and Collections
On the Client side, the main change required is in the author/controller validation procedure (since retrieving the objects at Actor-Relative URLs requires no additional change beyond ensuring that following HTTP redirects is not disabled).
On the Server side (specifically, the server hosting the Actor profile), two changes are required:
- (Data Model change) Adding a
service
section to the Actor profile, which is required for author/controller validation. - (Protocol change) Enabling http
302
redirect responses when an Actor profile request is made that has the required query parameters (service
andrelativeRef
params).
In addition:
- (Not required but recommended) Implementing FEP-8b32: Object Integrity Proofs is recommended, since it helps with author/controller validation even in the case that the Actor profile host is down or otherwise unavailable.
Validating an Object's Author/Controller
Given the following example Actor profile:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://www.w3.org/ns/did/v1"
],
"service": [{
"id": "https://alice-personal-site.example/actor#storage",
"serviceEndpoint": "https://storage-provider.example"
}],
// Rest of the Actor profile goes here
}
When fetching an ActivityPub Object or Collection identified by an Actor-Relative
URL (that is, when the Object or Collection ID contains the URL query parameters
service
and relativeRef
), a client MUST validate that the server hosting
the Object is authorized by the Actor profile:
- The Client performs an HTTP
GET
request on the Object or Collection, as usual, including any currently required authorization headers. - The client performing the
GET
request MUST be able to support HTTP redirection. For example, if using the WHATWGfetch
API, the request'sredirect
property cannot be set toerror
. - The Client follows the redirect and automatically fetches the object specified
in the
Location
header of the302
response (this behavior is the default in most HTTP clients). - The Client extracts the current URL of the Object. This is the URL specified
in the
Location
header of the redirect response; for example, if using the WHATWGfetch
API, this is the last URL in the response's URL list, retrievable by accessingresponse.url
. - The Client retrieves the Actor profile corresponding to this Object's author/
controller (the
actor
orattributedTo
property). -
The Client extracts the value of the authorized storage endpoint from the profile:
a. The Client checks to see if the Actor profile contains the
service
property. b. If theservice
property is found, the Client searches through the array of service endpoints until it finds a service endpoint with the relative id ending in#storage
(note: this is what theservice=storage
query parameter refers to, in the Actor-Relative URL). The Client extracts theserviceEndpoint
property of this service description object. This is the authorized storage endpoint. c. If no authorized storage endpoint is specified in the Actor profile (that is, if the Actor profile does not contain theservice
property, or if theservice
property isnull
or an empty array, or if theservice
array does not contain a service endpoint object with a relativeid
that ends in#storage
, or if that service endpoint does not contain aserviceEndpoint
property containing a URL), the Client SHOULD indicate to the user that the provenance of this Object cannot be determined, or that the storage location of the Object has not been authorized by the profile of the claimed author/controller. -
The Client MUST validate that the current URL of the object is authorized by the Actor's profile by checking that:
a. The Object's currentURL starts with the value of the authorized storage endpoint. b. The Object's currentURL ends with the value of the
relativeRef
query parameter. c. For example, in JS pseudocode, using string concatenation:response.url === (authorizedStorageEndpoint + query.relativeRef)
d. If these checks fail (if the current URL of the object is not equal to the string concatenation of the authorized storage endpoint and therelativeRef
query parameter), the Client SHOULD indicate to the user that the provenance of this Object cannot be determined, or that the storage location of the Object has not been authorized by the profile of the claimed author/controller.
This validation procedure establishes a two-way link: from the Object to its
author/controller Actor profile (via the Object's actor
or attributedTo
property), and from the Actor profile to the authorized storage service provider,
at whose domain the Object is currently stored.
Client-Side Implementation
An ActivityPub client conforming to this FEP:
- When encountering an Actor-Relative URL as an ID of an object, fetch it using the same
HTTP
GET
mechanism that it currently does. - Note: An Actor-Relative URL is defined as a URL containing the
service
andrelativeRef
query parameters. - The client MUST follow the
302
redirect in the response. - The client MUST perform the validation steps outlined in the Validating an Object's Author/Controller section above.
Server-Side Implementation
On the server side (specifically, the server hosting the Actor profile), an ActivityPub server conforming to this FEP:
- For every request to the Actor profile object (for example, to
https://alice-personal-site.example/actor
), examine the HTTP QUERY parameters. If theservice
andrelativeRef
query parameters are present in the request, treat this as an Actor-Relative URL Request (by following the steps below). -
Examine the Actor profile object for this request. If the profile does not contain a valid
serviceEndpoint
that corresponds to theservice
query parameter, the server MUST return a422 Unprocessable Entity
HTTP status code error. To determine whether the profile contains a valid service endpoint: -
If the Actor profile does not contain a top level
service
property: INVALID - If the Actor has a
service
property, but its value isnull
or[]
: INVALID -
Search through the array of service endpoints (the value of the
service
) property, until you find a service object with the id that ends in<actor profile url>#<contents of the 'service' query param>
. See sample Actor profile and request below. If no valid service endpoint is found: INVALID -
Assuming that a matching service endpoint is found, compose a current location URL from the
serviceEndpoint
contained in the profile concatenated with the contents of therelativeRef
query parameter (see below for example). -
Return a
302 Found
HTTP status code response, and set theLocation
response header to the value of the current location URL composed in the previous step. Note: Servers SHOULD NOT return a301
status response (a 301 response implies a permanent relocation, and the whole point of this FEP is that Actor-Relative URLs are changeable at any point). Similarly, servers SHOULD not return a303 See Other
status response.
Example Server-Side Request and Response
Example request URL:
GET https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567
The query parameters would be parsed on the server side as something similar to:
{ "service": "storage", "relativeRef": "/AP/objects/567" }
Example Actor profile at that URL:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://www.w3.org/ns/did/v1"
],
"service": [{
"id": "https://alice-personal-site.example/actor#storage",
"serviceEndpoint": "https://storage-provider.example"
}],
// Rest of the Actor profile goes here
}
Example current location URL (from concatenating the serviceEndpoint
value with the
relativeRef
query parameter): https://storage-provider.example/AP/objects/567
Example response from the server:
HTTP/1.1 302 Found
Location: https://storage-provider.example/AP/objects/567
Object Storage Migration Using Actor-Relative URLs
Actor-Relative URLs can be used as an option for portable Object and Collection IDs that remain unchanged even through migrating to a different object hosting provider (as long as the Actor ID remains constant).
Example Storage Provider Migration
Before migration, Alice uses the https://old-storage-provider.example
as a
storage provider for her AP objects. She makes sure https://old-storage-provider.example
is specified as a service endpoint in her Actor profile.
GET https://alice-personal-site.example/actor
returns
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://www.w3.org/ns/did/v1"
],
"id": "https://alice-personal-site.example/actor",
"type": "Person",
"service": [{
"id": "https://alice-personal-site.example/actor#storage",
"serviceEndpoint": "https://old-storage-provider.example"
}],
"assertionMethod": { /* … */ },
// All the other profile properties …
}
Alice then creates a Note and stores it with the storage provider (making sure to add an Object Identity Proof). Example request:
POST /AP/objects/
Host: old-storage-provider.example
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "This is a note",
"attributedTo": "https://alice-personal-site.example/actor",
"id": "https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567"
}
returns
HTTP 201 Created
Location: https://old-storage-provider.example/AP/objects/567
Note that this created Object can now be fetched at TWO different URLs:
- The direct URL (also called current location URL),
https://old-storage-provider.example/AP/objects/567
- The indirect Actor-Relative URL
https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567
When it comes time to migrate to a different service provider, the new one being
located at https://brand-new-storage.example
, Alice performs the following steps.
She updates her Actor profile service endpoint, to point to the new provider, so that it looks like this:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://www.w3.org/ns/did/v1"
],
"id": "https://alice-personal-site.example/actor",
"type": "Person",
"service": [{
"id": "https://alice-personal-site.example/actor#storage",
"serviceEndpoint": "https://brand-new-storage.example"
}],
"assertionMethod": { /* … */ },
// All the other profile properties …
}
Note that the serviceEndpoint
is the only property in the Actor profile that
has to change during migration.
Alice then transfers her Object to the new provider (for this example, she'll be transferring the object individually, though in future FEPs, we expect specification of APIs to transfer all of the objects in one's storage):
POST /AP/objects/
Host: brand-new-storage.example
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "This is a note",
"attributedTo": "https://alice-personal-site.example/actor",
"id": "https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567"
}
returns:
HTTP 201 Created
Location: https://brand-new-storage.example/AP/objects/567
Notice that the object being stored at the new provider is byte-for-byte
identical to the object hosted at the old provider; its indirect id
and
contents do not change.
Throughout this service provider migration, the external indirect id
of the
object does not change, for the purposes of all other AP mechanisms such as
Inbox delivery, Likes and Reposts, and so on.
参考文献
-
Christine Lemmer Webber, Jessica Tallon, [ActivityPub][AP], 2018
- S. Bradner, Key words for use in RFCs to Indicate Requirement Levels, 1997
Copyright
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
To the extent possible under law, the authors of this Fediverse Enhancement Proposal have waived all copyright and related or neighboring rights to this work.