@stvor/sdk 2.0.8 → 2.0.9
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.d.ts +8 -2
- package/dist/facade/app.js +44 -4
- package/dist/facade/crypto.d.ts +19 -0
- package/dist/facade/crypto.js +46 -0
- package/dist/facade/errors.d.ts +4 -0
- package/dist/facade/errors.js +4 -0
- package/dist/facade/relay-client.d.ts +12 -1
- package/dist/facade/relay-client.js +61 -16
- package/package.json +24 -5
package/dist/facade/app.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { StvorAppConfig, UserId, MessageContent
|
|
1
|
+
import type { StvorAppConfig, UserId, MessageContent } from './types.js';
|
|
2
2
|
import { RelayClient } from './relay-client.js';
|
|
3
|
+
type MessageHandler = (from: UserId, msg: string | Uint8Array) => void;
|
|
3
4
|
export declare class StvorFacadeClient {
|
|
4
5
|
readonly userId: UserId;
|
|
5
6
|
private readonly relay;
|
|
7
|
+
private crypto;
|
|
8
|
+
private handlers;
|
|
9
|
+
private knownPubKeys;
|
|
6
10
|
constructor(userId: UserId, relay: RelayClient);
|
|
11
|
+
private handleRelayMessage;
|
|
7
12
|
internalInitialize(): Promise<void>;
|
|
8
13
|
send(recipientId: UserId, content: MessageContent): Promise<void>;
|
|
9
|
-
onMessage(handler:
|
|
14
|
+
onMessage(handler: MessageHandler): () => void;
|
|
10
15
|
}
|
|
11
16
|
export declare class StvorApp {
|
|
12
17
|
private readonly config;
|
|
@@ -19,3 +24,4 @@ export declare function init(config: StvorAppConfig): Promise<StvorApp>;
|
|
|
19
24
|
export declare const Stvor: {
|
|
20
25
|
init: typeof init;
|
|
21
26
|
};
|
|
27
|
+
export {};
|
package/dist/facade/app.js
CHANGED
|
@@ -1,19 +1,59 @@
|
|
|
1
1
|
import { Errors, StvorError } from './errors.js';
|
|
2
2
|
import { RelayClient } from './relay-client.js';
|
|
3
|
+
import { CryptoSession } from './crypto.js';
|
|
3
4
|
export class StvorFacadeClient {
|
|
4
5
|
constructor(userId, relay) {
|
|
5
6
|
this.userId = userId;
|
|
6
7
|
this.relay = relay;
|
|
8
|
+
this.handlers = [];
|
|
9
|
+
this.knownPubKeys = new Map();
|
|
10
|
+
this.crypto = new CryptoSession();
|
|
11
|
+
// listen relay messages
|
|
12
|
+
this.relay.onMessage((m) => this.handleRelayMessage(m));
|
|
13
|
+
// announce our public key
|
|
14
|
+
this.relay.send({ type: 'announce', user: this.userId, pub: this.crypto.exportPublic() });
|
|
15
|
+
}
|
|
16
|
+
async handleRelayMessage(m) {
|
|
17
|
+
if (!m || typeof m !== 'object')
|
|
18
|
+
return;
|
|
19
|
+
if (m.type === 'announce' && m.user && m.pub) {
|
|
20
|
+
this.knownPubKeys.set(m.user, m.pub);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (m.type === 'message' && m.to === this.userId && m.payload) {
|
|
24
|
+
const payload = m.payload;
|
|
25
|
+
const sender = m.from;
|
|
26
|
+
try {
|
|
27
|
+
const plain = this.crypto.decrypt(payload, payload.senderPub);
|
|
28
|
+
const text = new TextDecoder().decode(plain);
|
|
29
|
+
for (const h of this.handlers)
|
|
30
|
+
h(sender, text);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// ignore decryption errors
|
|
34
|
+
}
|
|
35
|
+
}
|
|
7
36
|
}
|
|
8
37
|
async internalInitialize() {
|
|
9
|
-
//
|
|
38
|
+
// nothing for now; announce already sent in constructor
|
|
10
39
|
}
|
|
11
40
|
async send(recipientId, content) {
|
|
12
|
-
|
|
41
|
+
const recipientPub = this.knownPubKeys.get(recipientId);
|
|
42
|
+
if (!recipientPub) {
|
|
43
|
+
throw new StvorError(Errors.RECIPIENT_NOT_FOUND, 'Recipient public key unknown');
|
|
44
|
+
}
|
|
45
|
+
const plain = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
46
|
+
const payload = this.crypto.encrypt(plain, recipientPub);
|
|
47
|
+
const msg = { type: 'message', to: recipientId, from: this.userId, payload };
|
|
48
|
+
this.relay.send(msg);
|
|
13
49
|
}
|
|
14
50
|
onMessage(handler) {
|
|
15
|
-
|
|
16
|
-
return () => {
|
|
51
|
+
this.handlers.push(handler);
|
|
52
|
+
return () => {
|
|
53
|
+
const i = this.handlers.indexOf(handler);
|
|
54
|
+
if (i >= 0)
|
|
55
|
+
this.handlers.splice(i, 1);
|
|
56
|
+
};
|
|
17
57
|
}
|
|
18
58
|
}
|
|
19
59
|
export class StvorApp {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export type EncryptedMessage = {
|
|
3
|
+
version: number;
|
|
4
|
+
senderPub: string;
|
|
5
|
+
nonce: string;
|
|
6
|
+
ciphertext: string;
|
|
7
|
+
tag: string;
|
|
8
|
+
};
|
|
9
|
+
export declare class CryptoSession {
|
|
10
|
+
privateKey: crypto.KeyObject;
|
|
11
|
+
publicKey: crypto.KeyObject;
|
|
12
|
+
readonly publicKeyBase64: string;
|
|
13
|
+
constructor();
|
|
14
|
+
exportPublic(): string;
|
|
15
|
+
private deriveShared;
|
|
16
|
+
encrypt(plaintext: Uint8Array, remotePubBase64: string): EncryptedMessage;
|
|
17
|
+
decrypt(msg: EncryptedMessage, remotePubBase64: string): Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
export default CryptoSession;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export class CryptoSession {
|
|
3
|
+
constructor() {
|
|
4
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519');
|
|
5
|
+
this.privateKey = privateKey;
|
|
6
|
+
this.publicKey = publicKey;
|
|
7
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
8
|
+
this.publicKeyBase64 = spki.toString('base64');
|
|
9
|
+
}
|
|
10
|
+
exportPublic() {
|
|
11
|
+
return this.publicKeyBase64;
|
|
12
|
+
}
|
|
13
|
+
deriveShared(remotePubBase64) {
|
|
14
|
+
const remoteDer = Buffer.from(remotePubBase64, 'base64');
|
|
15
|
+
const remoteKey = crypto.createPublicKey({ key: remoteDer, type: 'spki', format: 'der' });
|
|
16
|
+
const shared = crypto.diffieHellman({ privateKey: this.privateKey, publicKey: remoteKey });
|
|
17
|
+
// HKDF-SHA256 to 32 bytes
|
|
18
|
+
const key = crypto.hkdfSync('sha256', shared, Buffer.alloc(0), Buffer.from('stvor v0.1'), 32);
|
|
19
|
+
return Buffer.from(key);
|
|
20
|
+
}
|
|
21
|
+
encrypt(plaintext, remotePubBase64) {
|
|
22
|
+
const key = this.deriveShared(remotePubBase64);
|
|
23
|
+
const nonce = crypto.randomBytes(12);
|
|
24
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
25
|
+
const ct = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
26
|
+
const tag = cipher.getAuthTag();
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
senderPub: this.publicKeyBase64,
|
|
30
|
+
nonce: nonce.toString('base64'),
|
|
31
|
+
ciphertext: ct.toString('base64'),
|
|
32
|
+
tag: tag.toString('base64'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
decrypt(msg, remotePubBase64) {
|
|
36
|
+
const key = this.deriveShared(remotePubBase64);
|
|
37
|
+
const nonce = Buffer.from(msg.nonce, 'base64');
|
|
38
|
+
const ct = Buffer.from(msg.ciphertext, 'base64');
|
|
39
|
+
const tag = Buffer.from(msg.tag, 'base64');
|
|
40
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
41
|
+
decipher.setAuthTag(tag);
|
|
42
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
43
|
+
return new Uint8Array(pt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export default CryptoSession;
|
package/dist/facade/errors.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ export declare const Errors: {
|
|
|
2
2
|
readonly INVALID_APP_TOKEN: "INVALID_APP_TOKEN";
|
|
3
3
|
readonly RELAY_UNAVAILABLE: "RELAY_UNAVAILABLE";
|
|
4
4
|
readonly DELIVERY_FAILED: "DELIVERY_FAILED";
|
|
5
|
+
readonly RECIPIENT_NOT_FOUND: "RECIPIENT_NOT_FOUND";
|
|
6
|
+
readonly MESSAGE_INTEGRITY_FAILED: "MESSAGE_INTEGRITY_FAILED";
|
|
7
|
+
readonly RECEIVE_TIMEOUT: "RECEIVE_TIMEOUT";
|
|
8
|
+
readonly RECEIVE_IN_PROGRESS: "RECEIVE_IN_PROGRESS";
|
|
5
9
|
};
|
|
6
10
|
export type ErrorCode = (typeof Errors)[keyof typeof Errors];
|
|
7
11
|
export declare class StvorError extends Error {
|
package/dist/facade/errors.js
CHANGED
|
@@ -2,6 +2,10 @@ export const Errors = {
|
|
|
2
2
|
INVALID_APP_TOKEN: 'INVALID_APP_TOKEN',
|
|
3
3
|
RELAY_UNAVAILABLE: 'RELAY_UNAVAILABLE',
|
|
4
4
|
DELIVERY_FAILED: 'DELIVERY_FAILED',
|
|
5
|
+
RECIPIENT_NOT_FOUND: 'RECIPIENT_NOT_FOUND',
|
|
6
|
+
MESSAGE_INTEGRITY_FAILED: 'MESSAGE_INTEGRITY_FAILED',
|
|
7
|
+
RECEIVE_TIMEOUT: 'RECEIVE_TIMEOUT',
|
|
8
|
+
RECEIVE_IN_PROGRESS: 'RECEIVE_IN_PROGRESS',
|
|
5
9
|
};
|
|
6
10
|
export class StvorError extends Error {
|
|
7
11
|
constructor(code, message, action, retryable) {
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR DX Facade - Relay Client
|
|
3
3
|
*/
|
|
4
|
+
type JSONable = Record<string, any>;
|
|
5
|
+
export type RelayHandler = (msg: JSONable) => void;
|
|
4
6
|
export declare class RelayClient {
|
|
5
7
|
private relayUrl;
|
|
6
8
|
private timeout;
|
|
7
9
|
private appToken;
|
|
10
|
+
private ws?;
|
|
8
11
|
private connected;
|
|
12
|
+
private backoff;
|
|
13
|
+
private queue;
|
|
14
|
+
private handlers;
|
|
15
|
+
private reconnecting;
|
|
9
16
|
constructor(relayUrl: string, appToken: string, timeout?: number);
|
|
10
17
|
private getAuthHeaders;
|
|
11
|
-
|
|
18
|
+
private connect;
|
|
19
|
+
private scheduleReconnect;
|
|
20
|
+
send(obj: JSONable): void;
|
|
21
|
+
onMessage(h: RelayHandler): void;
|
|
12
22
|
isConnected(): boolean;
|
|
13
23
|
}
|
|
24
|
+
export {};
|
|
@@ -1,36 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR DX Facade - Relay Client
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
import * as WS from 'ws';
|
|
5
5
|
export class RelayClient {
|
|
6
6
|
constructor(relayUrl, appToken, timeout = 10000) {
|
|
7
7
|
this.connected = false;
|
|
8
|
-
this.
|
|
8
|
+
this.backoff = 1000;
|
|
9
|
+
this.queue = [];
|
|
10
|
+
this.handlers = [];
|
|
11
|
+
this.reconnecting = false;
|
|
12
|
+
this.relayUrl = relayUrl.replace(/^http/, 'ws');
|
|
9
13
|
this.appToken = appToken;
|
|
10
14
|
this.timeout = timeout;
|
|
15
|
+
this.connect();
|
|
11
16
|
}
|
|
12
17
|
getAuthHeaders() {
|
|
13
18
|
return {
|
|
14
|
-
|
|
15
|
-
'Content-Type': 'application/json',
|
|
19
|
+
Authorization: `Bearer ${this.appToken}`,
|
|
16
20
|
};
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
connect() {
|
|
23
|
+
if (this.ws)
|
|
24
|
+
return;
|
|
25
|
+
const WSClass = WS.default ?? WS;
|
|
26
|
+
this.ws = new WSClass(this.relayUrl, { headers: this.getAuthHeaders() });
|
|
27
|
+
this.ws.on('open', () => {
|
|
28
|
+
this.connected = true;
|
|
29
|
+
this.backoff = 1000;
|
|
30
|
+
// flush queue
|
|
31
|
+
while (this.queue.length) {
|
|
32
|
+
const m = this.queue.shift();
|
|
33
|
+
this.send(m);
|
|
28
34
|
}
|
|
35
|
+
});
|
|
36
|
+
this.ws.on('message', (data) => {
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.parse(data.toString());
|
|
39
|
+
for (const h of this.handlers)
|
|
40
|
+
h(json);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
this.ws.on('close', () => {
|
|
47
|
+
this.connected = false;
|
|
48
|
+
this.ws = undefined;
|
|
49
|
+
this.scheduleReconnect();
|
|
50
|
+
});
|
|
51
|
+
this.ws.on('error', () => {
|
|
52
|
+
this.connected = false;
|
|
53
|
+
this.ws = undefined;
|
|
54
|
+
this.scheduleReconnect();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
scheduleReconnect() {
|
|
58
|
+
if (this.reconnecting)
|
|
59
|
+
return;
|
|
60
|
+
this.reconnecting = true;
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
this.reconnecting = false;
|
|
63
|
+
this.connect();
|
|
64
|
+
this.backoff = Math.min(this.backoff * 2, 30000);
|
|
65
|
+
}, this.backoff);
|
|
66
|
+
}
|
|
67
|
+
send(obj) {
|
|
68
|
+
const data = JSON.stringify(obj);
|
|
69
|
+
if (this.connected && this.ws) {
|
|
70
|
+
this.ws.send(data);
|
|
29
71
|
}
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
else {
|
|
73
|
+
this.queue.push(obj);
|
|
32
74
|
}
|
|
33
75
|
}
|
|
76
|
+
onMessage(h) {
|
|
77
|
+
this.handlers.push(h);
|
|
78
|
+
}
|
|
34
79
|
isConnected() {
|
|
35
80
|
return this.connected;
|
|
36
81
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stvor/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "STVOR DX Facade - Simple E2EE SDK for client-side encryption",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
14
|
"files": [
|
|
11
15
|
"dist"
|
|
@@ -15,18 +19,33 @@
|
|
|
15
19
|
"encryption",
|
|
16
20
|
"security",
|
|
17
21
|
"cryptography",
|
|
18
|
-
"end-to-end"
|
|
22
|
+
"end-to-end",
|
|
23
|
+
"x3dh",
|
|
24
|
+
"double-ratchet",
|
|
25
|
+
"libsodium"
|
|
19
26
|
],
|
|
20
27
|
"author": "Stvor",
|
|
21
28
|
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/izahii/stvor-api.git"
|
|
32
|
+
},
|
|
22
33
|
"engines": {
|
|
23
34
|
"node": ">=18.0.0"
|
|
24
35
|
},
|
|
25
36
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/stvor/
|
|
37
|
+
"url": "https://github.com/izahii/stvor-api/issues"
|
|
27
38
|
},
|
|
28
39
|
"homepage": "https://stvor.xyz",
|
|
29
40
|
"scripts": {
|
|
30
|
-
"build": "tsc"
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"prepublishOnly": "npm run build",
|
|
43
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ws": "^8.13.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"libsodium-wrappers": "^0.7.13"
|
|
31
50
|
}
|
|
32
51
|
}
|