first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# `sharing/tokens/tokens.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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`.