mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-25 02:36:06 +00:00
Compare commits
190
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b20f4cfb4 | ||
|
|
f3af04e4ae | ||
|
|
aab1a945c2 | ||
|
|
9dc184ee33 | ||
|
|
ab4eba6dc4 | ||
|
|
a171518290 | ||
|
|
28f2e04019 | ||
|
|
39a2e40b35 | ||
|
|
05ab0b7423 | ||
|
|
221b32d66f | ||
|
|
1b489776c5 | ||
|
|
2ca15fe7a0 | ||
|
|
48ae1e8b7b | ||
|
|
6838976969 | ||
|
|
3045ee25c0 | ||
|
|
04a90b1563 | ||
|
|
e13ba7f4c7 | ||
|
|
a3f729c3cd | ||
|
|
637b817a33 | ||
|
|
562f970bc3 | ||
|
|
06fd6a1ce9 | ||
|
|
3b07cdfb24 | ||
|
|
4a8826b7a1 | ||
|
|
80afd764d3 | ||
|
|
5bbe2146ff | ||
|
|
b009ccdaf2 | ||
|
|
39f74b5adf | ||
|
|
a231d7da29 | ||
|
|
63d84d38fb | ||
|
|
fe9b88141c | ||
|
|
72f5dce32b | ||
|
|
cd7e01100e | ||
|
|
7c248be153 | ||
|
|
91999819c6 | ||
|
|
ffce1d5d05 | ||
|
|
8e6ace2fe2 | ||
|
|
3018a20843 | ||
|
|
37fc06fb39 | ||
|
|
728a8c4d3b | ||
|
|
3d49e99cc3 | ||
|
|
7e823673ca | ||
|
|
320933598c | ||
|
|
500dc2e0e6 | ||
|
|
a220611a8b | ||
|
|
ca7a4b4b73 | ||
|
|
8ff02610e4 | ||
|
|
22f82489d5 | ||
|
|
cee483e358 | ||
|
|
7f8e858137 | ||
|
|
44d73dac58 | ||
|
|
b8cae15c12 | ||
|
|
9b58961bba | ||
|
|
31ee12ecd3 | ||
|
|
ea106d5246 | ||
|
|
4015217a2e | ||
|
|
7e4c9e1dab | ||
|
|
e9dc57d6f4 | ||
|
|
4e2525b245 | ||
|
|
38508c2c6e | ||
|
|
7a54ccf6a3 | ||
|
|
e85394c314 | ||
|
|
411c6be108 | ||
|
|
28aa7ab7fc | ||
|
|
348b69d76b | ||
|
|
4b23ca2c36 | ||
|
|
a7052e878c | ||
|
|
fc6063dba8 | ||
|
|
44cb016762 | ||
|
|
e6eee18ace | ||
|
|
96c8375706 | ||
|
|
86c5995568 | ||
|
|
036c317fd6 | ||
|
|
4d2c7d4723 | ||
|
|
f24a9f8bc0 | ||
|
|
f6ef6ecbd9 | ||
|
|
a2567bdb1a | ||
|
|
56120d252d | ||
|
|
210b0f463c | ||
|
|
c80f504dc0 | ||
|
|
a3bd7c59ad | ||
|
|
9ebde27540 | ||
|
|
28a7834304 | ||
|
|
191da5b311 | ||
|
|
a5b4ba7d65 | ||
|
|
90dabd8840 | ||
|
|
19eaccea74 | ||
|
|
f620a6bc43 | ||
|
|
04f370aaa1 | ||
|
|
3fd8f7ebbf | ||
|
|
0afcb2ee28 | ||
|
|
8bd556f65b | ||
|
|
cc92594506 | ||
|
|
76f22b801b | ||
|
|
021b7dbec4 | ||
|
|
eb034cb8b3 | ||
|
|
bdaf510898 | ||
|
|
deb2502112 | ||
|
|
1815ddba9f | ||
|
|
a7517ed5c9 | ||
|
|
d07d702dd4 | ||
|
|
1901b0a630 | ||
|
|
58f8d327c1 | ||
|
|
a13343e1de | ||
|
|
60e10af4aa | ||
|
|
f176f55862 | ||
|
|
fac9520a16 | ||
|
|
eee6fb9ff4 | ||
|
|
2462d0ab3b | ||
|
|
85f87f7b28 | ||
|
|
b1d86caa78 | ||
|
|
9b2cfb3a7e | ||
|
|
efd499bb26 | ||
|
|
d4dfe838eb | ||
|
|
af3821514f | ||
|
|
8f7aad3756 | ||
|
|
75b9e3165f | ||
|
|
ccc9916109 | ||
|
|
bc8fd66a45 | ||
|
|
180de29984 | ||
|
|
0455dc20bd | ||
|
|
2770b9b14a | ||
|
|
d436597184 | ||
|
|
4cfca6d956 | ||
|
|
98b94fb170 | ||
|
|
4fe01cff72 | ||
|
|
00c9eb732f | ||
|
|
6d2d3b4be4 | ||
|
|
9d81e58803 | ||
|
|
2afd075496 | ||
|
|
c74bf967cf | ||
|
|
1285d5c675 | ||
|
|
c30d73cd4d | ||
|
|
0fab8f2eb9 | ||
|
|
57336fa124 | ||
|
|
ace105899f | ||
|
|
c80a525e0d | ||
|
|
7edb76507b | ||
|
|
a20e4e05e6 | ||
|
|
3e7a199736 | ||
|
|
7ed9c8ce59 | ||
|
|
b0c3402a82 | ||
|
|
b9574f3ec1 | ||
|
|
baef2be844 | ||
|
|
1f21b85069 | ||
|
|
09d437b3e3 | ||
|
|
7f2e83e92f | ||
|
|
ec273fa814 | ||
|
|
d51bfd59da | ||
|
|
8ccdd30cb3 | ||
|
|
7a67361993 | ||
|
|
cdbbaaccdf | ||
|
|
55d3b7f83d | ||
|
|
f00b0231fb | ||
|
|
1dfbd27cce | ||
|
|
bc5e0e0446 | ||
|
|
1c87ef5536 | ||
|
|
255c7135f9 | ||
|
|
34f050a1f0 | ||
|
|
52c22060f3 | ||
|
|
c250d51978 | ||
|
|
378cb7a7fb | ||
|
|
bf20b4a78f | ||
|
|
05246aec58 | ||
|
|
8d0e0c6810 | ||
|
|
1720de8650 | ||
|
|
a5c68a3506 | ||
|
|
9ea33def13 | ||
|
|
a9e3ec63e1 | ||
|
|
dbc3c23c95 | ||
|
|
531ca7c1c0 | ||
|
|
add1ff176b | ||
|
|
bdb200a30c | ||
|
|
e5faba3e73 | ||
|
|
5dc810ceef | ||
|
|
6abc0eaca2 | ||
|
|
696940f732 | ||
|
|
f777c288da | ||
|
|
9039648705 | ||
|
|
770a16b043 | ||
|
|
d37f38e496 | ||
|
|
282f08f207 | ||
|
|
f07bcb82f7 | ||
|
|
e38343ce09 | ||
|
|
e454e99b8a | ||
|
|
19b0ea19c6 | ||
|
|
318818ad51 | ||
|
|
7c55a5ceb9 | ||
|
|
9d31ee9ace | ||
|
|
e3efcb43ba | ||
|
|
eb46824220 |
@@ -13,9 +13,44 @@ 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 }
|
||||
|
||||
[profile.gauntlet-pr]
|
||||
retries = 0
|
||||
fail-fast = true
|
||||
test-threads = "num-cpus"
|
||||
slow-timeout = { period = "60s", terminate-after = 5 }
|
||||
|
||||
[[profile.gauntlet-pr.overrides]]
|
||||
filter = "binary(gauntlet_smoke)"
|
||||
slow-timeout = { period = "300s", terminate-after = 2 }
|
||||
|
||||
[profile.gauntlet-nightly]
|
||||
retries = 0
|
||||
fail-fast = false
|
||||
test-threads = "num-cpus"
|
||||
slow-timeout = { period = "600s", terminate-after = 1 }
|
||||
|
||||
[profile.gauntlet-soak]
|
||||
retries = 0
|
||||
fail-fast = false
|
||||
test-threads = 1
|
||||
slow-timeout = { period = "5m", terminate-after = 1000 }
|
||||
|
||||
[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 +64,14 @@ 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 = "package(tranquil-config)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(whole_story)"
|
||||
test-group = "heavy-load-tests"
|
||||
@@ -41,6 +84,38 @@ 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.default.overrides]]
|
||||
filter = "binary(gauntlet_smoke)"
|
||||
slow-timeout = { period = "300s", terminate-after = 8 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(compaction_restart) | binary(mst_refcount_integrity) | binary(gc_compaction_restart)"
|
||||
slow-timeout = { period = "120s", terminate-after = 4 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(/retention_time_travel_survives_many_seeds/)"
|
||||
slow-timeout = { period = "300s", terminate-after = 24 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "binary(fd_lifecycle)"
|
||||
slow-timeout = { period = "300s", terminate-after = 4 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "test(/import_with_verification/) | test(/plc_migration/)"
|
||||
test-group = "serial-env-tests"
|
||||
@@ -53,6 +128,14 @@ 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 = "package(tranquil-config)"
|
||||
test-group = "serial-env-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(whole_story)"
|
||||
test-group = "heavy-load-tests"
|
||||
@@ -64,3 +147,13 @@ test-group = "heavy-load-tests"
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(repo_lifecycle)"
|
||||
test-group = "heavy-load-tests"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "test(/retention_time_travel_survives_many_seeds/)"
|
||||
slow-timeout = { period = "300s", terminate-after = 24 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "binary(fd_lifecycle)"
|
||||
slow-timeout = { period = "300s", terminate-after = 4 }
|
||||
test-group = "io-heavy-sim"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
target/
|
||||
.git/
|
||||
.jj/
|
||||
**/node_modules/
|
||||
frontend/dist/
|
||||
frontend/coverage/
|
||||
frontend/.pnpm-store/
|
||||
.direnv/
|
||||
result
|
||||
.env
|
||||
*.output
|
||||
reference-pds-bsky/
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
/target
|
||||
target/
|
||||
.env
|
||||
.direnv
|
||||
result
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/coverage/
|
||||
frontend/.pnpm-store
|
||||
frontend/.npmrc
|
||||
|
||||
-32
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1",
|
||||
"query": "SELECT did, password_hash FROM users WHERE handle = $1 OR did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,5 +24,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c4621f6a8a1ab78a6355b09fdfc2bf8999d276564e93015792ec07cb05e79038"
|
||||
"hash": "053c971024b0d29a441c3597d760b3e21db2383442c3e6f09de4eb49ea437e7c"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, did, email, password_hash, password_required, two_factor_enabled,\n preferred_comms_channel as \"preferred_comms_channel!: CommsChannel\",\n deactivated_at, takedown_ref,\n email_verified, discord_verified, telegram_verified, signal_verified,\n account_type as \"account_type!: AccountType\"\n FROM users\n WHERE handle = $1 OR email = $1\n ",
|
||||
"query": "\n SELECT id, did, email, password_hash, password_required, two_factor_enabled,\n preferred_comms_channel as \"preferred_comms_channel!: CommsChannel\",\n deactivated_at, takedown_ref,\n email_verified, discord_verified, telegram_verified, signal_verified,\n account_type as \"account_type!: AccountType\"\n FROM users\n WHERE handle = $1 OR did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -118,5 +118,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7061e8763ef7d91ff152ed0124f99e1820172fd06916d225ca6c5137a507b8fa"
|
||||
"hash": "060c285c93a05252aab7d474df0186e7b5083fafedc582b8eac9916983e8fc2d"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT nextval('firehose_seq') as \"seq!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "09142b5a6b91fe4fd795fb6c85af5461e2621f6c3545b6d26e4618d813904b75"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE repo_seq r\n SET seq = $1::bigint + (o.ord - 1)\n FROM unnest($2::bigint[]) WITH ORDINALITY AS o(id, ord)\n WHERE r.id = o.id",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0b895d105363a60040323c5c47bc897905af3da980d0dc1b3b60bbdcdd90ea7a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
|
||||
"query": "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1 AND available_uses > 0 AND COALESCE(disabled, false) = false",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1ee6eda3e44660e7f14fcfe56adc2d41c72901b9c701fc7b992314e5370b32dc"
|
||||
"hash": "0bb2cb6af37bff735b6b380697fc8e1fa2034ca0600e8c16e1e362b722192327"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM repo_seq\n WHERE did = $1\n AND id <> (SELECT id FROM repo_seq WHERE did = $1 ORDER BY id DESC LIMIT 1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "122d79e006c0e11214e3cf11cfb98bdff6fbf156893b482572532ebcd8979e61"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin\n FROM users WHERE handle = $1",
|
||||
"query": "SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin, inbound_migration\n FROM users WHERE handle = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "inbound_migration",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,8 +62,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "584bceda60d8b6a02e7dc44d833e3fba13151f36ba9f75c64e33d6cb534cc939"
|
||||
"hash": "18bbda5582db1b32d02ab8a3eee970c9508b9bd67239c2f936639a9f863b30ff"
|
||||
}
|
||||
+3
-2
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE access_jti = $1",
|
||||
"query": "UPDATE users SET is_admin = $1 WHERE did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "847ce3c34985d0957526c87e0a20c6b4e5daae08a338f7635def682ac0689cf6"
|
||||
"hash": "1e63d287c619a14e5c07d80e8e54193d2964c8b5e6a855256cb80c4d0cd2c6ea"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET deactivated_at = NULL WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "23201d4e26bc650939e30f69fb0bca00d351d057098afebc1017f70a84b4bd22"
|
||||
}
|
||||
+28
@@ -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"
|
||||
}
|
||||
+14
@@ -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"
|
||||
}
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, active, status)\n VALUES ($1, 'account', false, 'deleted')\n RETURNING seq\n ",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, active, status)\n VALUES ($1, 'account', false, 'deleted')\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a0ea3630adb9e411d94d88084df6b05d7fc3ba8c306146fc932b736fe342eceb"
|
||||
"hash": "2c3c0740793f29b9bd9e0f81c2372d427dee091e20da8f71ae1e5f2769aa985d"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM webauthn_challenges WHERE did = $1 AND challenge_type = 'discoverable'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3155ef4f35698a3fe6aa38d5d976fd51b7f6a0381c81c4907dad61d2f37992bd"
|
||||
}
|
||||
-15
@@ -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"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_try_advisory_xact_lock($1) as \"locked!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "locked!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3cc7247d2cd7e85a6f21d783dd956f8fecc6f63c994014bb4e91504d498b5fd5"
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "40e848f48afb50f5e4820445aee874a482333e9090ef14ce44db3fd9721cfff0"
|
||||
}
|
||||
+17
@@ -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"
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4dcee809896ead3de8ca0433856ed424211d79df201d08bbea0e4c576931a234"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, handle)\n VALUES ($1, 'identity', $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4f3ef746cb5fc0916d3df5f92494c538497d3b9c1cfcc6e44c9b80f2193094f6"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, handle)\n VALUES ($1, 'identity', $2)\n RETURNING seq\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "50b2178cd174af1dc132a22048e52a6af492f4d8025ec512dd9c0c9ba62eb202"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET deactivated_at = NULL, inbound_migration = FALSE WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "51e029e572777e6a103fd7fd5550494de9d4cac7e3ff84e27ddec1a6aaefc047"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "52df8ae809b990fa76e3ac45f37e352a9fc8f92f911a7b9c9be2fd9e8032b18c"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE comms_queue\n SET status = 'failed'::comms_status,\n attempts = max_attempts,\n last_error = $2,\n updated_at = NOW()\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5bee4ed5296667e4ca7e1a97aec28d30a470b8aee7b378ec9ca4e34de4faf349"
|
||||
}
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1",
|
||||
"query": "SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "session_id",
|
||||
"type_info": "Int4"
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fcd868a192d27fd4eccae92a884e881b8d6f09bf7ae08a9b431a44acbf2f91f3"
|
||||
"hash": "5cee16f49a727d66b5231a8d07d7f4bcb6a1136fbf3e3d249fd33600772ac80f"
|
||||
}
|
||||
+34
@@ -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"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT state_json FROM webauthn_challenges\n WHERE did = $1 AND challenge_type = 'discoverable' AND expires_at > NOW()\n ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "state_json",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6969c478a0922bac4b79902313a0e28c94d6b8d6b16035474dd8f484e6171d60"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id as \"id!\" FROM repo_seq WHERE seq IS NULL ORDER BY id LIMIT $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "714a1c9a73e2fed8182d4ab2fe4a2c33bc10a52e27ed95d3a6641477ae42395e"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM repo_seq WHERE created_at < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "746ca87e98f6eaa9c060770c0d1bca6f0d4da60df2dc2db088e57bdc914cf233"
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"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 as \"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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -80,10 +90,12 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
@@ -96,5 +108,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b8101757a50075d20147014e450cb7deb7e58f84310690c7bde61e1834dc5903"
|
||||
"hash": "7cdf3b8136782a5b2e4aff91907a680566b4610e6cbf9d9396d31f9bbd99c3b8"
|
||||
}
|
||||
+15
@@ -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"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE id = $1 AND did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8003624cedbac8b094c83933578517abfb2eaf8e59d1d52c7ea59bf5d11cfcfe"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"ByteaArray",
|
||||
"ByteaArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "800b93a38eef70bafdf0a1b221994f3c19ab562f84d18265376296a592b4c767"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"ByteaArray",
|
||||
"ByteaArray",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "82276a34f473ff8b9790b78915ae3f72873d4b748cdb47bd2b81edc2f7a6e5a8"
|
||||
}
|
||||
+15
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM repo_seq WHERE did = $1 AND seq != $2",
|
||||
"query": "DELETE FROM repo_seq WHERE did = $1 AND id <> $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b6d6548acb89d6384cd226f6ed0d66de27fde3af24b4a7a3fce7e098812e38a5"
|
||||
"hash": "856e458c1c78f030545dc147f76d1b91fa4cc6e178eb51133c52358855ba3f02"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE comms_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM comms_queue\n WHERE status = 'pending'\n AND scheduled_for <= $1\n AND attempts < max_attempts\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\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",
|
||||
"query": "UPDATE comms_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM comms_queue\n WHERE attempts < max_attempts\n AND scheduled_for <= $1\n AND (\n status = 'pending'\n OR (status = 'processing'\n AND updated_at < $1 - INTERVAL '10 minutes')\n )\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -154,5 +154,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8047fda41bd94f819213decb8b3e0aba49a8dbdb10217eefd77e3567f8c9694a"
|
||||
"hash": "890aa92acdcb0fe2a3bf04d87e1f16a801d271da7cedc32fc42c2ef5b100faae"
|
||||
}
|
||||
+36
@@ -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"
|
||||
}
|
||||
+4
-12
@@ -1,14 +1,8 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, active, status)\n VALUES ($1, 'account', $2, $3)\n RETURNING seq\n ",
|
||||
"query": "\n INSERT INTO repo_seq (did, event_type, active, status)\n VALUES ($1, 'account', $2, $3)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
@@ -16,9 +10,7 @@
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "623042c0b901e17ff37c5bcc1a4eefe56fc8b996ca8326e75796897d13c9e9ed"
|
||||
"hash": "8ccd42b5b1b75f45e094889ee26af8517de28ca687efa81fd9c4fce598338fe9"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_token\n WHERE id IN (\n SELECT id FROM oauth_token\n WHERE did = $1\n ORDER BY updated_at ASC\n OFFSET $2\n )\n ",
|
||||
"query": "\n DELETE FROM oauth_token\n WHERE id IN (\n SELECT id FROM oauth_token\n WHERE did = $1\n ORDER BY created_at DESC\n OFFSET $2\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "56cd24903171eddc2ededd9079ffe10937c34e99b0305f25c980ca754da44625"
|
||||
"hash": "8f4357f7a18ddcf6b686a4555f244d37c35917364b8f917ca6ee2d4030ace742"
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"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 as \"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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -80,10 +90,12 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
@@ -96,5 +108,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0d32a592a97ad47c65aa37cf0d45417f2966fcbd688be7434626ae5f6971fa1f"
|
||||
"hash": "957bc81c63510ab502d2f8cea75dafcbdf2c8ebb448c86990d5e404ee5280438"
|
||||
}
|
||||
+22
@@ -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"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"ByteaArray",
|
||||
"ByteaArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9ba7869f29286cfd6b811ffa1e26bda1d637b98b44be66cdc265b1af4cec2d4f"
|
||||
}
|
||||
+180
@@ -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"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_advisory_xact_lock($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pg_advisory_xact_lock",
|
||||
"type_info": "Void"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a06e1d9f6f95e4c4c2b98310ebddcc9d963cc033582bf2e945e8bf3a301b4247"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE access_jti = $1 AND did = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a27e93bc594babbada10afe5c3e33a65909ec69c579329916833e4b0fe2332d3"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login, u.migrated_to_pds,\n u.preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled,\n COALESCE((SELECT (value_json)::boolean FROM account_preferences WHERE user_id = u.id AND name = 'email_auth_factor' ORDER BY created_at DESC LIMIT 1), false) as \"email_2fa_enabled!\"\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1",
|
||||
"query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login, u.migrated_to_pds,\n u.preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled,\n COALESCE((SELECT (value_json)::boolean FROM account_preferences WHERE user_id = u.id AND name = 'email_auth_factor' ORDER BY created_at DESC LIMIT 1), false) as \"email_2fa_enabled!\"\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -132,5 +132,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a960b981a146a0e422ef53601dfc31e29cf777aa194227c48c6ebc6905ea3249"
|
||||
"hash": "aafc2a7e51200ca1e7071c63c13698bf34ef8b66758ca9ebab4ea706ffb62914"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "session_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b2e1736dbe2ab9114e373353bcc299176417f3c9220025f9521591ba62928bd7"
|
||||
}
|
||||
+44
@@ -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"
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bce9edc597592d2e7d672061883639bcdeff80be85e2ecacb6c75d8c027e6710"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT setval('firehose_seq', $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "setval",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bd0407818c8bdae1f1024777951b08cb9c386fe751722e1caf6ad243e271697a"
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT u.used_at, st.id AS session_id, st.did, st.scope, st.controller_did,\n st.access_jti, st.refresh_jti, st.access_expires_at, st.refresh_expires_at,\n k.key_bytes, k.encryption_version\n FROM used_refresh_tokens u\n JOIN session_tokens st ON st.id = u.session_id\n JOIN users us ON st.did = us.did\n JOIN user_keys k ON us.id = k.user_id\n WHERE u.refresh_jti = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "session_id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "scope",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "controller_did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "access_jti",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "refresh_jti",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "access_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "refresh_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c08e3760be75071ac5e281cb3b05b3a02dccbf35b96c3d50c652878a38c9f9e6"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO used_refresh_tokens (refresh_jti, session_id)\n VALUES ($1, $2)\n ON CONFLICT (refresh_jti) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c0a7b73070e6c5b9baa9caddca3c5d24427b48ed02b5cac913a0c0af1e5c9379"
|
||||
}
|
||||
-29
@@ -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"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO webauthn_challenges (id, did, challenge, challenge_type, state_json, expires_at)\n VALUES ($1, $2, $3, 'discoverable', $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Bytea",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c6e3388fc39983f1787917606ba3194c72322d2d1ec54402c262194791a2b06a"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cf874abcb72017e775fe699a0b77ae9341355f30e4af84968ffeb9135dba745f"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM backlinks WHERE uri = ANY($1::text[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cffe4c37fe949fbdc3d5cd83ccec5655aae248a0a69dc260d1da9cf1d9ed2c49"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "NOTIFY repo_updates",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d4eacb9bca838dd18dcfd8ca39b83ef50968bfaaabc213536c049cc222ffe9c8"
|
||||
}
|
||||
+14
@@ -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"
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"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 as \"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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -81,10 +91,12 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
@@ -97,5 +109,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d8524ad3f5dc03eb09ed60396a78df5003f804c43ad253d6476523eacdebf811"
|
||||
"hash": "da2770e44c0ec21756ce4b6b0c57abff4f38cd8c680c5ced7d3b35f3c1f58da7"
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"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 as \"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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -81,10 +91,12 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
@@ -97,5 +109,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e7aa1080be9eb3a8ddf1f050c93dc8afd10478f41e22307014784b4ee3740b4a"
|
||||
"hash": "dfe011b2d2bafbbbbea508a4977fc34bb1be4cf2612296df23763e6e5841c33e"
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"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 as \"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": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "seq",
|
||||
"name": "seq!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -81,10 +91,12 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
@@ -97,5 +109,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b26bf97a27783eb7fb524a92dda3e68ef8470a9751fcaefe5fd2d7909dead54b"
|
||||
"hash": "f16d41b41a5388997e6ed408da55056173340ed12b60b1a956de87f73f9d0824"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin\n FROM users WHERE did = $1",
|
||||
"query": "SELECT id, did, handle, email, created_at, deactivated_at, takedown_ref, is_admin, inbound_migration\n FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "inbound_migration",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,8 +62,9 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6b51995c40519a63f85c70f29ca8bd6ec1963c8562d78215d980785dc46a6384"
|
||||
"hash": "f1c4ec28b02d09ffce35aa8249c1747a70c12a3ecfc1ff6ca1847840f770db2f"
|
||||
}
|
||||
+52
@@ -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"
|
||||
}
|
||||
+28
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
when:
|
||||
- event: ["push", "manual"]
|
||||
branch: ["main"]
|
||||
|
||||
engine: nixery
|
||||
|
||||
dependencies:
|
||||
nixpkgs:
|
||||
- nix
|
||||
- cachix
|
||||
- gnugrep
|
||||
|
||||
steps:
|
||||
- name: Authenticate
|
||||
command: |
|
||||
cachix authtoken "$CACHIX_AUTH_TOKEN"
|
||||
|
||||
- name: Build and push aarch64
|
||||
command: |
|
||||
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
|
||||
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-pds-aarch64 $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
|
||||
@@ -0,0 +1,28 @@
|
||||
when:
|
||||
- event: ["push", "manual"]
|
||||
branch: ["main"]
|
||||
|
||||
engine: nixery
|
||||
|
||||
dependencies:
|
||||
nixpkgs:
|
||||
- nix
|
||||
- cachix
|
||||
- gnugrep
|
||||
|
||||
steps:
|
||||
- name: Authenticate
|
||||
command: |
|
||||
cachix authtoken "$CACHIX_AUTH_TOKEN"
|
||||
|
||||
- name: Build and push x86_64
|
||||
command: |
|
||||
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
|
||||
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-pds $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
|
||||
nix-store -qR --include-outputs $(nix-store -qd $(nix build .#packages.x86_64-linux.tranquil-frontend $SUBS --print-out-paths --no-link)) | grep -v '\.drv$' | cachix push tranquil
|
||||
|
||||
- name: Build and push devShell
|
||||
command: |
|
||||
SUBS="--option extra-substituters https://tranquil.cachix.org --option extra-trusted-public-keys tranquil.cachix.org-1:PoO+mGL6a6LcJiPakMDHN4E218/ei/7v2sxeDtNkSRg="
|
||||
nix develop $SUBS --profile dev-profile -c true
|
||||
cachix push tranquil dev-profile
|
||||
@@ -1,24 +1,36 @@
|
||||
when:
|
||||
- event: ["push"]
|
||||
branches: main
|
||||
- event: [ "manual" ]
|
||||
- event: [ "push" ]
|
||||
branch: [ "main" ]
|
||||
|
||||
engine: nixery
|
||||
|
||||
dependencies:
|
||||
nixpkgs:
|
||||
- podman
|
||||
- kaniko
|
||||
|
||||
environment:
|
||||
DOCKER_CONFIG: "/kaniko/.docker"
|
||||
|
||||
steps:
|
||||
- name: Create podman config
|
||||
- name: Configure Kaniko
|
||||
command: |
|
||||
mkdir -p ~/.config/containers
|
||||
echo "unqualified-search-registries = [\"docker.io\"]" >> ~/.config/containers/registries.conf
|
||||
mkdir -p /kaniko/.docker/
|
||||
echo "{
|
||||
\"auths\": {
|
||||
\"https://atcr.io/v1\":{
|
||||
\"auth\": \"$ATCR_CREDENTIALS\"
|
||||
}
|
||||
}
|
||||
}" > /kaniko/.docker/config.json
|
||||
|
||||
- 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-pds/tranquil:latest"
|
||||
podman push --creds "$ATCR_USERNAME:$ATCR_PASSWORD" "tranquil-pds:$TANGLED_COMMIT_SHA" "atcr.io/tranquil-pds/tranquil:$TANGLED_COMMIT_SHA"
|
||||
executor \
|
||||
--context=$(pwd) \
|
||||
--ignore-path=$(pwd) \
|
||||
--dockerfile=$(pwd)/Dockerfile \
|
||||
--destination="atcr.io/tranquil.farm/tranquil-pds:latest" \
|
||||
--destination="atcr.io/tranquil.farm/tranquil-pds:$TANGLED_COMMIT_SHA" \
|
||||
--push-retry=3 \
|
||||
--skip-push-permission-check
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
# Contributing to Tranquil PDS
|
||||
|
||||
## When PRing
|
||||
|
||||
In order of importance:
|
||||
|
||||
- **You must run your change! Every contribution that says "here's xyz. untested." does not help the project.**
|
||||
- Relevant tests to your PR must pass. The whole suite doesn't have to be proven to have run, because there are a *ton* of tests and they're quite heavy, but hopefully there are existing tests for whatever you're PRing, and if there aren't, please add those too.
|
||||
- Run cargo fmt :P
|
||||
|
||||
> 🦪 Lewis
|
||||
>
|
||||
> Good CI fixes some of these. We should really get around to that.
|
||||
|
||||
Things that would also be nice but aren't like, a pain in our side:
|
||||
|
||||
- Big changes should be stacked PRs that are broken up into digestible pieces. Those stacked PRs should hopefully be able to be merged individually if necessary.
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose
|
||||
- Add `pds.test` to your hosts file (one-time setup):
|
||||
|
||||
```
|
||||
127.0.0.1 pds.test
|
||||
```
|
||||
|
||||
- **macOS / Linux:** `/etc/hosts`
|
||||
- **Windows:** `C:\Windows\System32\drivers\etc\hosts`
|
||||
|
||||
### Starting the dev environment
|
||||
|
||||
```bash
|
||||
just run-dev
|
||||
```
|
||||
|
||||
This starts the following services via `docker-compose`:
|
||||
|
||||
- **Traefik** — HTTPS reverse proxy at `https://pds.test`
|
||||
- **Backend** — Rust server with `cargo-watch` (auto-rebuilds on file changes)
|
||||
- **Frontend** — Vite dev server with hot module replacement
|
||||
- **Postgres** — Database on port 5432
|
||||
- **PLC Directory** — Local [did-method-plc](https://github.com/did-method-plc/did-method-plc) server for DID registration
|
||||
- **Mailpit** — Local email server with web UI at [http://localhost:8025](http://localhost:8025)
|
||||
|
||||
Once all services are running, open **https://pds.test** in your browser.
|
||||
|
||||
### Trusting the self-signed certificate
|
||||
|
||||
Traefik generates a self-signed TLS certificate. Your browser will show a security warning on first visit. You can either click through it, or add the certificate to your system trust store for a seamless experience:
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
# Extract the cert from traefik and add it to the system keychain
|
||||
echo | openssl s_client -connect localhost:443 -servername pds.test 2>/dev/null | openssl x509 > /tmp/pds-test.pem
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/pds-test.pem
|
||||
```
|
||||
|
||||
**Linux (Debian/Ubuntu):**
|
||||
|
||||
```bash
|
||||
echo | openssl s_client -connect localhost:443 -servername pds.test 2>/dev/null | openssl x509 | sudo tee /usr/local/share/ca-certificates/pds-test.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
**Linux (Fedora/RHEL):**
|
||||
|
||||
```bash
|
||||
echo | openssl s_client -connect localhost:443 -servername pds.test 2>/dev/null | openssl x509 | sudo tee /etc/pki/ca-trust/source/anchors/pds-test.pem
|
||||
sudo update-ca-trust
|
||||
```
|
||||
|
||||
**Windows (PowerShell as Administrator):**
|
||||
|
||||
```powershell
|
||||
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
|
||||
$cert.Import([System.Text.Encoding]::UTF8.GetBytes((echo | openssl s_client -connect localhost:443 -servername pds.test 2>$null | openssl x509)))
|
||||
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine")
|
||||
$store.Open("ReadWrite")
|
||||
$store.Add($cert)
|
||||
$store.Close()
|
||||
```
|
||||
|
||||
Restart your browser after adding the certificate.
|
||||
|
||||
### Stopping the dev environment
|
||||
|
||||
```bash
|
||||
# Stop containers (preserves database + build cache)
|
||||
docker compose --profile dev down
|
||||
|
||||
# Stop and wipe all data (fresh start)
|
||||
docker compose --profile dev down -v
|
||||
```
|
||||
|
||||
### Direct database access
|
||||
|
||||
Postgres is exposed on port 5432:
|
||||
|
||||
```bash
|
||||
psql postgres://postgres:postgres@localhost:5432/pds
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
- **Source code** is bind-mounted into the containers so that changes made on the host will be immediately reflected in the application
|
||||
- **Backend** uses `cargo-watch` to recompile and restart when Rust files change
|
||||
- **Frontend** uses Vite's HMR for instant browser updates when frontend files change
|
||||
- **Build cache** (`target/` directory and cargo registry) are stored in Docker volumes, so incremental compilation persists across container restarts
|
||||
- **Traefik** routes `/`, `/xrpc`, `/oauth`, `/.well-known`, `/u`, and `/health` to the backend; everything else goes to the Vite dev server
|
||||
- **Mailpit** captures all outgoing email — open [http://localhost:8025](http://localhost:8025) to view verification emails during registration
|
||||
- **PLC Directory** runs locally so DID registration doesn't hit the real `plc.directory`
|
||||
|
||||
### Running the backend natively
|
||||
|
||||
If you prefer running the Rust backend outside Docker (faster incremental builds on host), you need:
|
||||
|
||||
- Rust toolchain (see `rust-toolchain.toml`)
|
||||
- `protoc` (`brew install protobuf` on macOS)
|
||||
- PostgreSQL (start with `docker compose up db`)
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
cargo run -p tranquil-server -- --config config.toml
|
||||
```
|
||||
|
||||
And start the frontend separately:
|
||||
|
||||
```bash
|
||||
cd frontend && pnpm install && pnpm dev
|
||||
```
|
||||
Generated
+1542
-657
File diff suppressed because it is too large
Load Diff
+35
-5
@@ -22,10 +22,11 @@ members = [
|
||||
"crates/tranquil-api",
|
||||
"crates/tranquil-lexicon",
|
||||
"crates/tranquil-signal",
|
||||
"crates/tranquil-store",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.7"
|
||||
version = "0.6.5"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -50,13 +51,15 @@ 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" }
|
||||
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"
|
||||
|
||||
aes-gcm = "0.10"
|
||||
arc-swap = "1"
|
||||
backon = "1"
|
||||
bincode = { version = "2", features = ["serde"] }
|
||||
anyhow = "1.0"
|
||||
@@ -79,11 +82,15 @@ foca = { version = "1", features = ["bincode-codec", "tracing"] }
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
governor = "0.10"
|
||||
h3 = "0.0.8"
|
||||
h3-quinn = "0.0.10"
|
||||
hex = "0.4"
|
||||
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
|
||||
hkdf = "0.12"
|
||||
hmac = "0.12"
|
||||
http = "1.4"
|
||||
hyper = { version = "1", features = ["server", "http1", "http2"] }
|
||||
hyper-util = { version = "0.1", features = ["server", "server-auto", "server-graceful", "service", "tokio"] }
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
|
||||
qrcodegen = "1.8"
|
||||
infer = "0.19"
|
||||
@@ -91,6 +98,7 @@ ipld-core = "0.4"
|
||||
iroh-car = "0.5"
|
||||
jacquard-common = { version = "0.9", features = ["crypto-k256"] }
|
||||
jacquard-repo = "0.9"
|
||||
lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "tokio1", "tokio1-rustls-tls", "pool", "dkim", "tracing"] }
|
||||
jsonwebtoken = { version = "10.2", features = ["rust_crypto"] }
|
||||
k256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
|
||||
metrics = "0.24"
|
||||
@@ -100,9 +108,15 @@ parking_lot = "0.12"
|
||||
multihash = "0.19"
|
||||
p256 = { version = "0.13", features = ["ecdsa"] }
|
||||
p384 = { version = "0.13", features = ["ecdsa"] }
|
||||
quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "rustls-ring", "log"] }
|
||||
rand = "0.8"
|
||||
rcgen = { version = "0.14", default-features = false, features = ["ring", "pem"] }
|
||||
redis = { version = "1.0", features = ["tokio-comp", "connection-manager"] }
|
||||
regex = "1"
|
||||
rsa = "0.9"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||
rustls-pemfile = "2"
|
||||
secrecy = { version = "0.10", features = ["serde"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots", "http2", "charset", "macos-system-configuration"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
@@ -114,8 +128,9 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid"
|
||||
subtle = "2.5"
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "time", "signal", "process", "io-util", "fs"] }
|
||||
tokio-util = "0.7.18"
|
||||
tokio-util = { version = "0.7.18", features = ["rt"] }
|
||||
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] }
|
||||
totp-rs = { version = "5", features = ["qr"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["fs", "cors"] }
|
||||
@@ -124,8 +139,9 @@ tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
urlencoding = "2.1"
|
||||
uuid = { version = "1.19", features = ["v4", "v5", "v7", "fast-rng", "serde"] }
|
||||
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
|
||||
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys", "conditional-ui"] }
|
||||
webauthn-rs-proto = "0.5"
|
||||
x509-parser = "0.18"
|
||||
zip = { version = "7.0", default-features = false, features = ["deflate"] }
|
||||
|
||||
ciborium = "0.2"
|
||||
@@ -138,6 +154,20 @@ wiremock = "0.6"
|
||||
curve25519-dalek = { git = "https://github.com/signalapp/curve25519-dalek", tag = "signal-curve25519-4.1.3" }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
opt-level = "s"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
codegen-units = 256
|
||||
|
||||
[profile.lewis]
|
||||
inherits = "release"
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.bench]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
debug = 1
|
||||
strip = false
|
||||
|
||||
+45
-28
@@ -1,10 +1,35 @@
|
||||
FROM denoland/deno:alpine AS frontend
|
||||
WORKDIR /app
|
||||
COPY frontend/ ./
|
||||
RUN deno task build
|
||||
ARG DISTROLESS_IMAGE=gcr.io/distroless/cc-debian13:latest@sha256:1e3c6d9c255be500eb680cdea0ad07554f52ae92dfcbdf07043a2a435b4c1fe3
|
||||
|
||||
FROM rust:1.92-alpine AS builder
|
||||
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
|
||||
FROM node:24-trixie-slim AS frontend
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
WORKDIR /app
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY frontend/ ./
|
||||
RUN pnpm build
|
||||
|
||||
FROM rust:1.96-slim-trixie AS builder
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates pkg-config libssl-dev mold clang protobuf-compiler curl xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ARG COMPRESS="true"
|
||||
RUN set -eux; \
|
||||
if [ "$COMPRESS" = "true" ]; then \
|
||||
arch="$(uname -m)"; \
|
||||
case "$arch" in \
|
||||
x86_64) upx_arch=amd64; upx_sha=ddc2654063fe4dc80d95b420788494e4db078ebb01a650692d623b5a9906e61e ;; \
|
||||
aarch64) upx_arch=arm64; upx_sha=100310f74eb6f67694d1d0377f1c729b6a49238ce8c4de21ea2e7d3406186f8b ;; \
|
||||
*) echo "upx: no prebuilt binary for $arch, skipping compression"; upx_arch="" ;; \
|
||||
esac; \
|
||||
if [ -n "$upx_arch" ]; then \
|
||||
curl -fsSL -o /tmp/upx.tar.xz "https://github.com/upx/upx/releases/download/v5.0.2/upx-5.0.2-${upx_arch}_linux.tar.xz"; \
|
||||
echo "${upx_sha} /tmp/upx.tar.xz" | sha256sum -c -; \
|
||||
tar -xJf /tmp/upx.tar.xz -C /tmp; \
|
||||
install -m0755 "/tmp/upx-5.0.2-${upx_arch}_linux/upx" /usr/local/bin/upx; \
|
||||
rm -rf /tmp/upx.tar.xz "/tmp/upx-5.0.2-${upx_arch}_linux"; \
|
||||
fi; \
|
||||
fi
|
||||
RUN mkdir -p /stage/var/lib/tranquil-pds/blobs /stage/var/lib/tranquil-pds/store
|
||||
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
|
||||
WORKDIR /app
|
||||
ARG SLIM="false"
|
||||
@@ -29,35 +54,27 @@ 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 \
|
||||
--mount=type=cache,target=/app/target \
|
||||
COPY migrations ./migrations
|
||||
RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,id=tranquil-target,target=/app/target,sharing=locked \
|
||||
if [ "$SLIM" = "true" ]; then \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-server --no-default-features; \
|
||||
else \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-server; \
|
||||
fi && \
|
||||
cp target/release/tranquil-server /tmp/tranquil-pds
|
||||
cp target/release/tranquil-server /tmp/tranquil-pds && \
|
||||
if [ "$COMPRESS" = "true" ] && command -v upx >/dev/null 2>&1; then upx --best --lzma /tmp/tranquil-pds; fi
|
||||
|
||||
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/* \
|
||||
&& 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
|
||||
FROM ${DISTROLESS_IMAGE}
|
||||
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
|
||||
COPY --from=builder --chown=65532:65532 /stage/var/lib/tranquil-pds /var/lib/tranquil-pds
|
||||
COPY --from=frontend --chown=65532:65532 /app/dist /var/lib/tranquil-pds/frontend
|
||||
WORKDIR /var/lib/tranquil-pds
|
||||
ENV SERVER_HOST=[::]
|
||||
ENV SERVER_PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["tranquil-pds"]
|
||||
ENTRYPOINT ["/usr/local/bin/tranquil-pds"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM rust:1.96-slim-trixie
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
mold \
|
||||
clang \
|
||||
protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN cargo install cargo-watch
|
||||
|
||||
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
|
||||
ENV SQLX_OFFLINE=true
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["cargo", "watch", "-x", "run -p tranquil-server -- --config /app/config.dev.toml"]
|
||||
@@ -2,17 +2,25 @@
|
||||
|
||||
A Personal Data Server for the AT Protocol.
|
||||
|
||||
Bluesky runs on a federated protocol called AT Protocol. Your account lives on a PDS, a server that stores your posts, profile, follows, and cryptographic keys. Bluesky hosts one for you at bsky.social, but you can run your own. Self-hosting means you control your data; you're not dependent on any company's servers, and your account + data is actually yours.
|
||||
"A what for the what?" -> glad you asked: Bluesky, Tangled, and a bunch of other web applications use a federated protocol called AT Protocol (atproto). Your account lives on a PDS, a server that stores your posts, profile, follows, cryptographic keys, et cetera. The beauty is that a PDS is the *only* place your data lives permanently - so you can navigate any atproto app knowing that your data is yours and not getting locked behind any one app's walls.
|
||||
|
||||
This particular PDS thrives under harsh conditions. It is a dandelion growing through the cracks in the sidewalk concrete.
|
||||
We came together to make this PDS to enable and empower our users to better host their data on this shared protocol. All of our decisions as a project are guided by their usefulness to the community: PDS hosters and end-users both.
|
||||
|
||||
It has full compatibility with Bluesky's reference PDS.
|
||||
Comparatively: Bluesky the company created a "reference PDS" that we can self-host quite easily, and that's great, but Bluesky has an incentive to make software for themselves first & foremost, then secondly their software can be useful for us self-hosters. In contrast, Tranquil is not from a company, and will never be.
|
||||
|
||||
## What's different about Tranquil PDS
|
||||
|
||||
It is a superset of the reference PDS, including: passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices), SSO login and signup, did:web support (PDS-hosted subdomains or bring-your-own), multi-channel communication (email, discord, telegram, signal) for verification and alerts, granular OAuth scopes with a consent UI showing human-readable descriptions, app passwords with granular permissions (read-only, post-only, or custom scopes), account delegation (letting others manage an account with configurable permission levels), and a built-in web UI for account management, repo browsing, and admin.
|
||||
It is a superset of the reference PDS, including:
|
||||
- passkeys and 2FA: WebAuthn/FIDO2, TOTP, backup codes, trusted devices
|
||||
- SSO login and signup
|
||||
- did:web support: PDS-hosted subdomains or bring-your-own
|
||||
- multi-channel communication: you can be notified via email, discord, telegram, and signal for verification and alerts
|
||||
- granular OAuth scopes with a consent UI that allows unchecking specific scopes
|
||||
- app passwords with the same granular permission scope system as OAuth
|
||||
- account delegation: letting others manage an account with configurable permission levels
|
||||
- a built-in web UI for account management, repo browsing, and admin
|
||||
|
||||
The PDS itself is a single binary with no nodeJS runtime. However, at time of writing, Tranquil requires postgres running separately. Blobs are stored on the local filesystem by default (S3 optional). Valkey is also optional (as an alternative to the built-in cache).
|
||||
Unlike the ref PDS, Tranquil is a single binary with no nodejs runtime. That said, at time of writing, Tranquil does require postgres running separately.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -38,33 +46,64 @@ just test
|
||||
just lint
|
||||
```
|
||||
|
||||
Nix users can enter a devshell with `nix develop`, or `direnv allow` to auto-enter via the bundled `.envrc`. Pre-built artifacts including the devshell are available from our [binary cache](docs/2_INSTALL_NIX.md#binary-cache).
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Quick Deploy (Docker/Podman Compose)
|
||||
|
||||
Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
|
||||
`docker-compose.prod.yaml` pulls the prebuilt image `atcr.io/tranquil.farm/tranquil-pds:latest`. Sign in to the registry first with `podman login atcr.io`. The Containers guide covers building from source.
|
||||
|
||||
```bash
|
||||
cp example.toml config.toml
|
||||
```
|
||||
|
||||
Edit `config.toml` with your values and generate secrets with `openssl rand -base64 48`. Set the postgres password to match `docker-compose.prod.yaml`. nginx needs a TLS certificate before it starts, so follow the wildcard cert steps in the [Containers guide](docs/2_INSTALL_CONTAINERS.md).
|
||||
|
||||
```bash
|
||||
podman-compose -f docker-compose.prod.yaml up -d
|
||||
```
|
||||
|
||||
### Installation Guides
|
||||
|
||||
- [Debian](docs/install-debian.md)
|
||||
- [Containers](docs/install-containers.md)
|
||||
- [Kubernetes](docs/install-kubernetes.md)
|
||||
- [Nix](docs/2_INSTALL_NIX.md)
|
||||
- [Containers](docs/2_INSTALL_CONTAINERS.md)
|
||||
|
||||
## Maintainers to ping
|
||||
## Community
|
||||
|
||||
- [@lewis.moe](https://bsky.app/profile/did:plc:3fwecdnvtcscjnrx2p4n7alz)
|
||||
- [@nel.pet](https://bsky.app/profile/did:plc:h5wsnqetncv6lu2weom35lg2)
|
||||
### "Let's connect!" or whatever linkedin-types say
|
||||
|
||||
## Thanks
|
||||
We currently don't have a shared space to chat and organize Tranquil things, but we're very interested in changing that in the near future. What do you suggest? Anything but a discord server.
|
||||
|
||||
This project is very grateful to [@nonbinary.computer](https://bsky.app/profile/did:plc:yfvwmnlztr4dwkb7hwz55r2g), [@juli.ee](https://bsky.app/profile/did:plc:7vimlesenouvuaqvle42yhvo), [@mary.my.id](https://bsky.app/profile/did:plc:ia76kvnndjutgedggx2ibrem), and [@baileytownsend.dev](https://bsky.app/profile/did:plc:rnpkyqnmsw4ipey6eotbdnnf) for their help and their code to lean on.
|
||||
### Core team
|
||||
|
||||
- [@oyster.cafe](https://tangled.org/did:plc:3fwecdnvtcscjnrx2p4n7alz)
|
||||
- [@nel.pet](https://tangled.org/did:plc:h5wsnqetncv6lu2weom35lg2)
|
||||
|
||||
### Amazing contributors
|
||||
|
||||
- [@isabelroses.com](https://tangled.org/did:plc:qxichs7jsycphrsmbujwqbfb)
|
||||
- [@quilling.dev](https://tangled.org/did:plc:jrtgsidnmxaen4offglr5lsh)
|
||||
- [@koi.rip](https://tangled.org/did:plc:b26ewgkrnx3yvsp2cdao3ntu)
|
||||
- [@bas.sh](https://tangled.org/did:plc:c52wep6lj4sfbsqiz3yvb55h)
|
||||
- [@nekomimi.pet](https://tangled.org/did:plc:ttdrpj45ibqunmfhdsb4zdwq)
|
||||
- [@islacant.win](https://tangled.org/did:plc:aut6evcs6d6ngaunqgfhdzzu)
|
||||
- [@a.starrysky.fyi](https://tangled.org/did:plc:uuyqs6y3pwtbteet4swt5i5y)
|
||||
- [@sans-self.org](https://tangled.org/did:plc:wydyrngmxbcsqdvhmd7whmye)
|
||||
- [@tachyonism.tngl.sh](https://tangled.org/did:plc:w6qiwij62bmdugsd3gemhpy2)
|
||||
- [@trezy.codes](https://tangled.org/did:plc:4jrld6fwpnwqehtce56qshzv)
|
||||
- [@ave.zone](https://tangled.org/did:plc:mchrltkrhuzpxleiwpmmvpar)
|
||||
- Could be your name here too!
|
||||
|
||||
### Tranquil PDS instances in the wild!
|
||||
|
||||
- [Tranquil Farm](https://tranquil.farm)
|
||||
- Your instance here!! Don't be a stranger.
|
||||
|
||||
### Special thanks
|
||||
|
||||
This project is very grateful to [@nonbinary.computer](https://tangled.org/did:plc:yfvwmnlztr4dwkb7hwz55r2g), [@juliet.paris](https://tangled.org/did:plc:hs3aly5l26pozymy4b6hz7ae), [@mary.my.id](https://tangled.org/did:plc:ia76kvnndjutgedggx2ibrem), [@baileytownsend.dev](https://tangled.org/did:plc:rnpkyqnmsw4ipey6eotbdnnf), and [@ptr.pet](https://tangled.org/did:plc:dfl62fgb7wtjj3fcbb72naae) for their help and their code to lean on.
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0-or-later. Documentation is CC BY-SA 4.0. See [LICENSE](LICENSE) for details.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[server]
|
||||
hostname = "pds.test"
|
||||
allow_http_proxy = true
|
||||
invite_code_required = false
|
||||
disable_rate_limiting = true
|
||||
|
||||
[frontend]
|
||||
enabled = true
|
||||
dir = "/app/frontend/public"
|
||||
|
||||
[database]
|
||||
url = "postgres://postgres:postgres@db:5432/pds"
|
||||
|
||||
[storage]
|
||||
path = "/var/lib/tranquil-pds/blobs"
|
||||
|
||||
[plc]
|
||||
directory_url = "http://plc:2582"
|
||||
|
||||
[email]
|
||||
from_address = "noreply@pds.test"
|
||||
from_name = "Tranquil PDS (Dev)"
|
||||
|
||||
[email.smarthost]
|
||||
host = "mailpit"
|
||||
port = 1025
|
||||
tls = "none"
|
||||
|
||||
[secrets]
|
||||
allow_insecure = true
|
||||
@@ -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 }
|
||||
|
||||
@@ -12,7 +12,6 @@ use tranquil_pds::auth::{Auth, NotTakendown, Permissive};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
const APP_BSKY_NAMESPACE: &str = "app.bsky";
|
||||
const MAX_PREFERENCES_COUNT: usize = 100;
|
||||
const MAX_PREFERENCE_SIZE: usize = 10_000;
|
||||
const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref";
|
||||
const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref";
|
||||
@@ -34,13 +33,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()))
|
||||
@@ -92,18 +91,20 @@ pub async fn put_preferences(
|
||||
auth: Auth<NotTakendown>,
|
||||
Json(input): Json<PutPreferencesInput>,
|
||||
) -> Response {
|
||||
let max_preferences_count: usize = tranquil_config::get().server.max_preferences_count;
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
if input.preferences.len() > MAX_PREFERENCES_COUNT {
|
||||
if input.preferences.len() > max_preferences_count {
|
||||
return ApiError::InvalidRequest(format!(
|
||||
"Too many preferences: {} exceeds limit of {}",
|
||||
input.preferences.len(),
|
||||
MAX_PREFERENCES_COUNT
|
||||
max_preferences_count
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
@@ -188,7 +189,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()
|
||||
|
||||
@@ -19,7 +19,8 @@ pub async fn delete_account(
|
||||
) -> 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")?
|
||||
@@ -27,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")?;
|
||||
|
||||
@@ -31,7 +31,8 @@ pub async fn send_email(
|
||||
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")?
|
||||
@@ -45,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,
|
||||
|
||||
@@ -69,7 +69,8 @@ pub async fn get_account_info(
|
||||
Query(params): Query<GetAccountInfoParams>,
|
||||
) -> Result<Json<AccountInfo>, ApiError> {
|
||||
let account = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_admin_account_info_by_did(¶ms.did)
|
||||
.await
|
||||
.log_db_err("in get_account_info")?
|
||||
@@ -98,7 +99,8 @@ pub async fn get_account_info(
|
||||
|
||||
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()??;
|
||||
@@ -111,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()?;
|
||||
@@ -123,7 +126,8 @@ 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()?;
|
||||
@@ -154,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()
|
||||
@@ -197,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")?;
|
||||
@@ -205,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();
|
||||
@@ -217,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()
|
||||
@@ -226,7 +234,8 @@ 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()
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -51,15 +51,14 @@ pub async fn search_accounts(
|
||||
Query(params): Query<SearchAccountsParams>,
|
||||
) -> 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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};
|
||||
@@ -30,7 +30,8 @@ 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
|
||||
{
|
||||
@@ -71,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()
|
||||
@@ -81,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
|
||||
{
|
||||
@@ -146,13 +150,11 @@ pub async fn update_account_password(
|
||||
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
|
||||
{
|
||||
@@ -164,3 +166,34 @@ pub async fn update_account_password(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {}))
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ pub async fn get_server_config(
|
||||
];
|
||||
|
||||
let rows = state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_server_configs(keys)
|
||||
.await
|
||||
.log_db_err("fetching server config")?;
|
||||
@@ -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,13 +225,15 @@ 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")?;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn disable_invite_codes(
|
||||
Json(input): Json<DisableInviteCodesInput>,
|
||||
) -> 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);
|
||||
}
|
||||
@@ -32,7 +32,8 @@ 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
|
||||
{
|
||||
@@ -87,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")?;
|
||||
@@ -96,7 +98,8 @@ 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()
|
||||
@@ -108,7 +111,8 @@ pub async fn get_invite_codes(
|
||||
} else {
|
||||
common::group_invite_uses_by_code(
|
||||
state
|
||||
.infra_repo
|
||||
.repos
|
||||
.infra
|
||||
.get_invite_code_uses_batch(&code_strings)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
@@ -168,7 +172,8 @@ 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
|
||||
{
|
||||
@@ -200,7 +205,8 @@ 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
|
||||
{
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -17,10 +17,10 @@ pub async fn get_server_stats(
|
||||
State(state): State<AppState>,
|
||||
_auth: Auth<Admin>,
|
||||
) -> Result<Json<ServerStatsOutput>, 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);
|
||||
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(ServerStatsOutput {
|
||||
user_count,
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
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 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 linked = match &state.signal_sender {
|
||||
Some(slot) => slot.is_linked().await,
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(Json(SignalStatusOutput { 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!("server shutting down, aborting signal linking");
|
||||
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");
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"discarding completed signal link, generation mismatch or already linked"
|
||||
);
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -45,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,
|
||||
@@ -77,7 +77,7 @@ 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,
|
||||
@@ -109,7 +109,7 @@ 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,
|
||||
@@ -172,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| {
|
||||
@@ -182,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!(
|
||||
@@ -194,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
|
||||
@@ -207,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
|
||||
@@ -218,7 +229,7 @@ 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))
|
||||
@@ -249,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| {
|
||||
@@ -282,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| {
|
||||
|
||||
@@ -68,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,
|
||||
@@ -89,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())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bcrypt::DEFAULT_COST;
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
use tracing::error;
|
||||
@@ -231,10 +231,19 @@ pub async fn verify_credential(
|
||||
app_passwords
|
||||
.into_iter()
|
||||
.find(|app| bcrypt::verify(password, &app.password_hash).unwrap_or(false))
|
||||
.map(|app| CredentialMatch::AppPassword {
|
||||
name: app.name,
|
||||
scopes: app.scopes,
|
||||
controller_did: app.created_by_controller_did,
|
||||
.map(|app| {
|
||||
let scopes = app.scopes.unwrap_or_else(|| {
|
||||
if app.privilege.is_privileged() {
|
||||
"transition:generic transition:chat.bsky".to_string()
|
||||
} else {
|
||||
"transition:generic".to_string()
|
||||
}
|
||||
});
|
||||
CredentialMatch::AppPassword {
|
||||
name: app.name,
|
||||
scopes: Some(scopes),
|
||||
controller_did: app.created_by_controller_did,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -245,6 +254,20 @@ pub fn hash_or_internal_error(value: &str) -> Result<String, ApiError> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn hash_password_async(password: &str) -> Result<String, ApiError> {
|
||||
let password = password.to_string();
|
||||
tokio::task::spawn_blocking(move || hash(password, DEFAULT_COST))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
error!("Failed to hash password: {:?}", e);
|
||||
ApiError::InternalError(None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_token_hash(
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
stored_hash: &str,
|
||||
|
||||
@@ -24,7 +24,8 @@ pub async fn list_controllers(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<ControllersOutput<Vec<tranquil_db_traits::ControllerInfo>>>, ApiError> {
|
||||
let controllers = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_delegations_for_account(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -37,8 +38,9 @@ pub async fn list_controllers(
|
||||
async move {
|
||||
if c.handle.is_none() {
|
||||
c.handle = did_resolver
|
||||
.resolve_did_document(c.did.as_str())
|
||||
.fetch_did_document(c.did.as_str())
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
|
||||
.map(Into::into);
|
||||
}
|
||||
@@ -64,7 +66,7 @@ pub async fn add_controller(
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
.map_err(|_| ApiError::ControllerNotFound)?;
|
||||
|
||||
if !resolved.is_local
|
||||
&& let Some(ref pds_url) = resolved.pds_url
|
||||
@@ -99,7 +101,8 @@ pub async fn add_controller(
|
||||
|
||||
if resolved.is_local
|
||||
&& state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.is_delegated_account(&input.controller_did)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
@@ -110,7 +113,8 @@ pub async fn add_controller(
|
||||
}
|
||||
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.create_delegation(
|
||||
can_add.did(),
|
||||
&input.controller_did,
|
||||
@@ -121,7 +125,8 @@ pub async fn add_controller(
|
||||
{
|
||||
Ok(_) => {
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
can_add.did(),
|
||||
can_add.did(),
|
||||
@@ -158,27 +163,29 @@ pub async fn remove_controller(
|
||||
Json(input): Json<RemoveControllerInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.revoke_delegation(&auth.did, &input.controller_did, &auth.did)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let revoked_app_passwords = state
|
||||
.session_repo
|
||||
.repos
|
||||
.session
|
||||
.delete_app_passwords_by_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.unwrap_or(0usize);
|
||||
.unwrap_or(0);
|
||||
|
||||
let revoked_oauth_tokens = state
|
||||
.oauth_repo
|
||||
.repos
|
||||
.oauth
|
||||
.revoke_tokens_for_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
@@ -217,20 +224,38 @@ pub async fn update_controller_scopes(
|
||||
Json(input): Json<UpdateControllerScopesInput>,
|
||||
) -> Result<Json<SuccessResponse>, ApiError> {
|
||||
match state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.update_delegation_scopes(&auth.did, &input.controller_did, &input.granted_scopes)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let revoked_app_passwords = state
|
||||
.repos
|
||||
.session
|
||||
.delete_app_passwords_by_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let revoked_oauth_tokens = state
|
||||
.repos
|
||||
.oauth
|
||||
.revoke_tokens_for_controller(&auth.did, &input.controller_did)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&auth.did,
|
||||
&auth.did,
|
||||
Some(&input.controller_did),
|
||||
DelegationActionType::ScopesModified,
|
||||
Some(json!({
|
||||
"new_scopes": input.granted_scopes.as_str()
|
||||
"new_scopes": input.granted_scopes.as_str(),
|
||||
"revoked_app_passwords": revoked_app_passwords,
|
||||
"revoked_oauth_tokens": revoked_oauth_tokens
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
@@ -254,7 +279,8 @@ pub async fn list_controlled_accounts(
|
||||
auth: Auth<Active>,
|
||||
) -> Result<Json<AccountsOutput<Vec<tranquil_db_traits::DelegatedAccountInfo>>>, ApiError> {
|
||||
let accounts = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_accounts_controlled_by(&auth.did)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -286,7 +312,8 @@ pub async fn get_audit_log(
|
||||
let offset = params.offset.max(0);
|
||||
|
||||
let entries = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.get_audit_log_for_account(&auth.did, limit, offset)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -295,7 +322,8 @@ pub async fn get_audit_log(
|
||||
})?;
|
||||
|
||||
let total = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.count_audit_log_entries(&auth.did)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -316,7 +344,6 @@ pub struct CreateDelegatedAccountInput {
|
||||
pub handle: String,
|
||||
pub email: Option<String>,
|
||||
pub controller_scopes: ValidatedDelegationScope,
|
||||
pub invite_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -348,19 +375,6 @@ pub async fn create_delegated_account(
|
||||
return Err(ApiError::InvalidEmail);
|
||||
}
|
||||
|
||||
let validated_invite_code = if let Some(ref code) = input.invite_code {
|
||||
match state.infra_repo.validate_invite_code(code).await {
|
||||
Ok(validated) => Some(validated),
|
||||
Err(_) => return Err(ApiError::InvalidInviteCode),
|
||||
}
|
||||
} else {
|
||||
let invite_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_required {
|
||||
return Err(ApiError::InviteCodeRequired);
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
let plc = create_plc_did(&state, &handle).await.map_err(|e| {
|
||||
tracing::error!("PLC DID creation failed: {:?}", e);
|
||||
e
|
||||
@@ -383,15 +397,15 @@ pub async fn create_delegated_account(
|
||||
commit_cid: repo.commit_cid.to_string(),
|
||||
repo_rev: repo.repo_rev.clone(),
|
||||
genesis_block_cids: repo.genesis_block_cids,
|
||||
invite_code: input.invite_code.clone(),
|
||||
};
|
||||
|
||||
let user_id = match state
|
||||
.user_repo
|
||||
match state
|
||||
.repos
|
||||
.user
|
||||
.create_delegated_account(&create_input)
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
Ok(_) => {}
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return Err(ApiError::HandleNotAvailable(None));
|
||||
}
|
||||
@@ -402,15 +416,6 @@ pub async fn create_delegated_account(
|
||||
error!("Error creating delegated account: {:?}", e);
|
||||
return Err(ApiError::InternalError(None));
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(validated) = validated_invite_code
|
||||
&& let Err(e) = state
|
||||
.infra_repo
|
||||
.record_invite_code_use(&validated, user_id)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to record invite code use for {}: {:?}", did, e);
|
||||
}
|
||||
|
||||
crate::identity::provision::sequence_new_account(
|
||||
@@ -423,7 +428,8 @@ pub async fn create_delegated_account(
|
||||
.await;
|
||||
|
||||
let _ = state
|
||||
.delegation_repo
|
||||
.repos
|
||||
.delegation
|
||||
.log_delegation_action(
|
||||
&did,
|
||||
&auth.did,
|
||||
@@ -461,7 +467,7 @@ pub async fn resolve_controller(
|
||||
} else {
|
||||
let local_handle: Option<Handle> = identifier.parse().ok();
|
||||
let local_user = match local_handle {
|
||||
Some(ref h) => state.user_repo.get_by_handle(h).await.ok().flatten(),
|
||||
Some(ref h) => state.repos.user.get_by_handle(h).await.ok().flatten(),
|
||||
None => None,
|
||||
};
|
||||
match local_user {
|
||||
@@ -476,7 +482,7 @@ pub async fn resolve_controller(
|
||||
|
||||
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did)
|
||||
.await
|
||||
.ok_or(ApiError::ControllerNotFound)?;
|
||||
.map_err(|_| ApiError::ControllerNotFound)?;
|
||||
|
||||
Ok(Json(resolved))
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response {
|
||||
);
|
||||
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.store_discord_user_id(&discord_username, &discord_user_id, handle.as_deref())
|
||||
.await
|
||||
{
|
||||
@@ -180,8 +181,8 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response {
|
||||
"Verified Discord user and stored user ID"
|
||||
);
|
||||
if let Err(e) = comms_repo::enqueue_channel_verified(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
state.repos.user.as_ref(),
|
||||
state.repos.infra.as_ref(),
|
||||
user_id,
|
||||
tranquil_db_traits::CommsChannel::Discord,
|
||||
&discord_user_id,
|
||||
|
||||
@@ -6,17 +6,16 @@ use axum::{
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, info};
|
||||
use tranquil_pds::api::error::ApiError;
|
||||
use tranquil_pds::api::invite::check_registration_invite;
|
||||
use tranquil_pds::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
|
||||
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
|
||||
use tranquil_pds::state::AppState;
|
||||
use tranquil_pds::types::{Did, Handle, PlainPassword};
|
||||
use tranquil_pds::validation::validate_password;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateAccountInput {
|
||||
@@ -68,14 +67,16 @@ async fn try_reactivate_migration(
|
||||
new_email: email.clone(),
|
||||
};
|
||||
match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.reactivate_migration_account(&reactivate_input)
|
||||
.await
|
||||
{
|
||||
Ok(reactivated) => {
|
||||
info!(did = %did, old_handle = %reactivated.old_handle, new_handle = %handle, "Preparing existing account for inbound migration");
|
||||
let secret_key_bytes = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_key_by_id(reactivated.user_id)
|
||||
.await
|
||||
{
|
||||
@@ -130,7 +131,7 @@ async fn try_reactivate_migration(
|
||||
controller_did: None,
|
||||
app_password_name: None,
|
||||
};
|
||||
if let Err(e) = state.session_repo.create_session(&session_data).await {
|
||||
if let Err(e) = state.repos.session.create_session(&session_data).await {
|
||||
error!("Error creating session: {:?}", e);
|
||||
return Some(ApiError::InternalError(None).into_response());
|
||||
}
|
||||
@@ -154,7 +155,12 @@ async fn try_reactivate_migration(
|
||||
Json(CreateAccountOutput {
|
||||
handle: handle.to_string().into(),
|
||||
did: did_typed.clone(),
|
||||
did_doc: state.did_resolver.resolve_did_document(did).await,
|
||||
did_doc: state
|
||||
.did_resolver
|
||||
.fetch_did_document(did)
|
||||
.await
|
||||
.ok()
|
||||
.map(|f| (*f).clone()),
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
verification_required,
|
||||
@@ -395,7 +401,8 @@ pub async fn create_account(
|
||||
Err(_) => return ApiError::InvalidHandle(None).into_response(),
|
||||
};
|
||||
let handle_available = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_handle_available_for_new_account(&handle_typed)
|
||||
.await
|
||||
{
|
||||
@@ -409,58 +416,20 @@ pub async fn create_account(
|
||||
return ApiError::HandleTaken.into_response();
|
||||
}
|
||||
|
||||
let is_bootstrap = state.bootstrap_invite_code.is_some()
|
||||
&& state.user_repo.count_users().await.unwrap_or(1) == 0;
|
||||
|
||||
if is_bootstrap {
|
||||
match input.invite_code.as_deref() {
|
||||
Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => {}
|
||||
_ => return ApiError::InvalidInviteCode.into_response(),
|
||||
}
|
||||
} else {
|
||||
let invite_code_required = tranquil_config::get().server.invite_code_required;
|
||||
if invite_code_required
|
||||
&& input
|
||||
.invite_code
|
||||
.as_ref()
|
||||
.map(|c| c.trim().is_empty())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return ApiError::InviteCodeRequired.into_response();
|
||||
}
|
||||
if let Some(code) = &input.invite_code
|
||||
&& !code.trim().is_empty()
|
||||
{
|
||||
let valid = match state.user_repo.check_and_consume_invite_code(code).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error checking invite code: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
if !valid {
|
||||
return ApiError::InvalidInviteCode.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
let invite_registration =
|
||||
match check_registration_invite(&state, input.invite_code.as_deref()).await {
|
||||
Ok(outcome) => outcome,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
if let Err(e) = validate_password(&input.password) {
|
||||
return ApiError::InvalidRequest(e.to_string()).into_response();
|
||||
}
|
||||
|
||||
let password_clone = input.password.clone();
|
||||
let password_hash =
|
||||
match tokio::task::spawn_blocking(move || hash(&password_clone, DEFAULT_COST)).await {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(e)) => {
|
||||
error!("Error hashing password: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn blocking task: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
};
|
||||
let password_hash = match crate::common::hash_password_async(&input.password).await {
|
||||
Ok(h) => h,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
let deactivated_at: Option<chrono::DateTime<chrono::Utc>> = if is_migration || is_did_web_byod {
|
||||
Some(chrono::Utc::now())
|
||||
@@ -513,21 +482,23 @@ pub async fn create_account(
|
||||
telegram_username: comms.telegram,
|
||||
signal_username: comms.signal,
|
||||
deactivated_at,
|
||||
inbound_migration: is_migration || is_did_web_byod,
|
||||
encrypted_key_bytes: repo.encrypted_key_bytes,
|
||||
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
|
||||
reserved_key_id,
|
||||
commit_cid: commit_cid_str.clone(),
|
||||
repo_rev: rev_str.clone(),
|
||||
genesis_block_cids: repo.genesis_block_cids,
|
||||
invite_code: if is_bootstrap {
|
||||
None
|
||||
} else {
|
||||
input.invite_code.clone()
|
||||
},
|
||||
invite_code: invite_registration.into_invite_code(),
|
||||
birthdate_pref,
|
||||
};
|
||||
|
||||
let create_result = match state.user_repo.create_password_account(&create_input).await {
|
||||
let create_result = match state
|
||||
.repos
|
||||
.user
|
||||
.create_password_account(&create_input)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(tranquil_db_traits::CreateAccountError::HandleTaken) => {
|
||||
return ApiError::HandleNotAvailable(None).into_response();
|
||||
@@ -538,6 +509,9 @@ pub async fn create_account(
|
||||
Err(tranquil_db_traits::CreateAccountError::DidExists) => {
|
||||
return ApiError::AccountAlreadyExists.into_response();
|
||||
}
|
||||
Err(tranquil_db_traits::CreateAccountError::InviteCodeUnavailable) => {
|
||||
return ApiError::InvalidInviteCode.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error creating password account: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
@@ -590,7 +564,7 @@ pub async fn create_account(
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
|
||||
let did_doc = state.did_resolver.resolve_did_document(&did).await;
|
||||
let did_doc = state.did_resolver.fetch_did_document(&did).await.ok();
|
||||
|
||||
if is_migration {
|
||||
info!(
|
||||
@@ -604,7 +578,7 @@ pub async fn create_account(
|
||||
Json(CreateAccountOutput {
|
||||
handle: handle.clone().into(),
|
||||
did: did_for_commit,
|
||||
did_doc,
|
||||
did_doc: did_doc.map(|f| (*f).clone()),
|
||||
access_jwt: session.access_jwt,
|
||||
refresh_jwt: session.refresh_jwt,
|
||||
verification_required: !is_migration,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub async fn resolve_handle(
|
||||
return ApiError::InvalidHandle(Some("Invalid handle format".into())).into_response();
|
||||
}
|
||||
};
|
||||
let user = state.user_repo.get_by_handle(&handle).await;
|
||||
let user = state.repos.user.get_by_handle(&handle).await;
|
||||
match user {
|
||||
Ok(Some(row)) => {
|
||||
let _ = state
|
||||
@@ -165,7 +165,8 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
|
||||
};
|
||||
let user = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_user_for_did_doc_build(&expected_did_typed)
|
||||
.await
|
||||
{
|
||||
@@ -182,7 +183,8 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
|
||||
let did = expected_did;
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_overrides(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -218,7 +220,8 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
}
|
||||
};
|
||||
let user = match state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_info_by_handle(¤t_handle_typed)
|
||||
.await
|
||||
{
|
||||
@@ -246,7 +249,8 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
}
|
||||
|
||||
let overrides = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_did_web_overrides(user_id)
|
||||
.await
|
||||
.ok()
|
||||
@@ -295,7 +299,7 @@ async fn build_override_or_key_verification_methods(
|
||||
.collect());
|
||||
}
|
||||
|
||||
let key_info = match state.user_repo.get_user_key_by_id(user_id).await {
|
||||
let key_info = match state.repos.user.get_user_key_by_id(user_id).await {
|
||||
Ok(Some(k)) => k,
|
||||
_ => return Err(ApiError::InternalError(None).into_response()),
|
||||
};
|
||||
@@ -468,7 +472,8 @@ pub async fn get_recommended_did_credentials(
|
||||
auth: Auth<NotTakendown>,
|
||||
) -> Result<Json<GetRecommendedDidCredentialsOutput>, ApiError> {
|
||||
let handle = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_handle_by_did(&auth.did)
|
||||
.await
|
||||
.log_db_err("fetching handle for DID credentials")?
|
||||
@@ -486,16 +491,10 @@ pub async fn get_recommended_did_credentials(
|
||||
let rotation_keys = if auth.did.starts_with("did:web:") {
|
||||
vec![]
|
||||
} else {
|
||||
let server_rotation_key = match &tranquil_config::get().secrets.plc_rotation_key {
|
||||
Some(key) => key.clone(),
|
||||
None => {
|
||||
warn!(
|
||||
"PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"
|
||||
);
|
||||
did_key.clone()
|
||||
}
|
||||
};
|
||||
vec![server_rotation_key]
|
||||
tranquil_pds::plc::rotation_keys_for(
|
||||
tranquil_config::get().secrets.plc_rotation_key.as_deref(),
|
||||
&signing_key,
|
||||
)
|
||||
};
|
||||
Ok(Json(GetRecommendedDidCredentialsOutput {
|
||||
rotation_keys,
|
||||
@@ -539,7 +538,8 @@ pub async fn update_handle(
|
||||
)
|
||||
.await?;
|
||||
let user_row = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.get_id_and_handle_by_did(&did)
|
||||
.await
|
||||
.log_db_err("fetching user for handle update")?
|
||||
@@ -661,7 +661,8 @@ pub async fn update_handle(
|
||||
.parse()
|
||||
.map_err(|_| ApiError::InvalidHandle(Some("Invalid handle format".into())))?;
|
||||
let handle_exists = state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.check_handle_exists(&handle_typed, user_id)
|
||||
.await
|
||||
.log_db_err("checking handle existence")?;
|
||||
@@ -669,7 +670,8 @@ pub async fn update_handle(
|
||||
return Err(ApiError::HandleTaken);
|
||||
}
|
||||
state
|
||||
.user_repo
|
||||
.repos
|
||||
.user
|
||||
.update_handle(user_id, &handle_typed)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -706,7 +708,7 @@ pub async fn update_plc_handle(
|
||||
if !did.as_str().starts_with("did:plc:") {
|
||||
return Ok(());
|
||||
}
|
||||
let user_row = match state.user_repo.get_user_with_key_by_did(did).await? {
|
||||
let user_row = match state.repos.user.get_user_with_key_by_did(did).await? {
|
||||
Some(r) => r,
|
||||
None => return Ok(()),
|
||||
};
|
||||
@@ -733,7 +735,7 @@ pub async fn well_known_atproto_did(State(state): State<AppState>, headers: Head
|
||||
Ok(h) => h,
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid handle format").into_response(),
|
||||
};
|
||||
let user = state.user_repo.get_by_handle(&handle).await;
|
||||
let user = state.repos.user.get_by_handle(&handle).await;
|
||||
match user {
|
||||
Ok(Some(row)) => row.did.to_string().into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Handle not found").into_response(),
|
||||
|
||||
@@ -3,5 +3,5 @@ mod sign;
|
||||
mod submit;
|
||||
|
||||
pub use request::request_plc_operation_signature;
|
||||
pub use sign::{ServiceInput, SignPlcOperationInput, SignPlcOperationOutput, sign_plc_operation};
|
||||
pub use sign::{SignPlcOperationInput, SignPlcOperationOutput, sign_plc_operation};
|
||||
pub use submit::{SubmitPlcOperationInput, submit_plc_operation};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user