This document describes the implementation of authentication data storage (users, tokens, passwords) using link-cli and Links Theory (associative link-based data representation).
Issue: #2178 - Создай базу данных на теории связей для хранения пользователей, токенов, паролей Implementation Date: 2025-11-06 Technology: link-cli (clink) v2.2.2 - Associative link-based data storage Based on: Links Notation protocol and Links Theory
Links Theory is a mathematical framework for representing data as associative relationships (links). Each link is a triple:
(id: source target)
Where:
- id: Unique identifier for the link
- source: Source node/value
- target: Target node/value
Links can represent:
- Entity types (e.g., "this is a User")
- Relationships (e.g., "this Token belongs to this User")
- Hierarchies (e.g., "this is a child of that")
The authentication data storage uses a hybrid approach:
- link-cli database (
menu.links): Stores relationships and entity types as links - JSON files (
data/auth-data/): Stores actual data content
- Links are perfect for: Entity types, relationships, associations
- JSON files are better for: Complex objects with multiple properties
- Together: Efficient storage and retrieval with flexible querying
Link Schema:
(linkId: userId USER_TYPE_ID)
Where:
userId: Numeric hash of user ID stringUSER_TYPE_ID = 2000: Constant identifying user entities
IMPORTANT: Link notation uses SPACES, not commas. The correct format is (id: source target) without commas.
File Storage:
data/auth-data/users/{userId}.json
File Content:
{
"userId": "user_abc123def456",
"username": "alice",
"email": "alice@example.com",
"profile": {
"firstName": "Alice",
"lastName": "Smith",
"role": "admin",
"department": "Engineering"
},
"createdAt": "2025-11-06T10:30:00.000Z",
"updatedAt": "2025-11-06T15:45:00.000Z"
}Link Schema:
(linkId: tokenId userId)
Where:
tokenId: Numeric hash of token ID stringuserId: Numeric hash of the user who owns this token
Note: No commas between source and target.
File Storage:
data/auth-data/tokens/{tokenId}.json
File Content:
{
"tokenId": "token_xyz789abc123",
"userId": "user_abc123def456",
"apiKey": "sk_live_abc123def456...",
"permissions": ["read", "write", "admin"],
"expiresAt": "2025-12-06T10:30:00.000Z",
"description": "Admin API token",
"createdAt": "2025-11-06T10:30:00.000Z"
}Link Schema:
(linkId: passwordId userId)
Where:
passwordId: Numeric hash of password ID stringuserId: Numeric hash of the user who owns this password
Note: No commas between source and target.
File Storage:
data/auth-data/passwords/{passwordId}.json
File Content:
{
"passwordId": "pwd_def456ghi789",
"userId": "user_abc123def456",
"hash": "a1b2c3d4e5f6...",
"salt": "f6e5d4c3b2a1...",
"algorithm": "pbkdf2-sha512",
"iterations": 100000,
"createdAt": "2025-11-06T10:30:00.000Z"
}backend/monolith/
├── src/
│ ├── services/
│ │ └── linkdb/
│ │ ├── link-db-service.js # Low-level link-cli wrapper
│ │ ├── menu-storage-service.js # Menu storage (existing)
│ │ ├── auth-storage-service.js # Authentication storage (NEW)
│ │ └── __tests__/
│ │ └── AuthStorageService.spec.js # Unit tests
│ └── api/
│ └── routes/
│ └── authDataLinkDB.js # API routes for auth data
└── data/
├── menu.links # Link database (shared)
└── auth-data/ # Auth data files
├── users/
│ ├── user_abc123.json
│ └── user_def456.json
├── tokens/
│ ├── token_xyz789.json
│ └── token_abc123.json
└── passwords/
├── pwd_def456.json
└── pwd_ghi789.json
High-level service for authentication data storage using link-cli.
Key Methods:
User Operations:
createUser(userData): Create a new usergetUser(userId): Get user by IDgetAllUsers(): Get all usersupdateUser(userId, updates): Update user datadeleteUser(userId): Delete user and all associated datafindUserByUsername(username): Find user by usernamefindUserByEmail(email): Find user by email
Password Operations:
setPassword(userId, passwordData): Set/update password for usergetUserPassword(userId): Get user's password datadeletePassword(passwordId): Delete a password
Token Operations:
createToken(userId, tokenData): Create token for usergetToken(tokenId): Get token by IDgetUserTokens(userId): Get all tokens for userdeleteToken(tokenId): Delete a tokenfindTokenByApiKey(apiKey): Find token by API key
Utilities:
getStatistics(): Get storage statisticsclearAllAuthData(): Clear all data (dangerous!)
Example Usage:
import AuthStorageService from './services/auth-storage-service.js';
const authStorage = new AuthStorageService();
// Create a user
const user = await authStorage.createUser({
username: 'alice',
email: 'alice@example.com',
profile: {
firstName: 'Alice',
lastName: 'Smith',
role: 'admin'
}
});
// Set password for user
await authStorage.setPassword(user.userId, {
hash: 'hashed_password_here',
salt: 'random_salt_here',
algorithm: 'pbkdf2-sha512',
iterations: 100000
});
// Create API token for user
const token = await authStorage.createToken(user.userId, {
apiKey: 'sk_live_abc123...',
permissions: ['read', 'write'],
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
description: 'Admin API token'
});
// Find user by username
const foundUser = await authStorage.findUserByUsername('alice');
// Verify token
const verifiedToken = await authStorage.findTokenByApiKey('sk_live_abc123...');
// Delete user (cascade deletes tokens and passwords)
await authStorage.deleteUser(user.userId);Base Path: /api/auth-data
Create a new user
Request:
{
"username": "alice",
"email": "alice@example.com",
"password": "secure_password_123",
"profile": {
"firstName": "Alice",
"lastName": "Smith"
}
}Response:
{
"success": true,
"message": "User created successfully",
"data": {
"userId": "user_abc123def456",
"username": "alice",
"email": "alice@example.com",
"createdAt": "2025-11-06T10:30:00.000Z"
}
}Get all users
Response:
{
"success": true,
"data": [
{
"userId": "user_abc123",
"username": "alice",
"email": "alice@example.com",
"profile": { ... },
"createdAt": "2025-11-06T10:30:00.000Z"
}
]
}Get user by ID
Response:
{
"success": true,
"data": {
"userId": "user_abc123",
"username": "alice",
"email": "alice@example.com",
"profile": { ... }
}
}Update user
Request:
{
"email": "newemail@example.com",
"profile": {
"department": "Engineering"
}
}Delete user (cascade deletes tokens and passwords)
Create token for user
Request:
{
"permissions": ["read", "write"],
"expiresAt": "2025-12-06T10:30:00.000Z",
"description": "API access token"
}Response:
{
"success": true,
"message": "Token created successfully",
"data": {
"tokenId": "token_xyz789",
"apiKey": "sk_live_abc123...",
"permissions": ["read", "write"],
"expiresAt": "2025-12-06T10:30:00.000Z",
"createdAt": "2025-11-06T10:30:00.000Z"
}
}Get all tokens for user
Delete a token
Verify token by API key
Request:
{
"apiKey": "sk_live_abc123..."
}Response:
{
"success": true,
"data": {
"tokenId": "token_xyz789",
"userId": "user_abc123",
"permissions": ["read", "write"],
"expiresAt": "2025-12-06T10:30:00.000Z"
}
}Set/update password for user
Request:
{
"password": "new_secure_password_456"
}Verify password
Request:
{
"password": "user_entered_password"
}Response:
{
"success": true,
"data": {
"valid": true
}
}Login with username/email and password
Request:
{
"username": "alice",
"password": "secure_password_123"
}Response:
{
"success": true,
"message": "Login successful",
"data": {
"userId": "user_abc123",
"username": "alice",
"email": "alice@example.com",
"sessionToken": "sk_session_xyz789...",
"expiresAt": "2025-11-07T10:30:00.000Z"
}
}Get statistics
Response:
{
"success": true,
"data": {
"totalLinks": 15,
"users": {
"links": 5,
"files": 5
},
"tokens": {
"links": 8,
"files": 8
},
"passwords": {
"files": 5
}
}
}Tests the AuthStorageService directly with comprehensive scenarios.
Run:
cd /tmp/gh-issue-solver-1762438481744
node experiments/test-auth-linkdb.jsTests:
- User creation, retrieval, update, deletion
- Password setting, retrieval, updating
- Token creation, retrieval, deletion
- Cascade deletion (user → tokens, passwords)
- Statistics and search operations
Comprehensive unit tests using Vitest.
Run:
cd backend/monolith
npm test -- AuthStorageService.spec.jsTest Coverage:
- User operations (CRUD, search)
- Password operations (set, get, update)
- Token operations (create, retrieve, verify, delete)
- Statistics
- ID generation and conversion
- Error handling
=== Testing AuthStorageService with Link-CLI ===
--- USER OPERATIONS ---
Test 1: Create users
✓ User 1 created: { userId: 'user_abc123', username: 'alice', email: 'alice@example.com' }
✓ User 2 created: { userId: 'user_def456', username: 'bob', email: 'bob@example.com' }
Test 2: Get user by ID
✓ Retrieved user: { userId: 'user_abc123', username: 'alice', email: 'alice@example.com' }
--- PASSWORD OPERATIONS ---
Test 7: Set password for user
✓ Password set for user: alice
Test 8: Get user password
✓ Retrieved password data: { algorithm: 'pbkdf2-sha512', iterations: 100000, hasHash: true, hasSalt: true }
--- TOKEN OPERATIONS ---
Test 10: Create token for user
✓ Token created: { tokenId: 'token_xyz789', apiKey: 'api_key_alice_12345', permissions: ['read', 'write', 'admin'] }
=== All Tests Completed Successfully ===
The API routes use PBKDF2-SHA512 for password hashing:
import crypto from 'crypto';
function hashPassword(password, salt = null) {
if (!salt) {
salt = crypto.randomBytes(16).toString('hex');
}
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
return { hash, salt, algorithm: 'pbkdf2-sha512', iterations: 100000 };
}Parameters:
- Algorithm: PBKDF2 with SHA-512
- Iterations: 100,000
- Salt: 16 random bytes (hex encoded)
- Output: 64-byte hash (hex encoded)
API keys use 32 random bytes (hex encoded):
const apiKey = crypto.randomBytes(32).toString('hex');
// Example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6..."- Never expose passwords: Store only hashes and salts
- Rotate tokens regularly: Set expiration dates
- Use HTTPS: Always use encrypted connections
- Rate limiting: Implement on authentication endpoints
- Audit logging: Log authentication events
- Token permissions: Use granular permissions
- Efficient relationships: link-cli excels at associative queries
- Fast lookups: Direct link traversal for relationships
- Scalable: Can handle large numbers of users/tokens
- Flexible schema: Easy to add new entity types
- External dependency: Requires .NET runtime and clink tool
- Command execution overhead: Each link operation spawns a process
- Hybrid storage: Requires managing both links and JSON files
- Batch operations: Create multiple entities in transactions (if supported)
- Cache frequently accessed data: Users, permissions
- Index searches: Build search indices for username/email lookups
- Connection pooling: Reuse link-cli connections if possible
If you have existing user data in PostgreSQL/MySQL:
- Export users, tokens, passwords to JSON
- For each user:
const user = await authStorage.createUser(userData); await authStorage.setPassword(user.userId, passwordData); for (const token of userTokens) { await authStorage.createToken(user.userId, token); }
import AuthStorageService from './services/auth-storage-service.js';
import oldDatabase from './old-db-connection.js';
const authStorage = new AuthStorageService();
// Migrate users
const users = await oldDatabase.query('SELECT * FROM users');
for (const oldUser of users) {
const newUser = await authStorage.createUser({
username: oldUser.username,
email: oldUser.email,
profile: JSON.parse(oldUser.profile || '{}')
});
// Migrate password
await authStorage.setPassword(newUser.userId, {
hash: oldUser.password_hash,
salt: oldUser.password_salt,
algorithm: 'pbkdf2-sha512',
iterations: 100000
});
// Migrate tokens
const tokens = await oldDatabase.query('SELECT * FROM tokens WHERE user_id = ?', [oldUser.id]);
for (const token of tokens) {
await authStorage.createToken(newUser.userId, {
apiKey: token.api_key,
permissions: JSON.parse(token.permissions || '[]'),
expiresAt: token.expires_at,
description: token.description
});
}
console.log(`Migrated user: ${oldUser.username}`);
}Error: clink: command not found
Solution:
dotnet tool install --global clink
export PATH="$PATH:$HOME/.dotnet/tools"Error: Database file is locked
Solution: Ensure only one process accesses the database at a time. Use proper locking mechanisms.
Error: Failed to parse query
Solution: Check LiNo query syntax in link-db-service.js. Common issues:
- Missing parentheses
- Incorrect variable names
- Quote escaping
Issue: Created user but cannot retrieve it
Debug Steps:
- Check link database:
clink '((($i: $s $t)) (($i: $s $t)))' --db menu.links --after - Check user files:
ls -la backend/monolith/data/auth-data/users/ - Check logs: Look for errors in backend logs
- Verify user ID matches between link and file
- User roles and permissions: Role-based access control
- OAuth integration: External identity providers
- Multi-factor authentication: TOTP, SMS, email codes
- Session management: Active sessions tracking
- Audit logging: Comprehensive authentication logs
- Password reset: Secure password recovery
- Email verification: Email confirmation workflow
- Account locking: Brute-force protection
Links Theory can be extended for:
- Role hierarchies: Admin → Manager → User
- Permission groups: Group permissions together
- Organization structures: Multi-tenant support
- API key scopes: Fine-grained permissions
- Token families: Refresh tokens, access tokens
- link-cli Repository: https://github.com/link-foundation/link-cli
- Links Notation: https://github.com/link-foundation/links-notation
- Issue #2178: https://github.com/unidel2035/dronedoc2025/issues/2178
- Issue #1800: Menu storage with LinkDB (reference implementation)
- Backend Guidelines: See
CLAUDE.mdfor backend architecture rules
For issues or questions:
- Check this documentation
- Review test scripts in
experiments/ - Check backend logs:
backend/monolith/logs/ - Refer to link-cli documentation: https://github.com/link-foundation/link-cli
- Open issue: https://github.com/unidel2035/dronedoc2025/issues
Last Updated: 2025-11-06 Author: Claude AI (Issue Solver) Version: 1.0.0