@stvor/sdk 2.3.2 → 2.4.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/dist/facade/app.cjs +29 -0
- package/dist/facade/crypto.cjs +29 -0
- package/dist/facade/errors.cjs +29 -0
- package/dist/facade/index.cjs +29 -0
- package/dist/facade/relay-client.cjs +29 -0
- package/dist/facade/types.cjs +29 -0
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +2 -0
- package/dist/legacy.cjs +29 -0
- package/dist/mock-relay-server.cjs +29 -0
- package/dist/mock-relay-server.d.ts +30 -0
- package/dist/mock-relay-server.js +236 -0
- package/package.json +23 -8
- package/dist/facade/crypto-session.d.ts +0 -76
- package/dist/facade/crypto-session.js +0 -175
- package/dist/facade/replay-manager.d.ts +0 -58
- package/dist/facade/replay-manager.js +0 -117
- package/dist/facade/sodium-singleton.d.ts +0 -20
- package/dist/facade/sodium-singleton.js +0 -44
- package/dist/facade/tofu-manager.d.ts +0 -80
- package/dist/facade/tofu-manager.js +0 -134
- package/dist/ratchet/index.d.ts +0 -88
- package/dist/ratchet/index.js +0 -318
- package/dist/ratchet/key-recovery.d.ts +0 -45
- package/dist/ratchet/key-recovery.js +0 -148
- package/dist/ratchet/replay-protection.d.ts +0 -21
- package/dist/ratchet/replay-protection.js +0 -50
- package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
- package/dist/ratchet/tests/ratchet.test.js +0 -160
- package/dist/ratchet/tofu.d.ts +0 -27
- package/dist/ratchet/tofu.js +0 -62
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/app.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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/crypto.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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/errors.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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/relay-client.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,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for facade/types.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/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
|
+
});
|
package/dist/index.d.cts
ADDED
package/dist/legacy.cjs
ADDED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for mock-relay-server.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,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* STVOR Mock Relay Server
|
|
4
|
+
*
|
|
5
|
+
* A lightweight local development server that emulates the production
|
|
6
|
+
* STVOR relay over WebSocket. Run it locally to develop and test
|
|
7
|
+
* without internet access or a production relay.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx @stvor/sdk mock-relay # via npx
|
|
11
|
+
* npm run mock-relay # from SDK root
|
|
12
|
+
* node dist/mock-relay-server.js # direct
|
|
13
|
+
* PORT=9000 node dist/mock-relay-server.js # custom port
|
|
14
|
+
* STVOR_MOCK_VERBOSE=1 node dist/mock-relay-server.js # verbose
|
|
15
|
+
*
|
|
16
|
+
* Accepts any AppToken starting with "stvor_" for easy local testing.
|
|
17
|
+
*
|
|
18
|
+
* Protocol:
|
|
19
|
+
* Connection: ws://localhost:PORT with Authorization header
|
|
20
|
+
* Handshake: Server sends { type: 'handshake', status: 'ok' }
|
|
21
|
+
* Announce: { type: 'announce', user: string, pub: string }
|
|
22
|
+
* Message: { type: 'message', to: string, from: string, payload: any }
|
|
23
|
+
* Ack: { type: 'ack', id: string }
|
|
24
|
+
* Error: { type: 'error', code: string, message: string }
|
|
25
|
+
*/
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
declare const PORT: number;
|
|
28
|
+
declare const httpServer: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
29
|
+
declare const wss: any;
|
|
30
|
+
export { PORT, wss, httpServer };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* STVOR Mock Relay Server
|
|
4
|
+
*
|
|
5
|
+
* A lightweight local development server that emulates the production
|
|
6
|
+
* STVOR relay over WebSocket. Run it locally to develop and test
|
|
7
|
+
* without internet access or a production relay.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx @stvor/sdk mock-relay # via npx
|
|
11
|
+
* npm run mock-relay # from SDK root
|
|
12
|
+
* node dist/mock-relay-server.js # direct
|
|
13
|
+
* PORT=9000 node dist/mock-relay-server.js # custom port
|
|
14
|
+
* STVOR_MOCK_VERBOSE=1 node dist/mock-relay-server.js # verbose
|
|
15
|
+
*
|
|
16
|
+
* Accepts any AppToken starting with "stvor_" for easy local testing.
|
|
17
|
+
*
|
|
18
|
+
* Protocol:
|
|
19
|
+
* Connection: ws://localhost:PORT with Authorization header
|
|
20
|
+
* Handshake: Server sends { type: 'handshake', status: 'ok' }
|
|
21
|
+
* Announce: { type: 'announce', user: string, pub: string }
|
|
22
|
+
* Message: { type: 'message', to: string, from: string, payload: any }
|
|
23
|
+
* Ack: { type: 'ack', id: string }
|
|
24
|
+
* Error: { type: 'error', code: string, message: string }
|
|
25
|
+
*/
|
|
26
|
+
import * as WS from 'ws';
|
|
27
|
+
import http from 'node:http';
|
|
28
|
+
const { WebSocketServer } = WS;
|
|
29
|
+
// ── Configuration ────────────────────────────────────────────────────
|
|
30
|
+
const PORT = parseInt(process.env.STVOR_MOCK_PORT || process.env.PORT || '4444', 10);
|
|
31
|
+
const VERBOSE = process.env.STVOR_MOCK_VERBOSE === '1';
|
|
32
|
+
// ── In-memory state ──────────────────────────────────────────────────
|
|
33
|
+
/** userId → WebSocket */
|
|
34
|
+
const clients = new Map();
|
|
35
|
+
/** userId → public key (base64) */
|
|
36
|
+
const pubkeys = new Map();
|
|
37
|
+
/** userId → pending messages (for offline delivery) */
|
|
38
|
+
const mailboxes = new Map();
|
|
39
|
+
let totalConnections = 0;
|
|
40
|
+
let totalMessages = 0;
|
|
41
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
42
|
+
function log(...args) {
|
|
43
|
+
if (VERBOSE)
|
|
44
|
+
console.log('[mock-relay]', new Date().toISOString(), ...args);
|
|
45
|
+
}
|
|
46
|
+
function validateAuth(req) {
|
|
47
|
+
const auth = req.headers.authorization;
|
|
48
|
+
if (!auth)
|
|
49
|
+
return false;
|
|
50
|
+
const token = auth.replace(/^Bearer\s+/i, '');
|
|
51
|
+
return token.startsWith('stvor_');
|
|
52
|
+
}
|
|
53
|
+
function broadcast(obj, exceptWs) {
|
|
54
|
+
const data = JSON.stringify(obj);
|
|
55
|
+
for (const ws of clients.values()) {
|
|
56
|
+
if (ws !== exceptWs && ws.readyState === 1 /* OPEN */) {
|
|
57
|
+
ws.send(data);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ── HTTP server for health check (and future REST endpoints) ─────────
|
|
62
|
+
const httpServer = http.createServer((req, res) => {
|
|
63
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
64
|
+
// CORS
|
|
65
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
66
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
67
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
68
|
+
if (req.method === 'OPTIONS') {
|
|
69
|
+
res.writeHead(204);
|
|
70
|
+
return res.end();
|
|
71
|
+
}
|
|
72
|
+
if (url.pathname === '/health') {
|
|
73
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
74
|
+
return res.end(JSON.stringify({
|
|
75
|
+
status: 'ok',
|
|
76
|
+
server: 'stvor-mock-relay',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
uptime: process.uptime(),
|
|
79
|
+
connectedUsers: clients.size,
|
|
80
|
+
totalConnections,
|
|
81
|
+
totalMessages,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
// GET /status/:userId - check if user is online
|
|
85
|
+
const statusMatch = url.pathname.match(/^\/status\/(.+)$/);
|
|
86
|
+
if (statusMatch) {
|
|
87
|
+
const userId = decodeURIComponent(statusMatch[1]);
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
89
|
+
return res.end(JSON.stringify({
|
|
90
|
+
userId,
|
|
91
|
+
online: clients.has(userId),
|
|
92
|
+
hasPublicKey: pubkeys.has(userId),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
// GET /usage - mock unlimited usage
|
|
96
|
+
if (url.pathname === '/usage') {
|
|
97
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
98
|
+
return res.end(JSON.stringify({ used: 0, limit: -1 }));
|
|
99
|
+
}
|
|
100
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
102
|
+
});
|
|
103
|
+
// ── WebSocket server ─────────────────────────────────────────────────
|
|
104
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
105
|
+
wss.on('connection', (ws, req) => {
|
|
106
|
+
totalConnections++;
|
|
107
|
+
// ── Auth check ───────────────────────────────────────────────────
|
|
108
|
+
if (!validateAuth(req)) {
|
|
109
|
+
log('Auth failed for connection');
|
|
110
|
+
ws.send(JSON.stringify({
|
|
111
|
+
type: 'handshake',
|
|
112
|
+
status: 'error',
|
|
113
|
+
reason: 'Invalid AppToken. Token must start with "stvor_".',
|
|
114
|
+
}));
|
|
115
|
+
ws.close(4001, 'Unauthorized');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// ── Successful handshake ─────────────────────────────────────────
|
|
119
|
+
ws.send(JSON.stringify({ type: 'handshake', status: 'ok' }));
|
|
120
|
+
log('Client connected (auth OK)');
|
|
121
|
+
// Send all known user announcements to the new client
|
|
122
|
+
for (const [user, pub] of pubkeys.entries()) {
|
|
123
|
+
try {
|
|
124
|
+
ws.send(JSON.stringify({ type: 'announce', user, pub }));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Message handler ──────────────────────────────────────────────
|
|
131
|
+
ws.on('message', (data) => {
|
|
132
|
+
let msg;
|
|
133
|
+
try {
|
|
134
|
+
msg = JSON.parse(data.toString());
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// ── Announce: register user + broadcast public key ────────────
|
|
140
|
+
if (msg.type === 'announce' && msg.user) {
|
|
141
|
+
const oldWs = clients.get(msg.user);
|
|
142
|
+
clients.set(msg.user, ws);
|
|
143
|
+
if (msg.pub) {
|
|
144
|
+
pubkeys.set(msg.user, msg.pub);
|
|
145
|
+
}
|
|
146
|
+
log(`User announced: ${msg.user}`);
|
|
147
|
+
// Broadcast announce to all other clients
|
|
148
|
+
broadcast({ type: 'announce', user: msg.user, pub: msg.pub }, ws);
|
|
149
|
+
// Deliver any pending messages
|
|
150
|
+
const pending = mailboxes.get(msg.user) || [];
|
|
151
|
+
if (pending.length > 0) {
|
|
152
|
+
log(`Delivering ${pending.length} pending messages to ${msg.user}`);
|
|
153
|
+
for (const m of pending) {
|
|
154
|
+
try {
|
|
155
|
+
ws.send(JSON.stringify(m));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
mailboxes.set(msg.user, []);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// ── Message: route to recipient ──────────────────────────────
|
|
166
|
+
if (msg.type === 'message' && msg.to) {
|
|
167
|
+
totalMessages++;
|
|
168
|
+
const target = clients.get(msg.to);
|
|
169
|
+
if (target && target.readyState === 1 /* OPEN */) {
|
|
170
|
+
// Deliver immediately
|
|
171
|
+
target.send(JSON.stringify(msg));
|
|
172
|
+
log(`Message delivered: ${msg.from} → ${msg.to}`);
|
|
173
|
+
// ACK back to sender
|
|
174
|
+
if (msg.from && ws.readyState === 1 /* OPEN */) {
|
|
175
|
+
ws.send(JSON.stringify({ type: 'ack', id: msg.id ?? null }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Store for later delivery
|
|
180
|
+
if (!mailboxes.has(msg.to)) {
|
|
181
|
+
mailboxes.set(msg.to, []);
|
|
182
|
+
}
|
|
183
|
+
mailboxes.get(msg.to).push(msg);
|
|
184
|
+
log(`Message queued: ${msg.from} → ${msg.to} (recipient offline)`);
|
|
185
|
+
// Notify sender that message is queued
|
|
186
|
+
if (ws.readyState === 1 /* OPEN */) {
|
|
187
|
+
ws.send(JSON.stringify({
|
|
188
|
+
type: 'queued',
|
|
189
|
+
id: msg.id ?? null,
|
|
190
|
+
to: msg.to,
|
|
191
|
+
message: 'Recipient is offline. Message queued for delivery.',
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
log('Unknown message type:', msg.type);
|
|
198
|
+
});
|
|
199
|
+
// ── Disconnect ───────────────────────────────────────────────────
|
|
200
|
+
ws.on('close', () => {
|
|
201
|
+
for (const [user, sock] of clients.entries()) {
|
|
202
|
+
if (sock === ws) {
|
|
203
|
+
log(`User disconnected: ${user}`);
|
|
204
|
+
clients.delete(user);
|
|
205
|
+
// Keep public key so reconnecting users can still be found
|
|
206
|
+
// pubkeys.delete(user); // intentionally NOT deleted
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
ws.on('error', (err) => {
|
|
211
|
+
log('WebSocket error:', err.message);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ── Start server ─────────────────────────────────────────────────────
|
|
215
|
+
httpServer.listen(PORT, () => {
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(' ╔══════════════════════════════════════════════════════╗');
|
|
218
|
+
console.log(' ║ STVOR Mock Relay Server v1.0.0 ║');
|
|
219
|
+
console.log(' ╠══════════════════════════════════════════════════════╣');
|
|
220
|
+
console.log(` ║ WebSocket: ws://localhost:${String(PORT).padEnd(27)}║`);
|
|
221
|
+
console.log(` ║ Health: http://localhost:${String(PORT).padEnd(22)}║`);
|
|
222
|
+
console.log(' ║ Auth: Any token starting with "stvor_" ║');
|
|
223
|
+
console.log(' ║ Data: In-memory (resets on restart) ║');
|
|
224
|
+
console.log(' ╚══════════════════════════════════════════════════════╝');
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(' Usage in your app:');
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(" const app = await Stvor.init({");
|
|
229
|
+
console.log(" appToken: 'stvor_dev_test123',");
|
|
230
|
+
console.log(` relayUrl: 'ws://localhost:${PORT}'`);
|
|
231
|
+
console.log(" });");
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(' Set STVOR_MOCK_VERBOSE=1 for detailed logging.');
|
|
234
|
+
console.log('');
|
|
235
|
+
});
|
|
236
|
+
export { PORT, wss, httpServer };
|
package/package.json
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stvor/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Stvor DX Facade - Simple E2EE SDK for client-side encryption",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
|
-
"import":
|
|
11
|
-
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./mock-relay": {
|
|
21
|
+
"import": "./dist/mock-relay-server.js",
|
|
22
|
+
"require": "./dist/mock-relay-server.cjs"
|
|
12
23
|
}
|
|
13
24
|
},
|
|
14
25
|
"files": [
|
|
@@ -28,19 +39,23 @@
|
|
|
28
39
|
"license": "MIT",
|
|
29
40
|
"repository": {
|
|
30
41
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/
|
|
42
|
+
"url": "git+https://github.com/sapogeth/stvor_sdk.git"
|
|
32
43
|
},
|
|
33
44
|
"engines": {
|
|
34
45
|
"node": ">=18.0.0"
|
|
35
46
|
},
|
|
36
47
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/
|
|
48
|
+
"url": "https://github.com/sapogeth/stvor_sdk/issues"
|
|
38
49
|
},
|
|
39
50
|
"homepage": "https://stvor.xyz",
|
|
40
51
|
"scripts": {
|
|
41
|
-
"build": "tsc",
|
|
52
|
+
"build": "tsc && node postbuild-fix-extensions.cjs && node build-cjs.cjs",
|
|
42
53
|
"prepublishOnly": "npm run build",
|
|
43
|
-
"test": "echo \"No tests yet\" && exit 0"
|
|
54
|
+
"test": "echo \"No tests yet\" && exit 0",
|
|
55
|
+
"mock-relay": "node dist/mock-relay-server.js"
|
|
56
|
+
},
|
|
57
|
+
"bin": {
|
|
58
|
+
"stvor-mock-relay": "./dist/mock-relay-server.js"
|
|
44
59
|
},
|
|
45
60
|
"dependencies": {
|
|
46
61
|
"ws": "^8.13.0",
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* STVOR Crypto Session Manager
|
|
3
|
-
* Integrates X3DH + Double Ratchet from ratchet module
|
|
4
|
-
*
|
|
5
|
-
* CRITICAL: Identity keys generated ONCE per userId
|
|
6
|
-
* Currently in-memory only - keys lost on restart
|
|
7
|
-
*
|
|
8
|
-
* TODO: Add persistent storage (IndexedDB/Keychain)
|
|
9
|
-
*/
|
|
10
|
-
export interface IdentityKeys {
|
|
11
|
-
identityKeyPair: {
|
|
12
|
-
publicKey: Uint8Array;
|
|
13
|
-
privateKey: Uint8Array;
|
|
14
|
-
};
|
|
15
|
-
signedPreKeyPair: {
|
|
16
|
-
publicKey: Uint8Array;
|
|
17
|
-
privateKey: Uint8Array;
|
|
18
|
-
};
|
|
19
|
-
oneTimePreKeys: Uint8Array[];
|
|
20
|
-
}
|
|
21
|
-
export interface SerializedPublicKeys {
|
|
22
|
-
identityKey: string;
|
|
23
|
-
signedPreKey: string;
|
|
24
|
-
signedPreKeySignature: string;
|
|
25
|
-
oneTimePreKey: string;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Manages cryptographic sessions for all peers
|
|
29
|
-
*/
|
|
30
|
-
export declare class CryptoSessionManager {
|
|
31
|
-
private userId;
|
|
32
|
-
private identityKeys;
|
|
33
|
-
private sessions;
|
|
34
|
-
private initialized;
|
|
35
|
-
private initPromise;
|
|
36
|
-
constructor(userId: string);
|
|
37
|
-
/**
|
|
38
|
-
* Initialize libsodium and generate identity keys
|
|
39
|
-
* RACE CONDITION SAFE: Returns same promise if called concurrently
|
|
40
|
-
*/
|
|
41
|
-
initialize(): Promise<void>;
|
|
42
|
-
private _doInitialize;
|
|
43
|
-
/**
|
|
44
|
-
* Get serialized public keys for relay registration
|
|
45
|
-
*/
|
|
46
|
-
getPublicKeys(): SerializedPublicKeys;
|
|
47
|
-
/**
|
|
48
|
-
* Establish session with peer (X3DH handshake)
|
|
49
|
-
*/
|
|
50
|
-
establishSessionWithPeer(peerId: string, peerPublicKeys: SerializedPublicKeys): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Encrypt message for peer using Double Ratchet
|
|
53
|
-
*/
|
|
54
|
-
encryptForPeer(peerId: string, plaintext: string): Promise<{
|
|
55
|
-
ciphertext: Uint8Array;
|
|
56
|
-
header: {
|
|
57
|
-
publicKey: Uint8Array;
|
|
58
|
-
nonce: Uint8Array;
|
|
59
|
-
};
|
|
60
|
-
}>;
|
|
61
|
-
/**
|
|
62
|
-
* Decrypt message from peer using Double Ratchet
|
|
63
|
-
*/
|
|
64
|
-
decryptFromPeer(peerId: string, ciphertext: Uint8Array, header: {
|
|
65
|
-
publicKey: Uint8Array;
|
|
66
|
-
nonce: Uint8Array;
|
|
67
|
-
}): Promise<string>;
|
|
68
|
-
/**
|
|
69
|
-
* Check if session exists with peer
|
|
70
|
-
*/
|
|
71
|
-
hasSession(peerId: string): boolean;
|
|
72
|
-
/**
|
|
73
|
-
* Destroy all sessions (cleanup)
|
|
74
|
-
*/
|
|
75
|
-
destroy(): void;
|
|
76
|
-
}
|