+22
.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json
+22
.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "token",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc"
22
+
}
+15
.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json
+15
.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET deactivated_at = $1 WHERE did = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Timestamptz",
9
+
"Text"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107"
15
+
}
+22
.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json
+22
.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "body",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99"
22
+
}
+3
-3
.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json
.sqlx/query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json
+3
-3
.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json
.sqlx/query-d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87.json
···
1
1
{
2
2
"db_name": "PostgreSQL",
3
-
"query": "\n 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: super::types::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 ",
3
+
"query": "\n 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\n ",
4
4
"describe": {
5
5
"columns": [
6
6
{
···
32
32
},
33
33
{
34
34
"ordinal": 3,
35
-
"name": "comms_type: super::types::CommsType",
35
+
"name": "comms_type: CommsType",
36
36
"type_info": {
37
37
"Custom": {
38
38
"name": "comms_type",
···
153
153
true
154
154
]
155
155
},
156
-
"hash": "20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08"
156
+
"hash": "d4c68f8502bc81c27383f15dca1990c41b5e5534a3db9c137e3ef8e66fdf0a87"
157
157
}
+22
.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json
+22
.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "count",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
null
19
+
]
20
+
},
21
+
"hash": "24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6"
22
+
}
+14
.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json
+14
.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
14
+
}
+54
.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json
+54
.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "subject",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "body",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "comms_type: String",
19
+
"type_info": {
20
+
"Custom": {
21
+
"name": "comms_type",
22
+
"kind": {
23
+
"Enum": [
24
+
"welcome",
25
+
"email_verification",
26
+
"password_reset",
27
+
"email_update",
28
+
"account_deletion",
29
+
"admin_email",
30
+
"plc_operation",
31
+
"two_factor_code",
32
+
"channel_verification",
33
+
"passkey_recovery",
34
+
"legacy_login_alert",
35
+
"migration_verification"
36
+
]
37
+
}
38
+
}
39
+
}
40
+
}
41
+
],
42
+
"parameters": {
43
+
"Left": [
44
+
"Uuid"
45
+
]
46
+
},
47
+
"nullable": [
48
+
true,
49
+
false,
50
+
false
51
+
]
52
+
},
53
+
"hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe"
54
+
}
+22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
+22
.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT id FROM users WHERE email = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068"
22
+
}
+28
.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json
+28
.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT body, metadata FROM comms_queue WHERE user_id = $1 AND comms_type = 'channel_verification' ORDER BY created_at DESC LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "body",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "metadata",
14
+
"type_info": "Jsonb"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Uuid"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
true
25
+
]
26
+
},
27
+
"hash": "4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a"
28
+
}
+28
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json
+28
.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "token",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "expires_at",
14
+
"type_info": "Timestamptz"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
false
25
+
]
26
+
},
27
+
"hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88"
28
+
}
+22
.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json
+22
.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT email_verified FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "email_verified",
9
+
"type_info": "Bool"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6"
22
+
}
+28
.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json
+28
.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT k.key_bytes, k.encryption_version\n FROM user_keys k\n JOIN users u ON k.user_id = u.id\n WHERE u.did = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "key_bytes",
9
+
"type_info": "Bytea"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "encryption_version",
14
+
"type_info": "Int4"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
true
25
+
]
26
+
},
27
+
"hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0"
28
+
}
+22
.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json
+22
.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT body FROM comms_queue WHERE user_id = $1 AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "body",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45"
22
+
}
+22
.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json
+22
.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "subject",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
true
19
+
]
20
+
},
21
+
"hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f"
22
+
}
+22
.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json
+22
.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT t.token\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "token",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631"
22
+
}
+28
.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json
+28
.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "token",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "expires_at",
14
+
"type_info": "Timestamptz"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
false
25
+
]
26
+
},
27
+
"hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81"
28
+
}
+22
.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json
+22
.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "body",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5"
22
+
}
+28
.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json
+28
.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "did",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "public_key_did_key",
14
+
"type_info": "Text"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
true,
24
+
false
25
+
]
26
+
},
27
+
"hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002"
28
+
}
+108
.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json
+108
.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT\n id, user_id, recipient, subject, body,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: CommsType\",\n status as \"status: CommsStatus\"\n FROM comms_queue\n WHERE id = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "user_id",
14
+
"type_info": "Uuid"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "recipient",
19
+
"type_info": "Text"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "subject",
24
+
"type_info": "Text"
25
+
},
26
+
{
27
+
"ordinal": 4,
28
+
"name": "body",
29
+
"type_info": "Text"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "channel: CommsChannel",
34
+
"type_info": {
35
+
"Custom": {
36
+
"name": "comms_channel",
37
+
"kind": {
38
+
"Enum": [
39
+
"email",
40
+
"discord",
41
+
"telegram",
42
+
"signal"
43
+
]
44
+
}
45
+
}
46
+
}
47
+
},
48
+
{
49
+
"ordinal": 6,
50
+
"name": "comms_type: CommsType",
51
+
"type_info": {
52
+
"Custom": {
53
+
"name": "comms_type",
54
+
"kind": {
55
+
"Enum": [
56
+
"welcome",
57
+
"email_verification",
58
+
"password_reset",
59
+
"email_update",
60
+
"account_deletion",
61
+
"admin_email",
62
+
"plc_operation",
63
+
"two_factor_code",
64
+
"channel_verification",
65
+
"passkey_recovery",
66
+
"legacy_login_alert",
67
+
"migration_verification"
68
+
]
69
+
}
70
+
}
71
+
}
72
+
},
73
+
{
74
+
"ordinal": 7,
75
+
"name": "status: CommsStatus",
76
+
"type_info": {
77
+
"Custom": {
78
+
"name": "comms_status",
79
+
"kind": {
80
+
"Enum": [
81
+
"pending",
82
+
"processing",
83
+
"sent",
84
+
"failed"
85
+
]
86
+
}
87
+
}
88
+
}
89
+
}
90
+
],
91
+
"parameters": {
92
+
"Left": [
93
+
"Uuid"
94
+
]
95
+
},
96
+
"nullable": [
97
+
false,
98
+
false,
99
+
false,
100
+
true,
101
+
false,
102
+
false,
103
+
false,
104
+
false
105
+
]
106
+
},
107
+
"hash": "9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe"
108
+
}
+34
.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json
+34
.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "private_key_bytes",
9
+
"type_info": "Bytea"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "expires_at",
14
+
"type_info": "Timestamptz"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "used_at",
19
+
"type_info": "Timestamptz"
20
+
}
21
+
],
22
+
"parameters": {
23
+
"Left": [
24
+
"Text"
25
+
]
26
+
},
27
+
"nullable": [
28
+
false,
29
+
false,
30
+
true
31
+
]
32
+
},
33
+
"hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5"
34
+
}
+22
.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json
+22
.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT token FROM account_deletion_requests WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "token",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5"
22
+
}
+60
.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json
+60
.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT\n recipient, subject, body,\n comms_type as \"comms_type: CommsType\"\n FROM comms_queue\n WHERE id = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "recipient",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "subject",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "body",
19
+
"type_info": "Text"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "comms_type: CommsType",
24
+
"type_info": {
25
+
"Custom": {
26
+
"name": "comms_type",
27
+
"kind": {
28
+
"Enum": [
29
+
"welcome",
30
+
"email_verification",
31
+
"password_reset",
32
+
"email_update",
33
+
"account_deletion",
34
+
"admin_email",
35
+
"plc_operation",
36
+
"two_factor_code",
37
+
"channel_verification",
38
+
"passkey_recovery",
39
+
"legacy_login_alert",
40
+
"migration_verification"
41
+
]
42
+
}
43
+
}
44
+
}
45
+
}
46
+
],
47
+
"parameters": {
48
+
"Left": [
49
+
"Uuid"
50
+
]
51
+
},
52
+
"nullable": [
53
+
false,
54
+
true,
55
+
false,
56
+
false
57
+
]
58
+
},
59
+
"hash": "b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5"
60
+
}
+22
.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json
+22
.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT password_reset_code FROM users WHERE email = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "password_reset_code",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
true
19
+
]
20
+
},
21
+
"hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee"
22
+
}
+22
.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json
+22
.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "count!",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
null
19
+
]
20
+
},
21
+
"hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3"
22
+
}
+14
.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json
+14
.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
14
+
}
+22
.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json
+22
.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "count",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
null
19
+
]
20
+
},
21
+
"hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3"
22
+
}
+22
.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json
+22
.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "used_at",
9
+
"type_info": "Timestamptz"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
true
19
+
]
20
+
},
21
+
"hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9"
22
+
}
+22
.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json
+22
.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT email FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "email",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
true
19
+
]
20
+
},
21
+
"hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7"
22
+
}
+14
.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json
+14
.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET is_admin = TRUE WHERE did = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814"
14
+
}
+28
.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json
+28
.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "password_reset_code",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "password_reset_code_expires_at",
14
+
"type_info": "Timestamptz"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
true,
24
+
true
25
+
]
26
+
},
27
+
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
28
+
}
+168
Cargo.lock
+168
Cargo.lock
···
6314
6314
]
6315
6315
6316
6316
[[package]]
6317
+
name = "tranquil-auth"
6318
+
version = "0.1.0"
6319
+
dependencies = [
6320
+
"anyhow",
6321
+
"base32",
6322
+
"base64 0.22.1",
6323
+
"bcrypt",
6324
+
"chrono",
6325
+
"hmac",
6326
+
"k256",
6327
+
"rand 0.8.5",
6328
+
"serde",
6329
+
"serde_json",
6330
+
"sha2",
6331
+
"subtle",
6332
+
"thiserror 2.0.17",
6333
+
"totp-rs",
6334
+
"tranquil-crypto",
6335
+
"urlencoding",
6336
+
"uuid",
6337
+
]
6338
+
6339
+
[[package]]
6340
+
name = "tranquil-cache"
6341
+
version = "0.1.0"
6342
+
dependencies = [
6343
+
"async-trait",
6344
+
"base64 0.22.1",
6345
+
"redis",
6346
+
"thiserror 2.0.17",
6347
+
"tracing",
6348
+
"tranquil-infra",
6349
+
]
6350
+
6351
+
[[package]]
6352
+
name = "tranquil-comms"
6353
+
version = "0.1.0"
6354
+
dependencies = [
6355
+
"async-trait",
6356
+
"base64 0.22.1",
6357
+
"chrono",
6358
+
"reqwest",
6359
+
"serde",
6360
+
"serde_json",
6361
+
"sqlx",
6362
+
"thiserror 2.0.17",
6363
+
"tokio",
6364
+
"urlencoding",
6365
+
"uuid",
6366
+
]
6367
+
6368
+
[[package]]
6369
+
name = "tranquil-crypto"
6370
+
version = "0.1.0"
6371
+
dependencies = [
6372
+
"aes-gcm",
6373
+
"base64 0.22.1",
6374
+
"hkdf",
6375
+
"hmac",
6376
+
"p256 0.13.2",
6377
+
"rand 0.8.5",
6378
+
"serde",
6379
+
"serde_json",
6380
+
"sha2",
6381
+
"subtle",
6382
+
"thiserror 2.0.17",
6383
+
]
6384
+
6385
+
[[package]]
6386
+
name = "tranquil-infra"
6387
+
version = "0.1.0"
6388
+
dependencies = [
6389
+
"async-trait",
6390
+
"bytes",
6391
+
"futures",
6392
+
"thiserror 2.0.17",
6393
+
"tokio",
6394
+
"tracing",
6395
+
]
6396
+
6397
+
[[package]]
6398
+
name = "tranquil-oauth"
6399
+
version = "0.1.0"
6400
+
dependencies = [
6401
+
"anyhow",
6402
+
"axum",
6403
+
"base64 0.22.1",
6404
+
"chrono",
6405
+
"ed25519-dalek",
6406
+
"p256 0.13.2",
6407
+
"p384",
6408
+
"rand 0.8.5",
6409
+
"reqwest",
6410
+
"serde",
6411
+
"serde_json",
6412
+
"sha2",
6413
+
"sqlx",
6414
+
"tokio",
6415
+
"tracing",
6416
+
"tranquil-types",
6417
+
"uuid",
6418
+
]
6419
+
6420
+
[[package]]
6317
6421
name = "tranquil-pds"
6318
6422
version = "0.1.0"
6319
6423
dependencies = [
···
6380
6484
"tower-layer",
6381
6485
"tracing",
6382
6486
"tracing-subscriber",
6487
+
"tranquil-auth",
6488
+
"tranquil-cache",
6489
+
"tranquil-comms",
6490
+
"tranquil-crypto",
6491
+
"tranquil-infra",
6492
+
"tranquil-oauth",
6493
+
"tranquil-repo",
6494
+
"tranquil-scopes",
6495
+
"tranquil-storage",
6496
+
"tranquil-types",
6383
6497
"urlencoding",
6384
6498
"uuid",
6385
6499
"webauthn-rs",
6386
6500
"webauthn-rs-proto",
6387
6501
"wiremock",
6388
6502
"zip",
6503
+
]
6504
+
6505
+
[[package]]
6506
+
name = "tranquil-repo"
6507
+
version = "0.1.0"
6508
+
dependencies = [
6509
+
"bytes",
6510
+
"cid",
6511
+
"jacquard-repo",
6512
+
"multihash",
6513
+
"sha2",
6514
+
"sqlx",
6515
+
"tranquil-types",
6516
+
]
6517
+
6518
+
[[package]]
6519
+
name = "tranquil-scopes"
6520
+
version = "0.1.0"
6521
+
dependencies = [
6522
+
"axum",
6523
+
"futures",
6524
+
"reqwest",
6525
+
"serde",
6526
+
"serde_json",
6527
+
"tokio",
6528
+
"tracing",
6529
+
]
6530
+
6531
+
[[package]]
6532
+
name = "tranquil-storage"
6533
+
version = "0.1.0"
6534
+
dependencies = [
6535
+
"async-trait",
6536
+
"aws-config",
6537
+
"aws-sdk-s3",
6538
+
"bytes",
6539
+
"futures",
6540
+
"sha2",
6541
+
"thiserror 2.0.17",
6542
+
"tracing",
6543
+
"tranquil-infra",
6544
+
]
6545
+
6546
+
[[package]]
6547
+
name = "tranquil-types"
6548
+
version = "0.1.0"
6549
+
dependencies = [
6550
+
"chrono",
6551
+
"cid",
6552
+
"jacquard",
6553
+
"serde",
6554
+
"serde_json",
6555
+
"sqlx",
6556
+
"thiserror 2.0.17",
6389
6557
]
6390
6558
6391
6559
[[package]]
+89
-65
Cargo.toml
+89
-65
Cargo.toml
···
1
-
[package]
2
-
name = "tranquil-pds"
1
+
[workspace]
2
+
resolver = "2"
3
+
members = [
4
+
"crates/tranquil-types",
5
+
"crates/tranquil-infra",
6
+
"crates/tranquil-crypto",
7
+
"crates/tranquil-storage",
8
+
"crates/tranquil-cache",
9
+
"crates/tranquil-repo",
10
+
"crates/tranquil-scopes",
11
+
"crates/tranquil-auth",
12
+
"crates/tranquil-oauth",
13
+
"crates/tranquil-comms",
14
+
"crates/tranquil-pds",
15
+
]
16
+
17
+
[workspace.package]
3
18
version = "0.1.0"
4
19
edition = "2024"
5
20
license = "AGPL-3.0-or-later"
6
-
[dependencies]
7
-
anyhow = "1.0.100"
8
-
async-trait = "0.1.89"
9
-
aws-config = "1.8.12"
10
-
aws-sdk-s3 = "1.118.0"
11
-
axum = { version = "0.8.8", features = ["ws", "macros"] }
21
+
22
+
[workspace.dependencies]
23
+
tranquil-types = { path = "crates/tranquil-types" }
24
+
tranquil-infra = { path = "crates/tranquil-infra" }
25
+
tranquil-crypto = { path = "crates/tranquil-crypto" }
26
+
tranquil-storage = { path = "crates/tranquil-storage" }
27
+
tranquil-cache = { path = "crates/tranquil-cache" }
28
+
tranquil-repo = { path = "crates/tranquil-repo" }
29
+
tranquil-scopes = { path = "crates/tranquil-scopes" }
30
+
tranquil-auth = { path = "crates/tranquil-auth" }
31
+
tranquil-oauth = { path = "crates/tranquil-oauth" }
32
+
tranquil-comms = { path = "crates/tranquil-comms" }
33
+
34
+
aes-gcm = "0.10"
35
+
anyhow = "1.0"
36
+
async-trait = "0.1"
37
+
aws-config = "1.8"
38
+
aws-sdk-s3 = "1.118"
39
+
axum = { version = "0.8", features = ["ws", "macros"] }
12
40
base32 = "0.5"
13
-
base64 = "0.22.1"
14
-
bcrypt = "0.17.1"
15
-
bytes = "1.11.0"
16
-
chrono = { version = "0.4.42", features = ["serde"] }
17
-
cid = "0.11.1"
18
-
dotenvy = "0.15.7"
19
-
futures = "0.3.30"
41
+
base64 = "0.22"
42
+
bcrypt = "0.17"
43
+
bs58 = "0.5"
44
+
bytes = "1.11"
45
+
chrono = { version = "0.4", features = ["serde"] }
46
+
cid = "0.11"
47
+
dotenvy = "0.15"
48
+
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
49
+
futures = "0.3"
50
+
futures-util = "0.3"
20
51
governor = "0.10"
21
52
hex = "0.4"
53
+
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
22
54
hkdf = "0.12"
23
55
hmac = "0.12"
56
+
http = "1.4"
57
+
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
24
58
infer = "0.19"
25
-
aes-gcm = "0.10"
26
-
jacquard = { version = "0.9.5", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] }
27
-
jacquard-axum = "0.9.6"
28
-
jacquard-repo = "0.9.6"
29
-
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
30
-
k256 = { version = "0.13.3", features = ["ecdsa", "pem", "pkcs8"] }
31
-
multibase = "0.9.1"
32
-
multihash = "0.19.3"
33
-
rand = "0.8.5"
59
+
ipld-core = "0.4"
60
+
iroh-car = "0.5"
61
+
jacquard = { version = "0.9", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] }
62
+
jacquard-axum = "0.9"
63
+
jacquard-repo = "0.9"
64
+
jsonwebtoken = { version = "10.2", features = ["rust_crypto"] }
65
+
k256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
66
+
metrics = "0.24"
67
+
metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] }
68
+
multibase = "0.9"
69
+
multihash = "0.19"
70
+
p256 = { version = "0.13", features = ["ecdsa"] }
71
+
p384 = { version = "0.13", features = ["ecdsa"] }
72
+
rand = "0.8"
73
+
redis = { version = "1.0", features = ["tokio-comp", "connection-manager"] }
34
74
regex = "1"
35
-
reqwest = { version = "0.12.28", features = ["json"] }
36
-
serde = { version = "1.0.228", features = ["derive"] }
37
-
serde_bytes = "0.11.14"
38
-
serde_ipld_dagcbor = "0.6.4"
39
-
ipld-core = "0.4.2"
40
-
serde_json = "1.0.146"
75
+
reqwest = { version = "0.12", features = ["json"] }
76
+
serde = { version = "1.0", features = ["derive"] }
77
+
serde_bytes = "0.11"
78
+
serde_ipld_dagcbor = "0.6"
79
+
serde_json = "1.0"
41
80
serde_urlencoded = "0.7"
42
-
sha2 = "0.10.9"
81
+
sha2 = "0.10"
82
+
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
43
83
subtle = "2.5"
44
-
p256 = { version = "0.13", features = ["ecdsa"] }
45
-
p384 = { version = "0.13", features = ["ecdsa"] }
46
-
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
47
-
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
48
-
thiserror = "2.0.17"
49
-
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time", "signal", "process"] }
50
-
tracing = "0.1.43"
51
-
tracing-subscriber = "0.3.22"
52
-
tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] }
84
+
thiserror = "2.0"
85
+
tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "time", "signal", "process"] }
86
+
tokio-tungstenite = { version = "0.28", features = ["native-tls"] }
87
+
totp-rs = { version = "5", features = ["qr"] }
88
+
tower = "0.5"
89
+
tower-http = { version = "0.6", features = ["fs", "cors"] }
90
+
tower-layer = "0.3"
91
+
tracing = "0.1"
92
+
tracing-subscriber = "0.3"
53
93
urlencoding = "2.1"
54
-
uuid = { version = "1.19.0", features = ["v4", "v5", "fast-rng"] }
55
-
iroh-car = "0.5.1"
56
-
image = { version = "0.25.9", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
57
-
redis = { version = "1.0.1", features = ["tokio-comp", "connection-manager"] }
58
-
tower-http = { version = "0.6.8", features = ["fs", "cors"] }
59
-
hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
60
-
metrics = "0.24.3"
61
-
metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] }
62
-
bs58 = "0.5.1"
63
-
totp-rs = { version = "5", features = ["qr"] }
64
-
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
65
-
webauthn-rs-proto = "0.5.4"
66
-
zip = { version = "7.0.0", default-features = false, features = ["deflate"] }
67
-
tower = "0.5.2"
68
-
tower-layer = "0.3.3"
69
-
futures-util = "0.3.31"
70
-
http = "1.4.0"
71
-
[features]
72
-
external-infra = []
73
-
[dev-dependencies]
94
+
uuid = { version = "1.19", features = ["v4", "v5", "fast-rng"] }
95
+
webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] }
96
+
webauthn-rs-proto = "0.5"
97
+
zip = { version = "7.0", default-features = false, features = ["deflate"] }
98
+
74
99
ciborium = "0.2"
75
-
ctor = "0.6.3"
76
-
testcontainers = "0.26.2"
77
-
testcontainers-modules = { version = "0.14.0", features = ["postgres"] }
78
-
wiremock = "0.6.5"
79
-
# urlencoding is also in dependencies, but tests use it directly
100
+
ctor = "0.6"
101
+
testcontainers = "0.26"
102
+
testcontainers-modules = { version = "0.14", features = ["postgres"] }
103
+
wiremock = "0.6"
+25
crates/tranquil-auth/Cargo.toml
+25
crates/tranquil-auth/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-auth"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-crypto = { workspace = true }
9
+
10
+
anyhow = { workspace = true }
11
+
base32 = { workspace = true }
12
+
base64 = { workspace = true }
13
+
bcrypt = { workspace = true }
14
+
chrono = { workspace = true }
15
+
hmac = { workspace = true }
16
+
k256 = { workspace = true }
17
+
rand = { workspace = true }
18
+
serde = { workspace = true }
19
+
serde_json = { workspace = true }
20
+
sha2 = { workspace = true }
21
+
subtle = { workspace = true }
22
+
thiserror = { workspace = true }
23
+
totp-rs = { workspace = true }
24
+
urlencoding = { workspace = true }
25
+
uuid = { workspace = true }
+29
crates/tranquil-auth/src/lib.rs
+29
crates/tranquil-auth/src/lib.rs
···
1
+
mod token;
2
+
mod totp;
3
+
mod types;
4
+
mod verify;
5
+
6
+
pub use token::{
7
+
SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS,
8
+
TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token, create_access_token_hs256,
9
+
create_access_token_hs256_with_metadata, create_access_token_with_delegation,
10
+
create_access_token_with_metadata, create_access_token_with_scope_metadata,
11
+
create_refresh_token, create_refresh_token_hs256, create_refresh_token_hs256_with_metadata,
12
+
create_refresh_token_with_metadata, create_service_token, create_service_token_hs256,
13
+
};
14
+
15
+
pub use totp::{
16
+
decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64,
17
+
generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format,
18
+
verify_backup_code, verify_totp_code,
19
+
};
20
+
21
+
pub use types::{
22
+
ActClaim, Claims, Header, TokenData, TokenVerifyError, TokenWithMetadata, UnsafeClaims,
23
+
};
24
+
25
+
pub use verify::{
26
+
get_algorithm_from_token, get_did_from_token, get_jti_from_token, verify_access_token,
27
+
verify_access_token_hs256, verify_access_token_typed, verify_refresh_token,
28
+
verify_refresh_token_hs256, verify_token,
29
+
};
+63
crates/tranquil-auth/src/types.rs
+63
crates/tranquil-auth/src/types.rs
···
1
+
use chrono::{DateTime, Utc};
2
+
use serde::{Deserialize, Serialize};
3
+
use std::fmt;
4
+
5
+
#[derive(Debug, Clone, Serialize, Deserialize)]
6
+
pub struct ActClaim {
7
+
pub sub: String,
8
+
}
9
+
10
+
#[derive(Debug, Serialize, Deserialize)]
11
+
pub struct Claims {
12
+
pub iss: String,
13
+
pub sub: String,
14
+
pub aud: String,
15
+
pub exp: usize,
16
+
pub iat: usize,
17
+
#[serde(skip_serializing_if = "Option::is_none")]
18
+
pub scope: Option<String>,
19
+
#[serde(skip_serializing_if = "Option::is_none")]
20
+
pub lxm: Option<String>,
21
+
pub jti: String,
22
+
#[serde(skip_serializing_if = "Option::is_none")]
23
+
pub act: Option<ActClaim>,
24
+
}
25
+
26
+
#[derive(Debug, Serialize, Deserialize)]
27
+
pub struct Header {
28
+
pub alg: String,
29
+
pub typ: String,
30
+
}
31
+
32
+
#[derive(Debug, Serialize, Deserialize)]
33
+
pub struct UnsafeClaims {
34
+
pub iss: String,
35
+
pub sub: Option<String>,
36
+
}
37
+
38
+
pub struct TokenData<T> {
39
+
pub claims: T,
40
+
}
41
+
42
+
pub struct TokenWithMetadata {
43
+
pub token: String,
44
+
pub jti: String,
45
+
pub expires_at: DateTime<Utc>,
46
+
}
47
+
48
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49
+
pub enum TokenVerifyError {
50
+
Expired,
51
+
Invalid,
52
+
}
53
+
54
+
impl fmt::Display for TokenVerifyError {
55
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56
+
match self {
57
+
Self::Expired => write!(f, "Token expired"),
58
+
Self::Invalid => write!(f, "Token invalid"),
59
+
}
60
+
}
61
+
}
62
+
63
+
impl std::error::Error for TokenVerifyError {}
+14
crates/tranquil-cache/Cargo.toml
+14
crates/tranquil-cache/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-cache"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-infra = { workspace = true }
9
+
10
+
async-trait = { workspace = true }
11
+
base64 = { workspace = true }
12
+
redis = { workspace = true }
13
+
thiserror = { workspace = true }
14
+
tracing = { workspace = true }
+18
crates/tranquil-comms/Cargo.toml
+18
crates/tranquil-comms/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-comms"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
async-trait = { workspace = true }
9
+
base64 = { workspace = true }
10
+
chrono = { workspace = true }
11
+
reqwest = { workspace = true }
12
+
serde = { workspace = true }
13
+
serde_json = { workspace = true }
14
+
sqlx = { workspace = true }
15
+
thiserror = { workspace = true }
16
+
tokio = { workspace = true }
17
+
urlencoding = { workspace = true }
18
+
uuid = { workspace = true }
+13
crates/tranquil-comms/src/lib.rs
+13
crates/tranquil-comms/src/lib.rs
···
1
+
mod locale;
2
+
mod sender;
3
+
mod types;
4
+
5
+
pub use locale::{
6
+
DEFAULT_LOCALE, NotificationStrings, VALID_LOCALES, format_message, get_strings,
7
+
validate_locale,
8
+
};
9
+
pub use sender::{
10
+
CommsSender, DiscordSender, EmailSender, SendError, SignalSender, TelegramSender,
11
+
is_valid_phone_number, mime_encode_header, sanitize_header_value,
12
+
};
13
+
pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
+18
crates/tranquil-crypto/Cargo.toml
+18
crates/tranquil-crypto/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-crypto"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
aes-gcm = { workspace = true }
9
+
base64 = { workspace = true }
10
+
hkdf = { workspace = true }
11
+
hmac = { workspace = true }
12
+
p256 = { workspace = true }
13
+
rand = { workspace = true }
14
+
serde = { workspace = true }
15
+
serde_json = { workspace = true }
16
+
sha2 = { workspace = true }
17
+
subtle = { workspace = true }
18
+
thiserror = { workspace = true }
+78
crates/tranquil-crypto/src/encryption.rs
+78
crates/tranquil-crypto/src/encryption.rs
···
1
+
#[allow(deprecated)]
2
+
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
3
+
use hkdf::Hkdf;
4
+
use sha2::Sha256;
5
+
6
+
use crate::CryptoError;
7
+
8
+
pub fn derive_key(master_key: &[u8], context: &[u8]) -> Result<[u8; 32], CryptoError> {
9
+
let hk = Hkdf::<Sha256>::new(None, master_key);
10
+
let mut output = [0u8; 32];
11
+
hk.expand(context, &mut output)
12
+
.map_err(|e| CryptoError::KeyDerivationFailed(format!("{}", e)))?;
13
+
Ok(output)
14
+
}
15
+
16
+
pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
17
+
use rand::RngCore;
18
+
19
+
let cipher = Aes256Gcm::new_from_slice(key)
20
+
.map_err(|e| CryptoError::EncryptionFailed(format!("Failed to create cipher: {}", e)))?;
21
+
22
+
let mut nonce_bytes = [0u8; 12];
23
+
rand::thread_rng().fill_bytes(&mut nonce_bytes);
24
+
25
+
#[allow(deprecated)]
26
+
let nonce = Nonce::from_slice(&nonce_bytes);
27
+
28
+
let ciphertext = cipher
29
+
.encrypt(nonce, plaintext)
30
+
.map_err(|e| CryptoError::EncryptionFailed(format!("{}", e)))?;
31
+
32
+
let mut result = Vec::with_capacity(12 + ciphertext.len());
33
+
result.extend_from_slice(&nonce_bytes);
34
+
result.extend_from_slice(&ciphertext);
35
+
36
+
Ok(result)
37
+
}
38
+
39
+
pub fn decrypt_with_key(key: &[u8; 32], encrypted: &[u8]) -> Result<Vec<u8>, CryptoError> {
40
+
if encrypted.len() < 12 {
41
+
return Err(CryptoError::DecryptionFailed(
42
+
"Encrypted data too short".to_string(),
43
+
));
44
+
}
45
+
46
+
let cipher = Aes256Gcm::new_from_slice(key)
47
+
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to create cipher: {}", e)))?;
48
+
49
+
#[allow(deprecated)]
50
+
let nonce = Nonce::from_slice(&encrypted[..12]);
51
+
let ciphertext = &encrypted[12..];
52
+
53
+
cipher
54
+
.decrypt(nonce, ciphertext)
55
+
.map_err(|e| CryptoError::DecryptionFailed(format!("{}", e)))
56
+
}
57
+
58
+
#[cfg(test)]
59
+
mod tests {
60
+
use super::*;
61
+
62
+
#[test]
63
+
fn test_encrypt_decrypt() {
64
+
let key = [0u8; 32];
65
+
let plaintext = b"hello world";
66
+
let encrypted = encrypt_with_key(&key, plaintext).unwrap();
67
+
let decrypted = decrypt_with_key(&key, &encrypted).unwrap();
68
+
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
69
+
}
70
+
71
+
#[test]
72
+
fn test_derive_key() {
73
+
let master = b"master-key-for-testing";
74
+
let key1 = derive_key(master, b"context-1").unwrap();
75
+
let key2 = derive_key(master, b"context-2").unwrap();
76
+
assert_ne!(key1, key2);
77
+
}
78
+
}
+19
crates/tranquil-crypto/src/lib.rs
+19
crates/tranquil-crypto/src/lib.rs
···
1
+
mod encryption;
2
+
mod jwk;
3
+
mod signing;
4
+
5
+
pub use encryption::{decrypt_with_key, derive_key, encrypt_with_key};
6
+
pub use jwk::{Jwk, JwkSet, create_jwk_set};
7
+
pub use signing::{DeviceCookieSigner, SigningKeyPair};
8
+
9
+
#[derive(Debug, Clone, thiserror::Error)]
10
+
pub enum CryptoError {
11
+
#[error("Encryption failed: {0}")]
12
+
EncryptionFailed(String),
13
+
#[error("Decryption failed: {0}")]
14
+
DecryptionFailed(String),
15
+
#[error("Invalid key: {0}")]
16
+
InvalidKey(String),
17
+
#[error("Key derivation failed: {0}")]
18
+
KeyDerivationFailed(String),
19
+
}
+150
crates/tranquil-crypto/src/signing.rs
+150
crates/tranquil-crypto/src/signing.rs
···
1
+
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
2
+
use hmac::Mac;
3
+
use p256::ecdsa::SigningKey;
4
+
use sha2::{Digest, Sha256};
5
+
use subtle::ConstantTimeEq;
6
+
7
+
use crate::CryptoError;
8
+
9
+
type HmacSha256 = hmac::Hmac<Sha256>;
10
+
11
+
pub struct SigningKeyPair {
12
+
#[allow(dead_code)]
13
+
signing_key: SigningKey,
14
+
pub key_id: String,
15
+
pub x: String,
16
+
pub y: String,
17
+
}
18
+
19
+
impl SigningKeyPair {
20
+
pub fn from_seed(seed: &[u8]) -> Result<Self, CryptoError> {
21
+
let mut hasher = Sha256::new();
22
+
hasher.update(b"oauth-signing-key-derivation:");
23
+
hasher.update(seed);
24
+
let hash = hasher.finalize();
25
+
26
+
let signing_key = SigningKey::from_slice(&hash)
27
+
.map_err(|e| CryptoError::InvalidKey(format!("Failed to create signing key: {}", e)))?;
28
+
29
+
let verifying_key = signing_key.verifying_key();
30
+
let point = verifying_key.to_encoded_point(false);
31
+
32
+
let x = URL_SAFE_NO_PAD.encode(
33
+
point
34
+
.x()
35
+
.ok_or_else(|| CryptoError::InvalidKey("Missing X coordinate".to_string()))?,
36
+
);
37
+
let y = URL_SAFE_NO_PAD.encode(
38
+
point
39
+
.y()
40
+
.ok_or_else(|| CryptoError::InvalidKey("Missing Y coordinate".to_string()))?,
41
+
);
42
+
43
+
let mut kid_hasher = Sha256::new();
44
+
kid_hasher.update(x.as_bytes());
45
+
kid_hasher.update(y.as_bytes());
46
+
let kid_hash = kid_hasher.finalize();
47
+
let key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
48
+
49
+
Ok(Self {
50
+
signing_key,
51
+
key_id,
52
+
x,
53
+
y,
54
+
})
55
+
}
56
+
}
57
+
58
+
pub struct DeviceCookieSigner {
59
+
key: [u8; 32],
60
+
}
61
+
62
+
impl DeviceCookieSigner {
63
+
pub fn new(key: [u8; 32]) -> Self {
64
+
Self { key }
65
+
}
66
+
67
+
pub fn sign(&self, device_id: &str) -> String {
68
+
let timestamp = std::time::SystemTime::now()
69
+
.duration_since(std::time::UNIX_EPOCH)
70
+
.unwrap_or_default()
71
+
.as_secs();
72
+
73
+
let message = format!("{}:{}", device_id, timestamp);
74
+
let mut mac =
75
+
<HmacSha256 as Mac>::new_from_slice(&self.key).expect("HMAC key size is valid");
76
+
mac.update(message.as_bytes());
77
+
let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
78
+
79
+
format!("{}.{}.{}", device_id, timestamp, signature)
80
+
}
81
+
82
+
pub fn verify(&self, cookie_value: &str, max_age_days: u64) -> Option<String> {
83
+
let parts: Vec<&str> = cookie_value.splitn(3, '.').collect();
84
+
if parts.len() != 3 {
85
+
return None;
86
+
}
87
+
88
+
let device_id = parts[0];
89
+
let timestamp_str = parts[1];
90
+
let provided_signature = parts[2];
91
+
92
+
let timestamp: u64 = timestamp_str.parse().ok()?;
93
+
94
+
let now = std::time::SystemTime::now()
95
+
.duration_since(std::time::UNIX_EPOCH)
96
+
.unwrap_or_default()
97
+
.as_secs();
98
+
99
+
if now.saturating_sub(timestamp) > max_age_days * 24 * 60 * 60 {
100
+
return None;
101
+
}
102
+
103
+
let message = format!("{}:{}", device_id, timestamp);
104
+
let mut mac =
105
+
<HmacSha256 as Mac>::new_from_slice(&self.key).expect("HMAC key size is valid");
106
+
mac.update(message.as_bytes());
107
+
let expected_signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
108
+
109
+
if provided_signature
110
+
.as_bytes()
111
+
.ct_eq(expected_signature.as_bytes())
112
+
.into()
113
+
{
114
+
Some(device_id.to_string())
115
+
} else {
116
+
None
117
+
}
118
+
}
119
+
}
120
+
121
+
#[cfg(test)]
122
+
mod tests {
123
+
use super::*;
124
+
125
+
#[test]
126
+
fn test_signing_key_pair() {
127
+
let seed = b"test-seed-for-signing-key";
128
+
let kp = SigningKeyPair::from_seed(seed).unwrap();
129
+
assert!(!kp.key_id.is_empty());
130
+
assert!(!kp.x.is_empty());
131
+
assert!(!kp.y.is_empty());
132
+
}
133
+
134
+
#[test]
135
+
fn test_device_cookie_signer() {
136
+
let key = [0u8; 32];
137
+
let signer = DeviceCookieSigner::new(key);
138
+
let signed = signer.sign("device-123");
139
+
let verified = signer.verify(&signed, 400);
140
+
assert_eq!(verified, Some("device-123".to_string()));
141
+
}
142
+
143
+
#[test]
144
+
fn test_device_cookie_invalid() {
145
+
let key = [0u8; 32];
146
+
let signer = DeviceCookieSigner::new(key);
147
+
assert!(signer.verify("invalid", 400).is_none());
148
+
assert!(signer.verify("a.b.c", 400).is_none());
149
+
}
150
+
}
+13
crates/tranquil-infra/Cargo.toml
+13
crates/tranquil-infra/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-infra"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
async-trait = { workspace = true }
9
+
bytes = { workspace = true }
10
+
futures = { workspace = true }
11
+
thiserror = { workspace = true }
12
+
tokio = { workspace = true }
13
+
tracing = { workspace = true }
+58
crates/tranquil-infra/src/lib.rs
+58
crates/tranquil-infra/src/lib.rs
···
1
+
use async_trait::async_trait;
2
+
use bytes::Bytes;
3
+
use futures::Stream;
4
+
use std::pin::Pin;
5
+
use std::time::Duration;
6
+
7
+
#[derive(Debug, thiserror::Error)]
8
+
pub enum StorageError {
9
+
#[error("IO error: {0}")]
10
+
Io(#[from] std::io::Error),
11
+
#[error("S3 error: {0}")]
12
+
S3(String),
13
+
#[error("Other: {0}")]
14
+
Other(String),
15
+
}
16
+
17
+
pub struct StreamUploadResult {
18
+
pub sha256_hash: [u8; 32],
19
+
pub size: u64,
20
+
}
21
+
22
+
#[async_trait]
23
+
pub trait BlobStorage: Send + Sync {
24
+
async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError>;
25
+
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError>;
26
+
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;
27
+
async fn get_bytes(&self, key: &str) -> Result<Bytes, StorageError>;
28
+
async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError>;
29
+
async fn delete(&self, key: &str) -> Result<(), StorageError>;
30
+
async fn put_stream(
31
+
&self,
32
+
key: &str,
33
+
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
34
+
) -> Result<StreamUploadResult, StorageError>;
35
+
async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError>;
36
+
}
37
+
38
+
#[derive(Debug, thiserror::Error)]
39
+
pub enum CacheError {
40
+
#[error("Cache connection error: {0}")]
41
+
Connection(String),
42
+
#[error("Serialization error: {0}")]
43
+
Serialization(String),
44
+
}
45
+
46
+
#[async_trait]
47
+
pub trait Cache: Send + Sync {
48
+
async fn get(&self, key: &str) -> Option<String>;
49
+
async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError>;
50
+
async fn delete(&self, key: &str) -> Result<(), CacheError>;
51
+
async fn get_bytes(&self, key: &str) -> Option<Vec<u8>>;
52
+
async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError>;
53
+
}
54
+
55
+
#[async_trait]
56
+
pub trait DistributedRateLimiter: Send + Sync {
57
+
async fn check_rate_limit(&self, key: &str, limit: u32, window_ms: u64) -> bool;
58
+
}
+25
crates/tranquil-oauth/Cargo.toml
+25
crates/tranquil-oauth/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-oauth"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-types = { workspace = true }
9
+
10
+
anyhow = { workspace = true }
11
+
sqlx = { workspace = true }
12
+
axum = { workspace = true }
13
+
base64 = { workspace = true }
14
+
chrono = { workspace = true }
15
+
ed25519-dalek = { workspace = true }
16
+
p256 = { workspace = true }
17
+
p384 = { workspace = true }
18
+
rand = { workspace = true }
19
+
reqwest = { workspace = true }
20
+
serde = { workspace = true }
21
+
serde_json = { workspace = true }
22
+
sha2 = { workspace = true }
23
+
tokio = { workspace = true }
24
+
tracing = { workspace = true }
25
+
uuid = { workspace = true }
+17
crates/tranquil-oauth/src/lib.rs
+17
crates/tranquil-oauth/src/lib.rs
···
1
+
mod client;
2
+
mod dpop;
3
+
mod error;
4
+
mod types;
5
+
6
+
pub use client::{ClientMetadata, ClientMetadataCache, verify_client_auth};
7
+
pub use dpop::{
8
+
DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult,
9
+
compute_access_token_hash, compute_jwk_thumbprint,
10
+
};
11
+
pub use error::OAuthError;
12
+
pub use types::{
13
+
AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata,
14
+
AuthorizedClientData, ClientAuth, Code, DPoPClaims, DeviceData, DeviceId, JwkPublicKey, Jwks,
15
+
OAuthClientMetadata, ParResponse, ProtectedResourceMetadata, RefreshToken, RefreshTokenState,
16
+
RequestData, RequestId, SessionId, TokenData, TokenId, TokenRequest, TokenResponse,
17
+
};
+1
crates/tranquil-pds/.sqlx
+1
crates/tranquil-pds/.sqlx
···
1
+
../../.sqlx
+92
crates/tranquil-pds/Cargo.toml
+92
crates/tranquil-pds/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-pds"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-types = { workspace = true }
9
+
tranquil-infra = { workspace = true }
10
+
tranquil-crypto = { workspace = true }
11
+
tranquil-storage = { workspace = true }
12
+
tranquil-cache = { workspace = true }
13
+
tranquil-repo = { workspace = true }
14
+
tranquil-scopes = { workspace = true }
15
+
tranquil-auth = { workspace = true }
16
+
tranquil-oauth = { workspace = true }
17
+
tranquil-comms = { workspace = true }
18
+
19
+
aes-gcm = { workspace = true }
20
+
anyhow = { workspace = true }
21
+
async-trait = { workspace = true }
22
+
aws-config = { workspace = true }
23
+
aws-sdk-s3 = { workspace = true }
24
+
axum = { workspace = true }
25
+
base32 = { workspace = true }
26
+
base64 = { workspace = true }
27
+
bcrypt = { workspace = true }
28
+
bs58 = { workspace = true }
29
+
bytes = { workspace = true }
30
+
chrono = { workspace = true }
31
+
cid = { workspace = true }
32
+
dotenvy = { workspace = true }
33
+
ed25519-dalek = { workspace = true }
34
+
futures = { workspace = true }
35
+
futures-util = { workspace = true }
36
+
governor = { workspace = true }
37
+
hex = { workspace = true }
38
+
hickory-resolver = { workspace = true }
39
+
hkdf = { workspace = true }
40
+
hmac = { workspace = true }
41
+
http = { workspace = true }
42
+
image = { workspace = true }
43
+
infer = { workspace = true }
44
+
ipld-core = { workspace = true }
45
+
iroh-car = { workspace = true }
46
+
jacquard = { workspace = true }
47
+
jacquard-axum = { workspace = true }
48
+
jacquard-repo = { workspace = true }
49
+
jsonwebtoken = { workspace = true }
50
+
k256 = { workspace = true }
51
+
metrics = { workspace = true }
52
+
metrics-exporter-prometheus = { workspace = true }
53
+
multibase = { workspace = true }
54
+
multihash = { workspace = true }
55
+
p256 = { workspace = true }
56
+
p384 = { workspace = true }
57
+
rand = { workspace = true }
58
+
redis = { workspace = true }
59
+
regex = { workspace = true }
60
+
reqwest = { workspace = true }
61
+
serde = { workspace = true }
62
+
serde_bytes = { workspace = true }
63
+
serde_ipld_dagcbor = { workspace = true }
64
+
serde_json = { workspace = true }
65
+
serde_urlencoded = { workspace = true }
66
+
sha2 = { workspace = true }
67
+
sqlx = { workspace = true }
68
+
subtle = { workspace = true }
69
+
thiserror = { workspace = true }
70
+
tokio = { workspace = true }
71
+
tokio-tungstenite = { workspace = true }
72
+
totp-rs = { workspace = true }
73
+
tower = { workspace = true }
74
+
tower-http = { workspace = true }
75
+
tower-layer = { workspace = true }
76
+
tracing = { workspace = true }
77
+
tracing-subscriber = { workspace = true }
78
+
urlencoding = { workspace = true }
79
+
uuid = { workspace = true }
80
+
webauthn-rs = { workspace = true }
81
+
webauthn-rs-proto = { workspace = true }
82
+
zip = { workspace = true }
83
+
84
+
[features]
85
+
external-infra = []
86
+
87
+
[dev-dependencies]
88
+
ciborium = { workspace = true }
89
+
ctor = { workspace = true }
90
+
testcontainers = { workspace = true }
91
+
testcontainers-modules = { workspace = true }
92
+
wiremock = { workspace = true }
+1
crates/tranquil-pds/migrations
+1
crates/tranquil-pds/migrations
···
1
+
../../migrations
+4
crates/tranquil-pds/src/cache/mod.rs
+4
crates/tranquil-pds/src/cache/mod.rs
+15
crates/tranquil-pds/src/comms/mod.rs
+15
crates/tranquil-pds/src/comms/mod.rs
···
1
+
mod service;
2
+
3
+
pub use tranquil_comms::{
4
+
CommsChannel, CommsSender, CommsStatus, CommsType, DEFAULT_LOCALE, DiscordSender, EmailSender,
5
+
NewComms, NotificationStrings, QueuedComms, SendError, SignalSender, TelegramSender,
6
+
VALID_LOCALES, format_message, get_strings, is_valid_phone_number, mime_encode_header,
7
+
sanitize_header_value, validate_locale,
8
+
};
9
+
10
+
pub use service::{
11
+
CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion, enqueue_comms,
12
+
enqueue_email_update, enqueue_email_update_token, enqueue_migration_verification,
13
+
enqueue_passkey_recovery, enqueue_password_reset, enqueue_plc_operation,
14
+
enqueue_signup_verification, enqueue_welcome, queue_legacy_login_notification,
15
+
};
+1
crates/tranquil-pds/src/oauth/jwks.rs
+1
crates/tranquil-pds/src/oauth/jwks.rs
···
1
+
pub use tranquil_crypto::{Jwk, JwkSet, create_jwk_set};
+20
crates/tranquil-pds/src/oauth/mod.rs
+20
crates/tranquil-pds/src/oauth/mod.rs
···
1
+
pub mod db;
2
+
pub mod endpoints;
3
+
pub mod jwks;
4
+
pub mod scopes;
5
+
pub mod verify;
6
+
7
+
pub use tranquil_oauth::{
8
+
AuthFlowState, AuthorizationRequestParameters, AuthorizationServerMetadata,
9
+
AuthorizedClientData, ClientAuth, ClientMetadata, ClientMetadataCache, Code, DPoPClaims,
10
+
DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, DPoPVerifyResult, DeviceData,
11
+
DeviceId, JwkPublicKey, Jwks, OAuthClientMetadata, OAuthError, ParResponse,
12
+
ProtectedResourceMetadata, RefreshToken, RefreshTokenState, RequestData, RequestId, SessionId,
13
+
TokenData, TokenId, TokenRequest, TokenResponse, compute_access_token_hash,
14
+
compute_jwk_thumbprint, verify_client_auth,
15
+
};
16
+
17
+
pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions};
18
+
pub use verify::{
19
+
OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token,
20
+
};
+6
crates/tranquil-pds/src/oauth/scopes/mod.rs
+6
crates/tranquil-pds/src/oauth/scopes/mod.rs
···
1
+
pub use tranquil_scopes::{
2
+
AccountAction, AccountAttr, AccountScope, BlobScope, IdentityAttr, IdentityScope, IncludeScope,
3
+
ParsedScope, RepoAction, RepoScope, RpcScope, SCOPE_DEFINITIONS, ScopeCategory,
4
+
ScopeDefinition, ScopeError, ScopePermissions, expand_include_scopes, format_scope_for_display,
5
+
get_required_scopes, get_scope_definition, is_valid_scope, parse_scope, parse_scope_string,
6
+
};
+5
crates/tranquil-pds/src/repo/mod.rs
+5
crates/tranquil-pds/src/repo/mod.rs
+3
crates/tranquil-pds/src/storage/mod.rs
+3
crates/tranquil-pds/src/storage/mod.rs
+1
crates/tranquil-pds/src/types.rs
+1
crates/tranquil-pds/src/types.rs
···
1
+
pub use tranquil_types::*;
+1
crates/tranquil-repo/.sqlx
+1
crates/tranquil-repo/.sqlx
···
1
+
../../.sqlx
+15
crates/tranquil-repo/Cargo.toml
+15
crates/tranquil-repo/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-repo"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-types = { workspace = true }
9
+
10
+
bytes = { workspace = true }
11
+
cid = { workspace = true }
12
+
jacquard-repo = { workspace = true }
13
+
multihash = { workspace = true }
14
+
sha2 = { workspace = true }
15
+
sqlx = { workspace = true }
+228
crates/tranquil-repo/src/lib.rs
+228
crates/tranquil-repo/src/lib.rs
···
1
+
use bytes::Bytes;
2
+
use cid::Cid;
3
+
use jacquard_repo::error::RepoError;
4
+
use jacquard_repo::repo::CommitData;
5
+
use jacquard_repo::storage::BlockStore;
6
+
use multihash::Multihash;
7
+
use sha2::{Digest, Sha256};
8
+
use sqlx::PgPool;
9
+
use std::collections::HashSet;
10
+
use std::sync::{Arc, Mutex};
11
+
12
+
#[derive(Clone)]
13
+
pub struct PostgresBlockStore {
14
+
pool: PgPool,
15
+
}
16
+
17
+
impl PostgresBlockStore {
18
+
pub fn new(pool: PgPool) -> Self {
19
+
Self { pool }
20
+
}
21
+
22
+
pub fn pool(&self) -> &PgPool {
23
+
&self.pool
24
+
}
25
+
}
26
+
27
+
impl BlockStore for PostgresBlockStore {
28
+
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
29
+
let cid_bytes = cid.to_bytes();
30
+
let row = sqlx::query!("SELECT data FROM blocks WHERE cid = $1", &cid_bytes)
31
+
.fetch_optional(&self.pool)
32
+
.await
33
+
.map_err(RepoError::storage)?;
34
+
match row {
35
+
Some(row) => Ok(Some(Bytes::from(row.data))),
36
+
None => Ok(None),
37
+
}
38
+
}
39
+
40
+
async fn put(&self, data: &[u8]) -> Result<Cid, RepoError> {
41
+
let mut hasher = Sha256::new();
42
+
hasher.update(data);
43
+
let hash = hasher.finalize();
44
+
let multihash = Multihash::wrap(0x12, &hash).map_err(|e| {
45
+
RepoError::storage(std::io::Error::new(
46
+
std::io::ErrorKind::InvalidData,
47
+
format!("Failed to wrap multihash: {:?}", e),
48
+
))
49
+
})?;
50
+
let cid = Cid::new_v1(0x71, multihash);
51
+
let cid_bytes = cid.to_bytes();
52
+
sqlx::query!(
53
+
"INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING",
54
+
&cid_bytes,
55
+
data
56
+
)
57
+
.execute(&self.pool)
58
+
.await
59
+
.map_err(RepoError::storage)?;
60
+
Ok(cid)
61
+
}
62
+
63
+
async fn has(&self, cid: &Cid) -> Result<bool, RepoError> {
64
+
let cid_bytes = cid.to_bytes();
65
+
let row = sqlx::query!("SELECT 1 as one FROM blocks WHERE cid = $1", &cid_bytes)
66
+
.fetch_optional(&self.pool)
67
+
.await
68
+
.map_err(RepoError::storage)?;
69
+
Ok(row.is_some())
70
+
}
71
+
72
+
async fn put_many(
73
+
&self,
74
+
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
75
+
) -> Result<(), RepoError> {
76
+
let blocks: Vec<_> = blocks.into_iter().collect();
77
+
if blocks.is_empty() {
78
+
return Ok(());
79
+
}
80
+
let cids: Vec<Vec<u8>> = blocks.iter().map(|(cid, _)| cid.to_bytes()).collect();
81
+
let data: Vec<&[u8]> = blocks.iter().map(|(_, d)| d.as_ref()).collect();
82
+
sqlx::query!(
83
+
r#"
84
+
INSERT INTO blocks (cid, data)
85
+
SELECT * FROM UNNEST($1::bytea[], $2::bytea[])
86
+
ON CONFLICT (cid) DO NOTHING
87
+
"#,
88
+
&cids,
89
+
&data as &[&[u8]]
90
+
)
91
+
.execute(&self.pool)
92
+
.await
93
+
.map_err(RepoError::storage)?;
94
+
Ok(())
95
+
}
96
+
97
+
async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Bytes>>, RepoError> {
98
+
if cids.is_empty() {
99
+
return Ok(Vec::new());
100
+
}
101
+
let cid_bytes: Vec<Vec<u8>> = cids.iter().map(|c| c.to_bytes()).collect();
102
+
let rows = sqlx::query!(
103
+
"SELECT cid, data FROM blocks WHERE cid = ANY($1)",
104
+
&cid_bytes
105
+
)
106
+
.fetch_all(&self.pool)
107
+
.await
108
+
.map_err(RepoError::storage)?;
109
+
let found: std::collections::HashMap<Vec<u8>, Bytes> = rows
110
+
.into_iter()
111
+
.map(|row| (row.cid, Bytes::from(row.data)))
112
+
.collect();
113
+
let results = cid_bytes
114
+
.iter()
115
+
.map(|cid| found.get(cid).cloned())
116
+
.collect();
117
+
Ok(results)
118
+
}
119
+
120
+
async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> {
121
+
self.put_many(commit.blocks).await?;
122
+
Ok(())
123
+
}
124
+
}
125
+
126
+
#[derive(Clone)]
127
+
pub struct TrackingBlockStore {
128
+
inner: PostgresBlockStore,
129
+
written_cids: Arc<Mutex<Vec<Cid>>>,
130
+
read_cids: Arc<Mutex<HashSet<Cid>>>,
131
+
}
132
+
133
+
impl TrackingBlockStore {
134
+
pub fn new(store: PostgresBlockStore) -> Self {
135
+
Self {
136
+
inner: store,
137
+
written_cids: Arc::new(Mutex::new(Vec::new())),
138
+
read_cids: Arc::new(Mutex::new(HashSet::new())),
139
+
}
140
+
}
141
+
142
+
pub fn get_written_cids(&self) -> Vec<Cid> {
143
+
match self.written_cids.lock() {
144
+
Ok(guard) => guard.clone(),
145
+
Err(poisoned) => poisoned.into_inner().clone(),
146
+
}
147
+
}
148
+
149
+
pub fn get_read_cids(&self) -> Vec<Cid> {
150
+
match self.read_cids.lock() {
151
+
Ok(guard) => guard.iter().cloned().collect(),
152
+
Err(poisoned) => poisoned.into_inner().iter().cloned().collect(),
153
+
}
154
+
}
155
+
156
+
pub fn get_all_relevant_cids(&self) -> Vec<Cid> {
157
+
let written = self.get_written_cids();
158
+
let read = self.get_read_cids();
159
+
let mut all: HashSet<Cid> = written.into_iter().collect();
160
+
all.extend(read);
161
+
all.into_iter().collect()
162
+
}
163
+
}
164
+
165
+
impl BlockStore for TrackingBlockStore {
166
+
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
167
+
let result = self.inner.get(cid).await?;
168
+
if result.is_some() {
169
+
match self.read_cids.lock() {
170
+
Ok(mut guard) => {
171
+
guard.insert(*cid);
172
+
}
173
+
Err(poisoned) => {
174
+
poisoned.into_inner().insert(*cid);
175
+
}
176
+
}
177
+
}
178
+
Ok(result)
179
+
}
180
+
181
+
async fn put(&self, data: &[u8]) -> Result<Cid, RepoError> {
182
+
let cid = self.inner.put(data).await?;
183
+
match self.written_cids.lock() {
184
+
Ok(mut guard) => guard.push(cid),
185
+
Err(poisoned) => poisoned.into_inner().push(cid),
186
+
}
187
+
Ok(cid)
188
+
}
189
+
190
+
async fn has(&self, cid: &Cid) -> Result<bool, RepoError> {
191
+
self.inner.has(cid).await
192
+
}
193
+
194
+
async fn put_many(
195
+
&self,
196
+
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
197
+
) -> Result<(), RepoError> {
198
+
let blocks: Vec<_> = blocks.into_iter().collect();
199
+
let cids: Vec<Cid> = blocks.iter().map(|(cid, _)| *cid).collect();
200
+
self.inner.put_many(blocks).await?;
201
+
match self.written_cids.lock() {
202
+
Ok(mut guard) => guard.extend(cids),
203
+
Err(poisoned) => poisoned.into_inner().extend(cids),
204
+
}
205
+
Ok(())
206
+
}
207
+
208
+
async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Bytes>>, RepoError> {
209
+
let results = self.inner.get_many(cids).await?;
210
+
cids.iter()
211
+
.zip(results.iter())
212
+
.filter(|(_, result)| result.is_some())
213
+
.for_each(|(cid, _)| match self.read_cids.lock() {
214
+
Ok(mut guard) => {
215
+
guard.insert(*cid);
216
+
}
217
+
Err(poisoned) => {
218
+
poisoned.into_inner().insert(*cid);
219
+
}
220
+
});
221
+
Ok(results)
222
+
}
223
+
224
+
async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> {
225
+
self.put_many(commit.blocks).await?;
226
+
Ok(())
227
+
}
228
+
}
+14
crates/tranquil-scopes/Cargo.toml
+14
crates/tranquil-scopes/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-scopes"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
axum = { workspace = true }
9
+
futures = { workspace = true }
10
+
reqwest = { workspace = true }
11
+
serde = { workspace = true }
12
+
serde_json = { workspace = true }
13
+
tokio = { workspace = true }
14
+
tracing = { workspace = true }
+17
crates/tranquil-storage/Cargo.toml
+17
crates/tranquil-storage/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-storage"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
tranquil-infra = { workspace = true }
9
+
10
+
async-trait = { workspace = true }
11
+
aws-config = { workspace = true }
12
+
aws-sdk-s3 = { workspace = true }
13
+
bytes = { workspace = true }
14
+
futures = { workspace = true }
15
+
sha2 = { workspace = true }
16
+
thiserror = { workspace = true }
17
+
tracing = { workspace = true }
+14
crates/tranquil-types/Cargo.toml
+14
crates/tranquil-types/Cargo.toml
···
1
+
[package]
2
+
name = "tranquil-types"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
7
+
[dependencies]
8
+
chrono = { workspace = true }
9
+
cid = { workspace = true }
10
+
jacquard = { workspace = true }
11
+
serde = { workspace = true }
12
+
serde_json = { workspace = true }
13
+
sqlx = { workspace = true }
14
+
thiserror = { workspace = true }
src/api/actor/mod.rs
crates/tranquil-pds/src/api/actor/mod.rs
src/api/actor/mod.rs
crates/tranquil-pds/src/api/actor/mod.rs
src/api/actor/preferences.rs
crates/tranquil-pds/src/api/actor/preferences.rs
src/api/actor/preferences.rs
crates/tranquil-pds/src/api/actor/preferences.rs
+2
-7
src/api/admin/account/delete.rs
crates/tranquil-pds/src/api/admin/account/delete.rs
+2
-7
src/api/admin/account/delete.rs
crates/tranquil-pds/src/api/admin/account/delete.rs
···
130
130
error!("Failed to commit account deletion transaction: {:?}", e);
131
131
return ApiError::InternalError(Some("Failed to commit deletion".into())).into_response();
132
132
}
133
-
if let Err(e) = crate::api::repo::record::sequence_account_event(
134
-
&state,
135
-
did,
136
-
false,
137
-
Some("deleted"),
138
-
)
139
-
.await
133
+
if let Err(e) =
134
+
crate::api::repo::record::sequence_account_event(&state, did, false, Some("deleted")).await
140
135
{
141
136
warn!(
142
137
"Failed to sequence account deletion event for {}: {}",
src/api/admin/account/email.rs
crates/tranquil-pds/src/api/admin/account/email.rs
src/api/admin/account/email.rs
crates/tranquil-pds/src/api/admin/account/email.rs
src/api/admin/account/info.rs
crates/tranquil-pds/src/api/admin/account/info.rs
src/api/admin/account/info.rs
crates/tranquil-pds/src/api/admin/account/info.rs
src/api/admin/account/mod.rs
crates/tranquil-pds/src/api/admin/account/mod.rs
src/api/admin/account/mod.rs
crates/tranquil-pds/src/api/admin/account/mod.rs
src/api/admin/account/search.rs
crates/tranquil-pds/src/api/admin/account/search.rs
src/api/admin/account/search.rs
crates/tranquil-pds/src/api/admin/account/search.rs
+3
-6
src/api/admin/account/update.rs
crates/tranquil-pds/src/api/admin/account/update.rs
+3
-6
src/api/admin/account/update.rs
crates/tranquil-pds/src/api/admin/account/update.rs
···
104
104
}
105
105
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
106
106
let handle_typed = Handle::new_unchecked(&handle);
107
-
if let Err(e) = crate::api::repo::record::sequence_identity_event(
108
-
&state,
109
-
did,
110
-
Some(&handle_typed),
111
-
)
112
-
.await
107
+
if let Err(e) =
108
+
crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle_typed))
109
+
.await
113
110
{
114
111
warn!(
115
112
"Failed to sequence identity event for admin handle update: {}",
src/api/admin/config.rs
crates/tranquil-pds/src/api/admin/config.rs
src/api/admin/config.rs
crates/tranquil-pds/src/api/admin/config.rs
src/api/admin/invite.rs
crates/tranquil-pds/src/api/admin/invite.rs
src/api/admin/invite.rs
crates/tranquil-pds/src/api/admin/invite.rs
src/api/admin/mod.rs
crates/tranquil-pds/src/api/admin/mod.rs
src/api/admin/mod.rs
crates/tranquil-pds/src/api/admin/mod.rs
src/api/admin/server_stats.rs
crates/tranquil-pds/src/api/admin/server_stats.rs
src/api/admin/server_stats.rs
crates/tranquil-pds/src/api/admin/server_stats.rs
+6
-3
src/api/admin/status.rs
crates/tranquil-pds/src/api/admin/status.rs
+6
-3
src/api/admin/status.rs
crates/tranquil-pds/src/api/admin/status.rs
···
224
224
.execute(&mut *tx)
225
225
.await
226
226
} else {
227
-
sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did.as_str())
228
-
.execute(&mut *tx)
229
-
.await
227
+
sqlx::query!(
228
+
"UPDATE users SET deactivated_at = NULL WHERE did = $1",
229
+
did.as_str()
230
+
)
231
+
.execute(&mut *tx)
232
+
.await
230
233
};
231
234
if let Err(e) = result {
232
235
error!(
src/api/age_assurance.rs
crates/tranquil-pds/src/api/age_assurance.rs
src/api/age_assurance.rs
crates/tranquil-pds/src/api/age_assurance.rs
src/api/backup.rs
crates/tranquil-pds/src/api/backup.rs
src/api/backup.rs
crates/tranquil-pds/src/api/backup.rs
+1
-5
src/api/delegation.rs
crates/tranquil-pds/src/api/delegation.rs
+1
-5
src/api/delegation.rs
crates/tranquil-pds/src/api/delegation.rs
···
768
768
769
769
info!(did = %did, handle = %handle, controller = %&auth.0.did, "Delegated account created");
770
770
771
-
Json(CreateDelegatedAccountResponse {
772
-
did: did.into(),
773
-
handle: handle.into(),
774
-
})
775
-
.into_response()
771
+
Json(CreateDelegatedAccountResponse { did, handle }).into_response()
776
772
}
src/api/error.rs
crates/tranquil-pds/src/api/error.rs
src/api/error.rs
crates/tranquil-pds/src/api/error.rs
+6
-2
src/api/identity/account.rs
crates/tranquil-pds/src/api/identity/account.rs
+6
-2
src/api/identity/account.rs
crates/tranquil-pds/src/api/identity/account.rs
···
796
796
if !is_migration && !is_did_web_byod {
797
797
let did_typed = Did::new_unchecked(&did);
798
798
let handle_typed = Handle::new_unchecked(&handle);
799
-
if let Err(e) =
800
-
crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed)).await
799
+
if let Err(e) = crate::api::repo::record::sequence_identity_event(
800
+
&state,
801
+
&did_typed,
802
+
Some(&handle_typed),
803
+
)
804
+
.await
801
805
{
802
806
warn!("Failed to sequence identity event for {}: {}", did, e);
803
807
}
+2
-1
src/api/identity/did.rs
crates/tranquil-pds/src/api/identity/did.rs
+2
-1
src/api/identity/did.rs
crates/tranquil-pds/src/api/identity/did.rs
···
754
754
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
755
755
let handle_typed = Handle::new_unchecked(&handle);
756
756
if let Err(e) =
757
-
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await
757
+
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
758
+
.await
758
759
{
759
760
warn!("Failed to sequence identity event for handle update: {}", e);
760
761
}
src/api/identity/mod.rs
crates/tranquil-pds/src/api/identity/mod.rs
src/api/identity/mod.rs
crates/tranquil-pds/src/api/identity/mod.rs
src/api/identity/plc/mod.rs
crates/tranquil-pds/src/api/identity/plc/mod.rs
src/api/identity/plc/mod.rs
crates/tranquil-pds/src/api/identity/plc/mod.rs
src/api/identity/plc/request.rs
crates/tranquil-pds/src/api/identity/plc/request.rs
src/api/identity/plc/request.rs
crates/tranquil-pds/src/api/identity/plc/request.rs
src/api/identity/plc/sign.rs
crates/tranquil-pds/src/api/identity/plc/sign.rs
src/api/identity/plc/sign.rs
crates/tranquil-pds/src/api/identity/plc/sign.rs
src/api/identity/plc/submit.rs
crates/tranquil-pds/src/api/identity/plc/submit.rs
src/api/identity/plc/submit.rs
crates/tranquil-pds/src/api/identity/plc/submit.rs
src/api/mod.rs
crates/tranquil-pds/src/api/mod.rs
src/api/mod.rs
crates/tranquil-pds/src/api/mod.rs
src/api/moderation/mod.rs
crates/tranquil-pds/src/api/moderation/mod.rs
src/api/moderation/mod.rs
crates/tranquil-pds/src/api/moderation/mod.rs
src/api/notification_prefs.rs
crates/tranquil-pds/src/api/notification_prefs.rs
src/api/notification_prefs.rs
crates/tranquil-pds/src/api/notification_prefs.rs
+1
-2
src/api/proxy.rs
crates/tranquil-pds/src/api/proxy.rs
+1
-2
src/api/proxy.rs
crates/tranquil-pds/src/api/proxy.rs
···
268
268
}
269
269
Err(e) => {
270
270
warn!("Token validation failed: {:?}", e);
271
-
if matches!(e, crate::auth::TokenValidationError::TokenExpired)
272
-
&& extracted.is_dpop
271
+
if matches!(e, crate::auth::TokenValidationError::TokenExpired) && extracted.is_dpop
273
272
{
274
273
let www_auth =
275
274
"DPoP error=\"invalid_token\", error_description=\"Token has expired\"";
src/api/proxy_client.rs
crates/tranquil-pds/src/api/proxy_client.rs
src/api/proxy_client.rs
crates/tranquil-pds/src/api/proxy_client.rs
src/api/repo/blob.rs
crates/tranquil-pds/src/api/repo/blob.rs
src/api/repo/blob.rs
crates/tranquil-pds/src/api/repo/blob.rs
src/api/repo/import.rs
crates/tranquil-pds/src/api/repo/import.rs
src/api/repo/import.rs
crates/tranquil-pds/src/api/repo/import.rs
src/api/repo/meta.rs
crates/tranquil-pds/src/api/repo/meta.rs
src/api/repo/meta.rs
crates/tranquil-pds/src/api/repo/meta.rs
src/api/repo/mod.rs
crates/tranquil-pds/src/api/repo/mod.rs
src/api/repo/mod.rs
crates/tranquil-pds/src/api/repo/mod.rs
+8
-1
src/api/repo/record/batch.rs
crates/tranquil-pds/src/api/repo/record/batch.rs
+8
-1
src/api/repo/record/batch.rs
crates/tranquil-pds/src/api/repo/record/batch.rs
···
421
421
ops,
422
422
modified_keys,
423
423
all_blob_cids,
424
-
} = match process_writes(&input.writes, initial_mst, &did, input.validate, &tracking_store).await
424
+
} = match process_writes(
425
+
&input.writes,
426
+
initial_mst,
427
+
&did,
428
+
input.validate,
429
+
&tracking_store,
430
+
)
431
+
.await
425
432
{
426
433
Ok(acc) => acc,
427
434
Err(response) => return response,
src/api/repo/record/delete.rs
crates/tranquil-pds/src/api/repo/record/delete.rs
src/api/repo/record/delete.rs
crates/tranquil-pds/src/api/repo/record/delete.rs
src/api/repo/record/mod.rs
crates/tranquil-pds/src/api/repo/record/mod.rs
src/api/repo/record/mod.rs
crates/tranquil-pds/src/api/repo/record/mod.rs
+8
-6
src/api/repo/record/read.rs
crates/tranquil-pds/src/api/repo/record/read.rs
+8
-6
src/api/repo/record/read.rs
crates/tranquil-pds/src/api/repo/record/read.rs
···
257
257
.zip(blocks.into_iter())
258
258
.filter_map(|((_, rkey, cid_str), block_opt)| {
259
259
block_opt.and_then(|block| {
260
-
serde_ipld_dagcbor::from_slice::<Ipld>(&block).ok().map(|ipld| {
261
-
json!({
262
-
"uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey),
263
-
"cid": cid_str,
264
-
"value": ipld_to_json(ipld)
260
+
serde_ipld_dagcbor::from_slice::<Ipld>(&block)
261
+
.ok()
262
+
.map(|ipld| {
263
+
json!({
264
+
"uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey),
265
+
"cid": cid_str,
266
+
"value": ipld_to_json(ipld)
267
+
})
265
268
})
266
-
})
267
269
})
268
270
})
269
271
.collect();
+3
-3
src/api/repo/record/utils.rs
crates/tranquil-pds/src/api/repo/record/utils.rs
+3
-3
src/api/repo/record/utils.rs
crates/tranquil-pds/src/api/repo/record/utils.rs
···
219
219
.await
220
220
.map_err(|e| format!("DB Error (user_blocks delete obsolete): {}", e))?;
221
221
}
222
-
let (upserts, deletes): (Vec<_>, Vec<_>) = ops.iter().partition(|op| {
223
-
matches!(op, RecordOp::Create { .. } | RecordOp::Update { .. })
224
-
});
222
+
let (upserts, deletes): (Vec<_>, Vec<_>) = ops
223
+
.iter()
224
+
.partition(|op| matches!(op, RecordOp::Create { .. } | RecordOp::Update { .. }));
225
225
let (upsert_collections, upsert_rkeys, upsert_cids): (Vec<String>, Vec<String>, Vec<String>) =
226
226
upserts
227
227
.into_iter()
src/api/repo/record/validation.rs
crates/tranquil-pds/src/api/repo/record/validation.rs
src/api/repo/record/validation.rs
crates/tranquil-pds/src/api/repo/record/validation.rs
src/api/repo/record/write.rs
crates/tranquil-pds/src/api/repo/record/write.rs
src/api/repo/record/write.rs
crates/tranquil-pds/src/api/repo/record/write.rs
src/api/responses.rs
crates/tranquil-pds/src/api/responses.rs
src/api/responses.rs
crates/tranquil-pds/src/api/responses.rs
+2
-3
src/api/server/account_status.rs
crates/tranquil-pds/src/api/server/account_status.rs
+2
-3
src/api/server/account_status.rs
crates/tranquil-pds/src/api/server/account_status.rs
···
449
449
did
450
450
);
451
451
if let Err(e) =
452
-
crate::api::repo::record::sequence_account_event(&state, &did, true, None)
453
-
.await
452
+
crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
454
453
{
455
454
warn!(
456
455
"[MIGRATION] activateAccount: Failed to sequence account activation event: {}",
···
463
462
"[MIGRATION] activateAccount: Sequencing identity event for did={} handle={:?}",
464
463
did, handle
465
464
);
466
-
let handle_typed = handle.as_ref().map(|h| Handle::new_unchecked(h));
465
+
let handle_typed = handle.as_ref().map(Handle::new_unchecked);
467
466
if let Err(e) = crate::api::repo::record::sequence_identity_event(
468
467
&state,
469
468
&did,
src/api/server/app_password.rs
crates/tranquil-pds/src/api/server/app_password.rs
src/api/server/app_password.rs
crates/tranquil-pds/src/api/server/app_password.rs
src/api/server/email.rs
crates/tranquil-pds/src/api/server/email.rs
src/api/server/email.rs
crates/tranquil-pds/src/api/server/email.rs
src/api/server/invite.rs
crates/tranquil-pds/src/api/server/invite.rs
src/api/server/invite.rs
crates/tranquil-pds/src/api/server/invite.rs
src/api/server/logo.rs
crates/tranquil-pds/src/api/server/logo.rs
src/api/server/logo.rs
crates/tranquil-pds/src/api/server/logo.rs
src/api/server/meta.rs
crates/tranquil-pds/src/api/server/meta.rs
src/api/server/meta.rs
crates/tranquil-pds/src/api/server/meta.rs
src/api/server/migration.rs
crates/tranquil-pds/src/api/server/migration.rs
src/api/server/migration.rs
crates/tranquil-pds/src/api/server/migration.rs
src/api/server/mod.rs
crates/tranquil-pds/src/api/server/mod.rs
src/api/server/mod.rs
crates/tranquil-pds/src/api/server/mod.rs
+7
-3
src/api/server/passkey_account.rs
crates/tranquil-pds/src/api/server/passkey_account.rs
+7
-3
src/api/server/passkey_account.rs
crates/tranquil-pds/src/api/server/passkey_account.rs
···
602
602
603
603
if !is_byod_did_web {
604
604
let handle_typed = Handle::new_unchecked(&handle);
605
-
if let Err(e) =
606
-
crate::api::repo::record::sequence_identity_event(&state, &did_typed, Some(&handle_typed)).await
605
+
if let Err(e) = crate::api::repo::record::sequence_identity_event(
606
+
&state,
607
+
&did_typed,
608
+
Some(&handle_typed),
609
+
)
610
+
.await
607
611
{
608
612
warn!("Failed to sequence identity event for {}: {}", did, e);
609
613
}
···
654
658
info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion");
655
659
656
660
let access_jwt = if byod_auth.is_some() {
657
-
match crate::auth::token::create_access_token_with_metadata(&did, &secret_key_bytes) {
661
+
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
658
662
Ok(token_meta) => {
659
663
let refresh_jti = uuid::Uuid::new_v4().to_string();
660
664
let refresh_expires = chrono::Utc::now() + chrono::Duration::hours(24);
src/api/server/passkeys.rs
crates/tranquil-pds/src/api/server/passkeys.rs
src/api/server/passkeys.rs
crates/tranquil-pds/src/api/server/passkeys.rs
src/api/server/password.rs
crates/tranquil-pds/src/api/server/password.rs
src/api/server/password.rs
crates/tranquil-pds/src/api/server/password.rs
src/api/server/reauth.rs
crates/tranquil-pds/src/api/server/reauth.rs
src/api/server/reauth.rs
crates/tranquil-pds/src/api/server/reauth.rs
src/api/server/service_auth.rs
crates/tranquil-pds/src/api/server/service_auth.rs
src/api/server/service_auth.rs
crates/tranquil-pds/src/api/server/service_auth.rs
+3
-1
src/api/server/session.rs
crates/tranquil-pds/src/api/server/session.rs
+3
-1
src/api/server/session.rs
crates/tranquil-pds/src/api/server/session.rs
···
212
212
&key_bytes,
213
213
app_password_scopes.as_deref(),
214
214
app_password_controller.as_deref(),
215
+
None,
215
216
) {
216
217
Ok(m) => m,
217
218
Err(e) => {
···
489
490
&key_bytes,
490
491
session_row.scope.as_deref(),
491
492
session_row.controller_did.as_deref(),
493
+
None,
492
494
) {
493
495
Ok(m) => m,
494
496
Err(e) => {
···
1186
1188
}
1187
1189
}
1188
1190
1189
-
use crate::comms::locale::VALID_LOCALES;
1191
+
use crate::comms::VALID_LOCALES;
1190
1192
1191
1193
#[derive(Deserialize)]
1192
1194
#[serde(rename_all = "camelCase")]
src/api/server/signing_key.rs
crates/tranquil-pds/src/api/server/signing_key.rs
src/api/server/signing_key.rs
crates/tranquil-pds/src/api/server/signing_key.rs
+1
-1
src/api/server/totp.rs
crates/tranquil-pds/src/api/server/totp.rs
+1
-1
src/api/server/totp.rs
crates/tranquil-pds/src/api/server/totp.rs
···
1
1
use crate::api::EmptyResponse;
2
2
use crate::api::error::ApiError;
3
3
use crate::auth::BearerAuth;
4
-
use crate::auth::totp::{
4
+
use crate::auth::{
5
5
decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64,
6
6
generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format,
7
7
verify_backup_code, verify_totp_code,
src/api/server/trusted_devices.rs
crates/tranquil-pds/src/api/server/trusted_devices.rs
src/api/server/trusted_devices.rs
crates/tranquil-pds/src/api/server/trusted_devices.rs
src/api/server/verify_email.rs
crates/tranquil-pds/src/api/server/verify_email.rs
src/api/server/verify_email.rs
crates/tranquil-pds/src/api/server/verify_email.rs
src/api/server/verify_token.rs
crates/tranquil-pds/src/api/server/verify_token.rs
src/api/server/verify_token.rs
crates/tranquil-pds/src/api/server/verify_token.rs
src/api/temp.rs
crates/tranquil-pds/src/api/temp.rs
src/api/temp.rs
crates/tranquil-pds/src/api/temp.rs
src/api/validation.rs
crates/tranquil-pds/src/api/validation.rs
src/api/validation.rs
crates/tranquil-pds/src/api/validation.rs
src/api/verification.rs
crates/tranquil-pds/src/api/verification.rs
src/api/verification.rs
crates/tranquil-pds/src/api/verification.rs
src/appview/mod.rs
crates/tranquil-pds/src/appview/mod.rs
src/appview/mod.rs
crates/tranquil-pds/src/appview/mod.rs
src/auth/extractor.rs
crates/tranquil-pds/src/auth/extractor.rs
src/auth/extractor.rs
crates/tranquil-pds/src/auth/extractor.rs
+23
-51
src/auth/mod.rs
crates/tranquil-pds/src/auth/mod.rs
+23
-51
src/auth/mod.rs
crates/tranquil-pds/src/auth/mod.rs
···
11
11
pub mod extractor;
12
12
pub mod scope_check;
13
13
pub mod service;
14
-
pub mod token;
15
-
pub mod totp;
16
14
pub mod verification_token;
17
-
pub mod verify;
18
15
pub mod webauthn;
19
16
20
17
pub use extractor::{
···
22
19
extract_auth_token_from_header, extract_bearer_token_from_header,
23
20
};
24
21
pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
25
-
pub use token::{
26
-
SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS,
27
-
TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenWithMetadata, create_access_token,
22
+
23
+
pub use tranquil_auth::{
24
+
ActClaim, Claims, Header, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED,
25
+
SCOPE_REFRESH, TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenData,
26
+
TokenVerifyError, TokenWithMetadata, UnsafeClaims, create_access_token,
27
+
create_access_token_hs256, create_access_token_hs256_with_metadata,
28
28
create_access_token_with_delegation, create_access_token_with_metadata,
29
-
create_access_token_with_scope_metadata, create_refresh_token,
30
-
create_refresh_token_with_metadata, create_service_token,
31
-
};
32
-
pub use verify::{
33
-
TokenVerifyError, get_did_from_token, get_jti_from_token, verify_access_token,
34
-
verify_access_token_typed, verify_refresh_token, verify_token,
29
+
create_access_token_with_scope_metadata, create_refresh_token, create_refresh_token_hs256,
30
+
create_refresh_token_hs256_with_metadata, create_refresh_token_with_metadata,
31
+
create_service_token, create_service_token_hs256, generate_backup_codes,
32
+
generate_qr_png_base64, generate_totp_secret, generate_totp_uri, get_algorithm_from_token,
33
+
get_did_from_token, get_jti_from_token, hash_backup_code, is_backup_code_format,
34
+
verify_access_token, verify_access_token_hs256, verify_access_token_typed, verify_backup_code,
35
+
verify_refresh_token, verify_refresh_token_hs256, verify_token, verify_totp_code,
35
36
};
37
+
38
+
pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, String> {
39
+
crate::config::encrypt_key(secret)
40
+
}
41
+
42
+
pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result<Vec<u8>, String> {
43
+
crate::config::decrypt_key(encrypted, Some(version))
44
+
}
36
45
37
46
const KEY_CACHE_TTL_SECS: u64 = 300;
38
47
const SESSION_CACHE_TTL_SECS: u64 = 60;
···
347
356
});
348
357
}
349
358
}
350
-
Err(verify::TokenVerifyError::Expired) => {
359
+
Err(TokenVerifyError::Expired) => {
351
360
return Err(TokenValidationError::TokenExpired);
352
361
}
353
-
Err(verify::TokenVerifyError::Invalid) => {}
362
+
Err(TokenVerifyError::Invalid) => {}
354
363
}
355
364
}
356
365
}
···
492
501
Err(_) => Err(TokenValidationError::AuthenticationFailed),
493
502
}
494
503
}
495
-
496
-
#[derive(Debug, Clone, Serialize, Deserialize)]
497
-
pub struct ActClaim {
498
-
pub sub: String,
499
-
}
500
-
501
-
#[derive(Debug, Serialize, Deserialize)]
502
-
pub struct Claims {
503
-
pub iss: String,
504
-
pub sub: String,
505
-
pub aud: String,
506
-
pub exp: usize,
507
-
pub iat: usize,
508
-
#[serde(skip_serializing_if = "Option::is_none")]
509
-
pub scope: Option<String>,
510
-
#[serde(skip_serializing_if = "Option::is_none")]
511
-
pub lxm: Option<String>,
512
-
pub jti: String,
513
-
#[serde(skip_serializing_if = "Option::is_none")]
514
-
pub act: Option<ActClaim>,
515
-
}
516
-
517
-
#[derive(Debug, Serialize, Deserialize)]
518
-
pub struct Header {
519
-
pub alg: String,
520
-
pub typ: String,
521
-
}
522
-
523
-
#[derive(Debug, Serialize, Deserialize)]
524
-
pub struct UnsafeClaims {
525
-
pub iss: String,
526
-
pub sub: Option<String>,
527
-
}
528
-
529
-
pub struct TokenData<T> {
530
-
pub claims: T,
531
-
}
+1
-1
src/auth/scope_check.rs
crates/tranquil-pds/src/auth/scope_check.rs
+1
-1
src/auth/scope_check.rs
crates/tranquil-pds/src/auth/scope_check.rs
src/auth/service.rs
crates/tranquil-pds/src/auth/service.rs
src/auth/service.rs
crates/tranquil-pds/src/auth/service.rs
+16
-15
src/auth/token.rs
crates/tranquil-auth/src/token.rs
+16
-15
src/auth/token.rs
crates/tranquil-auth/src/token.rs
···
1
-
use super::{ActClaim, Claims, Header};
1
+
use super::types::{ActClaim, Claims, Header, TokenWithMetadata};
2
2
use anyhow::Result;
3
3
use base64::Engine as _;
4
4
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5
-
use chrono::{DateTime, Duration, Utc};
5
+
use chrono::{Duration, Utc};
6
6
use hmac::{Hmac, Mac};
7
7
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
8
8
use sha2::Sha256;
9
-
use uuid;
10
9
11
10
type HmacSha256 = Hmac<Sha256>;
12
11
···
18
17
pub const SCOPE_APP_PASS: &str = "com.atproto.appPass";
19
18
pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged";
20
19
21
-
pub struct TokenWithMetadata {
22
-
pub token: String,
23
-
pub jti: String,
24
-
pub expires_at: DateTime<Utc>,
25
-
}
26
-
27
20
pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
28
21
Ok(create_access_token_with_metadata(did, key_bytes)?.token)
29
22
}
···
33
26
}
34
27
35
28
pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
36
-
create_access_token_with_scope_metadata(did, key_bytes, None)
29
+
create_access_token_with_scope_metadata(did, key_bytes, None, None)
37
30
}
38
31
39
32
pub fn create_access_token_with_scope_metadata(
40
33
did: &str,
41
34
key_bytes: &[u8],
42
35
scopes: Option<&str>,
36
+
hostname: Option<&str>,
43
37
) -> Result<TokenWithMetadata> {
44
38
let scope = scopes.unwrap_or(SCOPE_ACCESS);
45
39
create_signed_token_with_metadata(
···
48
42
TOKEN_TYPE_ACCESS,
49
43
key_bytes,
50
44
Duration::minutes(15),
45
+
hostname,
51
46
)
52
47
}
53
48
···
56
51
key_bytes: &[u8],
57
52
scopes: Option<&str>,
58
53
controller_did: Option<&str>,
54
+
hostname: Option<&str>,
59
55
) -> Result<TokenWithMetadata> {
60
56
let scope = scopes.unwrap_or(SCOPE_ACCESS);
61
57
let act = controller_did.map(|c| ActClaim { sub: c.to_string() });
···
66
62
key_bytes,
67
63
Duration::minutes(15),
68
64
act,
65
+
hostname,
69
66
)
70
67
}
71
68
···
79
76
TOKEN_TYPE_REFRESH,
80
77
key_bytes,
81
78
Duration::days(14),
79
+
None,
82
80
)
83
81
}
84
82
···
111
109
typ: &str,
112
110
key_bytes: &[u8],
113
111
duration: Duration,
112
+
hostname: Option<&str>,
114
113
) -> Result<TokenWithMetadata> {
115
-
create_signed_token_with_act(did, scope, typ, key_bytes, duration, None)
114
+
create_signed_token_with_act(did, scope, typ, key_bytes, duration, None, hostname)
116
115
}
117
116
118
117
fn create_signed_token_with_act(
···
122
121
key_bytes: &[u8],
123
122
duration: Duration,
124
123
act: Option<ActClaim>,
124
+
hostname: Option<&str>,
125
125
) -> Result<TokenWithMetadata> {
126
126
let signing_key = SigningKey::from_slice(key_bytes)?;
127
127
···
132
132
let expiration = expires_at.timestamp();
133
133
let jti = uuid::Uuid::new_v4().to_string();
134
134
135
+
let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| {
136
+
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
137
+
});
138
+
135
139
let claims = Claims {
136
140
iss: did.to_owned(),
137
141
sub: did.to_owned(),
138
-
aud: format!(
139
-
"did:web:{}",
140
-
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
141
-
),
142
+
aud: format!("did:web:{}", aud_hostname),
142
143
exp: expiration as usize,
143
144
iat: Utc::now().timestamp() as usize,
144
145
scope: Some(scope.to_string()),
+23
-20
src/auth/totp.rs
crates/tranquil-auth/src/totp.rs
+23
-20
src/auth/totp.rs
crates/tranquil-auth/src/totp.rs
···
13
13
secret
14
14
}
15
15
16
-
pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, String> {
17
-
crate::config::encrypt_key(secret)
16
+
pub fn encrypt_totp_secret(
17
+
secret: &[u8],
18
+
master_key: &[u8; 32],
19
+
) -> Result<Vec<u8>, tranquil_crypto::CryptoError> {
20
+
tranquil_crypto::encrypt_with_key(master_key, secret)
18
21
}
19
22
20
-
pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result<Vec<u8>, String> {
21
-
crate::config::decrypt_key(encrypted, Some(version))
23
+
pub fn decrypt_totp_secret(
24
+
encrypted: &[u8],
25
+
master_key: &[u8; 32],
26
+
) -> Result<Vec<u8>, tranquil_crypto::CryptoError> {
27
+
tranquil_crypto::decrypt_with_key(master_key, encrypted)
22
28
}
23
29
24
30
fn create_totp(
···
53
59
.map(|d| d.as_secs())
54
60
.unwrap_or(0);
55
61
56
-
for offset in [-1i64, 0, 1] {
62
+
[-1i64, 0, 1].iter().any(|&offset| {
57
63
let time = (now as i64 + offset * TOTP_STEP as i64) as u64;
58
64
let expected = totp.generate(time);
59
65
let is_valid: bool = code.as_bytes().ct_eq(expected.as_bytes()).into();
60
-
if is_valid {
61
-
return true;
62
-
}
63
-
}
64
-
65
-
false
66
+
is_valid
67
+
})
66
68
}
67
69
68
70
pub fn generate_totp_uri(secret: &[u8], account_name: &str, issuer: &str) -> String {
···
107
109
let mut codes = Vec::with_capacity(BACKUP_CODE_COUNT);
108
110
let mut rng = rand::thread_rng();
109
111
110
-
for _ in 0..BACKUP_CODE_COUNT {
111
-
let mut code = String::with_capacity(BACKUP_CODE_LENGTH);
112
-
for _ in 0..BACKUP_CODE_LENGTH {
113
-
let idx = (rng.next_u32() as usize) % BACKUP_CODE_ALPHABET.len();
114
-
code.push(BACKUP_CODE_ALPHABET[idx] as char);
115
-
}
112
+
(0..BACKUP_CODE_COUNT).for_each(|_| {
113
+
let code: String = (0..BACKUP_CODE_LENGTH)
114
+
.map(|_| {
115
+
let idx = (rng.next_u32() as usize) % BACKUP_CODE_ALPHABET.len();
116
+
BACKUP_CODE_ALPHABET[idx] as char
117
+
})
118
+
.collect();
116
119
codes.push(code);
117
-
}
120
+
});
118
121
119
122
codes
120
123
}
···
167
170
fn test_backup_codes() {
168
171
let codes = generate_backup_codes();
169
172
assert_eq!(codes.len(), BACKUP_CODE_COUNT);
170
-
for code in &codes {
173
+
codes.iter().for_each(|code| {
171
174
assert_eq!(code.len(), BACKUP_CODE_LENGTH);
172
175
assert!(is_backup_code_format(code));
173
-
}
176
+
});
174
177
}
175
178
176
179
#[test]
src/auth/verification_token.rs
crates/tranquil-pds/src/auth/verification_token.rs
src/auth/verification_token.rs
crates/tranquil-pds/src/auth/verification_token.rs
+17
-35
src/auth/verify.rs
crates/tranquil-auth/src/verify.rs
+17
-35
src/auth/verify.rs
crates/tranquil-auth/src/verify.rs
···
2
2
SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS,
3
3
TOKEN_TYPE_REFRESH,
4
4
};
5
-
use super::{Claims, Header, TokenData, UnsafeClaims};
5
+
use super::types::{Claims, Header, TokenData, TokenVerifyError, UnsafeClaims};
6
6
use anyhow::{Context, Result, anyhow};
7
7
use base64::Engine as _;
8
8
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
···
10
10
use hmac::{Hmac, Mac};
11
11
use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
12
12
use sha2::Sha256;
13
-
use std::fmt;
14
13
use subtle::ConstantTimeEq;
15
14
16
15
type HmacSha256 = Hmac<Sha256>;
17
-
18
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19
-
pub enum TokenVerifyError {
20
-
Expired,
21
-
Invalid,
22
-
}
23
-
24
-
impl fmt::Display for TokenVerifyError {
25
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26
-
match self {
27
-
Self::Expired => write!(f, "Token expired"),
28
-
Self::Invalid => write!(f, "Token invalid"),
29
-
}
30
-
}
31
-
}
32
-
33
-
impl std::error::Error for TokenVerifyError {}
34
16
35
17
pub fn get_did_from_token(token: &str) -> Result<String, String> {
36
18
let parts: Vec<&str> = token.split('.').collect();
···
66
48
.and_then(|j| j.as_str())
67
49
.map(|s| s.to_string())
68
50
.ok_or_else(|| "No jti claim in token".to_string())
51
+
}
52
+
53
+
pub fn get_algorithm_from_token(token: &str) -> Result<String, String> {
54
+
let parts: Vec<&str> = token.split('.').collect();
55
+
if parts.len() != 3 {
56
+
return Err("Invalid token format".to_string());
57
+
}
58
+
59
+
let header_bytes = URL_SAFE_NO_PAD
60
+
.decode(parts[0])
61
+
.map_err(|e| format!("Base64 decode failed: {}", e))?;
62
+
63
+
let header: Header =
64
+
serde_json::from_slice(&header_bytes).map_err(|e| format!("JSON decode failed: {}", e))?;
65
+
66
+
Ok(header.alg)
69
67
}
70
68
71
69
pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> {
···
331
329
332
330
Ok(TokenData { claims })
333
331
}
334
-
335
-
pub fn get_algorithm_from_token(token: &str) -> Result<String, String> {
336
-
let parts: Vec<&str> = token.split('.').collect();
337
-
if parts.len() != 3 {
338
-
return Err("Invalid token format".to_string());
339
-
}
340
-
341
-
let header_bytes = URL_SAFE_NO_PAD
342
-
.decode(parts[0])
343
-
.map_err(|e| format!("Base64 decode failed: {}", e))?;
344
-
345
-
let header: Header =
346
-
serde_json::from_slice(&header_bytes).map_err(|e| format!("JSON decode failed: {}", e))?;
347
-
348
-
Ok(header.alg)
349
-
}
src/auth/webauthn.rs
crates/tranquil-pds/src/auth/webauthn.rs
src/auth/webauthn.rs
crates/tranquil-pds/src/auth/webauthn.rs
+18
-26
src/cache/mod.rs
crates/tranquil-cache/src/lib.rs
+18
-26
src/cache/mod.rs
crates/tranquil-cache/src/lib.rs
···
1
+
pub use tranquil_infra::{Cache, CacheError, DistributedRateLimiter};
2
+
1
3
use async_trait::async_trait;
2
4
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
3
5
use std::sync::Arc;
4
6
use std::time::Duration;
5
-
6
-
#[derive(Debug, thiserror::Error)]
7
-
pub enum CacheError {
8
-
#[error("Cache connection error: {0}")]
9
-
Connection(String),
10
-
#[error("Serialization error: {0}")]
11
-
Serialization(String),
12
-
}
13
-
14
-
#[async_trait]
15
-
pub trait Cache: Send + Sync {
16
-
async fn get(&self, key: &str) -> Option<String>;
17
-
async fn set(&self, key: &str, value: &str, ttl: Duration) -> Result<(), CacheError>;
18
-
async fn delete(&self, key: &str) -> Result<(), CacheError>;
19
-
async fn get_bytes(&self, key: &str) -> Option<Vec<u8>> {
20
-
self.get(key).await.and_then(|s| BASE64.decode(&s).ok())
21
-
}
22
-
async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError> {
23
-
let encoded = BASE64.encode(value);
24
-
self.set(key, &encoded, ttl).await
25
-
}
26
-
}
27
7
28
8
#[derive(Clone)]
29
9
pub struct ValkeyCache {
···
77
57
.await
78
58
.map_err(|e| CacheError::Connection(e.to_string()))
79
59
}
60
+
61
+
async fn get_bytes(&self, key: &str) -> Option<Vec<u8>> {
62
+
self.get(key).await.and_then(|s| BASE64.decode(&s).ok())
63
+
}
64
+
65
+
async fn set_bytes(&self, key: &str, value: &[u8], ttl: Duration) -> Result<(), CacheError> {
66
+
let encoded = BASE64.encode(value);
67
+
self.set(key, &encoded, ttl).await
68
+
}
80
69
}
81
70
82
71
pub struct NoOpCache;
···
94
83
async fn delete(&self, _key: &str) -> Result<(), CacheError> {
95
84
Ok(())
96
85
}
97
-
}
98
86
99
-
#[async_trait]
100
-
pub trait DistributedRateLimiter: Send + Sync {
101
-
async fn check_rate_limit(&self, key: &str, limit: u32, window_ms: u64) -> bool;
87
+
async fn get_bytes(&self, _key: &str) -> Option<Vec<u8>> {
88
+
None
89
+
}
90
+
91
+
async fn set_bytes(&self, _key: &str, _value: &[u8], _ttl: Duration) -> Result<(), CacheError> {
92
+
Ok(())
93
+
}
102
94
}
103
95
104
96
#[derive(Clone)]
src/circuit_breaker.rs
crates/tranquil-pds/src/circuit_breaker.rs
src/circuit_breaker.rs
crates/tranquil-pds/src/circuit_breaker.rs
src/comms/locale.rs
crates/tranquil-comms/src/locale.rs
src/comms/locale.rs
crates/tranquil-comms/src/locale.rs
-18
src/comms/mod.rs
-18
src/comms/mod.rs
···
1
-
pub mod locale;
2
-
mod sender;
3
-
mod service;
4
-
mod types;
5
-
6
-
pub use sender::{
7
-
CommsSender, DiscordSender, EmailSender, SendError, SignalSender, TelegramSender,
8
-
is_valid_phone_number, sanitize_header_value,
9
-
};
10
-
11
-
pub use service::{
12
-
CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion, enqueue_comms,
13
-
enqueue_email_update, enqueue_email_update_token, enqueue_migration_verification,
14
-
enqueue_passkey_recovery, enqueue_password_reset, enqueue_plc_operation,
15
-
enqueue_signup_verification, enqueue_welcome, queue_legacy_login_notification,
16
-
};
17
-
18
-
pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
src/comms/sender.rs
crates/tranquil-comms/src/sender.rs
src/comms/sender.rs
crates/tranquil-comms/src/sender.rs
+18
-18
src/comms/service.rs
crates/tranquil-pds/src/comms/service.rs
+18
-18
src/comms/service.rs
crates/tranquil-pds/src/comms/service.rs
···
7
7
use tokio::sync::watch;
8
8
use tokio::time::interval;
9
9
use tracing::{debug, error, info, warn};
10
+
use tranquil_comms::{
11
+
CommsChannel, CommsSender, CommsStatus, CommsType, NewComms, QueuedComms, SendError,
12
+
format_message, get_strings,
13
+
};
10
14
use uuid::Uuid;
11
-
12
-
use super::locale::{format_message, get_strings};
13
-
use super::sender::{CommsSender, SendError};
14
-
use super::types::{CommsChannel, CommsStatus, NewComms, QueuedComms};
15
15
16
16
pub struct CommsService {
17
17
db: PgPool,
···
63
63
"#,
64
64
item.user_id,
65
65
item.channel as CommsChannel,
66
-
item.comms_type as super::types::CommsType,
66
+
item.comms_type as CommsType,
67
67
item.recipient,
68
68
item.subject,
69
69
item.body,
···
140
140
RETURNING
141
141
id, user_id,
142
142
channel as "channel: CommsChannel",
143
-
comms_type as "comms_type: super::types::CommsType",
143
+
comms_type as "comms_type: CommsType",
144
144
status as "status: CommsStatus",
145
145
recipient, subject, body, metadata,
146
146
attempts, max_attempts, last_error,
···
244
244
"#,
245
245
item.user_id,
246
246
item.channel as CommsChannel,
247
-
item.comms_type as super::types::CommsType,
247
+
item.comms_type as CommsType,
248
248
item.recipient,
249
249
item.subject,
250
250
item.body,
···
304
304
NewComms::new(
305
305
user_id,
306
306
prefs.channel,
307
-
super::types::CommsType::Welcome,
307
+
CommsType::Welcome,
308
308
prefs.email.unwrap_or_default(),
309
309
Some(subject),
310
310
body,
···
331
331
NewComms::new(
332
332
user_id,
333
333
prefs.channel,
334
-
super::types::CommsType::PasswordReset,
334
+
CommsType::PasswordReset,
335
335
prefs.email.unwrap_or_default(),
336
336
Some(subject),
337
337
body,
···
371
371
db,
372
372
NewComms::email(
373
373
user_id,
374
-
super::types::CommsType::EmailUpdate,
374
+
CommsType::EmailUpdate,
375
375
new_email.to_string(),
376
376
subject,
377
377
body,
···
409
409
db,
410
410
NewComms::email(
411
411
user_id,
412
-
super::types::CommsType::EmailUpdate,
412
+
CommsType::EmailUpdate,
413
413
current_email,
414
414
subject,
415
415
body,
···
436
436
NewComms::new(
437
437
user_id,
438
438
prefs.channel,
439
-
super::types::CommsType::AccountDeletion,
439
+
CommsType::AccountDeletion,
440
440
prefs.email.unwrap_or_default(),
441
441
Some(subject),
442
442
body,
···
463
463
NewComms::new(
464
464
user_id,
465
465
prefs.channel,
466
-
super::types::CommsType::PlcOperation,
466
+
CommsType::PlcOperation,
467
467
prefs.email.unwrap_or_default(),
468
468
Some(subject),
469
469
body,
···
490
490
NewComms::new(
491
491
user_id,
492
492
prefs.channel,
493
-
super::types::CommsType::TwoFactorCode,
493
+
CommsType::TwoFactorCode,
494
494
prefs.email.unwrap_or_default(),
495
495
Some(subject),
496
496
body,
···
517
517
NewComms::new(
518
518
user_id,
519
519
prefs.channel,
520
-
super::types::CommsType::PasskeyRecovery,
520
+
CommsType::PasskeyRecovery,
521
521
prefs.email.unwrap_or_default(),
522
522
Some(subject),
523
523
body,
···
586
586
NewComms::new(
587
587
user_id,
588
588
comms_channel,
589
-
super::types::CommsType::EmailVerification,
589
+
CommsType::EmailVerification,
590
590
recipient.to_string(),
591
591
subject,
592
592
body,
···
628
628
db,
629
629
NewComms::email(
630
630
user_id,
631
-
super::types::CommsType::MigrationVerification,
631
+
CommsType::MigrationVerification,
632
632
email.to_string(),
633
633
subject,
634
634
body,
···
664
664
NewComms::new(
665
665
user_id,
666
666
channel,
667
-
super::types::CommsType::LegacyLoginAlert,
667
+
CommsType::LegacyLoginAlert,
668
668
prefs.email.unwrap_or_default(),
669
669
Some(subject),
670
670
body,
+7
-5
src/comms/types.rs
crates/tranquil-comms/src/types.rs
+7
-5
src/comms/types.rs
crates/tranquil-comms/src/types.rs
···
1
1
use chrono::{DateTime, Utc};
2
2
use serde::{Deserialize, Serialize};
3
-
use sqlx::FromRow;
4
3
use uuid::Uuid;
5
4
6
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, Serialize, Deserialize)]
5
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
6
+
#[serde(rename_all = "lowercase")]
7
7
#[sqlx(type_name = "comms_channel", rename_all = "lowercase")]
8
8
pub enum CommsChannel {
9
9
Email,
···
12
12
Signal,
13
13
}
14
14
15
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)]
15
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
16
+
#[serde(rename_all = "lowercase")]
16
17
#[sqlx(type_name = "comms_status", rename_all = "lowercase")]
17
18
pub enum CommsStatus {
18
19
Pending,
···
21
22
Failed,
22
23
}
23
24
24
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)]
25
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
26
+
#[serde(rename_all = "snake_case")]
25
27
#[sqlx(type_name = "comms_type", rename_all = "snake_case")]
26
28
pub enum CommsType {
27
29
Welcome,
···
37
39
MigrationVerification,
38
40
}
39
41
40
-
#[derive(Debug, Clone, FromRow)]
42
+
#[derive(Debug, Clone)]
41
43
pub struct QueuedComms {
42
44
pub id: Uuid,
43
45
pub user_id: Uuid,
src/config.rs
crates/tranquil-pds/src/config.rs
src/config.rs
crates/tranquil-pds/src/config.rs
src/crawlers.rs
crates/tranquil-pds/src/crawlers.rs
src/crawlers.rs
crates/tranquil-pds/src/crawlers.rs
src/delegation/audit.rs
crates/tranquil-pds/src/delegation/audit.rs
src/delegation/audit.rs
crates/tranquil-pds/src/delegation/audit.rs
src/delegation/db.rs
crates/tranquil-pds/src/delegation/db.rs
src/delegation/db.rs
crates/tranquil-pds/src/delegation/db.rs
src/delegation/mod.rs
crates/tranquil-pds/src/delegation/mod.rs
src/delegation/mod.rs
crates/tranquil-pds/src/delegation/mod.rs
src/delegation/scopes.rs
crates/tranquil-pds/src/delegation/scopes.rs
src/delegation/scopes.rs
crates/tranquil-pds/src/delegation/scopes.rs
src/handle/mod.rs
crates/tranquil-pds/src/handle/mod.rs
src/handle/mod.rs
crates/tranquil-pds/src/handle/mod.rs
src/handle/reserved.rs
crates/tranquil-pds/src/handle/reserved.rs
src/handle/reserved.rs
crates/tranquil-pds/src/handle/reserved.rs
src/image/mod.rs
crates/tranquil-pds/src/image/mod.rs
src/image/mod.rs
crates/tranquil-pds/src/image/mod.rs
src/lib.rs
crates/tranquil-pds/src/lib.rs
src/lib.rs
crates/tranquil-pds/src/lib.rs
src/main.rs
crates/tranquil-pds/src/main.rs
src/main.rs
crates/tranquil-pds/src/main.rs
src/metrics.rs
crates/tranquil-pds/src/metrics.rs
src/metrics.rs
crates/tranquil-pds/src/metrics.rs
src/moderation/mod.rs
crates/tranquil-pds/src/moderation/mod.rs
src/moderation/mod.rs
crates/tranquil-pds/src/moderation/mod.rs
+25
-31
src/oauth/client.rs
crates/tranquil-oauth/src/client.rs
+25
-31
src/oauth/client.rs
crates/tranquil-oauth/src/client.rs
···
4
4
use std::sync::Arc;
5
5
use tokio::sync::RwLock;
6
6
7
-
use super::OAuthError;
7
+
use crate::OAuthError;
8
+
use crate::types::ClientAuth;
8
9
9
10
#[derive(Debug, Clone, Serialize, Deserialize)]
10
11
pub struct ClientMetadata {
···
96
97
url.scheme() == "http"
97
98
&& url.host_str() == Some("localhost")
98
99
&& url.port().is_none()
99
-
// empty path
100
100
&& url.path() == "/"
101
101
} else {
102
102
false
···
108
108
.map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?;
109
109
let mut redirect_uris = Vec::<String>::new();
110
110
let mut scope: Option<String> = None;
111
-
for (key, value) in url.query_pairs() {
112
-
if key == "redirect_uri" {
111
+
url.query_pairs().for_each(|(key, value)| {
112
+
if key == "redirect_uri" && redirect_uris.is_empty() {
113
113
redirect_uris.push(value.to_string());
114
-
break;
115
114
}
116
-
if key == "scope" {
115
+
if key == "scope" && scope.is_none() {
117
116
scope = Some(value.into());
118
-
break;
119
117
}
120
-
}
118
+
});
121
119
if redirect_uris.is_empty() {
122
120
redirect_uris.push("http://127.0.0.1/".into());
123
121
redirect_uris.push("http://[::1]/".into());
···
289
287
"redirect_uris is required".to_string(),
290
288
));
291
289
}
292
-
for uri in &metadata.redirect_uris {
293
-
self.validate_redirect_uri_format(uri)?;
294
-
}
290
+
metadata
291
+
.redirect_uris
292
+
.iter()
293
+
.try_for_each(|uri| self.validate_redirect_uri_format(uri))?;
295
294
if !metadata.grant_types.is_empty()
296
295
&& !metadata
297
296
.grant_types
···
357
356
if !scheme
358
357
.chars()
359
358
.next()
360
-
.map(|c| c.is_ascii_lowercase())
361
-
.unwrap_or(false)
359
+
.is_some_and(|c| c.is_ascii_lowercase())
362
360
{
363
361
return Err(OAuthError::InvalidClient(format!(
364
362
"Invalid redirect_uri scheme: {}",
···
388
386
pub async fn verify_client_auth(
389
387
cache: &ClientMetadataCache,
390
388
metadata: &ClientMetadata,
391
-
client_auth: &super::ClientAuth,
389
+
client_auth: &ClientAuth,
392
390
) -> Result<(), OAuthError> {
393
391
let expected_method = metadata.auth_method();
394
392
match (expected_method, client_auth) {
395
-
("none", super::ClientAuth::None) => Ok(()),
393
+
("none", ClientAuth::None) => Ok(()),
396
394
("none", _) => Err(OAuthError::InvalidClient(
397
395
"Client is configured for no authentication, but credentials were provided".to_string(),
398
396
)),
399
-
("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => {
397
+
("private_key_jwt", ClientAuth::PrivateKeyJwt { client_assertion }) => {
400
398
verify_private_key_jwt_async(cache, metadata, client_assertion).await
401
399
}
402
400
("private_key_jwt", _) => Err(OAuthError::InvalidClient(
403
401
"Client requires private_key_jwt authentication".to_string(),
404
402
)),
405
-
("client_secret_post", super::ClientAuth::SecretPost { .. }) => {
406
-
Err(OAuthError::InvalidClient(
407
-
"client_secret_post is not supported for ATProto OAuth".to_string(),
408
-
))
409
-
}
410
-
("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => {
411
-
Err(OAuthError::InvalidClient(
412
-
"client_secret_basic is not supported for ATProto OAuth".to_string(),
413
-
))
414
-
}
403
+
("client_secret_post", ClientAuth::SecretPost { .. }) => Err(OAuthError::InvalidClient(
404
+
"client_secret_post is not supported for ATProto OAuth".to_string(),
405
+
)),
406
+
("client_secret_basic", ClientAuth::SecretBasic { .. }) => Err(OAuthError::InvalidClient(
407
+
"client_secret_basic is not supported for ATProto OAuth".to_string(),
408
+
)),
415
409
(method, _) => Err(OAuthError::InvalidClient(format!(
416
410
"Unsupported or mismatched authentication method: {}",
417
411
method
···
519
513
.get("keys")
520
514
.and_then(|k| k.as_array())
521
515
.ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?;
522
-
let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid {
523
-
keys.iter()
516
+
let matching_keys: Vec<&serde_json::Value> = match kid {
517
+
Some(kid) => keys
518
+
.iter()
524
519
.filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid))
525
-
.collect()
526
-
} else {
527
-
keys.iter().collect()
520
+
.collect(),
521
+
None => keys.iter().collect(),
528
522
};
529
523
if matching_keys.is_empty() {
530
524
return Err(OAuthError::InvalidClient(
src/oauth/db/client.rs
crates/tranquil-pds/src/oauth/db/client.rs
src/oauth/db/client.rs
crates/tranquil-pds/src/oauth/db/client.rs
src/oauth/db/device.rs
crates/tranquil-pds/src/oauth/db/device.rs
src/oauth/db/device.rs
crates/tranquil-pds/src/oauth/db/device.rs
src/oauth/db/dpop.rs
crates/tranquil-pds/src/oauth/db/dpop.rs
src/oauth/db/dpop.rs
crates/tranquil-pds/src/oauth/db/dpop.rs
src/oauth/db/helpers.rs
crates/tranquil-pds/src/oauth/db/helpers.rs
src/oauth/db/helpers.rs
crates/tranquil-pds/src/oauth/db/helpers.rs
src/oauth/db/mod.rs
crates/tranquil-pds/src/oauth/db/mod.rs
src/oauth/db/mod.rs
crates/tranquil-pds/src/oauth/db/mod.rs
src/oauth/db/request.rs
crates/tranquil-pds/src/oauth/db/request.rs
src/oauth/db/request.rs
crates/tranquil-pds/src/oauth/db/request.rs
src/oauth/db/scope_preference.rs
crates/tranquil-pds/src/oauth/db/scope_preference.rs
src/oauth/db/scope_preference.rs
crates/tranquil-pds/src/oauth/db/scope_preference.rs
src/oauth/db/token.rs
crates/tranquil-pds/src/oauth/db/token.rs
src/oauth/db/token.rs
crates/tranquil-pds/src/oauth/db/token.rs
src/oauth/db/two_factor.rs
crates/tranquil-pds/src/oauth/db/two_factor.rs
src/oauth/db/two_factor.rs
crates/tranquil-pds/src/oauth/db/two_factor.rs
+2
-2
src/oauth/dpop.rs
crates/tranquil-oauth/src/dpop.rs
+2
-2
src/oauth/dpop.rs
crates/tranquil-oauth/src/dpop.rs
···
4
4
use serde::{Deserialize, Serialize};
5
5
use sha2::{Digest, Sha256};
6
6
7
-
use super::OAuthError;
8
-
use crate::types::{DPoPProofId, JwkThumbprint};
7
+
use crate::OAuthError;
8
+
use tranquil_types::{DPoPProofId, JwkThumbprint};
9
9
10
10
const DPOP_NONCE_VALIDITY_SECS: i64 = 300;
11
11
const DPOP_MAX_AGE_SECS: i64 = 300;
src/oauth/endpoints/delegation.rs
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
src/oauth/endpoints/delegation.rs
crates/tranquil-pds/src/oauth/endpoints/delegation.rs
src/oauth/endpoints/metadata.rs
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
src/oauth/endpoints/metadata.rs
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
src/oauth/endpoints/mod.rs
crates/tranquil-pds/src/oauth/endpoints/mod.rs
src/oauth/endpoints/mod.rs
crates/tranquil-pds/src/oauth/endpoints/mod.rs
+3
-4
src/oauth/endpoints/par.rs
crates/tranquil-pds/src/oauth/endpoints/par.rs
+3
-4
src/oauth/endpoints/par.rs
crates/tranquil-pds/src/oauth/endpoints/par.rs
···
1
1
use crate::oauth::{
2
-
AuthorizationRequestParameters, ClientAuth, OAuthError, RequestData, RequestId,
3
-
client::ClientMetadataCache,
4
-
db,
2
+
AuthorizationRequestParameters, ClientAuth, ClientMetadataCache, OAuthError, RequestData,
3
+
RequestId, db,
5
4
scopes::{ParsedScope, parse_scope},
6
5
};
7
6
use crate::state::{AppState, RateLimitKind};
···
173
172
174
173
fn validate_scope(
175
174
requested_scope: &Option<String>,
176
-
client_metadata: &crate::oauth::client::ClientMetadata,
175
+
client_metadata: &crate::oauth::ClientMetadata,
177
176
) -> Result<Option<String>, OAuthError> {
178
177
let scope_str = match requested_scope {
179
178
Some(s) if !s.is_empty() => s,
+4
-5
src/oauth/endpoints/token/grants.rs
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
+4
-5
src/oauth/endpoints/token/grants.rs
crates/tranquil-pds/src/oauth/endpoints/token/grants.rs
···
3
3
use crate::config::AuthConfig;
4
4
use crate::delegation;
5
5
use crate::oauth::{
6
-
AuthFlowState, ClientAuth, OAuthError, RefreshToken, TokenData, TokenId,
7
-
client::{ClientMetadataCache, verify_client_auth},
6
+
AuthFlowState, ClientAuth, ClientMetadataCache, DPoPVerifier, OAuthError, RefreshToken,
7
+
TokenData, TokenId,
8
8
db::{self, RefreshTokenLookup},
9
-
dpop::DPoPVerifier,
10
9
scopes::expand_include_scopes,
10
+
verify_client_auth,
11
11
};
12
12
use crate::state::AppState;
13
13
use axum::Json;
···
110
110
Some(result.jkt.as_str().to_string())
111
111
} else if auth_request.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() {
112
112
return Err(OAuthError::UseDpopNonce(
113
-
crate::oauth::dpop::DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes())
114
-
.generate_nonce(),
113
+
DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes()).generate_nonce(),
115
114
));
116
115
} else {
117
116
None
src/oauth/endpoints/token/helpers.rs
crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs
src/oauth/endpoints/token/helpers.rs
crates/tranquil-pds/src/oauth/endpoints/token/helpers.rs
src/oauth/endpoints/token/introspect.rs
crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs
src/oauth/endpoints/token/introspect.rs
crates/tranquil-pds/src/oauth/endpoints/token/introspect.rs
src/oauth/endpoints/token/mod.rs
crates/tranquil-pds/src/oauth/endpoints/token/mod.rs
src/oauth/endpoints/token/mod.rs
crates/tranquil-pds/src/oauth/endpoints/token/mod.rs
src/oauth/endpoints/token/types.rs
crates/tranquil-pds/src/oauth/endpoints/token/types.rs
src/oauth/endpoints/token/types.rs
crates/tranquil-pds/src/oauth/endpoints/token/types.rs
+6
-6
src/oauth/error.rs
crates/tranquil-oauth/src/error.rs
+6
-6
src/oauth/error.rs
crates/tranquil-oauth/src/error.rs
···
82
82
}
83
83
}
84
84
85
-
impl From<sqlx::Error> for OAuthError {
86
-
fn from(err: sqlx::Error) -> Self {
87
-
tracing::error!("Database error in OAuth flow: {}", err);
85
+
impl From<anyhow::Error> for OAuthError {
86
+
fn from(err: anyhow::Error) -> Self {
87
+
tracing::error!("Internal error in OAuth flow: {}", err);
88
88
OAuthError::ServerError("An internal error occurred".to_string())
89
89
}
90
90
}
91
91
92
-
impl From<anyhow::Error> for OAuthError {
93
-
fn from(err: anyhow::Error) -> Self {
94
-
tracing::error!("Internal error in OAuth flow: {}", err);
92
+
impl From<sqlx::Error> for OAuthError {
93
+
fn from(err: sqlx::Error) -> Self {
94
+
tracing::error!("Database error in OAuth flow: {}", err);
95
95
OAuthError::ServerError("An internal error occurred".to_string())
96
96
}
97
97
}
src/oauth/jwks.rs
crates/tranquil-crypto/src/jwk.rs
src/oauth/jwks.rs
crates/tranquil-crypto/src/jwk.rs
-16
src/oauth/mod.rs
-16
src/oauth/mod.rs
···
1
-
pub mod client;
2
-
pub mod db;
3
-
pub mod dpop;
4
-
pub mod endpoints;
5
-
pub mod error;
6
-
pub mod jwks;
7
-
pub mod scopes;
8
-
pub mod types;
9
-
pub mod verify;
10
-
11
-
pub use error::OAuthError;
12
-
pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions};
13
-
pub use types::*;
14
-
pub use verify::{
15
-
OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token,
16
-
};
src/oauth/scopes/definitions.rs
crates/tranquil-scopes/src/definitions.rs
src/oauth/scopes/definitions.rs
crates/tranquil-scopes/src/definitions.rs
src/oauth/scopes/error.rs
crates/tranquil-scopes/src/error.rs
src/oauth/scopes/error.rs
crates/tranquil-scopes/src/error.rs
+4
-1
src/oauth/scopes/mod.rs
crates/tranquil-scopes/src/lib.rs
+4
-1
src/oauth/scopes/mod.rs
crates/tranquil-scopes/src/lib.rs
···
4
4
mod permission_set;
5
5
mod permissions;
6
6
7
-
pub use definitions::{SCOPE_DEFINITIONS, ScopeCategory, ScopeDefinition};
7
+
pub use definitions::{
8
+
SCOPE_DEFINITIONS, ScopeCategory, ScopeDefinition, format_scope_for_display,
9
+
get_required_scopes, get_scope_definition, is_valid_scope,
10
+
};
8
11
pub use error::ScopeError;
9
12
pub use parser::{
10
13
AccountAction, AccountAttr, AccountScope, BlobScope, IdentityAttr, IdentityScope, IncludeScope,
src/oauth/scopes/parser.rs
crates/tranquil-scopes/src/parser.rs
src/oauth/scopes/parser.rs
crates/tranquil-scopes/src/parser.rs
src/oauth/scopes/permission_set.rs
crates/tranquil-scopes/src/permission_set.rs
src/oauth/scopes/permission_set.rs
crates/tranquil-scopes/src/permission_set.rs
src/oauth/scopes/permissions.rs
crates/tranquil-scopes/src/permissions.rs
src/oauth/scopes/permissions.rs
crates/tranquil-scopes/src/permissions.rs
src/oauth/types.rs
crates/tranquil-oauth/src/types.rs
src/oauth/types.rs
crates/tranquil-oauth/src/types.rs
+1
-2
src/oauth/verify.rs
crates/tranquil-pds/src/oauth/verify.rs
+1
-2
src/oauth/verify.rs
crates/tranquil-pds/src/oauth/verify.rs
···
11
11
use sqlx::PgPool;
12
12
use subtle::ConstantTimeEq;
13
13
14
-
use super::OAuthError;
15
14
use super::db;
16
-
use super::dpop::DPoPVerifier;
17
15
use super::scopes::ScopePermissions;
16
+
use super::{DPoPVerifier, OAuthError};
18
17
use crate::config::AuthConfig;
19
18
use crate::state::AppState;
20
19
src/plc/mod.rs
crates/tranquil-pds/src/plc/mod.rs
src/plc/mod.rs
crates/tranquil-pds/src/plc/mod.rs
src/rate_limit.rs
crates/tranquil-pds/src/rate_limit.rs
src/rate_limit.rs
crates/tranquil-pds/src/rate_limit.rs
-125
src/repo/mod.rs
-125
src/repo/mod.rs
···
1
-
use bytes::Bytes;
2
-
use cid::Cid;
3
-
use jacquard_repo::error::RepoError;
4
-
use jacquard_repo::repo::CommitData;
5
-
use jacquard_repo::storage::BlockStore;
6
-
use multihash::Multihash;
7
-
use sha2::{Digest, Sha256};
8
-
use sqlx::PgPool;
9
-
10
-
pub mod tracking;
11
-
12
-
#[derive(Clone)]
13
-
pub struct PostgresBlockStore {
14
-
pool: PgPool,
15
-
}
16
-
17
-
impl PostgresBlockStore {
18
-
pub fn new(pool: PgPool) -> Self {
19
-
Self { pool }
20
-
}
21
-
}
22
-
23
-
impl BlockStore for PostgresBlockStore {
24
-
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
25
-
crate::metrics::record_block_operation("get");
26
-
let cid_bytes = cid.to_bytes();
27
-
let row = sqlx::query!("SELECT data FROM blocks WHERE cid = $1", &cid_bytes)
28
-
.fetch_optional(&self.pool)
29
-
.await
30
-
.map_err(RepoError::storage)?;
31
-
match row {
32
-
Some(row) => Ok(Some(Bytes::from(row.data))),
33
-
None => Ok(None),
34
-
}
35
-
}
36
-
37
-
async fn put(&self, data: &[u8]) -> Result<Cid, RepoError> {
38
-
crate::metrics::record_block_operation("put");
39
-
let mut hasher = Sha256::new();
40
-
hasher.update(data);
41
-
let hash = hasher.finalize();
42
-
let multihash = Multihash::wrap(0x12, &hash).map_err(|e| {
43
-
RepoError::storage(std::io::Error::new(
44
-
std::io::ErrorKind::InvalidData,
45
-
format!("Failed to wrap multihash: {:?}", e),
46
-
))
47
-
})?;
48
-
let cid = Cid::new_v1(0x71, multihash);
49
-
let cid_bytes = cid.to_bytes();
50
-
sqlx::query!(
51
-
"INSERT INTO blocks (cid, data) VALUES ($1, $2) ON CONFLICT (cid) DO NOTHING",
52
-
&cid_bytes,
53
-
data
54
-
)
55
-
.execute(&self.pool)
56
-
.await
57
-
.map_err(RepoError::storage)?;
58
-
Ok(cid)
59
-
}
60
-
61
-
async fn has(&self, cid: &Cid) -> Result<bool, RepoError> {
62
-
crate::metrics::record_block_operation("has");
63
-
let cid_bytes = cid.to_bytes();
64
-
let row = sqlx::query!("SELECT 1 as one FROM blocks WHERE cid = $1", &cid_bytes)
65
-
.fetch_optional(&self.pool)
66
-
.await
67
-
.map_err(RepoError::storage)?;
68
-
Ok(row.is_some())
69
-
}
70
-
71
-
async fn put_many(
72
-
&self,
73
-
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
74
-
) -> Result<(), RepoError> {
75
-
let blocks: Vec<_> = blocks.into_iter().collect();
76
-
if blocks.is_empty() {
77
-
return Ok(());
78
-
}
79
-
crate::metrics::record_block_operation("put_many");
80
-
let cids: Vec<Vec<u8>> = blocks.iter().map(|(cid, _)| cid.to_bytes()).collect();
81
-
let data: Vec<&[u8]> = blocks.iter().map(|(_, d)| d.as_ref()).collect();
82
-
sqlx::query!(
83
-
r#"
84
-
INSERT INTO blocks (cid, data)
85
-
SELECT * FROM UNNEST($1::bytea[], $2::bytea[])
86
-
ON CONFLICT (cid) DO NOTHING
87
-
"#,
88
-
&cids,
89
-
&data as &[&[u8]]
90
-
)
91
-
.execute(&self.pool)
92
-
.await
93
-
.map_err(RepoError::storage)?;
94
-
Ok(())
95
-
}
96
-
97
-
async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Bytes>>, RepoError> {
98
-
if cids.is_empty() {
99
-
return Ok(Vec::new());
100
-
}
101
-
crate::metrics::record_block_operation("get_many");
102
-
let cid_bytes: Vec<Vec<u8>> = cids.iter().map(|c| c.to_bytes()).collect();
103
-
let rows = sqlx::query!(
104
-
"SELECT cid, data FROM blocks WHERE cid = ANY($1)",
105
-
&cid_bytes
106
-
)
107
-
.fetch_all(&self.pool)
108
-
.await
109
-
.map_err(RepoError::storage)?;
110
-
let found: std::collections::HashMap<Vec<u8>, Bytes> = rows
111
-
.into_iter()
112
-
.map(|row| (row.cid, Bytes::from(row.data)))
113
-
.collect();
114
-
let results = cid_bytes
115
-
.iter()
116
-
.map(|cid| found.get(cid).cloned())
117
-
.collect();
118
-
Ok(results)
119
-
}
120
-
121
-
async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> {
122
-
self.put_many(commit.blocks).await?;
123
-
Ok(())
124
-
}
125
-
}
-113
src/repo/tracking.rs
-113
src/repo/tracking.rs
···
1
-
use crate::repo::PostgresBlockStore;
2
-
use bytes::Bytes;
3
-
use cid::Cid;
4
-
use jacquard_repo::error::RepoError;
5
-
use jacquard_repo::repo::CommitData;
6
-
use jacquard_repo::storage::BlockStore;
7
-
use std::collections::HashSet;
8
-
use std::sync::{Arc, Mutex};
9
-
10
-
#[derive(Clone)]
11
-
pub struct TrackingBlockStore {
12
-
inner: PostgresBlockStore,
13
-
written_cids: Arc<Mutex<Vec<Cid>>>,
14
-
read_cids: Arc<Mutex<HashSet<Cid>>>,
15
-
}
16
-
17
-
impl TrackingBlockStore {
18
-
pub fn new(store: PostgresBlockStore) -> Self {
19
-
Self {
20
-
inner: store,
21
-
written_cids: Arc::new(Mutex::new(Vec::new())),
22
-
read_cids: Arc::new(Mutex::new(HashSet::new())),
23
-
}
24
-
}
25
-
26
-
pub fn get_written_cids(&self) -> Vec<Cid> {
27
-
match self.written_cids.lock() {
28
-
Ok(guard) => guard.clone(),
29
-
Err(poisoned) => poisoned.into_inner().clone(),
30
-
}
31
-
}
32
-
33
-
pub fn get_read_cids(&self) -> Vec<Cid> {
34
-
match self.read_cids.lock() {
35
-
Ok(guard) => guard.iter().cloned().collect(),
36
-
Err(poisoned) => poisoned.into_inner().iter().cloned().collect(),
37
-
}
38
-
}
39
-
40
-
pub fn get_all_relevant_cids(&self) -> Vec<Cid> {
41
-
let written = self.get_written_cids();
42
-
let read = self.get_read_cids();
43
-
let mut all: HashSet<Cid> = written.into_iter().collect();
44
-
all.extend(read);
45
-
all.into_iter().collect()
46
-
}
47
-
}
48
-
49
-
impl BlockStore for TrackingBlockStore {
50
-
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
51
-
let result = self.inner.get(cid).await?;
52
-
if result.is_some() {
53
-
match self.read_cids.lock() {
54
-
Ok(mut guard) => {
55
-
guard.insert(*cid);
56
-
}
57
-
Err(poisoned) => {
58
-
poisoned.into_inner().insert(*cid);
59
-
}
60
-
}
61
-
}
62
-
Ok(result)
63
-
}
64
-
65
-
async fn put(&self, data: &[u8]) -> Result<Cid, RepoError> {
66
-
let cid = self.inner.put(data).await?;
67
-
match self.written_cids.lock() {
68
-
Ok(mut guard) => guard.push(cid),
69
-
Err(poisoned) => poisoned.into_inner().push(cid),
70
-
}
71
-
Ok(cid)
72
-
}
73
-
74
-
async fn has(&self, cid: &Cid) -> Result<bool, RepoError> {
75
-
self.inner.has(cid).await
76
-
}
77
-
78
-
async fn put_many(
79
-
&self,
80
-
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
81
-
) -> Result<(), RepoError> {
82
-
let blocks: Vec<_> = blocks.into_iter().collect();
83
-
let cids: Vec<Cid> = blocks.iter().map(|(cid, _)| *cid).collect();
84
-
self.inner.put_many(blocks).await?;
85
-
match self.written_cids.lock() {
86
-
Ok(mut guard) => guard.extend(cids),
87
-
Err(poisoned) => poisoned.into_inner().extend(cids),
88
-
}
89
-
Ok(())
90
-
}
91
-
92
-
async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Bytes>>, RepoError> {
93
-
let results = self.inner.get_many(cids).await?;
94
-
for (cid, result) in cids.iter().zip(results.iter()) {
95
-
if result.is_some() {
96
-
match self.read_cids.lock() {
97
-
Ok(mut guard) => {
98
-
guard.insert(*cid);
99
-
}
100
-
Err(poisoned) => {
101
-
poisoned.into_inner().insert(*cid);
102
-
}
103
-
}
104
-
}
105
-
}
106
-
Ok(results)
107
-
}
108
-
109
-
async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> {
110
-
self.put_many(commit.blocks).await?;
111
-
Ok(())
112
-
}
113
-
}
+2
-2
src/scheduled.rs
crates/tranquil-pds/src/scheduled.rs
+2
-2
src/scheduled.rs
crates/tranquil-pds/src/scheduled.rs
···
816
816
.map_err(|e| format!("Failed to fetch repo: {}", e))?
817
817
.ok_or_else(|| "Repository not found".to_string())?;
818
818
819
-
let actual_head_cid = Cid::from_str(&repo_root_cid_str)
820
-
.map_err(|e| format!("Invalid repo_root_cid: {}", e))?;
819
+
let actual_head_cid =
820
+
Cid::from_str(&repo_root_cid_str).map_err(|e| format!("Invalid repo_root_cid: {}", e))?;
821
821
822
822
generate_repo_car(block_store, &actual_head_cid).await
823
823
}
src/state.rs
crates/tranquil-pds/src/state.rs
src/state.rs
crates/tranquil-pds/src/state.rs
+19
-90
src/storage/mod.rs
crates/tranquil-storage/src/lib.rs
+19
-90
src/storage/mod.rs
crates/tranquil-storage/src/lib.rs
···
1
+
pub use tranquil_infra::{BlobStorage, StorageError, StreamUploadResult};
2
+
1
3
use async_trait::async_trait;
2
4
use aws_config::BehaviorVersion;
3
5
use aws_config::meta::region::RegionProviderChain;
···
9
11
use futures::Stream;
10
12
use sha2::{Digest, Sha256};
11
13
use std::pin::Pin;
12
-
use thiserror::Error;
13
14
14
15
const MIN_PART_SIZE: usize = 5 * 1024 * 1024;
15
16
16
-
#[derive(Error, Debug)]
17
-
pub enum StorageError {
18
-
#[error("IO error: {0}")]
19
-
Io(#[from] std::io::Error),
20
-
#[error("S3 error: {0}")]
21
-
S3(String),
22
-
#[error("Other: {0}")]
23
-
Other(String),
24
-
}
25
-
26
-
pub struct StreamUploadResult {
27
-
pub sha256_hash: [u8; 32],
28
-
pub size: u64,
29
-
}
30
-
31
-
#[async_trait]
32
-
pub trait BlobStorage: Send + Sync {
33
-
async fn put(&self, key: &str, data: &[u8]) -> Result<(), StorageError>;
34
-
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError>;
35
-
async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;
36
-
async fn get_bytes(&self, key: &str) -> Result<Bytes, StorageError>;
37
-
async fn get_head(&self, key: &str, size: usize) -> Result<Bytes, StorageError>;
38
-
async fn delete(&self, key: &str) -> Result<(), StorageError>;
39
-
async fn put_stream(
40
-
&self,
41
-
key: &str,
42
-
stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
43
-
) -> Result<StreamUploadResult, StorageError>;
44
-
async fn copy(&self, src_key: &str, dst_key: &str) -> Result<(), StorageError>;
45
-
}
46
-
47
17
pub struct S3BlobStorage {
48
18
client: Client,
49
19
bucket: String,
···
52
22
impl S3BlobStorage {
53
23
pub async fn new() -> Self {
54
24
let bucket = std::env::var("S3_BUCKET").expect("S3_BUCKET must be set");
25
+
let client = create_s3_client().await;
26
+
Self { client, bucket }
27
+
}
28
+
29
+
pub async fn with_bucket(bucket: String) -> Self {
55
30
let client = create_s3_client().await;
56
31
Self { client, bucket }
57
32
}
···
124
99
.body(ByteStream::from(Bytes::copy_from_slice(data)))
125
100
.send()
126
101
.await
127
-
.map_err(|e| {
128
-
crate::metrics::record_s3_operation("backup_put", "error");
129
-
StorageError::S3(e.to_string())
130
-
})?;
102
+
.map_err(|e| StorageError::S3(e.to_string()))?;
131
103
132
-
crate::metrics::record_s3_operation("backup_put", "success");
133
104
Ok(key)
134
105
}
135
106
···
141
112
.key(storage_key)
142
113
.send()
143
114
.await
144
-
.map_err(|e| {
145
-
crate::metrics::record_s3_operation("backup_get", "error");
146
-
StorageError::S3(e.to_string())
147
-
})?;
115
+
.map_err(|e| StorageError::S3(e.to_string()))?;
148
116
149
117
let data = resp
150
118
.body
151
119
.collect()
152
120
.await
153
-
.map_err(|e| {
154
-
crate::metrics::record_s3_operation("backup_get", "error");
155
-
StorageError::S3(e.to_string())
156
-
})?
121
+
.map_err(|e| StorageError::S3(e.to_string()))?
157
122
.into_bytes();
158
123
159
-
crate::metrics::record_s3_operation("backup_get", "success");
160
124
Ok(data)
161
125
}
162
126
···
167
131
.key(storage_key)
168
132
.send()
169
133
.await
170
-
.map_err(|e| {
171
-
crate::metrics::record_s3_operation("backup_delete", "error");
172
-
StorageError::S3(e.to_string())
173
-
})?;
134
+
.map_err(|e| StorageError::S3(e.to_string()))?;
174
135
175
-
crate::metrics::record_s3_operation("backup_delete", "success");
176
136
Ok(())
177
137
}
178
138
}
···
184
144
}
185
145
186
146
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError> {
187
-
let result = self
188
-
.client
147
+
self.client
189
148
.put_object()
190
149
.bucket(&self.bucket)
191
150
.key(key)
192
151
.body(ByteStream::from(data))
193
152
.send()
194
153
.await
195
-
.map_err(|e| StorageError::S3(e.to_string()));
154
+
.map_err(|e| StorageError::S3(e.to_string()))?;
196
155
197
-
match &result {
198
-
Ok(_) => crate::metrics::record_s3_operation("put", "success"),
199
-
Err(_) => crate::metrics::record_s3_operation("put", "error"),
200
-
}
201
-
202
-
result?;
203
156
Ok(())
204
157
}
205
158
···
215
168
.key(key)
216
169
.send()
217
170
.await
218
-
.map_err(|e| {
219
-
crate::metrics::record_s3_operation("get", "error");
220
-
StorageError::S3(e.to_string())
221
-
})?;
171
+
.map_err(|e| StorageError::S3(e.to_string()))?;
222
172
223
173
let data = resp
224
174
.body
225
175
.collect()
226
176
.await
227
-
.map_err(|e| {
228
-
crate::metrics::record_s3_operation("get", "error");
229
-
StorageError::S3(e.to_string())
230
-
})?
177
+
.map_err(|e| StorageError::S3(e.to_string()))?
231
178
.into_bytes();
232
179
233
-
crate::metrics::record_s3_operation("get", "success");
234
180
Ok(data)
235
181
}
236
182
···
244
190
.range(range)
245
191
.send()
246
192
.await
247
-
.map_err(|e| {
248
-
crate::metrics::record_s3_operation("get_head", "error");
249
-
StorageError::S3(e.to_string())
250
-
})?;
193
+
.map_err(|e| StorageError::S3(e.to_string()))?;
251
194
252
195
let data = resp
253
196
.body
254
197
.collect()
255
198
.await
256
-
.map_err(|e| {
257
-
crate::metrics::record_s3_operation("get_head", "error");
258
-
StorageError::S3(e.to_string())
259
-
})?
199
+
.map_err(|e| StorageError::S3(e.to_string()))?
260
200
.into_bytes();
261
201
262
-
crate::metrics::record_s3_operation("get_head", "success");
263
202
Ok(data)
264
203
}
265
204
266
205
async fn delete(&self, key: &str) -> Result<(), StorageError> {
267
-
let result = self
268
-
.client
206
+
self.client
269
207
.delete_object()
270
208
.bucket(&self.bucket)
271
209
.key(key)
272
210
.send()
273
211
.await
274
-
.map_err(|e| StorageError::S3(e.to_string()));
275
-
276
-
match &result {
277
-
Ok(_) => crate::metrics::record_s3_operation("delete", "success"),
278
-
Err(_) => crate::metrics::record_s3_operation("delete", "error"),
279
-
}
212
+
.map_err(|e| StorageError::S3(e.to_string()))?;
280
213
281
-
result?;
282
214
Ok(())
283
215
}
284
216
···
423
355
.await
424
356
.map_err(|e| StorageError::S3(format!("Failed to complete multipart upload: {}", e)))?;
425
357
426
-
crate::metrics::record_s3_operation("put_stream", "success");
427
-
428
358
let hash: [u8; 32] = hasher.finalize().into();
429
359
Ok(StreamUploadResult {
430
360
sha256_hash: hash,
···
444
374
.await
445
375
.map_err(|e| StorageError::S3(format!("Failed to copy object: {}", e)))?;
446
376
447
-
crate::metrics::record_s3_operation("copy", "success");
448
377
Ok(())
449
378
}
450
379
}
src/sync/blob.rs
crates/tranquil-pds/src/sync/blob.rs
src/sync/blob.rs
crates/tranquil-pds/src/sync/blob.rs
src/sync/car.rs
crates/tranquil-pds/src/sync/car.rs
src/sync/car.rs
crates/tranquil-pds/src/sync/car.rs
src/sync/commit.rs
crates/tranquil-pds/src/sync/commit.rs
src/sync/commit.rs
crates/tranquil-pds/src/sync/commit.rs
src/sync/crawl.rs
crates/tranquil-pds/src/sync/crawl.rs
src/sync/crawl.rs
crates/tranquil-pds/src/sync/crawl.rs
src/sync/deprecated.rs
crates/tranquil-pds/src/sync/deprecated.rs
src/sync/deprecated.rs
crates/tranquil-pds/src/sync/deprecated.rs
src/sync/firehose.rs
crates/tranquil-pds/src/sync/firehose.rs
src/sync/firehose.rs
crates/tranquil-pds/src/sync/firehose.rs
src/sync/frame.rs
crates/tranquil-pds/src/sync/frame.rs
src/sync/frame.rs
crates/tranquil-pds/src/sync/frame.rs
src/sync/import.rs
crates/tranquil-pds/src/sync/import.rs
src/sync/import.rs
crates/tranquil-pds/src/sync/import.rs
src/sync/listener.rs
crates/tranquil-pds/src/sync/listener.rs
src/sync/listener.rs
crates/tranquil-pds/src/sync/listener.rs
src/sync/mod.rs
crates/tranquil-pds/src/sync/mod.rs
src/sync/mod.rs
crates/tranquil-pds/src/sync/mod.rs
src/sync/repo.rs
crates/tranquil-pds/src/sync/repo.rs
src/sync/repo.rs
crates/tranquil-pds/src/sync/repo.rs
src/sync/subscribe_repos.rs
crates/tranquil-pds/src/sync/subscribe_repos.rs
src/sync/subscribe_repos.rs
crates/tranquil-pds/src/sync/subscribe_repos.rs
src/sync/util.rs
crates/tranquil-pds/src/sync/util.rs
src/sync/util.rs
crates/tranquil-pds/src/sync/util.rs
src/sync/verify.rs
crates/tranquil-pds/src/sync/verify.rs
src/sync/verify.rs
crates/tranquil-pds/src/sync/verify.rs
src/sync/verify_tests.rs
crates/tranquil-pds/src/sync/verify_tests.rs
src/sync/verify_tests.rs
crates/tranquil-pds/src/sync/verify_tests.rs
+20
-110
src/types.rs
crates/tranquil-types/src/lib.rs
+20
-110
src/types.rs
crates/tranquil-types/src/lib.rs
···
123
123
}
124
124
}
125
125
126
-
#[derive(Debug, Clone)]
126
+
#[derive(Debug, Clone, thiserror::Error)]
127
127
pub enum DidError {
128
+
#[error("invalid DID: {0}")]
128
129
Invalid(String),
129
130
}
130
-
131
-
impl fmt::Display for DidError {
132
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133
-
match self {
134
-
Self::Invalid(s) => write!(f, "invalid DID: {}", s),
135
-
}
136
-
}
137
-
}
138
-
139
-
impl std::error::Error for DidError {}
140
131
141
132
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
142
133
#[serde(transparent)]
···
239
230
}
240
231
}
241
232
242
-
#[derive(Debug, Clone)]
233
+
#[derive(Debug, Clone, thiserror::Error)]
243
234
pub enum HandleError {
235
+
#[error("invalid handle: {0}")]
244
236
Invalid(String),
245
237
}
246
238
247
-
impl fmt::Display for HandleError {
248
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249
-
match self {
250
-
Self::Invalid(s) => write!(f, "invalid handle: {}", s),
251
-
}
252
-
}
253
-
}
254
-
255
-
impl std::error::Error for HandleError {}
256
-
257
239
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258
240
pub enum AtIdentifier {
259
241
Did(Did),
···
370
352
}
371
353
}
372
354
373
-
#[derive(Debug, Clone)]
355
+
#[derive(Debug, Clone, thiserror::Error)]
374
356
pub enum AtIdentifierError {
357
+
#[error("invalid AT identifier: {0}")]
375
358
Invalid(String),
376
359
}
377
360
378
-
impl fmt::Display for AtIdentifierError {
379
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380
-
match self {
381
-
Self::Invalid(s) => write!(f, "invalid AT identifier: {}", s),
382
-
}
383
-
}
384
-
}
385
-
386
-
impl std::error::Error for AtIdentifierError {}
387
-
388
361
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
389
362
#[serde(transparent)]
390
363
#[sqlx(type_name = "rkey")]
···
495
468
}
496
469
}
497
470
498
-
#[derive(Debug, Clone)]
471
+
#[derive(Debug, Clone, thiserror::Error)]
499
472
pub enum RkeyError {
473
+
#[error("invalid rkey: {0}")]
500
474
Invalid(String),
501
475
}
502
476
503
-
impl fmt::Display for RkeyError {
504
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505
-
match self {
506
-
Self::Invalid(s) => write!(f, "invalid rkey: {}", s),
507
-
}
508
-
}
509
-
}
510
-
511
-
impl std::error::Error for RkeyError {}
512
-
513
477
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
514
478
#[serde(transparent)]
515
479
#[sqlx(type_name = "nsid")]
···
624
588
}
625
589
}
626
590
627
-
#[derive(Debug, Clone)]
591
+
#[derive(Debug, Clone, thiserror::Error)]
628
592
pub enum NsidError {
593
+
#[error("invalid NSID: {0}")]
629
594
Invalid(String),
630
595
}
631
-
632
-
impl fmt::Display for NsidError {
633
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634
-
match self {
635
-
Self::Invalid(s) => write!(f, "invalid NSID: {}", s),
636
-
}
637
-
}
638
-
}
639
-
640
-
impl std::error::Error for NsidError {}
641
596
642
597
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
643
598
#[serde(transparent)]
···
744
699
}
745
700
}
746
701
747
-
#[derive(Debug, Clone)]
702
+
#[derive(Debug, Clone, thiserror::Error)]
748
703
pub enum AtUriError {
704
+
#[error("invalid AT URI: {0}")]
749
705
Invalid(String),
750
706
}
751
-
752
-
impl fmt::Display for AtUriError {
753
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754
-
match self {
755
-
Self::Invalid(s) => write!(f, "invalid AT URI: {}", s),
756
-
}
757
-
}
758
-
}
759
-
760
-
impl std::error::Error for AtUriError {}
761
707
762
708
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
763
709
#[serde(transparent)]
···
865
811
}
866
812
}
867
813
868
-
#[derive(Debug, Clone)]
814
+
#[derive(Debug, Clone, thiserror::Error)]
869
815
pub enum TidError {
816
+
#[error("invalid TID: {0}")]
870
817
Invalid(String),
871
818
}
872
819
873
-
impl fmt::Display for TidError {
874
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
875
-
match self {
876
-
Self::Invalid(s) => write!(f, "invalid TID: {}", s),
877
-
}
878
-
}
879
-
}
880
-
881
-
impl std::error::Error for TidError {}
882
-
883
820
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
884
821
#[serde(transparent)]
885
822
#[sqlx(transparent)]
···
990
927
}
991
928
}
992
929
993
-
#[derive(Debug, Clone)]
930
+
#[derive(Debug, Clone, thiserror::Error)]
994
931
pub enum DatetimeError {
932
+
#[error("invalid datetime: {0}")]
995
933
Invalid(String),
996
934
}
997
935
998
-
impl fmt::Display for DatetimeError {
999
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1000
-
match self {
1001
-
Self::Invalid(s) => write!(f, "invalid datetime: {}", s),
1002
-
}
1003
-
}
1004
-
}
1005
-
1006
-
impl std::error::Error for DatetimeError {}
1007
-
1008
936
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
1009
937
#[serde(transparent)]
1010
938
#[sqlx(transparent)]
···
1107
1035
}
1108
1036
}
1109
1037
1110
-
#[derive(Debug, Clone)]
1038
+
#[derive(Debug, Clone, thiserror::Error)]
1111
1039
pub enum LanguageError {
1040
+
#[error("invalid language tag: {0}")]
1112
1041
Invalid(String),
1113
1042
}
1114
1043
1115
-
impl fmt::Display for LanguageError {
1116
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1117
-
match self {
1118
-
Self::Invalid(s) => write!(f, "invalid language tag: {}", s),
1119
-
}
1120
-
}
1121
-
}
1122
-
1123
-
impl std::error::Error for LanguageError {}
1124
-
1125
1044
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
1126
1045
#[serde(transparent)]
1127
1046
#[sqlx(transparent)]
···
1227
1146
}
1228
1147
}
1229
1148
1230
-
#[derive(Debug, Clone)]
1149
+
#[derive(Debug, Clone, thiserror::Error)]
1231
1150
pub enum CidLinkError {
1151
+
#[error("invalid CID: {0}")]
1232
1152
Invalid(String),
1233
1153
}
1234
-
1235
-
impl fmt::Display for CidLinkError {
1236
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1237
-
match self {
1238
-
Self::Invalid(s) => write!(f, "invalid CID: {}", s),
1239
-
}
1240
-
}
1241
-
}
1242
-
1243
-
impl std::error::Error for CidLinkError {}
1244
1154
1245
1155
#[derive(Debug, Clone, PartialEq, Eq)]
1246
1156
pub enum AccountState {
src/util.rs
crates/tranquil-pds/src/util.rs
src/util.rs
crates/tranquil-pds/src/util.rs
src/validation/mod.rs
crates/tranquil-pds/src/validation/mod.rs
src/validation/mod.rs
crates/tranquil-pds/src/validation/mod.rs
tests/account_lifecycle.rs
crates/tranquil-pds/tests/account_lifecycle.rs
tests/account_lifecycle.rs
crates/tranquil-pds/tests/account_lifecycle.rs
tests/account_notifications.rs
crates/tranquil-pds/tests/account_notifications.rs
tests/account_notifications.rs
crates/tranquil-pds/tests/account_notifications.rs
tests/actor.rs
crates/tranquil-pds/tests/actor.rs
tests/actor.rs
crates/tranquil-pds/tests/actor.rs
tests/admin_email.rs
crates/tranquil-pds/tests/admin_email.rs
tests/admin_email.rs
crates/tranquil-pds/tests/admin_email.rs
tests/admin_invite.rs
crates/tranquil-pds/tests/admin_invite.rs
tests/admin_invite.rs
crates/tranquil-pds/tests/admin_invite.rs
tests/admin_moderation.rs
crates/tranquil-pds/tests/admin_moderation.rs
tests/admin_moderation.rs
crates/tranquil-pds/tests/admin_moderation.rs
tests/admin_search.rs
crates/tranquil-pds/tests/admin_search.rs
tests/admin_search.rs
crates/tranquil-pds/tests/admin_search.rs
tests/admin_stats.rs
crates/tranquil-pds/tests/admin_stats.rs
tests/admin_stats.rs
crates/tranquil-pds/tests/admin_stats.rs
tests/backup.rs
crates/tranquil-pds/tests/backup.rs
tests/backup.rs
crates/tranquil-pds/tests/backup.rs
tests/banned_words.rs
crates/tranquil-pds/tests/banned_words.rs
tests/banned_words.rs
crates/tranquil-pds/tests/banned_words.rs
tests/change_password.rs
crates/tranquil-pds/tests/change_password.rs
tests/change_password.rs
crates/tranquil-pds/tests/change_password.rs
tests/commit_signing.rs
crates/tranquil-pds/tests/commit_signing.rs
tests/commit_signing.rs
crates/tranquil-pds/tests/commit_signing.rs
tests/common/mod.rs
crates/tranquil-pds/tests/common/mod.rs
tests/common/mod.rs
crates/tranquil-pds/tests/common/mod.rs
tests/delete_account.rs
crates/tranquil-pds/tests/delete_account.rs
tests/delete_account.rs
crates/tranquil-pds/tests/delete_account.rs
tests/did_web.rs
crates/tranquil-pds/tests/did_web.rs
tests/did_web.rs
crates/tranquil-pds/tests/did_web.rs
+1
-1
tests/dpop_unit.rs
crates/tranquil-pds/tests/dpop_unit.rs
+1
-1
tests/dpop_unit.rs
crates/tranquil-pds/tests/dpop_unit.rs
tests/email_update.rs
crates/tranquil-pds/tests/email_update.rs
tests/email_update.rs
crates/tranquil-pds/tests/email_update.rs
tests/firehose_validation.rs
crates/tranquil-pds/tests/firehose_validation.rs
tests/firehose_validation.rs
crates/tranquil-pds/tests/firehose_validation.rs
tests/helpers/mod.rs
crates/tranquil-pds/tests/helpers/mod.rs
tests/helpers/mod.rs
crates/tranquil-pds/tests/helpers/mod.rs
tests/identity.rs
crates/tranquil-pds/tests/identity.rs
tests/identity.rs
crates/tranquil-pds/tests/identity.rs
tests/image_processing.rs
crates/tranquil-pds/tests/image_processing.rs
tests/image_processing.rs
crates/tranquil-pds/tests/image_processing.rs
tests/import_verification.rs
crates/tranquil-pds/tests/import_verification.rs
tests/import_verification.rs
crates/tranquil-pds/tests/import_verification.rs
tests/import_with_verification.rs
crates/tranquil-pds/tests/import_with_verification.rs
tests/import_with_verification.rs
crates/tranquil-pds/tests/import_with_verification.rs
tests/invite.rs
crates/tranquil-pds/tests/invite.rs
tests/invite.rs
crates/tranquil-pds/tests/invite.rs
tests/jwt_security.rs
crates/tranquil-pds/tests/jwt_security.rs
tests/jwt_security.rs
crates/tranquil-pds/tests/jwt_security.rs
tests/lifecycle_record.rs
crates/tranquil-pds/tests/lifecycle_record.rs
tests/lifecycle_record.rs
crates/tranquil-pds/tests/lifecycle_record.rs
tests/lifecycle_session.rs
crates/tranquil-pds/tests/lifecycle_session.rs
tests/lifecycle_session.rs
crates/tranquil-pds/tests/lifecycle_session.rs
tests/moderation.rs
crates/tranquil-pds/tests/moderation.rs
tests/moderation.rs
crates/tranquil-pds/tests/moderation.rs
tests/notifications.rs
crates/tranquil-pds/tests/notifications.rs
tests/notifications.rs
crates/tranquil-pds/tests/notifications.rs
tests/oauth.rs
crates/tranquil-pds/tests/oauth.rs
tests/oauth.rs
crates/tranquil-pds/tests/oauth.rs
tests/oauth_client_metadata.rs
crates/tranquil-pds/tests/oauth_client_metadata.rs
tests/oauth_client_metadata.rs
crates/tranquil-pds/tests/oauth_client_metadata.rs
tests/oauth_lifecycle.rs
crates/tranquil-pds/tests/oauth_lifecycle.rs
tests/oauth_lifecycle.rs
crates/tranquil-pds/tests/oauth_lifecycle.rs
tests/oauth_scopes.rs
crates/tranquil-pds/tests/oauth_scopes.rs
tests/oauth_scopes.rs
crates/tranquil-pds/tests/oauth_scopes.rs
+1
-1
tests/oauth_security.rs
crates/tranquil-pds/tests/oauth_security.rs
+1
-1
tests/oauth_security.rs
crates/tranquil-pds/tests/oauth_security.rs
···
8
8
use reqwest::StatusCode;
9
9
use serde_json::{Value, json};
10
10
use sha2::{Digest, Sha256};
11
-
use tranquil_pds::oauth::dpop::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint};
11
+
use tranquil_pds::oauth::{DPoPJwk, DPoPVerifier, compute_jwk_thumbprint};
12
12
use wiremock::matchers::{method, path};
13
13
use wiremock::{Mock, MockServer, ResponseTemplate};
14
14
tests/password_reset.rs
crates/tranquil-pds/tests/password_reset.rs
tests/password_reset.rs
crates/tranquil-pds/tests/password_reset.rs
tests/plc_migration.rs
crates/tranquil-pds/tests/plc_migration.rs
tests/plc_migration.rs
crates/tranquil-pds/tests/plc_migration.rs
tests/plc_operations.rs
crates/tranquil-pds/tests/plc_operations.rs
tests/plc_operations.rs
crates/tranquil-pds/tests/plc_operations.rs
tests/plc_validation.rs
crates/tranquil-pds/tests/plc_validation.rs
tests/plc_validation.rs
crates/tranquil-pds/tests/plc_validation.rs
tests/rate_limit.rs
crates/tranquil-pds/tests/rate_limit.rs
tests/rate_limit.rs
crates/tranquil-pds/tests/rate_limit.rs
tests/record_validation.rs
crates/tranquil-pds/tests/record_validation.rs
tests/record_validation.rs
crates/tranquil-pds/tests/record_validation.rs
tests/repo_batch.rs
crates/tranquil-pds/tests/repo_batch.rs
tests/repo_batch.rs
crates/tranquil-pds/tests/repo_batch.rs
tests/repo_blob.rs
crates/tranquil-pds/tests/repo_blob.rs
tests/repo_blob.rs
crates/tranquil-pds/tests/repo_blob.rs
tests/repo_conformance.rs
crates/tranquil-pds/tests/repo_conformance.rs
tests/repo_conformance.rs
crates/tranquil-pds/tests/repo_conformance.rs
tests/scope_edge_cases.rs
crates/tranquil-pds/tests/scope_edge_cases.rs
tests/scope_edge_cases.rs
crates/tranquil-pds/tests/scope_edge_cases.rs
tests/security_fixes.rs
crates/tranquil-pds/tests/security_fixes.rs
tests/security_fixes.rs
crates/tranquil-pds/tests/security_fixes.rs
tests/server.rs
crates/tranquil-pds/tests/server.rs
tests/server.rs
crates/tranquil-pds/tests/server.rs
tests/session_management.rs
crates/tranquil-pds/tests/session_management.rs
tests/session_management.rs
crates/tranquil-pds/tests/session_management.rs
tests/signing_key.rs
crates/tranquil-pds/tests/signing_key.rs
tests/signing_key.rs
crates/tranquil-pds/tests/signing_key.rs
tests/sync_blob.rs
crates/tranquil-pds/tests/sync_blob.rs
tests/sync_blob.rs
crates/tranquil-pds/tests/sync_blob.rs
tests/sync_conformance.rs
crates/tranquil-pds/tests/sync_conformance.rs
tests/sync_conformance.rs
crates/tranquil-pds/tests/sync_conformance.rs
tests/sync_deprecated.rs
crates/tranquil-pds/tests/sync_deprecated.rs
tests/sync_deprecated.rs
crates/tranquil-pds/tests/sync_deprecated.rs
tests/sync_repo.rs
crates/tranquil-pds/tests/sync_repo.rs
tests/sync_repo.rs
crates/tranquil-pds/tests/sync_repo.rs
tests/validation_edge_cases.rs
crates/tranquil-pds/tests/validation_edge_cases.rs
tests/validation_edge_cases.rs
crates/tranquil-pds/tests/validation_edge_cases.rs
tests/verify_live_commit.rs
crates/tranquil-pds/tests/verify_live_commit.rs
tests/verify_live_commit.rs
crates/tranquil-pds/tests/verify_live_commit.rs