Compare commits

..
17 Commits
Author SHA1 Message Date
LewisandTangled ebf5af4922 fix(backups): remove useless backups concept 2026-03-18 12:17:33 +00:00
LewisandTangled 938df6495d feat(frontend): add UI component test page 2026-03-18 10:43:03 +00:00
LewisandTangled 113bea7da3 refactor(frontend): extract migration component styles + migration.css 2026-03-18 10:43:03 +00:00
LewisandTangled da8b577408 refactor(frontend): extract auth and registration page styles + pages.css 2026-03-18 10:43:03 +00:00
LewisandTangled 6683d03a9e refactor(frontend): extract oauth secondary page styles 2026-03-18 10:43:03 +00:00
LewisandTangled 72f45bf6a3 refactor(frontend): extract oauth primary flow page styles 2026-03-18 10:43:03 +00:00
LewisandTangled 14bac09cb1 refactor(frontend): extract controllers, repo styles + dashboard.css 2026-03-18 10:43:02 +00:00
LewisandTangled 76dc3b125a refactor(frontend): extract admin, settings, app passwords styles 2026-03-18 10:43:02 +00:00
LewisandTangled c7d3dfbce0 refactor(frontend): extract security, sessions, DID document styles 2026-03-18 10:43:02 +00:00
LewisandTangled 154f9fae89 refactor(frontend): extract dashboard shell and lightweight component styles 2026-03-18 10:43:02 +00:00
LewisandTangled 331d843886 refactor(frontend): consolidate base stylesheet 2026-03-18 10:43:02 +00:00
LewisandTangled 79a3db8ff2 refactor(frontend): extract UI primitive component styles 2026-03-18 10:43:02 +00:00
LewisandTangled f95a1c7cfd feat(frontend): design tokens, app wiring, locales 2026-03-18 10:43:02 +00:00
LewisandTangled 60b4697b5a chore: inline delegation audit page 2026-03-17 19:22:34 +00:00
LewisandTangled 7926c798c6 feat: cross-pds delegation 2026-03-17 19:22:34 +00:00
LewisandTangled d5ed420dd7 fix: move code to more correct crates 2026-03-17 10:16:36 +00:00
Not HerselfandTangled 92e609d367 fix: handle AT Protocol $bytes type in json_to_ipld
json_to_ipld handled $link (→ Ipld::Link) but treated $bytes objects
as regular maps, producing a CBOR map (major type 5) instead of a byte
string (major type 2). This caused downstream consumers expecting
spec-compliant CBOR — notably Jetstream's atdata.UnmarshalCBOR — to
fail with "decoding $byte value: illegal base64 data".

Decode $bytes from standard base64 (RFC 4648 §4, padding optional)
into Ipld::Bytes, matching the existing $link handling pattern.
2026-03-15 07:15:07 +00:00
311 changed files with 10019 additions and 16065 deletions
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, backup_enabled FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "backup_enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "017b04caf42b30f2c8f9468acf61a83244b7c2fa5cacfaee41a946a6af5ef68e"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET deactivated_at = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Text"
]
},
"nullable": []
},
"hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107"
}
@@ -1,53 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT u.id, u.did, u.backup_enabled, u.deactivated_at, r.repo_root_cid, r.repo_rev\n FROM users u\n JOIN repos r ON r.user_id = u.id\n WHERE u.backup_enabled = true\n AND u.deactivated_at IS NULL\n AND (\n NOT EXISTS (\n SELECT 1 FROM account_backups ab WHERE ab.user_id = u.id\n )\n OR (\n SELECT MAX(ab.created_at) FROM account_backups ab WHERE ab.user_id = u.id\n ) < NOW() - make_interval(secs => $1)\n )\n LIMIT $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "backup_enabled",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "deactivated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "repo_root_cid",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "repo_rev",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Float8",
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
false,
true
]
},
"hash": "07c54d6bcfdc08d821e788c31dc46cc240dc8507c7dd5bdd2ceeb36d2084e8ce"
}
@@ -1,87 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n delegated_did,\n actor_did,\n controller_did,\n action_type as \"action_type: PgDelegationActionType\",\n action_details,\n ip_address,\n user_agent,\n created_at\n FROM delegation_audit_log\n WHERE controller_did = $1\n ORDER BY created_at DESC\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "delegated_did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "actor_did",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "controller_did",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "action_type: PgDelegationActionType",
"type_info": {
"Custom": {
"name": "delegation_action_type",
"kind": {
"Enum": [
"grant_created",
"grant_revoked",
"scopes_modified",
"token_issued",
"repo_write",
"blob_upload",
"account_action"
]
}
}
}
},
{
"ordinal": 5,
"name": "action_details",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "ip_address",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "user_agent",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
false,
true,
true,
true,
false
]
},
"hash": "0ce61cd393867522de318c1f86c787851ec7bbc31b147b9c2fb2bac9b2676ba6"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT ab.storage_key, ab.repo_rev\n FROM account_backups ab\n JOIN users u ON u.id = ab.user_id\n WHERE ab.id = $1 AND u.did = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "storage_key",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "repo_rev",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "0d70da6b077d6fbab5056c38490acbe772626a3447182b76b0cbcee15c5a46dc"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"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",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "body",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token FROM sso_pending_registration WHERE token = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, storage_key\n FROM account_backups\n WHERE user_id = $1\n ORDER BY created_at DESC\n OFFSET $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "storage_key",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid",
"Int8"
]
},
"nullable": [
false,
false
]
},
"hash": "120c30d563b3cc40a2bb670f0c38c4ad63360b4a4f8e20c21afbe60c6fb7087a"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": [
false
]
},
"hash": "2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
}
@@ -1,52 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT u.id, u.did, u.backup_enabled, u.deactivated_at, r.repo_root_cid, r.repo_rev\n FROM users u\n JOIN repos r ON r.user_id = u.id\n WHERE u.did = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "backup_enabled",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "deactivated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "repo_root_cid",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "repo_rev",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
true
]
},
"hash": "3043db19db2d0f51d1db6965a3ba0af4a1a4a76cc4ab1cad8395ba6babecc37d"
}
@@ -1,32 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM external_identities WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12"
}
@@ -1,55 +0,0 @@
{
"db_name": "PostgreSQL",
"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",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subject",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "body",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "comms_type: String",
"type_info": {
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
}
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
false,
false
]
},
"hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email_verified FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT token FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at)\n VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Jsonb"
]
},
"nullable": []
},
"hash": "575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15"
}
@@ -1,33 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e"
}
@@ -1,81 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id, provider_username, provider_email\n FROM external_identities\n WHERE provider = $1 AND provider_user_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "provider_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"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 ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "key_bytes",
"type_info": "Bytea"
},
{
"ordinal": 1,
"name": "encryption_version",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1"
}
@@ -1,43 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT provider_user_id, provider_email_verified\n FROM external_identities\n WHERE did = $1 AND provider = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "provider_email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
]
},
"nullable": [
false,
false
]
},
"hash": "5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06"
}
@@ -1,33 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, email_verified FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e"
}
@@ -1,66 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT state, request_uri, provider as \"provider: SsoProviderType\", action, nonce, code_verifier\n FROM sso_auth_state\n WHERE state = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "action",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "nonce",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "code_verifier",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee"
}
@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO account_backups (user_id, storage_key, repo_root_cid, repo_rev, block_count, size_bytes)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (storage_key) DO UPDATE SET created_at = NOW()\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Text",
"Int4",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT state FROM sso_auth_state WHERE state = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba"
}
@@ -1,33 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subject",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f"
}
@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT ab.id, ab.storage_key, u.deactivated_at\n FROM account_backups ab\n JOIN users u ON u.id = ab.user_id\n WHERE ab.id = $1 AND u.did = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "storage_key",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "deactivated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "78be3d837cfb54d5e915ff0c5b1aaedc104776a22ad28b637de574e58d052036"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"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 ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT provider_username, last_login_at FROM external_identities WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "last_login_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
true
]
},
"hash": "7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0"
}
@@ -1,46 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n u.did,\n u.handle,\n d.granted_scopes,\n d.granted_at,\n (u.deactivated_at IS NULL AND u.takedown_ref IS NULL) as \"is_active!\"\n FROM account_delegations d\n JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1 AND d.revoked_at IS NULL\n ORDER BY d.granted_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "granted_scopes",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "granted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "is_active!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
null
]
},
"hash": "7d43e7e917104d7bce29459149a345145361c6940e9b8e2ccc561a0a032f63c0"
}
@@ -1,52 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, repo_rev, repo_root_cid, block_count, size_bytes, created_at\n FROM account_backups\n WHERE user_id = $1\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "repo_rev",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "repo_root_cid",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "block_count",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "size_bytes",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "7ec9500e28b450ab569ec50f968f5f66b7e8e80b160db9734cb78f91fc9f2cc6"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"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",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"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",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "body",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "public_key_did_key",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
false
]
},
"hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id FROM external_identities WHERE did = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65"
}
@@ -1,66 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "provider_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "private_key_bytes",
"type_info": "Bytea"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE external_identities\n SET provider_username = $2, last_login_at = NOW()\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5"
}
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT preferred_comms_channel as \"preferred_comms_channel: String\", discord_username FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "preferred_comms_channel: String",
"type_info": {
"Custom": {
"name": "comms_channel",
"kind": {
"Enum": [
"email",
"discord",
"telegram",
"signal"
]
}
}
}
},
{
"ordinal": 1,
"name": "discord_username",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "a844774d8dd3c50c5faf3de5d43f534b80234759c8437434e467ca33ea10fd1f"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state, request_uri\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT b.cid, b.storage_key, b.mime_type\n FROM blobs b\n JOIN record_blobs rb ON rb.blob_cid = b.cid\n WHERE rb.repo_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "cid",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "storage_key",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "mime_type",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "b22003e74a58303beb993547a8e2bf2415152a5546d6a10c9aebc223420b0088"
}
@@ -1,31 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM sso_auth_state WHERE expires_at < NOW()",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM account_backups WHERE user_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "bb83d46014c20edc99ef7e81808087097e719771c6c8163e4e8e11a98bb97dde"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT deactivated_at FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "deactivated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "bf2b237ee5cfe66d038dce3f564c05a683d391aebda7871d65d302e04b5d733f"
}
@@ -1,46 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n u.did,\n u.handle,\n d.granted_scopes,\n d.granted_at,\n true as \"is_active!\"\n FROM account_delegations d\n JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND u.deactivated_at IS NULL\n AND u.takedown_ref IS NULL\n ORDER BY d.granted_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "handle",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "granted_scopes",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "granted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "is_active!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
null
]
},
"hash": "c69577bc8e88e2f2cc516d6bec3d201419b589b6088ceb933cb182265b14e363"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password_reset_code FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password_reset_code",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3"
}
@@ -1,31 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM account_delegations d\n JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND u.deactivated_at IS NULL\n AND u.takedown_ref IS NULL\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "d92bfa1e67c60c4fb333b6390e49224381c1dd23c3ef09aff3a6d59eadd13c64"
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n d.controller_did,\n u.handle as \"handle?\",\n d.granted_scopes,\n d.granted_at,\n CASE WHEN u.did IS NOT NULL\n THEN u.deactivated_at IS NULL AND u.takedown_ref IS NULL\n ELSE true\n END as \"is_active!\",\n u.did IS NOT NULL as \"is_local!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1 AND d.revoked_at IS NULL\n ORDER BY d.granted_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "controller_did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "handle?",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "granted_scopes",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "granted_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "is_active!",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "is_local!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
null,
null
]
},
"hash": "dd6021dd12823e042b011b2c1507736a46c0dcf0eb94cb41de58f8ca0b3a2f08"
}
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9"
}
@@ -1,30 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM external_identities WHERE id = $1 AND did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET is_admin = TRUE WHERE did = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET backup_enabled = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "f71428b1ce982504cd531937131d49196ec092b4d13e9ae7dcdaedfe98de5a70"
}
@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password_reset_code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "password_reset_code_expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM account_backups WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "f85f8d49bbd2d5e048bd8c29081aef5b8097e2384793e85df72eeeb858b7c532"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM account_delegations d\n LEFT JOIN users u ON u.did = d.controller_did\n WHERE d.delegated_did = $1\n AND d.revoked_at IS NULL\n AND (u.did IS NULL OR (u.deactivated_at IS NULL AND u.takedown_ref IS NULL))\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "ff2ffeb1ea1c1375ff0edc4c9ce2f3cdeb92e2b0a72405ea0441b0340b177b96"
}
Generated
+142 -19
View File
@@ -6092,9 +6092,57 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "tranquil-api"
version = "0.4.3"
dependencies = [
"anyhow",
"axum",
"backon",
"base32",
"base64 0.22.1",
"bcrypt",
"bs58",
"bytes",
"chrono",
"cid",
"ed25519-dalek",
"futures",
"hex",
"http 1.4.0",
"infer",
"ipld-core",
"jacquard-common",
"jacquard-repo",
"k256",
"multibase",
"multihash",
"rand 0.8.5",
"reqwest",
"serde",
"serde_ipld_dagcbor",
"serde_json",
"sha2",
"subtle",
"thiserror 2.0.17",
"tokio",
"tracing",
"tranquil-config",
"tranquil-db",
"tranquil-db-traits",
"tranquil-lexicon",
"tranquil-pds",
"tranquil-scopes",
"tranquil-types",
"urlencoding",
"uuid",
"webauthn-rs",
"zip",
]
[[package]]
name = "tranquil-auth"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"anyhow",
"base32",
@@ -6117,7 +6165,7 @@ dependencies = [
[[package]]
name = "tranquil-cache"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6131,7 +6179,7 @@ dependencies = [
[[package]]
name = "tranquil-comms"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6146,7 +6194,7 @@ dependencies = [
[[package]]
name = "tranquil-config"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"confique",
"serde",
@@ -6154,7 +6202,7 @@ dependencies = [
[[package]]
name = "tranquil-crypto"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"aes-gcm",
"base64 0.22.1",
@@ -6170,7 +6218,7 @@ dependencies = [
[[package]]
name = "tranquil-db"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"chrono",
@@ -6187,7 +6235,7 @@ dependencies = [
[[package]]
name = "tranquil-db-traits"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -6203,7 +6251,7 @@ dependencies = [
[[package]]
name = "tranquil-infra"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"bytes",
@@ -6214,7 +6262,7 @@ dependencies = [
[[package]]
name = "tranquil-lexicon"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"chrono",
"hickory-resolver",
@@ -6232,7 +6280,7 @@ dependencies = [
[[package]]
name = "tranquil-oauth"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"anyhow",
"axum",
@@ -6253,9 +6301,42 @@ dependencies = [
"uuid",
]
[[package]]
name = "tranquil-oauth-server"
version = "0.4.3"
dependencies = [
"axum",
"base64 0.22.1",
"bcrypt",
"chrono",
"cid",
"hmac",
"http 1.4.0",
"jacquard-common",
"jacquard-repo",
"k256",
"rand 0.8.5",
"serde",
"serde_json",
"serde_urlencoded",
"sha2",
"subtle",
"tokio",
"tracing",
"tranquil-api",
"tranquil-config",
"tranquil-crypto",
"tranquil-db-traits",
"tranquil-pds",
"tranquil-types",
"urlencoding",
"uuid",
"webauthn-rs",
]
[[package]]
name = "tranquil-pds"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"aes-gcm",
"anyhow",
@@ -6272,9 +6353,7 @@ dependencies = [
"chrono",
"ciborium",
"cid",
"clap",
"ctor",
"dotenvy",
"ed25519-dalek",
"futures",
"futures-util",
@@ -6319,7 +6398,7 @@ dependencies = [
"tower-http",
"tower-layer",
"tracing",
"tracing-subscriber",
"tranquil-api",
"tranquil-auth",
"tranquil-cache",
"tranquil-comms",
@@ -6329,10 +6408,12 @@ dependencies = [
"tranquil-db-traits",
"tranquil-lexicon",
"tranquil-oauth",
"tranquil-oauth-server",
"tranquil-repo",
"tranquil-ripple",
"tranquil-scopes",
"tranquil-storage",
"tranquil-sync",
"tranquil-types",
"urlencoding",
"uuid",
@@ -6343,7 +6424,7 @@ dependencies = [
[[package]]
name = "tranquil-repo"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"bytes",
"cid",
@@ -6355,7 +6436,7 @@ dependencies = [
[[package]]
name = "tranquil-ripple"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"backon",
@@ -6380,7 +6461,7 @@ dependencies = [
[[package]]
name = "tranquil-scopes"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"axum",
"futures",
@@ -6394,9 +6475,29 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "tranquil-server"
version = "0.4.3"
dependencies = [
"axum",
"clap",
"dotenvy",
"ed25519-dalek",
"hex",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"tranquil-api",
"tranquil-config",
"tranquil-oauth-server",
"tranquil-pds",
"tranquil-sync",
]
[[package]]
name = "tranquil-storage"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-trait",
"aws-config",
@@ -6411,9 +6512,31 @@ dependencies = [
"uuid",
]
[[package]]
name = "tranquil-sync"
version = "0.4.3"
dependencies = [
"anyhow",
"axum",
"bytes",
"chrono",
"cid",
"futures",
"ipld-core",
"jacquard-repo",
"serde",
"serde_ipld_dagcbor",
"tokio",
"tracing",
"tranquil-config",
"tranquil-db-traits",
"tranquil-pds",
"tranquil-types",
]
[[package]]
name = "tranquil-types"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"chrono",
"cid",
+10 -1
View File
@@ -16,11 +16,15 @@ members = [
"crates/tranquil-db-traits",
"crates/tranquil-db",
"crates/tranquil-pds",
"crates/tranquil-server",
"crates/tranquil-sync",
"crates/tranquil-oauth-server",
"crates/tranquil-api",
"crates/tranquil-lexicon",
]
[workspace.package]
version = "0.4.1"
version = "0.4.4"
edition = "2024"
license = "AGPL-3.0-or-later"
@@ -40,6 +44,11 @@ tranquil-db-traits = { path = "crates/tranquil-db-traits" }
tranquil-db = { path = "crates/tranquil-db" }
tranquil-ripple = { path = "crates/tranquil-ripple" }
tranquil-lexicon = { path = "crates/tranquil-lexicon" }
tranquil-pds = { path = "crates/tranquil-pds" }
tranquil-server = { path = "crates/tranquil-server" }
tranquil-sync = { path = "crates/tranquil-sync" }
tranquil-oauth-server = { path = "crates/tranquil-oauth-server" }
tranquil-api = { path = "crates/tranquil-api" }
unicode-segmentation = "1"
+9 -4
View File
@@ -4,7 +4,8 @@ COPY frontend/ ./
RUN deno task build
FROM rust:1.92-alpine AS builder
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static
RUN apk add --no-cache ca-certificates musl-dev pkgconfig openssl-dev openssl-libs-static mold clang
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold"
WORKDIR /app
ARG SLIM="false"
COPY Cargo.toml Cargo.lock ./
@@ -25,15 +26,19 @@ COPY crates/tranquil-ripple ./crates/tranquil-ripple
COPY crates/tranquil-storage ./crates/tranquil-storage
COPY crates/tranquil-cache ./crates/tranquil-cache
COPY crates/tranquil-pds ./crates/tranquil-pds
COPY crates/tranquil-sync ./crates/tranquil-sync
COPY crates/tranquil-api ./crates/tranquil-api
COPY crates/tranquil-oauth-server ./crates/tranquil-oauth-server
COPY crates/tranquil-server ./crates/tranquil-server
COPY migrations ./crates/tranquil-pds/migrations
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
if [ "$SLIM" = "true" ]; then \
SQLX_OFFLINE=true cargo build --release -p tranquil-pds --no-default-features; \
SQLX_OFFLINE=true cargo build --release -p tranquil-server --no-default-features; \
else \
SQLX_OFFLINE=true cargo build --release -p tranquil-pds; \
SQLX_OFFLINE=true cargo build --release -p tranquil-server; \
fi && \
cp target/release/tranquil-pds /tmp/tranquil-pds
cp target/release/tranquil-server /tmp/tranquil-pds
FROM alpine:3.23 AS signal-cli
RUN apk add --no-cache curl tar
+50
View File
@@ -0,0 +1,50 @@
[package]
name = "tranquil-api"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
tranquil-pds = { workspace = true }
tranquil-types = { workspace = true }
tranquil-config = { workspace = true }
tranquil-db = { workspace = true }
tranquil-db-traits = { workspace = true }
tranquil-lexicon = { workspace = true, features = ["resolve"] }
tranquil-scopes = { workspace = true }
anyhow = { workspace = true }
axum = { workspace = true }
backon = { workspace = true }
base32 = { workspace = true }
base64 = { workspace = true }
bcrypt = { workspace = true }
bs58 = { workspace = true }
bytes = { workspace = true }
chrono = { workspace = true }
cid = { workspace = true }
ed25519-dalek = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
http = { workspace = true }
infer = { workspace = true }
ipld-core = { workspace = true }
jacquard-common = { workspace = true }
jacquard-repo = { workspace = true }
k256 = { workspace = true }
multibase = { workspace = true }
multihash = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_ipld_dagcbor = { workspace = true }
sha2 = { workspace = true }
subtle = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true }
webauthn-rs = { workspace = true }
zip = { workspace = true }
@@ -1,6 +1,3 @@
use crate::api::error::ApiError;
use crate::auth::{Auth, NotTakendown, Permissive};
use crate::state::AppState;
use axum::{
Json,
extract::State,
@@ -10,6 +7,9 @@ use axum::{
use chrono::{Datelike, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Auth, NotTakendown, Permissive};
use tranquil_pds::state::AppState;
const APP_BSKY_NAMESPACE: &str = "app.bsky";
const MAX_PREFERENCES_COUNT: usize = 100;
@@ -1,8 +1,3 @@
use crate::api::EmptyResponse;
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::Did;
use axum::{
Json,
extract::State,
@@ -10,6 +5,11 @@ use axum::{
};
use serde::Deserialize;
use tracing::warn;
use tranquil_pds::api::EmptyResponse;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::Did;
#[derive(Deserialize)]
pub struct DeleteAccountInput {
@@ -36,7 +36,7 @@ pub async fn delete_account(
.await
.log_db_err("deleting account")?;
if let Err(e) = crate::api::repo::record::sequence_account_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
&state,
did,
tranquil_db_traits::AccountStatus::Deleted,
@@ -50,7 +50,7 @@ pub async fn delete_account(
}
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&handle))
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
Ok(EmptyResponse::ok().into_response())
}
@@ -1,7 +1,3 @@
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::Did;
use axum::{
Json,
extract::State,
@@ -10,6 +6,10 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use tracing::warn;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::Did;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -1,7 +1,3 @@
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::{Did, Handle};
use axum::{
Json,
extract::{Query, RawQuery, State},
@@ -10,6 +6,10 @@ use axum::{
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{Did, Handle};
#[derive(Deserialize)]
pub struct GetAccountInfoParams {
@@ -196,10 +196,11 @@ pub async fn get_account_infos(
_auth: Auth<Admin>,
RawQuery(raw_query): RawQuery,
) -> Result<Response, ApiError> {
let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
.into_iter()
.filter(|d| !d.is_empty())
.collect();
let dids: Vec<String> =
tranquil_pds::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
.into_iter()
.filter(|d| !d.is_empty())
.collect();
if dids.is_empty() {
return Err(ApiError::InvalidRequest("dids is required".into()));
@@ -1,7 +1,3 @@
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::{Did, Handle};
use axum::{
Json,
extract::{Query, State},
@@ -9,6 +5,10 @@ use axum::{
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{Did, Handle};
#[derive(Deserialize)]
pub struct SearchAccountsParams {
@@ -1,8 +1,3 @@
use crate::api::EmptyResponse;
use crate::api::error::ApiError;
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::{Did, Handle, PlainPassword};
use axum::{
Json,
extract::State,
@@ -10,6 +5,11 @@ use axum::{
};
use serde::Deserialize;
use tracing::{error, warn};
use tranquil_pds::api::EmptyResponse;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{Did, Handle, PlainPassword};
#[derive(Deserialize)]
pub struct UpdateAccountEmailInput {
@@ -101,14 +101,14 @@ pub async fn update_account_handle(
if let Some(old) = old_handle {
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&old))
.delete(&tranquil_pds::cache_keys::handle_key(&old))
.await;
}
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&handle))
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
if let Err(e) = crate::api::repo::record::sequence_identity_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
&state,
did,
Some(&handle_for_check),
@@ -121,7 +121,7 @@ pub async fn update_account_handle(
);
}
if let Err(e) =
crate::api::identity::did::update_plc_handle(&state, did, &handle_for_check).await
crate::identity::did::update_plc_handle(&state, did, &handle_for_check).await
{
warn!("Failed to update PLC handle for admin handle update: {}", e);
}
@@ -1,9 +1,9 @@
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use tracing::{error, warn};
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_types::CidLink;
#[derive(Serialize)]
@@ -1,7 +1,3 @@
use crate::api::EmptyResponse;
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use axum::{
Json,
extract::{Query, State},
@@ -11,6 +7,10 @@ use axum::{
use serde::{Deserialize, Serialize};
use tracing::error;
use tranquil_db_traits::InviteCodeSortOrder;
use tranquil_pds::api::EmptyResponse;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -1,12 +1,12 @@
use crate::api::error::ApiError;
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use serde::Serialize;
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@@ -1,7 +1,3 @@
use crate::api::error::ApiError;
use crate::auth::{Admin, Auth};
use crate::state::AppState;
use crate::types::{CidLink, Did};
use axum::{
Json,
extract::{Query, State},
@@ -11,6 +7,10 @@ use axum::{
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, warn};
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Admin, Auth};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{CidLink, Did};
#[derive(Deserialize)]
pub struct GetSubjectStatusParams {
@@ -215,7 +215,7 @@ pub async fn update_subject_status(
tranquil_db_traits::AccountStatus::Active
};
if let Err(e) =
crate::api::repo::record::sequence_account_event(&state, &did, status).await
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
{
warn!("Failed to sequence account event for takedown: {}", e);
}
@@ -227,7 +227,7 @@ pub async fn update_subject_status(
tranquil_db_traits::AccountStatus::Active
};
if let Err(e) =
crate::api::repo::record::sequence_account_event(&state, &did, status).await
tranquil_pds::repo_ops::sequence_account_event(&state, &did, status).await
{
warn!("Failed to sequence account event for deactivation: {}", e);
}
@@ -235,7 +235,7 @@ pub async fn update_subject_status(
if let Ok(Some(handle)) = state.user_repo.get_handle_by_did(&did).await {
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&handle))
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
}
return Ok((
@@ -1,5 +1,3 @@
use crate::auth::{AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop};
use crate::state::AppState;
use axum::{
Json,
extract::State,
@@ -7,6 +5,10 @@ use axum::{
response::{IntoResponse, Response},
};
use serde_json::json;
use tranquil_pds::auth::{
AccountRequirement, extract_auth_token_from_header, validate_token_with_dpop,
};
use tranquil_pds::state::AppState;
pub async fn get_state(State(state): State<AppState>, headers: HeaderMap) -> Response {
let created_at = get_account_created_at(&state, &headers).await;
@@ -33,13 +35,13 @@ pub async fn get_age_assurance_state() -> Response {
}
async fn get_account_created_at(state: &AppState, headers: &HeaderMap) -> Option<String> {
let auth_header = crate::util::get_header_str(headers, http::header::AUTHORIZATION);
let auth_header = tranquil_pds::util::get_header_str(headers, http::header::AUTHORIZATION);
tracing::debug!(?auth_header, "age assurance: extracting token");
let extracted = extract_auth_token_from_header(auth_header)?;
tracing::debug!("age assurance: got token, validating");
let dpop_proof = crate::util::get_header_str(headers, crate::util::HEADER_DPOP);
let dpop_proof = tranquil_pds::util::get_header_str(headers, tranquil_pds::util::HEADER_DPOP);
let http_uri = "/";
let auth_user = match validate_token_with_dpop(
@@ -1,40 +1,22 @@
use crate::api::error::ApiError;
use crate::api::repo::record::utils::create_signed_commit;
use crate::auth::{Active, Auth};
use crate::delegation::{
DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers,
verify_can_be_controller, verify_can_control_accounts,
};
use crate::rate_limit::{AccountCreationLimit, RateLimited};
use crate::state::AppState;
use crate::types::{Did, Handle};
use crate::identity::provision::{create_plc_did, init_genesis_repo};
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use jacquard_common::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{error, info, warn};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ControllerInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
pub is_active: bool,
}
#[derive(Debug, Serialize)]
pub struct ListControllersResponse {
pub controllers: Vec<ControllerInfo>,
}
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{Active, Auth};
use tranquil_pds::delegation::{
DelegationActionType, SCOPE_PRESETS, ValidatedDelegationScope, verify_can_add_controllers,
verify_can_control_accounts,
};
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{Did, Handle};
pub async fn list_controllers(
State(state): State<AppState>,
@@ -54,19 +36,23 @@ pub async fn list_controllers(
}
};
Ok(Json(ListControllersResponse {
controllers: controllers
.into_iter()
.map(|c| ControllerInfo {
did: c.did,
handle: c.handle,
granted_scopes: c.granted_scopes.into_string(),
granted_at: c.granted_at,
is_active: c.is_active,
})
.collect(),
})
.into_response())
let resolve_futures = controllers.into_iter().map(|mut c| {
let did_resolver = state.did_resolver.clone();
async move {
if c.handle.is_none() {
c.handle = did_resolver
.resolve_did_document(c.did.as_str())
.await
.and_then(|doc| tranquil_types::did_doc::extract_handle(&doc))
.map(|h| h.into());
}
c
}
});
let controllers = futures::future::join_all(resolve_futures).await;
Ok(Json(serde_json::json!({ "controllers": controllers })).into_response())
}
#[derive(Debug, Deserialize)]
@@ -80,16 +66,38 @@ pub async fn add_controller(
auth: Auth<Active>,
Json(input): Json<AddControllerInput>,
) -> Result<Response, ApiError> {
let controller_exists = state
.user_repo
.get_by_did(&input.controller_did)
let resolved = tranquil_pds::delegation::resolve_identity(&state, &input.controller_did)
.await
.ok()
.flatten()
.is_some();
.ok_or(ApiError::ControllerNotFound)?;
if !controller_exists {
return Ok(ApiError::ControllerNotFound.into_response());
if !resolved.is_local
&& let Some(ref pds_url) = resolved.pds_url
{
if !pds_url.starts_with("https://") {
return Ok(
ApiError::InvalidDelegation("Controller PDS must use HTTPS".into()).into_response(),
);
}
match state
.cross_pds_oauth
.check_remote_is_delegated(pds_url, input.controller_did.as_str())
.await
{
Some(true) => {
return Ok(ApiError::InvalidDelegation(
"Cannot add a delegated account from another PDS as a controller".into(),
)
.into_response());
}
Some(false) => {}
None => {
warn!(
controller = %input.controller_did,
pds = %pds_url,
"Could not verify remote controller delegation status"
);
}
}
}
let can_add = match verify_can_add_controllers(&state, &auth).await {
@@ -97,16 +105,24 @@ pub async fn add_controller(
Err(response) => return Ok(response),
};
let can_be_controller = match verify_can_be_controller(&state, &input.controller_did).await {
Ok(proof) => proof,
Err(response) => return Ok(response),
};
if resolved.is_local
&& state
.delegation_repo
.is_delegated_account(&input.controller_did)
.await
.unwrap_or(false)
{
return Ok(ApiError::InvalidDelegation(
"Cannot add a controlled account as a controller".into(),
)
.into_response());
}
match state
.delegation_repo
.create_delegation(
can_add.did(),
can_be_controller.did(),
&input.controller_did,
&input.granted_scopes,
can_add.did(),
)
@@ -118,10 +134,11 @@ pub async fn add_controller(
.log_delegation_action(
can_add.did(),
can_add.did(),
Some(can_be_controller.did()),
Some(&input.controller_did),
DelegationActionType::GrantCreated,
Some(serde_json::json!({
"granted_scopes": input.granted_scopes.as_str()
"granted_scopes": input.granted_scopes.as_str(),
"is_local": resolved.is_local
})),
None,
None,
@@ -256,20 +273,6 @@ pub async fn update_controller_scopes(
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegatedAccountInfo {
pub did: Did,
pub handle: Handle,
pub granted_scopes: String,
pub granted_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct ListControlledAccountsResponse {
pub accounts: Vec<DelegatedAccountInfo>,
}
pub async fn list_controlled_accounts(
State(state): State<AppState>,
auth: Auth<Active>,
@@ -289,18 +292,7 @@ pub async fn list_controlled_accounts(
}
};
Ok(Json(ListControlledAccountsResponse {
accounts: accounts
.into_iter()
.map(|a| DelegatedAccountInfo {
did: a.did,
handle: a.handle,
granted_scopes: a.granted_scopes.into_string(),
granted_at: a.granted_at,
})
.collect(),
})
.into_response())
Ok(Json(serde_json::json!({ "accounts": accounts })).into_response())
}
#[derive(Debug, Deserialize)]
@@ -315,24 +307,6 @@ fn default_limit() -> i64 {
50
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditLogEntry {
pub id: String,
pub delegated_did: Did,
pub actor_did: Did,
pub controller_did: Option<Did>,
pub action_type: String,
pub action_details: Option<serde_json::Value>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
pub struct GetAuditLogResponse {
pub entries: Vec<AuditLogEntry>,
pub total: i64,
}
pub async fn get_audit_log(
State(state): State<AppState>,
auth: Auth<Active>,
@@ -361,50 +335,11 @@ pub async fn get_audit_log(
.await
.unwrap_or_default();
Ok(Json(GetAuditLogResponse {
entries: entries
.into_iter()
.map(|e| AuditLogEntry {
id: e.id.to_string(),
delegated_did: e.delegated_did,
actor_did: e.actor_did,
controller_did: e.controller_did,
action_type: format!("{:?}", e.action_type),
action_details: e.action_details,
created_at: e.created_at,
})
.collect(),
total,
})
.into_response())
}
#[derive(Debug, Serialize)]
pub struct ScopePresetInfo {
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub scopes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct GetScopePresetsResponse {
pub presets: Vec<ScopePresetInfo>,
Ok(Json(serde_json::json!({ "entries": entries, "total": total })).into_response())
}
pub async fn get_scope_presets() -> Response {
Json(GetScopePresetsResponse {
presets: SCOPE_PRESETS
.iter()
.map(|p| ScopePresetInfo {
name: p.name,
label: p.label,
description: p.description,
scopes: p.scopes,
})
.collect(),
})
.into_response()
Json(serde_json::json!({ "presets": SCOPE_PRESETS })).into_response()
}
#[derive(Debug, Deserialize)]
@@ -434,29 +369,11 @@ pub async fn create_delegated_account(
Err(response) => return Ok(response),
};
let hostname = &tranquil_config::get().server.hostname;
let available_domains = tranquil_config::get().server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
let handle = if !input.handle.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.handle
.strip_suffix(&format!(".{}", domain))
.unwrap_or(&input.handle),
None => &input.handle,
};
match crate::api::validation::validate_short_handle(handle_to_validate) {
Ok(h) => format!("{}.{}", h, matched_domain.unwrap_or(&available_domains[0])),
Err(e) => {
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
}
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
Ok(h) => h,
Err(e) => {
return Ok(ApiError::InvalidRequest(e.to_string()).into_response());
}
} else {
input.handle.to_lowercase()
};
let email = input
@@ -465,7 +382,7 @@ pub async fn create_delegated_account(
.map(|e| e.trim().to_string())
.filter(|e| !e.is_empty());
if let Some(ref email) = email
&& !crate::api::validation::is_valid_email(email)
&& !tranquil_pds::api::validation::is_valid_email(email)
{
return Ok(ApiError::InvalidEmail.into_response());
}
@@ -483,96 +400,15 @@ pub async fn create_delegated_account(
None
};
use k256::ecdsa::SigningKey;
use rand::rngs::OsRng;
let pds_endpoint = format!("https://{}", hostname);
let secret_key = k256::SecretKey::random(&mut OsRng);
let secret_key_bytes = secret_key.to_bytes().to_vec();
let signing_key = match SigningKey::from_slice(&secret_key_bytes) {
Ok(k) => k,
Err(e) => {
error!("Error creating signing key: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| crate::plc::signing_key_to_did_key(&signing_key));
let genesis_result = match crate::plc::create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return Ok(
ApiError::InternalError(Some("Failed to create PLC operation".into()))
.into_response(),
);
}
};
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return Ok(ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response());
}
let did: Did = genesis_result
.did
.parse()
.map_err(|_| ApiError::InternalError(Some("PLC genesis returned invalid DID".into())))?;
let plc = create_plc_did(&state, &handle).await.map_err(|e| {
tracing::error!("PLC DID creation failed: {:?}", e);
e
})?;
let did = plc.did;
let handle: Handle = handle.parse().map_err(|_| ApiError::InvalidHandle(None))?;
info!(did = %did, handle = %handle, controller = %can_control.did(), "Created DID for delegated account");
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
Ok(bytes) => bytes,
Err(e) => {
error!("Error encrypting signing key: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = match mst.persist().await {
Ok(c) => c,
Err(e) => {
error!("Error persisting MST: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let rev = Tid::now(LimitedU32::MIN);
let (commit_bytes, _sig) =
match create_signed_commit(&did, mst_root, rev.as_ref(), None, &signing_key) {
Ok(result) => result,
Err(e) => {
error!("Error creating genesis commit: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await {
Ok(c) => c,
Err(e) => {
error!("Error saving genesis commit: {:?}", e);
return Ok(ApiError::InternalError(None).into_response());
}
};
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
let repo = init_genesis_repo(&state, &did, &plc.signing_key, &plc.signing_key_bytes).await?;
let create_input = tranquil_db_traits::CreateDelegatedAccountInput {
handle: handle.clone(),
@@ -580,11 +416,11 @@ pub async fn create_delegated_account(
did: did.clone(),
controller_did: can_control.did().clone(),
controller_scopes: input.controller_scopes.as_str().to_string(),
encrypted_key_bytes,
encryption_version: crate::config::ENCRYPTION_VERSION,
commit_cid: commit_cid.to_string(),
repo_rev: rev.as_ref().to_string(),
genesis_block_cids,
encrypted_key_bytes: repo.encrypted_key_bytes,
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
commit_cid: repo.commit_cid.to_string(),
repo_rev: repo.repo_rev.clone(),
genesis_block_cids: repo.genesis_block_cids,
invite_code: input.invite_code.clone(),
};
@@ -616,11 +452,11 @@ pub async fn create_delegated_account(
}
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle)).await
{
warn!("Failed to sequence identity event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_account_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
&state,
&did,
tranquil_db_traits::AccountStatus::Active,
@@ -634,11 +470,11 @@ pub async fn create_delegated_account(
"$type": "app.bsky.actor.profile",
"displayName": handle
});
if let Err(e) = crate::api::repo::record::create_record_internal(
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
&state,
&did,
&crate::types::PROFILE_COLLECTION,
&crate::types::PROFILE_RKEY,
&tranquil_pds::types::PROFILE_COLLECTION,
&tranquil_pds::types::PROFILE_RKEY,
&profile_record,
)
.await
@@ -666,3 +502,41 @@ pub async fn create_delegated_account(
Ok(Json(CreateDelegatedAccountResponse { did, handle }).into_response())
}
#[derive(Debug, Deserialize)]
pub struct ResolveControllerParams {
pub identifier: String,
}
pub async fn resolve_controller(
State(state): State<AppState>,
Query(params): Query<ResolveControllerParams>,
) -> Result<Response, ApiError> {
let identifier = params.identifier.trim().trim_start_matches('@');
let did: Did = if identifier.starts_with("did:") {
identifier
.parse()
.map_err(|_| ApiError::ControllerNotFound)?
} else {
let local_handle: Option<Handle> = identifier.parse().ok();
let local_user = match local_handle {
Some(ref h) => state.user_repo.get_by_handle(h).await.ok().flatten(),
None => None,
};
match local_user {
Some(user) => user.did,
None => tranquil_pds::handle::resolve_handle(identifier)
.await
.map_err(|_| ApiError::ControllerNotFound)?
.parse()
.map_err(|_| ApiError::ControllerNotFound)?,
}
};
let resolved = tranquil_pds::delegation::resolve_identity(&state, &did)
.await
.ok_or(ApiError::ControllerNotFound)?;
Ok(Json(resolved).into_response())
}
@@ -10,9 +10,9 @@ use serde_json::json;
use tracing::{debug, info, warn};
use tranquil_types::Handle;
use crate::comms::comms_repo;
use crate::state::AppState;
use crate::util::discord_public_key;
use tranquil_pds::comms::comms_repo;
use tranquil_pds::state::AppState;
use tranquil_pds::util::discord_public_key;
#[derive(Deserialize)]
struct Interaction {
@@ -1,12 +1,4 @@
use super::did::verify_did_web;
use crate::api::error::ApiError;
use crate::api::repo::record::utils::create_signed_commit;
use crate::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key};
use crate::rate_limit::{AccountCreationLimit, RateLimited};
use crate::state::AppState;
use crate::types::{Did, Handle, PlainPassword};
use crate::validation::validate_password;
use axum::{
Json,
extract::State,
@@ -14,14 +6,17 @@ use axum::{
response::{IntoResponse, Response},
};
use bcrypt::{DEFAULT_COST, hash};
use jacquard_common::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::{mst::Mst, storage::BlockStore};
use k256::{SecretKey, ecdsa::SigningKey};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use tracing::{debug, error, info, warn};
use tranquil_pds::api::error::ApiError;
use tranquil_pds::auth::{ServiceTokenVerifier, extract_auth_token_from_header, is_service_token};
use tranquil_pds::rate_limit::{AccountCreationLimit, RateLimited};
use tranquil_pds::state::AppState;
use tranquil_pds::types::{Did, Handle, PlainPassword};
use tranquil_pds::validation::validate_password;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -73,7 +68,7 @@ pub async fn create_account(
}
let migration_auth = if let Some(extracted) = extract_auth_token_from_header(
crate::util::get_header_str(&headers, http::header::AUTHORIZATION),
tranquil_pds::util::get_header_str(&headers, http::header::AUTHORIZATION),
) {
let token = extracted.token;
if is_service_token(&token) {
@@ -141,31 +136,9 @@ pub async fn create_account(
}
let cfg = tranquil_config::get();
let available_domains = cfg.server.available_user_domain_list();
let matched_domain = available_domains
.iter()
.filter(|d| input.handle.ends_with(&format!(".{}", d)))
.max_by_key(|d| d.len());
let validated_short_handle = if !input.handle.contains('.') || matched_domain.is_some() {
let handle_to_validate = match matched_domain {
Some(domain) => input
.handle
.strip_suffix(&format!(".{}", domain))
.unwrap_or(&input.handle),
None => &input.handle,
};
match crate::api::validation::validate_short_handle(handle_to_validate) {
Ok(h) => h,
Err(e) => {
return ApiError::from(e).into_response();
}
}
} else {
match crate::api::validation::validate_full_domain_handle(&input.handle) {
Ok(h) => h,
Err(e) => return ApiError::from(e).into_response(),
}
let handle = match tranquil_pds::api::validation::resolve_handle_input(&input.handle) {
Ok(h) => h,
Err(e) => return ApiError::from(e).into_response(),
};
let email: Option<String> = input
.email
@@ -173,7 +146,7 @@ pub async fn create_account(
.map(|e| e.trim().to_string())
.filter(|e| !e.is_empty());
if let Some(ref email) = email
&& !crate::api::validation::is_valid_email(email)
&& !tranquil_pds::api::validation::is_valid_email(email)
{
return ApiError::InvalidEmail.into_response();
}
@@ -191,7 +164,7 @@ pub async fn create_account(
tranquil_db_traits::CommsChannel::Discord => match &input.discord_username {
Some(username) if !username.trim().is_empty() => {
let clean = username.trim().to_lowercase();
if !crate::api::validation::is_valid_discord_username(&clean) {
if !tranquil_pds::api::validation::is_valid_discord_username(&clean) {
return ApiError::InvalidRequest(
"Invalid Discord username. Must be 2-32 lowercase characters (letters, numbers, underscores, periods)".into(),
).into_response();
@@ -203,7 +176,7 @@ pub async fn create_account(
tranquil_db_traits::CommsChannel::Telegram => match &input.telegram_username {
Some(username) if !username.trim().is_empty() => {
let clean = username.trim().trim_start_matches('@');
if !crate::api::validation::is_valid_telegram_username(clean) {
if !tranquil_pds::api::validation::is_valid_telegram_username(clean) {
return ApiError::InvalidRequest(
"Invalid Telegram username. Must be 5-32 characters, alphanumeric or underscore".into(),
).into_response();
@@ -221,12 +194,6 @@ pub async fn create_account(
})
};
let hostname = &cfg.server.hostname;
let pds_endpoint = format!("https://{}", hostname);
let handle = match matched_domain {
Some(domain) => format!("{}.{}", validated_short_handle, domain),
None if input.handle.contains('.') => validated_short_handle.clone(),
None => format!("{}.{}", validated_short_handle, &available_domains[0]),
};
let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<uuid::Uuid>) =
if let Some(signing_key_did) = &input.signing_key {
match state
@@ -257,7 +224,7 @@ pub async fn create_account(
let did_type = input.did_type.as_deref().unwrap_or("plc");
let did = match did_type {
"web" => {
if !crate::api::server::meta::is_self_hosted_did_web_enabled() {
if !tranquil_pds::util::is_self_hosted_did_web_enabled() {
return ApiError::SelfHostedDidWebDisabled.into_response();
}
let encoded_handle = handle.replace(':', "%3A");
@@ -308,76 +275,17 @@ pub async fn create_account(
)
.into_response();
} else {
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
let genesis_result = match create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return ApiError::InternalError(Some(
"Failed to create PLC operation".into(),
))
.into_response();
}
};
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response();
Ok(did) => did,
Err(e) => return e.into_response(),
}
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
genesis_result.did
}
} else {
let rotation_key = tranquil_config::get()
.secrets
.plc_rotation_key
.clone()
.unwrap_or_else(|| signing_key_to_did_key(&signing_key));
let genesis_result = match create_genesis_operation(
&signing_key,
&rotation_key,
&handle,
&pds_endpoint,
) {
Ok(r) => r,
Err(e) => {
error!("Error creating PLC genesis operation: {:?}", e);
return ApiError::InternalError(Some(
"Failed to create PLC operation".into(),
))
.into_response();
}
};
let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
if let Err(e) = plc_client
.send_operation(&genesis_result.did, &genesis_result.signed_operation)
.await
{
error!("Failed to submit PLC genesis operation: {:?}", e);
return ApiError::UpstreamErrorMsg(format!(
"Failed to register DID with PLC directory: {}",
e
))
.into_response();
match super::provision::submit_plc_genesis(&state, &signing_key, &handle).await {
Ok(did) => did,
Err(e) => return e.into_response(),
}
info!(did = %genesis_result.did, "Successfully registered DID with PLC directory");
genesis_result.did
}
}
};
@@ -408,7 +316,7 @@ pub async fn create_account(
.await
{
Ok(Some(key_info)) => {
match crate::config::decrypt_key(
match tranquil_pds::config::decrypt_key(
&key_info.key_bytes,
key_info.encryption_version,
) {
@@ -427,15 +335,17 @@ pub async fn create_account(
.into_response();
}
};
let access_meta =
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Error creating access token: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(
let access_meta = match tranquil_pds::auth::create_access_token_with_metadata(
&did,
&secret_key_bytes,
) {
Ok(m) => m,
Err(e) => {
error!("Error creating access token: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let refresh_meta = match tranquil_pds::auth::create_refresh_token_with_metadata(
&did,
&secret_key_bytes,
) {
@@ -453,7 +363,7 @@ pub async fn create_account(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic transition:chat.bsky".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -463,12 +373,12 @@ pub async fn create_account(
}
let hostname = &tranquil_config::get().server.hostname;
let verification_required = if let Some(ref user_email) = email {
let token = crate::auth::verification_token::generate_migration_token(
let token = tranquil_pds::auth::verification_token::generate_migration_token(
&did_typed, user_email,
);
let formatted_token =
crate::auth::verification_token::format_token_for_display(&token);
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
tranquil_pds::auth::verification_token::format_token_for_display(&token);
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
state.user_repo.as_ref(),
state.infra_repo.as_ref(),
reactivated.user_id,
@@ -590,45 +500,23 @@ pub async fn create_account(
None
};
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
Ok(enc) => enc,
Err(e) => {
error!("Error encrypting user key: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let mst = Mst::new(Arc::new(state.block_store.clone()));
let mst_root = match mst.persist().await {
Ok(c) => c,
Err(e) => {
error!("Error persisting MST: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let rev = Tid::now(LimitedU32::MIN);
let did_for_commit: Did = match did.parse() {
Ok(d) => d,
Err(_) => return ApiError::InternalError(Some("Invalid DID".into())).into_response(),
};
let (commit_bytes, _sig) =
match create_signed_commit(&did_for_commit, mst_root, rev.as_ref(), None, &signing_key) {
Ok(result) => result,
Err(e) => {
error!("Error creating genesis commit: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let commit_cid = match state.block_store.put(&commit_bytes).await {
Ok(c) => c,
Err(e) => {
error!("Error saving genesis commit: {:?}", e);
return ApiError::InternalError(None).into_response();
}
let repo = match super::provision::init_genesis_repo(
&state,
&did_for_commit,
&signing_key,
&secret_key_bytes,
)
.await
{
Ok(r) => r,
Err(e) => return e.into_response(),
};
let commit_cid_str = commit_cid.to_string();
let rev_str = rev.as_ref().to_string();
let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()];
let commit_cid_str = repo.commit_cid.to_string();
let rev_str = repo.repo_rev.clone();
let birthdate_pref = if tranquil_config::get().server.age_assurance_override {
Some(json!({
@@ -665,12 +553,12 @@ pub async fn create_account(
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase()),
deactivated_at,
encrypted_key_bytes,
encryption_version: crate::config::ENCRYPTION_VERSION,
encrypted_key_bytes: repo.encrypted_key_bytes,
encryption_version: tranquil_pds::config::ENCRYPTION_VERSION,
reserved_key_id,
commit_cid: commit_cid_str.clone(),
repo_rev: rev_str.clone(),
genesis_block_cids,
genesis_block_cids: repo.genesis_block_cids,
invite_code: if is_bootstrap {
None
} else {
@@ -697,7 +585,7 @@ pub async fn create_account(
};
let user_id = create_result.user_id;
if !is_migration && !is_did_web_byod {
if let Err(e) = crate::api::repo::record::sequence_identity_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_identity_event(
&state,
&did_for_commit,
Some(&handle_typed),
@@ -706,7 +594,7 @@ pub async fn create_account(
{
warn!("Failed to sequence identity event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_account_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_account_event(
&state,
&did_for_commit,
tranquil_db_traits::AccountStatus::Active,
@@ -715,22 +603,22 @@ pub async fn create_account(
{
warn!("Failed to sequence account event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_genesis_commit(
if let Err(e) = tranquil_pds::repo_ops::sequence_genesis_commit(
&state,
&did_for_commit,
&commit_cid,
&mst_root,
&repo.commit_cid,
&repo.mst_root_cid,
&rev_str,
)
.await
{
warn!("Failed to sequence commit event for {}: {}", did, e);
}
if let Err(e) = crate::api::repo::record::sequence_sync_event(
if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event(
&state,
&did_for_commit,
&commit_cid_str,
Some(rev.as_ref()),
Some(&rev_str),
)
.await
{
@@ -740,11 +628,11 @@ pub async fn create_account(
"$type": "app.bsky.actor.profile",
"displayName": input.handle
});
if let Err(e) = crate::api::repo::record::create_record_internal(
if let Err(e) = tranquil_pds::repo_ops::create_record_internal(
&state,
&did_for_commit,
&crate::types::PROFILE_COLLECTION,
&crate::types::PROFILE_RKEY,
&tranquil_pds::types::PROFILE_COLLECTION,
&tranquil_pds::types::PROFILE_RKEY,
&profile_record,
)
.await
@@ -755,14 +643,15 @@ pub async fn create_account(
let hostname = &tranquil_config::get().server.hostname;
if !is_migration {
if let Some(ref recipient) = verification_recipient {
let verification_token = crate::auth::verification_token::generate_signup_token(
let verification_token = tranquil_pds::auth::verification_token::generate_signup_token(
&did_for_commit,
verification_channel,
recipient,
);
let formatted_token =
crate::auth::verification_token::format_token_for_display(&verification_token);
if let Err(e) = crate::comms::comms_repo::enqueue_signup_verification(
let formatted_token = tranquil_pds::auth::verification_token::format_token_for_display(
&verification_token,
);
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_signup_verification(
state.user_repo.as_ref(),
state.infra_repo.as_ref(),
user_id,
@@ -780,10 +669,13 @@ pub async fn create_account(
}
}
} else if let Some(ref user_email) = email {
let token =
crate::auth::verification_token::generate_migration_token(&did_for_commit, user_email);
let formatted_token = crate::auth::verification_token::format_token_for_display(&token);
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
let token = tranquil_pds::auth::verification_token::generate_migration_token(
&did_for_commit,
user_email,
);
let formatted_token =
tranquil_pds::auth::verification_token::format_token_for_display(&token);
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_migration_verification(
state.user_repo.as_ref(),
state.infra_repo.as_ref(),
user_id,
@@ -797,16 +689,16 @@ pub async fn create_account(
}
}
let access_meta = match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes)
{
Ok(m) => m,
Err(e) => {
error!("createAccount: Error creating access token: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let access_meta =
match tranquil_pds::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("createAccount: Error creating access token: {:?}", e);
return ApiError::InternalError(None).into_response();
}
};
let refresh_meta =
match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
match tranquil_pds::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("createAccount: Error creating refresh token: {:?}", e);
@@ -821,7 +713,7 @@ pub async fn create_account(
refresh_expires_at: refresh_meta.expires_at,
login_type: tranquil_db_traits::LoginType::Modern,
mfa_verified: false,
scope: None,
scope: Some("transition:generic transition:chat.bsky".to_string()),
controller_did: None,
app_password_name: None,
};
@@ -1,12 +1,3 @@
use crate::api::{ApiError, DidResponse, EmptyResponse};
use crate::auth::{Auth, NotTakendown};
use crate::plc::signing_key_to_did_key;
use crate::rate_limit::{
HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message,
};
use crate::state::AppState;
use crate::types::Handle;
use crate::util::get_header_str;
use axum::{
Json,
extract::{Path, Query, State},
@@ -19,6 +10,15 @@ use k256::elliptic_curve::sec1::ToEncodedPoint;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, warn};
use tranquil_pds::api::{ApiError, DidResponse, EmptyResponse};
use tranquil_pds::auth::{Auth, NotTakendown};
use tranquil_pds::plc::signing_key_to_did_key;
use tranquil_pds::rate_limit::{
HandleUpdateDailyLimit, HandleUpdateLimit, check_user_rate_limit_with_message,
};
use tranquil_pds::state::AppState;
use tranquil_pds::types::Handle;
use tranquil_pds::util::get_header_str;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -42,7 +42,7 @@ pub async fn resolve_handle(
if handle_str.is_empty() {
return ApiError::InvalidRequest("handle is required".into()).into_response();
}
let cache_key = crate::cache_keys::handle_key(handle_str);
let cache_key = tranquil_pds::cache_keys::handle_key(handle_str);
if let Some(did) = state.cache.get(&cache_key).await {
return DidResponse::response(did).into_response();
}
@@ -61,7 +61,7 @@ pub async fn resolve_handle(
.await;
DidResponse::response(row.did).into_response()
}
Ok(None) => match crate::handle::resolve_handle(handle.as_str()).await {
Ok(None) => match tranquil_pds::handle::resolve_handle(handle.as_str()).await {
Ok(did) => {
let _ = state
.cache
@@ -148,7 +148,7 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
"id": did,
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": format!("https://{}", hostname)
}]
}))
@@ -158,7 +158,7 @@ pub async fn well_known_did(State(state): State<AppState>, headers: HeaderMap) -
async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) -> Response {
let encoded_handle = handle.replace(':', "%3A");
let expected_did = format!("did:web:{}", encoded_handle);
let expected_did_typed: crate::types::Did = match expected_did.parse() {
let expected_did_typed: tranquil_pds::types::Did = match expected_did.parse() {
Ok(d) => d,
Err(_) => return ApiError::InvalidRequest("Invalid DID format".into()).into_response(),
};
@@ -216,7 +216,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
})).collect::<Vec<_>>(),
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": service_endpoint
}]
}))
@@ -229,7 +229,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
Err(_) => return ApiError::InternalError(None).into_response(),
};
let key_bytes: Vec<u8> =
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
Ok(k) => k,
Err(_) => {
return ApiError::InternalError(None).into_response();
@@ -269,7 +269,7 @@ async fn serve_handle_did_doc(state: &AppState, handle: &str, hostname: &str) ->
}],
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": service_endpoint
}]
}))
@@ -351,7 +351,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
})).collect::<Vec<_>>(),
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": service_endpoint
}]
}))
@@ -364,7 +364,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
Err(_) => return ApiError::InternalError(None).into_response(),
};
let key_bytes: Vec<u8> =
match crate::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
match tranquil_pds::config::decrypt_key(&key_info.key_bytes, key_info.encryption_version) {
Ok(k) => k,
Err(_) => {
return ApiError::InternalError(None).into_response();
@@ -404,7 +404,7 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
}],
"service": [{
"id": "#atproto_pds",
"type": crate::plc::ServiceType::Pds.as_str(),
"type": tranquil_pds::plc::ServiceType::Pds.as_str(),
"serviceEndpoint": service_endpoint
}]
}))
@@ -480,7 +480,7 @@ pub async fn verify_did_web(
let path = parts[3..].join("/");
format!("{}://{}/{}/did.json", scheme, domain, path)
};
let client = crate::api::proxy_client::did_resolution_client();
let client = tranquil_pds::api::proxy_client::did_resolution_client();
let resp = client
.get(&url)
.send()
@@ -503,7 +503,8 @@ pub async fn verify_did_web(
))?;
let pds_endpoint = format!("https://{}", hostname);
let has_valid_service = services.iter().any(|s| {
s["type"] == crate::plc::ServiceType::Pds.as_str() && s["serviceEndpoint"] == pds_endpoint
s["type"] == tranquil_pds::plc::ServiceType::Pds.as_str()
&& s["serviceEndpoint"] == pds_endpoint
});
if !has_valid_service {
return Err(DidWebVerifyError::PdsNotListed(pds_endpoint));
@@ -600,7 +601,7 @@ pub async fn get_recommended_did_credentials(
verification_methods: VerificationMethods { atproto: did_key },
services: Services {
atproto_pds: AtprotoPds {
service_type: crate::plc::ServiceType::Pds.as_str().to_string(),
service_type: tranquil_pds::plc::ServiceType::Pds.as_str().to_string(),
endpoint: pds_endpoint,
},
},
@@ -619,10 +620,10 @@ pub async fn update_handle(
auth: Auth<NotTakendown>,
Json(input): Json<UpdateHandleInput>,
) -> Result<Response, ApiError> {
if let Err(e) = crate::auth::scope_check::check_identity_scope(
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
&auth.auth_source,
auth.scope.as_deref(),
crate::oauth::scopes::IdentityAttr::Handle,
tranquil_pds::oauth::scopes::IdentityAttr::Handle,
) {
return Ok(e);
}
@@ -672,7 +673,7 @@ pub async fn update_handle(
"Handle segment cannot start or end with hyphen".into(),
)));
}
if crate::moderation::has_explicit_slur(&new_handle) {
if tranquil_pds::moderation::has_explicit_slur(&new_handle) {
return Err(ApiError::InvalidHandle(Some(
"Inappropriate language in handle".into(),
)));
@@ -704,7 +705,7 @@ pub async fn update_handle(
Err(_) => return Err(ApiError::InvalidHandle(None)),
};
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed))
.await
{
warn!("Failed to sequence identity event for handle update: {}", e);
@@ -730,19 +731,19 @@ pub async fn update_handle(
Err(_) => return Err(ApiError::InvalidHandle(None)),
};
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed))
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed))
.await
{
warn!("Failed to sequence identity event for handle update: {}", e);
}
return Ok(EmptyResponse::ok().into_response());
}
match crate::handle::verify_handle_ownership(&new_handle, &did).await {
match tranquil_pds::handle::verify_handle_ownership(&new_handle, &did).await {
Ok(()) => {}
Err(crate::handle::HandleResolutionError::NotFound) => {
Err(tranquil_pds::handle::HandleResolutionError::NotFound) => {
return Err(ApiError::HandleNotAvailable(None));
}
Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
Err(tranquil_pds::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
return Err(ApiError::HandleNotAvailable(Some(format!(
"Handle points to different DID. Expected {}, got {}",
expected, actual
@@ -781,15 +782,15 @@ pub async fn update_handle(
if !current_handle.is_empty() {
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&current_handle))
.delete(&tranquil_pds::cache_keys::handle_key(&current_handle))
.await;
}
let _ = state
.cache
.delete(&crate::cache_keys::handle_key(&handle))
.delete(&tranquil_pds::cache_keys::handle_key(&handle))
.await;
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle_typed)).await
tranquil_pds::repo_ops::sequence_identity_event(&state, &did, Some(&handle_typed)).await
{
warn!("Failed to sequence identity event for handle update: {}", e);
}
@@ -801,7 +802,7 @@ pub async fn update_handle(
pub async fn update_plc_handle(
state: &AppState,
did: &crate::types::Did,
did: &tranquil_pds::types::Did,
new_handle: &Handle,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if !did.as_str().starts_with("did:plc:") {
@@ -811,20 +812,21 @@ pub async fn update_plc_handle(
Some(r) => r,
None => return Ok(()),
};
let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
let key_bytes =
tranquil_pds::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
let plc_client = state.plc_client();
let last_op = plc_client.get_last_op(did).await?;
let new_also_known_as = vec![format!("at://{}", new_handle)];
let update_op =
crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
tranquil_pds::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
let signed_op = tranquil_pds::plc::sign_operation(&update_op, &signing_key)?;
plc_client.send_operation(did, &signed_op).await?;
Ok(())
}
pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
let host = match crate::util::get_header_str(&headers, http::header::HOST) {
let host = match tranquil_pds::util::get_header_str(&headers, http::header::HOST) {
Some(h) => h,
None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
};
@@ -1,10 +1,10 @@
use crate::rate_limit::{HandleVerificationLimit, RateLimited};
use crate::types::{Did, Handle};
use axum::{
Json,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use tranquil_pds::rate_limit::{HandleVerificationLimit, RateLimited};
use tranquil_pds::types::{Did, Handle};
#[derive(Deserialize)]
pub struct VerifyHandleOwnershipInput {
@@ -29,7 +29,7 @@ pub async fn verify_handle_ownership(
let handle_str = input.handle.as_str();
let did_str = input.did.as_str();
let dns_mismatch = match crate::handle::resolve_handle_dns(handle_str).await {
let dns_mismatch = match tranquil_pds::handle::resolve_handle_dns(handle_str).await {
Ok(did) if did == did_str => {
return Json(VerifyHandleOwnershipOutput {
verified: true,
@@ -45,7 +45,7 @@ pub async fn verify_handle_ownership(
Err(_) => None,
};
match crate::handle::resolve_handle_http(handle_str).await {
match tranquil_pds::handle::resolve_handle_http(handle_str).await {
Ok(did) if did == did_str => Json(VerifyHandleOwnershipOutput {
verified: true,
method: Some("http".to_string()),
@@ -2,6 +2,7 @@ pub mod account;
pub mod did;
pub mod handle;
pub mod plc;
pub mod provision;
pub use account::create_account;
pub use did::{
@@ -1,26 +1,26 @@
use crate::api::EmptyResponse;
use crate::api::error::{ApiError, DbResultExt};
use crate::auth::{Auth, Permissive};
use crate::state::AppState;
use axum::{
extract::State,
response::{IntoResponse, Response},
};
use chrono::{Duration, Utc};
use tracing::{info, warn};
use tranquil_pds::api::EmptyResponse;
use tranquil_pds::api::error::{ApiError, DbResultExt};
use tranquil_pds::auth::{Auth, Permissive};
use tranquil_pds::state::AppState;
fn generate_plc_token() -> String {
crate::util::generate_token_code()
tranquil_pds::util::generate_token_code()
}
pub async fn request_plc_operation_signature(
State(state): State<AppState>,
auth: Auth<Permissive>,
) -> Result<Response, ApiError> {
if let Err(e) = crate::auth::scope_check::check_identity_scope(
if let Err(e) = tranquil_pds::auth::scope_check::check_identity_scope(
&auth.auth_source,
auth.scope.as_deref(),
crate::oauth::scopes::IdentityAttr::Wildcard,
tranquil_pds::oauth::scopes::IdentityAttr::Wildcard,
) {
return Ok(e);
}
@@ -41,7 +41,7 @@ pub async fn request_plc_operation_signature(
.log_db_err("creating PLC token")?;
let hostname = &tranquil_config::get().server.hostname;
if let Err(e) = crate::comms::comms_repo::enqueue_plc_operation(
if let Err(e) = tranquil_pds::comms::comms_repo::enqueue_plc_operation(
state.user_repo.as_ref(),
state.infra_repo.as_ref(),
user_id,

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