global-identifier
v2.1.1
Published
Type-safe, compact identifiers with optional encryption support
Readme
global-identifier
Type-safe, compact, and cryptographically secure identifiers for distributed systems.
Why global-identifier?
Modern distributed systems need identifiers that are:
- Self-describing – Know what type of entity an ID refers to at a glance
- Compact – Efficient for URLs, databases, and wire transfer
- Type-safe – Prevent mixing up User IDs with Order IDs at compile time
- Secure – Encrypt sensitive data, sign for authenticity
- Sortable – Chronologically ordered for time-series data
global-identifier delivers all of this in a zero-dependency*, developer-friendly package.
*CBOR encoding uses the cbor-x package for optimal performance.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Features
- Advanced Usage
- API Reference
- Performance
- Security
- Best Practices
- FAQ
- Contributing
- License
Installation
npm install global-identifierOptional peer dependencies:
# For Zod runtime validation
npm install zodQuick Start
import { Identifier, createCodec } from "global-identifier";
// 1. Define your type mapping
const codec = createCodec({
types: {
User: "usr",
Order: "ord",
Product: "prd"
}
});
// 2. Create identifiers
const userId = new Identifier("User", { id: "abc-123", name: "Alice" });
const orderId = new Identifier("Order", "order-456");
// 3. Encode to compact strings
const encodedUser = codec.encode(userId);
// → "usr_o2JpZGdhYmMtMTIzZG5hbWVlQWxpY2U"
const encodedOrder = codec.encode(orderId);
// → "ord_aW9yZGVyLTQ1Ng"
// 4. Decode back to typed identifiers
const decoded = codec.decode<"User", { id: string; name: string }>(encodedUser);
console.log(decoded.type); // "User"
console.log(decoded.data); // { id: "abc-123", name: "Alice" }Core Concepts
Identifier
The Identifier class is an immutable container holding:
type: A string identifying what kind of entity this isdata: Any serializable payload (string, number, object, array)
// String data
const userId = new Identifier("User", "user-123");
// Object data
const orderId = new Identifier("Order", {
orderId: "ord-456",
customerId: "cust-789",
items: ["item-1", "item-2"]
});
// Immutability
userId.data = "new-value"; // ❌ Error: Cannot assign to read-only propertyKey characteristics:
| Property | Description |
| ------------ | -------------------------------------------------- |
| Immutable | Instance is frozen after creation |
| Generic | Identifier<TType, TData> for full type inference |
| Serializable | toJSON() / fromJSON() for easy persistence |
| Comparable | equals() method with deep equality |
Codec
A Codec handles encoding Identifiers to compact strings and decoding them back:
const codec = createCodec({
types: { User: "usr", Order: "ord" }
});
// Encoding: Identifier → String
const encoded = codec.encode(new Identifier("User", "data"));
// Decoding: String → Identifier
const decoded = codec.decode(encoded);Encoding pipeline:
Identifier { type, data }
↓
CBOR encode (data → binary)
↓
Base64URL encode (binary → URL-safe string)
↓
Prefix/suffix type indicator
↓
"usr_b2JqZWN0LWRhdGE"Type Mapping
The type mapping is a simple object that associates:
- Type names (what you use in code):
'User','Order','Product' - Short prefixes (what appears in encoded strings):
'usr','ord','prd'
const types = {
User: "usr", // 3 chars
Organization: "org", // Shortened from 12 chars
Transaction: "txn" // Domain-specific abbreviation
} as const;Why mappings?
- Shorter IDs:
usr_abcvsUser_abc - Obfuscation: Don't expose internal type names
- Flexibility: Change internal names without breaking encoded IDs
Features
Configurable Encoding
Default Encoding
const codec = createCodec({
types: { User: "usr" }
});
codec.encode(new Identifier("User", "data"));
// → "usr_ZGF0YQ"Custom Separator
// Colon separator
const codec = createCodec({
types: { User: "usr" },
separator: ":"
});
codec.encode(new Identifier("User", "data"));
// → "usr:ZGF0YQ"
// Dash separator
const codec = createCodec({
types: { User: "usr" },
separator: "-"
});
codec.encode(new Identifier("User", "data"));
// → "usr-ZGF0YQ"
// No separator
const codec = createCodec({
types: { User: "usr" },
separator: ""
});
codec.encode(new Identifier("User", "data"));
// → "usrZGF0YQ"Type Position
// Prefix (default) - type at start
const prefixCodec = createCodec({
types: { User: "usr" },
typePosition: "prefix"
});
// → "usr_ZGF0YQ"
// Suffix - type at end
const suffixCodec = createCodec({
types: { User: "usr" },
typePosition: "suffix"
});
// → "ZGF0YQ_usr"Sortable IDs
Enable lexicographically sortable IDs with embedded timestamps:
const codec = createCodec({
types: { Event: "evt" },
sortable: { enabled: true }
});
// IDs include a Crockford Base32 timestamp
const id1 = codec.encode(new Identifier("Event", "first"));
// → "evt_01JFQK4X20ZGlyc3Q"
// Later ID sorts after earlier ID
const id2 = codec.encode(new Identifier("Event", "second"));
// → "evt_01JFQK5Y30c2Vjb25k"
// Lexicographic sorting = chronological sorting!
[id2, id1].sort(); // → [id1, id2]Sortable configuration:
const codec = createCodec({
types: { Event: "evt" },
sortable: {
enabled: true,
precision: "millisecond", // or 'second'
epoch: new Date("2024-01-01"), // Custom epoch for shorter IDs
length: 10 // Timestamp characters (10 = ~35 years)
},
typePosition: "suffix" // Better for cross-type sorting
});Pro tip: Use
typePosition: 'suffix'with sortable IDs so the timestamp is at the start, enabling sorting across different entity types.
Codec Extension & Multi-Format Support
Extending a Codec
const userCodec = createCodec({
types: { User: "usr" },
separator: ":"
});
// Extend with additional types
const extendedCodec = userCodec.extend({
types: { Order: "ord", Product: "prd" }
});
// Extended codec handles all types
extendedCodec.encode(new Identifier("User", "data")); // ✅
extendedCodec.encode(new Identifier("Order", "data")); // ✅
extendedCodec.encode(new Identifier("Product", "data")); // ✅Merging Codecs
import { mergeCodecs } from "global-identifier";
const codec1 = createCodec({ types: { User: "usr" } });
const codec2 = createCodec({ types: { Order: "ord" } });
const codec3 = createCodec({ types: { Product: "prd" } });
const merged = mergeCodecs(codec1, codec2, codec3);Multi-Format Codec (Migration Support)
When migrating from one encoding format to another:
import { createMultiCodec } from "global-identifier";
// Old format (legacy)
const legacyCodec = createCodec({
types: { User: "usr" },
separator: ":"
});
// New format
const newCodec = createCodec({
types: { User: "usr", Order: "ord" },
separator: "_"
});
// Multi-codec decodes BOTH formats
const multi = createMultiCodec(newCodec, legacyCodec);
multi.decode("usr:bGVnYWN5"); // ✅ Decodes legacy format
multi.decode("usr_bmV3"); // ✅ Decodes new format
multi.encode(id); // Uses new format (primary codec)Symmetric Encryption
Encrypt identifiers with AES-256-GCM using password-based key derivation:
import { Identifier, encrypt, decrypt } from "global-identifier";
// Create identifier with sensitive data
const sensitiveId = new Identifier("User", {
ssn: "123-45-6789",
email: "[email protected]"
});
// Encrypt with password
const encrypted = await encrypt(sensitiveId, {
password: "strong-password-here"
});
// Decrypt
const decrypted = await decrypt(encrypted, {
password: "strong-password-here"
});
console.log(decrypted.data.ssn); // '123-45-6789'With pre-generated key:
import { generateKey, encrypt, decrypt } from "global-identifier";
// Generate a secure key
const key = await generateKey();
// Encrypt/decrypt with key (faster, no key derivation)
const encrypted = await encrypt(id, { key });
const decrypted = await decrypt(encrypted, { key });Serialization for storage/transfer:
import { serializeEncrypted, deserializeEncrypted } from "global-identifier";
// Serialize to Base64URL string
const serialized = serializeEncrypted(encrypted);
// → "eyJjIjoiYWJj..."
// Deserialize back
const deserialized = deserializeEncrypted(serialized);
const decrypted = await decrypt(deserialized, { password });Asymmetric Encryption
Use ECDH for secure key exchange and encryption:
import { Identifier, generateEncryptionKeyPair, encryptAsymmetric, decryptAsymmetric, exportKeyPair } from "global-identifier";
// Generate key pair (keep privateKey secret!)
const keyPair = await generateEncryptionKeyPair("P-256");
// Export for storage/distribution
const exported = await exportKeyPair(keyPair, "encryption");
// Share exported.publicKey, keep exported.privateKey secret
// Encrypt with public key
const encrypted = await encryptAsymmetric(new Identifier("User", "secret-data"), keyPair.publicKey);
// Decrypt with private key
const decrypted = await decryptAsymmetric(encrypted, keyPair.privateKey);Supported curves:
| Curve | Security | Performance | | ----- | -------- | ----------- | | P-256 | 128-bit | Fastest | | P-384 | 192-bit | Balanced | | P-521 | 256-bit | Most secure |
Digital Signatures
Sign identifiers for authenticity verification (JWT-like):
import { Identifier, generateSigningKeyPair, sign, verify } from "global-identifier";
// Generate signing key pair
const keyPair = await generateSigningKeyPair("P-256");
// Sign an identifier
const signed = await sign(new Identifier("Transaction", { amount: 1000, currency: "USD" }), keyPair.privateKey, {
kid: "key-2024-01", // Key ID for rotation
iss: "payment-service", // Issuer
expiresIn: 3600 // 1 hour expiration
});
// Verify signature
const result = await verify(signed, keyPair.publicKey);
if (result.valid) {
console.log("Verified:", result.id.data);
} else {
console.error("Invalid:", result.error);
if (result.expired) {
console.error("Signature has expired");
}
}Signed identifier structure:
{
id: Identifier, // The signed identifier
signature: string, // ECDSA signature (Base64URL)
kid: string, // Key ID
alg: string, // Algorithm (ES256, ES384, ES512)
iat: string, // Issued at (ISO 8601)
iss?: string, // Issuer
exp?: string, // Expiration (ISO 8601)
}JWKS Key Management
Enterprise-grade key management with JSON Web Key Sets:
import { createKeyStore, KeyStore } from "global-identifier";
// Create a key store with signing and encryption keys
const keyStore = await createKeyStore({
signingKeys: 2, // Generate 2 signing keys (for rotation)
encryptionKeys: 1, // Generate 1 encryption key
defaultCurve: "P-256"
});
// Get JWKS for public distribution
const jwks = keyStore.getJWKS();
// Publish at: https://your-service.com/.well-known/jwks.json
// Get active keys for operations
const signingKey = keyStore.getActiveSigningKey();
const encryptionKey = keyStore.getActiveEncryptionKey();
// Sign with key from store
const signed = await sign(id, signingKey.privateKey, {
kid: signingKey.kid,
iss: "your-service"
});Fetching keys from remote JWKS:
import { createJWKSFetcher } from "global-identifier";
const fetcher = createJWKSFetcher({
issuer: "https://auth.example.com",
jwksUri: "https://auth.example.com/.well-known/jwks.json",
cacheDuration: 3600000 // Cache for 1 hour
});
// Fetch and cache public keys
const keys = await fetcher.fetchKeys();
// Find key by ID
const key = await fetcher.getKey("key-2024-01");
if (key) {
const result = await verify(signed, key);
}Zod Validation
Runtime validation with Zod schemas:
import { z } from "zod";
import { createValidated, validate } from "global-identifier/zod";
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(0).max(150)
});
// Create validated identifier
const userId = createValidated("User", UserSchema, {
id: "550e8400-e29b-41d4-a716-446655440000",
email: "[email protected]",
age: 30
});
// ✅ Type-safe: userId.data is typed as z.infer<typeof UserSchema>
// Validation error
createValidated("User", UserSchema, {
id: "not-a-uuid",
email: "invalid-email",
age: -5
});
// ❌ Throws ValidationError with Zod issues
// Validate existing identifier
const validated = validate(existingId, UserSchema);Advanced Usage
Custom Separators
Choose separators based on your use case:
| Separator | Example | Best For |
| ------------- | ------------ | --------------------- |
| _ (default) | usr_abc123 | General purpose |
| : | usr:abc123 | URN-style identifiers |
| - | usr-abc123 | URL slugs |
| . | usr.abc123 | Domain-like |
| `` (none) | usrabc123 | Maximum compactness |
Type Position (Prefix vs Suffix)
Prefix (default):
usr_ZGF0YQ
^^^
Type first - good for filtering by typeSuffix:
ZGF0YQ_usr
^^^
Type last - better with sortable timestampsBranded Types
Prevent mixing up different ID types at compile time:
import { Identifier, type BrandedIdentifier } from "global-identifier";
// Define branded types
type UserId = BrandedIdentifier<"User", string, "UserId">;
type OrderId = BrandedIdentifier<"Order", string, "OrderId">;
// Create branded IDs
const userId = new Identifier("User", "user-123") as UserId;
const orderId = new Identifier("Order", "order-456") as OrderId;
// Type-safe functions
function processUser(id: UserId) {
/* ... */
}
function processOrder(id: OrderId) {
/* ... */
}
processUser(userId); // ✅ OK
processUser(orderId); // ❌ Type error!Migration Between Formats
Safely migrate from one encoding format to another:
import { createCodec, createMultiCodec } from "global-identifier";
// Phase 1: Read both, write new
const legacyCodec = createCodec({
types: { User: "usr" },
separator: ":"
});
const newCodec = createCodec({
types: { User: "usr" },
separator: "_"
});
const multiCodec = createMultiCodec(newCodec, legacyCodec);
// Read from any format
function getUser(id: string) {
const decoded = multiCodec.decode(id); // Works with both formats
return fetchUser(decoded.data);
}
// Write in new format
function createUser(data: UserData) {
const id = new Identifier("User", data);
return multiCodec.encode(id); // Always uses new format
}
// Phase 2: Once all IDs migrated, remove legacy codec
const finalCodec = newCodec;API Reference
Core
| Export | Description |
| -------------------- | ----------------------------------------------- |
| Identifier | Immutable identifier class |
| GlobalId | Alias for Identifier (backward compatibility) |
| createCodec() | Create a new codec |
| mergeCodecs() | Merge multiple codecs |
| createMultiCodec() | Create multi-format decoder |
Encoding
| Export | Description |
| ------------------- | ------------------------- |
| encodeBase64Url() | Encode bytes to Base64URL |
| decodeBase64Url() | Decode Base64URL to bytes |
| encodeCbor() | Encode value to CBOR |
| decodeCbor() | Decode CBOR to value |
Symmetric Crypto
| Export | Description |
| ------------------------ | -------------------------------- |
| encrypt() | Encrypt identifier (AES-256-GCM) |
| decrypt() | Decrypt identifier |
| generateKey() | Generate AES key |
| deriveKey() | Derive key from password |
| serializeEncrypted() | Serialize encrypted data |
| deserializeEncrypted() | Deserialize encrypted data |
Asymmetric Crypto
| Export | Description |
| ----------------------------- | --------------------------- |
| generateEncryptionKeyPair() | Generate ECDH key pair |
| generateSigningKeyPair() | Generate ECDSA key pair |
| encryptAsymmetric() | Encrypt with public key |
| decryptAsymmetric() | Decrypt with private key |
| sign() | Sign identifier |
| verify() | Verify signature |
| exportKeyPair() | Export keys to JWK |
| importPublicKey() | Import public key from JWK |
| importPrivateKey() | Import private key from JWK |
JWKS
| Export | Description |
| --------------------- | -------------------------- |
| KeyStore | In-memory key store |
| createKeyStore() | Create key store with keys |
| JWKSFetcher | Remote JWKS fetcher |
| createJWKSFetcher() | Create JWKS fetcher |
Errors
| Export | Description |
| -------------------- | ----------------------- |
| IdentifierError | Base error class |
| EncodingError | Encoding failures |
| DecodingError | Decoding failures |
| UnknownTypeError | Unknown type name |
| UnknownPrefixError | Unknown type prefix |
| EncryptionError | Encryption failures |
| DecryptionError | Decryption failures |
| KeyDerivationError | Key derivation failures |
| ValidationError | Validation failures |
Performance
Benchmarks on Node.js 22, Apple M1 Pro:
| Operation | Speed | Notes | | ------------------ | ------------- | -------------- | | Encode (simple) | ~950K ops/sec | String data | | Encode (complex) | ~520K ops/sec | Nested objects | | Decode (simple) | ~1.2M ops/sec | String data | | Decode (complex) | ~680K ops/sec | Nested objects | | Symmetric encrypt | ~15K ops/sec | AES-256-GCM | | Symmetric decrypt | ~18K ops/sec | AES-256-GCM | | Asymmetric encrypt | ~2K ops/sec | ECDH + AES | | Sign | ~5K ops/sec | ECDSA P-256 | | Verify | ~3K ops/sec | ECDSA P-256 |
Size comparison:
| Format | 'user-123' | { id: 'abc', name: 'Alice' } |
| ----------------- | ---------- | ------------------------------ |
| JSON | 31 chars | 53 chars |
| global-identifier | 17 chars | 33 chars |
| Savings | 45% | 38% |
Security
Algorithms
| Purpose | Algorithm | Details | | -------------------- | ------------- | -------------------- | | Symmetric encryption | AES-256-GCM | 128-bit auth tag | | Key derivation | PBKDF2-SHA256 | 100K iterations | | Key exchange | ECDH | P-256, P-384, P-521 | | Signing | ECDSA | P-256, P-384, P-521 | | Encoding | Base64URL | URL-safe, no padding |
Best Practices
- Passwords: Use strong passwords (12+ characters, mixed case, numbers, symbols)
- Keys: Store private keys in secure vaults (AWS KMS, Google Secret Manager, HashiCorp Vault)
- Rotation: Rotate signing keys periodically (use
kidfor key identification) - Expiration: Set reasonable expiration times for signed identifiers
- HTTPS: Always transmit encrypted/signed data over HTTPS
What global-identifier Does NOT Do
- ❌ Store keys (use a secure vault)
- ❌ Manage user passwords (use a proper auth system)
- ❌ Provide authentication (use with an auth layer)
- ❌ Replace UUIDs for primary keys (use alongside them)
Best Practices
Choosing Type Prefixes
// ✅ Good: Short, meaningful, unique
const types = {
User: "usr",
Organization: "org",
Transaction: "txn",
Invoice: "inv"
};
// ❌ Bad: Too long, confusing, colliding
const types = {
User: "user", // Too long
UserAccount: "usr", // Collision with User
Transaction: "t" // Too short, unclear
};When to Use Each Feature
| Use Case | Feature | | --------------------------- | --------------------- | | Public IDs in URLs | Basic encoding | | Time-ordered events | Sortable IDs | | Sensitive data | Symmetric encryption | | Cross-service communication | Asymmetric encryption | | API authentication | Digital signatures | | Microservices | JWKS key management | | Form validation | Zod integration |
Error Handling
import { codec, isDecodingError, isEncryptionError } from "global-identifier";
try {
const id = codec.decode(userInput);
} catch (error) {
if (isDecodingError(error)) {
// Invalid format, unknown prefix, etc.
console.error("Invalid ID format:", error.message);
}
throw error;
}
// Or use safe decode
const result = codec.decodeSafe(userInput);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}FAQ
Why not just use UUIDs?
UUIDs are great for unique identifiers but lack:
- Type information (what entity is this?)
- Payload capacity (just a random string)
- Sortability (random order)
- Encryption (plaintext only)
Use global-identifier when you need these features. Use UUIDs as the data payload for uniqueness.
Can I use this in the browser?
Yes! All crypto operations use the Web Crypto API, which is available in all modern browsers.
How do I handle key rotation?
Use the JWKS features:
- Generate new key, keep old key
- Sign new tokens with new key
- Verify with both keys (by
kid) - After grace period, remove old key
What's the maximum data size?
Practical limit is a few KB. For larger payloads, store the data elsewhere and use global-identifier for the reference.
Is it compatible with existing systems?
Yes! Use createMultiCodec to decode legacy formats while encoding in your preferred format.
Contributing
Contributions are welcome! Please see our Contributing Guide.
# Clone the repository
git clone https://github.com/anthropics/global-identifier.git
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run buildLicense
MIT License - see LICENSE for details.
