Capability links: hand someone a link to one thing, with no account. The token is the permission; a password may gate it; it expires; it can be revoked; password attempts are limited. Standard library only, no web framework.
pip install caplinks
from caplinks import CapabilityLinks
links = CapabilityLinks() # records in a dict; pass any MutableMapping to persist them
rec = links.create({"doc": "report-1"}, password="open sesame", expires="7d")
url = f"https://example.com/s/{rec['token']}/"
state, stored = links.status(rec["token"]) # "ok", "expired" or "missing"
links.check_password(stored, "open sesame") # True
cookie = links.unlock_value(stored) # what a correct password earns
links.unlocked(stored, cookie, max_age_s=3600) # True, until the password changes or the link is revoked
links.revoke(rec["token"]) # now "missing"CapabilityLinks: create, read, list and revoke links, and gate them on a password. Records are plain dicts in anyMutableMappingkeyed by token: the generic fields aretoken,created,expires,password(a hash) andrevoked; your own fields (what the link reaches) sit beside them.- Gating an id you already have:
create(..., token=existing_id)keeps the id;set_password(token, pw_or_None)andset_expires(token, when)change a live link later (a new password ends every open unlock). AttemptGuard: the per-link lock-out.allow(link, client)before checking a password,failed(link)after a wrong one; one client is limited on its own, and a link that takes too many wrong passwords from everyone is locked for a while.JsonRecords(texts)andFileTexts(folder): a readystoreforCapabilityLinks, records as<token>.jsonin a text mapping (a local folder: atomic, mode 0600). Passvalid=to check your own fields anddeletable=Falseto keep revoked links as tombstones.ScryptHasher(stdlib scrypt) behind thePasswordHasherprotocol (hash,verify -> bool): passhasher=for argon2 or anything else.parse_expiresreads what people write (30m,12h,7d,2w,2026-11-01meaning the end of that day UTC, an ISO timestamp,never) and refuses the past;is_expiredfails closed.SlidingWindowLimiterbehind theRateLimiterprotocol (allow(key) -> bool): count password attempts per client, per link, or both. It is per process; a shared limiter of the same shape (Redis, say) fits where several workers serve the links.
The unlock value is an HMAC keyed by the stored password hash, so changing the password or revoking the link (which clears the hash) ends every open session at once.
What a link reaches (its scope), how it is served (routes, cookie name, headers), and the attempt policy. annals (share links) is the first host.
| keyword | default | for |
|---|---|---|
store |
dict |
persistence: a dol store, a file-backed mapping, S3 |
hasher |
ScryptHasher() |
argon2id or another scheme |
clock |
now_utc |
tests that pin the time |
min_password_len |
8 | a stricter or laxer gate |
A store that cannot vouch for a record raises KeyError, ValueError or OSError when it is read, and the link is then missing; so is a record whose generic fields have the wrong types. Every failure closes the link.