"""Storage for per-user Synapse s2s delivery secrets (postgres, asyncpg).
The secret is verified on every delivery, so unlike api_tokens it cannot be
stored as a hash — it is kept Fernet-encrypted (see navi.auth.encrypt) and is
shown in the UI exactly once, on creation.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from navi.auth.encrypt import get_encryptor
@dataclass
class SynapseTarget:
id: int
user_id: str
token_ref: str
created_at: datetime
revoked_at: datetime | None = None
_CREATE = """
INSERT INTO synapse_targets (user_id, token_ref, secret_enc, created_at)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at
"""
_LIST_FOR_USER = """
SELECT id, user_id, token_ref, created_at, revoked_at
FROM synapse_targets WHERE user_id = $1 AND revoked_at IS NULL
ORDER BY created_at DESC
"""
_REVOKE = """
UPDATE synapse_targets SET revoked_at = $2 WHERE id = $1 AND user_id = $3 AND revoked_at IS NULL
"""
# For the gateway: every active secret, decrypted, with its owner.
_ALL_ACTIVE_SECRETS = """
SELECT t.id, t.user_id, t.token_ref, t.secret_enc
FROM synapse_targets t
WHERE t.revoked_at IS NULL
"""
class SynapseTargetStore:
def __init__(self, pool):
self._pool = pool
async def create(self, user_id: str, token_ref: str, secret: str) -> SynapseTarget:
row = await self._pool.fetchrow(
_CREATE, user_id, token_ref, get_encryptor().encrypt(secret),
datetime.now(timezone.utc),
)
return SynapseTarget(
id=row["id"], user_id=user_id, token_ref=token_ref, created_at=row["created_at"],
)
async def list_for_user(self, user_id: str) -> list[SynapseTarget]:
rows = await self._pool.fetch(_LIST_FOR_USER, user_id)
return [
SynapseTarget(
id=r["id"], user_id=r["user_id"], token_ref=r["token_ref"],
created_at=r["created_at"], revoked_at=r["revoked_at"],
)
for r in rows
]
async def revoke(self, target_id: int, user_id: str) -> bool:
result = await self._pool.fetchval(
_REVOKE, target_id, datetime.now(timezone.utc), user_id,
)
return result is not None
async def active_secrets(self) -> list[dict]:
"""[(id, user_id, token_ref, secret_plain)] — ordered by creation."""
rows = await self._pool.fetch(_ALL_ACTIVE_SECRETS)
decrypt = get_encryptor().decrypt
return [
{
"id": r["id"],
"user_id": r["user_id"],
"token_ref": r["token_ref"],
"secret": decrypt(r["secret_enc"]),
}
for r in rows
]