Agrees to a verification the other side asked for, and queues the answer
for the pump.
For the side that received an invitation. The flow reaches 'ready'
once the answer has been sent and reported.
Where verificationId comes from on this side
onCryptoSignal announces it, which this package's root exports
alongside every function here. Forward the sync to
receiveSyncChanges as usual -- that is what makes the flow exist
at all -- and a subscriber receives:
onCryptoSignal((signal) => { if (signal.kind !== 'verification_requested') return // signal.user and signal.device say who is asking. // Ask the person, then: acceptVerification(signal.verificationId) // or cancelVerification })
From there the flow is the one requestVerification documents,
from its step 2 onward, by whichever method the two sides negotiated.
This is the side most likely to be holding the camera, so if your
product scans, this is the flow submitScannedCode is called on.
Whether a code is available at all was settled before the flow existed,
by offerScannableCodes, and is not something answering an
invitation can change.
You may need to call this twice, and it is not a retry
There are two things the other side can ask you, and this answers both.
The invitation asks may we verify?. The comparison -- which either side
may open once both are ready -- asks here it is, will you take part?.
If the other side opens it before you do, that second question is
outstanding and only this call answers it: they are waiting for your
answer and getVerificationStage sits at 'started' until you
give it.
You do not have to work out which is which. Call this whenever the stage
reads 'requested' or 'started' and the flow is waiting on you; it
rejects with 'wrong_stage' when nothing is.
Subscribe before your first sync, and prefer keeping the
subscription for the process's life. Nothing is queued for a subscriber
that is not there -- but for an ordinary invitation nothing is consumed
either, because the layer underneath does no work at all with nobody
subscribed. So one that arrives while you are unsubscribed is still
'requested' when you come back, and the first
receiveSyncChanges after you resubscribe announces it.
useEffect(() => onCryptoSignal(h), []) does not lose those. What you
cannot get back is an invitation that arrived before this process existed
at all; see the restart note below. The one exception is the shape
described two sections down, which cannot be re-offered.
This used to be a listing of the m.key.verification.request to-device
event's JSON, with an instruction to filter your own to_device_events
for it and read content.transaction_id out of one. That was a real seam
-- one field of protocol JSON this library otherwise keeps to itself --
and the announcement is what closes it. The identifier still is that
transaction id on the wire; you no longer have to know that.
The other shape an invitation arrives in, and the one thing it costs
Some clients -- matrix-nio among them, and it is the whole of what it
implements -- do not send an invitation at all. They open the comparison
directly, with the older message the specification deprecated but did not
remove. Nothing about this call changes: such a flow is announced on
the same channel, under the same 'verification_requested' signal, and
this is still what you call to agree to it.
Two differences are visible afterwards, and neither needs a branch in
your code:
the flow never reads 'ready'. It is a comparison from the moment it
exists, so it goes straight to 'started' and
startVerificationComparison on it rejects with
'comparison_already_started' -- which already means "the other side
started it, carry on and wait for the string";
confirmVerification can finish it outright, rather than leaving
it 'confirmed' until the other side acknowledges. The device is
verified when that call resolves; the 'trust_changed' signal for it
still arrives on your next receiveSyncChanges, because that is
where the channel's producers run. Read getDeviceStatuses if
you need the answer without waiting for a sync.
What it costs: this shape is not re-offered across an unsubscribe. An
ordinary invitation is re-announced after you resubscribe because it can
be enumerated afresh on every sync; this one cannot -- the sync that
carried it is its only witness. Subscribing before your first sync is
therefore load-bearing for it rather than merely advisable.
An unmet sender's invitation is dropped on arrival, and not announced
If this library has never been told about the sender's device, the
invitation is discarded as it arrives. The layer underneath needs the
sender's device keys to build the flow at all; without them it drops the
event. receiveSyncChanges still resolves successfully, no flow exists,
nothing is announced, and this function rejects that transaction id with
'unknown_flow'.
The silence is deliberate rather than a gap. The channel announces flows,
and there is no flow: announcing the wire event's own identifier instead
would hand you a value every call in this group then rejects.
It is recoverable, and recovering it is your job because nothing here
kept the event. What was discarded is that arrival, not the
invitation: the same event fed in again, once the device is known, does
create the flow -- and announces it, exactly as a first-time arrival
would. So:
keep the to-device events you could not act on. You never have to open
one: what you keep is an opaque blob, and what you get back is the
announcement. Keep the ones you did act on too, until their flow
finishes -- see the restart note below;
learn the sender's devices -- a real /sync names them in
device_lists.changed, which encryptionSlice maps to
changed_devices; forward that, then drain the resulting
'keys_query' and report it with markRequestSent.
getDeviceStatuses for that user answering non-empty is how you
know it worked;
pass the kept events to receiveSyncChanges a second time, and
wait to be told.
Promptly, though: an invitation expires ten minutes after it was sent, so
a recovery that takes longer than that leaves the other side to ask
again. A product that discards to-device events it could not act on has
no way back, which is the reason this is spelled out rather than left to
the error kind.
A restart loses the flow, and the recovery is the same one
Flows live in memory, on both sides of this boundary. A process that
restarts mid-verification holds a verificationId that now rejects with
'unknown_flow', and nothing is announced for it, because there is
nothing left to announce. The only way back is the one above: feed the
kept m.key.verification.request event in again, and be told the flow's
name as though it had just arrived.
That is why the retention advice covers events you did act on and not
only ones you could not. An invitation you accepted a second before the
process died is exactly the event you now need, and the ten-minute expiry
is still running.
Skipping this call does not fail silently. Nothing advances: the flow
stays at 'requested', and startVerificationComparison on it
rejects with 'wrong_stage' rather than starting a comparison the other
side never agreed to.
Rejects with 'wrong_stage' for a flow this device asked for itself, or
one already answered, cancelled or finished. It is never a successful
no-op. Rejects with 'unknown_flow' for a transaction id that names no
flow -- see the two sections above for the two ways that happens.
Two refusals that depend on whose flow it is
Both apply only when the invitation came from another device of your own
account, and for one reason: completing a self-verification signs one of
your devices with this device's self-signing key and asks your other
devices for your cross-signing seeds, both under whatever identity this
store holds. So this call reads the same gate
bootstrapCrossSigning does. Either refusal leaves the invitation
answerable and sends nothing.
Rejects with 'account_keys_not_fetched' when this process has not yet
asked the homeserver about the account, so it cannot say what identity the
account has. The refusal queues that key query itself, and the remedy is
the ordinary loop: drain the pump, send, report sent, and call this again.
Rejects with 'identity_not_known' when the server has been asked and
this device holds an identity it minted that no homeserver has ever
asserted back, which getIdentityStatus().identityPublicationPending
reports. Signing under that identity is signing under one the account may
never have. The remedy is to finish the publication rather than to retry:
createCrossSigningIdentity re-queues it, and it is cleared by the
key query answer that comes back carrying the identity, not by your report
of the upload.
Accepting a verification from anybody else reads neither, because
verifying another user needs nothing of your own identity. This section
exists because the sending side carried these warnings and the receiving
side did not, and the receiving side reaches the identical write.
Agrees to a verification the other side asked for, and queues the answer for the pump.
For the side that received an invitation. The flow reaches
'ready'once the answer has been sent and reported.Where
verificationIdcomes from on this sideonCryptoSignalannounces it, which this package's root exports alongside every function here. Forward the sync to receiveSyncChanges as usual -- that is what makes the flow exist at all -- and a subscriber receives:From there the flow is the one requestVerification documents, from its step 2 onward, by whichever method the two sides negotiated. This is the side most likely to be holding the camera, so if your product scans, this is the flow submitScannedCode is called on. Whether a code is available at all was settled before the flow existed, by offerScannableCodes, and is not something answering an invitation can change.
You may need to call this twice, and it is not a retry
There are two things the other side can ask you, and this answers both. The invitation asks may we verify?. The comparison -- which either side may open once both are ready -- asks here it is, will you take part?. If the other side opens it before you do, that second question is outstanding and only this call answers it: they are waiting for your answer and getVerificationStage sits at
'started'until you give it.You do not have to work out which is which. Call this whenever the stage reads
'requested'or'started'and the flow is waiting on you; it rejects with'wrong_stage'when nothing is.Subscribe before your first sync, and prefer keeping the subscription for the process's life. Nothing is queued for a subscriber that is not there -- but for an ordinary invitation nothing is consumed either, because the layer underneath does no work at all with nobody subscribed. So one that arrives while you are unsubscribed is still
'requested'when you come back, and the first receiveSyncChanges after you resubscribe announces it.useEffect(() => onCryptoSignal(h), [])does not lose those. What you cannot get back is an invitation that arrived before this process existed at all; see the restart note below. The one exception is the shape described two sections down, which cannot be re-offered.This used to be a listing of the
m.key.verification.requestto-device event's JSON, with an instruction to filter your ownto_device_eventsfor it and readcontent.transaction_idout of one. That was a real seam -- one field of protocol JSON this library otherwise keeps to itself -- and the announcement is what closes it. The identifier still is that transaction id on the wire; you no longer have to know that.The other shape an invitation arrives in, and the one thing it costs
Some clients --
matrix-nioamong them, and it is the whole of what it implements -- do not send an invitation at all. They open the comparison directly, with the older message the specification deprecated but did not remove. Nothing about this call changes: such a flow is announced on the same channel, under the same'verification_requested'signal, and this is still what you call to agree to it.Two differences are visible afterwards, and neither needs a branch in your code:
'ready'. It is a comparison from the moment it exists, so it goes straight to'started'and startVerificationComparison on it rejects with'comparison_already_started'-- which already means "the other side started it, carry on and wait for the string";'confirmed'until the other side acknowledges. The device is verified when that call resolves; the'trust_changed'signal for it still arrives on your next receiveSyncChanges, because that is where the channel's producers run. Read getDeviceStatuses if you need the answer without waiting for a sync.What it costs: this shape is not re-offered across an unsubscribe. An ordinary invitation is re-announced after you resubscribe because it can be enumerated afresh on every sync; this one cannot -- the sync that carried it is its only witness. Subscribing before your first sync is therefore load-bearing for it rather than merely advisable.
An unmet sender's invitation is dropped on arrival, and not announced
If this library has never been told about the sender's device, the invitation is discarded as it arrives. The layer underneath needs the sender's device keys to build the flow at all; without them it drops the event.
receiveSyncChangesstill resolves successfully, no flow exists, nothing is announced, and this function rejects that transaction id with'unknown_flow'.The silence is deliberate rather than a gap. The channel announces flows, and there is no flow: announcing the wire event's own identifier instead would hand you a value every call in this group then rejects.
It is recoverable, and recovering it is your job because nothing here kept the event. What was discarded is that arrival, not the invitation: the same event fed in again, once the device is known, does create the flow -- and announces it, exactly as a first-time arrival would. So:
/syncnames them indevice_lists.changed, which encryptionSlice maps tochanged_devices; forward that, then drain the resulting'keys_query'and report it with markRequestSent. getDeviceStatuses for that user answering non-empty is how you know it worked;Promptly, though: an invitation expires ten minutes after it was sent, so a recovery that takes longer than that leaves the other side to ask again. A product that discards to-device events it could not act on has no way back, which is the reason this is spelled out rather than left to the error kind.
A restart loses the flow, and the recovery is the same one
Flows live in memory, on both sides of this boundary. A process that restarts mid-verification holds a
verificationIdthat now rejects with'unknown_flow', and nothing is announced for it, because there is nothing left to announce. The only way back is the one above: feed the keptm.key.verification.requestevent in again, and be told the flow's name as though it had just arrived.That is why the retention advice covers events you did act on and not only ones you could not. An invitation you accepted a second before the process died is exactly the event you now need, and the ten-minute expiry is still running.
Skipping this call does not fail silently. Nothing advances: the flow stays at
'requested', and startVerificationComparison on it rejects with'wrong_stage'rather than starting a comparison the other side never agreed to.Rejects with
'wrong_stage'for a flow this device asked for itself, or one already answered, cancelled or finished. It is never a successful no-op. Rejects with'unknown_flow'for a transaction id that names no flow -- see the two sections above for the two ways that happens.Two refusals that depend on whose flow it is
Both apply only when the invitation came from another device of your own account, and for one reason: completing a self-verification signs one of your devices with this device's self-signing key and asks your other devices for your cross-signing seeds, both under whatever identity this store holds. So this call reads the same gate bootstrapCrossSigning does. Either refusal leaves the invitation answerable and sends nothing.
Rejects with
'account_keys_not_fetched'when this process has not yet asked the homeserver about the account, so it cannot say what identity the account has. The refusal queues that key query itself, and the remedy is the ordinary loop: drain the pump, send, report sent, and call this again.Rejects with
'identity_not_known'when the server has been asked and this device holds an identity it minted that no homeserver has ever asserted back, whichgetIdentityStatus().identityPublicationPendingreports. Signing under that identity is signing under one the account may never have. The remedy is to finish the publication rather than to retry: createCrossSigningIdentity re-queues it, and it is cleared by the key query answer that comes back carrying the identity, not by your report of the upload.Accepting a verification from anybody else reads neither, because verifying another user needs nothing of your own identity. This section exists because the sending side carried these warnings and the receiving side did not, and the receiving side reaches the identical write.