@stvor/sdk 2.4.0 → 3.0.0

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.
Files changed (82) hide show
  1. package/dist/facade/app.cjs +29 -0
  2. package/dist/facade/app.d.ts +83 -76
  3. package/dist/facade/app.js +330 -195
  4. package/dist/facade/crypto-session.cjs +29 -0
  5. package/dist/facade/crypto-session.d.ts +49 -54
  6. package/dist/facade/crypto-session.js +117 -140
  7. package/dist/facade/errors.cjs +29 -0
  8. package/dist/facade/errors.d.ts +29 -12
  9. package/dist/facade/errors.js +49 -8
  10. package/dist/facade/index.cjs +29 -0
  11. package/dist/facade/index.d.ts +27 -8
  12. package/dist/facade/index.js +23 -3
  13. package/dist/facade/local-storage-identity-store.cjs +29 -0
  14. package/dist/facade/local-storage-identity-store.d.ts +50 -0
  15. package/dist/facade/local-storage-identity-store.js +100 -0
  16. package/dist/facade/metrics-attestation.cjs +29 -0
  17. package/dist/facade/metrics-attestation.d.ts +209 -0
  18. package/dist/facade/metrics-attestation.js +333 -0
  19. package/dist/facade/metrics-engine.cjs +29 -0
  20. package/dist/facade/metrics-engine.d.ts +91 -0
  21. package/dist/facade/metrics-engine.js +170 -0
  22. package/dist/facade/redis-replay-cache.cjs +29 -0
  23. package/dist/facade/redis-replay-cache.d.ts +88 -0
  24. package/dist/facade/redis-replay-cache.js +60 -0
  25. package/dist/facade/relay-client.cjs +29 -0
  26. package/dist/facade/relay-client.d.ts +22 -23
  27. package/dist/facade/relay-client.js +107 -128
  28. package/dist/facade/replay-manager.cjs +29 -0
  29. package/dist/facade/replay-manager.d.ts +28 -35
  30. package/dist/facade/replay-manager.js +102 -69
  31. package/dist/facade/sodium-singleton.cjs +29 -0
  32. package/dist/facade/tofu-manager.cjs +29 -0
  33. package/dist/facade/tofu-manager.d.ts +38 -36
  34. package/dist/facade/tofu-manager.js +109 -77
  35. package/dist/facade/types.cjs +29 -0
  36. package/dist/facade/types.d.ts +2 -0
  37. package/dist/index.cjs +29 -0
  38. package/dist/index.d.cts +6 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +7 -0
  41. package/dist/legacy.cjs +29 -0
  42. package/dist/legacy.d.ts +31 -1
  43. package/dist/legacy.js +90 -2
  44. package/dist/ratchet/core-production.cjs +29 -0
  45. package/dist/ratchet/core-production.d.ts +95 -0
  46. package/dist/ratchet/core-production.js +286 -0
  47. package/dist/ratchet/index.cjs +29 -0
  48. package/dist/ratchet/index.d.ts +49 -78
  49. package/dist/ratchet/index.js +313 -288
  50. package/dist/ratchet/key-recovery.cjs +29 -0
  51. package/dist/ratchet/replay-protection.cjs +29 -0
  52. package/dist/ratchet/tofu.cjs +29 -0
  53. package/dist/src/facade/app.cjs +29 -0
  54. package/dist/src/facade/app.d.ts +105 -0
  55. package/dist/src/facade/app.js +245 -0
  56. package/dist/src/facade/crypto.cjs +29 -0
  57. package/dist/src/facade/errors.cjs +29 -0
  58. package/dist/src/facade/errors.d.ts +19 -0
  59. package/dist/src/facade/errors.js +21 -0
  60. package/dist/src/facade/index.cjs +29 -0
  61. package/dist/src/facade/index.d.ts +8 -0
  62. package/dist/src/facade/index.js +5 -0
  63. package/dist/src/facade/relay-client.cjs +29 -0
  64. package/dist/src/facade/relay-client.d.ts +36 -0
  65. package/dist/src/facade/relay-client.js +154 -0
  66. package/dist/src/facade/types.cjs +29 -0
  67. package/dist/src/facade/types.d.ts +50 -0
  68. package/dist/src/facade/types.js +4 -0
  69. package/dist/src/index.cjs +29 -0
  70. package/dist/src/index.d.ts +2 -0
  71. package/dist/src/index.js +2 -0
  72. package/dist/src/legacy.cjs +29 -0
  73. package/dist/src/legacy.d.ts +0 -0
  74. package/dist/src/legacy.js +1 -0
  75. package/dist/src/mock-relay-server.cjs +29 -0
  76. package/dist/src/mock-relay-server.d.ts +30 -0
  77. package/dist/src/mock-relay-server.js +236 -0
  78. package/package.json +37 -11
  79. package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
  80. package/dist/ratchet/tests/ratchet.test.js +0 -160
  81. /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
  82. /package/dist/{facade → src/facade}/crypto.js +0 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Auto-generated CommonJS wrapper for index.js
4
+ // This allows `require('@stvor/sdk')` to work alongside ESM `import`.
5
+
6
+ const mod = require('module');
7
+ const url = require('url');
8
+
9
+ // Use dynamic import to load the ESM module
10
+ let _cached;
11
+ async function _load() {
12
+ if (!_cached) {
13
+ _cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
14
+ }
15
+ return _cached;
16
+ }
17
+
18
+ // For simple CJS usage, expose a promise-based loader
19
+ module.exports = new Proxy({ load: _load }, {
20
+ get(target, prop) {
21
+ if (prop === '__esModule') return true;
22
+ if (prop === 'then') return undefined; // prevent treating as thenable
23
+ if (prop === 'load') return _load;
24
+ if (prop === 'default') {
25
+ return _load().then(m => m.default);
26
+ }
27
+ return _load().then(m => m[prop]);
28
+ }
29
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * STVOR SDK - Main exports
3
+ */
4
+ export * from './legacy.js';
5
+ export * from './facade/index.js';
6
+ export * from './ratchet/index.js';
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
+ /**
2
+ * STVOR SDK - Main exports
3
+ */
1
4
  export * from './legacy.js';
2
5
  export * from './facade/index.js';
6
+ export * from './ratchet/index.js';
package/dist/index.js CHANGED
@@ -1,2 +1,9 @@
1
+ /**
2
+ * STVOR SDK - Main exports
3
+ */
4
+ // Legacy core API (for migration)
1
5
  export * from './legacy.js';
6
+ // New DX Facade API
2
7
  export * from './facade/index.js';
8
+ // X3DH + Double Ratchet implementation
9
+ export * from './ratchet/index.js';
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Auto-generated CommonJS wrapper for legacy.js
4
+ // This allows `require('@stvor/sdk')` to work alongside ESM `import`.
5
+
6
+ const mod = require('module');
7
+ const url = require('url');
8
+
9
+ // Use dynamic import to load the ESM module
10
+ let _cached;
11
+ async function _load() {
12
+ if (!_cached) {
13
+ _cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
14
+ }
15
+ return _cached;
16
+ }
17
+
18
+ // For simple CJS usage, expose a promise-based loader
19
+ module.exports = new Proxy({ load: _load }, {
20
+ get(target, prop) {
21
+ if (prop === '__esModule') return true;
22
+ if (prop === 'then') return undefined; // prevent treating as thenable
23
+ if (prop === 'load') return _load;
24
+ if (prop === 'default') {
25
+ return _load().then(m => m.default);
26
+ }
27
+ return _load().then(m => m[prop]);
28
+ }
29
+ });
package/dist/legacy.d.ts CHANGED
@@ -1 +1,31 @@
1
- export {};
1
+ /**
2
+ * STVOR SDK - Legacy Core API
3
+ * Kept for backwards compatibility
4
+ */
5
+ export interface StvorConfig {
6
+ apiKey: string;
7
+ serverUrl?: string;
8
+ }
9
+ export interface Peer {
10
+ id: string;
11
+ publicKey: any;
12
+ }
13
+ export interface EncryptedMessage {
14
+ ciphertext: string;
15
+ nonce: string;
16
+ from: string;
17
+ }
18
+ export declare class StvorClient {
19
+ private config;
20
+ private myKeyPair;
21
+ private myId;
22
+ private peers;
23
+ constructor(config: StvorConfig);
24
+ ready(): Promise<void>;
25
+ createPeer(name: string): Promise<Peer>;
26
+ send({ to, message }: {
27
+ to: string;
28
+ message: string;
29
+ }): Promise<EncryptedMessage>;
30
+ receive(encrypted: EncryptedMessage): Promise<string>;
31
+ }
package/dist/legacy.js CHANGED
@@ -1,2 +1,90 @@
1
- export {};
2
- // ...existing code from packages/sdk/legacy.ts...
1
+ /**
2
+ * STVOR SDK - Legacy Core API
3
+ * Kept for backwards compatibility
4
+ */
5
+ export class StvorClient {
6
+ constructor(config) {
7
+ this.myKeyPair = null;
8
+ this.myId = '';
9
+ this.peers = new Map();
10
+ this.config = {
11
+ serverUrl: 'http://localhost:3001',
12
+ ...config,
13
+ };
14
+ }
15
+ async ready() {
16
+ if (!this.config.apiKey) {
17
+ throw new Error('API key is required');
18
+ }
19
+ this.myKeyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits']);
20
+ this.myId = this.config.apiKey.substring(0, 8);
21
+ }
22
+ async createPeer(name) {
23
+ if (!this.myKeyPair)
24
+ throw new Error('Call ready() first');
25
+ const publicKey = await crypto.subtle.exportKey('jwk', this.myKeyPair.publicKey);
26
+ const res = await fetch(`${this.config.serverUrl}/register`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ user_id: name, publicKey }),
30
+ });
31
+ if (!res.ok) {
32
+ const err = await res.json();
33
+ throw new Error(`Registration failed: ${JSON.stringify(err)}`);
34
+ }
35
+ return { id: name, publicKey };
36
+ }
37
+ async send({ to, message }) {
38
+ if (!this.myKeyPair)
39
+ throw new Error('Call ready() first');
40
+ let recipientKey = this.peers.get(to);
41
+ if (!recipientKey) {
42
+ const res = await fetch(`${this.config.serverUrl}/public-key/${to}`);
43
+ if (!res.ok)
44
+ throw new Error(`Peer ${to} not found`);
45
+ const { publicKey } = await res.json();
46
+ recipientKey = await crypto.subtle.importKey('jwk', publicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
47
+ this.peers.set(to, recipientKey);
48
+ }
49
+ const sharedKey = await crypto.subtle.deriveKey({ name: 'ECDH', public: recipientKey }, this.myKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
50
+ const iv = crypto.getRandomValues(new Uint8Array(12));
51
+ const encoder = new TextEncoder();
52
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, encoder.encode(message));
53
+ const sendRes = await fetch(`${this.config.serverUrl}/message`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({
57
+ from: this.myId,
58
+ to,
59
+ ciphertext: Buffer.from(encrypted).toString('base64'),
60
+ nonce: Buffer.from(iv).toString('base64'),
61
+ }),
62
+ });
63
+ if (!sendRes.ok) {
64
+ throw new Error('Failed to send message');
65
+ }
66
+ return {
67
+ ciphertext: Buffer.from(encrypted).toString('base64'),
68
+ nonce: Buffer.from(iv).toString('base64'),
69
+ from: this.myId
70
+ };
71
+ }
72
+ async receive(encrypted) {
73
+ if (!this.myKeyPair)
74
+ throw new Error('Call ready() first');
75
+ let senderKey = this.peers.get(encrypted.from);
76
+ if (!senderKey) {
77
+ const res = await fetch(`${this.config.serverUrl}/public-key/${encrypted.from}`);
78
+ if (!res.ok)
79
+ throw new Error(`Sender ${encrypted.from} not found`);
80
+ const { publicKey } = await res.json();
81
+ senderKey = await crypto.subtle.importKey('jwk', publicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
82
+ this.peers.set(encrypted.from, senderKey);
83
+ }
84
+ const sharedKey = await crypto.subtle.deriveKey({ name: 'ECDH', public: senderKey }, this.myKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
85
+ const iv = Buffer.from(encrypted.nonce, 'base64');
86
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'base64');
87
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, sharedKey, ciphertext);
88
+ return new TextDecoder().decode(decrypted);
89
+ }
90
+ }
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Auto-generated CommonJS wrapper for ratchet/core-production.js
4
+ // This allows `require('@stvor/sdk')` to work alongside ESM `import`.
5
+
6
+ const mod = require('module');
7
+ const url = require('url');
8
+
9
+ // Use dynamic import to load the ESM module
10
+ let _cached;
11
+ async function _load() {
12
+ if (!_cached) {
13
+ _cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
14
+ }
15
+ return _cached;
16
+ }
17
+
18
+ // For simple CJS usage, expose a promise-based loader
19
+ module.exports = new Proxy({ load: _load }, {
20
+ get(target, prop) {
21
+ if (prop === '__esModule') return true;
22
+ if (prop === 'then') return undefined; // prevent treating as thenable
23
+ if (prop === 'load') return _load;
24
+ if (prop === 'default') {
25
+ return _load().then(m => m.default);
26
+ }
27
+ return _load().then(m => m[prop]);
28
+ }
29
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * STVOR SDK v2.4.0 - Production-Ready Core Ratchet
3
+ * Key changes from v2:
4
+ * - Header AAD authentication
5
+ * - Immutable state transitions
6
+ * - Functional design (pure functions)
7
+ * - Explicit error codes
8
+ */
9
+ export interface EncryptedMessage {
10
+ ciphertext: Uint8Array;
11
+ header: {
12
+ publicKey: Uint8Array;
13
+ nonce: Uint8Array;
14
+ sendCounter: number;
15
+ receiveCounter: number;
16
+ timestamp: number;
17
+ };
18
+ }
19
+ export interface SessionState {
20
+ peerId: string;
21
+ peerIdentityKey: Uint8Array;
22
+ rootKey: Uint8Array;
23
+ sendingChainKey: Uint8Array;
24
+ receivingChainKey: Uint8Array;
25
+ sendCounter: number;
26
+ receiveCounter: number;
27
+ skippedMessageKeys: Map<string, {
28
+ key: Uint8Array;
29
+ timestamp: number;
30
+ counter: number;
31
+ }>;
32
+ state: SessionFSMState;
33
+ lastRatchetTime: number;
34
+ lastRatchetCounter: number;
35
+ createdAt: number;
36
+ metadata: Record<string, any>;
37
+ }
38
+ export type SessionFSMState = 'INIT' | 'ESTABLISHED' | 'RATCHETING' | 'COMPROMISED';
39
+ export declare const ErrorCode: {
40
+ readonly DECRYPT_FAILED: "DECRYPT_FAILED";
41
+ readonly AUTH_FAILED: "AUTH_FAILED";
42
+ readonly INVALID_KEY_FORMAT: "INVALID_KEY_FORMAT";
43
+ readonly SPK_SIGNATURE_INVALID: "SPK_SIGNATURE_INVALID";
44
+ readonly REPLAY_DETECTED: "REPLAY_DETECTED";
45
+ readonly TOFU_MISMATCH: "TOFU_MISMATCH";
46
+ readonly INVALID_STATE_TRANSITION: "INVALID_STATE_TRANSITION";
47
+ readonly SESSION_COMPROMISED: "SESSION_COMPROMISED";
48
+ readonly STORAGE_UNAVAILABLE: "STORAGE_UNAVAILABLE";
49
+ readonly STORAGE_WRITE_FAILED: "STORAGE_WRITE_FAILED";
50
+ readonly SKIPPED_KEYS_LIMIT_EXCEEDED: "SKIPPED_KEYS_LIMIT_EXCEEDED";
51
+ readonly REPLAY_WINDOW_EXPIRED: "REPLAY_WINDOW_EXPIRED";
52
+ };
53
+ export declare class StvorSDKError extends Error {
54
+ readonly code: keyof typeof ErrorCode;
55
+ readonly metadata?: Record<string, any>;
56
+ constructor(code: keyof typeof ErrorCode, message: string, metadata?: Record<string, any>);
57
+ }
58
+ /**
59
+ * Decrypt message with full validation
60
+ * ATOMICALLY: validate ALL, then update session
61
+ */
62
+ export declare function decryptMessageWithValidation(ciphertext: Uint8Array, header: EncryptedMessage['header'], session: SessionState, validators: {
63
+ replayCache: IReplayCache;
64
+ tofuStore?: ITofuStore;
65
+ }): Promise<{
66
+ plaintext: string;
67
+ updatedSession: SessionState;
68
+ }>;
69
+ /**
70
+ * Encrypt message with policy enforcement
71
+ */
72
+ export declare function encryptMessageWithPolicy(plaintext: string, session: SessionState): {
73
+ message: EncryptedMessage;
74
+ updatedSession: SessionState;
75
+ };
76
+ export interface IReplayCache {
77
+ /**
78
+ * Check if nonce already seen
79
+ * Returns true if REPLAY detected
80
+ * MUST be atomic
81
+ */
82
+ checkAndMark(peerId: string, nonceHex: string, timestamp: number): Promise<boolean>;
83
+ }
84
+ export interface ITofuStore {
85
+ storeFingerprint(peerId: string, fingerprint: string): Promise<void>;
86
+ getFingerprint(peerId: string): Promise<string | null>;
87
+ }
88
+ export interface ISessionStore {
89
+ saveSession(peerId: string, session: SessionState): Promise<void>;
90
+ loadSession(peerId: string): Promise<SessionState | null>;
91
+ }
92
+ export interface IIdentityStore {
93
+ saveIdentityKeys(userId: string, keys: any): Promise<void>;
94
+ loadIdentityKeys(userId: string): Promise<any | null>;
95
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * STVOR SDK v2.4.0 - Production-Ready Core Ratchet
3
+ * Key changes from v2:
4
+ * - Header AAD authentication
5
+ * - Immutable state transitions
6
+ * - Functional design (pure functions)
7
+ * - Explicit error codes
8
+ */
9
+ import sodium from 'libsodium-wrappers';
10
+ // Constants for invariants
11
+ const DH_RATCHET_POLICY = {
12
+ maxMessages: 50,
13
+ maxTimeMs: 10 * 60 * 1000, // 10 minutes
14
+ };
15
+ const MAX_SKIPPED_KEYS_PER_SESSION = 50;
16
+ const MAX_TOTAL_SKIPPED_KEYS = 500;
17
+ const SKIPPED_KEY_TTL_MS = 5 * 60 * 1000; // 5 minutes
18
+ // ============================================================================
19
+ // PART 2: ERROR CODES (EXPLICIT)
20
+ // ============================================================================
21
+ export const ErrorCode = {
22
+ // Crypto errors
23
+ DECRYPT_FAILED: 'DECRYPT_FAILED',
24
+ AUTH_FAILED: 'AUTH_FAILED', // AAD verification failed
25
+ INVALID_KEY_FORMAT: 'INVALID_KEY_FORMAT',
26
+ SPK_SIGNATURE_INVALID: 'SPK_SIGNATURE_INVALID',
27
+ // Replay / TOFU
28
+ REPLAY_DETECTED: 'REPLAY_DETECTED',
29
+ TOFU_MISMATCH: 'TOFU_MISMATCH',
30
+ // State machine
31
+ INVALID_STATE_TRANSITION: 'INVALID_STATE_TRANSITION',
32
+ SESSION_COMPROMISED: 'SESSION_COMPROMISED',
33
+ // Storage
34
+ STORAGE_UNAVAILABLE: 'STORAGE_UNAVAILABLE',
35
+ STORAGE_WRITE_FAILED: 'STORAGE_WRITE_FAILED',
36
+ // DoS protection
37
+ SKIPPED_KEYS_LIMIT_EXCEEDED: 'SKIPPED_KEYS_LIMIT_EXCEEDED',
38
+ REPLAY_WINDOW_EXPIRED: 'REPLAY_WINDOW_EXPIRED',
39
+ };
40
+ export class StvorSDKError extends Error {
41
+ constructor(code, message, metadata) {
42
+ super(message);
43
+ this.code = code;
44
+ this.metadata = metadata;
45
+ this.name = 'StvorSDKError';
46
+ }
47
+ }
48
+ // ============================================================================
49
+ // PART 3: IMMUTABLE STATE MANAGEMENT
50
+ // ============================================================================
51
+ /**
52
+ * Pure function: compute new session state WITHOUT mutating input
53
+ * Returns new state object on success, throws on error
54
+ *
55
+ * CRITICAL: This function has NO SIDE EFFECTS
56
+ * It only computes, doesn't update globals or storage
57
+ */
58
+ function tryDecryptMessage(ciphertext, header, session) {
59
+ // Attempt to use skipped key first
60
+ const skippedKeyEntry = findAndValidateSkippedKey(session, header);
61
+ if (skippedKeyEntry) {
62
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, constructAAD(header), header.nonce, skippedKeyEntry.key);
63
+ // New state: remove used skipped key, increment receive counter
64
+ const newSession = structuredClone(session);
65
+ const skippedKeyId = generateSkippedKeyId(header);
66
+ newSession.skippedMessageKeys.delete(skippedKeyId);
67
+ newSession.receiveCounter = header.receiveCounter + 1;
68
+ return newSession;
69
+ }
70
+ // Standard ratchet decryption
71
+ const sharedSecret = sodium.crypto_kx_client_session_keys(session.peerIdentityKey, session.sendingChainKey, // Recipient's SPK
72
+ header.publicKey);
73
+ // Compute new root key
74
+ const newRootKey = deriveRootKeyFromDH(session.rootKey, sharedSecret.sharedTx, 'receive');
75
+ // Derive new chain key
76
+ const newReceivingChainKey = deriveChainKey(newRootKey, 'receiving');
77
+ // Derive message key
78
+ const messageKey = deriveMessageKey(newReceivingChainKey);
79
+ // Decrypt with AAD verification
80
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, constructAAD(header), // ← NEW: authenticate header
81
+ header.nonce, messageKey);
82
+ // Create new state (not mutating input)
83
+ const newSession = structuredClone(session);
84
+ newSession.rootKey = newRootKey;
85
+ newSession.receivingChainKey = newReceivingChainKey;
86
+ newSession.receiveCounter = header.receiveCounter + 1;
87
+ return newSession;
88
+ }
89
+ /**
90
+ * Pure function: compute new session state for encryption
91
+ * Returns { ciphertext, header, newSession }
92
+ */
93
+ function tryEncryptMessage(plaintext, session) {
94
+ // Check if ratchet needed (pure predicate)
95
+ const shouldRatchet = checkDHRatchetPolicy(session);
96
+ let currentSession = structuredClone(session);
97
+ // Apply ratchet if needed
98
+ if (shouldRatchet) {
99
+ currentSession = performDHRatchet(currentSession);
100
+ }
101
+ // Encrypt
102
+ const ratchetKeyPair = sodium.crypto_kx_keypair();
103
+ const sharedSecret = sodium.crypto_kx_client_session_keys(ratchetKeyPair.publicKey, ratchetKeyPair.privateKey, currentSession.peerIdentityKey);
104
+ // Derive new root key
105
+ const newRootKey = deriveRootKeyFromDH(currentSession.rootKey, sharedSecret.sharedTx, 'send');
106
+ // Derive chain key
107
+ const newSendingChainKey = deriveChainKey(newRootKey, 'sending');
108
+ const messageKey = deriveMessageKey(newSendingChainKey);
109
+ // Build header
110
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
111
+ const header = {
112
+ publicKey: ratchetKeyPair.publicKey,
113
+ nonce,
114
+ sendCounter: currentSession.sendCounter,
115
+ receiveCounter: currentSession.receiveCounter,
116
+ timestamp: Date.now(),
117
+ };
118
+ // Encrypt with AAD
119
+ const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(sodium.from_string(plaintext), constructAAD(header), // ← NEW: authenticate header
120
+ null, nonce, messageKey);
121
+ // New state
122
+ const newSession = structuredClone(currentSession);
123
+ newSession.rootKey = newRootKey;
124
+ newSession.sendingChainKey = newSendingChainKey;
125
+ newSession.sendCounter++;
126
+ newSession.lastRatchetCounter = shouldRatchet
127
+ ? currentSession.sendCounter
128
+ : currentSession.lastRatchetCounter;
129
+ return { ciphertext, header, newSession };
130
+ }
131
+ // ============================================================================
132
+ // PART 4: STATE MACHINE ENFORCEMENT
133
+ // ============================================================================
134
+ /**
135
+ * Validate state transition
136
+ * Prevents invalid transitions and enforces FSM invariants
137
+ */
138
+ function validateStateTransition(fromState, toState) {
139
+ const validTransitions = {
140
+ 'INIT': ['ESTABLISHED'],
141
+ 'ESTABLISHED': ['RATCHETING', 'COMPROMISED'],
142
+ 'RATCHETING': ['ESTABLISHED', 'COMPROMISED'],
143
+ 'COMPROMISED': [], // Terminal state
144
+ };
145
+ if (!validTransitions[fromState]?.includes(toState)) {
146
+ throw new StvorSDKError('INVALID_STATE_TRANSITION', `Cannot transition from ${fromState} to ${toState}`);
147
+ }
148
+ }
149
+ function checkDHRatchetPolicy(session) {
150
+ const elapsed = Date.now() - session.lastRatchetTime;
151
+ const messagesSinceRatchet = session.sendCounter - session.lastRatchetCounter;
152
+ return (messagesSinceRatchet >= DH_RATCHET_POLICY.maxMessages ||
153
+ elapsed >= DH_RATCHET_POLICY.maxTimeMs);
154
+ }
155
+ function performDHRatchet(session) {
156
+ // Generate new ephemeral
157
+ const ephemeralKeyPair = sodium.crypto_kx_keypair();
158
+ // Perform DH
159
+ const dhOutput = sodium.crypto_kx_client_session_keys(ephemeralKeyPair.publicKey, ephemeralKeyPair.privateKey, session.peerIdentityKey);
160
+ // Derive new root key
161
+ const newRootKey = deriveRootKeyFromDH(session.rootKey, dhOutput.sharedTx, 'ratchet');
162
+ // New state
163
+ const newSession = structuredClone(session);
164
+ newSession.rootKey = newRootKey;
165
+ newSession.sendingChainKey = deriveChainKey(newRootKey, 'sending');
166
+ newSession.receivingChainKey = deriveChainKey(newRootKey, 'receiving');
167
+ newSession.skippedMessageKeys.clear(); // Clear old keys
168
+ newSession.lastRatchetTime = Date.now();
169
+ newSession.state = 'RATCHETING';
170
+ return newSession;
171
+ }
172
+ // ============================================================================
173
+ // PART 5: KEY DERIVATION (PURE)
174
+ // ============================================================================
175
+ function deriveRootKeyFromDH(oldRootKey, dhOutput, context) {
176
+ const info = sodium.from_string(`stvor:dh:${context}`);
177
+ return sodium.crypto_generichash(32, sodium.from_string('HKDF-Extract'), new Uint8Array([...oldRootKey, ...dhOutput, ...info]));
178
+ }
179
+ function deriveChainKey(rootKey, direction) {
180
+ const info = sodium.from_string(`stvor:chain:${direction}`);
181
+ return sodium.crypto_generichash(32, rootKey, info);
182
+ }
183
+ function deriveMessageKey(chainKey) {
184
+ const info = sodium.from_string('stvor:message-key');
185
+ return sodium.crypto_generichash(32, chainKey, info);
186
+ }
187
+ // ============================================================================
188
+ // PART 6: AAD CONSTRUCTION (AUTHENTICATED ADDITIONAL DATA)
189
+ // ============================================================================
190
+ /**
191
+ * Construct AAD from message header
192
+ * This ensures header cannot be tampered without detection
193
+ */
194
+ function constructAAD(header) {
195
+ return sodium.crypto_generichash(32, sodium.from_string('AAD'), new Uint8Array([
196
+ ...header.publicKey,
197
+ ...header.nonce,
198
+ ...(new Uint32Array([header.sendCounter])),
199
+ ...(new Uint32Array([header.receiveCounter])),
200
+ ...(new Uint32Array([header.timestamp])),
201
+ ]));
202
+ }
203
+ // ============================================================================
204
+ // PART 7: SKIPPED MESSAGE KEYS
205
+ // ============================================================================
206
+ function generateSkippedKeyId(header) {
207
+ return sodium.to_hex(sodium.crypto_generichash(32, header.nonce));
208
+ }
209
+ function findAndValidateSkippedKey(session, header) {
210
+ const keyId = generateSkippedKeyId(header);
211
+ const entry = session.skippedMessageKeys.get(keyId);
212
+ if (!entry) {
213
+ return null;
214
+ }
215
+ // Check TTL
216
+ if (Date.now() - entry.timestamp > SKIPPED_KEY_TTL_MS) {
217
+ return null; // Expired
218
+ }
219
+ return entry;
220
+ }
221
+ function addSkippedKey(session, nonce, key) {
222
+ // Check limits
223
+ if (session.skippedMessageKeys.size >= MAX_SKIPPED_KEYS_PER_SESSION) {
224
+ throw new StvorSDKError('SKIPPED_KEYS_LIMIT_EXCEEDED', `Per-session skipped keys limit (${MAX_SKIPPED_KEYS_PER_SESSION}) exceeded`);
225
+ }
226
+ const keyId = sodium.to_hex(sodium.crypto_generichash(32, nonce));
227
+ session.skippedMessageKeys.set(keyId, {
228
+ key,
229
+ timestamp: Date.now(),
230
+ counter: session.receiveCounter,
231
+ });
232
+ }
233
+ // ============================================================================
234
+ // PART 8: PUBLIC API (FACADE)
235
+ // ============================================================================
236
+ /**
237
+ * Decrypt message with full validation
238
+ * ATOMICALLY: validate ALL, then update session
239
+ */
240
+ export async function decryptMessageWithValidation(ciphertext, header, session, validators) {
241
+ // PHASE 1: Validation (no state changes)
242
+ // Check state machine
243
+ if (session.state === 'COMPROMISED') {
244
+ throw new StvorSDKError('SESSION_COMPROMISED', 'Session is compromised, recovery required');
245
+ }
246
+ // Check replay
247
+ const isReplay = await validators.replayCache.checkAndMark(session.peerId, sodium.to_hex(header.nonce), header.timestamp);
248
+ if (isReplay) {
249
+ throw new StvorSDKError('REPLAY_DETECTED', 'Message is a replay', { nonce: sodium.to_hex(header.nonce) });
250
+ }
251
+ // Compute new state (pure, no mutations)
252
+ let newSession;
253
+ try {
254
+ newSession = tryDecryptMessage(ciphertext, header, session);
255
+ }
256
+ catch (error) {
257
+ if (error.code === 'EBADMSG') {
258
+ throw new StvorSDKError('AUTH_FAILED', 'AAD authentication failed');
259
+ }
260
+ throw new StvorSDKError('DECRYPT_FAILED', error.message);
261
+ }
262
+ // PHASE 2: Commit (all validations passed)
263
+ // Update session state atomically
264
+ Object.assign(session, newSession);
265
+ return {
266
+ plaintext: newSession.toString(), // Placeholder
267
+ updatedSession: session,
268
+ };
269
+ }
270
+ /**
271
+ * Encrypt message with policy enforcement
272
+ */
273
+ export function encryptMessageWithPolicy(plaintext, session) {
274
+ // Check state
275
+ if (session.state === 'COMPROMISED') {
276
+ throw new StvorSDKError('SESSION_COMPROMISED', 'Cannot encrypt: session is compromised');
277
+ }
278
+ // Compute new state (pure)
279
+ const { ciphertext, header, newSession } = tryEncryptMessage(plaintext, session);
280
+ // Update session (atomic)
281
+ Object.assign(session, newSession);
282
+ return {
283
+ message: { ciphertext, header },
284
+ updatedSession: session,
285
+ };
286
+ }
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Auto-generated CommonJS wrapper for ratchet/index.js
4
+ // This allows `require('@stvor/sdk')` to work alongside ESM `import`.
5
+
6
+ const mod = require('module');
7
+ const url = require('url');
8
+
9
+ // Use dynamic import to load the ESM module
10
+ let _cached;
11
+ async function _load() {
12
+ if (!_cached) {
13
+ _cached = await import(url.pathToFileURL(__filename.replace(/\.cjs$/, '.js')).href);
14
+ }
15
+ return _cached;
16
+ }
17
+
18
+ // For simple CJS usage, expose a promise-based loader
19
+ module.exports = new Proxy({ load: _load }, {
20
+ get(target, prop) {
21
+ if (prop === '__esModule') return true;
22
+ if (prop === 'then') return undefined; // prevent treating as thenable
23
+ if (prop === 'load') return _load;
24
+ if (prop === 'default') {
25
+ return _load().then(m => m.default);
26
+ }
27
+ return _load().then(m => m[prop]);
28
+ }
29
+ });