“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:
GET request
(provided the client allows following 302 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.
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:
service section to the Actor profile, which
is required for author/controller validation.302 redirect responses when an Actor profile
request is made that has the required query parameters (service and
relativeRef params).In addition:
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:
GET request on the Object or Collection, as
usual, including any currently required authorization headers.GET request MUST be able to support HTTP
redirection. For example, if using the WHATWG fetch API, the request’s
redirect property cannot be set to error.Location header of the 302 response (this behavior is the default
in most HTTP clients).Location header of the redirect response; for example, if using
the WHATWG fetch API, this is the last URL in the response’s URL list,
retrievable by accessing response.url.actor or attributedTo 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 the service 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 the service=storage
query parameter refers to, in the Actor-Relative URL). The Client extracts
the serviceEndpoint 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 the service property,
or if the service property is null or an empty array, or if the
service array does not contain a service endpoint object with a relative
id that ends in #storage, or if that service endpoint does not contain
a serviceEndpoint 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 the
relativeRef 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.
An ActivityPub client conforming to this FEP:
GET mechanism that it currently does.
service and
relativeRef query parameters.302 redirect in the response.On the server side (specifically, the server hosting the Actor profile), an ActivityPub server conforming to this FEP:
https://alice-personal-site.example/actor), examine the HTTP QUERY parameters.
If the service and relativeRef 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 the service query parameter,
the server MUST return a 422 Unprocessable Entity HTTP status code error.
To determine whether the profile contains a valid service endpoint:
service property: INVALIDservice property, but its value is null or []: INVALIDservice) 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: INVALIDAssuming that a matching service endpoint is found, compose a current location URL
from the serviceEndpoint contained in the profile concatenated with the contents
of the relativeRef query parameter (see below for example).
302 Found HTTP status code response, and set the Location response header
to the value of the current location URL composed in the previous step.
Note: Servers SHOULD NOT return a 301 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 a 303 See Other status
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
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).
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:
https://old-storage-provider.example/AP/objects/567https://alice-personal-site.example/actor?service=storage&relativeRef=/AP/objects/567When 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.
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.