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
GETrequest (provided the client allows following302redirects).
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
servicesection to the Actor profile, which is required for author/controller validation. - (Protocol change) Enabling http
302redirect responses when an Actor profile request is made that has the required query parameters (serviceandrelativeRefparams).
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
GETrequest on the Object or Collection, as usual, including any currently required authorization headers. - The client performing the
GETrequest MUST be able to support HTTP redirection. For example, if using the WHATWGfetchAPI, the request'sredirectproperty cannot be set toerror. - The Client follows the redirect and automatically fetches the object specified
in the
Locationheader of the302response (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
Locationheader of the redirect response; for example, if using the WHATWGfetchAPI, 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
actororattributedToproperty). -
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
serviceproperty. b. If theserviceproperty 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=storagequery parameter refers to, in the Actor-Relative URL). The Client extracts theserviceEndpointproperty 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 theserviceproperty, or if theserviceproperty isnullor an empty array, or if theservicearray does not contain a service endpoint object with a relativeidthat ends in#storage, or if that service endpoint does not contain aserviceEndpointproperty 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
relativeRefquery 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 therelativeRefquery 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
GETmechanism that it currently does. - Note: An Actor-Relative URL is defined as a URL containing the
serviceandrelativeRefquery parameters. - The client MUST follow the
302redirect 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 theserviceandrelativeRefquery 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
serviceEndpointthat corresponds to theservicequery parameter, the server MUST return a422 Unprocessable EntityHTTP status code error. To determine whether the profile contains a valid service endpoint: -
If the Actor profile does not contain a top level
serviceproperty: INVALID - If the Actor has a
serviceproperty, but its value isnullor[]: 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
serviceEndpointcontained in the profile concatenated with the contents of therelativeRefquery parameter (see below for example). -
Return a
302 FoundHTTP status code response, and set theLocationresponse header to the value of the current location URL composed in the previous step. Note: Servers SHOULD NOT return a301status 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 Otherstatus 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.