ror-tokens
v1.0.1
Published
Stateless signed tokens with no expiration. Named after sumud and UN Resolution 194 — the Right of Return.
Maintainers
Readme
Overview
ror is a TypeScript library for issuing and verifying HMAC-signed tokens that do not expire. Tokens remain valid as long as their signature is intact — there is no exp, no nbf, and no silent invalidation after a TTL.
Where JWT assumes access is temporary and must be continuously renewed, ror treats a valid claim as persistent until it is voluntarily relinquished or the signing secret changes.
The library also supports generational transfer (remember()), voluntary surrender with verifiable receipts (relinquish()), and a trust score (weight()) that increases with time held and proximity to the original holder.
Background
ror is named after UN General Assembly Resolution 194 (1948) — the Right of Return — and after sumud (صمود), the Palestinian practice of steadfastness: keeping the keys to one's home and passing them to the next generation.
Resolution 194 affirms that refugees wishing to return to their homes should be permitted to do so. Nearly eight decades later, that right has not been honored. It has also not expired. This library applies the same principle to authentication tokens.
Installation
npm install ror-tokens
# or
yarn add ror-tokensRequires Node.js 18+ (uses the built-in crypto module).
Quick start
import { claim, verify, remember } from "ror-tokens";
const secret = process.env.SECRET!;
// Issue a token
const token = claim({
subject: "user-123",
origin: "your-app",
secret,
});
// Verify — no expiry check
const result = verify(token, secret);
if (result.valid) {
console.log(result.subject, result.weight, result.lineage);
}
// Pass to the next holder
const child = remember(token, { subject: "user-456", secret });Features
- No expiration — tokens have no
expornbffields - HMAC-SHA256 signing — shared-secret model, timing-safe verification
- Generational transfer —
remember()preserves origin and extends lineage - Voluntary surrender —
relinquish()produces a signed, verifiable receipt - Trust scoring —
weight()rewards longevity and original-holder status - Chain of custody — full holder history via
lineage - Zero dependencies — Node.js
cryptoonly
Token format
Wire format:
{base64url(payload)}.{hmac_sha256_signature}Payload:
{
"sub": "user-123",
"origin": "your-app",
"iat": 1750000000,
"generation": 0,
"lineage": ["user-123"],
"ror": true
}| Field | Description |
| ------------ | ------------------------------------------------ |
| sub | Current holder |
| origin | Issuing application (immutable across transfers) |
| iat | Issued-at timestamp (Unix seconds) |
| generation | 0 = original holder; increments on transfer |
| lineage | Ordered list of every holder |
| ror | Marker identifying a ROR token |
| data | Optional caller-defined payload |
API reference
| Function | Description |
| -------- | ----------- |
| claim({ subject, origin, secret, data? }) | Issue a generation-0 token |
| verify(token, secret) | Verify signature; returns age, weight, lineage |
| remember(token, { subject, secret }) | Transfer claim to a new holder |
| relinquish(token, secret) | Voluntary surrender; returns signed receipt |
| verifyReceipt(receipt, secret) | Verify a relinquishment receipt |
| forcedDisplacement(token, { authority, reason? }) | Log forced revocation to stderr |
| weight(token) | Trust score based on age and generation |
| age(token) | Time held by the current holder |
| lineage(token) | Full chain of custody |
| origin(token) | Original issuing application |
| generation(token) | Transfer depth (0 = original) |
| isROR(token) | Check whether a string is a ROR token |
| decode(token) | Decode payload without verifying (inspection only) |
Weight
Trust score formula:
weight = 1.0 + (years_held × 0.5) + (1 / (generation + 1) × 0.5)| Holder | Age | Approx. weight | | ------ | --- | -------------- | | Generation 0 | Day 1 | 1.0 | | Generation 0 | 1 year | 1.5 | | Generation 0 | 5 years | 3.5 | | Generation 1 | 1 year | 1.25 |
Useful for rate limiting, trust tiers, voting weight, or any policy where tenure should matter.
Use cases
| Scenario | How ror helps |
| -------- | --------------- |
| Long-lived API keys | Integrations stay valid without rotation schedules |
| Family subscriptions | Parent issues a claim; dependents inherit via remember() |
| Offline-first apps | Tokens remain valid during extended offline periods |
| B2B partnerships | Partner weight grows automatically with relationship tenure |
| Archive access | Access granted today remains verifiable years later |
| Audit trails | Full holder history and verifiable surrender receipts |
forcedDisplacement()
Forced revocation is logged, not silent. Every call to forcedDisplacement() writes a warning to stderr — there is no suppressWarning option. This records that the holder did not surrender the claim voluntarily.
Note: this function does not invalidate the token on its own. Actual revocation requires secret rotation or an application-level blocklist.
Documentation
Interactive documentation and a browser playground are in the docs/ directory.
| Resource | Description | | -------- | ----------- | | Docs | Full API reference and guides | | Playground | Create and verify tokens in the browser | | Resolution 194 | Background on the Right of Return |
GitHub Pages
- Push the repository to GitHub
- Open Settings → Pages
- Set source to Deploy from a branch, folder
/docs - The site will be available at
https://<username>.github.io/<repo>/
The playground uses Web Crypto in the browser and produces tokens compatible with the ror-tokens npm package.
License
MIT — see LICENSE.
Publishing
See PUBLISHING.md for npm publish steps and package name options.
