
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@tact-ddd/ids
Advanced tools
**Type-safe, prefixed, and globally unique identifiers for Domain-Driven Design**
@tact-ddd/idsType-safe, prefixed, and globally unique identifiers for Domain-Driven Design
You can think of this as: "one global friendly alphabet + a tiny factory that makes branded, prefixed ID types."
This package provides:
0/O or 1/l)ws_, usr_, ord_, etc.)WorkspaceId and UserIdInspired by Unkey's ID implementation.
npm install @tact-ddd/ids
# or
bun add @tact-ddd/ids
# or
pnpm add @tact-ddd/ids
import { defineIdType } from '@tact-ddd/ids';
// Define a User ID type
export const UserIdFactory = defineIdType({ prefix: 'usr' });
export type UserId = ReturnType<(typeof UserIdFactory)['parse']>;
// Create a new ID
const userId = UserIdFactory.create();
// => "usr_A3fK8mN9pQrS2tUv"
// Use it in your domain
function findUser(id: UserId) {
// ...
}
findUser(userId); // ✅ OK
The package uses TypeScript's nominal typing pattern to create distinct types at compile-time while keeping them as strings at runtime:
export type Brand<TBase, TBrand extends string> = TBase & {
readonly __brand: TBrand;
};
This ensures that UserId and WorkspaceId are treated as different types by TypeScript, even though they're both strings under the hood.
The package uses a Base58-like alphabet without ambiguous characters:
const FRIENDLY_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
This excludes:
0 (zero) and O (capital o)1 (one), l (lowercase L), and I (capital i)The defineIdType function creates a factory for each ID type with:
WorkspaceId)create() function that generates prefix_xxx stringsparse() function that validates and brands existing stringsis() type guard for runtime checksdefineIdType<P>(config: IdConfig<P>)Creates a type-safe ID factory with the specified configuration.
interface IdConfig<P extends string> {
prefix: P; // The prefix for this ID type (e.g., "usr", "ws")
length?: number; // Optional: override default length (default: 16)
strict?: boolean; // Optional: enforce alphabet validation (default: false)
}
An object with the following methods and properties:
{
prefix: string; // The prefix used
length: number; // The core ID length (excluding prefix)
create: () => Id; // Generate a new ID
parse: (raw: string) => Id; // Validate and brand an existing string
is: (raw: string) => raw is Id; // Type guard for runtime checks
}
import { defineIdType } from '@tact-ddd/ids';
// Define your ID types
export const WorkspaceIdFactory = defineIdType({ prefix: 'ws' });
export type WorkspaceId = ReturnType<(typeof WorkspaceIdFactory)['parse']>;
export const UserIdFactory = defineIdType({ prefix: 'usr' });
export type UserId = ReturnType<(typeof UserIdFactory)['parse']>;
// Create new IDs
const workspaceId = WorkspaceIdFactory.create();
// => "ws_A3fK8mN9pQrS2tUv"
const userId = UserIdFactory.create();
// => "usr_X7yZ4bC6dE9fG2hJ"
TypeScript prevents you from mixing different ID types:
function loadWorkspace(id: WorkspaceId) {
// ...
}
function findUser(id: UserId) {
// ...
}
const workspaceId = WorkspaceIdFactory.create();
const userId = UserIdFactory.create();
loadWorkspace(workspaceId); // ✅ OK
findUser(userId); // ✅ OK
// These cause compile-time errors:
loadWorkspace(userId); // ❌ Type error!
findUser(workspaceId); // ❌ Type error!
// Plain strings don't work either:
const plainString = 'ws_abc123';
loadWorkspace(plainString); // ❌ Type error!
Use parse() to validate and brand IDs from external sources (e.g., databases, APIs):
const OrderIdFactory = defineIdType({ prefix: 'ord' });
export type OrderId = ReturnType<(typeof OrderIdFactory)['parse']>;
// From a database or API
const rawId = 'ord_K8mN9pQrS2tU';
try {
const orderId = OrderIdFactory.parse(rawId);
// orderId is now branded as OrderId
processOrder(orderId);
} catch (error) {
console.error('Invalid order ID:', error.message);
}
// Invalid examples that throw errors:
OrderIdFactory.parse('usr_abc123'); // ❌ Wrong prefix
OrderIdFactory.parse('ord_123'); // ❌ Wrong length
OrderIdFactory.parse('ord_'); // ❌ Empty core
Different ID types can have different lengths:
// Short IDs for frequently used entities
const SessionIdFactory = defineIdType({
prefix: 'sess',
length: 12,
});
// => "sess_A3fK8mN9pQr"
// Longer IDs for permanent entities
const DocumentIdFactory = defineIdType({
prefix: 'doc',
length: 24,
});
// => "doc_A3fK8mN9pQrS2tUvW4xY7zA2"
Enable strict mode to validate that IDs only contain characters from the friendly alphabet:
const ProductIdFactory = defineIdType({
prefix: 'prod',
length: 16,
strict: true,
});
// Valid - uses only friendly alphabet characters
ProductIdFactory.parse('prod_A3fK8mN9pQrS2tUv'); // ✅ OK
// Invalid - contains disallowed characters
ProductIdFactory.parse('prod_0O1lI_invalid!!'); // ❌ Throws error
Without strict mode (default), parse() only checks prefix and length.
Use the is() method for runtime type checks:
const WorkspaceIdFactory = defineIdType({ prefix: 'ws' });
function handleId(raw: string) {
if (WorkspaceIdFactory.is(raw)) {
// TypeScript knows 'raw' is WorkspaceId here
loadWorkspace(raw);
} else {
console.log('Not a valid workspace ID');
}
}
Create a single source of truth for all your ID prefixes:
// id-registry.ts
import { defineIdType } from '@tact-ddd/ids';
export const prefixes = {
workspace: 'ws',
user: 'usr',
team: 'team',
project: 'proj',
task: 'task',
comment: 'cmt',
api_key: 'key',
} as const;
export type IdKind = keyof typeof prefixes;
// Define all ID factories
export const WorkspaceIdFactory = defineIdType({ prefix: prefixes.workspace });
export type WorkspaceId = ReturnType<(typeof WorkspaceIdFactory)['parse']>;
export const UserIdFactory = defineIdType({ prefix: prefixes.user });
export type UserId = ReturnType<(typeof UserIdFactory)['parse']>;
export const TeamIdFactory = defineIdType({ prefix: prefixes.team });
export type TeamId = ReturnType<(typeof TeamIdFactory)['parse']>;
// ... export other factories
// Optional: Generic ID creator
export function createId(kind: IdKind) {
switch (kind) {
case 'workspace':
return WorkspaceIdFactory.create();
case 'user':
return UserIdFactory.create();
case 'team':
return TeamIdFactory.create();
case 'project':
return ProjectIdFactory.create();
case 'task':
return TaskIdFactory.create();
case 'comment':
return CommentIdFactory.create();
case 'api_key':
return ApiKeyIdFactory.create();
}
}
Use branded IDs in your domain entities:
import { WorkspaceId, UserId } from './id-registry';
interface Workspace {
id: WorkspaceId;
name: string;
ownerId: UserId;
createdAt: Date;
}
interface User {
id: UserId;
email: string;
workspaceIds: WorkspaceId[];
}
// Repository with type-safe IDs
class WorkspaceRepository {
async findById(id: WorkspaceId): Promise<Workspace | null> {
// Implementation
}
async save(workspace: Workspace): Promise<void> {
// Implementation
}
}
When working with databases, parse IDs on the way in and out:
import { WorkspaceIdFactory, WorkspaceId } from './id-registry';
// Reading from database
async function getWorkspaceFromDb(rawId: string): Promise<Workspace> {
const id = WorkspaceIdFactory.parse(rawId); // Validate and brand
const row = await db.query('SELECT * FROM workspaces WHERE id = $1', [id]);
return {
id,
name: row.name,
// ...
};
}
// Writing to database
async function saveWorkspace(workspace: Workspace): Promise<void> {
await db.query(
'INSERT INTO workspaces (id, name) VALUES ($1, $2)',
[workspace.id, workspace.name] // ID is already a string at runtime
);
}
Validate IDs from request parameters:
import { WorkspaceIdFactory } from './id-registry';
app.get('/workspaces/:id', async (req, res) => {
try {
const workspaceId = WorkspaceIdFactory.parse(req.params.id);
const workspace = await workspaceRepo.findById(workspaceId);
if (!workspace) {
return res.status(404).json({ error: 'Workspace not found' });
}
res.json(workspace);
} catch (error) {
res.status(400).json({ error: 'Invalid workspace ID' });
}
});
The package includes comprehensive tests. Run them with:
bun test
Example test patterns:
import { describe, expect, test } from 'bun:test';
import { defineIdType } from '@tact-ddd/ids';
describe('ID Generation', () => {
test('creates valid IDs with correct format', () => {
const UserId = defineIdType({ prefix: 'user', length: 10 });
const id = UserId.create();
expect(typeof id).toBe('string');
expect(id.startsWith('user_')).toBe(true);
expect(id.length).toBe('user_'.length + 10);
});
test('parses valid IDs and rejects invalid ones', () => {
const OrderId = defineIdType({ prefix: 'order', length: 12 });
const validId = OrderId.create();
expect(OrderId.parse(validId)).toBe(validId);
expect(() => OrderId.parse('invalid_123')).toThrow();
expect(() => OrderId.parse('order_123')).toThrow(); // Too short
});
});
Export both the factory and the type:
export const UserIdFactory = defineIdType({ prefix: 'usr' });
export type UserId = ReturnType<(typeof UserIdFactory)['parse']>;
Use descriptive prefixes: Keep them short (2-4 chars) but meaningful
usr, ws, proj, ordu, w, p, o (too cryptic)user, workspace (too long)Parse IDs at boundaries: Always parse IDs from external sources (APIs, databases) before using them in your domain logic
Use strict mode for user-facing IDs: Enable strict: true when parsing IDs that users might manually enter
Document your prefix registry: Keep all your prefixes in one place with documentation
MIT
FAQs
**Type-safe, prefixed, and globally unique identifiers for Domain-Driven Design**
We found that @tact-ddd/ids demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.