Skip to content
i2mintPublic

About

Capability links: unguessable token, optional password gate, expiry, revocation, attempt limits. Stdlib only.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

caplinks

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"

What is in it

  • CapabilityLinks: create, read, list and revoke links, and gate them on a password. Records are plain dicts in any MutableMapping keyed by token: the generic fields are token, created, expires, password (a hash) and revoked; 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) and set_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) and FileTexts(folder): a ready store for CapabilityLinks, records as <token>.json in a text mapping (a local folder: atomic, mode 0600). Pass valid= to check your own fields and deletable=False to keep revoked links as tombstones.
  • ScryptHasher (stdlib scrypt) behind the PasswordHasher protocol (hash, verify -> bool): pass hasher= for argon2 or anything else.
  • parse_expires reads what people write (30m, 12h, 7d, 2w, 2026-11-01 meaning the end of that day UTC, an ISO timestamp, never) and refuses the past; is_expired fails closed.
  • SlidingWindowLimiter behind the RateLimiter protocol (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 stays with your app

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.

Seams

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.

About

Capability links: unguessable token, optional password gate, expiry, revocation, attempt limits. Stdlib only.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages