Some notes on how I think the API works from observing traffic.
fetch
- Standard JavaScriptfetch
- The majority of API requests come through here I think
electron
- Requests coming from the Electron app itself- I don't actually know how this differs from
fetch
- I don't actually know how this differs from
rsapi
- A Rust-based (!!) wrapper around some sync + encryption functionality, currently not open-source
All endpoints are HTTP POST
and prefixed with https://<API-DOMAIN>/file-sync/<api path>
unless otherwise noted.
Request
{
"Files":["e.<random hex>"],
"GraphUUID":"<uuid>"
}
Response
[
{
"FilePath":"e.<same random hex>",
"Checksum":"<16-byte hex checksum>",
"LastModified": <unix timestamp milliseconds>,
"Size":343,
"Txid":1
}
]
Can also return not found
, I think for new files
[
{
"FilePath":"e.<random hex>",
"Error":"not found <user uuid>/<graph uuid>/e.<same random hex>"
}
]
Self-hosted sync is currently single-user, so:
- In proxy mode, it just asks the real Logseq API for user info
- In not-proxy mode, it just makes stuff up
Request: {}
Response
{
"ExpireTime":<unix timestamp seconds>,
"UserGroups":[<group labels>],
"ProUser":<bool>,
"StorageLimit":<some number of bytes>,
"GraphCountLimit":<n>,
"LemonRenewsAt":null,
"LemonEndsAt":null,
"LemonStatus":null
}
Request: null
Response
{
"Graphs":[
{
"GraphStorageLimit":<large number of bytes>,
"GraphName":<graph name>,
"GraphUUID":<graph uuid>,
"GraphStorageUsage":<smaller number of bytes>
}
]
}
Request: {"GraphName":<graph name>}
Response: {"GraphUUID":"<graph uuid>","TXId":0}
Or if the graph already exists: {"message":"graph[<graph name>] already exists "}
Request: {"GraphUUID":"<graph uuid>"}
No response body, HTTP 200
Request: {"GraphUUID":"<graph uuid>"}
Response
{
"public-key": "age<age public key>",
"encrypted-private-key": "-----BEGIN AGE ENCRYPTED FILE-----\n<base64 encoded, encrypted data, starts with "age-encryption.org/v1">>\n-----END AGE ENCRYPTED FILE-----\n"
}
Response is HTTP 404 with no body if it doesn't exist
Request: {"GraphUUID":"<graph uuid>"}
Response
{
"value":"<64-byte Base64 salt>",
"expired-at":<unix timestamp milliseconds>
}
Request: {"GraphUUID":"<graph uuid>"}
Response
{"value":"<64-byte Base64 salt>","expired-at":<unix timestamp milliseconds>}
Response is HTTP 410 with no body if it doesn't exist (I think)
Request
{
"encrypted-private-key":"-----BEGIN AGE ENCRYPTED FILE-----\n<base64 encoded, encrypted data, starts with "age-encryption.org/v1">\n-----END AGE ENCRYPTED FILE-----\n",
"GraphUUID":"<graph uuid>",
"public-key":"age<age public key>"
}
Response is HTTP 200 with no body
Endpoint is just missing pagination, but otherwise is functional
Request: {"GraphUUID":"<graph uuid>"}
Response
{
"Objects": [
{
"Key": "<user uuid>/<graph uuid>/e.<35-bytes hex-encoded>",
"LastModified": <unix ts millis>,
"checksum": "<16-bytes hex-encoded>",
"Size": <size in bytes>,
"Txid": <tx num>
},
{
"Key": "<user uuid>/<graph uuid>/e.<35-bytes hex-encoded>",
"LastModified": <unix ts millis>,
"checksum": "<16-bytes hex-encoded>",
"Size": <size in bytes>,
"Txid": <tx num>
},
{
"Key": "<user uuid>/<graph uuid>/e.<35-bytes hex-encoded>",
"LastModified": <unix ts millis>,
"checksum": "<16-bytes hex-encoded>",
"Size": <size in bytes>,
"Txid": <tx num>
},
{
"Key": "<user uuid>/<graph uuid>/e.<35-bytes hex-encoded>",
"LastModified": <unix ts millis>,
"checksum": "<16-bytes hex-encoded>",
"Size": <size in bytes>,
"Txid": <tx num>
},
{
"Key": "<user uuid>/<graph uuid>/e.<35-bytes hex-encoded>",
"LastModified": <unix ts millis>,
"checksum": "<16-bytes hex-encoded>",
"Size": <size in bytes>,
"Txid": <tx num>
}
],
"NextContinuationToken": ""
}
Notes:
- Keys are all different
- Txid not all the same either
Request: {"GraphUUID":"<graph uuid>"}
Response: {"TXId":<a number>}
The endpoint just returns nothing, I still need to figure out the shape of this endpoint.
Request
{
"GraphUUID":"<graph uuid>",
"FromTXId":<a number>
}
Response: {"Transactions":[]}
Note: This comes from rsapi
Request
{
"Files":["e.<hex>"],
"GraphUUID":"<graph uuid>"
}
Response
{
"PresignedFileUrls": {
"e.<file>": "https://logseq-file-sync-bucket-prod.s3.amazonaws.com/<user>/<graph>/e.<file>?<lots of params for temporary access to read from S3>"
}
}
Response
{
"Credentials": {
"AccessKeyId": "<AWS key ID",
"Expiration": "YYYY-MM-DDTHH:MM:SSZ",
"SecretKey": "<random string>",
"SessionToken": "<long string>"
},
"S3Prefix": "logseq-file-sync-bucket-prod/temp/us-east-1:<random, but stable uuid>"
}
I think this gives access to some random scratchpad to upload files, which are then moved into place?
Request
{
"Files": {
"e.<hex1>": [
"temp/us-east-1:<stable UUID from above>/<random string>",
"<probably a checksum>"
],
"e.<hex2>": [
"temp/us-east-1:<stable UUID from above>/<different random string>",
"<probably a checksum>"
],
"e.<hex3>": [
"temp/us-east-1:<stable UUID from above>/<different random string>",
"<probably a checksum>"
],
"e.<hex4>": [
"temp/us-east-1:<stable UUID from above>/<different random string>",
"<probably a checksum>"
]
},
"GraphUUID": "<graph uuid>",
"TXId": 0
}
Response
{
"TXId": 1,
"UpdateFailedFiles": {},
"UpdateSuccFiles": [
"<user id>/<graph id>/e.<file ids>",
"<user id>/<graph id>/e.<file ids>",
"<user id>/<graph id>/e.<file ids>",
"<user id>/<graph id>/e.<file ids>"
]
}
GET https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing
Specified here, separate from API endpoint in the code. Seems like just a "do we have internet?" check.
Nothing in the request body, HTTP 304 in the response
I have data on these ones and just need to clean it up a bit.
-
GET https://<API-DOMAIN>/logseq/version
-
/update_files
I'm aware of these ones and just need to capture them better
- /delete_files
- /rename_files