@derec-alliance/react-native 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +394 -6
- package/android/src/main/jniLibs/arm64-v8a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/armeabi-v7a/libderec_library.a +0 -0
- package/android/src/main/jniLibs/x86_64/libderec_library.a +0 -0
- package/cpp/Convert.cpp +9 -0
- package/cpp/Convert.h +8 -2
- package/cpp/Primitives.cpp +113 -25
- package/cpp/StoreCallbacks.cpp +119 -20
- package/cpp/StoreCallbacks.h +8 -1
- package/cpp/derec_ffi.h +83 -15
- package/ios/DeRecFFI.xcframework/ios-arm64/Headers/derec_ffi.h +83 -15
- package/ios/DeRecFFI.xcframework/ios-arm64/libderec_library.a +0 -0
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/Headers/derec_ffi.h +83 -15
- package/ios/DeRecFFI.xcframework/ios-arm64_x86_64-simulator/libderec_library.a +2 -2
- package/lib/errors.d.ts +11 -1
- package/lib/errors.js +63 -1
- package/lib/primitives.d.ts +2 -2
- package/lib/primitives.js +3 -1
- package/lib/protocol.d.ts +102 -6
- package/lib/protocol.js +158 -7
- package/lib/types.d.ts +426 -28
- package/lib/types.js +19 -0
- package/package.json +1 -1
- package/src/errors.ts +66 -1
- package/src/primitives.ts +13 -2
- package/src/protocol.ts +166 -8
- package/src/types.ts +432 -29
package/README.md
CHANGED
|
@@ -91,12 +91,16 @@ import {
|
|
|
91
91
|
SenderKind,
|
|
92
92
|
} from '@derec-alliance/react-native';
|
|
93
93
|
|
|
94
|
+
// withStores sets all five stores in one call. Each with*Store setter is
|
|
95
|
+
// still available; the last call for a store wins.
|
|
94
96
|
const protocol = new DeRecProtocolBuilder(secretId) // bigint | number
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
.withStores({
|
|
98
|
+
channelStore: myChannelStore,
|
|
99
|
+
shareStore: myShareStore,
|
|
100
|
+
secretStore: mySecretStore,
|
|
101
|
+
userSecretStore: myUserSecretStore,
|
|
102
|
+
stateStore: myStateStore,
|
|
103
|
+
})
|
|
100
104
|
.withTransport(myTransport)
|
|
101
105
|
.withOwnTransports([{ uri: 'https://owner.example.com', protocol: 'https' }])
|
|
102
106
|
.withThreshold(2)
|
|
@@ -125,6 +129,76 @@ longer needed (safe to call more than once).
|
|
|
125
129
|
|
|
126
130
|
---
|
|
127
131
|
|
|
132
|
+
## Verification outcomes
|
|
133
|
+
|
|
134
|
+
`protocol.start(FlowKind.VerifyShares, …)` challenges each targeted helper,
|
|
135
|
+
and every challenged channel ends with exactly one event:
|
|
136
|
+
|
|
137
|
+
- `ShareVerified` — the helper proved it holds the exact share. `subshares`
|
|
138
|
+
is how many subshares that helper holds for `version`, read from this
|
|
139
|
+
device's stored share, so it is the weight of the round that published
|
|
140
|
+
`version`, not the helper's current weight; it is absent when that share was
|
|
141
|
+
stored before the count was recorded. `threshold` is the threshold that version was split with, from the
|
|
142
|
+
same stored share, not the current one; it is absent when that share was
|
|
143
|
+
stored before the threshold was recorded. To judge whether a version is
|
|
144
|
+
recoverable, sum `subshares` over its `ShareVerified` events and compare the
|
|
145
|
+
sum with their `threshold`. If either is absent on any of those events,
|
|
146
|
+
the result is unknown: a absent count is not `0` or `1`. The count lives on `Share.subshares`, which the
|
|
147
|
+
library sets on owner-side shares only (absent on a helper's share and never
|
|
148
|
+
backfilled): your `ShareStore` must persist it as given and return it
|
|
149
|
+
unchanged. `Share.threshold` follows the same rules, and must be
|
|
150
|
+
persisted the same way.
|
|
151
|
+
- `ShareVerifyInvalid { channel_id, version }` — the helper's proof does not
|
|
152
|
+
match the share this device stored. It is an event; `process()` does not
|
|
153
|
+
reject.
|
|
154
|
+
- `ShareVerifyRejected { status, memo }` — the helper refused the challenge
|
|
155
|
+
with a non-OK status.
|
|
156
|
+
- `VerifySharesFailed { reason, error }` — no verdict could be reached on
|
|
157
|
+
this device. `reason` is a `VerifyFailureReason`: `'NoSharedKey'`,
|
|
158
|
+
`'NoProof'` or `'DispatchFailed'`.
|
|
159
|
+
|
|
160
|
+
A stored proof is either confirmed by its helper or part of a sharing round
|
|
161
|
+
still open: when a round settles a channel as failed (a rejection or a
|
|
162
|
+
timeout), that channel's proof is deleted. A confirmation also deletes the
|
|
163
|
+
proofs of versions the helper dropped: a helper that stores a version
|
|
164
|
+
deletes every other version that round's `keepList` leaves out, so when it
|
|
165
|
+
confirms, its proofs for those versions are deleted here too (the confirmed
|
|
166
|
+
version stays). A helper that rejects the round keeps its older versions, and
|
|
167
|
+
so do their proofs; with no keep list nothing is deleted. A replica Destination never sees the helpers' answers, and it keeps every
|
|
168
|
+
proof it receives: it never deletes one on its own, and freeing that storage
|
|
169
|
+
is the app's concern. Verifying on a Destination a version that a helper
|
|
170
|
+
refused, or later dropped under the `keepList`, gets `ShareVerifyRejected`
|
|
171
|
+
(`UNKNOWN_SHARE_VERSION`) from that helper, where the Source reports
|
|
172
|
+
`NoProof`. The app may delete such rows from the Destination's share store;
|
|
173
|
+
verifying a version whose rows are gone then gives `VerifySharesFailed` with
|
|
174
|
+
`reason: 'NoProof'`. Verify only versions whose round
|
|
175
|
+
completed (`SharingComplete`). Starting verification on a channel with no
|
|
176
|
+
proof sends nothing and returns `VerifySharesFailed` with
|
|
177
|
+
`reason: 'NoProof'` at once. A late response to an earlier challenge is
|
|
178
|
+
ignored as `MessageIgnored` with `reason: 'StaleResponse'`, and the current
|
|
179
|
+
challenge stays open; a response missing its `result` is still an error.
|
|
180
|
+
|
|
181
|
+
**After recovery: restore or join.** `SecretRecovered` writes nothing, so
|
|
182
|
+
decide before calling `restore`. When `secret.replicas?.members` names live
|
|
183
|
+
members other than the lost device, do not restore: tell the user and offer
|
|
184
|
+
to join that group as a new replica — pair with a live member, confirm the
|
|
185
|
+
fingerprint, then run `start(FlowKind.ReplicaDiscovery)`. With no live member, restore. Neither path gives this device the proofs of
|
|
186
|
+
earlier versions, so it cannot verify them: restore writes none, and a
|
|
187
|
+
joining device gets only the share map of the single version each push or
|
|
188
|
+
catch-up carries. The live members keep the proofs they hold. A restored
|
|
189
|
+
device also cannot publish, or answer a member's catch-up, until the app
|
|
190
|
+
configures a `replica_id` the roster names. Joining adds the device as a new
|
|
191
|
+
member, while restoring next to live replicas makes a second device claim
|
|
192
|
+
the lost device's role. See
|
|
193
|
+
[Restore or join](https://derec-alliance.gitbook.io/docs/concepts/replicas/restore-or-join).
|
|
194
|
+
|
|
195
|
+
Every channel message carries one timestamp, its send time, in both the
|
|
196
|
+
envelope and the inner message. A message whose two timestamps differ, or
|
|
197
|
+
where either or both are absent, is ignored as `MessageIgnored` with
|
|
198
|
+
`reason: 'TimestampMismatch'`; this includes replies from 0.0.8 peers.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
128
202
|
## When `process()` fails
|
|
129
203
|
|
|
130
204
|
`process()` settles expired deadlines (sharing-round and unpair timeouts)
|
|
@@ -144,6 +218,80 @@ try {
|
|
|
144
218
|
}
|
|
145
219
|
```
|
|
146
220
|
|
|
221
|
+
A request this device fails to handle does not reject the call; see
|
|
222
|
+
[Failed requests](#failed-requests).
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Failed requests
|
|
227
|
+
|
|
228
|
+
Every request that arrives on an authenticated channel gets exactly one
|
|
229
|
+
response, even when handling it fails. Such a failure does not make
|
|
230
|
+
`process()` or `accept()` reject: it is reported as a `RequestFailed` event,
|
|
231
|
+
and the call's other events are kept.
|
|
232
|
+
|
|
233
|
+
- `action_kind` is the type of the request that failed, a
|
|
234
|
+
`PendingActionKind` label (`"Failed"` when its body could not be decoded,
|
|
235
|
+
or was a request type the channel cannot carry).
|
|
236
|
+
- `responded` is `true` when the library already sent the response.
|
|
237
|
+
- `status` is the `StatusEnum` that was sent, or would have been. It is
|
|
238
|
+
always present. `memo` is the wire-safe memo sent with it, or that would
|
|
239
|
+
have been, so `reject(action, status, memo)` sends exactly the response
|
|
240
|
+
the library would have sent.
|
|
241
|
+
- `error_code` is the error's `DeRecErrorCode` and `message` a local
|
|
242
|
+
description; neither goes on the wire.
|
|
243
|
+
- `action`, when `responded` is `false`, is the token that answers the
|
|
244
|
+
request. Pass it to `reject`, which sends the one response; `accept`
|
|
245
|
+
refuses it. The library never expires it. It is absent only when `reject`
|
|
246
|
+
could never answer it: a request on a replica group channel that names no
|
|
247
|
+
member and carries no `replyTo`, which has no reply target, or, with
|
|
248
|
+
`withAutoRespondOnFailure` off, a replica copy that hands this device the
|
|
249
|
+
group key and fails before that key is stored.
|
|
250
|
+
|
|
251
|
+
Resolve each token once. The library is stateless, so rejecting the same
|
|
252
|
+
token twice, or rejecting an action already accepted, sends a second
|
|
253
|
+
response.
|
|
254
|
+
|
|
255
|
+
`withAutoRespondOnFailure` decides who answers. With it on, the library sends
|
|
256
|
+
the response itself (a known failure keeps its specific status; anything else
|
|
257
|
+
is `FAIL` with a generic memo). With it off (the default), nothing is sent and
|
|
258
|
+
the event carries the token:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
for (const event of events) {
|
|
262
|
+
if (event.type === "RequestFailed" && !event.responded && event.action) {
|
|
263
|
+
await protocol.reject(event.action, event.status, event.memo);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
A message that decrypts under the channel key but whose body cannot be
|
|
269
|
+
decoded is answered with an `ErrorResponseMessage` (`FORMAT_ERROR` when the
|
|
270
|
+
library answers it; the status passed to `reject` otherwise). A peer that
|
|
271
|
+
answers this device that way is reported as an `ErrorResponseReceived` event
|
|
272
|
+
with `status` and `memo`; it is never answered.
|
|
273
|
+
|
|
274
|
+
An inbound `PairRequest` refused under a protocol rule (a parameter range that
|
|
275
|
+
does not overlap this device's, `INCOMPATIBLE_PARAMETER_RANGE`, or a replica id
|
|
276
|
+
already in the group, `REPLICA_ID_CONFLICT`) is answered with that status in
|
|
277
|
+
every mode and reported as a `PairingRefused` event with `status` and `memo`,
|
|
278
|
+
the reason sent to the requester and written from its side. `error_code`
|
|
279
|
+
(`incompatible_parameter_range` or `replica_id_conflict`) and `message` carry
|
|
280
|
+
the local reason for logging or display, as on `RequestFailed`; they are never
|
|
281
|
+
sent. `process()` does not reject for it, and no channel is created. The
|
|
282
|
+
requester's `process()` rejects with code `non_ok_status`, whose `status` and
|
|
283
|
+
`memo` hold the refusal.
|
|
284
|
+
|
|
285
|
+
An inbound share the library refuses before any `ActionRequired` (undecodable,
|
|
286
|
+
failing its commitment, outside this device's share-size range, or conflicting
|
|
287
|
+
with the share stored at that version) is reported as a `ShareRefused` event
|
|
288
|
+
with the `status` and `memo` sent; the owner observes it as `ShareRejected`.
|
|
289
|
+
|
|
290
|
+
A replica group member's copy of the secret that does not decode or verify is
|
|
291
|
+
refused, nothing is written, and it is reported as a `ReplicaSecretRefused`
|
|
292
|
+
event with `status` and `memo`; on a push the publisher observes it as
|
|
293
|
+
`ReplicaSyncRejected`.
|
|
294
|
+
|
|
147
295
|
---
|
|
148
296
|
|
|
149
297
|
## Store interfaces
|
|
@@ -172,12 +320,72 @@ declared in `src/types.ts`:
|
|
|
172
320
|
deliver once, to the first one that accepts it, and reject only when none
|
|
173
321
|
did. `sequentialFailover` and `singleEndpointTransport` implement that
|
|
174
322
|
choice for you. It is a mailbox, not a request/response call, since not
|
|
175
|
-
every peer (e.g. a phone behind NAT) can be dialed back.
|
|
323
|
+
every peer (e.g. a phone behind NAT) can be dialed back. Recommended HTTP
|
|
324
|
+
conventions (`POST`, `application/octet-stream` body, `202 Accepted`) and
|
|
325
|
+
the same-connection pattern for unreachable peers are in
|
|
326
|
+
[Transports](https://derec-alliance.gitbook.io/docs/protocol-specification/transports);
|
|
327
|
+
they are recommendations, not protocol requirements.
|
|
176
328
|
|
|
177
329
|
Every method is `async` and keyed by decimal-string ids — see the next
|
|
178
330
|
section for why that differs from `DeRecProtocol`'s own methods. Full field-
|
|
179
331
|
level contracts are documented as TSDoc on each interface in `src/types.ts`.
|
|
180
332
|
|
|
333
|
+
### Helpers and shares
|
|
334
|
+
|
|
335
|
+
- Each share goes to a **different** helper. Threshold security depends on it:
|
|
336
|
+
compromising one helper must yield at most one share.
|
|
337
|
+
- The library hands out one share per channel per version, and every pairing
|
|
338
|
+
produces its own channel. It cannot tell whether two channels lead to the
|
|
339
|
+
same party; keeping helpers distinct is the application's job.
|
|
340
|
+
- **Discouraged:** a single guardian acting as several helpers for the same
|
|
341
|
+
owner. It is against the protocol's recommendation, since one compromise
|
|
342
|
+
yields several shares. The library does not prevent it, but channel linking
|
|
343
|
+
then needs the application's help (below).
|
|
344
|
+
- **Channel linking is expected to be one-to-one.** After an owner re-pairs in
|
|
345
|
+
recovery mode, the helper application links the new channel to the old one
|
|
346
|
+
with `ChannelStore.linkChannel`. Linking assumes a helper holds **at most one share per owner
|
|
347
|
+
for a given `secretId` and version**, so each recovery channel is linked to the
|
|
348
|
+
single old channel it replaces. On `GetShare` the helper answers with
|
|
349
|
+
**one** share from the linked set: the first whose `secretId` matches the
|
|
350
|
+
request, in the order `ShareStore.loadMany` returns. The library imposes no order. If
|
|
351
|
+
several channels holding shares for the same owner are linked to one
|
|
352
|
+
recovery channel and every recovery channel is answered from the same
|
|
353
|
+
share, the owner receives it more than once (it counts once) and can fall
|
|
354
|
+
below the threshold. An application that runs such a helper keeps each
|
|
355
|
+
recovery channel resolving to a distinct share: either links stay
|
|
356
|
+
one-to-one, or its share store returns, for each requesting channel, the
|
|
357
|
+
share intended for it first, in a deterministic order.
|
|
358
|
+
|
|
359
|
+
### Refusing a share or a verification
|
|
360
|
+
|
|
361
|
+
A helper enforces share size itself: with a parameter range configured, a
|
|
362
|
+
share outside its own `minShareSize`/`maxShareSize` (inclusive; a max of `0` means
|
|
363
|
+
unbounded) is answered with `SIZE_LIMIT_EXCEEDED` before any
|
|
364
|
+
`ActionRequired` or auto-accept. Frequency limits on share updates and
|
|
365
|
+
verifications are the application's to enforce, from its share store.
|
|
366
|
+
|
|
367
|
+
On a helper channel, throw (or reject with) `shareStoreRefusal(reason)` from
|
|
368
|
+
`ShareStore.save` (a genuinely new share version) or
|
|
369
|
+
`ShareStore.recordVerification` (an accepted verification challenge) to
|
|
370
|
+
refuse it:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { shareStoreRefusal } from "@derec-alliance/react-native";
|
|
374
|
+
|
|
375
|
+
async recordVerification(secretId: string, channelId: string, version: number) {
|
|
376
|
+
if (limiter.tooSoon(channelId)) throw shareStoreRefusal("too_frequent");
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The library answers the owner with the mapped status (`"too_frequent"` →
|
|
381
|
+
`TOO_FREQUENT`, `"size_limit_exceeded"` → `SIZE_LIMIT_EXCEEDED`,
|
|
382
|
+
`"rejected"` → `REJECTED`), and the owner sees `ShareRejected` or
|
|
383
|
+
`ShareVerifyRejected` with that `status`. Any other rejection is an ordinary
|
|
384
|
+
store failure, reported as a `RequestFailed` event (see
|
|
385
|
+
[Failed requests](#failed-requests)). Every `ShareStore` implements
|
|
386
|
+
`recordVerification`; an app with no limit resolves it. See
|
|
387
|
+
[Helper-side admission control](https://github.com/derecalliance/lib-derec/tree/main/library#helper-side-admission-control).
|
|
388
|
+
|
|
181
389
|
### Filtered listings
|
|
182
390
|
|
|
183
391
|
`listHelpers` and `listReplicas` receive a filter object — `ids`, `status`,
|
|
@@ -226,6 +434,102 @@ for its duration. Applications that need this work off the JS thread should
|
|
|
226
434
|
drive it through `DeRecProtocol` (e.g. `start(FlowKind.ProtectSecret, ...)`)
|
|
227
435
|
instead of calling the primitive directly.
|
|
228
436
|
|
|
437
|
+
`split`'s `channels` argument is a `WeightedChannel[]` — `[channelId,
|
|
438
|
+
weight]` tuples, one per channel, letting a chosen channel hold more than
|
|
439
|
+
one subshare (e.g. a device the owner trusts more than the others). A
|
|
440
|
+
weight of `1` is an ordinary helper holding a single subshare, so with every
|
|
441
|
+
weight at `1`, `threshold` counts helpers as before. `ProtectSecretParams`
|
|
442
|
+
accepts an equivalent `weights: Map<bigint, number>` when driving the flow
|
|
443
|
+
through `DeRecProtocol`; `ProtectSecretStarted` and `ProtectSecretFailed`
|
|
444
|
+
report the resulting `weight` for the channel they name.
|
|
445
|
+
|
|
446
|
+
That map is the **full assignment, not a patch**: a paired helper it does not
|
|
447
|
+
name holds one subshare. Weights are recorded: each round writes the weight it gave every paired helper
|
|
448
|
+
to that helper's channel record before anything is sent, and the publishes the
|
|
449
|
+
library starts on its own (after pairing, after a fingerprint is confirmed)
|
|
450
|
+
reuse those weights; a newly paired helper starts at 1. The weights also travel
|
|
451
|
+
in the recovered secret (`helpers[].weight`), so `restore` and replica
|
|
452
|
+
hydration write them back into the channel records they create.
|
|
453
|
+
|
|
454
|
+
The threshold is recorded too. A round splits at `ProtectSecretParams.threshold` when
|
|
455
|
+
given, otherwise at the stored threshold (`UserSecrets.threshold`), and only otherwise
|
|
456
|
+
at `withThreshold`, which is therefore the starting threshold for a secret with
|
|
457
|
+
nothing stored. Every publish stores the threshold it used, the recovered
|
|
458
|
+
secret carries it (`secret.threshold`), and `restore` and replica
|
|
459
|
+
hydration put it back, so the publishes the library starts on its own and
|
|
460
|
+
the follow-up after `restore` keep it: leave `threshold` unset there. To
|
|
461
|
+
change it, pass `threshold` on `start(FlowKind.ProtectSecret, …)`. It must be at least 2,
|
|
462
|
+
above every weight, and no greater than a subshare cap; a round that fails
|
|
463
|
+
a check writes and sends nothing. Your `UserSecretStore` must persist the
|
|
464
|
+
snapshot's threshold as given.
|
|
465
|
+
|
|
466
|
+
The intended flow. Weights are the user's choice: before `start`, show the
|
|
467
|
+
user each paired helper's current weight, pre-filled from your `ChannelStore`,
|
|
468
|
+
and pass the user's final choice as the whole `weights` map. Reading the
|
|
469
|
+
stored weights first is the normal flow. Every other `start(FlowKind.ProtectSecret, ...)`
|
|
470
|
+
the app makes, such as a republish after editing the secrets or the follow-up
|
|
471
|
+
after `restore`, must pass the stored weights unchanged: a helper left out
|
|
472
|
+
returns to 1, which silently undoes the user's choice. Only the publishes the
|
|
473
|
+
library starts on its own (after pairing, after a fingerprint is confirmed)
|
|
474
|
+
reuse the stored weights by themselves.
|
|
475
|
+
|
|
476
|
+
A helper record is opaque JSON bytes, `{"Helper": {...}}`. Parsing one to read
|
|
477
|
+
`weight` is safe: it is a small integer, and the channel id comes from the
|
|
478
|
+
`channelId` argument your store received, never from the parsed object's
|
|
479
|
+
`channel_id` (a `u64` that `JSON.parse` can round). What the `ChannelStore`
|
|
480
|
+
docs forbid is re-serialising a parsed record; keep persisting the original
|
|
481
|
+
bytes.
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const weight = JSON.parse(new TextDecoder().decode(helperRecordBytes)).Helper.weight || 1;
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
When the builder's threshold applies (no threshold is stored), lowering it to
|
|
488
|
+
or below a stored weight makes the next publish fail
|
|
489
|
+
with code `invalid_weight` before anything is sent, until `ProtectSecret`
|
|
490
|
+
assigns valid weights.
|
|
491
|
+
|
|
492
|
+
That failure surfaces where the library started the publish, and in each
|
|
493
|
+
case nothing is sent:
|
|
494
|
+
|
|
495
|
+
- `process()` throws an error whose `events` keep the message's events,
|
|
496
|
+
`PairingCompleted` included.
|
|
497
|
+
- `accept()` throws. The pairing has completed and its response was sent,
|
|
498
|
+
but `PairingCompleted` is not returned.
|
|
499
|
+
- `verifyFingerprint()` throws. The promotion stands: calling it again
|
|
500
|
+
resolves `true` and publishes nothing.
|
|
501
|
+
|
|
502
|
+
Call `start` with `ProtectSecret` and valid weights to publish to every
|
|
503
|
+
helper.
|
|
504
|
+
|
|
505
|
+
**Below the threshold.** When the paired helpers' total weight is below the
|
|
506
|
+
threshold, `start` skips the split: no helper receives anything, so no
|
|
507
|
+
`ProtectSecretStarted` / `ProtectSecretFailed` comes back for one. A paired
|
|
508
|
+
replica still gets its usual secret-only copy and events; with none paired,
|
|
509
|
+
`start` resolves with no events at all — this is not an error. The version
|
|
510
|
+
still advances and the secrets/description are saved as the latest
|
|
511
|
+
snapshot, so the next publish that reaches threshold (including one the
|
|
512
|
+
library starts on its own) shares it. Sum the weights about to be assigned
|
|
513
|
+
against the threshold beforehand, or count the returned helper events
|
|
514
|
+
afterward, to tell the two cases apart. This is more reachable with
|
|
515
|
+
weighted helpers than with the default of 1: two helpers at weight 2 against
|
|
516
|
+
a threshold of 3 (total weight 4) drop below it when a publish leaves both
|
|
517
|
+
out of the weights map, resetting each to weight 1 for a total of 2 — a
|
|
518
|
+
risk for a restore follow-up publish that does not pass the stored weights
|
|
519
|
+
unchanged.
|
|
520
|
+
|
|
521
|
+
`withMaxSubshares(max)` on the builder caps the total subshares one round
|
|
522
|
+
splits the secret into — the sum of every helper's weight. Unset means no
|
|
523
|
+
cap. Recovery time grows quadratically with that total (about 0.2 s at 200,
|
|
524
|
+
5.5 s at 1,000 and 87 s at 4,000 subshares per attempt) while splitting stays
|
|
525
|
+
cheap, so this is the place to bound it. A round over the cap fails
|
|
526
|
+
`start(FlowKind.ProtectSecret, ...)` with code `too_many_subshares` before
|
|
527
|
+
anything is sent; publishes the library starts itself count the stored weights
|
|
528
|
+
and respect it too. A cap
|
|
529
|
+
below the threshold makes `build()` throw `invalid_input`. The cap applies at
|
|
530
|
+
sharing only, never at recovery: a recovering device cannot know the cap the
|
|
531
|
+
shares were made under.
|
|
532
|
+
|
|
229
533
|
**Do not call protocol methods from inside a store callback.** A store
|
|
230
534
|
method (`load`, `save`, `remove`, ...) is invoked as a callback into
|
|
231
535
|
JavaScript, with the protocol's worker blocked waiting for the promise that
|
|
@@ -249,6 +553,11 @@ omits `"wasm"`, which a C-ABI binding can never emit, and adds `"ok"`,
|
|
|
249
553
|
`"ffi"` and `"state_store"`, which it can. A test pins the union to the crate's shared enum fixture so it stays in step
|
|
250
554
|
with the Rust enum.
|
|
251
555
|
|
|
556
|
+
**`DeRecError.code` is typed `DeRecErrorCode`.** It lists exactly the codes
|
|
557
|
+
`derec_error_code_name` can return — no WASM-only marshalling codes, since
|
|
558
|
+
this SDK never goes through WASM. A test pins it to the same shared enum
|
|
559
|
+
fixture as `DeRecErrorCategory`.
|
|
560
|
+
|
|
252
561
|
**Ids are decimal strings in stores, `bigint | number` on `DeRecProtocol`.**
|
|
253
562
|
Store interfaces (`ChannelStore`, `ShareStore`, event fields such as
|
|
254
563
|
`channel_id` / `replica_id`, etc.) use decimal strings for every `u64` id,
|
|
@@ -263,6 +572,85 @@ everywhere.
|
|
|
263
572
|
|
|
264
573
|
---
|
|
265
574
|
|
|
575
|
+
## Configuration from a file or record
|
|
576
|
+
|
|
577
|
+
Applications usually load settings as a whole: general settings from the
|
|
578
|
+
environment or a config file, per-user settings from a user record.
|
|
579
|
+
`DeRecProtocolConfig` holds every data setting of the builder, each one
|
|
580
|
+
optional, and `withConfig` applies it in one call:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
import { DeRecProtocolBuilder } from "@derec-alliance/react-native";
|
|
584
|
+
import type { DeRecProtocolConfig } from "@derec-alliance/react-native";
|
|
585
|
+
|
|
586
|
+
const general: DeRecProtocolConfig = JSON.parse(generalJson);
|
|
587
|
+
const perUser: DeRecProtocolConfig = JSON.parse(userRecordJson);
|
|
588
|
+
|
|
589
|
+
const protocol = new DeRecProtocolBuilder(secretId)
|
|
590
|
+
.withChannelStore(channelStore)
|
|
591
|
+
.withShareStore(shareStore)
|
|
592
|
+
.withSecretStore(secretStore)
|
|
593
|
+
.withUserSecretStore(userSecretStore)
|
|
594
|
+
.withStateStore(stateStore)
|
|
595
|
+
.withTransport(myTransport)
|
|
596
|
+
.withConfig(general) // 1. general settings
|
|
597
|
+
.withConfig(perUser) // 2. per-user settings
|
|
598
|
+
.withThreshold(4) // 3. individual setters
|
|
599
|
+
.build();
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
- **Only set (present and non-`null`) fields are applied.** An absent or
|
|
603
|
+
`null` field leaves whatever the builder already holds.
|
|
604
|
+
- **Last write wins, per field.** Nested objects replace the builder's value
|
|
605
|
+
as a whole, except `timeouts`: each of its fields is merged separately, so
|
|
606
|
+
a per-user config that sets only `inbound_message_secs` keeps the
|
|
607
|
+
`sharing_round_secs` the general config set.
|
|
608
|
+
- **This binding assigns; it validates nothing.** Every key is forwarded
|
|
609
|
+
verbatim, under its own spelling, into the same `snake_case` config JSON
|
|
610
|
+
`build()` sends over the FFI. Rust applies the same rules a matching
|
|
611
|
+
`with*` setter's value would and rejects an unknown key at any level — a
|
|
612
|
+
misspelled key, or a value that fails validation, throws from `build()`
|
|
613
|
+
rather than being silently ignored.
|
|
614
|
+
- **Stores, the transport and the constructor's `secretId` are not settings**
|
|
615
|
+
and are not carried in the config.
|
|
616
|
+
- **A `secret_id` key in a config is ignored, not rejected.** `build()`
|
|
617
|
+
assigns the constructor's `secretId` last, so a config carrying a
|
|
618
|
+
`secret_id` key (for example a per-user record that includes it by
|
|
619
|
+
mistake) never changes which secret this instance manages. Every other
|
|
620
|
+
SDK rejects that key outright, since `DeRecProtocolConfig` has no
|
|
621
|
+
`secret_id` field there and unknown keys fail.
|
|
622
|
+
|
|
623
|
+
The canonical JSON, shared by every SDK, is:
|
|
624
|
+
|
|
625
|
+
```json
|
|
626
|
+
{
|
|
627
|
+
"threshold": 3,
|
|
628
|
+
"max_subshares": "12",
|
|
629
|
+
"timeouts": {
|
|
630
|
+
"inbound_message_secs": 300,
|
|
631
|
+
"sharing_round_secs": 60,
|
|
632
|
+
"unpair_ack_secs": 60,
|
|
633
|
+
"expired_channels": { "enabled": true, "timeout_in_secs": 300 }
|
|
634
|
+
},
|
|
635
|
+
"parameter_range": { "min_share_size": 1, "max_share_size": 4096 },
|
|
636
|
+
"auto_accept": { "pairing": true, "store_share": true },
|
|
637
|
+
"auto_respond_on_failure": false,
|
|
638
|
+
"auto_reply_to": false,
|
|
639
|
+
"unpair_ack": "required",
|
|
640
|
+
"unsafe_connection": false,
|
|
641
|
+
"communication_info": { "name": "alice" },
|
|
642
|
+
"own_transports": [{ "uri": "https://me.example.com/derec", "protocol": "https" }],
|
|
643
|
+
"replica_id": "9007199254740993"
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Keys are `snake_case` and every key is optional. `replica_id` and
|
|
648
|
+
`max_subshares` are `u64` values written as decimal strings. Timeouts are
|
|
649
|
+
whole seconds. See the `DeRecProtocolConfig` doc comment in `src/types.ts` for
|
|
650
|
+
every field's accepted forms.
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
266
654
|
## Package contents
|
|
267
655
|
|
|
268
656
|
```text
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/cpp/Convert.cpp
CHANGED
|
@@ -169,6 +169,15 @@ std::optional<DecodedShare> decodeShareRecord(const uint8_t* ptr, size_t len) {
|
|
|
169
169
|
out.secretId = std::move(*secretId);
|
|
170
170
|
out.version = static_cast<uint32_t>(parseUnsignedAt(json, versionPos));
|
|
171
171
|
out.bytes = parseByteArrayAt(json, bytesPos);
|
|
172
|
+
auto optionalUnsigned = [&json](const char* field) -> std::optional<uint32_t> {
|
|
173
|
+
size_t pos = fieldValueOffset(json, field);
|
|
174
|
+
if (pos != std::string::npos && pos < json.size() && json[pos] >= '0' && json[pos] <= '9') {
|
|
175
|
+
return static_cast<uint32_t>(parseUnsignedAt(json, pos));
|
|
176
|
+
}
|
|
177
|
+
return std::nullopt;
|
|
178
|
+
};
|
|
179
|
+
out.subshares = optionalUnsigned("subshares");
|
|
180
|
+
out.threshold = optionalUnsigned("threshold");
|
|
172
181
|
return out;
|
|
173
182
|
}
|
|
174
183
|
|
package/cpp/Convert.h
CHANGED
|
@@ -60,14 +60,20 @@ std::vector<uint8_t> decodeSecretValueBytes(const uint8_t* ptr, size_t len);
|
|
|
60
60
|
|
|
61
61
|
/// A `ShareRecord` as `ShareStore.save` receives it. `secretId` is the
|
|
62
62
|
/// record's own `secret_id`, kept as the decimal string Rust wrote.
|
|
63
|
+
/// `subshares` is the owner-side subshare count and `threshold` the
|
|
64
|
+
/// owner-side threshold the version was split with, each empty when Rust
|
|
65
|
+
/// wrote none.
|
|
63
66
|
struct DecodedShare {
|
|
64
67
|
std::string secretId;
|
|
65
68
|
uint32_t version;
|
|
66
69
|
std::vector<uint8_t> bytes;
|
|
70
|
+
std::optional<uint32_t> subshares;
|
|
71
|
+
std::optional<uint32_t> threshold;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
|
-
/// Decode `{"secret_id":"...","version":<n>,"bytes":[<n>,...]}`.
|
|
70
|
-
/// any of the three fields is absent
|
|
74
|
+
/// Decode `{"secret_id":"...","version":<n>,"bytes":[<n>,...],"subshares":<n>,"threshold":<n>}`.
|
|
75
|
+
/// Empty when any of the first three fields is absent; `subshares` and
|
|
76
|
+
/// `threshold` are optional, and absent or `null` decodes as none.
|
|
71
77
|
std::optional<DecodedShare> decodeShareRecord(const uint8_t* ptr, size_t len);
|
|
72
78
|
|
|
73
79
|
} // namespace derec
|