@agora-sdk/secure-chat-core 0.1.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/LICENSE +202 -0
- package/dist/cjs/context/secure-chat-context.d.ts +60 -0
- package/dist/cjs/context/secure-chat-context.js +70 -0
- package/dist/cjs/context/secure-chat-context.js.map +1 -0
- package/dist/cjs/contract/index.d.ts +150 -0
- package/dist/cjs/contract/index.js +16 -0
- package/dist/cjs/contract/index.js.map +1 -0
- package/dist/cjs/hooks/useSecureConversations.d.ts +35 -0
- package/dist/cjs/hooks/useSecureConversations.js +109 -0
- package/dist/cjs/hooks/useSecureConversations.js.map +1 -0
- package/dist/cjs/hooks/useSecureDevice.d.ts +47 -0
- package/dist/cjs/hooks/useSecureDevice.js +120 -0
- package/dist/cjs/hooks/useSecureDevice.js.map +1 -0
- package/dist/cjs/hooks/useSecureMessages.d.ts +52 -0
- package/dist/cjs/hooks/useSecureMessages.js +113 -0
- package/dist/cjs/hooks/useSecureMessages.js.map +1 -0
- package/dist/cjs/index.d.ts +15 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/transport/rest.d.ts +154 -0
- package/dist/cjs/transport/rest.js +208 -0
- package/dist/cjs/transport/rest.js.map +1 -0
- package/dist/cjs/transport/socket.d.ts +79 -0
- package/dist/cjs/transport/socket.js +70 -0
- package/dist/cjs/transport/socket.js.map +1 -0
- package/dist/cjs/util/base64.d.ts +8 -0
- package/dist/cjs/util/base64.js +69 -0
- package/dist/cjs/util/base64.js.map +1 -0
- package/dist/esm/context/secure-chat-context.d.ts +60 -0
- package/dist/esm/context/secure-chat-context.js +66 -0
- package/dist/esm/context/secure-chat-context.js.map +1 -0
- package/dist/esm/contract/index.d.ts +150 -0
- package/dist/esm/contract/index.js +15 -0
- package/dist/esm/contract/index.js.map +1 -0
- package/dist/esm/hooks/useSecureConversations.d.ts +35 -0
- package/dist/esm/hooks/useSecureConversations.js +106 -0
- package/dist/esm/hooks/useSecureConversations.js.map +1 -0
- package/dist/esm/hooks/useSecureDevice.d.ts +47 -0
- package/dist/esm/hooks/useSecureDevice.js +117 -0
- package/dist/esm/hooks/useSecureDevice.js.map +1 -0
- package/dist/esm/hooks/useSecureMessages.d.ts +52 -0
- package/dist/esm/hooks/useSecureMessages.js +110 -0
- package/dist/esm/hooks/useSecureMessages.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/transport/rest.d.ts +154 -0
- package/dist/esm/transport/rest.js +201 -0
- package/dist/esm/transport/rest.js.map +1 -0
- package/dist/esm/transport/socket.d.ts +79 -0
- package/dist/esm/transport/socket.js +66 -0
- package/dist/esm/transport/socket.js.map +1 -0
- package/dist/esm/util/base64.d.ts +8 -0
- package/dist/esm/util/base64.js +63 -0
- package/dist/esm/util/base64.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SecureDeviceModel } from "../contract";
|
|
2
|
+
/** Options for {@link useSecureDevice}. */
|
|
3
|
+
export interface UseSecureDeviceOptions {
|
|
4
|
+
/** Stable, persisted client device id. Generated if omitted (persist it in the platform layer). */
|
|
5
|
+
deviceId?: string;
|
|
6
|
+
/** MLS ciphersuite to register under. Defaults to the crypto implementation's preferred suite. */
|
|
7
|
+
ciphersuite?: number;
|
|
8
|
+
/** How many KeyPackages to publish on registration and replenish toward. Default 20. */
|
|
9
|
+
keyPackageTarget?: number;
|
|
10
|
+
/** Auto-replenish when `secure:key-packages-low` fires. Default true. */
|
|
11
|
+
autoReplenish?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The state and actions returned by {@link useSecureDevice}. */
|
|
14
|
+
export interface UseSecureDeviceValues {
|
|
15
|
+
/** The registered device row (its `.id` is the uuid used as targetDeviceId everywhere). */
|
|
16
|
+
device: SecureDeviceModel | null;
|
|
17
|
+
/** True while {@link UseSecureDeviceValues.register} is in flight. */
|
|
18
|
+
registering: boolean;
|
|
19
|
+
/** The last error thrown by registration or replenishment, or `null`. */
|
|
20
|
+
error: unknown;
|
|
21
|
+
/** Last known count of unconsumed KeyPackages, or `null` until refreshed. */
|
|
22
|
+
keyPackagesAvailable: number | null;
|
|
23
|
+
/** Generate identity + register (idempotent server-side on (userId, deviceId)). */
|
|
24
|
+
register: () => Promise<SecureDeviceModel>;
|
|
25
|
+
/** Generate + publish `count` fresh KeyPackages (default = keyPackageTarget). */
|
|
26
|
+
publishKeyPackages: (count?: number) => Promise<number>;
|
|
27
|
+
/** Re-query the server for the available KeyPackage count and update `keyPackagesAvailable`. */
|
|
28
|
+
refreshKeyPackageCount: () => Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register this client as an MLS device (one device = one leaf) and keep its KeyPackages stocked.
|
|
32
|
+
*
|
|
33
|
+
* On {@link UseSecureDeviceValues.register} it generates a device identity, POSTs it to `/devices`,
|
|
34
|
+
* and (when `autoReplenish` is on) republishes KeyPackages whenever the server emits
|
|
35
|
+
* `secure:key-packages-low` for this device. The device's private key material must be persisted by
|
|
36
|
+
* the platform layer — this hook only handles registration and relay.
|
|
37
|
+
*
|
|
38
|
+
* @param options - {@link UseSecureDeviceOptions} — device id, ciphersuite, and replenishment tuning.
|
|
39
|
+
* @returns {@link UseSecureDeviceValues} — the device row, status flags, and register/publish actions.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { device, register } = useSecureDevice({ keyPackageTarget: 20 });
|
|
44
|
+
* useEffect(() => { register(); }, []);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function useSecureDevice(options?: UseSecureDeviceOptions): UseSecureDeviceValues;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// useSecureDevice — register this client as an MLS device (leaf) and keep its KeyPackages topped up.
|
|
3
|
+
//
|
|
4
|
+
// Flow (server spec §14): generateDeviceIdentity → POST /devices → publish a batch of KeyPackages;
|
|
5
|
+
// replenish on the `secure:key-packages-low` realtime signal or via the count endpoint.
|
|
6
|
+
//
|
|
7
|
+
// NOTE: the device's PRIVATE state (`privateState` from generateDeviceIdentity, and MLS group
|
|
8
|
+
// state) must be persisted by the platform layer (IndexedDB on web, keystore on native — Phase 2/3).
|
|
9
|
+
// Core only performs registration + relay; it does not persist secrets.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.useSecureDevice = useSecureDevice;
|
|
12
|
+
const react_1 = require("react");
|
|
13
|
+
const base64_1 = require("../util/base64");
|
|
14
|
+
const secure_chat_context_1 = require("../context/secure-chat-context");
|
|
15
|
+
/**
|
|
16
|
+
* Mint a device id when the caller doesn't supply one — `crypto.randomUUID()` when available, else a
|
|
17
|
+
* non-cryptographic timestamp+random fallback. The platform layer should supply a persisted id.
|
|
18
|
+
*
|
|
19
|
+
* @returns A fresh device id string.
|
|
20
|
+
*/
|
|
21
|
+
function newDeviceId() {
|
|
22
|
+
const g = globalThis.crypto;
|
|
23
|
+
if (g?.randomUUID)
|
|
24
|
+
return g.randomUUID();
|
|
25
|
+
// Platform layer should supply a persisted, stable device id; this is a non-crypto fallback.
|
|
26
|
+
return `dev-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register this client as an MLS device (one device = one leaf) and keep its KeyPackages stocked.
|
|
30
|
+
*
|
|
31
|
+
* On {@link UseSecureDeviceValues.register} it generates a device identity, POSTs it to `/devices`,
|
|
32
|
+
* and (when `autoReplenish` is on) republishes KeyPackages whenever the server emits
|
|
33
|
+
* `secure:key-packages-low` for this device. The device's private key material must be persisted by
|
|
34
|
+
* the platform layer — this hook only handles registration and relay.
|
|
35
|
+
*
|
|
36
|
+
* @param options - {@link UseSecureDeviceOptions} — device id, ciphersuite, and replenishment tuning.
|
|
37
|
+
* @returns {@link UseSecureDeviceValues} — the device row, status flags, and register/publish actions.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { device, register } = useSecureDevice({ keyPackageTarget: 20 });
|
|
42
|
+
* useEffect(() => { register(); }, []);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function useSecureDevice(options = {}) {
|
|
46
|
+
const { crypto, rest, socket } = (0, secure_chat_context_1.useSecureChat)();
|
|
47
|
+
const { ciphersuite, keyPackageTarget = 20, autoReplenish = true } = options;
|
|
48
|
+
const [device, setDevice] = (0, react_1.useState)(null);
|
|
49
|
+
const [registering, setRegistering] = (0, react_1.useState)(false);
|
|
50
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
51
|
+
const [keyPackagesAvailable, setKeyPackagesAvailable] = (0, react_1.useState)(null);
|
|
52
|
+
const deviceIdRef = (0, react_1.useRef)(options.deviceId ?? newDeviceId());
|
|
53
|
+
const publishKeyPackages = (0, react_1.useCallback)(async (count = keyPackageTarget) => {
|
|
54
|
+
if (!device)
|
|
55
|
+
throw new Error("Register the device before publishing KeyPackages.");
|
|
56
|
+
const bundles = await crypto.generateKeyPackages(count);
|
|
57
|
+
const published = await rest.publishKeyPackages(device.id, {
|
|
58
|
+
keyPackages: bundles.map((b) => ({
|
|
59
|
+
keyPackageRef: b.keyPackageRef,
|
|
60
|
+
keyPackage: (0, base64_1.toBase64)(b.keyPackage),
|
|
61
|
+
ciphersuite: b.ciphersuite,
|
|
62
|
+
expiresAt: b.expiresAt,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
return published;
|
|
66
|
+
}, [crypto, rest, device, keyPackageTarget]);
|
|
67
|
+
const refreshKeyPackageCount = (0, react_1.useCallback)(async () => {
|
|
68
|
+
if (!device)
|
|
69
|
+
throw new Error("Register the device before checking KeyPackage count.");
|
|
70
|
+
const available = await rest.keyPackageCount(device.id);
|
|
71
|
+
setKeyPackagesAvailable(available);
|
|
72
|
+
return available;
|
|
73
|
+
}, [rest, device]);
|
|
74
|
+
const register = (0, react_1.useCallback)(async () => {
|
|
75
|
+
setRegistering(true);
|
|
76
|
+
setError(null);
|
|
77
|
+
try {
|
|
78
|
+
const { identity } = await crypto.generateDeviceIdentity({
|
|
79
|
+
deviceId: deviceIdRef.current,
|
|
80
|
+
ciphersuite,
|
|
81
|
+
});
|
|
82
|
+
const registered = await rest.registerDevice({
|
|
83
|
+
deviceId: identity.deviceId,
|
|
84
|
+
signaturePublicKey: (0, base64_1.toBase64)(identity.signaturePublicKey),
|
|
85
|
+
credential: (0, base64_1.toBase64)(identity.credential),
|
|
86
|
+
ciphersuite: identity.ciphersuite,
|
|
87
|
+
});
|
|
88
|
+
setDevice(registered);
|
|
89
|
+
return registered;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
setError(err);
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
setRegistering(false);
|
|
97
|
+
}
|
|
98
|
+
}, [crypto, rest, ciphersuite]);
|
|
99
|
+
// Auto-replenish on the server's low-water signal for this device.
|
|
100
|
+
(0, react_1.useEffect)(() => {
|
|
101
|
+
if (!autoReplenish || !device)
|
|
102
|
+
return;
|
|
103
|
+
const off = socket.on("secure:key-packages-low", (signal) => {
|
|
104
|
+
if (signal.deviceId !== device.id)
|
|
105
|
+
return;
|
|
106
|
+
publishKeyPackages().catch(setError);
|
|
107
|
+
});
|
|
108
|
+
return off;
|
|
109
|
+
}, [autoReplenish, device, socket, publishKeyPackages]);
|
|
110
|
+
return {
|
|
111
|
+
device,
|
|
112
|
+
registering,
|
|
113
|
+
error,
|
|
114
|
+
keyPackagesAvailable,
|
|
115
|
+
register,
|
|
116
|
+
publishKeyPackages,
|
|
117
|
+
refreshKeyPackageCount,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=useSecureDevice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSecureDevice.js","sourceRoot":"","sources":["../../../src/hooks/useSecureDevice.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SecureMessageModel } from "../contract";
|
|
2
|
+
import { GroupHandle } from "@agora-sdk/secure-chat-crypto";
|
|
3
|
+
/** A stored message paired with its decrypted text (when a group handle is available). */
|
|
4
|
+
export interface DecryptedSecureMessage {
|
|
5
|
+
/** The raw message row from the server (still holds the base64 ciphertext). */
|
|
6
|
+
model: SecureMessageModel;
|
|
7
|
+
/** Decrypted text, or null when no group handle is available or decryption is pending/failed. */
|
|
8
|
+
plaintext: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** Options for {@link useSecureMessages}. */
|
|
11
|
+
export interface UseSecureMessagesOptions {
|
|
12
|
+
/** The MLS group handle for this conversation (from the platform persistence layer). */
|
|
13
|
+
group?: GroupHandle;
|
|
14
|
+
/** The caller's device row id — required to send (the server verifies it belongs to the caller). */
|
|
15
|
+
senderDeviceId?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The state and actions returned by {@link useSecureMessages}. */
|
|
18
|
+
export interface UseSecureMessagesValues {
|
|
19
|
+
/** Loaded messages, newest first, each with decrypted text when possible. */
|
|
20
|
+
messages: DecryptedSecureMessage[];
|
|
21
|
+
/** True while a page load or refresh is in flight. */
|
|
22
|
+
loading: boolean;
|
|
23
|
+
/** Whether older messages remain to {@link UseSecureMessagesValues.loadMore}. */
|
|
24
|
+
hasMore: boolean;
|
|
25
|
+
/** The last error thrown by loading or sending, or `null`. */
|
|
26
|
+
error: unknown;
|
|
27
|
+
/** Append the next page of older messages. No-op when already loading or exhausted. */
|
|
28
|
+
loadMore: () => Promise<void>;
|
|
29
|
+
/** Reload from the newest message, replacing the current list. */
|
|
30
|
+
refresh: () => Promise<void>;
|
|
31
|
+
/** Encrypt + send a text message. Requires `group` + `senderDeviceId`. */
|
|
32
|
+
sendMessage: (text: string) => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Load, decrypt, send, and live-receive messages in one secure conversation.
|
|
36
|
+
*
|
|
37
|
+
* Decryption runs through the injected `SecureChatCrypto` against the conversation's MLS
|
|
38
|
+
* `GroupHandle`. Without a `group` in `options`, ciphertext is still listed and received (as
|
|
39
|
+
* `plaintext: null`) and sending is disabled — letting the transport work ahead of the crypto
|
|
40
|
+
* wiring. Joins the conversation's socket room to receive `secure:message` events live.
|
|
41
|
+
*
|
|
42
|
+
* @param conversationId - The conversation to read and send within.
|
|
43
|
+
* @param options - {@link UseSecureMessagesOptions} — the MLS `group` handle and `senderDeviceId`.
|
|
44
|
+
* @returns {@link UseSecureMessagesValues} — the message list, paging state, and `sendMessage`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const { messages, sendMessage } = useSecureMessages(conversationId, { group, senderDeviceId });
|
|
49
|
+
* await sendMessage("hello 💜");
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function useSecureMessages(conversationId: string, options?: UseSecureMessagesOptions): UseSecureMessagesValues;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// useSecureMessages — load, decrypt, send, and live-receive messages in a secure conversation.
|
|
3
|
+
//
|
|
4
|
+
// Encryption/decryption runs through the injected `SecureChatCrypto` against the conversation's MLS
|
|
5
|
+
// `GroupHandle`. Resolving conversationId → GroupHandle is owned by the platform persistence layer
|
|
6
|
+
// (Phase 2: IndexedDB group state via processWelcome/importGroupState), so the caller passes the
|
|
7
|
+
// handle in. Without it, ciphertext is still listed/received but left undecrypted (`plaintext: null`)
|
|
8
|
+
// and sending is disabled — keeping the transport usable ahead of the crypto wiring.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.useSecureMessages = useSecureMessages;
|
|
11
|
+
const react_1 = require("react");
|
|
12
|
+
const base64_1 = require("../util/base64");
|
|
13
|
+
const secure_chat_context_1 = require("../context/secure-chat-context");
|
|
14
|
+
/**
|
|
15
|
+
* Load, decrypt, send, and live-receive messages in one secure conversation.
|
|
16
|
+
*
|
|
17
|
+
* Decryption runs through the injected `SecureChatCrypto` against the conversation's MLS
|
|
18
|
+
* `GroupHandle`. Without a `group` in `options`, ciphertext is still listed and received (as
|
|
19
|
+
* `plaintext: null`) and sending is disabled — letting the transport work ahead of the crypto
|
|
20
|
+
* wiring. Joins the conversation's socket room to receive `secure:message` events live.
|
|
21
|
+
*
|
|
22
|
+
* @param conversationId - The conversation to read and send within.
|
|
23
|
+
* @param options - {@link UseSecureMessagesOptions} — the MLS `group` handle and `senderDeviceId`.
|
|
24
|
+
* @returns {@link UseSecureMessagesValues} — the message list, paging state, and `sendMessage`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { messages, sendMessage } = useSecureMessages(conversationId, { group, senderDeviceId });
|
|
29
|
+
* await sendMessage("hello 💜");
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function useSecureMessages(conversationId, options = {}) {
|
|
33
|
+
const { rest, crypto, socket } = (0, secure_chat_context_1.useSecureChat)();
|
|
34
|
+
const { group, senderDeviceId } = options;
|
|
35
|
+
const [messages, setMessages] = (0, react_1.useState)([]);
|
|
36
|
+
const [before, setBefore] = (0, react_1.useState)(undefined);
|
|
37
|
+
const [hasMore, setHasMore] = (0, react_1.useState)(true);
|
|
38
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
39
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
40
|
+
const decrypt = (0, react_1.useCallback)(async (model) => {
|
|
41
|
+
if (!group)
|
|
42
|
+
return { model, plaintext: null };
|
|
43
|
+
try {
|
|
44
|
+
const { plaintext } = await crypto.decryptMessage(group, (0, base64_1.fromBase64)(model.ciphertext));
|
|
45
|
+
return { model, plaintext: (0, base64_1.bytesToUtf8)(plaintext) };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Buffer/skip: epoch not yet reached, or undecryptable. Surface ciphertext without text.
|
|
49
|
+
return { model, plaintext: null };
|
|
50
|
+
}
|
|
51
|
+
}, [crypto, group]);
|
|
52
|
+
const load = (0, react_1.useCallback)(async (reset) => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
try {
|
|
56
|
+
const page = await rest.listMessages(conversationId, {
|
|
57
|
+
before: reset ? undefined : before,
|
|
58
|
+
limit: 40,
|
|
59
|
+
});
|
|
60
|
+
const decrypted = await Promise.all(page.messages.map(decrypt));
|
|
61
|
+
const oldest = page.messages[page.messages.length - 1];
|
|
62
|
+
setBefore(oldest ? oldest.createdAt : before);
|
|
63
|
+
setHasMore(page.hasMore);
|
|
64
|
+
// Server returns created_at DESC; keep newest-first in state.
|
|
65
|
+
setMessages((prev) => (reset ? decrypted : [...prev, ...decrypted]));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
setError(err);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [rest, conversationId, before, decrypt]);
|
|
74
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
75
|
+
setBefore(undefined);
|
|
76
|
+
await load(true);
|
|
77
|
+
}, [load]);
|
|
78
|
+
const loadMore = (0, react_1.useCallback)(async () => {
|
|
79
|
+
if (!hasMore || loading)
|
|
80
|
+
return;
|
|
81
|
+
await load(false);
|
|
82
|
+
}, [hasMore, loading, load]);
|
|
83
|
+
const sendMessage = (0, react_1.useCallback)(async (text) => {
|
|
84
|
+
if (!group)
|
|
85
|
+
throw new Error("Cannot send: no MLS group handle for this conversation.");
|
|
86
|
+
if (!senderDeviceId)
|
|
87
|
+
throw new Error("Cannot send: senderDeviceId is required.");
|
|
88
|
+
const { ciphertext, epoch } = await crypto.encryptMessage(group, (0, base64_1.utf8ToBytes)(text));
|
|
89
|
+
const sent = await rest.sendMessage(conversationId, {
|
|
90
|
+
ciphertext: (0, base64_1.toBase64)(ciphertext),
|
|
91
|
+
epoch: epoch.toString(),
|
|
92
|
+
senderDeviceId,
|
|
93
|
+
});
|
|
94
|
+
// Optimistic: we know our own plaintext without a round-trip through decrypt.
|
|
95
|
+
setMessages((prev) => [{ model: sent, plaintext: text }, ...prev]);
|
|
96
|
+
}, [crypto, rest, conversationId, group, senderDeviceId]);
|
|
97
|
+
(0, react_1.useEffect)(() => {
|
|
98
|
+
refresh();
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [conversationId]);
|
|
101
|
+
// Live receive: join the conversation room and decrypt inbound ciphertext.
|
|
102
|
+
(0, react_1.useEffect)(() => {
|
|
103
|
+
socket.joinConversation(conversationId);
|
|
104
|
+
const off = socket.on("secure:message", (model) => {
|
|
105
|
+
if (model.conversationId !== conversationId)
|
|
106
|
+
return;
|
|
107
|
+
decrypt(model).then((m) => setMessages((prev) => prev.some((p) => p.model.id === m.model.id) ? prev : [m, ...prev]));
|
|
108
|
+
});
|
|
109
|
+
return off;
|
|
110
|
+
}, [socket, conversationId, decrypt]);
|
|
111
|
+
return { messages, loading, hasMore, error, loadMore, refresh, sendMessage };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=useSecureMessages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSecureMessages.js","sourceRoot":"","sources":["../../../src/hooks/useSecureMessages.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { SecureChatProvider, useSecureChat } from "./context/secure-chat-context";
|
|
2
|
+
export type { SecureChatProviderProps, SecureChatContextValue, } from "./context/secure-chat-context";
|
|
3
|
+
export { useSecureDevice } from "./hooks/useSecureDevice";
|
|
4
|
+
export type { UseSecureDeviceOptions, UseSecureDeviceValues } from "./hooks/useSecureDevice";
|
|
5
|
+
export { useSecureConversations } from "./hooks/useSecureConversations";
|
|
6
|
+
export type { UseSecureConversationsValues } from "./hooks/useSecureConversations";
|
|
7
|
+
export { useSecureMessages } from "./hooks/useSecureMessages";
|
|
8
|
+
export type { UseSecureMessagesOptions, UseSecureMessagesValues, DecryptedSecureMessage, } from "./hooks/useSecureMessages";
|
|
9
|
+
export { SecureChatRestClient } from "./transport/rest";
|
|
10
|
+
export type { SecureChatRestConfig } from "./transport/rest";
|
|
11
|
+
export { SecureChatSocketClient } from "./transport/socket";
|
|
12
|
+
export type { SecureSocket, SecureServerEvents, SecureClientEvents, SecureChatSocketConfig, } from "./transport/socket";
|
|
13
|
+
export type { SecureChatCrypto, DeviceIdentity, KeyPackageBundle, GroupHandle, TargetedWelcome, CommitResult, PassphraseBackup, } from "@agora-sdk/secure-chat-crypto";
|
|
14
|
+
export type * from "./contract";
|
|
15
|
+
export { toBase64, fromBase64, utf8ToBytes, bytesToUtf8 } from "./util/base64";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @agora-sdk/secure-chat-core — platform-agnostic client for Agora end-to-end-encrypted chat.
|
|
3
|
+
//
|
|
4
|
+
// Transport (REST + /secure socket) + provider/hooks, with MLS crypto supplied by dependency
|
|
5
|
+
// injection of a `SecureChatCrypto`. Platform packages (@agora-sdk/secure-chat-react-js, etc.)
|
|
6
|
+
// re-export this and add the concrete crypto + persistence.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.bytesToUtf8 = exports.utf8ToBytes = exports.fromBase64 = exports.toBase64 = exports.SecureChatSocketClient = exports.SecureChatRestClient = exports.useSecureMessages = exports.useSecureConversations = exports.useSecureDevice = exports.useSecureChat = exports.SecureChatProvider = void 0;
|
|
9
|
+
// ── context / provider ──────────────────────────────────────────────────────
|
|
10
|
+
var secure_chat_context_1 = require("./context/secure-chat-context");
|
|
11
|
+
Object.defineProperty(exports, "SecureChatProvider", { enumerable: true, get: function () { return secure_chat_context_1.SecureChatProvider; } });
|
|
12
|
+
Object.defineProperty(exports, "useSecureChat", { enumerable: true, get: function () { return secure_chat_context_1.useSecureChat; } });
|
|
13
|
+
// ── hooks ────────────────────────────────────────────────────────────────────
|
|
14
|
+
var useSecureDevice_1 = require("./hooks/useSecureDevice");
|
|
15
|
+
Object.defineProperty(exports, "useSecureDevice", { enumerable: true, get: function () { return useSecureDevice_1.useSecureDevice; } });
|
|
16
|
+
var useSecureConversations_1 = require("./hooks/useSecureConversations");
|
|
17
|
+
Object.defineProperty(exports, "useSecureConversations", { enumerable: true, get: function () { return useSecureConversations_1.useSecureConversations; } });
|
|
18
|
+
var useSecureMessages_1 = require("./hooks/useSecureMessages");
|
|
19
|
+
Object.defineProperty(exports, "useSecureMessages", { enumerable: true, get: function () { return useSecureMessages_1.useSecureMessages; } });
|
|
20
|
+
// ── transport (for advanced / non-React use) ─────────────────────────────────
|
|
21
|
+
var rest_1 = require("./transport/rest");
|
|
22
|
+
Object.defineProperty(exports, "SecureChatRestClient", { enumerable: true, get: function () { return rest_1.SecureChatRestClient; } });
|
|
23
|
+
var socket_1 = require("./transport/socket");
|
|
24
|
+
Object.defineProperty(exports, "SecureChatSocketClient", { enumerable: true, get: function () { return socket_1.SecureChatSocketClient; } });
|
|
25
|
+
// ── utils ────────────────────────────────────────────────────────────────────
|
|
26
|
+
var base64_1 = require("./util/base64");
|
|
27
|
+
Object.defineProperty(exports, "toBase64", { enumerable: true, get: function () { return base64_1.toBase64; } });
|
|
28
|
+
Object.defineProperty(exports, "fromBase64", { enumerable: true, get: function () { return base64_1.fromBase64; } });
|
|
29
|
+
Object.defineProperty(exports, "utf8ToBytes", { enumerable: true, get: function () { return base64_1.utf8ToBytes; } });
|
|
30
|
+
Object.defineProperty(exports, "bytesToUtf8", { enumerable: true, get: function () { return base64_1.bytesToUtf8; } });
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,8FAA8F;AAC9F,EAAE;AACF,6FAA6F;AAC7F,+FAA+F;AAC/F,4DAA4D;;;AAE5D,+EAA+E;AAC/E,qEAAkF;AAAzE,yHAAA,kBAAkB,OAAA;AAAE,oHAAA,aAAa,OAAA;AAM1C,gFAAgF;AAChF,2DAA0D;AAAjD,kHAAA,eAAe,OAAA;AAExB,yEAAwE;AAA/D,gIAAA,sBAAsB,OAAA;AAE/B,+DAA8D;AAArD,sHAAA,iBAAiB,OAAA;AAO1B,gFAAgF;AAChF,yCAAwD;AAA/C,4GAAA,oBAAoB,OAAA;AAE7B,6CAA4D;AAAnD,gHAAA,sBAAsB,OAAA;AAsB/B,gFAAgF;AAChF,wCAA+E;AAAtE,kGAAA,QAAQ,OAAA;AAAE,oGAAA,UAAU,OAAA;AAAE,qGAAA,WAAW,OAAA;AAAE,qGAAA,WAAW,OAAA"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { AddSecureMemberBody, CreateSecureConversationBody, PublishKeyPackagesBody, RegisterDeviceBody, RemoveSecureMemberBody, SecureConversationMemberModel, SecureConversationModel, SecureDeviceModel, SecureHandshakeModel, SecureKeyBackupModel, SecureKeyPackageClaim, SecureMessageModel, SendSecureMessageBody, UploadKeyBackupBody } from "../contract";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for {@link SecureChatRestClient}. The base URL and access token are read through
|
|
4
|
+
* resolver callbacks rather than captured once, so a late-set `baseUrl` or a refreshed token take
|
|
5
|
+
* effect on the next request without rebuilding the client.
|
|
6
|
+
*/
|
|
7
|
+
export interface SecureChatRestConfig {
|
|
8
|
+
/** Resolve the API base URL (e.g. `getApiBaseUrl()` from @agora-sdk/core → `http://host/v7`). */
|
|
9
|
+
getBaseUrl: () => string;
|
|
10
|
+
/** Resolve the current access token, or undefined when signed out. */
|
|
11
|
+
getAccessToken: () => string | undefined;
|
|
12
|
+
/** The Agora project id (path-scoped on every endpoint). */
|
|
13
|
+
projectId: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Typed REST client for the secure-chat blind Delivery Service.
|
|
17
|
+
*
|
|
18
|
+
* Wraps every endpoint in agora-server `docs/SECURE_CHAT.md` §9. Each request lazily resolves the
|
|
19
|
+
* base URL and bearer token via the configured resolvers, scopes the path to
|
|
20
|
+
* `{baseUrl}/{projectId}/secure-chat`, and passes payloads through untouched — all binary is base64
|
|
21
|
+
* on the wire and the client never inspects it.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Construct this directly only for advanced / non-React use. Inside React, prefer the
|
|
25
|
+
* `SecureChatProvider` + hooks, which build and share one client for you.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const rest = new SecureChatRestClient({
|
|
30
|
+
* projectId,
|
|
31
|
+
* getBaseUrl: () => getApiBaseUrl(),
|
|
32
|
+
* getAccessToken: () => session.accessToken,
|
|
33
|
+
* });
|
|
34
|
+
* const device = await rest.registerDevice(body);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare class SecureChatRestClient {
|
|
38
|
+
private readonly config;
|
|
39
|
+
private readonly http;
|
|
40
|
+
constructor(config: SecureChatRestConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Register (or idempotently re-assert) the caller's MLS device — its signature key + credential.
|
|
43
|
+
*
|
|
44
|
+
* @param body - The device identity to publish (deviceId, base64 signature key, credential, ciphersuite).
|
|
45
|
+
* @returns The persisted device row; its `.id` is the uuid used as `targetDeviceId` elsewhere.
|
|
46
|
+
*/
|
|
47
|
+
registerDevice(body: RegisterDeviceBody): Promise<SecureDeviceModel>;
|
|
48
|
+
/** Public device records (signature key + credential) so peers can build a group. */
|
|
49
|
+
listDevices(userId: string): Promise<SecureDeviceModel[]>;
|
|
50
|
+
/** Revoke the caller's own device. */
|
|
51
|
+
revokeDevice(deviceId: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Publish a batch of fresh, single-use KeyPackages for one of the caller's devices so peers can
|
|
54
|
+
* add it to groups.
|
|
55
|
+
*
|
|
56
|
+
* @param deviceId - The caller's device row id (`SecureDeviceModel.id`).
|
|
57
|
+
* @param body - The KeyPackage bundles to upload (base64 KeyPackage + ref + ciphersuite).
|
|
58
|
+
* @returns How many KeyPackages the server accepted and stored.
|
|
59
|
+
*/
|
|
60
|
+
publishKeyPackages(deviceId: string, body: PublishKeyPackagesBody): Promise<number>;
|
|
61
|
+
/** Unconsumed + unexpired KeyPackage count for the caller's device (replenishment signal). */
|
|
62
|
+
keyPackageCount(deviceId: string): Promise<number>;
|
|
63
|
+
/**
|
|
64
|
+
* Atomically claim one KeyPackage for a TARGET device (to add it to a group).
|
|
65
|
+
* Throws on `409 secure-chat/key-packages-exhausted` — caller should surface "device unavailable".
|
|
66
|
+
*/
|
|
67
|
+
claimKeyPackage(targetDeviceId: string): Promise<SecureKeyPackageClaim>;
|
|
68
|
+
/**
|
|
69
|
+
* Register a new secure conversation on the blind DS, relaying the client-built Welcomes.
|
|
70
|
+
*
|
|
71
|
+
* @param body - The conversation type, base64 MLS group id, member user ids, and targeted Welcomes.
|
|
72
|
+
* @returns The created conversation row.
|
|
73
|
+
*/
|
|
74
|
+
createConversation(body: CreateSecureConversationBody): Promise<SecureConversationModel>;
|
|
75
|
+
/**
|
|
76
|
+
* List the caller's secure conversations, newest activity first.
|
|
77
|
+
*
|
|
78
|
+
* @param params - Optional `limit` and opaque `cursor` for keyset pagination.
|
|
79
|
+
* @returns A page of conversations plus a `hasMore` flag for further paging.
|
|
80
|
+
*/
|
|
81
|
+
listConversations(params?: {
|
|
82
|
+
limit?: number;
|
|
83
|
+
cursor?: string;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
conversations: SecureConversationModel[];
|
|
86
|
+
hasMore: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Fetch a single conversation by id (including the caller's membership + unread metadata).
|
|
90
|
+
*
|
|
91
|
+
* @param conversationId - The conversation's id.
|
|
92
|
+
* @returns The conversation row.
|
|
93
|
+
*/
|
|
94
|
+
getConversation(conversationId: string): Promise<SecureConversationModel>;
|
|
95
|
+
/** Mark the conversation read up to now (sets the member's `last_read_at`). */
|
|
96
|
+
markRead(conversationId: string): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Add a user to a conversation by relaying the caller-built Commit + per-device Welcomes.
|
|
99
|
+
*
|
|
100
|
+
* @param conversationId - The conversation to add the member to.
|
|
101
|
+
* @param body - The new member's user id, the broadcast Commit, and the targeted Welcomes.
|
|
102
|
+
* @returns The newly created membership row.
|
|
103
|
+
*/
|
|
104
|
+
addMember(conversationId: string, body: AddSecureMemberBody): Promise<SecureConversationMemberModel>;
|
|
105
|
+
/** Admin remove, or self-leave. Returns the new epoch. Throws on `409 epoch-conflict` (rebase). */
|
|
106
|
+
removeMember(conversationId: string, userId: string, body: RemoveSecureMemberBody): Promise<{
|
|
107
|
+
epoch: string;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* Send one MLS application message (opaque ciphertext) to a conversation.
|
|
111
|
+
*
|
|
112
|
+
* @param conversationId - The target conversation.
|
|
113
|
+
* @param body - The base64 ciphertext, its epoch, the sending device id, and optional content type.
|
|
114
|
+
* @returns The stored message row.
|
|
115
|
+
*/
|
|
116
|
+
sendMessage(conversationId: string, body: SendSecureMessageBody): Promise<SecureMessageModel>;
|
|
117
|
+
/**
|
|
118
|
+
* Page through a conversation's messages, newest first (still encrypted — decrypt client-side).
|
|
119
|
+
*
|
|
120
|
+
* @param conversationId - The conversation to read.
|
|
121
|
+
* @param params - Optional `limit` and a `before` cursor (page older than this point).
|
|
122
|
+
* @returns A page of message rows plus a `hasMore` flag.
|
|
123
|
+
*/
|
|
124
|
+
listMessages(conversationId: string, params?: {
|
|
125
|
+
limit?: number;
|
|
126
|
+
before?: string;
|
|
127
|
+
}): Promise<{
|
|
128
|
+
messages: SecureMessageModel[];
|
|
129
|
+
hasMore: boolean;
|
|
130
|
+
}>;
|
|
131
|
+
/**
|
|
132
|
+
* The durable catch-up path: union of targeted Welcomes + broadcast Commits/Proposals for the
|
|
133
|
+
* caller's device, ordered by `seq > since`. Page by passing the last `seq` you processed.
|
|
134
|
+
*/
|
|
135
|
+
fetchHandshakes(deviceId: string, params?: {
|
|
136
|
+
since?: string;
|
|
137
|
+
limit?: number;
|
|
138
|
+
}): Promise<{
|
|
139
|
+
handshakes: SecureHandshakeModel[];
|
|
140
|
+
hasMore: boolean;
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* Upload (replace) the caller's passphrase-encrypted key backup blob. The server stores it opaquely
|
|
144
|
+
* and cannot decrypt it.
|
|
145
|
+
*
|
|
146
|
+
* @param body - The encrypted blob, nonce, KDF + cipher descriptors, and version.
|
|
147
|
+
* @returns The server's `updatedAt` timestamp for the stored backup.
|
|
148
|
+
*/
|
|
149
|
+
uploadKeyBackup(body: UploadKeyBackupBody): Promise<{
|
|
150
|
+
updatedAt: string;
|
|
151
|
+
}>;
|
|
152
|
+
/** Returns the caller's passphrase-encrypted backup blob, or null on 404. */
|
|
153
|
+
getKeyBackup(deviceId?: string): Promise<SecureKeyBackupModel | null>;
|
|
154
|
+
}
|