@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.
- package/dist/facade/app.cjs +29 -0
- package/dist/facade/app.d.ts +83 -76
- package/dist/facade/app.js +330 -195
- package/dist/facade/crypto-session.cjs +29 -0
- package/dist/facade/crypto-session.d.ts +49 -54
- package/dist/facade/crypto-session.js +117 -140
- package/dist/facade/errors.cjs +29 -0
- package/dist/facade/errors.d.ts +29 -12
- package/dist/facade/errors.js +49 -8
- package/dist/facade/index.cjs +29 -0
- package/dist/facade/index.d.ts +27 -8
- package/dist/facade/index.js +23 -3
- package/dist/facade/local-storage-identity-store.cjs +29 -0
- package/dist/facade/local-storage-identity-store.d.ts +50 -0
- package/dist/facade/local-storage-identity-store.js +100 -0
- package/dist/facade/metrics-attestation.cjs +29 -0
- package/dist/facade/metrics-attestation.d.ts +209 -0
- package/dist/facade/metrics-attestation.js +333 -0
- package/dist/facade/metrics-engine.cjs +29 -0
- package/dist/facade/metrics-engine.d.ts +91 -0
- package/dist/facade/metrics-engine.js +170 -0
- package/dist/facade/redis-replay-cache.cjs +29 -0
- package/dist/facade/redis-replay-cache.d.ts +88 -0
- package/dist/facade/redis-replay-cache.js +60 -0
- package/dist/facade/relay-client.cjs +29 -0
- package/dist/facade/relay-client.d.ts +22 -23
- package/dist/facade/relay-client.js +107 -128
- package/dist/facade/replay-manager.cjs +29 -0
- package/dist/facade/replay-manager.d.ts +28 -35
- package/dist/facade/replay-manager.js +102 -69
- package/dist/facade/sodium-singleton.cjs +29 -0
- package/dist/facade/tofu-manager.cjs +29 -0
- package/dist/facade/tofu-manager.d.ts +38 -36
- package/dist/facade/tofu-manager.js +109 -77
- package/dist/facade/types.cjs +29 -0
- package/dist/facade/types.d.ts +2 -0
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/legacy.cjs +29 -0
- package/dist/legacy.d.ts +31 -1
- package/dist/legacy.js +90 -2
- package/dist/ratchet/core-production.cjs +29 -0
- package/dist/ratchet/core-production.d.ts +95 -0
- package/dist/ratchet/core-production.js +286 -0
- package/dist/ratchet/index.cjs +29 -0
- package/dist/ratchet/index.d.ts +49 -78
- package/dist/ratchet/index.js +313 -288
- package/dist/ratchet/key-recovery.cjs +29 -0
- package/dist/ratchet/replay-protection.cjs +29 -0
- package/dist/ratchet/tofu.cjs +29 -0
- package/dist/src/facade/app.cjs +29 -0
- package/dist/src/facade/app.d.ts +105 -0
- package/dist/src/facade/app.js +245 -0
- package/dist/src/facade/crypto.cjs +29 -0
- package/dist/src/facade/errors.cjs +29 -0
- package/dist/src/facade/errors.d.ts +19 -0
- package/dist/src/facade/errors.js +21 -0
- package/dist/src/facade/index.cjs +29 -0
- package/dist/src/facade/index.d.ts +8 -0
- package/dist/src/facade/index.js +5 -0
- package/dist/src/facade/relay-client.cjs +29 -0
- package/dist/src/facade/relay-client.d.ts +36 -0
- package/dist/src/facade/relay-client.js +154 -0
- package/dist/src/facade/types.cjs +29 -0
- package/dist/src/facade/types.d.ts +50 -0
- package/dist/src/facade/types.js +4 -0
- package/dist/src/index.cjs +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/legacy.cjs +29 -0
- package/dist/src/legacy.d.ts +0 -0
- package/dist/src/legacy.js +1 -0
- package/dist/src/mock-relay-server.cjs +29 -0
- package/dist/src/mock-relay-server.d.ts +30 -0
- package/dist/src/mock-relay-server.js +236 -0
- package/package.json +37 -11
- package/dist/ratchet/tests/ratchet.test.d.ts +0 -1
- package/dist/ratchet/tests/ratchet.test.js +0 -160
- /package/dist/{facade → src/facade}/crypto.d.ts +0 -0
- /package/dist/{facade → src/facade}/crypto.js +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for ratchet/key-recovery.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 ratchet/replay-protection.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 ratchet/tofu.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 src/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,105 @@
|
|
|
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;
|
|
4
|
+
type UserAvailableHandler = (userId: UserId) => void;
|
|
5
|
+
export declare class StvorFacadeClient {
|
|
6
|
+
readonly userId: UserId;
|
|
7
|
+
private readonly relay;
|
|
8
|
+
private readonly defaultTimeout;
|
|
9
|
+
private crypto;
|
|
10
|
+
private handlers;
|
|
11
|
+
private userAvailableHandlers;
|
|
12
|
+
private knownPubKeys;
|
|
13
|
+
private pendingKeyResolvers;
|
|
14
|
+
constructor(userId: UserId, relay: RelayClient, defaultTimeout?: number);
|
|
15
|
+
private handleRelayMessage;
|
|
16
|
+
internalInitialize(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a user's public key is available locally
|
|
19
|
+
*/
|
|
20
|
+
isUserAvailable(userId: UserId): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Get list of all known users (whose public keys we have)
|
|
23
|
+
*/
|
|
24
|
+
getAvailableUsers(): UserId[];
|
|
25
|
+
/**
|
|
26
|
+
* Wait until a specific user's public key becomes available.
|
|
27
|
+
* This is the recommended way to ensure you can send messages.
|
|
28
|
+
*
|
|
29
|
+
* @param userId - The user to wait for
|
|
30
|
+
* @param timeoutMs - Maximum time to wait (default: 10000ms)
|
|
31
|
+
* @throws StvorError with RECIPIENT_TIMEOUT if timeout expires
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* await alice.waitForUser('bob@example.com');
|
|
36
|
+
* await alice.send('bob@example.com', 'Hello!');
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
waitForUser(userId: UserId, timeoutMs?: number): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Send an encrypted message to a recipient.
|
|
42
|
+
*
|
|
43
|
+
* If the recipient's public key is not yet available, this method will
|
|
44
|
+
* automatically wait up to `timeoutMs` for the key to arrive via the relay.
|
|
45
|
+
*
|
|
46
|
+
* @param recipientId - The recipient's user ID
|
|
47
|
+
* @param content - Message content (string or Uint8Array)
|
|
48
|
+
* @param options - Optional: { timeout: number, waitForRecipient: boolean }
|
|
49
|
+
* @throws StvorError with RECIPIENT_TIMEOUT if recipient key doesn't arrive in time
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* // Auto-waits for recipient (recommended)
|
|
54
|
+
* await alice.send('bob@example.com', 'Hello!');
|
|
55
|
+
*
|
|
56
|
+
* // Skip waiting (throws immediately if not available)
|
|
57
|
+
* await alice.send('bob@example.com', 'Hello!', { waitForRecipient: false });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
send(recipientId: UserId, content: MessageContent, options?: {
|
|
61
|
+
timeout?: number;
|
|
62
|
+
waitForRecipient?: boolean;
|
|
63
|
+
}): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Register a handler for incoming messages
|
|
66
|
+
*/
|
|
67
|
+
onMessage(handler: MessageHandler): () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Register a handler that fires when a new user becomes available.
|
|
70
|
+
* This is triggered when we receive a user's public key announcement.
|
|
71
|
+
*
|
|
72
|
+
* **Edge-triggered**: Fires only ONCE per user, on first key discovery.
|
|
73
|
+
* Will NOT fire again if user reconnects with same identity.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* client.onUserAvailable((userId) => {
|
|
78
|
+
* console.log(`${userId} is now available for messaging`);
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
onUserAvailable(handler: UserAvailableHandler): () => void;
|
|
83
|
+
}
|
|
84
|
+
export declare class StvorApp {
|
|
85
|
+
private readonly config;
|
|
86
|
+
private clients;
|
|
87
|
+
constructor(config: StvorAppConfig);
|
|
88
|
+
connect(userId: UserId): Promise<StvorFacadeClient>;
|
|
89
|
+
/**
|
|
90
|
+
* Get a connected client by user ID
|
|
91
|
+
*/
|
|
92
|
+
getClient(userId: UserId): StvorFacadeClient | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Check if a user is connected locally
|
|
95
|
+
*/
|
|
96
|
+
isConnected(userId: UserId): boolean;
|
|
97
|
+
disconnect(userId?: UserId): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
export declare function init(config: StvorAppConfig): Promise<StvorApp>;
|
|
100
|
+
export declare const createApp: typeof init;
|
|
101
|
+
export declare const Stvor: {
|
|
102
|
+
init: typeof init;
|
|
103
|
+
createApp: typeof init;
|
|
104
|
+
};
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Errors, StvorError } from './errors.js';
|
|
2
|
+
import { RelayClient } from './relay-client.js';
|
|
3
|
+
import { CryptoSession } from './crypto.js';
|
|
4
|
+
/** Default timeout for waiting for recipient keys (ms) */
|
|
5
|
+
const DEFAULT_RECIPIENT_TIMEOUT = 10000;
|
|
6
|
+
/** Polling interval for key resolution (ms) */
|
|
7
|
+
const KEY_POLL_INTERVAL = 100;
|
|
8
|
+
export class StvorFacadeClient {
|
|
9
|
+
constructor(userId, relay, defaultTimeout = DEFAULT_RECIPIENT_TIMEOUT) {
|
|
10
|
+
this.userId = userId;
|
|
11
|
+
this.relay = relay;
|
|
12
|
+
this.defaultTimeout = defaultTimeout;
|
|
13
|
+
this.handlers = [];
|
|
14
|
+
this.userAvailableHandlers = [];
|
|
15
|
+
this.knownPubKeys = new Map();
|
|
16
|
+
this.pendingKeyResolvers = new Map();
|
|
17
|
+
this.crypto = new CryptoSession();
|
|
18
|
+
// listen relay messages
|
|
19
|
+
this.relay.onMessage((m) => this.handleRelayMessage(m));
|
|
20
|
+
// announce our public key
|
|
21
|
+
this.relay.send({ type: 'announce', user: this.userId, pub: this.crypto.exportPublic() });
|
|
22
|
+
}
|
|
23
|
+
async handleRelayMessage(m) {
|
|
24
|
+
if (!m || typeof m !== 'object')
|
|
25
|
+
return;
|
|
26
|
+
if (m.type === 'announce' && m.user && m.pub) {
|
|
27
|
+
const wasKnown = this.knownPubKeys.has(m.user);
|
|
28
|
+
this.knownPubKeys.set(m.user, m.pub);
|
|
29
|
+
// Notify pending resolvers
|
|
30
|
+
const resolvers = this.pendingKeyResolvers.get(m.user);
|
|
31
|
+
if (resolvers) {
|
|
32
|
+
resolvers.forEach(resolve => resolve());
|
|
33
|
+
this.pendingKeyResolvers.delete(m.user);
|
|
34
|
+
}
|
|
35
|
+
// Notify user available handlers (only for new users)
|
|
36
|
+
if (!wasKnown) {
|
|
37
|
+
for (const h of this.userAvailableHandlers) {
|
|
38
|
+
try {
|
|
39
|
+
h(m.user);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (m.type === 'message' && m.to === this.userId && m.payload) {
|
|
47
|
+
const payload = m.payload;
|
|
48
|
+
const sender = m.from;
|
|
49
|
+
try {
|
|
50
|
+
const plain = this.crypto.decrypt(payload, payload.senderPub);
|
|
51
|
+
const text = new TextDecoder().decode(plain);
|
|
52
|
+
for (const h of this.handlers)
|
|
53
|
+
h(sender, text);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// ignore decryption errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async internalInitialize() {
|
|
61
|
+
// nothing for now; announce already sent in constructor
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if a user's public key is available locally
|
|
65
|
+
*/
|
|
66
|
+
isUserAvailable(userId) {
|
|
67
|
+
return this.knownPubKeys.has(userId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get list of all known users (whose public keys we have)
|
|
71
|
+
*/
|
|
72
|
+
getAvailableUsers() {
|
|
73
|
+
return Array.from(this.knownPubKeys.keys()).filter(id => id !== this.userId);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wait until a specific user's public key becomes available.
|
|
77
|
+
* This is the recommended way to ensure you can send messages.
|
|
78
|
+
*
|
|
79
|
+
* @param userId - The user to wait for
|
|
80
|
+
* @param timeoutMs - Maximum time to wait (default: 10000ms)
|
|
81
|
+
* @throws StvorError with RECIPIENT_TIMEOUT if timeout expires
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* await alice.waitForUser('bob@example.com');
|
|
86
|
+
* await alice.send('bob@example.com', 'Hello!');
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
async waitForUser(userId, timeoutMs = this.defaultTimeout) {
|
|
90
|
+
// Already available
|
|
91
|
+
if (this.knownPubKeys.has(userId)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const timeout = setTimeout(() => {
|
|
96
|
+
// Remove from pending
|
|
97
|
+
const resolvers = this.pendingKeyResolvers.get(userId);
|
|
98
|
+
if (resolvers) {
|
|
99
|
+
const idx = resolvers.indexOf(resolveHandler);
|
|
100
|
+
if (idx >= 0)
|
|
101
|
+
resolvers.splice(idx, 1);
|
|
102
|
+
if (resolvers.length === 0)
|
|
103
|
+
this.pendingKeyResolvers.delete(userId);
|
|
104
|
+
}
|
|
105
|
+
reject(new StvorError(Errors.RECIPIENT_TIMEOUT, `Timed out waiting for user "${userId}" after ${timeoutMs}ms. ` +
|
|
106
|
+
`The user may not be connected to the relay. ` +
|
|
107
|
+
`Ensure both parties are online before sending messages.`, 'Verify the recipient is connected, or increase timeout', true));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
const resolveHandler = () => {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
resolve();
|
|
112
|
+
};
|
|
113
|
+
// Add to pending resolvers
|
|
114
|
+
if (!this.pendingKeyResolvers.has(userId)) {
|
|
115
|
+
this.pendingKeyResolvers.set(userId, []);
|
|
116
|
+
}
|
|
117
|
+
this.pendingKeyResolvers.get(userId).push(resolveHandler);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Send an encrypted message to a recipient.
|
|
122
|
+
*
|
|
123
|
+
* If the recipient's public key is not yet available, this method will
|
|
124
|
+
* automatically wait up to `timeoutMs` for the key to arrive via the relay.
|
|
125
|
+
*
|
|
126
|
+
* @param recipientId - The recipient's user ID
|
|
127
|
+
* @param content - Message content (string or Uint8Array)
|
|
128
|
+
* @param options - Optional: { timeout: number, waitForRecipient: boolean }
|
|
129
|
+
* @throws StvorError with RECIPIENT_TIMEOUT if recipient key doesn't arrive in time
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // Auto-waits for recipient (recommended)
|
|
134
|
+
* await alice.send('bob@example.com', 'Hello!');
|
|
135
|
+
*
|
|
136
|
+
* // Skip waiting (throws immediately if not available)
|
|
137
|
+
* await alice.send('bob@example.com', 'Hello!', { waitForRecipient: false });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async send(recipientId, content, options) {
|
|
141
|
+
const { timeout = this.defaultTimeout, waitForRecipient = true } = options ?? {};
|
|
142
|
+
// Try to resolve recipient key
|
|
143
|
+
let recipientPub = this.knownPubKeys.get(recipientId);
|
|
144
|
+
if (!recipientPub) {
|
|
145
|
+
if (!waitForRecipient) {
|
|
146
|
+
throw new StvorError(Errors.RECIPIENT_NOT_FOUND, `Recipient "${recipientId}" is not available. ` +
|
|
147
|
+
`Their public key has not been announced to the relay. ` +
|
|
148
|
+
`Use waitForUser() or enable waitForRecipient option.`, 'Call waitForUser(recipientId) before sending, or ensure recipient is connected', false);
|
|
149
|
+
}
|
|
150
|
+
// Wait for recipient key with timeout
|
|
151
|
+
await this.waitForUser(recipientId, timeout);
|
|
152
|
+
recipientPub = this.knownPubKeys.get(recipientId);
|
|
153
|
+
if (!recipientPub) {
|
|
154
|
+
// Should not happen, but safety check
|
|
155
|
+
throw new StvorError(Errors.RECIPIENT_NOT_FOUND, `Recipient "${recipientId}" key resolution failed unexpectedly.`, 'This is an internal error, please report it', false);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const plain = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
159
|
+
const payload = this.crypto.encrypt(plain, recipientPub);
|
|
160
|
+
const msg = { type: 'message', to: recipientId, from: this.userId, payload };
|
|
161
|
+
this.relay.send(msg);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Register a handler for incoming messages
|
|
165
|
+
*/
|
|
166
|
+
onMessage(handler) {
|
|
167
|
+
this.handlers.push(handler);
|
|
168
|
+
return () => {
|
|
169
|
+
const i = this.handlers.indexOf(handler);
|
|
170
|
+
if (i >= 0)
|
|
171
|
+
this.handlers.splice(i, 1);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Register a handler that fires when a new user becomes available.
|
|
176
|
+
* This is triggered when we receive a user's public key announcement.
|
|
177
|
+
*
|
|
178
|
+
* **Edge-triggered**: Fires only ONCE per user, on first key discovery.
|
|
179
|
+
* Will NOT fire again if user reconnects with same identity.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* client.onUserAvailable((userId) => {
|
|
184
|
+
* console.log(`${userId} is now available for messaging`);
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
onUserAvailable(handler) {
|
|
189
|
+
this.userAvailableHandlers.push(handler);
|
|
190
|
+
return () => {
|
|
191
|
+
const i = this.userAvailableHandlers.indexOf(handler);
|
|
192
|
+
if (i >= 0)
|
|
193
|
+
this.userAvailableHandlers.splice(i, 1);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export class StvorApp {
|
|
198
|
+
constructor(config) {
|
|
199
|
+
this.config = config;
|
|
200
|
+
this.clients = new Map();
|
|
201
|
+
}
|
|
202
|
+
async connect(userId) {
|
|
203
|
+
const existing = this.clients.get(userId);
|
|
204
|
+
if (existing)
|
|
205
|
+
return existing;
|
|
206
|
+
const relay = new RelayClient(this.config.relayUrl ?? 'wss://stvor.xyz/relay', this.config.appToken, this.config.timeout ?? 10000);
|
|
207
|
+
// Wait for relay handshake - throws if API key is invalid
|
|
208
|
+
await relay.init();
|
|
209
|
+
const client = new StvorFacadeClient(userId, relay, this.config.timeout ?? 10000);
|
|
210
|
+
await client.internalInitialize();
|
|
211
|
+
this.clients.set(userId, client);
|
|
212
|
+
return client;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get a connected client by user ID
|
|
216
|
+
*/
|
|
217
|
+
getClient(userId) {
|
|
218
|
+
return this.clients.get(userId);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if a user is connected locally
|
|
222
|
+
*/
|
|
223
|
+
isConnected(userId) {
|
|
224
|
+
return this.clients.has(userId);
|
|
225
|
+
}
|
|
226
|
+
async disconnect(userId) {
|
|
227
|
+
if (userId) {
|
|
228
|
+
this.clients.delete(userId);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.clients.clear();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export async function init(config) {
|
|
235
|
+
if (!config.appToken.startsWith('stvor_')) {
|
|
236
|
+
throw new StvorError(Errors.INVALID_APP_TOKEN, 'Invalid app token');
|
|
237
|
+
}
|
|
238
|
+
return new StvorApp(config);
|
|
239
|
+
}
|
|
240
|
+
// Alias for createApp
|
|
241
|
+
export const createApp = init;
|
|
242
|
+
export const Stvor = {
|
|
243
|
+
init,
|
|
244
|
+
createApp,
|
|
245
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for src/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 src/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,19 @@
|
|
|
1
|
+
export declare const Errors: {
|
|
2
|
+
readonly INVALID_APP_TOKEN: "INVALID_APP_TOKEN";
|
|
3
|
+
readonly INVALID_API_KEY: "INVALID_API_KEY";
|
|
4
|
+
readonly RELAY_UNAVAILABLE: "RELAY_UNAVAILABLE";
|
|
5
|
+
readonly DELIVERY_FAILED: "DELIVERY_FAILED";
|
|
6
|
+
readonly RECIPIENT_NOT_FOUND: "RECIPIENT_NOT_FOUND";
|
|
7
|
+
readonly RECIPIENT_TIMEOUT: "RECIPIENT_TIMEOUT";
|
|
8
|
+
readonly MESSAGE_INTEGRITY_FAILED: "MESSAGE_INTEGRITY_FAILED";
|
|
9
|
+
readonly RECEIVE_TIMEOUT: "RECEIVE_TIMEOUT";
|
|
10
|
+
readonly RECEIVE_IN_PROGRESS: "RECEIVE_IN_PROGRESS";
|
|
11
|
+
readonly NOT_CONNECTED: "NOT_CONNECTED";
|
|
12
|
+
};
|
|
13
|
+
export type ErrorCode = (typeof Errors)[keyof typeof Errors];
|
|
14
|
+
export declare class StvorError extends Error {
|
|
15
|
+
code: ErrorCode;
|
|
16
|
+
action?: string;
|
|
17
|
+
retryable?: boolean;
|
|
18
|
+
constructor(code: ErrorCode, message: string, action?: string, retryable?: boolean);
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const Errors = {
|
|
2
|
+
INVALID_APP_TOKEN: 'INVALID_APP_TOKEN',
|
|
3
|
+
INVALID_API_KEY: 'INVALID_API_KEY',
|
|
4
|
+
RELAY_UNAVAILABLE: 'RELAY_UNAVAILABLE',
|
|
5
|
+
DELIVERY_FAILED: 'DELIVERY_FAILED',
|
|
6
|
+
RECIPIENT_NOT_FOUND: 'RECIPIENT_NOT_FOUND',
|
|
7
|
+
RECIPIENT_TIMEOUT: 'RECIPIENT_TIMEOUT',
|
|
8
|
+
MESSAGE_INTEGRITY_FAILED: 'MESSAGE_INTEGRITY_FAILED',
|
|
9
|
+
RECEIVE_TIMEOUT: 'RECEIVE_TIMEOUT',
|
|
10
|
+
RECEIVE_IN_PROGRESS: 'RECEIVE_IN_PROGRESS',
|
|
11
|
+
NOT_CONNECTED: 'NOT_CONNECTED',
|
|
12
|
+
};
|
|
13
|
+
export class StvorError extends Error {
|
|
14
|
+
constructor(code, message, action, retryable) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.action = action;
|
|
18
|
+
this.retryable = retryable;
|
|
19
|
+
this.name = 'StvorError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for src/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,8 @@
|
|
|
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, createApp } from './app.js';
|
|
8
|
+
export { ErrorCode as STVOR_ERRORS } from './errors.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated CommonJS wrapper for src/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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STVOR DX Facade - Relay Client
|
|
3
|
+
*/
|
|
4
|
+
type JSONable = Record<string, any>;
|
|
5
|
+
export type RelayHandler = (msg: JSONable) => void;
|
|
6
|
+
export declare class RelayClient {
|
|
7
|
+
private relayUrl;
|
|
8
|
+
private timeout;
|
|
9
|
+
private appToken;
|
|
10
|
+
private ws?;
|
|
11
|
+
private connected;
|
|
12
|
+
private handshakeComplete;
|
|
13
|
+
private backoff;
|
|
14
|
+
private queue;
|
|
15
|
+
private handlers;
|
|
16
|
+
private reconnecting;
|
|
17
|
+
private connectPromise?;
|
|
18
|
+
private connectResolve?;
|
|
19
|
+
private connectReject?;
|
|
20
|
+
private authFailed;
|
|
21
|
+
constructor(relayUrl: string, appToken: string, timeout?: number);
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the connection and wait for handshake.
|
|
24
|
+
* Throws StvorError if API key is rejected.
|
|
25
|
+
*/
|
|
26
|
+
init(): Promise<void>;
|
|
27
|
+
private getAuthHeaders;
|
|
28
|
+
private connect;
|
|
29
|
+
private scheduleReconnect;
|
|
30
|
+
private doSend;
|
|
31
|
+
send(obj: JSONable): void;
|
|
32
|
+
onMessage(h: RelayHandler): void;
|
|
33
|
+
isConnected(): boolean;
|
|
34
|
+
isAuthenticated(): boolean;
|
|
35
|
+
}
|
|
36
|
+
export {};
|