“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/567
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.
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.