Compare commits

..
Author SHA1 Message Date
Lewis 05246aec58 feat(tranquil-store): gc and hardening
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:57:47 +03:00
Lewis 8d0e0c6810 feat(tranquil-store): whole test suite working
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:57:44 +03:00
Lewis 1720de8650 feat(tranquil-store): repository traits on MetastoreClient
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:57:43 +03:00
Lewis a5c68a3506 feat(tranquil-store): metastore
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:57:43 +03:00
Lewis 9ea33def13 feat(tranquil-store): eventlog
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:54:41 +03:00
Lewis a9e3ec63e1 feat(tranquil-store): blockstore
Lewis: May this revision serve well! <lu5a@proton.me>
2026-04-10 13:54:41 +03:00
HigherOrderLogicandTangled dbc3c23c95 fix(nix): allow fetch Git deps 2026-04-09 21:30:03 +00:00
nekomimi.petandTangled 531ca7c1c0 fix(lexicon): wildcard mime type handling and case insensitivity 2026-04-09 21:28:37 +00:00
nelindandTangled add1ff176b fix(auth): keep lxm optional and dont default to * when no lxm is given in getServiceAuth 2026-04-04 08:52:42 +00:00
nelind bdb200a30c fix(ci): dont run it at all for now. it doesnt work with the currently spindles 2026-04-01 23:45:03 +02:00
nelind e5faba3e73 fix(ci): define CI event run correctly so it actually runs 2026-04-01 23:38:19 +02:00
IslaandTangled 5dc810ceef Further harden systemd service 2026-03-31 22:25:11 +00:00
juneandTangled 6abc0eaca2 feat(plc): support AtprotoLabeler service 2026-03-28 20:54:43 +00:00
nelindandTangled 696940f732 fix(sync): dont keep websockets arround indefinetly if client abruptly closes connection 2026-03-25 16:47:38 +00:00
nelindandTangled f777c288da fix(pds): do service identifier resolution for proxying correctly 2026-03-23 03:00:57 +00:00
teqandTangled 9039648705 fix(nix): generate-lockfile for fix/signal-nix-build-deps 2026-03-23 03:00:23 +00:00
teqandTangled 770a16b043 fix(nix): add hashes for git deps 2026-03-23 03:00:23 +00:00
teqandTangled d37f38e496 fix(nix): add protobuf for spqr 2026-03-23 03:00:23 +00:00
Lewis 282f08f207 feat(rfc): outline tranquil-store idea for comment
Lewis: May this revision serve well! <lu5a@proton.me>
2026-03-22 18:54:51 +02:00
LewisandTangled f07bcb82f7 refactor(sync): extract MST parsing and serialization utilities 2026-03-22 12:51:44 +00:00
LewisandTangled e38343ce09 refactor(oauth): split authorize.rs into domain modules 2026-03-22 12:51:44 +00:00
LewisandTangled e454e99b8a refactor(api): migrate all endpoints to repos accessor pattern 2026-03-22 12:51:44 +00:00
LewisandTangled 19b0ea19c6 refactor(tranquil-pds): consolidate repos, rate limits, validation, and auth helpers 2026-03-22 12:51:44 +00:00
LewisandTangled 318818ad51 feat(signal): add admin UI, frontend, and build changes 2026-03-22 07:14:20 +00:00
LewisandTangled 7c55a5ceb9 feat(signal): add admin endpoints, config, and server wiring 2026-03-22 07:14:20 +00:00
LewisandTangled 9d31ee9ace refactor(comms): extract HTTP retry combinator and integrate signal sender 2026-03-22 07:14:20 +00:00
LewisandTangled e3efcb43ba test(signal): add protocol store integration tests 2026-03-22 07:14:20 +00:00
LewisandTangled eb46824220 feat(signal): add presage client, newtypes, and slot management 2026-03-22 07:14:20 +00:00
LewisandTangled e6c8a44a42 feat(signal): add postgres-backed signal protocol store 2026-03-22 07:14:19 +00:00
Lewis aac6f2818b fix(oauth): fail properly on non-expanded include scopes 2026-03-21 11:36:37 +02:00
Lewis 1a80a33e12 fix(ci): small tweaks 2026-03-21 10:06:31 +02:00
HigherOrderLogicandTangled 00d71fd3d6 feat(ci): publish image on commit 2026-03-21 02:10:23 +00:00
LewisandTangled c8bb4c9076 refactor(api): update delegation, notification prefs, email, meta, and age assurance endpoints 2026-03-20 13:39:11 +00:00
LewisandTangled d123168787 refactor(api): update password, reauth, verify, account_status, and totp endpoints 2026-03-20 13:39:11 +00:00
LewisandTangled b337d2b154 refactor(api): simplify passkey account creation and auth-adjacent server endpoints 2026-03-20 13:39:11 +00:00
LewisandTangled 1e07d674dd refactor(api): rework session login flow to use common credential verification 2026-03-20 13:39:11 +00:00
LewisandTangled a3f96b6367 refactor(api): centralize DID document building, update admin endpoints 2026-03-20 13:39:11 +00:00
LewisandTangled 7b7936d539 refactor(api): extract account provisioning helpers, simplify create_account flow 2026-03-20 13:39:11 +00:00
LewisandTangled 7e3dca4026 refactor(api): update repo batch/delete to use repo_ops, clean up remaining repo endpoints 2026-03-20 13:39:11 +00:00
LewisandTangled 7bc90d5e23 refactor(api): extract repo write lifecycle to repo_ops 2026-03-20 13:39:11 +00:00
350 changed files with 90752 additions and 11632 deletions
+35
View File
@@ -13,9 +13,22 @@ fail-fast = false
test-threads = "num-cpus"
slow-timeout = { period = "30s", terminate-after = 4 }
[profile.sim-pr]
retries = 0
fail-fast = true
test-threads = "num-cpus"
slow-timeout = { period = "60s", terminate-after = 4 }
[profile.sim-nightly]
retries = 0
fail-fast = false
test-threads = "num-cpus"
slow-timeout = { period = "300s", terminate-after = 2 }
[test-groups]
serial-env-tests = { max-threads = 1 }
heavy-load-tests = { max-threads = 4 }
io-heavy-sim = { max-threads = 2 }
[[profile.default.overrides]]
filter = "test(/import_with_verification/) | test(/plc_migration/)"
@@ -29,6 +42,10 @@ test-group = "serial-env-tests"
filter = "binary(ripple_cluster)"
test-group = "serial-env-tests"
[[profile.default.overrides]]
filter = "package(tranquil-signal)"
test-group = "serial-env-tests"
[[profile.default.overrides]]
filter = "binary(whole_story)"
test-group = "heavy-load-tests"
@@ -41,6 +58,20 @@ test-group = "heavy-load-tests"
filter = "binary(repo_lifecycle)"
test-group = "heavy-load-tests"
[[profile.default.overrides]]
filter = "binary(sim_soak)"
slow-timeout = { period = "300s", terminate-after = 10 }
test-group = "io-heavy-sim"
[[profile.default.overrides]]
filter = "binary(metastore_crash) | binary(sim_cross_store) | binary(sim_gc_concurrent) | binary(sim_reachability) | binary(sim_eventlog) | test(/sim_/)"
slow-timeout = { period = "120s", terminate-after = 8 }
test-group = "io-heavy-sim"
[[profile.default.overrides]]
filter = "test(/test_scale_/) | test(/full_backup_and_restore/)"
slow-timeout = { period = "120s", terminate-after = 4 }
[[profile.ci.overrides]]
filter = "test(/import_with_verification/) | test(/plc_migration/)"
test-group = "serial-env-tests"
@@ -53,6 +84,10 @@ test-group = "serial-env-tests"
filter = "binary(ripple_cluster)"
test-group = "serial-env-tests"
[[profile.ci.overrides]]
filter = "package(tranquil-signal)"
test-group = "serial-env-tests"
[[profile.ci.overrides]]
filter = "binary(whole_story)"
test-group = "heavy-load-tests"
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kyber_pre_keys",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "0295414146e4ddacad638486443fd1036609ab8443390820be8696f708be3812"
}
@@ -1,32 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT seq, did, commit_cid\n FROM repo_seq\n WHERE event_type = 'commit'\n AND prev_cid IS NULL\n AND (blocks_cids IS NULL OR array_length(blocks_cids, 1) IS NULL OR array_length(blocks_cids, 1) = 0)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "seq",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "commit_cid",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
true
]
},
"hash": "033ed5638ef6e30f1648c1acdbc477c173798cb57eb0127c3a582a9633fa5012"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1 AND seq < $2\n ORDER BY seq ASC",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
"describe": {
"columns": [
{
@@ -50,26 +50,36 @@
},
{
"ordinal": 9,
"name": "block_cids",
"type_info": "ByteaArray"
},
{
"ordinal": 10,
"name": "block_data",
"type_info": "ByteaArray"
},
{
"ordinal": 11,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 10,
"ordinal": 12,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 13,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 14,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 15,
"name": "rev",
"type_info": "Text"
}
@@ -94,8 +104,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "d8524ad3f5dc03eb09ed60396a78df5003f804c43ad253d6476523eacdebf811"
"hash": "0349b2af4d21105029ecc2bf81ad7be9987ec17ac088730596849d7a45558e57"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_sender_keys\n (address, device_id, identity, distribution_id, record)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (address, device_id, identity, distribution_id) DO UPDATE SET record = $5",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Text",
"Uuid",
"Bytea"
]
},
"nullable": []
},
"hash": "0a3eaf432e99ef22aa6b3b43978740b085f6416a058ea2da6d639ea1cf276eda"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kv",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "166ee5a554e3ddc75496616d9fba75a15cc0c072f493f579b881a4f2300f38c8"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_signed_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "1a061c261518573c29aa55c5d8e2181f1728034e723ed0bc836e928b4c69128f"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_kyber_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "1aac5ab7dbc29f0d0937767ae174e8b47d236b3cdc973974cec413dc65198d64"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kyber_pre_keys (id, identity, record, is_last_resort)\n VALUES ($1, $2, $3, FALSE)\n ON CONFLICT (id, identity) DO UPDATE SET record = $3, is_last_resort = FALSE",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "1d4459863eeddea9ccb5ce5c0eaffd39d56a763728ce5b1f76871265e704e241"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET is_admin = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "1e63d287c619a14e5c07d80e8e54193d2964c8b5e6a855256cb80c4d0cd2c6ea"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_sessions WHERE address = $1 AND identity = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "1f464d947e93274cbc974f8a6f31d8a47eb676df857fa83f08ed1937cf8a6d9d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_base_keys_seen",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "222836500a964ee6fd238ddb128691215cd15d2ede9a1a19e7a580e4d6644ce8"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT t.token, t.expires_at\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "237c2d912e89b7e0e5baa83503a22f158ea1614b5157f6c9e2aba6017fef6b26"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_identities",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "252d42bb50ef918fe414d036979daba055dbadd3cd7e1bdcce4526867be79930"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kv (key, value) VALUES ($1, $2)\n ON CONFLICT (key) DO UPDATE SET value = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "29c834b7870cdbcdcc2d2c9259a04326e20433237ebe25e33dbd39952ae9f976"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_base_keys_seen\n (kyber_pre_key_id, signed_pre_key_id, identity, base_key)\n VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "31edfe22cdd8e5f62322dad01a65165ec9546eaa92c9e372a5a4fce99d938d22"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kv WHERE key = 'registration'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "374be0bd97ed927b15929e2f86d88913b2fec45ab67e4805fb9f1e39b422ee7f"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "3a3eee11be6a9c5fe012d2a634c7ef514fb695ca6a4ba7631d1c5aba87a854a5"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE repo_seq SET blocks_cids = $1 WHERE seq = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"Int8"
]
},
"nullable": []
},
"hash": "3b791fdb8e29043c980963d4d18e1e492c73c39818a8648a7af70555418fb5d1"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_sender_keys",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "4011d789a6b619231f4e2b917753f0b4452f21243e1d95d47dbaa91cac019e64"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM signal_kv WHERE key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4316de453ec1d5ba65a1dbee29599287d42bf87517936b3b61958c0675ce8115"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_sessions",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "43583f8246016d15d9875c4d33855317c41ae07af5676228c0533724590ec6ed"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq = $1",
"describe": {
"columns": [
{
@@ -50,33 +50,42 @@
},
{
"ordinal": 9,
"name": "block_cids",
"type_info": "ByteaArray"
},
{
"ordinal": 10,
"name": "block_data",
"type_info": "ByteaArray"
},
{
"ordinal": 11,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 10,
"ordinal": 12,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 13,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 14,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 15,
"name": "rev",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
@@ -94,8 +103,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "b26bf97a27783eb7fb524a92dda3e68ef8470a9751fcaefe5fd2d7909dead54b"
"hash": "4702a94384ce938d3f5384bc2106a48500f8749134e6bfd1c619a9151f5d3772"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO backlinks (uri, path, link_to, repo_id)\n SELECT unnest($1::text[]), unnest($2::text[]), unnest($3::text[]), $4\n ON CONFLICT (uri, path) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray",
"TextArray",
"TextArray",
"Uuid"
]
},
"nullable": []
},
"hash": "47149c0577ad9e9b9b089820b0c93417769a4a37affe0e3972e324ec27ec532f"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_identities WHERE address = $1 AND identity = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "4bcd896340fc9178e2d97ad572269e8f57561190ec5f94ccfa06ca93f4827e79"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_sessions (address, device_id, identity, record)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (address, device_id, identity) DO UPDATE SET record = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "4d22ba21356bdc84ea549799c77cab8c34ca9a5f0815150254b590fd87509eb8"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT is_last_resort FROM signal_kyber_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "is_last_resort",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "513a0d3b907df1004f2178ebf14fc182f20fc13c5691cbeb7656d428cb6437f9"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kv (key, value) VALUES ('master_key', $1)\n ON CONFLICT (key) DO UPDATE SET value = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bytea"
]
},
"nullable": []
},
"hash": "53814b5db4f051c30573d146007ddc487f4c12c726bb970efe17e325536629e4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, rev)\n VALUES ($1, 'sync', $2, $3)\n RETURNING seq\n ",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, rev, block_cids, block_data)\n VALUES ($1, 'sync', $2, $3, $4, $5)\n RETURNING seq\n ",
"describe": {
"columns": [
{
@@ -13,12 +13,14 @@
"Left": [
"Text",
"Text",
"Text"
"Text",
"ByteaArray",
"ByteaArray"
]
},
"nullable": [
false
]
},
"hash": "bce9edc597592d2e7d672061883639bcdeff80be85e2ecacb6c75d8c027e6710"
"hash": "5996774d097a484f81353d49e4a99b07c30a003377c924add576bfb48ddde29b"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kv (key, value) VALUES ('sender_certificate', $1)\n ON CONFLICT (key) DO UPDATE SET value = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bytea"
]
},
"nullable": []
},
"hash": "5cd556968671fc8ecf57dd7fd40f65e3a168f17bd389ce2cf7ddee27d810cf1d"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "code",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "5cee16f49a727d66b5231a8d07d7f4bcb6a1136fbf3e3d249fd33600772ac80f"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, did, expires_at FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "61f489b4fc42f5b0aaea287cde4415da6f5e96b3a0f36216bdc6dea924b09abd"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_identities (address, identity, record)\n VALUES ($1, $2, $3)\n ON CONFLICT (address, identity) DO UPDATE SET record = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "62dc57cf7ad007a6458df06619fe7e0bf5ffeba195e55793f6bf6d170e053226"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) AS \"count!\" FROM signal_kyber_pre_keys\n WHERE identity = $1 AND is_last_resort = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": [
null
]
},
"hash": "63ecf786e52a52d83232f6d4c8d93d5d7065f5dbfd3cd1e259d7990d02da1114"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_kyber_pre_keys\n WHERE identity = $1 AND is_last_resort = TRUE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "6a91d97ed4f955ce0936eced6318c630f39ebebc52cee584f1b0dacefec984cd"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kv WHERE key = 'master_key'",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "6cb39ddd462ae93fe97d930ce64425ed16271adc4ca066c4d51bb742beffc22b"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "6cbfcd552a7b4eeb6a1e525f2f11e8f432405e3606e3af7d31b9eca32bc28607"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kv (key, value) VALUES ('registration', $1)\n ON CONFLICT (key) DO UPDATE SET value = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bytea"
]
},
"nullable": []
},
"hash": "6f14e2d74ed0c1e0946c62da2a3ddd63344892bdd4022073123604ee17114ce0"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM repo_seq WHERE created_at < $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz"
]
},
"nullable": []
},
"hash": "746ca87e98f6eaa9c060770c0d1bca6f0d4da60df2dc2db088e57bdc914cf233"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at)\n VALUES ($1, $2, 1, TRUE, NOW())\n ON CONFLICT (did) DO UPDATE SET secret_encrypted = $2, verified = TRUE",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "7d3a9f0545943bc6a3a14fcd596aac5cc731c8177d74e504606d7e92c7d0c73f"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_pre_keys (id, identity, record)\n VALUES ($1, $2, $3)\n ON CONFLICT (id, identity) DO UPDATE SET record = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "811af305fef56437151c1cfe27eaccf6312b8234f1217a81a4d36cd5758fd863"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET two_factor_enabled = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "84a1db51a98402323cb86bc19cd2b737f908222ea3426b8bf47d735aff5b6c75"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_profile_keys (uuid, key) VALUES ($1, $2)\n ON CONFLICT (uuid) DO UPDATE SET key = $2\n RETURNING (xmax = 0) AS \"inserted!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "inserted!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Bytea"
]
},
"nullable": [
null
]
},
"hash": "894169c5a40acade1000869bdde61d09984e17a686c1b487004d79897ecb4e4f"
}
@@ -0,0 +1,36 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM comms_queue WHERE user_id = $1 AND comms_type = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
{
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
}
]
},
"nullable": []
},
"hash": "89b0292d8d022fad8f9cda07b9a7870ca6a7ebe904b2d580956b0816b50bcdb7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
"describe": {
"columns": [
{
@@ -50,32 +50,43 @@
},
{
"ordinal": 9,
"name": "block_cids",
"type_info": "ByteaArray"
},
{
"ordinal": 10,
"name": "block_data",
"type_info": "ByteaArray"
},
{
"ordinal": 11,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 10,
"ordinal": 12,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 13,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 14,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 15,
"name": "rev",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
@@ -93,8 +104,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "b8101757a50075d20147014e450cb7deb7e58f84310690c7bde61e1834dc5903"
"hash": "89c8ce80d8c52b4668c34f63e48b51cacc7e8ff300958a192378f1d72de04f9a"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, prev_data_cid, rev)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n RETURNING seq\n ",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, block_cids, block_data, prev_data_cid, rev)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n RETURNING seq\n ",
"describe": {
"columns": [
{
@@ -17,7 +17,8 @@
"Text",
"Jsonb",
"TextArray",
"TextArray",
"ByteaArray",
"ByteaArray",
"Text",
"Text"
]
@@ -26,5 +27,5 @@
false
]
},
"hash": "40e848f48afb50f5e4820445aee874a482333e9090ef14ce44db3fd9721cfff0"
"hash": "97dd3cfe72b74b4dac686c030a07f6eb56dabb2df72815f3248760b4e3f7950e"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\"\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "990bf50e60fc5566639c2c12cd968d154d7b0c6863ad69141653135f98fbc998"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT MAX(id) FROM signal_kyber_pre_keys\n WHERE identity = $1 AND is_last_resort = TRUE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "max",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "9da9a790efa51edd6abcb4b0991b307c3ba7f0a929e114a4e0cb2ffb88adfbbb"
}
@@ -0,0 +1,180 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n id, user_id,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n FROM comms_queue\n WHERE user_id = $1 AND comms_type = $2\n ORDER BY created_at DESC\n LIMIT $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "channel: CommsChannel",
"type_info": {
"Custom": {
"name": "comms_channel",
"kind": {
"Enum": [
"email",
"discord",
"telegram",
"signal"
]
}
}
}
},
{
"ordinal": 3,
"name": "comms_type: CommsType",
"type_info": {
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
}
},
{
"ordinal": 4,
"name": "status: CommsStatus",
"type_info": {
"Custom": {
"name": "comms_status",
"kind": {
"Enum": [
"pending",
"processing",
"sent",
"failed"
]
}
}
}
},
{
"ordinal": 5,
"name": "recipient",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "subject",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "body",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "metadata",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "attempts",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "max_attempts",
"type_info": "Int4"
},
{
"ordinal": 11,
"name": "last_error",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 15,
"name": "processed_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
{
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
},
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
false,
false,
false,
true
]
},
"hash": "9f3f2b36f11e9446915d3ca29ef81e4ada0c6a6d72764116dac4f99a4e09785e"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT device_id FROM signal_sessions\n WHERE address = $1 AND device_id != $2 AND identity = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "device_id",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "9fdcff515573446f0b4340bf94421bb8a425cc65304fa8c3fd153945904136f0"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids, rev)\n VALUES ($1, 'commit', $2, $3::TEXT, $4, $5, $6, $7)\n RETURNING seq\n ",
"query": "\n INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, block_cids, block_data, rev)\n VALUES ($1, 'commit', $2, $3::TEXT, $4, $5, $6, $7, $8)\n RETURNING seq\n ",
"describe": {
"columns": [
{
@@ -16,7 +16,8 @@
"Text",
"Jsonb",
"TextArray",
"TextArray",
"ByteaArray",
"ByteaArray",
"Text"
]
},
@@ -24,5 +25,5 @@
false
]
},
"hash": "52df8ae809b990fa76e3ac45f37e352a9fc8f92f911a7b9c9be2fd9e8032b18c"
"hash": "a325862f897484bb1a8863fd7a1bc3fd17ab20b6e30498f9900daf80609c413d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_profile_keys",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a403c2982978159b120d4bfa6d6fdca5808b667e54b0765b5ada047ce6dac9d6"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC\n LIMIT $2",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1 AND seq < $2\n ORDER BY seq ASC",
"describe": {
"columns": [
{
@@ -50,26 +50,36 @@
},
{
"ordinal": 9,
"name": "block_cids",
"type_info": "ByteaArray"
},
{
"ordinal": 10,
"name": "block_data",
"type_info": "ByteaArray"
},
{
"ordinal": 11,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 10,
"ordinal": 12,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 13,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 14,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 15,
"name": "rev",
"type_info": "Text"
}
@@ -94,8 +104,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "e7aa1080be9eb3a8ddf1f050c93dc8afd10478f41e22307014784b4ee3740b4a"
"hash": "ad784aff9bb5c1f19f90a3b1c3b3694a3e1aa889b0fe3e55cfebb7af8c620256"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE signal_kyber_pre_keys\n SET stale_at = $1\n WHERE identity = $2 AND is_last_resort = FALSE AND stale_at IS NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Text"
]
},
"nullable": []
},
"hash": "adabc98c853ae77678ad6cf1747384706154334cc507eb7542a8b5490f5a1e2a"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_kyber_pre_keys (id, identity, record, is_last_resort)\n VALUES ($1, $2, $3, TRUE)\n ON CONFLICT (id, identity) DO UPDATE SET is_last_resort = TRUE, record = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "b157b94e003376d827b92eab1ff55caa773ed5691e2e555ab1d1f23fc5adcf8e"
}
@@ -0,0 +1,44 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM comms_queue WHERE user_id = $1 AND comms_type = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid",
{
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
}
]
},
"nullable": [
null
]
},
"hash": "b364a2b202bab17c0cdc5f70d23b13841b4d9063d94cd0b09268c3dc41824fd2"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT MAX(id) FROM signal_pre_keys WHERE identity = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "max",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "b558892051ad793b4ae38a76d072e4ea440092663bd0d4ed9d48f5074788157a"
}
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_sender_keys\n WHERE address = $1 AND device_id = $2 AND identity = $3 AND distribution_id = $4",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Text",
"Int4",
"Text",
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "b583ea011f17bd703a1dde860d1cc595a18c33dd0e37b575a71fa78f9fddc96f"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_signed_pre_keys",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c08770c1ca1cb553e9067c26dec9a47d40f4cd3e16461cf0edbbb49020695f18"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM signal_kv WHERE key = 'registration'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Bytea"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "c130944c8791acb05ebf5f7b5b0f82b8c548ea9d516c0e9310fb46ffd3907977"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT blocks_cids, commit_cid\n FROM repo_seq\n WHERE did = $1 AND rev > $2\n ORDER BY seq DESC",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 1,
"name": "commit_cid",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "c15d538fece4cd7db0056f9e0374a2290e26f5766881fd9dbf4f05788845aac0"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "WITH total AS (\n SELECT COUNT(*) AS cnt FROM signal_kyber_pre_keys\n WHERE identity = $1 AND is_last_resort = FALSE\n )\n DELETE FROM signal_kyber_pre_keys\n WHERE identity = $1 AND is_last_resort = FALSE\n AND stale_at IS NOT NULL AND stale_at < $2\n AND (SELECT cnt FROM total) > $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Timestamptz",
"Int8"
]
},
"nullable": []
},
"hash": "c30d8f2bb8a20154b565a93108d3871289a752fb8d0de35a2164619da2c03aa0"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq = $1",
"query": "SELECT seq, did, created_at, event_type as \"event_type: RepoEventType\", commit_cid, prev_cid, prev_data_cid,\n ops, blobs, block_cids, block_data, blocks_cids, handle, active, status, rev\n FROM repo_seq\n WHERE seq > $1\n ORDER BY seq ASC",
"describe": {
"columns": [
{
@@ -50,26 +50,36 @@
},
{
"ordinal": 9,
"name": "block_cids",
"type_info": "ByteaArray"
},
{
"ordinal": 10,
"name": "block_data",
"type_info": "ByteaArray"
},
{
"ordinal": 11,
"name": "blocks_cids",
"type_info": "TextArray"
},
{
"ordinal": 10,
"ordinal": 12,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 11,
"ordinal": 13,
"name": "active",
"type_info": "Bool"
},
{
"ordinal": 12,
"ordinal": 14,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 15,
"name": "rev",
"type_info": "Text"
}
@@ -93,8 +103,10 @@
true,
true,
true,
true,
true,
true
]
},
"hash": "0d32a592a97ad47c65aa37cf0d45417f2966fcbd688be7434626ae5f6971fa1f"
"hash": "c47746c256fd8c9d386551f3bd270e9a96fecdb713642d07d60fb4aeb02c8426"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) AS \"count!\" FROM signal_signed_pre_keys WHERE identity = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "c5310fca925ec4188e761f218e98c11182e5757c40a70a2aa50af0f2896d98e7"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT key FROM signal_profile_keys WHERE uuid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "key",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "c67709e3fb975148fa86d9fd5ed5e1ef805b2f57a2ce5d955629d598e1ab2b80"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kyber_pre_keys\n WHERE id = $1 AND identity = $2 AND is_last_resort = FALSE",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "c749d53e4e38dd53aa2ac59ba2b15b641ace056515c34ec977bf47d74dfd7072"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM signal_kv WHERE key = 'master_key' LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Bytea"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "c86da12578b2e53e0bff374121b22244c4663820cbb6edfd8f0b8c9aa583cad2"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM backlinks WHERE uri = ANY($1::text[])",
"describe": {
"columns": [],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": []
},
"hash": "cffe4c37fe949fbdc3d5cd83ccec5655aae248a0a69dc260d1da9cf1d9ed2c49"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM signal_kv WHERE key = 'sender_certificate' LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Bytea"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "dded08cb4fd0fc9bca55b3cf0c8d22f4e279bc8aedb72000f4c4bc093f61c750"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO signal_signed_pre_keys (id, identity, record)\n VALUES ($1, $2, $3)\n ON CONFLICT (id, identity) DO UPDATE SET record = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text",
"Bytea"
]
},
"nullable": []
},
"hash": "def40c1cfac196a066d81fb0efa0fa7bdf5f242bd13c8b70e47a512c5e9df5b1"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_pre_keys",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e2ed0b4bfe13a99be85311fcc6e3c2dbdb789b9a44c0122f736ead48f4713a5b"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_kyber_pre_keys WHERE id = $1 AND identity = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "eb837730e87ff990c4441d7bdab001d7542d1803702c634a6099365a9740e45a"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT MAX(id) FROM signal_kyber_pre_keys WHERE identity = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "max",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "eea566212ffce269028a8623a12b553c048ef1ea433aa5567e72b2b639f69b0d"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM signal_sessions WHERE address = $1 AND device_id = $2 AND identity = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "efcf513bba65213ed4b71259a7016ffd7b1b8ff85206416a86beba19323a3be7"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT record FROM signal_sessions\n WHERE address = $1 AND device_id = $2 AND identity = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Text",
"Int4",
"Text"
]
},
"nullable": [
false
]
},
"hash": "f332e618e59a298573c4b6b6c7a12252b461a6676f2d797c5dae5a0baee016a7"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, did, public_key_did_key, private_key_bytes, expires_at, used_at\n FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "public_key_did_key",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "private_key_bytes",
"type_info": "Bytea"
},
{
"ordinal": 4,
"name": "expires_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
false,
false,
false,
true
]
},
"hash": "f3b07f153284b6dd1f22c098af8628d85dcdf20dd6273443ff98d02b6f5ecbf1"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT MAX(id) FROM signal_signed_pre_keys WHERE identity = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "max",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "f41749b35c081fc9d0881c7e2fc988e7c4d164ab5996f18e9f3a29fe4d183db9"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password_reset_code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "password_reset_code_expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
}
+24
View File
@@ -0,0 +1,24 @@
when:
- event: []
branch: []
engine: nixery
dependencies:
nixpkgs:
- podman
steps:
- name: Create podman config
command: |
mkdir -p ~/.config/containers
echo "unqualified-search-registries = [\"docker.io\"]" >> ~/.config/containers/registries.conf
- name: Build image
command: |
podman build . -t tranquil-pds:latest -t "tranquil-pds:$TANGLED_COMMIT_SHA"
- name: Publish image
command: |
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" tranquil-pds:latest "atcr.io/tranquil.farm/tranquil-pds:latest"
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" "tranquil-pds:$TANGLED_COMMIT_SHA" "atcr.io/tranquil.farm/tranquil-pds:$TANGLED_COMMIT_SHA"
Generated
+2301 -585
View File
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -21,10 +21,12 @@ members = [
"crates/tranquil-oauth-server",
"crates/tranquil-api",
"crates/tranquil-lexicon",
"crates/tranquil-signal",
"crates/tranquil-store",
]
[workspace.package]
version = "0.4.6"
version = "0.5.0"
edition = "2024"
license = "AGPL-3.0-or-later"
@@ -49,6 +51,10 @@ tranquil-server = { path = "crates/tranquil-server" }
tranquil-sync = { path = "crates/tranquil-sync" }
tranquil-oauth-server = { path = "crates/tranquil-oauth-server" }
tranquil-api = { path = "crates/tranquil-api" }
tranquil-signal = { path = "crates/tranquil-signal", features = ["fjall-store"] }
tranquil-store = { path = "crates/tranquil-store" }
presage = { git = "https://github.com/whisperfish/presage", rev = "fe3ed54c4844ae51c3a9fa49cf80a7816a31a425", default-features = false }
unicode-segmentation = "1"
@@ -81,6 +87,7 @@ hkdf = "0.12"
hmac = "0.12"
http = "1.4"
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
qrcodegen = "1.8"
infer = "0.19"
ipld-core = "0.4"
iroh-car = "0.5"
@@ -129,7 +136,15 @@ testcontainers = "0.26"
testcontainers-modules = { version = "0.14", features = ["postgres"] }
wiremock = "0.6"
[patch.crates-io]
curve25519-dalek = { git = "https://github.com/signalapp/curve25519-dalek", tag = "signal-curve25519-4.1.3" }
[profile.release]
lto = "fat"
strip = true
codegen-units = 1
panic = "abort"
[profile.bench]
debug = 1
strip = false
+5 -13
View File
@@ -4,7 +4,7 @@ COPY frontend/ ./
RUN deno task build
FROM rust:1.92-alpine AS builder
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang protoc
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
WORKDIR /app
ARG SLIM="false"
@@ -29,6 +29,8 @@ COPY crates/tranquil-pds ./crates/tranquil-pds
COPY crates/tranquil-sync ./crates/tranquil-sync
COPY crates/tranquil-api ./crates/tranquil-api
COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server
COPY crates/tranquil-store ./crates/tranquil-store
COPY crates/tranquil-signal ./crates/tranquil-signal
COPY crates/tranquil-server ./crates/tranquil-server
COPY migrations ./crates/tranquil-pds/migrations
RUN --mount=type=cache,target=/usr/local/cargo/registry \
@@ -40,23 +42,13 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
fi && \
cp target/release/tranquil-server /tmp/tranquil-pds
FROM alpine:3.23 AS signal-cli
RUN apk add --no-cache curl tar
ARG SIGNAL_CLI_VERSION=0.13.24
RUN curl -fsSL "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux-native.tar.gz" \
| tar xz -C /usr/local/bin
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends msmtp ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
FROM alpine:3.23
RUN apk add --no-cache msmtp ca-certificates \
&& ln -sf /usr/bin/msmtp /usr/sbin/sendmail
COPY --from=signal-cli /usr/local/bin/signal-cli /usr/local/bin/signal-cli
VOLUME /var/lib/signal-cli
COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds
COPY --from=frontend /app/dist /var/lib/tranquil-pds/frontend
COPY migrations /app/migrations
WORKDIR /app
ENV SIGNAL_CLI_CONFIG=/var/lib/signal-cli
ENV SERVER_HOST=0.0.0.0
ENV SERVER_PORT=3000
EXPOSE 3000
+169
View File
@@ -0,0 +1,169 @@
tranquil-store: embedded storage engine for Tranquil PDS
RFC draft, 2026-03-22
By Lewis!
-- TLDR --
Add an embedded storage engine as an alternative to postgres (and leapfrog SQLite-per-actor)
that treats Tranquil's 3 types of storage workloads as 3 separate problems:
- BlockStore: bitcask-esque append log for immutable CID-keyed blocks [4]
- MetaStore: Fjall LSM keyspaces for mutable metadata [5]
- EventLog: segmented append log for the firehose
Group commit across users, content dedup, sub-ms firehose delivery.
We will use deterministic simulation testing [18][19].
Postgres will of course stay as the existing alternative backend.
-- Intro --
The ref PDS hits structural limits around 300k accounts [2].
SQLite-per-actor means no cross-user write batching.
tranquil-store is an embedded rust library. It lives in-process, no external deps.
Postgres remains supported; we plan to enable a storage transition path
such that users can seamlessly snapshot-n-switch between the backends.
The BlockStore is a bitcask-style append log [4] with a Fjall key index [5] that
maps each CID to a (file, offset, length) tuple. We use key-value separation
as per WiscKey [6]. Because blocks are immutable and keyed by CID, the value log
never needs compaction. An LRU hot tier keeps frequently-accessed blocks in
mem, and hint files allow fast index reconstruction on restart [4].
The main throughput enabler is group commit [7]. The ref PDS fsyncs once per user
per mutation [1], but BlockStore batches all concurrent commits into a single
write-and-sync cycle.
Content dedup occurs naturally: identical MST subtrees across users share
one CID-keyed block instead of N copies [3].
MetaStore uses Fjall [5] keyspaces for all mutable data. We chose Fjall over
redb and LMDB because both of those are single-writer [8][9]. Each keyspace
compacts independently.
For cross-store atomicity we use an intent log. Each mutation writes a single
intent record containing the BlockStore refcount updates, MetaStore changes,
and the serialized EventLog payload, fsynced via the group commit. After fsync,
the changes are applied to MetaStore and the event is appended to the EventLog,
then the intent is marked committed. Recovery replays any incomplete intents,
re-applying both metadata changes and event appends. This gives us crash-atomic
mutations across all three stores without full MVCC, since mutations
are already serialized per-user [3].
EventLog stores the firehose as segmented append-only files. Live subscribers
receive events via tokio broadcast, and consumers that are catching up will
read from mmap'ed segments [10]. Each event receives a monotonic u64 sequence number.
Segment headers store the base sequence number; a per-segment index maps sequence
ranges to byte offsets. This decouples consumer cursors from physical layout,
allowing transparent addition of per-segment zstd compression per the loom-v2
spec [11] without invalidating checkpoints. Retention is just deleting old
segments! :P
GC uses refcounted key index entries. GC is epoch-gated by the group commit
cycle: a block is only eligible for collection if its refcount reached zero in
a prior completed commit cycle. This prevents races between concurrent dedup
(which skips the block write but increments the refcount in the same batch) and
collection. Blocks past the epoch gate are collected by rewriting any data
files that fall below a liveness threshold.
For serialization we use postcard on disk and rkyv [12] for in-mem caches
only. All data files carry a version tag.
Memory is divided into fixed slices from a configurable total budget: Fjall
block cache, BlockStore hot tier, and CID index each receive a configured
percentage. Actual usage per component is exposed as metrics. The EventLog's
mmap pages live in the OS page cache and are excluded from the budget.
Backup acquires the group commit lock, which quiesces all writes at the next
commit boundary. Under the lock, the system notes the EventLog position, the
BlockStore file list, and takes a Fjall snapshot, then releases the lock.
Sealed data files and segments are immutable and can be copied without
co-ordination after the snapshot. The quiesce window is bounded by one commit
cycle. Point-in-time recovery replays the EventLog against a prior snapshot.
For continuous replication, a background process tails the EventLog and copies
sealed files to remote storage.
-- Runtime --
The storage core runs on tokio. It is synchronous internally, accessed through
dedicated handler threads that communicate via async channels [13]. Requests
are dispatched by hashing the DID, which gives us per-user write serialization
without locks. Global operations use round-robin. All disk IO goes through
pread/pwrite directly [13].
We rejected io_uring for three reasons: it creates orphan kernel operations
when futures are cancelled [22], it is blocked by default in both Docker [16]
and Podman [17] seccomp profiles, and it accounts for 60% of Google's kernel
vulnerability rewards [15].
We also rejected thread-per-core runtimes (glommio, etc.) because they are
incompatible with the tokio ecosystem. DID-sharded handler threads give us
the same shared-nothing property without a runtime split.
-- Testing --
We use deterministic simulation testing, following FoundationDB [18] and
TigerBeetle's VOPR [19]. All IO sits behind a StorageIO trait, and tests use an
in-memory implementation that injects faults: partial writes, bit flips, sync
failures, and misdirected writes. A single seed controls the entire fault
schedule, so any failure reproduces exactly [20][21].
-- Why these choices --
Bitcask for blocks:
Key-val separation [6] using Bitcask [4] for immutable CID blocks:
O(1) writes, O(1) reads, zero write amplification, & no compaction!
Fjall for metadata:
Only pure-Rust embedded engine with concurrent writers [5].
Otherwise we'd write our own.
Segmented log for events:
Write once -> scan forward -> delete by age.
Quite straightforward!
Postcard on disk:
rkyv is apparently faster [12] but couples on-disk format to library version.
Tokio & handler threads:
spawn_blocking & pread matches io_uring without security/compat costs [13][14][15][16].
Deterministic simulation:
Catches bug classes conventional testing can't reach [18][19].
StorageIO trait is needed anyway; but being harness-first is a one-time cost [20][21].
-- References --
[1] Bluesky PDS SQLite migration. github.com/bluesky-social/atproto/pull/1705
[2] G. Orosz. Building Bluesky: a Distributed Social Network. Pragmatic Engineer, April 2024.
newsletter.pragmaticengineer.com/p/bluesky
K. Suder. Introduction to AT Protocol. August 2025. mackuba.eu/2025/08/20/introduction-to-atproto
Bluesky PDS "Going to Production" guide. atproto.com/guides/going-to-production
[3] AT Protocol repository spec. atproto.com/specs/repository
[4] Bitcask: A Log-Structured Hash Table for Fast KV Data. Riak, 2010. riak.com/assets/bitcask-intro.pdf
[5] Fjall: LSM-based embedded storage engine. github.com/fjall-rs/fjall
[6] Lu et al. WiscKey: Separating Keys from Values in SSD-Conscious Storage. USENIX FAST 2016.
usenix.org/conference/fast16/technical-sessions/presentation/lu
[7] Phil Eaton. A Write-Ahead Log Is Not a Universal Part of Durability. July 2024.
notes.eatonphil.com/2024-07-01-a-write-ahead-log-is-not-a-universal-part-of-durability.html
[8] redb design document. github.com/cberner/redb/blob/master/docs/design.md
[9] LMDB source repository. github.com/LMDB/lmdb
[10] Crotty et al. Are You Sure You Want to Use MMAP in Your DBMS? CIDR 2022.
cs.brown.edu/people/acrotty/pubs/p13-crotty.pdf
[11] ybzeek. RFC: com.atproto.sync.getZstdStream (zstd-compressed relay streams).
github.com/bluesky-social/atproto/discussions/4582
[12] rkyv: zero-copy deserialization framework for Rust. rkyv.org
[13] Tonbo. Exploring Better Async Rust Disk IO. tonbo.io/blog/exploring-better-async-rust-disk-io
[14] Iroh. Async Rust Challenges in Iroh. iroh.computer/blog/async-rust-challenges-in-iroh
[15] Google restricting io_uring. phoronix.com/news/Google-Restricting-IO_uring
[16] Docker 4.42.0 and io_uring. forums.docker.com/t/4-42-0-and-io-uring/148620
[17] Podman io_uring discussion. github.com/containers/podman/discussions/27772
[18] FoundationDB simulation testing. apple.github.io/foundationdb/testing.html
[19] TigerBeetle VOPR. tigerbeetle.com/blog/2023-07-06-simulation-testing-for-liveness
[20] DST in Rust (S2). s2.dev/blog/dst
[21] Phil Eaton. What's the big deal about Deterministic Simulation Testing? August 2024.
notes.eatonphil.com/2024-08-20-deterministic-simulation-testing.html
[22] Tonbo. Async Rust Is Not Safe with io_uring. tonbo.io/blog/async-rust-is-not-safe-with-io-uring
Thank you for reading! Let's do some great work together.
+3
View File
@@ -12,12 +12,15 @@ tranquil-db = { workspace = true }
tranquil-db-traits = { workspace = true }
tranquil-lexicon = { workspace = true, features = ["resolve"] }
tranquil-scopes = { workspace = true }
tranquil-signal = { workspace = true }
anyhow = { workspace = true }
axum = { workspace = true }
backon = { workspace = true }
base32 = { workspace = true }
base64 = { workspace = true }
image = { workspace = true }
qrcodegen = { workspace = true }
bcrypt = { workspace = true }
bs58 = { workspace = true }
bytes = { workspace = true }
+8 -7
View File
@@ -34,13 +34,13 @@ pub struct GetPreferencesOutput {
}
pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissive>) -> Response {
let has_full_access = auth.permissions().has_full_access();
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await {
Ok(Some(id)) => id,
_ => {
return ApiError::InternalError(Some("User not found".into())).into_response();
}
};
let prefs = match state.infra_repo.get_account_preferences(user_id).await {
let prefs = match state.repos.infra.get_account_preferences(user_id).await {
Ok(rows) => rows,
Err(_) => {
return ApiError::InternalError(Some("Failed to fetch preferences".into()))
@@ -69,7 +69,7 @@ pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissiv
if let Some(age) = personal_details_pref
.as_ref()
.and_then(|pref| pref.get("birthDate"))
.and_then(|v| v.as_str())
.and_then(Value::as_str)
.and_then(get_age_from_datestring)
{
let declared_age_pref = serde_json::json!({
@@ -93,7 +93,7 @@ pub async fn put_preferences(
Json(input): Json<PutPreferencesInput>,
) -> Response {
let has_full_access = auth.permissions().has_full_access();
let user_id: uuid::Uuid = match state.user_repo.get_id_by_did(&auth.did).await {
let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await {
Ok(Some(id)) => id,
_ => {
return ApiError::InternalError(Some("User not found".into())).into_response();
@@ -122,7 +122,7 @@ pub async fn put_preferences(
if pref_str.len() > MAX_PREFERENCE_SIZE {
return PrefValidation::TooLarge(pref_str.len());
}
let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
let pref_type = match pref.get("$type").and_then(Value::as_str) {
Some(t) => t,
None => return PrefValidation::MissingType,
};
@@ -179,7 +179,7 @@ pub async fn put_preferences(
.preferences
.into_iter()
.filter_map(|pref| {
let pref_type = pref.get("$type").and_then(|t| t.as_str())?;
let pref_type = pref.get("$type").and_then(Value::as_str)?;
if pref_type == DECLARED_AGE_PREF {
return None;
}
@@ -188,7 +188,8 @@ pub async fn put_preferences(
.collect();
if state
.infra_repo
.repos
.infra
.replace_namespace_preferences(user_id, APP_BSKY_NAMESPACE, prefs_to_save)
.await
.is_err()
@@ -1,8 +1,4 @@
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use axum::{Json, extract::State};
use serde::Deserialize;
use tracing::warn;
use tranquil_pds::api::EmptyResponse;
@@ -20,10 +16,11 @@ pub async fn delete_account(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DeleteAccountInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let did = &input.did;
let (user_id, handle) = state
.user_repo
.repos
.user
.get_id_and_handle_by_did(did)
.await
.log_db_err("in delete_account")?
@@ -31,7 +28,8 @@ pub async fn delete_account(
.map(|row| (row.id, row.handle))?;
state
.user_repo
.repos
.user
.admin_delete_account_complete(user_id, did)
.await
.log_db_err("deleting account")?;
@@ -52,5 +50,5 @@ pub async fn delete_account(
.cache
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
Ok(EmptyResponse::ok().into_response())
Ok(Json(EmptyResponse {}))
}
+8 -11
View File
@@ -1,9 +1,4 @@
use axum::{
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use tracing::warn;
use tranquil_pds::api::error::{ApiError, DbResultExt};
@@ -30,13 +25,14 @@ pub async fn send_email(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<SendEmailInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<SendEmailOutput>, ApiError> {
let content = input.content.trim();
if content.is_empty() {
return Err(ApiError::InvalidRequest("content is required".into()));
}
let user = state
.user_repo
.repos
.user
.get_by_did(&input.recipient_did)
.await
.log_db_err("in send_email")?
@@ -50,7 +46,8 @@ pub async fn send_email(
.clone()
.unwrap_or_else(|| format!("Message from {}", hostname));
let result = state
.infra_repo
.repos
.infra
.enqueue_comms(
Some(user_id),
tranquil_db_traits::CommsChannel::Email,
@@ -68,11 +65,11 @@ pub async fn send_email(
handle,
input.recipient_did
);
Ok((StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response())
Ok(Json(SendEmailOutput { sent: true }))
}
Err(e) => {
warn!("Failed to enqueue admin email: {:?}", e);
Ok((StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response())
Ok(Json(SendEmailOutput { sent: false }))
}
}
}
+47 -57
View File
@@ -1,8 +1,7 @@
use crate::common;
use axum::{
Json,
extract::{Query, RawQuery, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -68,9 +67,10 @@ pub async fn get_account_info(
State(state): State<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetAccountInfoParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<AccountInfo>, ApiError> {
let account = state
.infra_repo
.repos
.infra
.get_admin_account_info_by_did(&params.did)
.await
.log_db_err("in get_account_info")?
@@ -79,31 +79,28 @@ pub async fn get_account_info(
let invited_by = get_invited_by(&state, account.id).await;
let invites = get_invites_for_user(&state, account.id).await;
Ok((
StatusCode::OK,
Json(AccountInfo {
did: account.did,
handle: account.handle,
email: account.email,
indexed_at: account.created_at.to_rfc3339(),
invite_note: None,
invites_disabled: account.invites_disabled,
email_confirmed_at: if account.email_verified {
Some(account.created_at.to_rfc3339())
} else {
None
},
deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()),
invited_by,
invites,
}),
)
.into_response())
Ok(Json(AccountInfo {
did: account.did,
handle: account.handle,
email: account.email,
indexed_at: account.created_at.to_rfc3339(),
invite_note: None,
invites_disabled: account.invites_disabled,
email_confirmed_at: if account.email_verified {
Some(account.created_at.to_rfc3339())
} else {
None
},
deactivated_at: account.deactivated_at.map(|dt| dt.to_rfc3339()),
invited_by,
invites,
}))
}
async fn get_invited_by(state: &AppState, user_id: uuid::Uuid) -> Option<InviteCodeInfo> {
let code = state
.infra_repo
.repos
.infra
.get_invite_code_used_by_user(user_id)
.await
.ok()??;
@@ -116,7 +113,8 @@ async fn get_invites_for_user(
user_id: uuid::Uuid,
) -> Option<Vec<InviteCodeInfo>> {
let invite_codes = state
.infra_repo
.repos
.infra
.get_invites_created_by_user(user_id)
.await
.ok()?;
@@ -128,21 +126,16 @@ async fn get_invites_for_user(
let code_strings: Vec<String> = invite_codes.iter().map(|ic| ic.code.clone()).collect();
let uses = state
.infra_repo
.repos
.infra
.get_invite_code_uses_batch(&code_strings)
.await
.ok()?;
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
uses.into_iter().fold(HashMap::new(), |mut acc, u| {
acc.entry(u.code.clone())
.or_default()
.push(InviteCodeUseInfo {
used_by: u.used_by_did,
used_at: u.used_at.to_rfc3339(),
});
acc
});
let uses_by_code = common::group_invite_uses_by_code(uses, |u| InviteCodeUseInfo {
used_by: u.used_by_did,
used_at: u.used_at.to_rfc3339(),
});
let invites: Vec<InviteCodeInfo> = invite_codes
.into_iter()
@@ -165,10 +158,11 @@ async fn get_invites_for_user(
}
async fn get_invite_code_info(state: &AppState, code: &str) -> Option<InviteCodeInfo> {
let info = state.infra_repo.get_invite_code_info(code).await.ok()??;
let info = state.repos.infra.get_invite_code_info(code).await.ok()??;
let uses = state
.infra_repo
.repos
.infra
.get_invite_code_uses(code)
.await
.ok()
@@ -195,7 +189,7 @@ pub async fn get_account_infos(
State(state): State<AppState>,
_auth: Auth<Admin>,
RawQuery(raw_query): RawQuery,
) -> Result<Response, ApiError> {
) -> Result<Json<GetAccountInfosOutput>, ApiError> {
let dids: Vec<String> =
tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
.into_iter()
@@ -208,7 +202,8 @@ pub async fn get_account_infos(
let dids_typed: Vec<Did> = dids.iter().filter_map(|d| d.parse().ok()).collect();
let accounts = state
.infra_repo
.repos
.infra
.get_admin_account_infos_by_dids(&dids_typed)
.await
.log_db_err("fetching account infos")?;
@@ -216,7 +211,8 @@ pub async fn get_account_infos(
let user_ids: Vec<uuid::Uuid> = accounts.iter().map(|u| u.id).collect();
let all_invite_codes = state
.infra_repo
.repos
.infra
.get_invite_codes_by_users(&user_ids)
.await
.unwrap_or_default();
@@ -228,7 +224,8 @@ pub async fn get_account_infos(
let all_invite_uses = if !all_codes.is_empty() {
state
.infra_repo
.repos
.infra
.get_invite_code_uses_batch(&all_codes)
.await
.unwrap_or_default()
@@ -237,25 +234,18 @@ pub async fn get_account_infos(
};
let invited_by_map: HashMap<uuid::Uuid, String> = state
.infra_repo
.repos
.infra
.get_invite_code_uses_by_users(&user_ids)
.await
.unwrap_or_default()
.into_iter()
.collect();
let uses_by_code: HashMap<String, Vec<InviteCodeUseInfo>> =
all_invite_uses
.into_iter()
.fold(HashMap::new(), |mut acc, u| {
acc.entry(u.code.clone())
.or_default()
.push(InviteCodeUseInfo {
used_by: u.used_by_did,
used_at: u.used_at.to_rfc3339(),
});
acc
});
let uses_by_code = common::group_invite_uses_by_code(all_invite_uses, |u| InviteCodeUseInfo {
used_by: u.used_by_did,
used_at: u.used_at.to_rfc3339(),
});
let (codes_by_user, code_info_map): (
HashMap<uuid::Uuid, Vec<InviteCodeInfo>>,
@@ -304,5 +294,5 @@ pub async fn get_account_infos(
})
.collect();
Ok((StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response())
Ok(Json(GetAccountInfosOutput { infos }))
}
+3 -2
View File
@@ -11,6 +11,7 @@ pub use info::{
};
pub use search::{SearchAccountsOutput, SearchAccountsParams, search_accounts};
pub use update::{
UpdateAccountEmailInput, UpdateAccountHandleInput, UpdateAccountPasswordInput,
update_account_email, update_account_handle, update_account_password,
SetAdminStatusInput, UpdateAccountEmailInput, UpdateAccountHandleInput,
UpdateAccountPasswordInput, set_admin_status, update_account_email, update_account_handle,
update_account_password,
};
@@ -1,8 +1,6 @@
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use tranquil_pds::api::error::{ApiError, DbResultExt};
@@ -51,17 +49,16 @@ pub async fn search_accounts(
State(state): State<AppState>,
_auth: Auth<Admin>,
Query(params): Query<SearchAccountsParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<SearchAccountsOutput>, ApiError> {
let limit = params.limit.clamp(1, 100);
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
let cursor_did: Option<Did> = params.cursor.as_ref().and_then(|c| c.parse().ok());
let rows = state
.user_repo
.repos
.user
.search_accounts(
cursor_did.as_ref(),
email_filter.as_deref(),
handle_filter.as_deref(),
params.email.as_deref(),
params.handle.as_deref(),
limit + 1,
)
.await
@@ -91,12 +88,8 @@ pub async fn search_accounts(
} else {
None
};
Ok((
StatusCode::OK,
Json(SearchAccountsOutput {
cursor: next_cursor,
accounts,
}),
)
.into_response())
Ok(Json(SearchAccountsOutput {
cursor: next_cursor,
accounts,
}))
}
+51 -22
View File
@@ -1,10 +1,6 @@
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use axum::{Json, extract::State};
use serde::Deserialize;
use tracing::{error, warn};
use tracing::{error, info, warn};
use tranquil_pds::api::EmptyResponse;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
@@ -21,7 +17,7 @@ pub async fn update_account_email(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountEmailInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let account = input.account.trim();
let email = input.email.trim();
if account.is_empty() || email.is_empty() {
@@ -34,12 +30,13 @@ pub async fn update_account_email(
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state
.user_repo
.repos
.user
.admin_update_email(&account_did, email)
.await
{
Ok(0) => Err(ApiError::AccountNotFound),
Ok(_) => Ok(EmptyResponse::ok().into_response()),
Ok(_) => Ok(Json(EmptyResponse {})),
Err(e) => {
error!("DB error updating email: {:?}", e);
Err(ApiError::InternalError(None))
@@ -57,7 +54,7 @@ pub async fn update_account_handle(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountHandleInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let did = &input.did;
let input_handle = input.handle.trim();
if input_handle.is_empty() {
@@ -75,9 +72,10 @@ pub async fn update_account_handle(
} else {
input_handle.to_string()
};
let old_handle = state.user_repo.get_handle_by_did(did).await.ok().flatten();
let old_handle = state.repos.user.get_handle_by_did(did).await.ok().flatten();
let user_id = state
.user_repo
.repos
.user
.get_id_by_did(did)
.await
.ok()
@@ -85,14 +83,16 @@ pub async fn update_account_handle(
.ok_or(ApiError::AccountNotFound)?;
let handle_for_check: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
if let Ok(true) = state
.user_repo
.repos
.user
.check_handle_exists(&handle_for_check, user_id)
.await
{
return Err(ApiError::HandleTaken);
}
match state
.user_repo
.repos
.user
.admin_update_handle(did, &handle_for_check)
.await
{
@@ -125,7 +125,7 @@ pub async fn update_account_handle(
{
warn!("Failed to update PLC handle for admin handle update: {}", e);
}
Ok(EmptyResponse::ok().into_response())
Ok(Json(EmptyResponse {}))
}
Err(e) => {
error!("DB error updating handle: {:?}", e);
@@ -144,27 +144,56 @@ pub async fn update_account_password(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateAccountPasswordInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let did = &input.did;
let password = input.password.trim();
if password.is_empty() {
return Err(ApiError::InvalidRequest("password is required".into()));
}
let password_hash = bcrypt::hash(password, bcrypt::DEFAULT_COST).map_err(|e| {
error!("Failed to hash password: {:?}", e);
ApiError::InternalError(None)
})?;
let password_hash = crate::common::hash_or_internal_error(password)?;
match state
.user_repo
.repos
.user
.admin_update_password(did, &password_hash)
.await
{
Ok(0) => Err(ApiError::AccountNotFound),
Ok(_) => Ok(EmptyResponse::ok().into_response()),
Ok(_) => Ok(Json(EmptyResponse {})),
Err(e) => {
error!("DB error updating password: {:?}", e);
Err(ApiError::InternalError(None))
}
}
}
#[derive(Deserialize)]
pub struct SetAdminStatusInput {
pub did: Did,
pub admin: bool,
}
pub async fn set_admin_status(
State(state): State<AppState>,
auth: Auth<Admin>,
Json(input): Json<SetAdminStatusInput>,
) -> Result<Json<EmptyResponse>, ApiError> {
info!(
actor = %auth.did,
target = %input.did,
admin = input.admin,
"admin status change"
);
state
.repos
.user
.set_admin_status(&input.did, input.admin)
.await
.map_err(|e| {
error!("DB error setting admin status: {:?}", e);
ApiError::InternalError(None)
})?;
Ok(Json(EmptyResponse {}))
}
+38 -22
View File
@@ -8,7 +8,7 @@ use tranquil_types::CidLink;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerConfigResponse {
pub struct ServerConfigOutput {
pub server_name: String,
pub primary_color: Option<String>,
pub primary_color_dark: Option<String>,
@@ -29,7 +29,7 @@ pub struct UpdateServerConfigRequest {
}
#[derive(Serialize)]
pub struct UpdateServerConfigResponse {
pub struct UpdateServerConfigOutput {
pub success: bool,
}
@@ -42,7 +42,7 @@ fn is_valid_hex_color(s: &str) -> bool {
pub async fn get_server_config(
State(state): State<AppState>,
) -> Result<Json<ServerConfigResponse>, ApiError> {
) -> Result<Json<ServerConfigOutput>, ApiError> {
let keys = &[
"server_name",
"primary_color",
@@ -53,14 +53,15 @@ pub async fn get_server_config(
];
let rows = state
.infra_repo
.repos
.infra
.get_server_configs(keys)
.await
.log_db_err("fetching server config")?;
let config_map: std::collections::HashMap<String, String> = rows.into_iter().collect();
Ok(Json(ServerConfigResponse {
Ok(Json(ServerConfigOutput {
server_name: config_map
.get("server_name")
.cloned()
@@ -77,7 +78,7 @@ pub async fn update_server_config(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(req): Json<UpdateServerConfigRequest>,
) -> Result<Json<UpdateServerConfigResponse>, ApiError> {
) -> Result<Json<UpdateServerConfigOutput>, ApiError> {
if let Some(server_name) = req.server_name {
let trimmed = server_name.trim();
if trimmed.is_empty() || trimmed.len() > 100 {
@@ -86,7 +87,8 @@ pub async fn update_server_config(
));
}
state
.infra_repo
.repos
.infra
.upsert_server_config("server_name", trimmed)
.await
.log_db_err("upserting server_name")?;
@@ -95,13 +97,15 @@ pub async fn update_server_config(
if let Some(ref color) = req.primary_color {
if color.is_empty() {
state
.infra_repo
.repos
.infra
.delete_server_config("primary_color")
.await
.log_db_err("deleting primary_color")?;
} else if is_valid_hex_color(color) {
state
.infra_repo
.repos
.infra
.upsert_server_config("primary_color", color)
.await
.log_db_err("upserting primary_color")?;
@@ -115,13 +119,15 @@ pub async fn update_server_config(
if let Some(ref color) = req.primary_color_dark {
if color.is_empty() {
state
.infra_repo
.repos
.infra
.delete_server_config("primary_color_dark")
.await
.log_db_err("deleting primary_color_dark")?;
} else if is_valid_hex_color(color) {
state
.infra_repo
.repos
.infra
.upsert_server_config("primary_color_dark", color)
.await
.log_db_err("upserting primary_color_dark")?;
@@ -135,13 +141,15 @@ pub async fn update_server_config(
if let Some(ref color) = req.secondary_color {
if color.is_empty() {
state
.infra_repo
.repos
.infra
.delete_server_config("secondary_color")
.await
.log_db_err("deleting secondary_color")?;
} else if is_valid_hex_color(color) {
state
.infra_repo
.repos
.infra
.upsert_server_config("secondary_color", color)
.await
.log_db_err("upserting secondary_color")?;
@@ -155,13 +163,15 @@ pub async fn update_server_config(
if let Some(ref color) = req.secondary_color_dark {
if color.is_empty() {
state
.infra_repo
.repos
.infra
.delete_server_config("secondary_color_dark")
.await
.log_db_err("deleting secondary_color_dark")?;
} else if is_valid_hex_color(color) {
state
.infra_repo
.repos
.infra
.upsert_server_config("secondary_color_dark", color)
.await
.log_db_err("upserting secondary_color_dark")?;
@@ -174,7 +184,8 @@ pub async fn update_server_config(
if let Some(ref logo_cid) = req.logo_cid {
let old_logo_cid = state
.infra_repo
.repos
.infra
.get_server_config("logo_cid")
.await
.ok()
@@ -189,13 +200,16 @@ pub async fn update_server_config(
if let Some(old_cid_str) = should_delete_old {
match CidLink::new(old_cid_str) {
Ok(old_cid) => {
if let Ok(Some(storage_key)) =
state.infra_repo.get_blob_storage_key_by_cid(&old_cid).await
if let Ok(Some(storage_key)) = state
.repos
.infra
.get_blob_storage_key_by_cid(&old_cid)
.await
{
if let Err(e) = state.blob_store.delete(&storage_key).await {
error!("Failed to delete old logo blob from storage: {:?}", e);
}
if let Err(e) = state.infra_repo.delete_blob_by_cid(&old_cid).await {
if let Err(e) = state.repos.infra.delete_blob_by_cid(&old_cid).await {
error!("Failed to delete old logo blob record: {:?}", e);
}
}
@@ -211,18 +225,20 @@ pub async fn update_server_config(
if logo_cid.is_empty() {
state
.infra_repo
.repos
.infra
.delete_server_config("logo_cid")
.await
.log_db_err("deleting logo_cid")?;
} else {
state
.infra_repo
.repos
.infra
.upsert_server_config("logo_cid", logo_cid)
.await
.log_db_err("upserting logo_cid")?;
}
}
Ok(Json(UpdateServerConfigResponse { success: true }))
Ok(Json(UpdateServerConfigOutput { success: true }))
}
+36 -40
View File
@@ -1,8 +1,7 @@
use crate::common;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use tracing::error;
@@ -23,9 +22,9 @@ pub async fn disable_invite_codes(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DisableInviteCodesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
if let Some(codes) = &input.codes
&& let Err(e) = state.infra_repo.disable_invite_codes_by_code(codes).await
&& let Err(e) = state.repos.infra.disable_invite_codes_by_code(codes).await
{
error!("DB error disabling invite codes: {:?}", e);
}
@@ -33,14 +32,15 @@ pub async fn disable_invite_codes(
let accounts_typed: Vec<tranquil_types::Did> =
accounts.iter().filter_map(|a| a.parse().ok()).collect();
if let Err(e) = state
.infra_repo
.repos
.infra
.disable_invite_codes_by_account(&accounts_typed)
.await
{
error!("DB error disabling invite codes by account: {:?}", e);
}
}
Ok(EmptyResponse::ok().into_response())
Ok(Json(EmptyResponse {}))
}
#[derive(Deserialize)]
@@ -80,7 +80,7 @@ pub async fn get_invite_codes(
State(state): State<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetInviteCodesParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<GetInviteCodesOutput>, ApiError> {
let limit = params.limit.unwrap_or(100).clamp(1, 500);
let sort_order = match params.sort.as_deref() {
Some("usage") => InviteCodeSortOrder::Usage,
@@ -88,7 +88,8 @@ pub async fn get_invite_codes(
};
let codes_rows = state
.infra_repo
.repos
.infra
.list_invite_codes(params.cursor.as_deref(), limit, sort_order)
.await
.log_db_err("fetching invite codes")?;
@@ -97,33 +98,30 @@ pub async fn get_invite_codes(
let code_strings: Vec<String> = codes_rows.iter().map(|r| r.code.clone()).collect();
let creator_dids: std::collections::HashMap<uuid::Uuid, tranquil_types::Did> = state
.infra_repo
.repos
.infra
.get_user_dids_by_ids(&user_ids)
.await
.unwrap_or_default()
.into_iter()
.collect();
let uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
if code_strings.is_empty() {
std::collections::HashMap::new()
} else {
let uses_by_code = if code_strings.is_empty() {
std::collections::HashMap::new()
} else {
common::group_invite_uses_by_code(
state
.infra_repo
.repos
.infra
.get_invite_code_uses_batch(&code_strings)
.await
.unwrap_or_default()
.into_iter()
.fold(std::collections::HashMap::new(), |mut acc, u| {
acc.entry(u.code.clone())
.or_default()
.push(InviteCodeUseInfo {
used_by: u.used_by_did.to_string(),
used_at: u.used_at.to_rfc3339(),
});
acc
})
};
.unwrap_or_default(),
|u| InviteCodeUseInfo {
used_by: u.used_by_did.to_string(),
used_at: u.used_at.to_rfc3339(),
},
)
};
let codes: Vec<InviteCodeInfo> = codes_rows
.iter()
@@ -149,14 +147,10 @@ pub async fn get_invite_codes(
} else {
None
};
Ok((
StatusCode::OK,
Json(GetInviteCodesOutput {
cursor: next_cursor,
codes,
}),
)
.into_response())
Ok(Json(GetInviteCodesOutput {
cursor: next_cursor,
codes,
}))
}
#[derive(Deserialize)]
@@ -168,7 +162,7 @@ pub async fn disable_account_invites(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<DisableAccountInvitesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let account = input.account.trim();
if account.is_empty() {
return Err(ApiError::InvalidRequest("account is required".into()));
@@ -178,11 +172,12 @@ pub async fn disable_account_invites(
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state
.user_repo
.repos
.user
.set_invites_disabled(&account_did, true)
.await
{
Ok(true) => Ok(EmptyResponse::ok().into_response()),
Ok(true) => Ok(Json(EmptyResponse {})),
Ok(false) => Err(ApiError::AccountNotFound),
Err(e) => {
error!("DB error disabling account invites: {:?}", e);
@@ -200,7 +195,7 @@ pub async fn enable_account_invites(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<EnableAccountInvitesInput>,
) -> Result<Response, ApiError> {
) -> Result<Json<EmptyResponse>, ApiError> {
let account = input.account.trim();
if account.is_empty() {
return Err(ApiError::InvalidRequest("account is required".into()));
@@ -210,11 +205,12 @@ pub async fn enable_account_invites(
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state
.user_repo
.repos
.user
.set_invites_disabled(&account_did, false)
.await
{
Ok(true) => Ok(EmptyResponse::ok().into_response()),
Ok(true) => Ok(Json(EmptyResponse {})),
Ok(false) => Err(ApiError::AccountNotFound),
Err(e) => {
error!("DB error enabling account invites: {:?}", e);
+3 -1
View File
@@ -2,15 +2,17 @@ pub mod account;
pub mod config;
pub mod invite;
pub mod server_stats;
pub mod signal;
pub mod status;
pub use account::{
delete_account, get_account_info, get_account_infos, search_accounts, send_email,
update_account_email, update_account_handle, update_account_password,
set_admin_status, update_account_email, update_account_handle, update_account_password,
};
pub use config::{get_server_config, update_server_config};
pub use invite::{
disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
};
pub use server_stats::get_server_stats;
pub use signal::{get_signal_status, link_signal_device, unlink_signal_device};
pub use status::{get_subject_status, update_subject_status};
+9 -14
View File
@@ -1,8 +1,4 @@
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use axum::{Json, extract::State};
use serde::Serialize;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
@@ -10,7 +6,7 @@ use tranquil_pds::state::AppState;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerStatsResponse {
pub struct ServerStatsOutput {
pub user_count: i64,
pub repo_count: i64,
pub record_count: i64,
@@ -20,17 +16,16 @@ pub struct ServerStatsResponse {
pub async fn get_server_stats(
State(state): State<AppState>,
_auth: Auth<Admin>,
) -> Result<Response, ApiError> {
let user_count = state.user_repo.count_users().await.unwrap_or(0);
let repo_count = state.repo_repo.count_repos().await.unwrap_or(0);
let record_count = state.repo_repo.count_all_records().await.unwrap_or(0);
let blob_storage_bytes = state.blob_repo.sum_blob_storage().await.unwrap_or(0);
) -> Result<Json<ServerStatsOutput>, ApiError> {
let user_count = state.repos.user.count_users().await.unwrap_or(0);
let repo_count = state.repos.repo.count_repos().await.unwrap_or(0);
let record_count = state.repos.repo.count_all_records().await.unwrap_or(0);
let blob_storage_bytes = state.repos.blob.sum_blob_storage().await.unwrap_or(0);
Ok(Json(ServerStatsResponse {
Ok(Json(ServerStatsOutput {
user_count,
repo_count,
record_count,
blob_storage_bytes,
})
.into_response())
}))
}
+171
View File
@@ -0,0 +1,171 @@
use axum::{Json, extract::State};
use base64::{Engine, engine::general_purpose::STANDARD};
use image::{ImageBuffer, Luma};
use serde::Serialize;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalStatusOutput {
pub enabled: bool,
pub linked: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignalLinkOutput {
pub qr_base64: String,
}
pub async fn get_signal_status(
State(state): State<AppState>,
_auth: Auth<Admin>,
) -> Result<Json<SignalStatusOutput>, ApiError> {
let enabled = tranquil_config::get().signal.enabled;
let linked = match &state.signal_sender {
Some(slot) => slot.is_linked().await,
None => false,
};
Ok(Json(SignalStatusOutput { enabled, linked }))
}
pub async fn link_signal_device(
State(state): State<AppState>,
_auth: Auth<Admin>,
) -> Result<Json<SignalLinkOutput>, ApiError> {
let slot = state
.signal_sender
.as_ref()
.ok_or_else(|| ApiError::InvalidRequest("Signal is not enabled".into()))?;
if slot.is_linked().await {
return Err(ApiError::InvalidRequest(
"Signal device already linked".into(),
));
}
let (generation, link_cancel) = slot.begin_link().await;
let device_name = tranquil_signal::DeviceName::new("tranquil-pds".to_string())
.map_err(|e| ApiError::InternalError(Some(format!("invalid device name: {e}"))))?;
let signal_store = state
.signal_store_provider
.as_ref()
.ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?;
let link_result = signal_store
.link_signal_device(
device_name,
state.shutdown.clone(),
link_cancel,
slot.linking_flag(),
)
.await
.map_err(|e| ApiError::InternalError(Some(format!("Signal linking failed: {e}"))))?;
let qr_base64 = url_to_qr_png_base64(link_result.url.as_str())
.map_err(|e| ApiError::InternalError(Some(format!("QR generation failed: {e}"))))?;
let slot_for_task = slot.clone();
let shutdown = state.shutdown.clone();
tokio::spawn(async move {
let result = tokio::select! {
biased;
_ = shutdown.cancelled() => {
tracing::info!("Signal linking aborted due to server shutdown");
return;
}
r = link_result.completion => r,
};
match result {
Ok(Ok(client)) => {
if slot_for_task.complete_link(generation, client).await {
tracing::info!("Signal device linked successfully");
} else {
tracing::warn!(
"Signal link completed but generation mismatch or already linked; discarding"
);
}
}
Ok(Err(e)) => {
tracing::error!(error = %e, "Signal device linking failed");
}
Err(_) => {
tracing::error!("Signal linking task dropped without completing");
}
}
});
Ok(Json(SignalLinkOutput { qr_base64 }))
}
pub async fn unlink_signal_device(
State(state): State<AppState>,
_auth: Auth<Admin>,
) -> Result<Json<serde_json::Value>, ApiError> {
let slot = state
.signal_sender
.as_ref()
.ok_or_else(|| ApiError::InvalidRequest("Signal is not enabled".into()))?;
let signal_store = state
.signal_store_provider
.as_ref()
.ok_or_else(|| ApiError::InternalError(Some("Signal store not configured".into())))?;
signal_store
.clear_signal_data()
.await
.map_err(|e| ApiError::InternalError(Some(format!("Failed to clear signal data: {e}"))))?;
slot.unlink().await;
Ok(Json(serde_json::json!({})))
}
const QR_MODULE_SCALE: u32 = 8;
const QR_QUIET_ZONE_MODULES: u32 = 4;
fn url_to_qr_png_base64(url: &str) -> Result<String, String> {
let qr = qrcodegen::QrCode::encode_text(url, qrcodegen::QrCodeEcc::Medium)
.map_err(|e| format!("QR encode failed: {e:?}"))?;
let size = u32::try_from(qr.size()).map_err(|_| "QR size is negative".to_string())?;
let img_size = size
.checked_add(
QR_QUIET_ZONE_MODULES
.checked_mul(2)
.ok_or("border overflow")?,
)
.ok_or("image size overflow")?
.checked_mul(QR_MODULE_SCALE)
.ok_or("scaled size overflow")?;
let img: ImageBuffer<Luma<u8>, Vec<u8>> = ImageBuffer::from_fn(img_size, img_size, |x, y| {
let module_x = x / QR_MODULE_SCALE;
let module_y = y / QR_MODULE_SCALE;
match (
module_x.checked_sub(QR_QUIET_ZONE_MODULES),
module_y.checked_sub(QR_QUIET_ZONE_MODULES),
) {
(Some(mx), Some(my)) if mx < size && my < size => {
if qr.get_module(mx as i32, my as i32) {
Luma([0u8])
} else {
Luma([255u8])
}
}
_ => Luma([255u8]),
}
});
let mut png_bytes = Vec::new();
let mut cursor = std::io::Cursor::new(&mut png_bytes);
img.write_to(&mut cursor, image::ImageFormat::Png)
.map_err(|e| format!("PNG encode failed: {e}"))?;
Ok(STANDARD.encode(&png_bytes))
}
+83 -96
View File
@@ -1,11 +1,9 @@
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Value, json};
use tracing::{error, warn};
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
@@ -37,7 +35,7 @@ pub async fn get_subject_status(
State(state): State<AppState>,
_auth: Auth<Admin>,
Query(params): Query<GetSubjectStatusParams>,
) -> Result<Response, ApiError> {
) -> Result<Json<SubjectStatus>, ApiError> {
if params.did.is_none() && params.uri.is_none() && params.blob.is_none() {
return Err(ApiError::InvalidRequest(
"Must provide did, uri, or blob".into(),
@@ -47,7 +45,7 @@ pub async fn get_subject_status(
let did: Did = did_str
.parse()
.map_err(|_| ApiError::InvalidDid("Invalid DID format".into()))?;
match state.user_repo.get_status_by_did(&did).await {
match state.repos.user.get_status_by_did(&did).await {
Ok(Some(status)) => {
let deactivated = status.deactivated_at.map(|_| StatusAttr {
applied: true,
@@ -57,18 +55,14 @@ pub async fn get_subject_status(
applied: true,
r#ref: Some(r.clone()),
});
return Ok((
StatusCode::OK,
Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.admin.defs#repoRef",
"did": did_str
}),
takedown,
deactivated,
return Ok(Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.admin.defs#repoRef",
"did": did_str
}),
)
.into_response());
takedown,
deactivated,
}));
}
Ok(None) => {
return Err(ApiError::SubjectNotFound);
@@ -83,25 +77,21 @@ pub async fn get_subject_status(
let cid: CidLink = uri_str
.parse()
.map_err(|_| ApiError::InvalidRequest("Invalid CID format".into()))?;
match state.repo_repo.get_record_by_cid(&cid).await {
match state.repos.repo.get_record_by_cid(&cid).await {
Ok(Some(record)) => {
let takedown = record.takedown_ref.as_ref().map(|r| StatusAttr {
applied: true,
r#ref: Some(r.clone()),
});
return Ok((
StatusCode::OK,
Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.repo.strongRef",
"uri": uri_str,
"cid": uri_str
}),
takedown,
deactivated: None,
return Ok(Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.repo.strongRef",
"uri": uri_str,
"cid": uri_str
}),
)
.into_response());
takedown,
deactivated: None,
}));
}
Ok(None) => {
return Err(ApiError::RecordNotFound);
@@ -119,25 +109,21 @@ pub async fn get_subject_status(
let did = params.did.as_ref().ok_or_else(|| {
ApiError::InvalidRequest("Must provide a did to request blob state".into())
})?;
match state.blob_repo.get_blob_with_takedown(&blob_cid).await {
match state.repos.blob.get_blob_with_takedown(&blob_cid).await {
Ok(Some(blob)) => {
let takedown = blob.takedown_ref.as_ref().map(|r| StatusAttr {
applied: true,
r#ref: Some(r.clone()),
});
return Ok((
StatusCode::OK,
Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.admin.defs#repoBlobRef",
"did": did,
"cid": blob.cid
}),
takedown,
deactivated: None,
return Ok(Json(SubjectStatus {
subject: json!({
"$type": "com.atproto.admin.defs#repoBlobRef",
"did": did,
"cid": blob.cid
}),
)
.into_response());
takedown,
deactivated: None,
}));
}
Ok(None) => {
return Err(ApiError::BlobNotFound(None));
@@ -169,11 +155,11 @@ pub async fn update_subject_status(
State(state): State<AppState>,
_auth: Auth<Admin>,
Json(input): Json<UpdateSubjectStatusInput>,
) -> Result<Response, ApiError> {
let subject_type = input.subject.get("$type").and_then(|t| t.as_str());
) -> Result<Json<serde_json::Value>, ApiError> {
let subject_type = input.subject.get("$type").and_then(Value::as_str);
match subject_type {
Some("com.atproto.admin.defs#repoRef") => {
let did_str = input.subject.get("did").and_then(|d| d.as_str());
let did_str = input.subject.get("did").and_then(Value::as_str);
if let Some(did_str) = did_str {
let did: Did = match did_str.parse() {
Ok(d) => d,
@@ -186,7 +172,8 @@ pub async fn update_subject_status(
None
};
state
.user_repo
.repos
.user
.set_user_takedown(&did, takedown_ref)
.await
.map_err(|e| {
@@ -196,9 +183,9 @@ pub async fn update_subject_status(
}
if let Some(deactivated) = &input.deactivated {
let result = if deactivated.applied {
state.user_repo.deactivate_account(&did, None).await
state.repos.user.deactivate_account(&did, None).await
} else {
state.user_repo.activate_account(&did).await
state.repos.user.activate_account(&did).await
};
result.map_err(|e| {
error!(
@@ -208,11 +195,22 @@ pub async fn update_subject_status(
ApiError::InternalError(Some("Failed to update deactivation status".into()))
})?;
}
let takedown_update = input.takedown.as_ref().map(|t| t.applied);
let takedown_ref = input.takedown.as_ref().and_then(|t| t.r#ref.as_deref());
let deactivated_update = input.deactivated.as_ref().map(|d| d.applied);
if (takedown_update.is_some() || deactivated_update.is_some())
&& let Err(e) = state
.repos
.repo
.update_repo_status(&did, takedown_update, takedown_ref, deactivated_update)
.await
{
warn!("failed to sync status to repo backend: {e:?}");
}
if let Some(takedown) = &input.takedown {
let status = if takedown.applied {
tranquil_db_traits::AccountStatus::Takendown
} else {
tranquil_db_traits::AccountStatus::Active
let status = match takedown.applied {
true => tranquil_db_traits::AccountStatus::Takendown,
false => tranquil_db_traits::AccountStatus::Active,
};
if let Err(e) =
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
@@ -221,10 +219,9 @@ pub async fn update_subject_status(
}
}
if let Some(deactivated) = &input.deactivated {
let status = if deactivated.applied {
tranquil_db_traits::AccountStatus::Deactivated
} else {
tranquil_db_traits::AccountStatus::Active
let status = match deactivated.applied {
true => tranquil_db_traits::AccountStatus::Deactivated,
false => tranquil_db_traits::AccountStatus::Active,
};
if let Err(e) =
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
@@ -232,30 +229,26 @@ pub async fn update_subject_status(
warn!("Failed to sequence account event for deactivation: {}", e);
}
}
if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await {
if let Ok(Some(handle)) = state.repos.user.get_handle_by_did(&did).await {
let _ = state
.cache
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
}
return Ok((
StatusCode::OK,
Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
})),
"deactivated": input.deactivated.as_ref().map(|d| json!({
"applied": d.applied
}))
return Ok(Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
})),
)
.into_response());
"deactivated": input.deactivated.as_ref().map(|d| json!({
"applied": d.applied
}))
})));
}
}
Some("com.atproto.repo.strongRef") => {
let uri_str = input.subject.get("uri").and_then(|u| u.as_str());
let uri_str = input.subject.get("uri").and_then(Value::as_str);
if let Some(uri_str) = uri_str {
let cid: CidLink = uri_str
.parse()
@@ -267,7 +260,8 @@ pub async fn update_subject_status(
None
};
state
.repo_repo
.repos
.repo
.set_record_takedown(&cid, takedown_ref)
.await
.map_err(|e| {
@@ -278,21 +272,17 @@ pub async fn update_subject_status(
ApiError::InternalError(Some("Failed to update takedown status".into()))
})?;
}
return Ok((
StatusCode::OK,
Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
}))
})),
)
.into_response());
return Ok(Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
}))
})));
}
}
Some("com.atproto.admin.defs#repoBlobRef") => {
let cid_str = input.subject.get("cid").and_then(|c| c.as_str());
let cid_str = input.subject.get("cid").and_then(Value::as_str);
if let Some(cid_str) = cid_str {
let cid: CidLink = cid_str
.parse()
@@ -304,7 +294,8 @@ pub async fn update_subject_status(
None
};
state
.blob_repo
.repos
.blob
.update_blob_takedown(&cid, takedown_ref)
.await
.map_err(|e| {
@@ -315,17 +306,13 @@ pub async fn update_subject_status(
ApiError::InternalError(Some("Failed to update takedown status".into()))
})?;
}
return Ok((
StatusCode::OK,
Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
}))
})),
)
.into_response());
return Ok(Json(json!({
"subject": input.subject,
"takedown": input.takedown.as_ref().map(|t| json!({
"applied": t.applied,
"ref": t.r#ref
}))
})));
}
}
_ => {}
+46 -23
View File
@@ -1,37 +1,60 @@
use axum::{
Json,
extract::State,
http::{HeaderMap, Method, StatusCode},
response::{IntoResponse, Response},
http::{HeaderMap, Method},
};
use serde_json::json;
use serde::Serialize;
use tranquil_pds::auth::{
AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop,
};
use tranquil_pds::state::AppState;
pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgeAssuranceState {
pub status: &'static str,
pub access: &'static str,
pub last_initiated_at: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgeAssuranceMetadata {
pub account_created_at: Option<String>,
}
#[derive(Serialize)]
pub struct GetAgeAssuranceOutput {
pub state: AgeAssuranceState,
pub metadata: AgeAssuranceMetadata,
}
#[derive(Serialize)]
pub struct AgeAssuranceStatusOutput {
pub status: &'static str,
}
pub async fn get_state(
State(state): State<AppState>,
headers: HeaderMap,
) -> Json<GetAgeAssuranceOutput> {
let created_at = get_account_created_at(&state, &headers).await;
let now = chrono::Utc::now().to_rfc3339();
(
StatusCode::OK,
Json(json!({
"state": {
"status": "assured",
"access": "full",
"lastInitiatedAt": now
},
"metadata": {
"accountCreatedAt": created_at
}
})),
)
.into_response()
Json(GetAgeAssuranceOutput {
state: AgeAssuranceState {
status: "assured",
access: "full",
last_initiated_at: now,
},
metadata: AgeAssuranceMetadata {
account_created_at: created_at,
},
})
}
pub async fn get_age_assurance_state() -> Response {
(StatusCode::OK, Json(json!({"status": "assured"}))).into_response()
pub async fn get_age_assurance_state() -> Json<AgeAssuranceStatusOutput> {
Json(AgeAssuranceStatusOutput { status: "assured" })
}
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
@@ -45,8 +68,8 @@ async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option
let http_uri = "/";
let auth_user = match validate_token_with_dpop(
state.user_repo.as_ref(),
state.oauth_repo.as_ref(),
state.repos.user.as_ref(),
state.repos.oauth.as_ref(),
&extracted.token,
extracted.scheme,
dpop_proof,
@@ -66,7 +89,7 @@ async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option
}
};
match state.user_repo.get_by_did(&auth_user.did).await {
match state.repos.user.get_by_did(&auth_user.did).await {
Ok(Some(user)) => {
tracing::debug!(created_at = ?user.created_at, "age assurance: got user");
Some(user.created_at.to_rfc3339())

Some files were not shown because too many files have changed in this diff Show More