Per-version upgrade notes. Skip to the version you're migrating FROM
— each section covers what changes you need to make to reach the
next published version. For the full feature history see
CHANGELOG.md.
TL;DR: No required changes; existing code keeps working.
New not_found / not_authorized / edit_window_expired
errors are now possible from editMessage / deleteMessage.
editMessageanddeleteMessagenow check the local store before shipping. They throwChatErrorwith new codes:"not_found"—targetIdisn't in the local store."not_authorized"— caller isn't the original sender."edit_window_expired"—editMessageonly; message older than 24h.
deleteConversationis deprecated — usedeleteConversationForMe(multi-device wipe) ordeleteConversationForEveryone(also wipe peer's side). Old method still works; emits a TS deprecation hint.- New events:
conversationDeletedBySelf,conversationDeletedByPeer. EDIT_WINDOW_MSexported (= 24 × 3600 × 1000).Conversation.lastMessage.editedAt/deletedAtformally documented as UI badge signals (they were already there in 0.8.1, this is JSDoc only).
-
Replace
deleteConversationcalls:// Before await chat.deleteConversation(peerUserId); // After (multi-device wipe of your side only) await chat.deleteConversationForMe(peerUserId); // Or (wipe both sides) await chat.deleteConversationForEveryone(peerUserId);
-
Gate the edit affordance on the 24h window (recipe):
import { EDIT_WINDOW_MS } from "@dtelecom/secure-chat-client"; const canEdit = msg.senderUserId === chat.currentUserId && msg.deletedAt == null && Date.now() - msg.sentAt < EDIT_WINDOW_MS;
Without this, the user clicks "Edit", types, hits send, gets a confusing
edit_window_expirederror. -
Subscribe to
conversationDeletedByPeerfor "Bob deleted the chat" toasts (recipe). -
Handle the new error codes in your edit/delete error branches (see
errors.md). -
Show "edited" / "deleted" badges using
StoredMessage.editedAtand.deletedAt(recipe).
SDK-only. No node change. New content events ride forward-compat (unknown receivers drop silently) — a 0.11 receiver paired with a 0.12 sender simply doesn't honor the delete or edit-window enforcement.
TL;DR: Requires dtelecom-node v1.1+ (livekit commit
380468a9). Coordinate the rollout. No SDK API change.
- The delivery model is now at-least-once with explicit
client-device ACK. The receiver SDK sends a new
chatEnvelopeAckWS frame after durably storing each envelope. The node returnschatSendResult.status = "live"only on that ack — not on the prior optimistic "writeJSON returned nil" signal. - Pre-decrypt envelope dedup (persisted LRU, capped at 1000 entries per scoped user). Required because the new at-least-once layer ships up to 5 copies of each envelope; without dedup, Olm's replay rejection would corrupt sessions.
- Sender-side retry within
fallback_timeout+ one post-webhook publish — seedelivery-semantics.md.
Nothing on the FE side. The new ack-after-store semantics are internal; existing listeners and methods behave the same.
If you have an in-house mock / proxy of the dtelecom node, it must
implement the new chatEnvelopeAck frame and the SignalDelivered
flow — see tasks/chat-client-ack.md for the design.
A 0.11 SDK against a v1.0 node still works — the chatEnvelopeAck
frame is ignored by the old node, and the old optimistic semantics
prevail. You don't get the new guarantees, but nothing breaks.
Conversely, a 0.10 SDK against a v1.1 node never sends ack, so the
node always falls back to webhook (slow but functional).
dTelecom deploys both in lockstep so neither degraded state should persist for real users.
TL;DR: Drop-in. Background discovery is on by default; can be disabled or rate-limit-tuned if needed.
- Fixed: mixed-msgType fanout. Outbound messages to peers with a
mix of existing-session + new devices now correctly split into
two
chatSendframes permsgType. Pre-fix, half the recipients silently failed to decrypt. SDK-internal — no API impact. - Added: background discovery polls
list_devicesafter each send; ships catch-up envelopes to newly-discovered peer devices.
Nothing required. If you want to tune:
const chat = await DTelecomSecureChat.connect({
// ...
backgroundDiscovery: true, // default
backgroundDiscoveryFloorMs: 30_000, // default (rate-limit floor)
});Set backgroundDiscovery: false to opt out (e.g. in tests). Set
backgroundDiscoveryFloorMs: 0 to remove the rate-limit (e.g. in
tests that need every send to discover instantly).
TL;DR (breaking): selfUserId is now a required
ConnectOptions field. Add it.
- All persisted SDK state lives under
u/<userId>/scope. Two distinct users on the same device are isolated. Sign-out → sign- in as a different user no longer leaks state. - New static
DTelecomSecureChat.wipeUserData(store, userId)for explicit sign-out cleanup. selfUserIdmoved from "parsed from JWT at connect time" to "required at construction time" — eliminates a window where storage could be written under the wrong scope.
-
Add
selfUserIdtoconnect():// Before const chat = await DTelecomSecureChat.connect({ apiBaseURL: "...", fetchChatToken: ..., fetchHttpBearer: ..., // selfUserId parsed from JWT }); // After const chat = await DTelecomSecureChat.connect({ apiBaseURL: "...", selfUserId: "alice", // explicit and required fetchChatToken: ..., fetchHttpBearer: ..., });
For dMeet:
selfUserIdis the Privydid:privy:...of the current user. -
Add a sign-out cleanup step:
async function signOut() { const userId = chat.currentUserId; await chat.disconnect(); if (userId) { await DTelecomSecureChat.wipeUserData(store, userId); } }
Without this, the previous user's namespace stays in the KV but inert (the new user's scope is separate; correctness is fine).
wipeUserDatareclaims the storage. -
Existing 0.8.x users — bootstrap auto-migrates legacy unscoped keys into the connecting user's scope. One-shot, idempotent. First connect after upgrade may take a beat longer.
TL;DR: Drop-in for web. React Native users: ensure
@dtelecom/vodozemac-rn is installed.
- React Native support via the
#vodozemacsubpath import. Metro bundles the native module (@dtelecom/vodozemac-rn); web bundles the WASM build (@dtelecom/vodozemac-wasm).
For RN:
npm install @dtelecom/vodozemac-rn
# follow the native-bridge setup in /tasks/rn-native-bridge.mdFor web: nothing.
TL;DR: Drop-in. New retrySend API; existing methods unchanged.
ChatErrorCode taxonomy formalized.
retrySend(messageId)method.messageSendFailedevent.ChatErrorCodeformalized:peer_unreachable,auth_expired,offline,rate_limited,server_error,internal.sendTextnow throws on unreachable peer (previously logged + dropped).
If you previously called sendText without try/catch and ignored
failures, wrap it now to surface peer_unreachable to the user.
Sample:
try {
await chat.sendText(peerUserId, text);
} catch (err) {
if (err instanceof ChatError && err.code === "peer_unreachable") {
toast("Can't reach this user");
} else {
throw err;
}
}TL;DR: Drop-in. New tab coordination — render an "open elsewhere" overlay if you support multi-tab.
See multi-device.md for
the recipe. Without the overlay, secondary tabs just appear non-
functional (sends queue and eventually fail) — annoying but not
broken.
TL;DR: Drop-in. New statusChange event, persisted message
status.
You may want to render delivery indicators per
ui-recipes.md now that the SDK
exposes them.
TL;DR (breaking): Split HTTP and WS auth. Add the new
fetchHttpBearer callback.
- The chat JWT from
fetchChatTokenis now used ONLY for the WS handshake. HTTP requests use a new requiredfetchHttpBearercallback returning whatever bearer the host backend's HTTP API accepts.
// Before
const chat = await DTelecomSecureChat.connect({
apiBaseURL: "...",
fetchChatToken: ...,
});
// After
const chat = await DTelecomSecureChat.connect({
apiBaseURL: "...",
fetchChatToken: ...,
fetchHttpBearer: async () => getPrivyAccessToken(), // or your host's session bearer
});For dMeet specifically: fetchHttpBearer returns the Privy access
token (same value the rest of the app's /api/* routes already
accept).
TL;DR: Drop-in. Adds listConversations,
connectionStateChange, block list. Use any that fit.
TL;DR (breaking): apiBaseURL is now the full endpoint prefix
(host + path), not just the host.
// Before
apiBaseURL: "https://your-host.example"
// After
apiBaseURL: "https://your-host.example/api/secure-chat"CHANGELOG.md— every version's release notesevents.md— full event referenceerrors.md— full error reference