first commit
This commit is contained in:
105
docs/networking/drive/drive-sharing/src/sharing/tokens/tokens.md
Normal file
105
docs/networking/drive/drive-sharing/src/sharing/tokens/tokens.md
Normal 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`.
|
||||
Reference in New Issue
Block a user