Repository navigation
v8.0.0
8.0.0 is a major release. The entries below give the details. To upgrade from
7.2.0:
- React Native and Expo: install a global random source. Install
react-native-get-random-values2.x orreact-native-quick-crypto, and
load it before the first SDK call. - Pass a secret vault. The profile key, the device ID, the
device-lifecycle state, and the key-value store's value key live in a
SignalProtocolLocalSecretVaultthat the app passes. A call without one
throwsSECRET_VAULT_REQUIRED. Bare React Native apps can use
./local/vault/react-native-keychain, Expo apps
./local/vault/expo-secure-store, and Electron apps
./local/vault/electron-safe-storage. - Key-value store: change the import and the names. The store moves from
./local/store/react-nativeto./local/store/key-value, and its names
drop theReactNativeprefix. To keep existing data, move its value key
into the vault before the first open. - Bare React Native:
./local/store/react-nativeis now the SQLite
store.reactNativeStore()opens an SDK-owned SQLCipher database on the
optional peer@op-engineering/op-sqlite18.2.5 or later, with the
op-sqlite SQLCipher build turned on. - Expo: await the store. Replace
expoStore()withawait expoStore().
The SDK owns the database, its schema, and its key. - Node and Electron: open the new SQLCipher store.
nodeStore({ directory, vault })opens an SDK-owned SQLCipher database on the optional
peerbetter-sqlite3-multiple-ciphers13.0.3 or later. It does not read
the files of the 7.2.0 Node store. - Remove the deleted names. The
./local/store/expo/dband
./local/store/expo/schemaentries, the Expo database and key functions,
the profile-key storage functions,createReferenceReactNativeBackend, and
thedrizzle-orm,expo-crypto, andfs-native-extensionspeers are
removed. - Hosted Relay: copy the connection URL from the console again. The
API path and the developer environment kind changed, and the sandbox and
staging hosts moved to new names.
Browser apps can also use the new ./local/store/web-sqlite store, an
encrypted SQLite database in the origin private file system. It is optional,
and the IndexedDB store stays at ./local/store/web.
-
Removed: the optional
expo-cryptopeer. The SDK does not load
expo-crypto, sopackage.jsonno longer lists it. An app that installed
expo-cryptoonly for the SDK can uninstall it. -
Added: web SQLite store gates in WebKit. A macOS job runs the web
SQLite store's contract, interruption, and multi-tab gates in WebKit on
each change to the web SQLite store, the SQLite core, the storage contract,
the key vault, the gate, or a dependency. Linux WebKit (WebKitGTK) has no
origin private file system, so the web SQLite store does not run there. In
each job that must run these gates, a gate that skips for want of the
origin private file system fails. The store stays experimental: its
storage-pressure and soak gates do not exist yet. -
Fixed: the SQLite stores report full storage as
StorageQuotaExceededError. When the database file cannot grow because
the disk or the storage quota is full, a write on the Expo, bare React
Native, Node, or web SQLite store rejects withStorageQuotaExceededError
(STORAGE_QUOTA_EXCEEDED), with the SQLite error asoriginalError. It
rejected with the SQLite error or aKEY_STORAGE_ERROR, and the web SQLite
store reported a full origin quota as a disk I/O error. The rejected write
does not persist, the store stays open, and the write succeeds after space
frees. -
Changed: each store and vault guide names the optional peers of its
entry. Each guide has an install command for the peers that its entry
loads. For example,./local/store/weband./local/store/web-sqliteneed
idb, and a bundler fails the app build without it. -
Fixed: a group delete deletes the skipped sender keys. The in-memory,
key-value, and IndexedDB stores kept them after
deleteAllSenderKeysForGroup. These keys decrypt skipped group messages.
The stores now delete them together with the sender keys of the group. A
group whose identifier starts with the identifier of the deleted group
keeps its keys. -
Fixed: sender keys no longer collide. Group
awith senderb:cand
groupa:bwith sendercshared keys. In the in-memory and IndexedDB
stores, they shared one sender key record. In IndexedDB, they also shared
one skipped key. In database version 7, a sender key record has an array
key of group, sender, and device. The key of a skipped key adds the index,
and a group index finds the skipped keys of a group. The in-memory store
keys both with JSON arrays. -
Changed: IndexedDB version 7 deletes stored sender keys. The upgrade
deletes the version 6 sender key records and skipped keys. After the
upgrade, each group needs a new sender key distribution before its messages
decrypt. A late message for a deleted skipped key does not decrypt. -
Fixed:
deleteAllSenderKeysForGroupon the Expo store deletes the
group's skipped message keys. On a store fromexpoStore(), it deleted
the group's sender keys and kept each skipped message key, which still
decrypts its message after the group is gone. -
Fixed: the Expo store keeps SESAME user records and device identity
pins. On a store fromexpoStore(),setUserRecordstored nothing, and
setDeviceRecordstored only the session. A user or device with no session
had no record, andgetDeviceRecordreturned the identity from the contact
trust store, not the pin that was set. The store now returns each user and
device that was set, with the pin that was set and the device's current
session, if any.setDeviceRecordanddeleteDeviceRecordchange the
record and its session together or not at all.getAllUserIdsand
getSesameDeviceIdslist the users and devices that were set, as on the
other stores. -
Fixed: the Expo store keeps a registration ID that is set before the
identity key. On a store fromexpoStore(),setLocalRegistrationIddid
nothing until an identity key was stored. -
Fixed: a client on the key-value store can send after it establishes a
session../local/store/key-value(./local/store/react-nativebefore
the move) lost the session from each SESAME device record, so every send
failed withNo active session. It also returned the device identity pin as
a plain object, not its bytes, after a reopen. The store now joins each
device with the session under its session key, and it keeps the pin bytes. -
Fixed: a client on the IndexedDB store can send after it establishes a
session../local/store/webreturned each SESAME device record with no
session, so every send failed withNo active session.getUserRecordand
getDeviceRecordon the IndexedDB store, andgetUserRecordon the
in-memory store, now join each device with the session under its address,
and the stored identity pin wins over one derived from the session. On the
IndexedDB store,setDeviceRecordwrites the device and its session in one
transaction, anddeleteDeviceRecordremoves the session with the device. -
Fixed: the in-memory store returns no session for a device whose session
is deleted. AfterdeleteSessionRecord,getDeviceRecordand
getUserRecordon./local/store/memoryreturned the session that
setDeviceRecordstored earlier. The store now keeps each device without
its session and joins the device with the session under its address, as
the other stores do.setDeviceRecordrejects a session record that is not
current, andsetUserRecordstores no session. -
Added:
./local/store/web-sqlite, a web store on encrypted SQLite.
webSqliteStore(options?)opens an SDK-owned SQLite database in the origin
private file system. The SQLite3 Multiple Ciphers Wasm engine runs in a
dedicated module worker and encrypts the whole file. On first use the store
creates the database key in the secret vault, by default IndexedDB in the
same origin, then the file. Each open applies the SDK migrations.
resetWebSqliteStore(options?)deletes the file, creates a new key, and
opens an empty store.createPreKeyMaintenanceStoretakes an open store.
The tabs of an origin share the database, and the opens and resets of a
name run one at a time across the tabs. A reset fails withINVALID_STATE
while any tab has the store open. The page's Content Security Policy must
allow'wasm-unsafe-eval'for the worker; without it, the open fails with
SQLITE_ENGINE_UNAVAILABLE. A context without OPFS gets
OPFS_UNAVAILABLE. The key in IndexedDB protects a copy of the database
file; it is not an XSS defense. -
Breaking: the SDK owns the Expo database.
expoStore(options?)
opens an SDK-owned SQLCipher database in theexpo-sqlitedefault
directory. On first use it creates the database key in the secret vault
(Expo SecureStore by default), then the file. Each open applies the SDK
migrations.ExpoSignalProtocolStorehas no public constructor, and
close()closes the store. An open fails withLOCAL_STORE_KEY_LOSTwhen
the file exists and the vault holds no key for it. It fails with
INVALID_STATEwhen a store of the process holds the same name, or when a
newer SDK wrote the file.createPreKeyMaintenanceStoretakes the open
store. The options acceptencryptionAtRest: false, which opens a
plaintext database and does not use the vault. An open with the other
setting than the one that created the file fails with
SqliteKeyMismatchError(KEY_STORAGE_ERROR), notLOCAL_STORE_KEY_LOST.
Keep application tables in a separate database file. -
Breaking:
expoStore()is now async. It returns a promise of the open
store. ReplaceexpoStore()withawait expoStore().
ExpoSignalProtocolStoreFactoryOptionsis nowExpoSignalProtocolStoreOptions. -
Breaking: the Expo module functions that exposed the database or its key
are removed. The./local/store/expoentry no longer exports
getKeyStorage,resetKeyStorage,getDatabaseKeyManager,
resetDatabaseKeyManager,clearDatabaseKeyCache,getPrimaryIdentityKey,
orgetContactIdentity. The./local/store/expo/dbentry, with
configureSignalProtocolExpoDbBindings,
resetSignalProtocolExpoDbBindings,getDrizzle,getRawDatabase, and its
tables, is removed. The./local/store/expo/schemaentry is removed. The
drizzle-ormpeer dependency is removed. -
Breaking:
ExpoSignalProtocolStore.getDatabaseKey()is removed. The
database key stays in the secret vault, and the store does not return it. -
Changed:
clearAllKeyson the Expo store empties every table and keeps
the database key. The file stays open with the same key.
resetExpoStore(options?)is the full teardown: it deletes the database
file, creates a new key, and opens an empty store. It is the only way to
open a store whose key is lost. -
Added:
@open-e2ee/signal-protocol-sdk/local/vault/electron-safe-storage.
ElectronSafeStorageSignalProtocolSecretVaultis a
SignalProtocolLocalSecretVaultfor the Electron main process. The app
passes its ownsafeStorage, so the SDK does not importelectron. The
vault encrypts each secret withsafeStorageand keeps only the ciphertexts
in a file that the app names. It fails closed when async encryption is not
available, on the Linuxbasic_textandunknownbackends, and on Linux
when a ciphertext does not start withv11. Av10ciphertext means that
no secret service answered, sosafeStorageused its fixed fallback key. -
Added: the optional peer
better-sqlite3-multiple-ciphers13.0.3 or
later. The SQLite store for Node and Electron uses it. An app that does
not use that store does not install it. -
Breaking: the Node store is an SDK-owned SQLCipher database.
nodeStore(options)takes{ directory, vault }in place of the optional
{ dataDir }, and opens an encrypted SQLite database on
better-sqlite3-multiple-ciphersindirectory. On first use it creates
the database key invault, then the file. The options also acceptname,
encryptionAtRest: false, andlogger, as the Expo store does. It runs in
Node and in the Electron main process; in Electron, pass the safeStorage
vault.NodeSignalProtocolStorehas no public constructor, and
NodeSignalProtocolStoreConfigis nowNodeSignalProtocolStoreOptions.
close()closes the store. An open fails withLOCAL_STORE_KEY_LOSTwhen
the file exists and the vault holds no key for it, and with
INVALID_STATEwhen a store of the process holds the same file or a newer
SDK wrote it. The store does not read the files of the earlier Node store.
Keep application tables in a separate database file. -
Breaking: the
fs-native-extensionspeer dependency is removed. -
The Node store has one owner process. An open store holds the owner
lock<name>.lockbeside the database untilclose(), and the operating
system releases it when the process exits. An open or a reset in another
process fails at once withSqliteStoreInUseError(KEY_STORAGE_ERROR),
before it reads the vault or touches the file. -
Added:
resetNodeStore(options),createPreKeyMaintenanceStore(store),
andsqliteDatabaseKeySlot(path)on./local/store/node.
resetNodeStoredeletes the database file, creates a new key, and opens an
empty store.sqliteDatabaseKeySlotnames the vault slot of the key for a
database path, so an app can move the key when it moves the directory. -
Changed: the Node store has the semantics of the Expo store. A user
record is the aggregate of the user's sessions, sosetUserRecordstores
nothing.setDeviceRecorddoes not pin the identity key of a device,
setDeviceSessiondoes not require a registered device, and
setLocalRegistrationIdstores nothing before the identity key.
deleteAllSenderKeysForGroupkeeps the group's skipped sender keys. -
The Expo store runs on a shared SQLite core. The store's schema,
queries, and transactions move from the Expo entry to an internal core that
runs on any SQLite binding. Every statement and transaction on the
database goes through one queue, so concurrent store calls no longer
collide inBEGINorROLLBACK. A save that replaces a signed prekey with
the same key ID now zeroes the private key that it replaces. -
The Expo account wipe empties every SDK table.
clearAllKeysand
wipeAllSignalProtocolDataon the Expo store now empty every SDK table, as
the web and Node stores do. This adds the metadata, sender keys, skipped
sender keys, message records, profile keys, group master keys, group state
cache, and auth credential cache, and forclearAllKeysthe KEM one-time
prekeys. -
A bare React Native app can bundle the SDK. The root entry no longer
usesexport * asto export thesafety,keys,encoding,blocking,
andmedianamespaces.@react-native/babel-presethas no transform for
that syntax, so Metro in a bare React Native app rejected the whole bundle.
The namespaces and their members do not change. -
Added:
EncryptionErrorCode.SQLITE_ENGINE_UNAVAILABLE,
EncryptionErrorCode.OPFS_UNAVAILABLE, and
EncryptionErrorCode.OPFS_FILE_BUSY. The web SQLite store fails with
these codes when its Wasm engine cannot load in its worker, when the browser context has no OPFS storage for it, or when it
cannot open its database file this time, usually because another tab still
holds the file. Retry afterOPFS_FILE_BUSY; the file is unchanged. An
exhaustiveswitchoverEncryptionErrorCodeneeds the three new cases. -
Changed: the package ships the SQLite3 Multiple Ciphers 2.5.1 Wasm
engine.dist/local/store/sqlite/web/sqlite3mc/holdssqlite3.mjsand
sqlite3.wasmfrom the signed release, about 1.5 MB.sqlite3.wasmis
unmodified.sqlite3.mjscarries two recorded patches, so a failed pool
install never deletes the database files and a failed attach holds no file
handle. Both files are pinned by sha256. Only the worker of
./local/store/web-sqliteloads them.THIRD_PARTY_NOTICES.mdlists their
licenses. -
Added: the bare React Native SQLite store at
./local/store/react-native.
reactNativeStore(options?)opens an SDK-owned SQLCipher database on
op-sqlite, with its key in the react-native-keychain vault by default, on
the same SQLite core and with the same options, errors, and reset as
expoStore.resetReactNativeStore(options?)deletes the database
and opens an empty store, andcreatePreKeyMaintenanceStoretakes the open
store.close()checkpoints the write-ahead log into the database file
before it closes the connection. The app turns on the op-sqlite SQLCipher build with
"op-sqlite": { "sqlcipher": true }in itspackage.json. Without it, an
encrypted open fails withKEY_STORAGE_ERRORbefore it creates a file.
@op-engineering/op-sqlite18.2.5 or later is an optional peer, and only
this entry imports it. -
Breaking: the own profile key lives in the application's secret vault.
The SDK no longer writes the profile key tolocalStorage, and it no longer
picks a storage by platform.getOwnProfileKey,setOwnProfileKey,
getOrCreateOwnProfileKey,getOwnProfileKeyBase64, and
rotateOwnProfileKeytake aSignalProtocolLocalSecretVaultas their first
argument.updateEncryptedProfiletakes avaultparameter when it gets no
profileKey. The vault holds the key as its raw 32 bytes under
signal_profile_key_v1. To upgrade, pass the vault to each call. On Expo,
useExpoSecureStoreSignalProtocolSecretVaultfrom
@open-e2ee/signal-protocol-sdk/local/vault/expo-secure-store. Remove each
use ofsetProfileKeyStorage,getProfileKeyStorage,
resetProfileKeyStorage,createMemoryStorage, andProfileKeyStorage.
These exports are deleted. -
Breaking: the device ID and the device-lifecycle state live in the
application's secret vault.getDeviceIdandpreloadDeviceIdtake the
vault as their first argument.DeviceLifecycleDepshas avaultfield in
place ofsecureStore, and theDeviceLifecycleSecureStoretype is
deleted. The vault holds each value as UTF-8 text. To upgrade, pass the same
vault toDeviceLifecycleManager,getDeviceId, andpreloadDeviceId. The
SDK does not read the plain SecureStore values that an earlier version
wrote, so reset the device when you upgrade. -
Breaking: a call without a vault throws
SECRET_VAULT_REQUIRED. Each
function above, and theDeviceLifecycleManagerconstructor, throws an
EncryptionErrorwith the new codeEncryptionErrorCode.SECRET_VAULT_REQUIRED
when the vault is absent. The error context names the operation. -
Breaking: the shared
./deviceentries import no platform package.
./device,./device/device-id, and./device/lifecyclenow load without
Expo, React Native, orreact-native-device-info. They take platform
metadata as input, and they omit each field that the application does not
supply. To upgrade:- Spread
getDeviceLifecyclePlatform()into theDeviceLifecycleManager
dependencies.DeviceLifecycleDepsextends the new
DeviceLifecyclePlatformtype:generateDeviceName,
getDeviceFingerprint,deviceType,platform,osVersion, and
appVersion, each optional. WithoutgenerateDeviceName, the device name
isUnknown Device. WithoutgetDeviceFingerprint, registration sends no
idfvand does not reclaim a device. - Pass the same facts to
getLocalDeviceMetadata, which now takes them as
its argument. - Pass a
TransferDeviceInfotogenerateTransferQRCode,
prepareNewDeviceTransfer,createEmptyBackup,createDeviceBackup, and
prepareOldDeviceTransferWithBackup. Get it from
getTransferDeviceInfo(), or pass{}to send no metadata. The QR code
omitsdeviceTypewhen the metadata has no platform, and
TransferQRCode.deviceTypeis optional.
- Spread
-
Added:
./device/react-native. It reads device metadata on bare React
Native throughreact-nativeandreact-native-device-info, and it reaches
no Expo package. It exportsgetDeviceMetadata,getTransferDeviceInfo,
andgetDeviceLifecyclePlatform. Its lifecycle facts include a fingerprint
fromgetUniqueId(). -
Changed:
./device/exporeads the lifecycle and transfer metadata. It
now importsexpo-devicetoo, and it addsgetTransferDeviceInfoand
getDeviceLifecyclePlatform. Expo has no device fingerprint without another
package, so its lifecycle facts have nogetDeviceFingerprint. Supply one
to keep reclaim detection. -
Added: a secret vault for bare React Native apps.
ReactNativeKeychainSignalProtocolSecretVaultfrom
@open-e2ee/signal-protocol-sdk/local/vault/react-native-keychainstores
vault secrets withreact-native-keychain, a new optional peer at 10.0.0 or
later. The adapter sets fixed options that the app cannot change: iOS
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY, AndroidAES_GCM_NO_AUTHstorage at
theSECURE_SOFTWARElevel, and nocloudSynckey. The secret stays on the
device. react-native-keychain 10.0.0 turns iCloud sync on for any
cloudSyncvalue,falseincluded, so the adapter never sends that key. -
Breaking: React Native and Expo apps must install a global random
source. The SDK reads random bytes on React Native only from the global
crypto.getRandomValues. It no longer usesexpo-cryptofor random bytes,
and the key-value store no longer importsexpo-crypto. To
upgrade, installreact-native-get-random-values2.x or
react-native-quick-crypto, and load it before the first SDK call.
Without a global source, the SDK throwsSecureRandomUnavailableError. -
Added:
SecureRandomUnavailableError, with the code
SECURE_RANDOM_UNAVAILABLEand the guardisSecureRandomUnavailableError,
exported from/types. The SDK throws it when the runtime has no
cryptographically secure random source. -
Breaking:
getGroupServiceIdson the in-memory relay is now
asynchronous. It returns aPromise. To upgrade,awaiteach call. -
Breaking:
GroupAuthorizationServerEnginetakes its runtime as a
required argument. The engine no longer picks a host random source, so a
server bundle of the engine reaches no client platform seam. To upgrade,
pass aGroupServerEngineRuntimewithnowandrandomBytesas the second
constructor argument. The in-memory relay's
InMemoryGroupAuthorizationServerstill supplies its own runtime. -
Fixed: the username discriminator had a small bias toward low values. The
SDK now draws it with rejection sampling, so each value in the range has
the same probability. -
Fixed: on React Native, the hosted Relay socket did not open. The SDK
built the socket URL with theprotocolsetter ofURL, and the React
NativeURLhas no setters. Now the SDK builds the URL itself. -
Fixed: on React Native,
isVerifyUrl,parseVerifyUrl, and
parseProvisioningQRaccepted a lookalike custom-scheme link, such as
signalprotocol://verify-evil. The React NativeURLreads no host or path
for a custom scheme. The SDK now parses these links, the relay connection
URL, Web Push endpoints, object store URLs, and TUS upload locations with
its own parser, which gives the same result on every runtime. -
Changed: the SDK rejects a URL that is not in a canonical form. Upgrade
step: give each URL as ASCII, without credentials, a fragment, an empty
query (?with nothing after it), or a.or..path segment. Write the
host in the form that a WHATWGURLgives, and do not use anxn--label,
an IPv4 address in a short or hexadecimal form, or an IPv6 address in a
non-compressed form. Do not write a port with a leading zero. Percent-encode
a space, a non-ASCII character, and a'in a query. This applies to the
verification and provisioning links and theirVerifyLinkConfigand link
prefix, the relay connection URL, Web Push endpoints, and object store
upload and download URLs. A TUS server must return aLocationthat is an
absolute URL, a path that starts with/, or a relative path, not a
network-path reference that starts with//. A query value that is not
valid UTF-8 now makes a verification or provisioning link invalid. -
Changed:
encryptFileanddecryptFilethrow anEncryptionErrorwith
the new codeBINARY_BLOB_UNAVAILABLEwhen the runtime'sBlobcannot hold
bytes. The React NativeBlobrejects binary parts and has no
arrayBuffer()ortext(), so these methods cannot work there. They now
throw before they touch the session, so the key message stays unconsumed.
Upgrade step: on React Native, usestreamingEncryptandstreamingDecrypt
from@open-e2ee/signal-protocol-sdk/files. -
Fixed: on a React Native runtime without
Buffer,TextDecoder, or
structuredClone, the stores, the relay clients, and message decoding
failed. These parts now use the SDK's base64, UTF-8, and clone functions.
UTF-8 decoding uses the runtime'sTextDecoderonly when it passes a
conformance check, and a built-in WHATWG decoder otherwise, so invalid
UTF-8 gives the same result on every runtime. -
Changed: the Expo store models decode base64 strictly. A stored key that is
not padded standard base64 with zero trailing bits now fails to load.
Before, the models skipped invalid characters. The SDK writes only this
form, so the change affects only a row that was changed by hand. -
Fixed: the Convex relay's
subscribethrew in a browser that loads the SDK
without a bundlerdefine, where there is noprocessglobal. It now
falls back to polling there, as it does whenEXPO_PUBLIC_CONVEX_URLis not
set. -
Fixed: the Expo store's
getDetailedStats()and full data wipe now
complete. Before, both read or deleted a table that does not exist, so
getDetailedStats()failed and the wipe (wipeAllSignalProtocolData()on
the store fromgetKeyStorage()) rolled back. Theuserscount is now the
number of distinct users that have a session. -
Fixed: on the Expo store, a signed prekey that reuses the ID of a replaced
signed prekey is now the current signed prekey. Before, the store kept the
replaced mark, sogetEcSignedPreKey()returnednull. -
Fixed:
createPreKeyMaintenanceStore().cullReplacedPreKeys()on the Expo
store now culls the four prekey tables one at a time. Before, it opened
four transactions at the same time on one connection, and the cull failed
with "cannot start a transaction within a transaction". -
Breaking: the key-value store moves to
./local/store/key-value. In
7.2.0,./local/store/react-nativeexported it. Change each import of the
key-value store from@open-e2ee/signal-protocol-sdk/local/store/react-native
to@open-e2ee/signal-protocol-sdk/local/store/key-value.
./local/store/react-nativenow exports the bare React Native SQLite
store. -
Breaking: the key-value store names drop the React Native prefix. The
store works over any key-value backend, so its names do not name a
platform. Change each name:ReactNativeSignalProtocolStore→KeyValueSignalProtocolStorereactNativeStore→keyValueStoreReactNativeStoreFactoryOptions→KeyValueSignalProtocolStoreOptions,
whichcreate,reset, andkeyValueStoreall takeReactNativeKeyValueStorage→KeyValueStorageReactNativeKeyValueOperation→KeyValueOperation
-
Breaking: the key-value store keeps its value key in a secret vault.
Before, the store wrote its 32-byte value key into the same key-value
backend as the data it encrypted, under@signal:databaseKey, and logged a
warning. Pass aSignalProtocolLocalSecretVaultbacked by the platform
keychain or keystore, such asReactNativeKeychainSignalProtocolSecretVault,
asvaulttocreate,reset, orkeyValueStore. Without a vault,
each of them throwsSECRET_VAULT_REQUIRED. To keep
existing data, before the first open on this version, read the base64 value
of@signal:databaseKeyfrom your backend, write its decoded bytes to the
vault undersignal_key_value_encryption_key, and remove
@signal:databaseKeyfrom the backend. The key-value store guide describes
key custody. -
Breaking: a lost value key is an error, not a new key. When the backend
holds store data and the vault holds no key,createrejects with an
EncryptionErrorwhose code is the newLOCAL_STORE_KEY_LOST, and it
changes nothing. Before, a missing key gave a new key that could not read
the old data. Catch the code, and then restore the vault entry or call
KeyValueSignalProtocolStore.reset({ storage, vault }).resetdeletes
every store record, and then makes a new key. If the process stops between
the two steps, the nextcreateopens an empty store. -
Breaking:
createReferenceReactNativeBackendis no longer exported. It
is now test code of the SDK. To verify your backend, use
assertBackendConformanceorrunBackendConformance, which stay exported.
To get an in-memory backend for your own tests, write one that passes the
kit. -
Added:
./local/store/key-value/realmwithcreateRealmKeyValueBackend, a
key-value backend over an open Realm. The SDK does not importrealmand
does not declare it as a dependency or peer. Your application installs
realmand addsrealmKeyValueSchemato its schema list. Each store write
runs in onerealm.writetransaction. The adapter is tested with the
conformance kit on realm 20.2.0 in Node. Realm has no maintainer since
2025-09-30. The key-value store guide lists its known build issues. -
Added:
createTransactionalKeyValueBackendin./local/store/key-value,
with the typesTransactionalKeyValueEngine,KeyValueTransaction, and
KeyValueReader. It makes a key-value backend from an engine's own
transaction and a synchronous handle withget,set,delete, and
keysWithPrefix. The SDK applies eachatomicWritebatch inside that
transaction, so a backend does not copy the batch rules. The Realm backend
uses it. To upgrade a backend that you wrote for an engine with
transactions, replace itsatomicWritecode with this function and run the
conformance kit again. The key-value store guide has an op-sqlite sketch. -
Fixed:
stop()resolved while a relay delivery or a retry request was in
progress. The decrypt then went on after logout and wrote to the store
after the app could have closed it.stop()now resolves only after the SDK
work of the deliveries, retry requests, and receipt sends in progress
finishes. It does not wait for app hooks, so a hook can awaitstop(). This
includes the session hooks of a resend for a retry request, when the resend
sets up a session with a new or stale device. Another resend to the same
user that waits while such a hook runs stops waiting and sends nothing, and
its message record stays for the next retry request. When a hook returns
afterstop(), the SDK drops the work after the hook and writes nothing
more. The relay delivers the envelope again after the next start, and the
hook gets the plaintext again. A delivery that fails is logged and no longer
rejects as an unhandled promise. -
Breaking: the managed Signal Protocol Relay hosts use the lane tokens.
The SDK pinshttps://sandbox.relay.open-e2ee.dev,
https://stage.relay.open-e2ee.dev, and
https://stage-sandbox.relay.open-e2ee.dev.
https://relay.open-e2ee.devdoes not change. A connection URL on
development.relay.open-e2ee.dev,staging.relay.open-e2ee.dev, or
staging-customer-development.relay.open-e2ee.devis now invalid. Copy the
connection URL from the console again. -
Breaking: the Signal Protocol Relay API path is
/signal/v1/*. A
connection URL is nowhttps://<host>/signal/v1/connection/<key>, and the
SDK sends each request under/signal/v1/. The SDK rejects a connection URL
with the old/v1/connection/path, and a connection document whose
protocolEndpointis not<origin>/signal/v1. -
Breaking: the developer environment kind is
sandbox. The
environmentof a hosted Relay connection is now'sandbox' | 'production'. The SDK rejects a connection document that names the old
kind. The Relay gives a sandbox publishable key the prefixpk_sandbox_. -
A bare React Native example.
examples/react-nativeruns the encrypted
exchange in a React Native 0.87.1 app with no Expo module. Alice uses
reactNativeStore()on the op-sqlite SQLCipher build, with the database key
inreact-native-keychain. On each launch, the app logs the SQLCipher
version and the first 16 bytes of the SDK database file, and requires that a
wrong key fails the open of an SDK database. The run fails when the build has
no SQLCipher or when the file starts with the plaintext SQLite header. CI
builds the release APK and runs it twice on an Android emulator, and requires
these lines on each run. On every change, CI also builds the example's Metro
release bundle with the packed SDK and the stock Babel preset, and compiles
it with Hermes. -
The Expo example checks that SQLCipher encrypts its database. On each launch,
examples/expologs the SQLCipher version and the first 16 bytes of the SDK
database file. The run fails when the build has no SQLCipher or when the
file starts with the plaintext SQLite header. The Android emulator job
requires both lines on each run. The example now depends on
expo-file-systemto read the file.