me2em-protocol-monorepo 0.5.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,870 @@
1
+ # πŸ“¦ `@me2em/core` β€” Developer Documentation
2
+
3
+ > Core primitives for the Me2em authorization protocol: `Identity`, `Handle`, and `Session` management with Ed25519 cryptography.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@me2em/core.svg)](https://www.npmjs.com/package/@me2em/core)
6
+ [![License](https://img.shields.io/npm/l/@me2em/core)](https://github.com/me2em-org/me2em-protocol/blob/main/LICENSE)
7
+ [![Docs](https://img.shields.io/badge/docs-docs.me2em.com-blue)](https://docs.me2em.com)
8
+
9
+ ---
10
+
11
+ ## 🎯 Quick Start
12
+
13
+ ```bash
14
+ npm install @me2em/core
15
+ # or
16
+ pnpm add @me2em/core
17
+ # or
18
+ yarn add @me2em/core
19
+ ```
20
+
21
+ ```typescript
22
+ import { Identity, Handle } from '@me2em/core';
23
+
24
+ // 1. Create or restore identity from seed
25
+ const seed = 'your-32-byte-hex-seed-or-24-words';
26
+ const identity = await Identity.fromSeed(seed);
27
+
28
+ // 2. Derive a contextual handle
29
+ const workHandle = await identity.deriveHandle('work', {
30
+ displayName: 'Alice @ Work',
31
+ avatar: 'https://example.com/avatar.png'
32
+ });
33
+
34
+ // 3. Use handle for cryptographic operations
35
+ const message = new TextEncoder().encode('Hello, world!');
36
+ const signature = await workHandle.sign(message);
37
+ const isValid = await Handle.verify(signature, message, workHandle.getPublicKey());
38
+
39
+ console.log('Handle ID:', workHandle.getId()); // base64url(publicKey)
40
+ console.log('Signature valid:', isValid); // true
41
+ ```
42
+
43
+ ---
44
+
45
+ ## πŸ“š Table of Contents
46
+
47
+ 1. [Core Concepts](#-core-concepts)
48
+ 2. [API Reference](#-api-reference)
49
+ 3. [Use Cases](#-use-cases)
50
+ 4. [Cryptographic Details](#-cryptographic-details)
51
+ 5. [Integration Guide](#-integration-guide)
52
+ 6. [Security Best Practices](#-security-best-practices)
53
+ 7. [Testing](#-testing)
54
+ 8. [Contributing](#-contributing)
55
+
56
+ ---
57
+
58
+ ## πŸ”‘ Core Concepts
59
+
60
+ ### Identity β€” Cryptographic Root
61
+
62
+ `Identity` represents the root cryptographic identity derived from a seed phrase.
63
+
64
+ ```
65
+ Seed (32 bytes / 24 words)
66
+ β”‚
67
+ β–Ό
68
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
69
+ β”‚ Identity β”‚
70
+ β”‚ β€’ Ed25519 key β”‚
71
+ β”‚ β€’ Deterministic β”‚
72
+ β”‚ β€’ Client-only β”‚
73
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
74
+ ```
75
+
76
+ **Key properties:**
77
+ - πŸ” **Never transmitted**: The root private key never leaves the client device
78
+ - πŸ”„ **Deterministic**: Same seed β†’ same Identity β†’ same derived Handles
79
+ - 🧩 **Stateless**: No server storage required for cryptographic operations
80
+
81
+ ### Handle β€” Contextual Profile
82
+
83
+ `Handle` is a derived Ed25519 keypair representing a specific context (work, personal, IoT device).
84
+
85
+ ```
86
+ Identity
87
+ β”‚
88
+ β”œβ”€ deriveHandle('work') β†’ Handle(@alice_work)
89
+ β”œβ”€ deriveHandle('private') β†’ Handle(@alice_private)
90
+ └─ deriveHandle('iot-device-001') β†’ Handle(@device_001)
91
+ ```
92
+
93
+ **Key properties:**
94
+ - πŸ”— **Cryptographically isolated**: Each Handle has its own keypair; compromise of one does not affect others
95
+ - 🏷️ **Public identifier**: `Handle.getId()` returns `base64url(publicKey)` β€” safe to share
96
+ - πŸ“¦ **Metadata support**: Attach public metadata (displayName, avatar) without revealing Identity
97
+
98
+ ### Session β€” Scoped Access Token
99
+
100
+ `Session` represents a time-limited, scoped authorization token for accessing a specific application.
101
+
102
+ ```typescript
103
+ interface SessionOptions {
104
+ audience: string; // e.g., 'https://api.myapp.com'
105
+ scopes: string[]; // e.g., ['read:profile', 'write:messages']
106
+ ttl?: number; // seconds until expiration (default: 3600)
107
+ }
108
+ ```
109
+
110
+ **Key properties:**
111
+ - ⏱️ **Short-lived**: Tokens expire automatically; refresh via Handle re-authentication
112
+ - πŸ” **Scoped**: Each token grants only specified permissions
113
+ - 🌐 **Stateless verification**: Servers verify signatures using Handle public key β€” no database lookup required
114
+
115
+ ---
116
+
117
+ ## πŸ“– API Reference
118
+
119
+ ### `Identity` Class
120
+
121
+ ```typescript
122
+ class Identity {
123
+ // Create Identity from seed (32-byte Uint8Array or hex string)
124
+ static fromSeed(seed: Uint8Array | string): Promise<Identity>;
125
+
126
+ // Derive a new Handle with optional metadata
127
+ deriveHandle(name: string, metadata?: HandleMetadata): Promise<Handle>;
128
+
129
+ // Get root public key (for verification, never share private key)
130
+ getPublicKey(): Uint8Array;
131
+ }
132
+ ```
133
+
134
+ #### `Identity.fromSeed(seed)`
135
+
136
+ ```typescript
137
+ // From hex string (64 chars = 32 bytes)
138
+ const identity = await Identity.fromSeed('a1b2c3...');
139
+
140
+ // From Uint8Array
141
+ const seedBytes = new Uint8Array(32).fill(42);
142
+ const identity = await Identity.fromSeed(seedBytes);
143
+ ```
144
+
145
+ ⚠️ **Security**: The seed must be kept secret. Never log, transmit, or store it in plaintext.
146
+
147
+ #### `Identity.deriveHandle(name, metadata?)`
148
+
149
+ ```typescript
150
+ const handle = await identity.deriveHandle('work', {
151
+ displayName: 'Alice Developer',
152
+ avatar: 'https://example.com/alice.png',
153
+ org: 'Acme Corp'
154
+ });
155
+ ```
156
+
157
+ - `name`: Unique identifier for this Handle within the Identity (case-insensitive, trimmed)
158
+ - `metadata`: Arbitrary public data attached to the Handle (visible to servers/other users)
159
+
160
+ Returns a `Handle` instance with its own keypair, derived deterministically from the Identity.
161
+
162
+ ---
163
+
164
+ ### `Handle` Class
165
+
166
+ ```typescript
167
+ class Handle {
168
+ // Get public identifier (safe to share)
169
+ getId(): string; // base64url-encoded public key
170
+
171
+ // Sign arbitrary data with Handle's private key
172
+ sign(data: Uint8Array): Promise<Uint8Array>;
173
+
174
+ // Verify a signature using a public key (static method)
175
+ static verify(signature: Uint8Array, data: Uint8Array, publicKey: Uint8Array): Promise<boolean>;
176
+
177
+ // Deterministically derive a password/secret for a specific context
178
+ derivePassword(context: string, length?: number): string;
179
+
180
+ // Derive a symmetric 256-bit channel key for encrypted communication
181
+ deriveChannelKey(context: string): Uint8Array;
182
+
183
+ // Accessors
184
+ getName(): string;
185
+ getMetadata(): HandleMetadata | undefined;
186
+ getPublicKey(): Uint8Array;
187
+ }
188
+ ```
189
+
190
+ #### `Handle.getId()`
191
+
192
+ Returns a URL-safe base64 string representing the Handle's public key:
193
+
194
+ ```typescript
195
+ const handleId = handle.getId();
196
+ // Example: "pK7xJ2mN8vQ3rL5wY9zB1cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0"
197
+ ```
198
+
199
+ This is the identifier you send to servers for authentication and routing.
200
+
201
+ #### `Handle.sign(data)`
202
+
203
+ Signs arbitrary binary data using Ed25519:
204
+
205
+ ```typescript
206
+ const payload = new TextEncoder().encode('{"action":"post","content":"Hello"}');
207
+ const signature = await handle.sign(payload);
208
+
209
+ // Send to server:
210
+ fetch('https://api.example.com/endpoint', {
211
+ method: 'POST',
212
+ headers: {
213
+ 'X-Handle-ID': handle.getId(),
214
+ 'Content-Type': 'application/json'
215
+ },
216
+ body: JSON.stringify({
217
+ payload: btoa(String.fromCharCode(...payload)),
218
+ signature: btoa(String.fromCharCode(...signature))
219
+ })
220
+ });
221
+ ```
222
+
223
+ #### `Handle.verify(signature, data, publicKey)`
224
+
225
+ Static method for server-side signature verification:
226
+
227
+ ```typescript
228
+ // Server receives: handleId, signature, payload
229
+ const publicKey = Uint8Array.from(atob(handleId), c => c.charCodeAt(0));
230
+ const data = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
231
+ const sig = Uint8Array.from(atob(signature), c => c.charCodeAt(0));
232
+
233
+ const isValid = await Handle.verify(sig, data, publicKey);
234
+ if (!isValid) throw new Error('Invalid signature');
235
+ ```
236
+
237
+ #### `Handle.derivePassword(context, length?)`
238
+
239
+ Generates a deterministic, cryptographically strong secret (e.g., a password) for a specific service, without ever exposing the private key.
240
+
241
+ ```typescript
242
+ // Derive a password for a specific service
243
+ const googlePassword = workHandle.derivePassword('google');
244
+ // Example output: "xK9mP2qL5wY9zB1cD4eF6g"
245
+
246
+ // Derive a longer secret (e.g., 32 bytes for an API key)
247
+ const apiKey = workHandle.derivePassword('aws-api', 32);
248
+ ```
249
+
250
+ βœ… **Security Benefit**: The `privateKey` remains strictly encapsulated within the `Handle` instance. It is used internally by HKDF-SHA256 and is never returned, logged, or serialized.
251
+
252
+ #### `Handle.deriveChannelKey(context)`
253
+
254
+ Derives a symmetric 256-bit key for establishing a secure, encrypted communication channel between the Identity (controller) and this Handle (device/context). Both parties can independently compute this key without any key exchange protocol, because they both have access to the Handle's private key.
255
+
256
+ ```typescript
257
+ // On the device (Handle side):
258
+ const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
259
+ // channelKey is a 32-byte Uint8Array, ready for AES-256-GCM
260
+
261
+ // On the controller (Identity side):
262
+ const droneHandle = await centerIdentity.deriveHandle('drone-001');
263
+ const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
264
+ // Identical key, derived independently
265
+ ```
266
+
267
+ βœ… **Security Benefit**: The `privateKey` remains strictly encapsulated within the `Handle` instance. No key exchange protocol (ECDH, etc.) is needed β€” both parties derive the same key independently from the shared Handle private key.
268
+
269
+ ⚠️ **Note**: This method returns raw bytes (Uint8Array), unlike `derivePassword` which returns a base64url string. Use the returned bytes directly with AES-256-GCM via Web Crypto API or similar.
270
+
271
+ ---
272
+
273
+ ### `Session` Class (Planned)
274
+
275
+ > ⚠️ Session management is planned for v0.2. Current implementations should use custom token logic with Handle signatures.
276
+
277
+ ```typescript
278
+ // Future API (not yet implemented)
279
+ const session = await handle.requestSession({
280
+ audience: 'https://api.myapp.com',
281
+ scopes: ['read:profile', 'write:messages'],
282
+ ttl: 3600
283
+ });
284
+
285
+ // Use token
286
+ fetch('https://api.myapp.com/me', {
287
+ headers: { Authorization: `Bearer ${session.token}` }
288
+ });
289
+ ```
290
+
291
+ ---
292
+
293
+ ## 🎯 Use Cases
294
+
295
+ ### 1. Multi-Context User Authentication
296
+
297
+ ```typescript
298
+ // User logs in with seed
299
+ const identity = await Identity.fromSeed(userSeed);
300
+
301
+ // Derive work handle
302
+ const workHandle = await identity.deriveHandle('work', {
303
+ displayName: 'Alice @ Acme',
304
+ role: 'engineer'
305
+ });
306
+
307
+ // Authenticate to work app
308
+ const workSig = await workHandle.sign(challenge);
309
+ // Send workHandle.getId() + workSig to https://work.acme.com/auth
310
+
311
+ // Later, derive personal handle for social app
312
+ const personalHandle = await identity.deriveHandle('personal', {
313
+ displayName: 'Alice',
314
+ interests: ['hiking', 'photography']
315
+ });
316
+
317
+ // Authenticate to social app with different handle
318
+ const socialSig = await personalHandle.sign(challenge);
319
+ // Send personalHandle.getId() + socialSig to https://social.app/auth
320
+ ```
321
+
322
+ βœ… **Benefit**: One seed, multiple isolated identities. Apps see only the Handle they interact with.
323
+
324
+ ---
325
+
326
+ ### 2. IoT Device Hierarchy
327
+
328
+ ```typescript
329
+ // Root identity for device fleet
330
+ const fleetIdentity = await Identity.fromSeed(fleetSeed);
331
+
332
+ // Derive handle for gateway
333
+ const gateway = await fleetIdentity.deriveHandle('gateway-001', {
334
+ type: 'gateway',
335
+ location: 'warehouse-a'
336
+ });
337
+
338
+ // Derive child handles for sensors (using name prefix for hierarchy)
339
+ const sensor1 = await fleetIdentity.deriveHandle('gateway-001/sensor-temp', {
340
+ type: 'temperature',
341
+ unit: 'celsius'
342
+ });
343
+
344
+ const sensor2 = await fleetIdentity.deriveHandle('gateway-001/sensor-humidity', {
345
+ type: 'humidity',
346
+ unit: 'percent'
347
+ });
348
+ ```
349
+
350
+ βœ… **Benefit**: Compromise of `sensor-temp` does not expose `gateway-001` or other sensors. Each device has isolated credentials.
351
+
352
+ ---
353
+
354
+ ### 3. Anonymous Guest Access
355
+
356
+ ```typescript
357
+ // Generate ephemeral seed for guest session
358
+ const guestSeed = crypto.getRandomValues(new Uint8Array(32));
359
+ const guestIdentity = await Identity.fromSeed(guestSeed);
360
+
361
+ // Create anonymous handle
362
+ const guestHandle = await guestIdentity.deriveHandle('guest', {
363
+ displayName: 'Anonymous User',
364
+ ephemeral: true
365
+ });
366
+
367
+ // Use for limited-scope access
368
+ const guestSig = await guestHandle.sign(challenge);
369
+ // Send to server with scope: ['read:public-content']
370
+ ```
371
+
372
+ βœ… **Benefit**: No email, no phone, no tracking. Guest access with cryptographic accountability.
373
+
374
+ ---
375
+
376
+ ### 4. Cross-App Single Sign-On (SSO)
377
+
378
+ ```typescript
379
+ // User authenticates once with Identity
380
+ const identity = await Identity.fromSeed(seed);
381
+
382
+ // App A requests session for 'app-a.example.com'
383
+ const handleA = await identity.deriveHandle('app-a', { app: 'example-a' });
384
+ const sessionA = { /* custom token logic */ };
385
+
386
+ // App B requests session for 'app-b.example.com'
387
+ const handleB = await identity.deriveHandle('app-b', { app: 'example-b' });
388
+ const sessionB = { /* custom token logic */ };
389
+
390
+ // User switches between apps without re-entering seed
391
+ // Each app sees only its Handle, not the others
392
+ ```
393
+
394
+ βœ… **Benefit**: Seamless SSO with privacy isolation β€” apps cannot correlate user activity across services.
395
+
396
+ ---
397
+
398
+ ### 5. Deterministic Password Manager (Access Key Keeper)
399
+
400
+ Instead of storing passwords in a database, derive them deterministically from the Handle. The user only needs to remember their root Seed and the service name.
401
+
402
+ ```typescript
403
+ // 1. User restores Identity from Seed (e.g., after entering a PIN)
404
+ const identity = await Identity.fromSeed(userSeed);
405
+
406
+ // 2. Derive the specific service Handle
407
+ const googleHandle = await identity.deriveHandle('google', {
408
+ displayName: 'Alice Personal'
409
+ });
410
+
411
+ // 3. Deterministically generate the password on the fly
412
+ const password = googleHandle.derivePassword('google');
413
+
414
+ // 4. Auto-fill the login form
415
+ console.log('Login:', 'alice@example.com');
416
+ console.log('Password:', password); // Always the same for this seed + handle + context
417
+ ```
418
+
419
+ βœ… **Benefit**: Zero-knowledge password management. No database of passwords is required on the server. If a service forces a password change, the user simply derives a new handle (e.g., `'google-v2'`) or adds a version suffix to the context (e.g., `derivePassword('google', 16, 2)`).
420
+
421
+ ### 6. IoT Fleet with Encrypted Channels
422
+
423
+ Control a fleet of devices (drones, sensors, robots) with zero-knowledge encrypted communication. The control center derives a channel key for each device, and the device independently derives the same key β€” no key exchange protocol required.
424
+
425
+ ```typescript
426
+ // === CONTROL CENTER (Identity) ===
427
+ const centerIdentity = await Identity.fromSeed(centerSeed);
428
+
429
+ // Minimal registry: just device names (no public keys stored!)
430
+ const allowedDevices = ['drone-001', 'drone-002', 'sensor-warehouse-a'];
431
+
432
+ // Receiving telemetry from a drone
433
+ async function receiveTelemetry(message: { name: string, signature: Uint8Array, encrypted: Uint8Array }) {
434
+ // 1. Check if device is in registry
435
+ if (!allowedDevices.includes(message.name)) {
436
+ throw new Error('Unknown device');
437
+ }
438
+
439
+ // 2. Derive the Handle (deterministic, no DB lookup)
440
+ const deviceHandle = await centerIdentity.deriveHandle(message.name);
441
+
442
+ // 3. Verify signature (proves device owns the private key)
443
+ const dataToVerify = concatBytes(
444
+ new TextEncoder().encode(message.name),
445
+ message.encrypted
446
+ );
447
+ const isValid = await Handle.verify(message.signature, dataToVerify, deviceHandle.getPublicKey());
448
+ if (!isValid) throw new Error('Invalid signature');
449
+
450
+ // 4. Derive the SAME channel key the device used for encryption
451
+ const channelKey = deviceHandle.deriveChannelKey('telemetry-v1');
452
+
453
+ // 5. Decrypt the message
454
+ const telemetry = await decryptAESGCM(message.encrypted, channelKey);
455
+ return telemetry;
456
+ }
457
+
458
+ // === DRONE (Handle, provisioned at factory) ===
459
+ // Drone is provisioned with its Handle's private key and name
460
+ async function sendTelemetry(telemetry: object) {
461
+ const name = 'drone-001';
462
+ const privateKey = /* loaded from secure enclave */;
463
+ const droneHandle = new Handle(privateKey, name);
464
+
465
+ // 1. Derive the SAME channel key the center will use for decryption
466
+ const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
467
+
468
+ // 2. Encrypt telemetry
469
+ const telemetryBytes = new TextEncoder().encode(JSON.stringify(telemetry));
470
+ const encrypted = await encryptAESGCM(telemetryBytes, channelKey);
471
+
472
+ // 3. Sign (name + encrypted data)
473
+ const dataToSign = concatBytes(new TextEncoder().encode(name), encrypted);
474
+ const signature = await droneHandle.sign(dataToSign);
475
+
476
+ return { name, signature, encrypted };
477
+ }
478
+ ```
479
+
480
+ βœ… **Benefits**:
481
+ - **Minimal registry**: Control center stores only device names (strings), not public keys
482
+ - **No key exchange**: Both parties derive the same key independently
483
+ - **Stateless verification**: Signature check proves device authenticity
484
+ - **Isolation**: Compromise of one device doesn't affect others (different Handles β†’ different channel keys)
485
+
486
+ ---
487
+
488
+ ### 7. Stateless Multi-Device Synchronization
489
+
490
+ A user with the same Identity on multiple devices (phone, laptop, tablet) can derive identical Handles and secrets on each device without any synchronization protocol.
491
+
492
+ ```typescript
493
+ // On the phone:
494
+ const identity = await Identity.fromSeed(userSeed);
495
+ const messengerHandle = await identity.deriveHandle('messenger-main');
496
+ const handleId = messengerHandle.getId();
497
+ // Send handleId to server for registration
498
+
499
+ // On the laptop (later, no sync needed):
500
+ const identity = await Identity.fromSeed(userSeed); // Same seed
501
+ const messengerHandle = await identity.deriveHandle('messenger-main'); // Same name
502
+ const handleId = messengerHandle.getId(); // Identical handleId!
503
+ // Server recognizes the same user automatically
504
+ ```
505
+
506
+ βœ… **Benefit**: Zero-knowledge multi-device support. No QR codes, no server-side key sync, no backup servers. The mathematics guarantees identity across devices.
507
+
508
+ ---
509
+
510
+ ### 8. Break-Glass Recovery and Inheritance
511
+
512
+ A user's entire digital identity can be recovered from a single seed phrase, even years later, on any device, without contacting any service provider.
513
+
514
+ ```typescript
515
+ // User stores seed phrase in a physical safe (or via Shamir's Secret Sharing with trusted heirs)
516
+
517
+ // Years later, on a new device:
518
+ const identity = await Identity.fromSeed(recoveredSeed);
519
+
520
+ // All handles are instantly recoverable:
521
+ const googleHandle = await identity.deriveHandle('google');
522
+ const bankHandle = await identity.deriveHandle('bank');
523
+ const messengerHandle = await identity.deriveHandle('messenger-main');
524
+
525
+ // All passwords are instantly recoverable:
526
+ const googlePassword = googleHandle.derivePassword('google');
527
+ const bankPassword = bankHandle.derivePassword('bank');
528
+
529
+ // All channel keys are instantly recoverable:
530
+ const messengerChannelKey = messengerHandle.deriveChannelKey('session-2026');
531
+ ```
532
+
533
+ βœ… **Benefit**: True self-sovereignty. No company can lock you out of your identity. Recovery is a mathematical certainty, not a customer support ticket.
534
+
535
+ ---
536
+
537
+ ### 9. Ephemeral Delegated Access
538
+
539
+ Grant time-limited access to a contractor or temporary service by deriving a Handle with a time-bound name.
540
+
541
+ ```typescript
542
+ // Grant access to contractor until 2026-12-31
543
+ const contractorHandle = await identity.deriveHandle('contractor-acme-2026-12-31', {
544
+ displayName: 'ACME Corp Contractor',
545
+ role: 'auditor',
546
+ expiresAt: '2026-12-31T23:59:59Z'
547
+ });
548
+
549
+ // Share the Handle ID with the contractor's system
550
+ // Contractor uses this Handle for authenticated access
551
+
552
+ // After expiration:
553
+ // - Server rejects requests (checks expiresAt metadata)
554
+ // - User simply stops using this Handle
555
+ // - No cleanup needed β€” the Handle is just a name in the derivation tree
556
+ ```
557
+
558
+ βœ… **Benefit**: Clean delegation without polluting the permanent identity. Expired Handles become inert cryptographic artifacts.
559
+
560
+ ## πŸ” Cryptographic Details
561
+
562
+ ### Key Derivation Algorithm
563
+
564
+ Handles are derived using **HKDF-SHA256** with domain separation:
565
+
566
+ ```
567
+ HandlePrivateKey = HKDF-SHA256(
568
+ inputKeyMaterial = IdentityPrivateKey,
569
+ salt = empty,
570
+ info = "me2em/handle/v1/" + lowercase(name),
571
+ length = 32
572
+ )
573
+ HandlePublicKey = Ed25519.PublicKey(HandlePrivateKey)
574
+ HandleId = Base64Url(HandlePublicKey)
575
+ ```
576
+
577
+ **Properties:**
578
+ - πŸ” **Deterministic**: Same inputs β†’ same output across all implementations
579
+ - πŸ”’ **One-way**: Cannot derive Identity key from Handle key
580
+ - 🧩 **Isolated**: Each Handle uses independent Ed25519 keypair
581
+
582
+ ### Channel Key Derivation
583
+
584
+ Channel keys are derived using **HKDF-SHA256** with the Handle's private key as input key material:
585
+
586
+ ```
587
+ ChannelKey = HKDF-SHA256(
588
+ inputKeyMaterial = HandlePrivateKey,
589
+ salt = "me2em/channel/" + lowercase(context),
590
+ info = "me2em/channel/v1",
591
+ length = 32
592
+ )
593
+ ```
594
+
595
+ **Properties**:
596
+ - πŸ” **Deterministic**: Same Handle + same context β†’ same 32-byte key
597
+ - πŸ” **Encapsulated**: Private key never leaves the Handle instance
598
+ - 🧩 **Domain-separated**: Different contexts produce different keys (e.g., `'telemetry-v1'` vs `'command-v1'`)
599
+ - ⚑ **No key exchange**: Both parties derive the key independently, no ECDH needed
600
+
601
+ ### Signature Scheme
602
+
603
+ - **Algorithm**: Ed25519 (RFC 8032)
604
+ - **Hash**: SHA-512 (via `@noble/ed25519`)
605
+ - **Encoding**: Raw bytes β†’ base64url for transport
606
+
607
+ ### Test Vectors
608
+
609
+ See [`specs/test-vectors.json`](../../specs/test-vectors.json) for canonical derivation examples to ensure cross-implementation compatibility.
610
+
611
+ Example vector:
612
+ ```json
613
+ {
614
+ "seed_hex": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
615
+ "handle_name": "work",
616
+ "expected_handle_id": "pK7xJ2mN8vQ3rL5wY9zB1cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0",
617
+ "expected_public_key_hex": "a4b5c6d7e8f90123456789abcdef0123456789abcdef0123456789abcdef0123"
618
+ }
619
+ ```
620
+
621
+ ---
622
+
623
+ ## πŸ”Œ Integration Guide
624
+
625
+ ### Backend Verification (Node.js Example)
626
+
627
+ ```typescript
628
+ import { Handle } from '@me2em/core';
629
+
630
+ // Middleware to verify Handle-signed requests
631
+ export async function me2emAuthMiddleware(req: Request, res: Response, next: NextFunction) {
632
+ const handleId = req.headers['x-handle-id'] as string;
633
+ const signature = req.headers['x-signature'] as string;
634
+ const payload = JSON.stringify(req.body);
635
+
636
+ if (!handleId || !signature) {
637
+ return res.status(401).json({ error: 'Missing authentication headers' });
638
+ }
639
+
640
+ try {
641
+ // Decode base64url to Uint8Array
642
+ const publicKey = base64urlToBytes(handleId);
643
+ const sig = base64urlToBytes(signature);
644
+ const data = new TextEncoder().encode(payload);
645
+
646
+ // Verify signature
647
+ const isValid = await Handle.verify(sig, data, publicKey);
648
+ if (!isValid) throw new Error('Invalid signature');
649
+
650
+ // Attach handle info to request for downstream use
651
+ req.me2em = { handleId, publicKey };
652
+ next();
653
+ } catch (err) {
654
+ res.status(401).json({ error: 'Authentication failed' });
655
+ }
656
+ }
657
+
658
+ function base64urlToBytes(str: string): Uint8Array {
659
+ const padding = '='.repeat((4 - (str.length % 4)) % 4);
660
+ const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
661
+ return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
662
+ }
663
+ ```
664
+
665
+ ### Frontend Secure Key Handling
666
+
667
+ ```typescript
668
+ // Store encrypted seed in IndexedDB (never plaintext)
669
+ import { encryptWithPin } from './crypto/local';
670
+
671
+ async function saveIdentity(seed: string, pin: string) {
672
+ const encrypted = await encryptWithPin(seed, pin);
673
+ await db.identities.add({
674
+ id: 'current',
675
+ encryptedSeed: encrypted,
676
+ // seed is NEVER stored in plaintext
677
+ });
678
+ }
679
+
680
+ // Load and decrypt on login
681
+ async function loadIdentity(pin: string) {
682
+ const record = await db.identities.get('current');
683
+ if (!record) throw new Error('No identity found');
684
+
685
+ const seed = await decryptWithPin(record.encryptedSeed, pin);
686
+ return Identity.fromSeed(seed);
687
+ }
688
+ ```
689
+
690
+ βœ… **Best practice**: Use Web Crypto API or secure enclave for PIN/biometric decryption.
691
+
692
+ ---
693
+
694
+ ## πŸ›‘οΈ Security Best Practices
695
+
696
+ ### βœ… Do
697
+
698
+ - **Store seeds encrypted**: Use PIN/biometric + AES-GCM before persisting to IndexedDB
699
+ - **Clear memory**: Zero out seed/private key buffers after use (`buffer.fill(0)`)
700
+ - **Use short TTLs**: Rotate session tokens frequently (≀1 hour recommended)
701
+ - **Validate metadata server-side**: Never trust client-provided Handle metadata without verification
702
+ - **Pin dependencies**: Lock `@me2em/core` to specific version in `package.json`
703
+
704
+ ### ❌ Don't
705
+
706
+ - **Never transmit seeds**: The seed is the root of trust β€” keep it client-side only
707
+ - **Don't reuse Handles across contexts**: Derive separate Handles for different apps/services
708
+ - **Don't log private keys**: Ensure debugging output never includes key material
709
+ - **Don't disable signature verification**: Always verify Ed25519 signatures server-side
710
+
711
+ ### Key Lifecycle
712
+
713
+ ```
714
+ [User enters seed]
715
+ β”‚
716
+ β–Ό
717
+ [Derive Identity in RAM]
718
+ β”‚
719
+ β–Ό
720
+ [Derive Handle(s) as needed]
721
+ β”‚
722
+ β–Ό
723
+ [Sign challenge / data]
724
+ β”‚
725
+ β–Ό
726
+ [Zero out private key buffers] ← Critical!
727
+ β”‚
728
+ β–Ό
729
+ [Keep only public HandleId for future use]
730
+ ```
731
+
732
+ ---
733
+
734
+ ## πŸ§ͺ Testing
735
+
736
+ ### Unit Tests
737
+
738
+ ```bash
739
+ cd packages/protocol-core
740
+ pnpm test
741
+ ```
742
+
743
+ Tests cover:
744
+ - βœ… Deterministic derivation (same seed + name β†’ same HandleId)
745
+ - βœ… Signature generation and verification
746
+ - βœ… Edge cases (empty metadata, long names, unicode)
747
+
748
+ ### Integration Test Example
749
+
750
+ ```typescript
751
+ import { Identity } from '@me2em/core';
752
+
753
+ test('full auth flow', async () => {
754
+ // Client side
755
+ const identity = await Identity.fromSeed(testSeed);
756
+ const handle = await identity.deriveHandle('test');
757
+ const challenge = new TextEncoder().encode('nonce-123');
758
+ const signature = await handle.sign(challenge);
759
+
760
+ // Server side
761
+ const isValid = await Handle.verify(
762
+ signature,
763
+ challenge,
764
+ handle.getPublicKey()
765
+ );
766
+ expect(isValid).toBe(true);
767
+ expect(handle.getId()).toMatch(/^[A-Za-z0-9_-]{43}$/);
768
+ });
769
+ ```
770
+
771
+ ### Cross-Implementation Testing
772
+
773
+ Use [`specs/test-vectors.json`](../../specs/test-vectors.json) to verify your implementation matches the reference:
774
+
775
+ ```typescript
776
+ import vectors from '@me2em/core/specs/test-vectors.json';
777
+
778
+ for (const vector of vectors) {
779
+ const identity = await Identity.fromSeed(vector.seed_hex);
780
+ const handle = await identity.deriveHandle(vector.handle_name);
781
+
782
+ expect(handle.getId()).toBe(vector.expected_handle_id);
783
+ expect(bytesToHex(handle.getPublicKey())).toBe(vector.expected_public_key_hex);
784
+ }
785
+ ```
786
+
787
+ ---
788
+
789
+ ## 🀝 Contributing
790
+
791
+ We welcome contributions! See:
792
+
793
+ - πŸ“„ [CONTRIBUTING.md](../../CONTRIBUTING.md) β€” How to contribute code/docs
794
+ - πŸ—³οΈ [GOVERNANCE.md](../../GOVERNANCE.md) β€” Project decision-making process
795
+ - πŸ” [SECURITY.md](../../SECURITY.md) β€” Responsible disclosure policy
796
+
797
+ ### Quick Start for Contributors
798
+
799
+ ```bash
800
+ git clone https://github.com/me2em-org/me2em-protocol.git
801
+ cd me2em-protocol
802
+ pnpm install
803
+
804
+ # Run tests
805
+ pnpm -r test
806
+
807
+ # Build all packages
808
+ pnpm -r build
809
+
810
+ # Lint
811
+ pnpm -r lint
812
+ ```
813
+
814
+ ### Adding a New Feature
815
+
816
+ 1. Open a [Discussion](https://github.com/me2em-org/me2em-protocol/discussions) to propose the change
817
+ 2. Implement in a feature branch
818
+ 3. Add tests covering new functionality
819
+ 4. Update `specs/core.md` if protocol behavior changes
820
+ 5. Submit PR with clear description and test results
821
+
822
+ ---
823
+
824
+ ## πŸ“¦ Package Info
825
+
826
+ ```json
827
+ {
828
+ "name": "@me2em/core",
829
+ "version": "0.3.0-alpha.1",
830
+ "type": "module",
831
+ "main": "./dist/index.js",
832
+ "types": "./dist/index.d.ts",
833
+ "exports": {
834
+ ".": {
835
+ "types": "./dist/index.d.ts",
836
+ "import": "./dist/index.js",
837
+ "require": "./dist/index.cjs"
838
+ }
839
+ },
840
+ "dependencies": {
841
+ "@noble/ed25519": "^3.1.0",
842
+ "@noble/hashes": "^2.2.0"
843
+ },
844
+ "engines": {
845
+ "node": ">=18.0.0"
846
+ }
847
+ }
848
+ ```
849
+
850
+ ---
851
+
852
+ ## 🌐 Ecosystem
853
+
854
+ This package is part of the Me2em ecosystem:
855
+
856
+ @me2em/core ← криптография (Identity, Handle, derivePassword, deriveChannelKey)
857
+ @me2em/sdk ← browser wrapper (IndexedDB, PIN, biometric, session management)
858
+ @me2em/react ← UI ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ (SeedDisplay, SeedVerification, HandleManager)
859
+ @me2em/auth-middleware ← NestJS/Express middleware для Handle.verify()
860
+ @me2em/server ← (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) Π³ΠΎΡ‚ΠΎΠ²Ρ‹ΠΉ NestJS backend ΠΈΠ· Phase 1
861
+
862
+ πŸ”— Learn more: [github.com/me2em-org](https://github.com/me2em-org) | [docs.me2em.com](https://docs.me2em.com)
863
+
864
+ ---
865
+
866
+ ## πŸ“œ License
867
+
868
+ Apache License 2.0 β€” see [LICENSE](../../LICENSE) for details.
869
+
870
+ Β© 2026 Me2em Organization. Built for privacy, openness, and user sovereignty.