@agora-sdk/secure-chat-core 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. package/dist/cjs/context/secure-chat-context.d.ts +24 -18
  2. package/dist/cjs/context/secure-chat-context.js +42 -23
  3. package/dist/cjs/context/secure-chat-context.js.map +1 -1
  4. package/dist/cjs/contract/index.js +2 -2
  5. package/dist/cjs/contract/index.js.map +1 -1
  6. package/dist/cjs/hooks/useSecureConversations.d.ts +1 -1
  7. package/dist/cjs/hooks/useSecureConversations.js +9 -7
  8. package/dist/cjs/hooks/useSecureConversations.js.map +1 -1
  9. package/dist/cjs/hooks/useSecureDevice.d.ts +14 -13
  10. package/dist/cjs/hooks/useSecureDevice.js +58 -25
  11. package/dist/cjs/hooks/useSecureDevice.js.map +1 -1
  12. package/dist/cjs/hooks/useSecureMessages.d.ts +10 -11
  13. package/dist/cjs/hooks/useSecureMessages.js +89 -21
  14. package/dist/cjs/hooks/useSecureMessages.js.map +1 -1
  15. package/dist/cjs/index.d.ts +18 -14
  16. package/dist/cjs/index.js +23 -19
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/package.json +1 -0
  19. package/dist/cjs/persistence/memory-store.d.ts +9 -0
  20. package/dist/cjs/persistence/memory-store.js +27 -0
  21. package/dist/cjs/persistence/memory-store.js.map +1 -0
  22. package/dist/cjs/persistence/repository.d.ts +36 -0
  23. package/dist/cjs/persistence/repository.js +72 -0
  24. package/dist/cjs/persistence/repository.js.map +1 -0
  25. package/dist/cjs/persistence/store.d.ts +11 -0
  26. package/dist/cjs/persistence/store.js +8 -0
  27. package/dist/cjs/persistence/store.js.map +1 -0
  28. package/dist/cjs/transport/rest.d.ts +1 -1
  29. package/dist/cjs/transport/socket.d.ts +1 -1
  30. package/dist/esm/context/secure-chat-context.d.ts +24 -18
  31. package/dist/esm/context/secure-chat-context.js +41 -22
  32. package/dist/esm/context/secure-chat-context.js.map +1 -1
  33. package/dist/esm/contract/index.js +2 -2
  34. package/dist/esm/contract/index.js.map +1 -1
  35. package/dist/esm/hooks/useSecureConversations.d.ts +1 -1
  36. package/dist/esm/hooks/useSecureConversations.js +6 -4
  37. package/dist/esm/hooks/useSecureConversations.js.map +1 -1
  38. package/dist/esm/hooks/useSecureDevice.d.ts +14 -13
  39. package/dist/esm/hooks/useSecureDevice.js +55 -22
  40. package/dist/esm/hooks/useSecureDevice.js.map +1 -1
  41. package/dist/esm/hooks/useSecureMessages.d.ts +10 -11
  42. package/dist/esm/hooks/useSecureMessages.js +86 -18
  43. package/dist/esm/hooks/useSecureMessages.js.map +1 -1
  44. package/dist/esm/index.d.ts +18 -14
  45. package/dist/esm/index.js +9 -7
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/persistence/memory-store.d.ts +9 -0
  48. package/dist/esm/persistence/memory-store.js +23 -0
  49. package/dist/esm/persistence/memory-store.js.map +1 -0
  50. package/dist/esm/persistence/repository.d.ts +36 -0
  51. package/dist/esm/persistence/repository.js +68 -0
  52. package/dist/esm/persistence/repository.js.map +1 -0
  53. package/dist/esm/persistence/store.d.ts +11 -0
  54. package/dist/esm/persistence/store.js +7 -0
  55. package/dist/esm/persistence/store.js.map +1 -0
  56. package/dist/esm/transport/rest.d.ts +1 -1
  57. package/dist/esm/transport/socket.d.ts +1 -1
  58. package/package.json +3 -3
@@ -1,36 +1,33 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // SecureChatProvider — wires the transport + crypto for the secure-chat hooks.
2
+ // SecureChatProvider — wires transport + crypto + persistence for the secure-chat hooks.
3
3
  //
4
4
  // Sits INSIDE a ReplykeProvider: by default it resolves the API base URL and socket origin from
5
- // @agora-sdk/core's runtime singletons (getApiBaseUrl / getSocketUrl), so whatever `baseUrl` the
6
- // app set on ReplykeProvider is honored here too. Crypto is injected (a `SecureChatCrypto`), keeping
7
- // core platform- and library-agnostic the web/native packages supply the concrete MLS impl.
8
- import { createContext, useContext, useEffect, useMemo, useRef } from "react";
5
+ // @agora-sdk/core's runtime singletons (getApiBaseUrl / getSocketUrl). Crypto AND the persistence
6
+ // store are injected, keeping core platform- and library-agnostic. The provider builds a typed
7
+ // SecureChatRepository over the store plus a cached resolveGroup/rememberGroup so the hooks become
8
+ // self-sufficient (no need to thread a GroupHandle in by hand).
9
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from "react";
9
10
  import { getApiBaseUrl, getSocketUrl } from "@agora-sdk/core";
10
- import { SecureChatRestClient } from "../transport/rest";
11
- import { SecureChatSocketClient } from "../transport/socket";
11
+ import { SecureChatRestClient } from "../transport/rest.js";
12
+ import { SecureChatSocketClient } from "../transport/socket.js";
13
+ import { MemoryStore } from "../persistence/memory-store.js";
14
+ import { SecureChatRepository } from "../persistence/repository.js";
12
15
  const SecureChatContext = createContext(null);
13
16
  /**
14
- * Provides the secure-chat transport + crypto to the `useSecure*` hooks. Render it inside a
15
- * `ReplykeProvider`: by default it inherits the API base URL and socket origin from
16
- * `@agora-sdk/core`'s runtime, and disconnects the socket on unmount.
17
+ * Provides secure-chat transport, crypto, and persistence to the `useSecure*` hooks. Render inside a
18
+ * `ReplykeProvider`; disconnects the socket on unmount.
17
19
  *
18
- * @param props - {@link SecureChatProviderProps} — the injected crypto, project id, token, and
19
- * optional base/socket URL overrides.
20
+ * @param props - {@link SecureChatProviderProps}.
20
21
  * @returns A context provider wrapping `children`.
21
22
  *
22
23
  * @example
23
24
  * ```tsx
24
- * <ReplykeProvider projectId={projectId} baseUrl={baseUrl}>
25
- * <SecureChatProvider crypto={crypto} projectId={projectId} accessToken={token}>
26
- * <Chat />
27
- * </SecureChatProvider>
28
- * </ReplykeProvider>
25
+ * <SecureChatProvider crypto={crypto} projectId={projectId} store={createIndexedDBStore()} accessToken={token}>
26
+ * <Chat />
27
+ * </SecureChatProvider>
29
28
  * ```
30
29
  */
31
- export function SecureChatProvider({ crypto, projectId, accessToken, getAccessToken, baseUrl, socketUrl, children, }) {
32
- // Keep the latest token in a ref so the per-request resolver always returns the current value
33
- // without rebuilding the transport clients on every token change.
30
+ export function SecureChatProvider({ crypto, projectId, store, accessToken, getAccessToken, baseUrl, socketUrl, children, }) {
34
31
  const tokenRef = useRef(accessToken);
35
32
  tokenRef.current = accessToken;
36
33
  const resolveToken = useMemo(() => getAccessToken ?? (() => tokenRef.current), [getAccessToken]);
@@ -44,16 +41,38 @@ export function SecureChatProvider({ crypto, projectId, accessToken, getAccessTo
44
41
  getAccessToken: resolveToken,
45
42
  getSocketUrl: () => socketUrl ?? getSocketUrl(),
46
43
  }), [projectId, resolveToken, socketUrl]);
44
+ const resolvedStore = useMemo(() => store ?? new MemoryStore(), [store]);
45
+ const repo = useMemo(() => new SecureChatRepository(resolvedStore), [resolvedStore]);
46
+ // In-memory GroupHandle cache, keyed by conversationId. Survives re-renders via the ref.
47
+ // NOTE: not cleared on a `store` prop swap — a store change in practice means a new provider
48
+ // instance (fresh cache), not a live prop change on the same mounted provider.
49
+ const groupCache = useRef(new Map());
50
+ const resolveGroup = useCallback(async (conversationId) => {
51
+ const cached = groupCache.current.get(conversationId);
52
+ if (cached)
53
+ return cached;
54
+ const bytes = await repo.loadGroupState(conversationId);
55
+ if (!bytes)
56
+ return null;
57
+ const handle = await crypto.importGroupState(bytes);
58
+ groupCache.current.set(conversationId, handle);
59
+ return handle;
60
+ }, [repo, crypto]);
61
+ const rememberGroup = useCallback(async (conversationId, handle) => {
62
+ groupCache.current.set(conversationId, handle);
63
+ const bytes = await crypto.exportGroupState(handle);
64
+ await repo.saveGroupState(conversationId, bytes);
65
+ }, [repo, crypto]);
47
66
  useEffect(() => {
48
67
  return () => socket.disconnect();
49
68
  }, [socket]);
50
- const value = useMemo(() => ({ rest, socket, crypto, projectId }), [rest, socket, crypto, projectId]);
69
+ const value = useMemo(() => ({ rest, socket, crypto, repo, resolveGroup, rememberGroup, projectId }), [rest, socket, crypto, repo, resolveGroup, rememberGroup, projectId]);
51
70
  return _jsx(SecureChatContext.Provider, { value: value, children: children });
52
71
  }
53
72
  /**
54
73
  * Access the nearest {@link SecureChatContextValue}.
55
74
  *
56
- * @returns The shared rest/socket/crypto/projectId for this provider subtree.
75
+ * @returns The shared rest/socket/crypto/repo/resolveGroup/projectId for this provider subtree.
57
76
  * @throws {Error} When called outside a `<SecureChatProvider>`.
58
77
  */
59
78
  export function useSecureChat() {
@@ -1 +1 @@
1
- {"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,EAAE;AACF,gGAAgG;AAChG,iGAAiG;AACjG,qGAAqG;AACrG,8FAA8F;AAE9F,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAiB7D,MAAM,iBAAiB,GAAG,aAAa,CAAgC,IAAI,CAAC,CAAC;AAmB7E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,MAAM,EACN,SAAS,EACT,WAAW,EACX,cAAc,EACd,OAAO,EACP,SAAS,EACT,QAAQ,GACgB;IACxB,8FAA8F;IAC9F,kEAAkE;IAClE,MAAM,QAAQ,GAAG,MAAM,CAAqB,WAAW,CAAC,CAAC;IACzD,QAAQ,CAAC,OAAO,GAAG,WAAW,CAAC;IAE/B,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,cAAc,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAChD,CAAC,cAAc,CAAC,CACjB,CAAC;IAEF,MAAM,IAAI,GAAG,OAAO,CAClB,GAAG,EAAE,CACH,IAAI,oBAAoB,CAAC;QACvB,SAAS;QACT,cAAc,EAAE,YAAY;QAC5B,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,IAAI,aAAa,EAAE;KAC7C,CAAC,EACJ,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,CAAC,CACnC,CAAC;IAEF,MAAM,MAAM,GAAG,OAAO,CACpB,GAAG,EAAE,CACH,IAAI,sBAAsB,CAAC;QACzB,SAAS;QACT,cAAc,EAAE,YAAY;QAC5B,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,IAAI,YAAY,EAAE;KAChD,CAAC,EACJ,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,CAAC,CACrC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACnC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,EAC3C,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAClC,CAAC;IAEF,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC1C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"secure-chat-context.js","sourceRoot":"","sources":["../../../src/context/secure-chat-context.tsx"],"names":[],"mappings":"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"}
@@ -1,11 +1,11 @@
1
- // Secure-chat wire contract — stand-in for @agora/contract (the Apache-2.0 server contract).
1
+ // Secure-chat wire contract — stand-in for @agora-server/contract (the Apache-2.0 server contract).
2
2
  //
3
3
  // Mirrors the response models + request bodies of agora-server's
4
4
  // `packages/contract/src/secure-chat.ts`. That package is the source of truth (zod + TS); the
5
5
  // client only needs the TypeScript shapes, so this copy is types-only.
6
6
  //
7
7
  // ⚠️ Keep this byte-faithful to the server contract. The arrow is SDK → contract: once
8
- // `@agora/contract` is published, DELETE this file and `import type { ... } from "@agora/contract"`.
8
+ // `@agora-server/contract` is published, DELETE this file and `import type { ... } from "@agora-server/contract"`.
9
9
  // Do NOT publish a separate `@agora-sdk/secure-chat-contract` (that would invert the dependency —
10
10
  // see STATUS.md). Do not let this copy drift in the meantime.
11
11
  //
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contract/index.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,EAAE;AACF,iEAAiE;AACjE,8FAA8F;AAC9F,uEAAuE;AACvE,EAAE;AACF,uFAAuF;AACvF,qGAAqG;AACrG,kGAAkG;AAClG,8DAA8D;AAC9D,EAAE;AACF,gGAAgG;AAChG,0FAA0F"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/contract/index.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,EAAE;AACF,iEAAiE;AACjE,8FAA8F;AAC9F,uEAAuE;AACvE,EAAE;AACF,uFAAuF;AACvF,mHAAmH;AACnH,kGAAkG;AAClG,8DAA8D;AAC9D,EAAE;AACF,gGAAgG;AAChG,0FAA0F"}
@@ -1,4 +1,4 @@
1
- import { SecureConversationModel } from "../contract";
1
+ import { SecureConversationModel } from "../contract/index.js";
2
2
  /** The state and actions returned by {@link useSecureConversations}. */
3
3
  export interface UseSecureConversationsValues {
4
4
  /** The loaded conversations, newest activity first. */
@@ -4,8 +4,8 @@
4
4
  // POST /conversations with the Welcomes targeted to each peer device. The MLS group secrets stay on
5
5
  // the client; the server only stores ciphertext metadata.
6
6
  import { useCallback, useEffect, useState } from "react";
7
- import { toBase64, fromBase64 } from "../util/base64";
8
- import { useSecureChat } from "../context/secure-chat-context";
7
+ import { toBase64, fromBase64 } from "../util/base64.js";
8
+ import { useSecureChat } from "../context/secure-chat-context.js";
9
9
  /**
10
10
  * List the caller's secure conversations and start new direct messages.
11
11
  *
@@ -23,7 +23,7 @@ import { useSecureChat } from "../context/secure-chat-context";
23
23
  * ```
24
24
  */
25
25
  export function useSecureConversations() {
26
- const { rest, crypto, socket } = useSecureChat();
26
+ const { rest, crypto, socket, rememberGroup } = useSecureChat();
27
27
  const [conversations, setConversations] = useState([]);
28
28
  const [cursor, setCursor] = useState(undefined);
29
29
  const [hasMore, setHasMore] = useState(true);
@@ -81,9 +81,11 @@ export function useSecureConversations() {
81
81
  epoch: group.epoch.toString(),
82
82
  })),
83
83
  });
84
+ // Persist + cache the creator's MLS group handle so messages resolve after a reload.
85
+ await rememberGroup(conversation.id, group);
84
86
  setConversations((prev) => [conversation, ...prev.filter((c) => c.id !== conversation.id)]);
85
87
  return conversation;
86
- }, [rest, crypto]);
88
+ }, [rest, crypto, rememberGroup]);
87
89
  useEffect(() => {
88
90
  refresh();
89
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1 +1 @@
1
- {"version":3,"file":"useSecureConversations.js","sourceRoot":"","sources":["../../../src/hooks/useSecureConversations.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSecureConversations.js","sourceRoot":"","sources":["../../../src/hooks/useSecureConversations.tsx"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
- import { SecureDeviceModel } from "../contract";
1
+ import { SecureDeviceModel } from "../contract/index.js";
2
2
  /** Options for {@link useSecureDevice}. */
3
3
  export interface UseSecureDeviceOptions {
4
- /** Stable, persisted client device id. Generated if omitted (persist it in the platform layer). */
4
+ /** Stable, persisted client device id. Generated (and persisted) if omitted. A previously
5
+ * persisted device's id takes precedence over this value when one exists on mount. */
5
6
  deviceId?: string;
6
7
  /** MLS ciphersuite to register under. Defaults to the crypto implementation's preferred suite. */
7
8
  ciphersuite?: number;
@@ -14,13 +15,15 @@ export interface UseSecureDeviceOptions {
14
15
  export interface UseSecureDeviceValues {
15
16
  /** The registered device row (its `.id` is the uuid used as targetDeviceId everywhere). */
16
17
  device: SecureDeviceModel | null;
18
+ /** True until the initial persisted-device load settles. */
19
+ loading: boolean;
17
20
  /** True while {@link UseSecureDeviceValues.register} is in flight. */
18
21
  registering: boolean;
19
- /** The last error thrown by registration or replenishment, or `null`. */
22
+ /** The last error thrown by load, registration, or replenishment, or `null`. */
20
23
  error: unknown;
21
24
  /** Last known count of unconsumed KeyPackages, or `null` until refreshed. */
22
25
  keyPackagesAvailable: number | null;
23
- /** Generate identity + register (idempotent server-side on (userId, deviceId)). */
26
+ /** Generate identity + register (idempotent server-side on (userId, deviceId)) and persist it. */
24
27
  register: () => Promise<SecureDeviceModel>;
25
28
  /** Generate + publish `count` fresh KeyPackages (default = keyPackageTarget). */
26
29
  publishKeyPackages: (count?: number) => Promise<number>;
@@ -28,20 +31,18 @@ export interface UseSecureDeviceValues {
28
31
  refreshKeyPackageCount: () => Promise<number>;
29
32
  }
30
33
  /**
31
- * Register this client as an MLS device (one device = one leaf) and keep its KeyPackages stocked.
34
+ * Register this client as an MLS device, persist its identity, and keep KeyPackages stocked.
32
35
  *
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.
36
+ * On mount it loads any persisted device and re-hydrates the crypto identity (stable id, no
37
+ * re-register). When none exists, await {@link UseSecureDeviceValues.register}.
37
38
  *
38
- * @param options - {@link UseSecureDeviceOptions} — device id, ciphersuite, and replenishment tuning.
39
- * @returns {@link UseSecureDeviceValues} — the device row, status flags, and register/publish actions.
39
+ * @param options - {@link UseSecureDeviceOptions}.
40
+ * @returns {@link UseSecureDeviceValues}.
40
41
  *
41
42
  * @example
42
43
  * ```tsx
43
- * const { device, register } = useSecureDevice({ keyPackageTarget: 20 });
44
- * useEffect(() => { register(); }, []);
44
+ * const { device, loading, register } = useSecureDevice();
45
+ * useEffect(() => { if (!loading && !device) register(); }, [loading, device, register]);
45
46
  * ```
46
47
  */
47
48
  export declare function useSecureDevice(options?: UseSecureDeviceOptions): UseSecureDeviceValues;
@@ -1,17 +1,15 @@
1
- // useSecureDevice — register this client as an MLS device (leaf) and keep its KeyPackages topped up.
1
+ // useSecureDevice — register this client as an MLS device (leaf), persist its identity, and keep its
2
+ // KeyPackages topped up.
2
3
  //
3
- // Flow (server spec §14): generateDeviceIdentity POST /devices publish a batch of KeyPackages;
4
- // replenish on the `secure:key-packages-low` realtime signal or via the count endpoint.
5
- //
6
- // NOTE: the device's PRIVATE state (`privateState` from generateDeviceIdentity, and MLS group
7
- // state) must be persisted by the platform layer (IndexedDB on web, keystore on native — Phase 2/3).
8
- // Core only performs registration + relay; it does not persist secrets.
4
+ // On mount it re-hydrates a persisted device (stable deviceId + private state via
5
+ // crypto.importDeviceState) so a reload does NOT mint a new identity. register() generates, registers
6
+ // on the server, and persists. Replenishes on the `secure:key-packages-low` realtime signal.
9
7
  import { useCallback, useEffect, useRef, useState } from "react";
10
- import { toBase64 } from "../util/base64";
11
- import { useSecureChat } from "../context/secure-chat-context";
8
+ import { toBase64 } from "../util/base64.js";
9
+ import { useSecureChat } from "../context/secure-chat-context.js";
12
10
  /**
13
11
  * Mint a device id when the caller doesn't supply one — `crypto.randomUUID()` when available, else a
14
- * non-cryptographic timestamp+random fallback. The platform layer should supply a persisted id.
12
+ * non-cryptographic timestamp+random fallback.
15
13
  *
16
14
  * @returns A fresh device id string.
17
15
  */
@@ -19,34 +17,64 @@ function newDeviceId() {
19
17
  const g = globalThis.crypto;
20
18
  if (g?.randomUUID)
21
19
  return g.randomUUID();
22
- // Platform layer should supply a persisted, stable device id; this is a non-crypto fallback.
23
20
  return `dev-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
24
21
  }
25
22
  /**
26
- * Register this client as an MLS device (one device = one leaf) and keep its KeyPackages stocked.
23
+ * Register this client as an MLS device, persist its identity, and keep KeyPackages stocked.
27
24
  *
28
- * On {@link UseSecureDeviceValues.register} it generates a device identity, POSTs it to `/devices`,
29
- * and (when `autoReplenish` is on) republishes KeyPackages whenever the server emits
30
- * `secure:key-packages-low` for this device. The device's private key material must be persisted by
31
- * the platform layer — this hook only handles registration and relay.
25
+ * On mount it loads any persisted device and re-hydrates the crypto identity (stable id, no
26
+ * re-register). When none exists, await {@link UseSecureDeviceValues.register}.
32
27
  *
33
- * @param options - {@link UseSecureDeviceOptions} — device id, ciphersuite, and replenishment tuning.
34
- * @returns {@link UseSecureDeviceValues} — the device row, status flags, and register/publish actions.
28
+ * @param options - {@link UseSecureDeviceOptions}.
29
+ * @returns {@link UseSecureDeviceValues}.
35
30
  *
36
31
  * @example
37
32
  * ```tsx
38
- * const { device, register } = useSecureDevice({ keyPackageTarget: 20 });
39
- * useEffect(() => { register(); }, []);
33
+ * const { device, loading, register } = useSecureDevice();
34
+ * useEffect(() => { if (!loading && !device) register(); }, [loading, device, register]);
40
35
  * ```
41
36
  */
42
37
  export function useSecureDevice(options = {}) {
43
- const { crypto, rest, socket } = useSecureChat();
38
+ const { crypto, rest, socket, repo } = useSecureChat();
44
39
  const { ciphersuite, keyPackageTarget = 20, autoReplenish = true } = options;
45
40
  const [device, setDevice] = useState(null);
41
+ const [loading, setLoading] = useState(true);
46
42
  const [registering, setRegistering] = useState(false);
47
43
  const [error, setError] = useState(null);
48
44
  const [keyPackagesAvailable, setKeyPackagesAvailable] = useState(null);
49
45
  const deviceIdRef = useRef(options.deviceId ?? newDeviceId());
46
+ const registerStartedRef = useRef(false);
47
+ // On mount: re-hydrate a persisted device (stable id + private state). No persisted device ⇒
48
+ // first-run; the app calls register().
49
+ useEffect(() => {
50
+ let alive = true;
51
+ (async () => {
52
+ const persisted = await repo.loadDevice();
53
+ if (!alive || registerStartedRef.current) {
54
+ setLoading(false);
55
+ return;
56
+ }
57
+ if (persisted) {
58
+ await crypto.importDeviceState(persisted.deviceState);
59
+ // register() may have started during the await — don't overwrite its identity.
60
+ if (!alive || registerStartedRef.current) {
61
+ setLoading(false);
62
+ return;
63
+ }
64
+ deviceIdRef.current = persisted.deviceId;
65
+ setDevice(persisted.device);
66
+ }
67
+ setLoading(false);
68
+ })().catch((err) => {
69
+ if (!alive)
70
+ return;
71
+ setError(err);
72
+ setLoading(false);
73
+ });
74
+ return () => {
75
+ alive = false;
76
+ };
77
+ }, [repo, crypto]);
50
78
  const publishKeyPackages = useCallback(async (count = keyPackageTarget) => {
51
79
  if (!device)
52
80
  throw new Error("Register the device before publishing KeyPackages.");
@@ -69,6 +97,7 @@ export function useSecureDevice(options = {}) {
69
97
  return available;
70
98
  }, [rest, device]);
71
99
  const register = useCallback(async () => {
100
+ registerStartedRef.current = true;
72
101
  setRegistering(true);
73
102
  setError(null);
74
103
  try {
@@ -82,6 +111,9 @@ export function useSecureDevice(options = {}) {
82
111
  credential: toBase64(identity.credential),
83
112
  ciphersuite: identity.ciphersuite,
84
113
  });
114
+ const deviceState = await crypto.exportDeviceState();
115
+ await repo.saveDevice({ deviceId: identity.deviceId, deviceState, device: registered });
116
+ deviceIdRef.current = identity.deviceId;
85
117
  setDevice(registered);
86
118
  return registered;
87
119
  }
@@ -92,7 +124,7 @@ export function useSecureDevice(options = {}) {
92
124
  finally {
93
125
  setRegistering(false);
94
126
  }
95
- }, [crypto, rest, ciphersuite]);
127
+ }, [crypto, rest, repo, ciphersuite]);
96
128
  // Auto-replenish on the server's low-water signal for this device.
97
129
  useEffect(() => {
98
130
  if (!autoReplenish || !device)
@@ -106,6 +138,7 @@ export function useSecureDevice(options = {}) {
106
138
  }, [autoReplenish, device, socket, publishKeyPackages]);
107
139
  return {
108
140
  device,
141
+ loading,
109
142
  registering,
110
143
  error,
111
144
  keyPackagesAvailable,
@@ -1 +1 @@
1
- {"version":3,"file":"useSecureDevice.js","sourceRoot":"","sources":["../../../src/hooks/useSecureDevice.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSecureDevice.js","sourceRoot":"","sources":["../../../src/hooks/useSecureDevice.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { SecureMessageModel } from "../contract";
1
+ import { SecureMessageModel } from "../contract/index.js";
2
2
  import { GroupHandle } from "@agora-sdk/secure-chat-crypto";
3
3
  /** A stored message paired with its decrypted text (when a group handle is available). */
4
4
  export interface DecryptedSecureMessage {
@@ -9,9 +9,9 @@ export interface DecryptedSecureMessage {
9
9
  }
10
10
  /** Options for {@link useSecureMessages}. */
11
11
  export interface UseSecureMessagesOptions {
12
- /** The MLS group handle for this conversation (from the platform persistence layer). */
12
+ /** Override the MLS group handle. Defaults to the persisted handle via `resolveGroup`. */
13
13
  group?: GroupHandle;
14
- /** The caller's device row id required to send (the server verifies it belongs to the caller). */
14
+ /** Override the sender device row id. Defaults to the persisted device's `.id`. */
15
15
  senderDeviceId?: string;
16
16
  }
17
17
  /** The state and actions returned by {@link useSecureMessages}. */
@@ -28,24 +28,23 @@ export interface UseSecureMessagesValues {
28
28
  loadMore: () => Promise<void>;
29
29
  /** Reload from the newest message, replacing the current list. */
30
30
  refresh: () => Promise<void>;
31
- /** Encrypt + send a text message. Requires `group` + `senderDeviceId`. */
31
+ /** Encrypt + send a text message. Requires a resolvable group + sender device. */
32
32
  sendMessage: (text: string) => Promise<void>;
33
33
  }
34
34
  /**
35
35
  * Load, decrypt, send, and live-receive messages in one secure conversation.
36
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.
37
+ * Auto-resolves the MLS group handle (via `resolveGroup`) and the sender device id (from the
38
+ * persisted device) unless overridden in `options`. Joins the conversation socket room for live
39
+ * `secure:message` events.
41
40
  *
42
41
  * @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`.
42
+ * @param options - {@link UseSecureMessagesOptions}.
43
+ * @returns {@link UseSecureMessagesValues}.
45
44
  *
46
45
  * @example
47
46
  * ```tsx
48
- * const { messages, sendMessage } = useSecureMessages(conversationId, { group, senderDeviceId });
47
+ * const { messages, sendMessage } = useSecureMessages(conversationId);
49
48
  * await sendMessage("hello 💜");
50
49
  * ```
51
50
  */
@@ -1,39 +1,86 @@
1
1
  // useSecureMessages — load, decrypt, send, and live-receive messages in a secure conversation.
2
2
  //
3
- // Encryption/decryption runs through the injected `SecureChatCrypto` against the conversation's MLS
4
- // `GroupHandle`. Resolving conversationId → GroupHandle is owned by the platform persistence layer
5
- // (Phase 2: IndexedDB group state via processWelcome/importGroupState), so the caller passes the
6
- // handle in. Without it, ciphertext is still listed/received but left undecrypted (`plaintext: null`)
7
- // and sending is disabled keeping the transport usable ahead of the crypto wiring.
8
- import { useCallback, useEffect, useState } from "react";
9
- import { toBase64, fromBase64, utf8ToBytes, bytesToUtf8 } from "../util/base64";
10
- import { useSecureChat } from "../context/secure-chat-context";
3
+ // Self-sufficient once a store is wired: the MLS GroupHandle is auto-resolved from persistence via
4
+ // resolveGroup, and senderDeviceId is read from the persisted device. Both stay overridable through
5
+ // options for advanced use. Without a resolvable handle, ciphertext is still listed/received
6
+ // (plaintext: null) and sending is disabled.
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { toBase64, fromBase64, utf8ToBytes, bytesToUtf8 } from "../util/base64.js";
9
+ import { useSecureChat } from "../context/secure-chat-context.js";
11
10
  /**
12
11
  * Load, decrypt, send, and live-receive messages in one secure conversation.
13
12
  *
14
- * Decryption runs through the injected `SecureChatCrypto` against the conversation's MLS
15
- * `GroupHandle`. Without a `group` in `options`, ciphertext is still listed and received (as
16
- * `plaintext: null`) and sending is disabled — letting the transport work ahead of the crypto
17
- * wiring. Joins the conversation's socket room to receive `secure:message` events live.
13
+ * Auto-resolves the MLS group handle (via `resolveGroup`) and the sender device id (from the
14
+ * persisted device) unless overridden in `options`. Joins the conversation socket room for live
15
+ * `secure:message` events.
18
16
  *
19
17
  * @param conversationId - The conversation to read and send within.
20
- * @param options - {@link UseSecureMessagesOptions} — the MLS `group` handle and `senderDeviceId`.
21
- * @returns {@link UseSecureMessagesValues} — the message list, paging state, and `sendMessage`.
18
+ * @param options - {@link UseSecureMessagesOptions}.
19
+ * @returns {@link UseSecureMessagesValues}.
22
20
  *
23
21
  * @example
24
22
  * ```tsx
25
- * const { messages, sendMessage } = useSecureMessages(conversationId, { group, senderDeviceId });
23
+ * const { messages, sendMessage } = useSecureMessages(conversationId);
26
24
  * await sendMessage("hello 💜");
27
25
  * ```
28
26
  */
29
27
  export function useSecureMessages(conversationId, options = {}) {
30
- const { rest, crypto, socket } = useSecureChat();
31
- const { group, senderDeviceId } = options;
28
+ const { rest, crypto, socket, repo, resolveGroup } = useSecureChat();
32
29
  const [messages, setMessages] = useState([]);
33
30
  const [before, setBefore] = useState(undefined);
34
31
  const [hasMore, setHasMore] = useState(true);
35
32
  const [loading, setLoading] = useState(false);
36
33
  const [error, setError] = useState(null);
34
+ const [group, setGroup] = useState(options.group ?? null);
35
+ const [senderDeviceId, setSenderDeviceId] = useState(options.senderDeviceId);
36
+ // Latest messages, read by the "decrypt history once the group resolves" effect below without
37
+ // making `messages` one of its deps (which would loop).
38
+ const messagesRef = useRef(messages);
39
+ messagesRef.current = messages;
40
+ // Resolve the group handle: explicit override, else persisted state.
41
+ useEffect(() => {
42
+ if (options.group) {
43
+ setGroup(options.group);
44
+ return;
45
+ }
46
+ // Clear any stale handle from the previous conversation before re-resolving, so live messages
47
+ // for the new conversation never decrypt against the old group during the async window.
48
+ setGroup(null);
49
+ let alive = true;
50
+ resolveGroup(conversationId)
51
+ .then((g) => {
52
+ if (alive)
53
+ setGroup(g);
54
+ })
55
+ .catch(() => {
56
+ if (alive)
57
+ setGroup(null);
58
+ });
59
+ return () => {
60
+ alive = false;
61
+ };
62
+ }, [options.group, conversationId, resolveGroup]);
63
+ // Resolve the sender device id: explicit override, else persisted device row.
64
+ useEffect(() => {
65
+ if (options.senderDeviceId) {
66
+ setSenderDeviceId(options.senderDeviceId);
67
+ return;
68
+ }
69
+ let alive = true;
70
+ repo
71
+ .loadDevice()
72
+ .then((d) => {
73
+ if (alive)
74
+ setSenderDeviceId(d?.device?.id ?? undefined);
75
+ })
76
+ .catch(() => {
77
+ if (alive)
78
+ setSenderDeviceId(undefined);
79
+ });
80
+ return () => {
81
+ alive = false;
82
+ };
83
+ }, [options.senderDeviceId, repo]);
37
84
  const decrypt = useCallback(async (model) => {
38
85
  if (!group)
39
86
  return { model, plaintext: null };
@@ -78,6 +125,9 @@ export function useSecureMessages(conversationId, options = {}) {
78
125
  await load(false);
79
126
  }, [hasMore, loading, load]);
80
127
  const sendMessage = useCallback(async (text) => {
128
+ // Assumes the crypto identity is already hydrated (mount `useSecureDevice` under the same
129
+ // provider): the crypto layer tags the sender from the restored device identity, so after a
130
+ // reload the first send must wait for useSecureDevice's importDeviceState to complete.
81
131
  if (!group)
82
132
  throw new Error("Cannot send: no MLS group handle for this conversation.");
83
133
  if (!senderDeviceId)
@@ -95,13 +145,31 @@ export function useSecureMessages(conversationId, options = {}) {
95
145
  refresh();
96
146
  // eslint-disable-next-line react-hooks/exhaustive-deps
97
147
  }, [conversationId]);
148
+ // Decrypt history that was listed before the group handle resolved. On reload, the first page loads
149
+ // while resolveGroup is still in flight, so those rows come back `plaintext: null`; once the handle
150
+ // arrives (decrypt is recreated with it), re-decrypt the still-undecrypted rows in place — no
151
+ // re-fetch, scroll/pagination preserved.
152
+ useEffect(() => {
153
+ if (!group)
154
+ return;
155
+ if (!messagesRef.current.some((m) => m.plaintext === null))
156
+ return;
157
+ let alive = true;
158
+ Promise.all(messagesRef.current.map((m) => (m.plaintext === null ? decrypt(m.model) : Promise.resolve(m)))).then((next) => {
159
+ if (alive)
160
+ setMessages(next);
161
+ });
162
+ return () => {
163
+ alive = false;
164
+ };
165
+ }, [group, decrypt]);
98
166
  // Live receive: join the conversation room and decrypt inbound ciphertext.
99
167
  useEffect(() => {
100
168
  socket.joinConversation(conversationId);
101
169
  const off = socket.on("secure:message", (model) => {
102
170
  if (model.conversationId !== conversationId)
103
171
  return;
104
- decrypt(model).then((m) => setMessages((prev) => prev.some((p) => p.model.id === m.model.id) ? prev : [m, ...prev]));
172
+ decrypt(model).then((m) => setMessages((prev) => (prev.some((p) => p.model.id === m.model.id) ? prev : [m, ...prev])));
105
173
  });
106
174
  return off;
107
175
  }, [socket, conversationId, decrypt]);