@stvor/sdk 2.0.4 → 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 +18 -37
- package/dist/facade/app.js +65 -224
- package/dist/facade/crypto.d.ts +19 -0
- package/dist/facade/crypto.js +46 -0
- package/dist/facade/errors.d.ts +7 -29
- package/dist/facade/errors.js +2 -49
- package/dist/facade/index.d.ts +8 -17
- package/dist/facade/index.js +5 -15
- package/dist/facade/relay-client.d.ts +11 -19
- package/dist/facade/relay-client.js +59 -105
- package/dist/facade/types.d.ts +0 -2
- package/dist/index.d.ts +2 -5
- package/dist/index.js +0 -3
- package/dist/legacy.d.ts +1 -31
- package/dist/legacy.js +2 -90
- package/package.json +24 -5
- package/dist/example.d.ts +0 -9
- package/dist/example.js +0 -57
package/dist/facade/app.d.ts
CHANGED
|
@@ -1,46 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { StvorAppConfig, UserId, MessageContent } from './types';
|
|
5
|
-
import { DecryptedMessage, SealedPayload } from './types';
|
|
6
|
-
import { Errors, StvorError, ErrorCode } from './errors';
|
|
7
|
-
export type { DecryptedMessage, SealedPayload, ErrorCode };
|
|
8
|
-
export { StvorError, Errors };
|
|
9
|
-
import { RelayClient } from './relay-client';
|
|
10
|
-
export declare class StvorApp {
|
|
11
|
-
private relay;
|
|
12
|
-
private config;
|
|
13
|
-
private connectedClients;
|
|
14
|
-
constructor(config: Required<StvorAppConfig>);
|
|
15
|
-
isReady(): boolean;
|
|
16
|
-
connect(userId: UserId): Promise<StvorFacadeClient>;
|
|
17
|
-
disconnect(userId?: UserId): Promise<void>;
|
|
18
|
-
private initClient;
|
|
19
|
-
}
|
|
1
|
+
import type { StvorAppConfig, UserId, MessageContent } from './types.js';
|
|
2
|
+
import { RelayClient } from './relay-client.js';
|
|
3
|
+
type MessageHandler = (from: UserId, msg: string | Uint8Array) => void;
|
|
20
4
|
export declare class StvorFacadeClient {
|
|
21
|
-
|
|
22
|
-
private relay;
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private messageHandlers;
|
|
27
|
-
private isReceiving;
|
|
5
|
+
readonly userId: UserId;
|
|
6
|
+
private readonly relay;
|
|
7
|
+
private crypto;
|
|
8
|
+
private handlers;
|
|
9
|
+
private knownPubKeys;
|
|
28
10
|
constructor(userId: UserId, relay: RelayClient);
|
|
11
|
+
private handleRelayMessage;
|
|
29
12
|
internalInitialize(): Promise<void>;
|
|
30
|
-
private initialize;
|
|
31
13
|
send(recipientId: UserId, content: MessageContent): Promise<void>;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
private decryptMessage;
|
|
41
|
-
private startMessagePolling;
|
|
14
|
+
onMessage(handler: MessageHandler): () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class StvorApp {
|
|
17
|
+
private readonly config;
|
|
18
|
+
private clients;
|
|
19
|
+
constructor(config: StvorAppConfig);
|
|
20
|
+
connect(userId: UserId): Promise<StvorFacadeClient>;
|
|
21
|
+
disconnect(userId?: UserId): Promise<void>;
|
|
42
22
|
}
|
|
43
23
|
export declare function init(config: StvorAppConfig): Promise<StvorApp>;
|
|
44
24
|
export declare const Stvor: {
|
|
45
25
|
init: typeof init;
|
|
46
26
|
};
|
|
27
|
+
export {};
|
package/dist/facade/app.js
CHANGED
|
@@ -1,247 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { Errors, StvorError } from './errors';
|
|
5
|
-
export { StvorError, Errors };
|
|
6
|
-
import { RelayClient } from './relay-client';
|
|
7
|
-
export class StvorApp {
|
|
8
|
-
constructor(config) {
|
|
9
|
-
this.connectedClients = new Map();
|
|
10
|
-
this.config = config;
|
|
11
|
-
this.relay = new RelayClient(config.relayUrl, config.appToken, config.timeout);
|
|
12
|
-
}
|
|
13
|
-
isReady() {
|
|
14
|
-
return this.relay.isConnected();
|
|
15
|
-
}
|
|
16
|
-
async connect(userId) {
|
|
17
|
-
const existingClient = this.connectedClients.get(userId);
|
|
18
|
-
if (existingClient) {
|
|
19
|
-
console.warn(`[STVOR] Warning: User "${userId}" is already connected. Returning cached client.`);
|
|
20
|
-
return existingClient;
|
|
21
|
-
}
|
|
22
|
-
const client = new StvorFacadeClient(userId, this.relay);
|
|
23
|
-
await this.initClient(client);
|
|
24
|
-
this.connectedClients.set(userId, client);
|
|
25
|
-
return client;
|
|
26
|
-
}
|
|
27
|
-
async disconnect(userId) {
|
|
28
|
-
if (userId) {
|
|
29
|
-
const client = this.connectedClients.get(userId);
|
|
30
|
-
if (client) {
|
|
31
|
-
await client.disconnect();
|
|
32
|
-
this.connectedClients.delete(userId);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
for (const client of this.connectedClients.values()) {
|
|
37
|
-
await client.disconnect();
|
|
38
|
-
}
|
|
39
|
-
this.connectedClients.clear();
|
|
40
|
-
this.relay.disconnect();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async initClient(client) {
|
|
44
|
-
await client.internalInitialize();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
import { Errors, StvorError } from './errors.js';
|
|
2
|
+
import { RelayClient } from './relay-client.js';
|
|
3
|
+
import { CryptoSession } from './crypto.js';
|
|
47
4
|
export class StvorFacadeClient {
|
|
48
5
|
constructor(userId, relay) {
|
|
49
|
-
this.initialized = false;
|
|
50
|
-
this.sessionKeyPair = null;
|
|
51
|
-
this.messageQueue = [];
|
|
52
|
-
this.messageHandlers = new Map();
|
|
53
|
-
this.isReceiving = false;
|
|
54
6
|
this.userId = userId;
|
|
55
7
|
this.relay = relay;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
62
21
|
return;
|
|
63
|
-
this.sessionKeyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'X25519' }, true, ['deriveKey', 'deriveBits']);
|
|
64
|
-
const publicKeyJwk = await crypto.subtle.exportKey('jwk', this.sessionKeyPair.publicKey);
|
|
65
|
-
await this.relay.register(this.userId, publicKeyJwk);
|
|
66
|
-
this.initialized = true;
|
|
67
|
-
this.startMessagePolling();
|
|
68
|
-
}
|
|
69
|
-
async send(recipientId, content) {
|
|
70
|
-
if (!this.initialized) {
|
|
71
|
-
throw Errors.clientNotReady();
|
|
72
|
-
}
|
|
73
|
-
const contentBytes = typeof content === 'string'
|
|
74
|
-
? new TextEncoder().encode(content)
|
|
75
|
-
: content;
|
|
76
|
-
const recipientKey = await this.relay.getPublicKey(recipientId);
|
|
77
|
-
if (!recipientKey) {
|
|
78
|
-
throw Errors.recipientNotFound(recipientId);
|
|
79
|
-
}
|
|
80
|
-
const sharedKey = await this.deriveSharedKey(recipientKey);
|
|
81
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
82
|
-
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, contentBytes.buffer);
|
|
83
|
-
await this.relay.send({
|
|
84
|
-
to: recipientId,
|
|
85
|
-
from: this.userId,
|
|
86
|
-
ciphertext: new Uint8Array(encrypted),
|
|
87
|
-
nonce: iv,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
async receive() {
|
|
91
|
-
if (!this.initialized) {
|
|
92
|
-
throw Errors.clientNotReady();
|
|
93
|
-
}
|
|
94
|
-
if (this.isReceiving) {
|
|
95
|
-
throw Errors.receiveInProgress();
|
|
96
|
-
}
|
|
97
|
-
if (this.messageQueue.length > 0) {
|
|
98
|
-
return this.messageQueue.shift();
|
|
99
|
-
}
|
|
100
|
-
this.isReceiving = true;
|
|
101
|
-
try {
|
|
102
|
-
return await this.fetchAndDecryptMessageWithTimeout(30000);
|
|
103
22
|
}
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
35
|
}
|
|
107
36
|
}
|
|
108
|
-
async
|
|
109
|
-
|
|
110
|
-
throw Errors.clientNotReady();
|
|
111
|
-
}
|
|
112
|
-
const contentBytes = typeof data === 'string'
|
|
113
|
-
? new TextEncoder().encode(data)
|
|
114
|
-
: data;
|
|
115
|
-
const recipientKey = await this.relay.getPublicKey(recipientId);
|
|
116
|
-
if (!recipientKey) {
|
|
117
|
-
throw Errors.recipientNotFound(recipientId);
|
|
118
|
-
}
|
|
119
|
-
const sharedKey = await this.deriveSharedKey(recipientKey);
|
|
120
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
121
|
-
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, contentBytes.buffer);
|
|
122
|
-
return {
|
|
123
|
-
ciphertext: new Uint8Array(encrypted),
|
|
124
|
-
nonce: iv,
|
|
125
|
-
recipientId,
|
|
126
|
-
};
|
|
37
|
+
async internalInitialize() {
|
|
38
|
+
// nothing for now; announce already sent in constructor
|
|
127
39
|
}
|
|
128
|
-
async
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const senderKey = await this.relay.getPublicKey(sealed.recipientId);
|
|
133
|
-
if (!senderKey) {
|
|
134
|
-
throw Errors.recipientNotFound(sealed.recipientId);
|
|
40
|
+
async send(recipientId, content) {
|
|
41
|
+
const recipientPub = this.knownPubKeys.get(recipientId);
|
|
42
|
+
if (!recipientPub) {
|
|
43
|
+
throw new StvorError(Errors.RECIPIENT_NOT_FOUND, 'Recipient public key unknown');
|
|
135
44
|
}
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
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);
|
|
139
49
|
}
|
|
140
50
|
onMessage(handler) {
|
|
141
|
-
|
|
142
|
-
this.messageHandlers.set(id, handler);
|
|
51
|
+
this.handlers.push(handler);
|
|
143
52
|
return () => {
|
|
144
|
-
this.
|
|
53
|
+
const i = this.handlers.indexOf(handler);
|
|
54
|
+
if (i >= 0)
|
|
55
|
+
this.handlers.splice(i, 1);
|
|
145
56
|
};
|
|
146
57
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
this.initialized = false;
|
|
153
|
-
this.sessionKeyPair = null;
|
|
154
|
-
this.messageQueue = [];
|
|
155
|
-
this.isReceiving = false;
|
|
156
|
-
}
|
|
157
|
-
async deriveSharedKey(peerPublicKey) {
|
|
158
|
-
return await crypto.subtle.deriveKey({ name: 'ECDH', public: peerPublicKey }, this.sessionKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
58
|
+
}
|
|
59
|
+
export class StvorApp {
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.config = config;
|
|
62
|
+
this.clients = new Map();
|
|
159
63
|
}
|
|
160
|
-
async
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Silent error on poll
|
|
172
|
-
}
|
|
173
|
-
waited += pollInterval;
|
|
174
|
-
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
175
|
-
}
|
|
176
|
-
throw Errors.receiveTimeout();
|
|
64
|
+
async connect(userId) {
|
|
65
|
+
const existing = this.clients.get(userId);
|
|
66
|
+
if (existing)
|
|
67
|
+
return existing;
|
|
68
|
+
const relay = new RelayClient(this.config.relayUrl ?? 'wss://stvor.xyz/relay', this.config.appToken, this.config.timeout ?? 10000);
|
|
69
|
+
const client = new StvorFacadeClient(userId, relay);
|
|
70
|
+
await client.internalInitialize();
|
|
71
|
+
this.clients.set(userId, client);
|
|
72
|
+
return client;
|
|
177
73
|
}
|
|
178
|
-
async
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
const sharedKey = await this.deriveSharedKey(senderKey);
|
|
184
|
-
try {
|
|
185
|
-
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: new Uint8Array(msg.nonce) }, sharedKey, new Uint8Array(msg.ciphertext).buffer);
|
|
186
|
-
return {
|
|
187
|
-
id: msg.id || crypto.randomUUID(),
|
|
188
|
-
senderId: msg.from,
|
|
189
|
-
content: new TextDecoder().decode(decrypted),
|
|
190
|
-
timestamp: new Date(msg.timestamp),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
throw Errors.messageIntegrityFailed();
|
|
74
|
+
async disconnect(userId) {
|
|
75
|
+
if (userId) {
|
|
76
|
+
this.clients.delete(userId);
|
|
77
|
+
return;
|
|
195
78
|
}
|
|
196
|
-
|
|
197
|
-
startMessagePolling() {
|
|
198
|
-
const poll = async () => {
|
|
199
|
-
try {
|
|
200
|
-
const messages = await this.relay.fetchMessages(this.userId);
|
|
201
|
-
if (messages.length > 0) {
|
|
202
|
-
const msg = await this.decryptMessage(messages[0]);
|
|
203
|
-
this.messageQueue.push(msg);
|
|
204
|
-
for (const handler of this.messageHandlers.values()) {
|
|
205
|
-
try {
|
|
206
|
-
handler(msg);
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
// Handler error does not break other handlers
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
// Silent error on poll
|
|
216
|
-
}
|
|
217
|
-
if (this.initialized) {
|
|
218
|
-
setTimeout(poll, 1000);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
poll();
|
|
79
|
+
this.clients.clear();
|
|
222
80
|
}
|
|
223
81
|
}
|
|
224
82
|
export async function init(config) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!config.appToken || !config.appToken.startsWith('stvor_')) {
|
|
228
|
-
throw Errors.invalidAppToken();
|
|
229
|
-
}
|
|
230
|
-
const appConfig = {
|
|
231
|
-
appToken: config.appToken,
|
|
232
|
-
relayUrl,
|
|
233
|
-
timeout,
|
|
234
|
-
};
|
|
235
|
-
const app = new StvorApp(appConfig);
|
|
236
|
-
try {
|
|
237
|
-
const relay = new RelayClient(relayUrl, config.appToken, timeout);
|
|
238
|
-
await relay.healthCheck();
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
throw Errors.relayUnavailable();
|
|
83
|
+
if (!config.appToken.startsWith('stvor_')) {
|
|
84
|
+
throw new StvorError(Errors.INVALID_APP_TOKEN, 'Invalid app token');
|
|
242
85
|
}
|
|
243
|
-
return
|
|
86
|
+
return new StvorApp(config);
|
|
244
87
|
}
|
|
245
|
-
export const Stvor = {
|
|
246
|
-
init,
|
|
247
|
-
};
|
|
88
|
+
export const Stvor = { init };
|
|
@@ -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
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
* STVOR DX Facade - Error Handling
|
|
3
|
-
*/
|
|
4
|
-
export declare const ErrorCode: {
|
|
5
|
-
readonly AUTH_FAILED: "AUTH_FAILED";
|
|
1
|
+
export declare const Errors: {
|
|
6
2
|
readonly INVALID_APP_TOKEN: "INVALID_APP_TOKEN";
|
|
7
3
|
readonly RELAY_UNAVAILABLE: "RELAY_UNAVAILABLE";
|
|
4
|
+
readonly DELIVERY_FAILED: "DELIVERY_FAILED";
|
|
8
5
|
readonly RECIPIENT_NOT_FOUND: "RECIPIENT_NOT_FOUND";
|
|
9
6
|
readonly MESSAGE_INTEGRITY_FAILED: "MESSAGE_INTEGRITY_FAILED";
|
|
10
|
-
readonly KEYSTORE_CORRUPTED: "KEYSTORE_CORRUPTED";
|
|
11
|
-
readonly DEVICE_COMPROMISED: "DEVICE_COMPROMISED";
|
|
12
|
-
readonly PROTOCOL_VERSION_MISMATCH: "PROTOCOL_VERSION_MISMATCH";
|
|
13
|
-
readonly CLIENT_NOT_READY: "CLIENT_NOT_READY";
|
|
14
|
-
readonly DELIVERY_FAILED: "DELIVERY_FAILED";
|
|
15
7
|
readonly RECEIVE_TIMEOUT: "RECEIVE_TIMEOUT";
|
|
16
8
|
readonly RECEIVE_IN_PROGRESS: "RECEIVE_IN_PROGRESS";
|
|
17
9
|
};
|
|
18
|
-
export type ErrorCode = typeof
|
|
10
|
+
export type ErrorCode = (typeof Errors)[keyof typeof Errors];
|
|
19
11
|
export declare class StvorError extends Error {
|
|
20
|
-
code:
|
|
21
|
-
action?: string;
|
|
22
|
-
retryable?: boolean;
|
|
23
|
-
constructor(code:
|
|
12
|
+
code: ErrorCode;
|
|
13
|
+
action?: string | undefined;
|
|
14
|
+
retryable?: boolean | undefined;
|
|
15
|
+
constructor(code: ErrorCode, message: string, action?: string | undefined, retryable?: boolean | undefined);
|
|
24
16
|
}
|
|
25
|
-
export declare const Errors: {
|
|
26
|
-
authFailed(): StvorError;
|
|
27
|
-
invalidAppToken(): StvorError;
|
|
28
|
-
relayUnavailable(): StvorError;
|
|
29
|
-
recipientNotFound(userId: string): StvorError;
|
|
30
|
-
messageIntegrityFailed(): StvorError;
|
|
31
|
-
keystoreCorrupted(): StvorError;
|
|
32
|
-
deviceCompromised(): StvorError;
|
|
33
|
-
protocolMismatch(): StvorError;
|
|
34
|
-
clientNotReady(): StvorError;
|
|
35
|
-
deliveryFailed(recipientId: string): StvorError;
|
|
36
|
-
receiveTimeout(): StvorError;
|
|
37
|
-
receiveInProgress(): StvorError;
|
|
38
|
-
};
|
package/dist/facade/errors.js
CHANGED
|
@@ -1,64 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
* STVOR DX Facade - Error Handling
|
|
3
|
-
*/
|
|
4
|
-
export const ErrorCode = {
|
|
5
|
-
AUTH_FAILED: 'AUTH_FAILED',
|
|
1
|
+
export const Errors = {
|
|
6
2
|
INVALID_APP_TOKEN: 'INVALID_APP_TOKEN',
|
|
7
3
|
RELAY_UNAVAILABLE: 'RELAY_UNAVAILABLE',
|
|
4
|
+
DELIVERY_FAILED: 'DELIVERY_FAILED',
|
|
8
5
|
RECIPIENT_NOT_FOUND: 'RECIPIENT_NOT_FOUND',
|
|
9
6
|
MESSAGE_INTEGRITY_FAILED: 'MESSAGE_INTEGRITY_FAILED',
|
|
10
|
-
KEYSTORE_CORRUPTED: 'KEYSTORE_CORRUPTED',
|
|
11
|
-
DEVICE_COMPROMISED: 'DEVICE_COMPROMISED',
|
|
12
|
-
PROTOCOL_VERSION_MISMATCH: 'PROTOCOL_VERSION_MISMATCH',
|
|
13
|
-
CLIENT_NOT_READY: 'CLIENT_NOT_READY',
|
|
14
|
-
DELIVERY_FAILED: 'DELIVERY_FAILED',
|
|
15
7
|
RECEIVE_TIMEOUT: 'RECEIVE_TIMEOUT',
|
|
16
8
|
RECEIVE_IN_PROGRESS: 'RECEIVE_IN_PROGRESS',
|
|
17
9
|
};
|
|
18
10
|
export class StvorError extends Error {
|
|
19
11
|
constructor(code, message, action, retryable) {
|
|
20
12
|
super(message);
|
|
21
|
-
this.name = 'StvorError';
|
|
22
13
|
this.code = code;
|
|
23
14
|
this.action = action;
|
|
24
15
|
this.retryable = retryable;
|
|
25
16
|
}
|
|
26
17
|
}
|
|
27
|
-
export const Errors = {
|
|
28
|
-
authFailed() {
|
|
29
|
-
return new StvorError(ErrorCode.AUTH_FAILED, 'The AppToken is invalid or has been revoked.', 'Check your dashboard and regenerate a new AppToken.', false);
|
|
30
|
-
},
|
|
31
|
-
invalidAppToken() {
|
|
32
|
-
return new StvorError(ErrorCode.INVALID_APP_TOKEN, 'Invalid AppToken format. AppToken must start with "stvor_".', 'Get your AppToken from the developer dashboard.', false);
|
|
33
|
-
},
|
|
34
|
-
relayUnavailable() {
|
|
35
|
-
return new StvorError(ErrorCode.RELAY_UNAVAILABLE, 'Cannot connect to STVOR relay server.', 'Check your internet connection.', true);
|
|
36
|
-
},
|
|
37
|
-
recipientNotFound(userId) {
|
|
38
|
-
return new StvorError(ErrorCode.RECIPIENT_NOT_FOUND, `User "${userId}" not found. They may not have registered with STVOR.`, 'Ask the recipient to initialize STVOR first, or verify the userId is correct.', false);
|
|
39
|
-
},
|
|
40
|
-
messageIntegrityFailed() {
|
|
41
|
-
return new StvorError(ErrorCode.MESSAGE_INTEGRITY_FAILED, 'Message integrity check failed. The message may be corrupted or tampered with.', 'Request the message again from the sender.', false);
|
|
42
|
-
},
|
|
43
|
-
keystoreCorrupted() {
|
|
44
|
-
return new StvorError(ErrorCode.KEYSTORE_CORRUPTED, 'Local keystore is corrupted. All local keys have been wiped.', 'The user will need to re-register this device.', false);
|
|
45
|
-
},
|
|
46
|
-
deviceCompromised() {
|
|
47
|
-
return new StvorError(ErrorCode.DEVICE_COMPROMISED, 'Security violation detected. This device has been flagged.', 'Immediately log out the user and investigate.', false);
|
|
48
|
-
},
|
|
49
|
-
protocolMismatch() {
|
|
50
|
-
return new StvorError(ErrorCode.PROTOCOL_VERSION_MISMATCH, 'This app version uses an older protocol version.', 'Update the SDK to the latest version.', false);
|
|
51
|
-
},
|
|
52
|
-
clientNotReady() {
|
|
53
|
-
return new StvorError(ErrorCode.CLIENT_NOT_READY, 'Client is not ready. Call connect() first and await it.', 'Make sure to await app.connect() before sending messages.', false);
|
|
54
|
-
},
|
|
55
|
-
deliveryFailed(recipientId) {
|
|
56
|
-
return new StvorError(ErrorCode.DELIVERY_FAILED, `Failed to deliver message to ${recipientId}.`, 'Check that the recipient exists and try again.', true);
|
|
57
|
-
},
|
|
58
|
-
receiveTimeout() {
|
|
59
|
-
return new StvorError(ErrorCode.RECEIVE_TIMEOUT, 'No messages received within 30 second timeout.', 'Use onMessage() subscription for real-time updates.', true);
|
|
60
|
-
},
|
|
61
|
-
receiveInProgress() {
|
|
62
|
-
return new StvorError(ErrorCode.RECEIVE_IN_PROGRESS, 'A receive() call is already in progress.', 'Wait for the current receive() to complete before calling again.', true);
|
|
63
|
-
},
|
|
64
|
-
};
|
package/dist/facade/index.d.ts
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* - Opinionated (no configuration)
|
|
10
|
-
*/
|
|
11
|
-
export type { DecryptedMessage, SealedPayload } from './app';
|
|
12
|
-
export type { StvorAppConfig, AppToken, UserId, MessageContent } from './types';
|
|
13
|
-
export type { ErrorCode } from './errors';
|
|
14
|
-
export { StvorError } from './errors';
|
|
15
|
-
export { StvorApp, StvorFacadeClient } from './app';
|
|
16
|
-
export { Stvor, init } from './app';
|
|
17
|
-
export { ErrorCode as STVOR_ERRORS } from './errors';
|
|
1
|
+
export * from './app.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export type { DecryptedMessage, SealedPayload } from './types.js';
|
|
5
|
+
export type { StvorAppConfig, AppToken, UserId, MessageContent } from './types.js';
|
|
6
|
+
export { StvorError } from './errors.js';
|
|
7
|
+
export { StvorApp, StvorFacadeClient, Stvor, init } from './app.js';
|
|
8
|
+
export { ErrorCode as STVOR_ERRORS } from './errors.js';
|
package/dist/facade/index.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* - Minimal API surface
|
|
7
|
-
* - Zero crypto knowledge required
|
|
8
|
-
* - Secure by default
|
|
9
|
-
* - Opinionated (no configuration)
|
|
10
|
-
*/
|
|
11
|
-
export { StvorError } from './errors';
|
|
12
|
-
// Re-export classes and functions
|
|
13
|
-
export { StvorApp, StvorFacadeClient } from './app';
|
|
14
|
-
export { Stvor, init } from './app';
|
|
15
|
-
export { ErrorCode as STVOR_ERRORS } from './errors';
|
|
1
|
+
export * from './app.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export { StvorError } from './errors.js';
|
|
5
|
+
export { StvorApp, StvorFacadeClient, Stvor, init } from './app.js';
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* STVOR DX Facade - Relay Client
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from: string;
|
|
7
|
-
ciphertext: Uint8Array;
|
|
8
|
-
nonce: Uint8Array;
|
|
9
|
-
}
|
|
10
|
-
interface IncomingMessage {
|
|
11
|
-
id?: string;
|
|
12
|
-
from: string;
|
|
13
|
-
ciphertext: number[];
|
|
14
|
-
nonce: number[];
|
|
15
|
-
timestamp: string;
|
|
16
|
-
}
|
|
4
|
+
type JSONable = Record<string, any>;
|
|
5
|
+
export type RelayHandler = (msg: JSONable) => void;
|
|
17
6
|
export declare class RelayClient {
|
|
18
7
|
private relayUrl;
|
|
19
8
|
private timeout;
|
|
20
9
|
private appToken;
|
|
10
|
+
private ws?;
|
|
21
11
|
private connected;
|
|
12
|
+
private backoff;
|
|
13
|
+
private queue;
|
|
14
|
+
private handlers;
|
|
15
|
+
private reconnecting;
|
|
22
16
|
constructor(relayUrl: string, appToken: string, timeout?: number);
|
|
23
17
|
private getAuthHeaders;
|
|
24
|
-
|
|
18
|
+
private connect;
|
|
19
|
+
private scheduleReconnect;
|
|
20
|
+
send(obj: JSONable): void;
|
|
21
|
+
onMessage(h: RelayHandler): void;
|
|
25
22
|
isConnected(): boolean;
|
|
26
|
-
register(userId: string, publicKey: JsonWebKey): Promise<void>;
|
|
27
|
-
getPublicKey(userId: string): Promise<CryptoKey | null>;
|
|
28
|
-
send(message: OutgoingMessage): Promise<void>;
|
|
29
|
-
fetchMessages(userId: string): Promise<IncomingMessage[]>;
|
|
30
|
-
disconnect(): void;
|
|
31
23
|
}
|
|
32
24
|
export {};
|
|
@@ -1,128 +1,82 @@
|
|
|
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
|
-
signal: controller.signal,
|
|
25
|
-
});
|
|
26
|
-
if (!res.ok) {
|
|
27
|
-
throw Errors.relayUnavailable();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
clearTimeout(timeoutId);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
isConnected() {
|
|
35
|
-
return this.connected;
|
|
36
|
-
}
|
|
37
|
-
async register(userId, publicKey) {
|
|
38
|
-
const controller = new AbortController();
|
|
39
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(`${this.relayUrl}/register`, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: this.getAuthHeaders(),
|
|
44
|
-
body: JSON.stringify({ user_id: userId, publicKey }),
|
|
45
|
-
signal: controller.signal,
|
|
46
|
-
});
|
|
47
|
-
if (!res.ok) {
|
|
48
|
-
const error = await res.json().catch(() => ({}));
|
|
49
|
-
if (error.code === 'AUTH_FAILED') {
|
|
50
|
-
throw Errors.authFailed();
|
|
51
|
-
}
|
|
52
|
-
throw Errors.relayUnavailable();
|
|
53
|
-
}
|
|
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', () => {
|
|
54
28
|
this.connected = true;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
async getPublicKey(userId) {
|
|
61
|
-
const controller = new AbortController();
|
|
62
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
63
|
-
try {
|
|
64
|
-
const res = await fetch(`${this.relayUrl}/public-key/${userId}`, {
|
|
65
|
-
method: 'GET',
|
|
66
|
-
headers: this.getAuthHeaders(),
|
|
67
|
-
signal: controller.signal,
|
|
68
|
-
});
|
|
69
|
-
if (res.status === 404) {
|
|
70
|
-
return null;
|
|
29
|
+
this.backoff = 1000;
|
|
30
|
+
// flush queue
|
|
31
|
+
while (this.queue.length) {
|
|
32
|
+
const m = this.queue.shift();
|
|
33
|
+
this.send(m);
|
|
71
34
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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);
|
|
74
41
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return await crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: 'X25519' }, false, []);
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
clearTimeout(timeoutId);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async send(message) {
|
|
84
|
-
const controller = new AbortController();
|
|
85
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
86
|
-
try {
|
|
87
|
-
const res = await fetch(`${this.relayUrl}/message`, {
|
|
88
|
-
method: 'POST',
|
|
89
|
-
headers: this.getAuthHeaders(),
|
|
90
|
-
body: JSON.stringify({
|
|
91
|
-
to: message.to,
|
|
92
|
-
from: message.from,
|
|
93
|
-
ciphertext: Array.from(message.ciphertext),
|
|
94
|
-
nonce: Array.from(message.nonce),
|
|
95
|
-
}),
|
|
96
|
-
signal: controller.signal,
|
|
97
|
-
});
|
|
98
|
-
if (!res.ok) {
|
|
99
|
-
throw Errors.deliveryFailed(message.to);
|
|
42
|
+
catch (e) {
|
|
43
|
+
// ignore
|
|
100
44
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
});
|
|
105
56
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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);
|
|
120
71
|
}
|
|
121
|
-
|
|
122
|
-
|
|
72
|
+
else {
|
|
73
|
+
this.queue.push(obj);
|
|
123
74
|
}
|
|
124
75
|
}
|
|
125
|
-
|
|
126
|
-
this.
|
|
76
|
+
onMessage(h) {
|
|
77
|
+
this.handlers.push(h);
|
|
78
|
+
}
|
|
79
|
+
isConnected() {
|
|
80
|
+
return this.connected;
|
|
127
81
|
}
|
|
128
82
|
}
|
package/dist/facade/types.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/legacy.d.ts
CHANGED
|
@@ -1,31 +1 @@
|
|
|
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
|
-
}
|
|
1
|
+
export {};
|
package/dist/legacy.js
CHANGED
|
@@ -1,90 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
export {};
|
|
2
|
+
// ...existing code from packages/sdk/legacy.ts...
|
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
|
}
|
package/dist/example.d.ts
DELETED
package/dist/example.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* STVOR DX Facade - Quick Start Example
|
|
3
|
-
*
|
|
4
|
-
* Copy-paste ready example for getting started.
|
|
5
|
-
*
|
|
6
|
-
* IMPORTANT: This is a DX facade. The actual security guarantees
|
|
7
|
-
* depend on the STVOR core implementation.
|
|
8
|
-
*/
|
|
9
|
-
import { Stvor, StvorError } from './index.js';
|
|
10
|
-
/**
|
|
11
|
-
* Basic messaging example with proper error handling
|
|
12
|
-
*/
|
|
13
|
-
async function main() {
|
|
14
|
-
try {
|
|
15
|
-
// 1. Initialize SDK with AppToken from environment
|
|
16
|
-
const app = await Stvor.init({
|
|
17
|
-
appToken: process.env.STVOR_APP_TOKEN || 'stvor_demo_abc123...'
|
|
18
|
-
});
|
|
19
|
-
console.log('SDK initialized');
|
|
20
|
-
// 2. Connect as a user
|
|
21
|
-
const alice = await app.connect('alice@example.com');
|
|
22
|
-
console.log(`Connected as ${alice.getUserId()}`);
|
|
23
|
-
// 3. Subscribe to incoming messages (recommended for production)
|
|
24
|
-
const unsubscribe = alice.onMessage((msg) => {
|
|
25
|
-
console.log(`[Push] ${msg.senderId}: ${msg.content}`);
|
|
26
|
-
});
|
|
27
|
-
// 4. Send encrypted message
|
|
28
|
-
await alice.send('bob@example.com', 'Hello Bob!');
|
|
29
|
-
// 5. Cleanup
|
|
30
|
-
await app.disconnect();
|
|
31
|
-
unsubscribe();
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (error instanceof StvorError) {
|
|
35
|
-
console.error(`[${error.code}] ${error.message}`);
|
|
36
|
-
console.error(`Action: ${error.action}`);
|
|
37
|
-
console.error(`Retryable: ${error.retryable}`);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
console.error('Unknown error:', error);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Note on security guarantees:
|
|
46
|
-
*
|
|
47
|
-
* The facade provides a convenient API, but actual security
|
|
48
|
-
* (E2EE, PFS, post-quantum resistance) depends on the STVOR core.
|
|
49
|
-
*
|
|
50
|
-
* For production use, verify:
|
|
51
|
-
* 1. Core implements Double Ratchet for PFS
|
|
52
|
-
* 2. Core implements ML-KEM for post-quantum resistance
|
|
53
|
-
* 3. Keys are stored securely (not in plain memory)
|
|
54
|
-
* 4. Relay is trusted or verified
|
|
55
|
-
*/
|
|
56
|
-
// Run example
|
|
57
|
-
main();
|