# Local Storage Storage
This project implements a simple API to synchronize your localStorage between
multiple devices.
## API
### API Key
While there is no automatic way to retrieve your API key right now, you need
one. Ask the hoster of this instance directly. An API key will always be bound
to a project identifier and a set of domains, and only allow frontends hosted
on those domains to access this API using standard CORS headers.
The data you need:
* Project identifier (referenced in this document by `{project}`)
* API key (referenced in this document by `MyApiKey`)
If the API key is unknown or does not match the domain or project, you'll receive
an authorization error for any of the documented requests. To not leak any
information no more details will be provided:
HTTP/1.1 403 Forbidden
{ok: false}
All API responses contain proper CORS headers to make it possible to access the
API from the domains in the include list for the given project. `OPTION`
requests are also properly answered because of this:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: my-domain.example
Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: User-Agent, Authorization, Origin, Content-Type, Accept
Access-Control-Expose-Headers: *
Access-Control-Allow-Credentials: true
Vary: Origin
### Store local storage
If there is no data stored yet, you can store data using a `PUT` request. All
data will always be stored under an ID, which probably will identify the user
of your application.
PUT https://local-storage-storage.io/api/{project}/{id}
Origin: my-domain.example
Authorization: Bearer MyApiKey
{myLocalStorageData}
Responses:
HTTP/1.1 201 Created
{ok: true, revision: "{newDataRevision}"}
If the project does not exist:
HTTP/1.1 403 Forbidden
{ok: false}
If there is already something stored under the given ID:
HTTP/1.1 409 Conflict
{ok: false}
### Get local storage
GET https://local-storage-storage.io/api/{project}/{id}
Origin: my-domain.example
Authorization: Bearer MyApiKey
Responses:
HTTP/1.1 200 OK
{
data: "{myLocalStorageData}",
project: "{project}",
revision: "{dataRevision}",
ok: true
}
If the ID does not exist:
HTTP/1.1 404 Not Found
{ok: false}
### Update local storage
If there is already data stored for an ID, you can update it using a `POST`
request. There are no partial updates possible. For an update you have to
provide the `revision` of the data.
The revision is used for Multi-Version-Concurrency-Control (MVCC), which means
that if multiple clients try to update the data conflicts can be detected and
handled on the client side. The revision will update any time the stored
data is changed.
POST https://local-storage-storage.io/api/{project}/{id}?revision={revision}
Origin: my-domain.example
Authorization: Bearer MyApiKey
{myLocalStorageData}
Responses:
HTTP/1.1 200 OK
{ok: true, revision: "{newDataRevision}"}
If the ID does not exist:
HTTP/1.1 404 Not Found
{ok: false}
If there is already something stored under the given ID and the revision does
not match:
HTTP/1.1 409 Conflict
{ok: false}
### Delete local storage
If there is already data stored for an ID, you can remove it using a `DELETE`
request. Like for any update you have to provide the `revision` of the data.
DELETE https://local-storage-storage.io/api/{project}/{id}?revision={revision}
Origin: my-domain.example
Authorization: Bearer MyApiKey
Responses:
HTTP/1.1 200 OK
{ok: true}
If the ID does not exist:
HTTP/1.1 404 Not Found
{ok: false}
If there is already something stored under the given ID and the revision does
not match:
HTTP/1.1 409 Conflict
{ok: false}
### Proxy request
An additional API method to proxy `GET` requests to other servers through this
API. This is useful for APIs which are not able to provide proper CORS headers
for your current domain.
Some headers of the request (`Accept`, `Accept-Language`, `Cache-Control`,
`Pragma`, `Referer` and `Cookie`) are passed on to the requested URL, if
provided. Redirects are followed. The response body is converted to UTF-8 and
always returned as `text/html; charset=UTF-8`; headers describing the original
body, CORS, cookies or the connection are not passed back, everything else is.
Any status below 400 is returned as `200 OK`.
GET https://local-storage-storage.io/proxy/{project}?url={urlEncodedUrl}
Origin: my-domain.example
Authorization: Bearer MyApiKey
Responses:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: my-domain.example
Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: User-Agent, Authorization, Origin, Content-Type, Accept
Access-Control-Expose-Headers: *
Access-Control-Allow-Credentials: true
Vary: Origin
{original response from URL}
Only `http` and `https` URLs of hosts on the public internet can be proxied.
Anything else — another scheme, credentials in the URL, a host resolving to a
loopback or private address, or a redirect to one:
HTTP/1.1 400 Bad Request
{ok: false, message: "This URL cannot be proxied"}
If the server could not be reached, or redirected more than five times:
HTTP/1.1 502 Bad Gateway
{ok: false, message: "The proxied server could not be reached"}
## Running your own
PHP 8.4 with Composer. The only runtime dependency is
[kore/framework](https://github.com/kore/framework).
### Configuration
`local-storage-storage.json`, next to `public/`, or wherever the `LSS_CONFIG`
environment variable points. It is not part of this repository, because it
differs per installation, and a deploy never touches it:
{
"storage": "database",
"projects": {
"my-project": {
"apiKey": "…",
"domains": ["https://my-domain.example", "http://localhost:3000"]
}
}
}
* `storage` — the directory data is stored in, relative to the configuration
file unless absolute. It has to be writable by the web server, and should be
outside of the document root.
* `projects` — the key of each entry is the `{project}` identifier. `domains`
are origins exactly as a browser sends them: scheme, host, and the port if it
is not the default — no path and no trailing slash. The server refuses to
start with anything else, instead of silently never matching.
* `exposeInternalErrors` (default `false`) — tell clients what an unexpected
error was. Only for development.
A random key: `openssl rand -hex 16`.
The web server's document root is `public/`, and every request that is not a
file there goes to `public/index.php`.
### Development
make serve # http://127.0.0.1:8089, using local-storage-storage.json
make validate # coding standard, PHPStan, and both test suites
make deploy # build and copy to the server, never touching configuration or data
How the code is organised, and why, is in `guidelines.md`.