Files
skills/docs/networking/drive/drive-sharing/src/sharing/tokens/tokens.md
2026-05-29 08:20:56 +01:00

4.5 KiB

sharing/tokens/tokens.ts

(no submodules)

Purpose

TokenManager issues, validates, revokes, and lists revocable share tokens. Every token carries an HMAC-SHA-256 integrity tag computed under a per-instance master key over the canonical wire form (id || partitionId || createdBy || sorted permissions || createdAt || expiresAt). Validation re-derives the tag in constant time and rejects any tampering. Tokens have a configurable default TTL (24 h) overridable per call via expiresIn.

Features

  • Constant-time integrity verification via constantTimeEqual from @suaveplan/crypto-utils.
  • Injectable Clock for deterministic expiry tests.
  • Configurable defaultTTL per instance (DEFAULT_TOKEN_TTL_MS = 24 h) and per-call expiresIn override.
  • Hard cap on active tokens (TOKEN_MANAGER_MAX_ENTRIES = 10_000_000).
  • EventBusLike integration: token:created / token:revoked carry context but never the integrity tag itself.
  • OTel: share.token.create_total, share.token.validate_total, share.token.revoked_total, plus share.token.create / validate spans.

Usage: Basic Example

import { TokenManager } from "@suaveplan/drive-sharing/sharing/tokens";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";

const tokens = new TokenManager();
const token = tokens.create({
    partitionId: partitionId("p"),
    createdBy: userId("alice"),
    permissions: ["read"],
});
const result = tokens.validate(token.id);
console.log(result.valid); // true

Usage: Advanced Example

import { TokenManager } from "@suaveplan/drive-sharing/sharing/tokens";
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";

let now = 0;
const events: Array<[string, unknown]> = [];
const masterKey = new Uint8Array(32).fill(0x42);
const tokens = new TokenManager({
    clock: () => now,
    defaultTTL: 60_000,
    masterKey,
    events: { on: () => () => {}, emit: (k, p) => events.push([k, p]) },
});

const token = tokens.create({
    partitionId: partitionId("p"),
    createdBy: userId("alice"),
    permissions: ["read"],
    expiresIn: 5_000,
});

now = 1_000;
console.log(tokens.validate(token.id).valid); // true

now = 6_001;
console.log(tokens.validate(token.id)); // { valid: false, reason: "expired" }

tokens.revoke(token.id);
console.log(tokens.validate(token.id)); // { valid: false, reason: "revoked" }

API Reference

class TokenManager

  • constructor(config?: TokenManagerConfig)
  • create(options: CreateTokenOptions): ShareToken
  • validate(id: TokenId): TokenValidationResult
  • revoke(id: TokenId): void
  • list(): ShareToken[]

interface CreateTokenOptions

{ partitionId, createdBy, permissions, expiresIn? }. permissions must be non-empty (Zod-enforced).

interface ShareToken

{ id, partitionId, createdBy, permissions, createdAt, expiresAt, revoked, integrity }.

interface TokenValidationResult

{ valid, token?, reason? } — reason is one of not_found, revoked, expired.

interface TokenManagerConfig

{ clock?, defaultTTL?, masterKey?, events?, idGenerator? }. Default master key is a fresh 32-byte CSPRNG buffer; default TTL is 24 hours.

interface TokenManagerEvents

  • token:created — full token (including the integrity tag, since the consumer who created the token already knows it)
  • token:revoked — { id, partitionId }

Implementation Notes

  • The canonical wire form sorts permissions before serialising so two tokens with the same logical content but different array order produce identical integrity tags. This protects against a subtle bug where a downstream serializer reorders array elements.
  • The HMAC key (masterKey) is held in instance state — it never appears in events, logs, or token bodies. Rotation is done by constructing a new TokenManager; the old one's tokens become invalid because the new master key produces different integrity tags.
  • validate always recomputes the integrity tag, even when the token is revoked or expired. This is deliberate: an attacker who has tampered with the in-memory token map cannot evade detection by also setting revoked = true. (We DO short-circuit on not_found, since a missing token cannot have been tampered with.)
  • The Zod schema rejects expiresIn: 0 and negative values; these would produce already-expired tokens which are dead on arrival.
  • revoke is idempotent so a duplicate revocation does not double-count share.token.revoked_total.