@derec-alliance/nodejs 0.0.1-alpha.6 → 0.0.1-alpha.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 CHANGED
@@ -53,68 +53,210 @@ These represent **opaque wire-level protocol messages**.
53
53
  ## Quick Example
54
54
 
55
55
  ```ts
56
- import * as derec from "@derec-alliance/nodejs";
56
+ import { primitives } from "@derec-alliance/nodejs";
57
57
 
58
- const version = derec.derec_protocol_version();
58
+ const channelId = 1n; // u64 → bigint
59
+ const secretId = 42n; // u64 → bigint
60
+ const version = 1; // u32 → number
61
+ const sharedKey = new Uint8Array(32); // established during pairing
59
62
 
60
- console.log(version.major, version.minor);
63
+ const result = primitives.verification.request.produce(channelId, secretId, version, sharedKey);
64
+ // result carries the encoded DeRecMessage envelope, ready to send over transport.
61
65
  ```
62
66
 
63
67
  ---
64
68
 
65
69
  ## Pairing Flow
66
70
 
71
+ The `ContactMessage` is exchanged out-of-band (QR codes, existing messaging
72
+ channels, etc.). Two `ContactMode` values select how the public encryption
73
+ material is delivered:
74
+
75
+ | Mode | What the contact carries | Use when |
76
+ |---|---|---|
77
+ | `InlineKeys` (default) | Full ML-KEM encapsulation key + ECIES public key | Out-of-band channel can carry the keys (NFC, messaging). |
78
+ | `HashedKeys` | Only a SHA-384 commitment to the keys | Channel is size-constrained (QR codes). Scanner fetches the actual keys via a plaintext `PrePair` round-trip and verifies them against the hash. |
79
+
80
+ After the handshake completes, **both modes** rekey the channel id. The
81
+ responder derives `SHA-384(u64_be(originalId) || sharedKey)[..8]` as a
82
+ `bigint`, includes it in the encrypted `PairResponseMessage`, and both sides
83
+ switch their local state to the new id. The new id never appears in plaintext
84
+ on the wire, so a passive observer who only saw pre-rekey traffic cannot link
85
+ the long-running channel to its pairing-time id.
86
+
87
+ ### `InlineKeys` flow
88
+
67
89
  ```ts
68
- import * as derec from "@derec-alliance/nodejs";
90
+ import { ContactMode, primitives, SenderKind } from "@derec-alliance/nodejs";
91
+
92
+ const channelId = 1n;
69
93
 
70
- // Step 1: Owner creates contact message (out-of-band)
71
- const contact = derec.create_contact_message(
72
- 1n,
73
- new TextEncoder().encode("wss://owner.example.com")
94
+ // Step 1: Initiator creates the out-of-band ContactMessage.
95
+ const contact = primitives.pairing.request.create_contact(
96
+ channelId,
97
+ ContactMode.InlineKeys,
98
+ { protocol: 0, uri: "https://owner.example.com" },
74
99
  );
75
100
 
76
- // Step 2: Helper produces pairing request
77
- const request = derec.produce_pairing_request_message(
78
- 2, // SenderKind.Helper
79
- new TextEncoder().encode("wss://helper.example.com"),
80
- contact.wire_bytes
101
+ // Step 2: Responder produces a pairing request from the contact.
102
+ const request = primitives.pairing.request.produce(
103
+ SenderKind.Helper,
104
+ { protocol: 0, uri: "https://helper.example.com" },
105
+ contact.contact_message,
106
+ null, // optional CommunicationInfo
81
107
  );
82
108
 
83
- // Step 3: Owner produces pairing response
84
- const response = derec.produce_pairing_response_message(
85
- 0, // SenderKind.SharerNonRecovery
86
- request.wire_bytes,
87
- request.secret_key_material
109
+ // Step 3: Initiator extracts the request and produces the response.
110
+ const { request: pairRequest } =
111
+ primitives.pairing.request.extract(request.envelope, contact.secret_key);
112
+ const produced = primitives.pairing.response.produce(
113
+ channelId,
114
+ pairRequest,
115
+ contact.secret_key,
116
+ null,
88
117
  );
89
118
 
90
- // Step 4: Helper processes response and derives shared key
91
- const result = derec.process_pairing_response_message(
92
- contact.wire_bytes,
93
- response.wire_bytes,
94
- request.secret_key_material
119
+ // Step 4: Responder extracts and processes the response.
120
+ const { response: pairResponse } =
121
+ primitives.pairing.response.extract(produced.envelope, request.secret_key);
122
+ const processed = primitives.pairing.response.process(
123
+ request.initiator_contact_message,
124
+ pairResponse,
125
+ request.secret_key,
95
126
  );
96
127
 
97
- console.log("Shared key length:", result.shared_key.length);
128
+ // Both sides hold the same shared key and rekeyed channel id.
129
+ // produced.shared_key == processed.shared_key
130
+ // produced.channel_id == processed.channel_id !== channelId
131
+ //
132
+ // Rename local channel state from `channelId` to `produced.channel_id`
133
+ // before sending any further traffic.
98
134
  ```
99
135
 
100
- ---
136
+ To reject the request, build a `PairResponseMessage` with a non-OK status and
137
+ encrypt it against `request.ecies_public_key` using the WASM-exposed pairing
138
+ envelope helpers. The higher-level `DeRecProtocol` orchestrator's `reject`
139
+ method does this for you. Rejected responses do not carry a meaningful
140
+ `channel_id` — the rekey only takes effect on `Ok` responses.
101
141
 
102
- ## Share Distribution (Sharing Flow)
142
+ ### `HashedKeys` flow (PrePair)
143
+
144
+ `HashedKeys` adds one plaintext round-trip before the regular `InlineKeys`
145
+ handshake. The scanner fetches the actual keys via `PrePair`, verifies them
146
+ against `contact.contact_binding_hash`, and then runs the normal pairing
147
+ flow on a synthesized contact with the keys filled in.
103
148
 
104
149
  ```ts
105
- import * as derec from "@derec-alliance/nodejs";
106
-
107
- const result = derec.protect_secret(
108
- new Uint8Array([1, 2, 3]), // secret ID
109
- new TextEncoder().encode("super-secret"), // secret bytes
110
- [1n, 2n, 3n], // helper channel IDs
111
- 2, // threshold
112
- 1 // version
150
+ import { ContactMode, primitives, SenderKind, type ContactMessage } from "@derec-alliance/nodejs";
151
+
152
+ const channelId = 7n;
153
+
154
+ // Initiator: HASHED_KEYS contact (no inline keys, only the binding hash).
155
+ // Transport URI MUST be ephemeral — PrePair envelopes are plaintext.
156
+ const contact = primitives.pairing.request.create_contact(
157
+ channelId,
158
+ ContactMode.HashedKeys,
159
+ { protocol: 0, uri: "https://relay.example.com/ephemeral" },
160
+ );
161
+
162
+ // Scanner: fetch keys via PrePair.
163
+ const prePairReqEnv = primitives.pairing.request.produce_pre_pair(
164
+ { protocol: 0, uri: "https://scanner.example.com/ephemeral" },
165
+ contact.contact_message,
166
+ );
167
+ const { request: prePairReq } =
168
+ primitives.pairing.request.extract_pre_pair(prePairReqEnv.envelope);
169
+ const prePairRespEnv = primitives.pairing.response.produce_pre_pair(
170
+ channelId, prePairReq, contact.secret_key,
113
171
  );
172
+ const { response: prePairResp } =
173
+ primitives.pairing.response.extract_pre_pair(prePairRespEnv.envelope);
114
174
 
115
- const shareMessages = result.share_message_wire_bytes_array;
175
+ // Scanner validates the published keys against contact.contact_binding_hash.
176
+ // Throws on mismatch (returns the keys + echoed nonce on match).
177
+ const validated = primitives.pairing.response.process_pre_pair(
178
+ contact.contact_message, prePairResp,
179
+ );
116
180
 
117
- console.log(shareMessages);
181
+ // Synthesize a "filled-in" contact and run the regular pairing flow. The
182
+ // mode flip is required — `primitives.pairing.request.produce` enforces
183
+ // `InlineKeys` and rejects a contact that still advertises `HashedKeys`.
184
+ const { contact_binding_hash: _omitBindingHash, ...contactBase } =
185
+ contact.contact_message;
186
+ const filledInContact: ContactMessage = {
187
+ ...contactBase,
188
+ contact_mode: ContactMode.InlineKeys,
189
+ mlkem_encapsulation_key: validated.mlkem_encapsulation_key,
190
+ ecies_public_key: validated.ecies_public_key,
191
+ };
192
+ // ... continue with primitives.pairing.request.produce / extract /
193
+ // primitives.pairing.response.produce / process against `filledInContact`
194
+ // exactly as in the InlineKeys example.
195
+ ```
196
+
197
+ After the PrePair exchange the application **must** swap the transport
198
+ endpoint to a long-term one via `UpdateChannelInfo`. The ephemeral endpoint
199
+ advertised in the `HashedKeys` contact is intended to be retired immediately
200
+ after pairing.
201
+
202
+ #### Using `DeRecProtocol` instead
203
+
204
+ The orchestrator handles the whole chain automatically:
205
+
206
+ - **Contact creator** — `protocol.createContact(channelId, ContactMode.HashedKeys)`
207
+ returns the small contact (binding hash only). When the scanner's
208
+ `PrePairRequest` arrives, `protocol.process(bytes)` emits an
209
+ `ActionRequired` event with `action_kind: "PrePair"`. Call
210
+ `protocol.accept(action)` to publish the keys (the library builds the
211
+ response and routes it), or `protocol.reject(action, status, memo)` to
212
+ refuse.
213
+ - **Scanner** — `protocol.start(FlowKind.Pairing, { kind, contact })` kicks
214
+ off the plaintext PrePair leg. `start()` returns a `DeRecEvent[]`
215
+ containing one `PairingStarted { channel_id, kind }` event that
216
+ describes the dispatched handshake; the scanner auto-proceeds to
217
+ `PairRequest`, and the application sees `PairingCompleted` only when
218
+ the final response lands via `process()`. Failure modes:
219
+ - Contact creator rejected → `DeRecEvent` with
220
+ `type: "PrePairRejected"`, plus `status` / `memo`.
221
+ - Binding-hash mismatch → `protocol.process(...)` throws a
222
+ `DeRecException`-shape error whose `message` carries
223
+ `"contact binding hash mismatch"`. This is security-relevant — the
224
+ keys published by the peer do not match the commitment the scanner
225
+ originally accepted.
226
+
227
+ End-to-end orchestrator-level coverage is in
228
+ `bindings/nodejs/protocol.ts::runHashedKeysPairingFlow` (happy path +
229
+ tampered-hash assertion).
230
+
231
+ ---
232
+
233
+ ## Share Distribution (Sharing Flow)
234
+
235
+ ```ts
236
+ import { primitives } from "@derec-alliance/nodejs";
237
+
238
+ const secretId = 42n; // u64
239
+ const secretData = new TextEncoder().encode("super-secret");
240
+ const channelIds = [1n, 2n, 3n];
241
+ const threshold = 2; // must be 2 <= threshold <= channelIds.length
242
+ const version = 1;
243
+ // sharedKeys: Map<bigint, Uint8Array> with the 32-byte channel keys
244
+
245
+ const splitResult = primitives.sharing.request.split(
246
+ secretId,
247
+ secretData,
248
+ channelIds,
249
+ threshold,
250
+ version,
251
+ );
252
+ // splitResult.value: Map<bigint, Uint8Array> — one CommittedDeRecShare per helper.
253
+
254
+ // Wrap each share into an encrypted delivery envelope.
255
+ for (const [channelId, committedShare] of splitResult.value) {
256
+ const envelope = primitives.sharing.request.produce(
257
+ channelId, version, secretId, committedShare, [], "", sharedKeys.get(channelId)!,
258
+ );
259
+ }
118
260
  ```
119
261
 
120
262
  ---
@@ -122,54 +264,93 @@ console.log(shareMessages);
122
264
  ## Recovery Flow
123
265
 
124
266
  ```ts
125
- import * as derec from "@derec-alliance/nodejs";
267
+ import { primitives } from "@derec-alliance/nodejs";
126
268
 
127
- // Owner requests shares from helpers
128
- const request = derec.generate_share_request(
129
- new Uint8Array([1, 2, 3]), // secret ID
130
- 1 // version
269
+ const secretId = 42n; // u64
270
+ const version = 1; // u32
271
+
272
+ // Owner side: produce the recovery request.
273
+ const shareRequest = primitives.recovery.request.produce(
274
+ 1n, // channel ID
275
+ secretId,
276
+ version,
277
+ sharedKey,
131
278
  );
132
279
 
133
- // Helper responds with its share
134
- const response = derec.generate_share_response(
135
- request,
136
- new Uint8Array() // share content
280
+ // Helper side: produce the response using the StoreShareRequest it persisted
281
+ // at sharing time.
282
+ const shareResponse = primitives.recovery.response.produce(
283
+ secretId,
284
+ 1n, // channel ID
285
+ storedShareEnvelope,
286
+ shareRequest,
287
+ sharedKey,
137
288
  );
138
289
 
139
- // Owner reconstructs secret from aggregated responses
140
- const secret = derec.recover_from_share_responses(
141
- new Uint8Array(), // aggregated responses
142
- new Uint8Array([1, 2, 3]),
143
- 1
290
+ // Owner side: collect at least `threshold` responses and reconstruct.
291
+ const recovered = primitives.recovery.response.recover(
292
+ [
293
+ { response: shareResponse, shared_key: sharedKey },
294
+ // …additional helper responses…
295
+ ],
296
+ secretId,
297
+ version,
144
298
  );
299
+ // `recovered` is a Uint8Array carrying the reconstructed secret payload.
300
+ ```
145
301
 
146
- console.log(secret);
302
+ When driving the protocol layer instead of the primitives, the recovering
303
+ device receives a `SecretRecovered` event carrying the typed `secret`. Pass it
304
+ to `protocol.restore(secret, version)` on a fresh `DeRecProtocol` instance to
305
+ commit canonical helper / replica state and wipe the throwaway recovery-mode
306
+ channels — at that point the device resumes normal operation as if the secret
307
+ had been protected here originally.
308
+
309
+ ```ts
310
+ const events = await protocol.process(responseBytes);
311
+ for (const ev of events) {
312
+ if (ev.type === "SecretRecovered") {
313
+ await freshProtocol.restore(ev.secret, recoveredVersion);
314
+ }
315
+ }
147
316
  ```
148
317
 
318
+ Errors surface as objects with a `code` field — `ALREADY_RESTORED`,
319
+ `CONFLICT` (with `channel_ids`), `INVARIANT`, or `STORAGE`.
320
+
321
+ > **Secret format:** the recoverable secret (the bytes helpers store and
322
+ > recovery reconstructs) is `[version byte] · payload` — v1's payload is
323
+ > **gzip (RFC 1952)** compressed **JSON**, with byte fields as **standard
324
+ > base64 with padding (RFC 4648 §4)** and `u64` fields as decimal strings.
325
+ > See the `derec-library` `protocol::types::secret` reference documentation
326
+ > for the full field schema.
327
+
149
328
  ---
150
329
 
151
330
  ## Verification Flow
152
331
 
153
332
  ```ts
154
- import * as derec from "@derec-alliance/nodejs";
155
-
156
- // Owner challenges helper
157
- const request = derec.generate_verification_request(
158
- new Uint8Array([1, 2, 3]),
159
- 1
160
- );
161
-
162
- // Helper proves it still holds the share
163
- const response = derec.generate_verification_response(
164
- new Uint8Array(), // share content
165
- request
333
+ import { primitives } from "@derec-alliance/nodejs";
334
+
335
+ // Owner side: produce the verification request.
336
+ const requestEnvelope = primitives.verification.request.produce(channelId, secretId, version, sharedKey);
337
+
338
+ // Helper side: decrypt and extract the challenge fields.
339
+ const req = primitives.verification.request.extract(requestEnvelope, sharedKey);
340
+ // req.channel_id, req.secret_id, req.version, req.nonce
341
+
342
+ // Helper side: produce the response.
343
+ const responseEnvelope = primitives.verification.response.produce(
344
+ channelId,
345
+ req.secret_id,
346
+ req.version,
347
+ req.nonce,
348
+ sharedKey,
349
+ storedShareEnvelope
166
350
  );
167
351
 
168
- const isValid = derec.verify_share_response(
169
- new Uint8Array(), // share content
170
- request,
171
- response
172
- );
352
+ // Owner side: verify the response.
353
+ const isValid = primitives.verification.response.process(responseEnvelope, sharedKey, storedShareEnvelope);
173
354
 
174
355
  console.log("Valid:", isValid);
175
356
  ```
@@ -185,23 +366,179 @@ console.log("Valid:", isValid);
185
366
 
186
367
  ---
187
368
 
369
+ ## Replica flows
370
+
371
+ Replicas mirror an Owner's secret onto a second device so the same secrets
372
+ remain reachable after device loss. Pairings are **unidirectional** — one
373
+ side runs as `SenderKind.ReplicaSource` (owns the secret), the other as
374
+ `SenderKind.ReplicaDestination` (receives it). Both must be constructed
375
+ with a stable `replicaId`:
376
+
377
+ ```ts
378
+ const owner = new DeRecProtocol(
379
+ channelStore, shareStore, secretStore, transport,
380
+ "https://owner.example.com", "https",
381
+ /* threshold */ 2, /* keepVersionsCount */ 3,
382
+ { name: "Owner" },
383
+ null, null, null, null,
384
+ /* replicaId */ 0xAAAA_AAAA_AAAA_AAAAn,
385
+ );
386
+ ```
387
+
388
+ A typical Source↔Destination handshake:
389
+
390
+ ```ts
391
+ const contact = await owner.createContact(channelId, ContactMode.InlineKeys);
392
+ await destination.start(FlowKind.Pairing, {
393
+ kind: SenderKind.ReplicaDestination,
394
+ contact,
395
+ });
396
+ // pump messages between the two protocols (drain transport → process)
397
+ ```
398
+
399
+ The channel ends up in `Pending` and is NOT eligible as a
400
+ `ProtectSecret` target until both sides confirm a deterministic
401
+ fingerprint derived from the shared key:
402
+
403
+ ```ts
404
+ const localFp = await owner.getFingerprint(channelId);
405
+ const peerFp = await destination.getFingerprint(channelId); // out of band
406
+
407
+ await owner.verifyFingerprint(channelId, peerFp); // → true
408
+ await destination.verifyFingerprint(channelId, localFp); // → true
409
+ ```
410
+
411
+ Once paired, the Source includes the Destination as a `ProtectSecret`
412
+ target alongside helpers. Helpers receive the usual VSS share via
413
+ `StoreShareRequest`; the Destination receives the full secret as a
414
+ typed `ReplicaSecretReceived` event:
415
+
416
+ ```ts
417
+ {
418
+ type: "ReplicaSecretReceived",
419
+ channel_id, from_replica_id, secret_id, version,
420
+ secret: {
421
+ helpers: [...], // every paired helper (channel_id, transport_uri, shared_key, ...)
422
+ secrets: [{ id, name, data }],
423
+ replicas: [...], // every paired destination (replica_id, sender_kind, ...)
424
+ owner_replica_id, // the Source's replica_id
425
+ },
426
+ shares: [{ channel_id, committed_share }, ...], // helper channel_id → share bytes
427
+ }
428
+ ```
429
+
430
+ `secret` + `shares` give the Destination everything it needs to act in the
431
+ Source's place during recovery.
432
+
433
+ End-to-end coverage lives in
434
+ [`runReplicaPairingAndSecretSyncFlow`](../../bindings/nodejs/protocol.ts).
435
+
436
+ ---
437
+
438
+ ## Correlation and routing
439
+
440
+ Two cross-cutting metadata fields appear on every channel-mode exchange:
441
+
442
+ - **`traceId`** — opaque `bigint` on the outer envelope, used to correlate
443
+ responses with requests. The `DeRecProtocol` orchestrator handles this
444
+ end-to-end (random token on every outbound request, echo on every
445
+ response). Primitive-only callers can manipulate it directly via
446
+ `envelope.apply_trace_id(bytes, traceId)` and `envelope.read_trace_id(bytes)`.
447
+ - **`replyTo`** — optional `TransportProtocol` on request bodies, telling
448
+ the responder to route this exchange's response to an alternate endpoint.
449
+ Set it per call (every `primitives.*.request.produce` takes a trailing
450
+ `reply_to` arg) or protocol-wide with the `autoReplyTo` constructor flag
451
+ on `DeRecProtocol` (stamps `replyTo = ownTransport` on every outbound
452
+ request). Excludes pairing and `UpdateChannelInfo`, which already carry
453
+ their own `transportProtocol` field.
454
+
455
+ The motivating case for `replyTo` is replicas: when Replica A sends a
456
+ request on a channel the helper paired with sibling Replica B, the
457
+ helper's stored peer endpoint points at B. `replyTo` lets A say "send the
458
+ response back to me," without rewriting channel state.
459
+
460
+ ---
461
+
188
462
  ## Package Contents
189
463
 
190
464
  ```text
191
465
  derec_library_bg.wasm
192
466
  derec_library.js
193
467
  derec_library.d.ts
468
+ index.js
469
+ index.d.ts
194
470
  ```
195
471
 
196
472
  - `.wasm` — compiled Rust core
197
- - `.js` — bindings
198
- - `.d.ts` — TypeScript definitions
473
+ - `derec_library.js` / `derec_library.d.ts` — raw wasm-bindgen bindings
474
+ - `index.js` / `index.d.ts` — `primitives.*` namespace assembly and TypeScript declarations
475
+
476
+ ---
477
+
478
+ ## Security considerations
479
+
480
+ ### Replica destinations inherit Source trust
481
+
482
+ `ReplicaSecretReceived.secret` carries the full secret, which
483
+ embeds every helper's `channel_id` and `shared_key`. Anyone holding the
484
+ secret can therefore authenticate as the Source toward every helper.
485
+ This is intentional — it is what makes Destination-driven recovery
486
+ work — but it means a compromised Destination can impersonate the
487
+ Source against every helper paired at the time the secret was sent.
488
+ Pick Destinations with at least the trust level of the Source device
489
+ itself; do not treat them as opaque backups.
490
+
491
+ All replicas of one `secret_id` also share a single **group channel
492
+ key**: every replica channel's `SharedKey` entry in the secret store
493
+ holds the same 32 bytes, established at the first replica pair and
494
+ handed to every subsequent joiner via the
495
+ `ReplicaSecretPayload.shared_key` field on its first sync round.
496
+ Compromise of any one Destination therefore exposes that single key;
497
+ the protocol does not provide per-pair forward secrecy across replicas.
498
+
499
+ ### `ContactMode.HashedKeys` requires an ephemeral transport URI
500
+
501
+ `HashedKeys` ships only a SHA-384 binding hash in the contact and
502
+ serves the actual public keys through a plaintext PrePair round-trip
503
+ on the contact creator's own transport. Any party that can reach that
504
+ URI before the legitimate scanner gets the keys. Use `HashedKeys` only
505
+ with a transport endpoint that is freshly minted for the pairing and
506
+ that you can retire as soon as the PrePair leg completes.
507
+ `ContactMode.InlineKeys` has no such constraint.
508
+
509
+ The recommended pattern is: pair on the ephemeral URI, then — as soon
510
+ as the pairing completes on the contact creator side — call
511
+ `setOwnTransport` with the permanent endpoint and start an
512
+ `UpdateChannelInfo` flow against the peer to announce the swap. Once
513
+ the peer acknowledges, retire the ephemeral URI. This keeps the
514
+ plaintext PrePair window tight while letting subsequent traffic ride
515
+ on the long-lived endpoint.
516
+
517
+ ### Replica fingerprint verification is mandatory
518
+
519
+ Replica channels are created with `status: "Pending"` and remain there
520
+ until both sides call `verifyFingerprint` with the value the peer
521
+ derived from the shared key — confirmed out of band. The orchestrator
522
+ enforces this: `start(FlowKind.ProtectSecret, ...)` throws when a
523
+ target is still `Pending`. Treat verification as a required step in
524
+ the pairing UX — a scanner that auto-pairs without it accepts a
525
+ MITM-vulnerable replica.
526
+
527
+ ### The `derec.*` namespace in `communicationInfo` is library-owned
528
+
529
+ `communicationInfo` is otherwise an opaque app-defined map, but every
530
+ key under the `derec.` prefix is reserved for the protocol. Today the
531
+ library owns `derec.replica_id`; future protocol additions will use
532
+ the same namespace. Application code must not write any `derec.*`
533
+ entry — the orchestrator silently overwrites or strips library-owned
534
+ keys at the protocol boundary, and app-set values are lost without
535
+ warning.
199
536
 
200
537
  ---
201
538
 
202
539
  ## Documentation
203
540
 
204
- - DeRec Alliance: https://derecalliance.org
541
+ - DeRec Alliance: https://derec.org
205
542
  - Protocol specification: https://derec-alliance.gitbook.io/docs/protocol-specification/protocol-overview
206
543
  - Rust SDK: https://github.com/derecalliance/lib-derec
207
544