@bcts/frost-hubert 1.0.0-alpha.17

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.
Files changed (114) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +35 -0
  3. package/dist/bin/frost.cjs +109 -0
  4. package/dist/bin/frost.cjs.map +1 -0
  5. package/dist/bin/frost.d.cts +1 -0
  6. package/dist/bin/frost.d.mts +1 -0
  7. package/dist/bin/frost.mjs +109 -0
  8. package/dist/bin/frost.mjs.map +1 -0
  9. package/dist/chunk-CQwRTUmo.cjs +53 -0
  10. package/dist/chunk-D3JzZLW2.mjs +21 -0
  11. package/dist/cmd/index.cjs +45 -0
  12. package/dist/cmd/index.d.cts +4 -0
  13. package/dist/cmd/index.d.mts +4 -0
  14. package/dist/cmd/index.mjs +7 -0
  15. package/dist/cmd-C8pmNd28.mjs +4664 -0
  16. package/dist/cmd-C8pmNd28.mjs.map +1 -0
  17. package/dist/cmd-CxUgryx_.cjs +4803 -0
  18. package/dist/cmd-CxUgryx_.cjs.map +1 -0
  19. package/dist/dkg/index.cjs +7 -0
  20. package/dist/dkg/index.d.cts +2 -0
  21. package/dist/dkg/index.d.mts +2 -0
  22. package/dist/dkg/index.mjs +3 -0
  23. package/dist/dkg-D4RcblWl.cjs +364 -0
  24. package/dist/dkg-D4RcblWl.cjs.map +1 -0
  25. package/dist/dkg-DqGrAV81.mjs +334 -0
  26. package/dist/dkg-DqGrAV81.mjs.map +1 -0
  27. package/dist/frost/index.cjs +37 -0
  28. package/dist/frost/index.d.cts +207 -0
  29. package/dist/frost/index.d.cts.map +1 -0
  30. package/dist/frost/index.d.mts +207 -0
  31. package/dist/frost/index.d.mts.map +1 -0
  32. package/dist/frost/index.mjs +3 -0
  33. package/dist/frost-CMH1K0Cw.cjs +511 -0
  34. package/dist/frost-CMH1K0Cw.cjs.map +1 -0
  35. package/dist/frost-Csp0IOrd.mjs +326 -0
  36. package/dist/frost-Csp0IOrd.mjs.map +1 -0
  37. package/dist/index-BGVoWW5P.d.cts +172 -0
  38. package/dist/index-BGVoWW5P.d.cts.map +1 -0
  39. package/dist/index-BJeUYrdE.d.mts +396 -0
  40. package/dist/index-BJeUYrdE.d.mts.map +1 -0
  41. package/dist/index-ByMDUYKw.d.mts +1098 -0
  42. package/dist/index-ByMDUYKw.d.mts.map +1 -0
  43. package/dist/index-DejLkr_F.d.mts +172 -0
  44. package/dist/index-DejLkr_F.d.mts.map +1 -0
  45. package/dist/index-Dib1OE-e.d.cts +1098 -0
  46. package/dist/index-Dib1OE-e.d.cts.map +1 -0
  47. package/dist/index-DnvBKgec.d.cts +396 -0
  48. package/dist/index-DnvBKgec.d.cts.map +1 -0
  49. package/dist/index.cjs +85 -0
  50. package/dist/index.cjs.map +1 -0
  51. package/dist/index.d.cts +15 -0
  52. package/dist/index.d.cts.map +1 -0
  53. package/dist/index.d.mts +15 -0
  54. package/dist/index.d.mts.map +1 -0
  55. package/dist/index.mjs +24 -0
  56. package/dist/index.mjs.map +1 -0
  57. package/dist/registry/index.cjs +13 -0
  58. package/dist/registry/index.d.cts +2 -0
  59. package/dist/registry/index.d.mts +2 -0
  60. package/dist/registry/index.mjs +3 -0
  61. package/dist/registry-CBjRRqNv.mjs +144 -0
  62. package/dist/registry-CBjRRqNv.mjs.map +1 -0
  63. package/dist/registry-CWp2amuo.mjs +789 -0
  64. package/dist/registry-CWp2amuo.mjs.map +1 -0
  65. package/dist/registry-D5yh293y.cjs +857 -0
  66. package/dist/registry-D5yh293y.cjs.map +1 -0
  67. package/dist/registry-DNUNW6SH.cjs +163 -0
  68. package/dist/registry-DNUNW6SH.cjs.map +1 -0
  69. package/package.json +119 -0
  70. package/src/bin/frost.ts +218 -0
  71. package/src/cmd/busy.ts +64 -0
  72. package/src/cmd/check.ts +20 -0
  73. package/src/cmd/common.ts +40 -0
  74. package/src/cmd/dkg/common.ts +275 -0
  75. package/src/cmd/dkg/coordinator/finalize.ts +592 -0
  76. package/src/cmd/dkg/coordinator/index.ts +12 -0
  77. package/src/cmd/dkg/coordinator/invite.ts +217 -0
  78. package/src/cmd/dkg/coordinator/round1.ts +889 -0
  79. package/src/cmd/dkg/coordinator/round2.ts +959 -0
  80. package/src/cmd/dkg/index.ts +11 -0
  81. package/src/cmd/dkg/participant/finalize.ts +575 -0
  82. package/src/cmd/dkg/participant/index.ts +12 -0
  83. package/src/cmd/dkg/participant/receive.ts +348 -0
  84. package/src/cmd/dkg/participant/round1.ts +464 -0
  85. package/src/cmd/dkg/participant/round2.ts +627 -0
  86. package/src/cmd/index.ts +18 -0
  87. package/src/cmd/parallel.ts +334 -0
  88. package/src/cmd/registry/index.ts +88 -0
  89. package/src/cmd/registry/owner/index.ts +9 -0
  90. package/src/cmd/registry/owner/set.ts +70 -0
  91. package/src/cmd/registry/participant/add.ts +70 -0
  92. package/src/cmd/registry/participant/index.ts +9 -0
  93. package/src/cmd/sign/common.ts +108 -0
  94. package/src/cmd/sign/coordinator/index.ts +11 -0
  95. package/src/cmd/sign/coordinator/invite.ts +431 -0
  96. package/src/cmd/sign/coordinator/round1.ts +751 -0
  97. package/src/cmd/sign/coordinator/round2.ts +836 -0
  98. package/src/cmd/sign/index.ts +11 -0
  99. package/src/cmd/sign/participant/finalize.ts +823 -0
  100. package/src/cmd/sign/participant/index.ts +12 -0
  101. package/src/cmd/sign/participant/receive.ts +378 -0
  102. package/src/cmd/sign/participant/round1.ts +479 -0
  103. package/src/cmd/sign/participant/round2.ts +748 -0
  104. package/src/cmd/storage.ts +116 -0
  105. package/src/dkg/group-invite.ts +414 -0
  106. package/src/dkg/index.ts +10 -0
  107. package/src/dkg/proposed-participant.ts +132 -0
  108. package/src/frost/index.ts +456 -0
  109. package/src/index.ts +45 -0
  110. package/src/registry/group-record.ts +392 -0
  111. package/src/registry/index.ts +12 -0
  112. package/src/registry/owner-record.ts +146 -0
  113. package/src/registry/participant-record.ts +186 -0
  114. package/src/registry/registry-impl.ts +364 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * DKG participant round 1 command.
3
+ *
4
+ * Port of cmd/dkg/participant/round1.rs from frost-hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+
12
+ import { ARID, JSON as JSONWrapper, XID } from "@bcts/components";
13
+ import { Envelope } from "@bcts/envelope";
14
+ import { SealedResponse } from "@bcts/gstp";
15
+ import type { XIDDocument } from "@bcts/xid";
16
+
17
+ import {
18
+ ContributionPaths,
19
+ GroupRecord,
20
+ Registry,
21
+ resolveRegistryPath,
22
+ } from "../../../registry/index.js";
23
+ import { getWithIndicator, putWithIndicator } from "../../busy.js";
24
+ import { createStorageClient, type StorageClient, type StorageSelection } from "../../storage.js";
25
+ import { groupStateDir } from "../../common.js";
26
+ import {
27
+ buildGroupParticipants,
28
+ groupParticipantFromRegistry,
29
+ parseAridUr,
30
+ parseEnvelopeUr,
31
+ } from "../common.js";
32
+ import {
33
+ dkgPart1,
34
+ identifierFromU16,
35
+ createRng,
36
+ bytesToHex,
37
+ type DkgRound1Package,
38
+ type DkgRound1SecretPackage,
39
+ } from "../../../frost/index.js";
40
+ import { Ed25519Sha512, serde } from "@frosts/ed25519";
41
+ import { decodeInviteDetails } from "./receive.js";
42
+ import { CborDate } from "@bcts/dcbor";
43
+
44
+ /**
45
+ * Options for the DKG round1 command.
46
+ */
47
+ export interface DkgRound1Options {
48
+ registryPath?: string;
49
+ timeoutSeconds?: number;
50
+ responseArid?: string;
51
+ preview?: boolean;
52
+ rejectReason?: string;
53
+ sender?: string;
54
+ invite: string;
55
+ storageSelection?: StorageSelection;
56
+ verbose?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Result of the DKG round1 command.
61
+ */
62
+ export interface DkgRound1Result {
63
+ accepted: boolean;
64
+ listeningArid?: string;
65
+ envelopeUr?: string;
66
+ }
67
+
68
+ /**
69
+ * Resolve an invite envelope from either storage (ARID) or direct UR.
70
+ *
71
+ * Port of `resolve_invite_envelope()` from cmd/dkg/participant/round1.rs lines 256-288.
72
+ */
73
+ async function resolveInviteEnvelope(
74
+ selection: StorageSelection | undefined,
75
+ invite: string,
76
+ timeout?: number,
77
+ ): Promise<Envelope> {
78
+ if (selection !== undefined) {
79
+ // Try to parse as ARID
80
+ try {
81
+ const arid = parseAridUr(invite);
82
+ const client = await createStorageClient(selection);
83
+ const envelope = await getWithIndicator(client, arid, "Invite", timeout, false);
84
+ if (envelope === null || envelope === undefined) {
85
+ throw new Error("Invite not found in Hubert storage");
86
+ }
87
+ return envelope;
88
+ } catch (e) {
89
+ // Not an ARID, fall through to envelope parsing
90
+ if (e instanceof Error && e.message.includes("Invite not found in Hubert storage")) {
91
+ throw e;
92
+ }
93
+ }
94
+
95
+ if (timeout !== undefined) {
96
+ throw new Error("--timeout is only valid when retrieving invites from Hubert");
97
+ }
98
+
99
+ return parseEnvelopeUr(invite);
100
+ }
101
+
102
+ // No storage selection
103
+ try {
104
+ parseAridUr(invite);
105
+ throw new Error("Hubert storage parameters are required to retrieve invites by ARID");
106
+ } catch (e) {
107
+ // Not an ARID, parse as envelope
108
+ if (e instanceof Error && e.message.includes("Hubert storage parameters are required")) {
109
+ throw e;
110
+ }
111
+ }
112
+
113
+ return parseEnvelopeUr(invite);
114
+ }
115
+
116
+ /**
117
+ * Build the response body envelope.
118
+ *
119
+ * Port of `build_response_body()` from cmd/dkg/participant/round1.rs lines 290-308.
120
+ */
121
+ function buildResponseBody(
122
+ groupId: ARID,
123
+ participant: XID,
124
+ responseArid: ARID,
125
+ round1Package: DkgRound1Package | undefined,
126
+ ): Envelope {
127
+ let envelope = Envelope.unit()
128
+ .addType("dkgRound1Response")
129
+ .addAssertion("group", groupId)
130
+ .addAssertion("participant", participant)
131
+ .addAssertion("response_arid", responseArid);
132
+
133
+ if (round1Package !== undefined) {
134
+ // Serialize the package to JSON and wrap as CBOR JSON
135
+ const packageJson = serde.round1PackageToJson(round1Package);
136
+ const jsonStr = globalThis.JSON.stringify(packageJson);
137
+ const jsonBytes = new TextEncoder().encode(jsonStr);
138
+ const jsonWrapper = JSONWrapper.fromData(jsonBytes);
139
+ // Pass the JSONWrapper directly - it implements CborTaggedEncodable
140
+ envelope = envelope.addAssertion("round1_package", jsonWrapper);
141
+ }
142
+
143
+ return envelope;
144
+ }
145
+
146
+ /**
147
+ * Serialize round 1 secret package to JSON-compatible format.
148
+ *
149
+ * The @frosts/ed25519 serde module doesn't provide a serializer for SecretPackage,
150
+ * so we manually serialize it here.
151
+ */
152
+ function serializeRound1SecretPackage(secret: DkgRound1SecretPackage): Record<string, unknown> {
153
+ // Access the coefficients and serialize them
154
+ const coefficients = secret.coefficients();
155
+ const serializedCoefficients = coefficients.map((c: unknown) =>
156
+ bytesToHex(
157
+ Ed25519Sha512.serializeScalar(c as Parameters<typeof Ed25519Sha512.serializeScalar>[0]),
158
+ ),
159
+ );
160
+
161
+ // Get the commitment coefficients
162
+ const commitment = secret.commitment;
163
+ const commitmentCoefficients = commitment.serialize().map((c: Uint8Array) => bytesToHex(c));
164
+
165
+ return {
166
+ header: serde.DEFAULT_HEADER,
167
+ identifier: bytesToHex(secret.identifier.serialize()),
168
+ coefficients: serializedCoefficients,
169
+ commitment: commitmentCoefficients,
170
+ min_signers: secret.minSigners,
171
+ max_signers: secret.maxSigners,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Persist round 1 state to disk.
177
+ *
178
+ * Port of `persist_round1_state()` from cmd/dkg/participant/round1.rs lines 310-337.
179
+ */
180
+ function persistRound1State(
181
+ registryPath: string,
182
+ groupId: ARID,
183
+ round1Secret: DkgRound1SecretPackage,
184
+ round1Package: DkgRound1Package,
185
+ ): ContributionPaths {
186
+ const dir = groupStateDir(registryPath, groupId.hex());
187
+ fs.mkdirSync(dir, { recursive: true });
188
+
189
+ const secretPath = path.join(dir, "round1_secret.json");
190
+ const packagePath = path.join(dir, "round1_package.json");
191
+
192
+ // Serialize the secret package manually since serde doesn't provide it
193
+ const secretJson = serializeRound1SecretPackage(round1Secret);
194
+ // Serialize the public package using the standard serde function
195
+ const packageJson = serde.round1PackageToJson(round1Package);
196
+
197
+ fs.writeFileSync(secretPath, globalThis.JSON.stringify(secretJson, null, 2));
198
+ fs.writeFileSync(packagePath, globalThis.JSON.stringify(packageJson, null, 2));
199
+
200
+ return new ContributionPaths({
201
+ round1Secret: secretPath,
202
+ round1Package: packagePath,
203
+ round2Secret: undefined,
204
+ keyPackage: undefined,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Execute the DKG participant round 1 command.
210
+ *
211
+ * Responds to the DKG invite with commitment packages.
212
+ *
213
+ * Port of `CommandArgs::exec()` from cmd/dkg/participant/round1.rs lines 66-254.
214
+ */
215
+ export async function round1(
216
+ _client: StorageClient | undefined,
217
+ options: DkgRound1Options,
218
+ cwd: string,
219
+ ): Promise<DkgRound1Result> {
220
+ // Validate options
221
+ if (options.storageSelection === undefined && options.timeoutSeconds !== undefined) {
222
+ throw new Error("--timeout requires Hubert storage parameters");
223
+ }
224
+ if (options.storageSelection !== undefined && options.preview === true) {
225
+ throw new Error("--preview cannot be used with Hubert storage options");
226
+ }
227
+
228
+ const registryPath = resolveRegistryPath(options.registryPath, cwd);
229
+ const registry = Registry.load(registryPath);
230
+
231
+ const owner = registry.owner();
232
+ if (!owner) {
233
+ throw new Error("Registry owner with private keys is required");
234
+ }
235
+
236
+ // Resolve expected sender if provided
237
+ let expectedSender: XIDDocument | undefined;
238
+ if (options.sender !== undefined) {
239
+ expectedSender = resolveSenderXidDocument(registry, options.sender);
240
+ }
241
+
242
+ const nextResponseArid =
243
+ options.responseArid !== undefined ? parseAridUr(options.responseArid) : ARID.new();
244
+
245
+ // Resolve the invite envelope
246
+ const inviteEnvelope = await resolveInviteEnvelope(
247
+ options.storageSelection,
248
+ options.invite,
249
+ options.timeoutSeconds,
250
+ );
251
+
252
+ // Decode the invite details
253
+ const now = CborDate.now().datetime();
254
+ const details = decodeInviteDetails(
255
+ inviteEnvelope,
256
+ now,
257
+ registry,
258
+ owner.xidDocument(),
259
+ expectedSender,
260
+ );
261
+
262
+ // Sort participants by XID and find our position
263
+ const sortedParticipants = [...details.participants].sort((a, b) =>
264
+ a.xid().urString().localeCompare(b.xid().urString()),
265
+ );
266
+
267
+ const ownerIndex = sortedParticipants.findIndex(
268
+ (doc) => doc.xid().urString() === owner.xid().urString(),
269
+ );
270
+ if (ownerIndex === -1) {
271
+ throw new Error("Invite does not include the registry owner");
272
+ }
273
+
274
+ const identifierIndex = ownerIndex + 1; // FROST uses 1-indexed identifiers
275
+ if (identifierIndex > 65535) {
276
+ throw new Error("Too many participants for identifiers");
277
+ }
278
+ const identifier = identifierFromU16(identifierIndex);
279
+
280
+ const total = sortedParticipants.length;
281
+ if (total > 65535) {
282
+ throw new Error("Too many participants for FROST identifiers");
283
+ }
284
+
285
+ const minSigners = details.invitation.minSigners();
286
+ if (minSigners > 65535) {
287
+ throw new Error("min_signers does not fit into identifier space");
288
+ }
289
+
290
+ // Build group participants for the registry
291
+ const groupParticipants = buildGroupParticipants(registry, owner, sortedParticipants);
292
+ const coordinator = groupParticipantFromRegistry(registry, owner, details.invitation.sender());
293
+
294
+ // Check if we're posting to storage
295
+ const isPosting = options.storageSelection !== undefined;
296
+
297
+ // Build the response body
298
+ let responseBody: Envelope;
299
+ let contributions: ContributionPaths | undefined;
300
+
301
+ if (options.rejectReason === undefined && isPosting) {
302
+ // Actually posting - generate and persist round1 state
303
+ const [round1Secret, round1Package] = dkgPart1(identifier, total, minSigners, createRng());
304
+
305
+ contributions = persistRound1State(
306
+ registryPath,
307
+ details.invitation.groupId(),
308
+ round1Secret,
309
+ round1Package,
310
+ );
311
+
312
+ responseBody = buildResponseBody(
313
+ details.invitation.groupId(),
314
+ owner.xid(),
315
+ nextResponseArid,
316
+ round1Package,
317
+ );
318
+
319
+ // Create and save group record
320
+ const groupRecord = new GroupRecord(
321
+ details.invitation.charter(),
322
+ details.invitation.minSigners(),
323
+ coordinator,
324
+ groupParticipants,
325
+ );
326
+ groupRecord.setContributions(contributions);
327
+ groupRecord.setListeningAtArid(nextResponseArid);
328
+
329
+ registry.recordGroup(details.invitation.groupId(), groupRecord);
330
+ registry.save(registryPath);
331
+ } else if (options.rejectReason === undefined) {
332
+ // Preview mode - generate dummy round1 for envelope structure only
333
+ const [, round1Package] = dkgPart1(identifier, total, minSigners, createRng());
334
+
335
+ responseBody = buildResponseBody(
336
+ details.invitation.groupId(),
337
+ owner.xid(),
338
+ nextResponseArid,
339
+ round1Package,
340
+ );
341
+ } else {
342
+ // Rejecting - no round1 needed
343
+ responseBody = buildResponseBody(
344
+ details.invitation.groupId(),
345
+ owner.xid(),
346
+ nextResponseArid,
347
+ undefined,
348
+ );
349
+ }
350
+
351
+ // Build the sealed response
352
+ const signerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
353
+ if (signerPrivateKeys === undefined) {
354
+ throw new Error("Owner XID document has no signing keys");
355
+ }
356
+
357
+ let sealed: SealedResponse;
358
+ if (options.rejectReason !== undefined) {
359
+ // Build rejection error body
360
+ const errorBody = Envelope.new("dkgInviteReject")
361
+ .addAssertion("group", details.invitation.groupId())
362
+ .addAssertion("response_arid", nextResponseArid)
363
+ .addAssertion("reason", options.rejectReason);
364
+
365
+ sealed = SealedResponse.newFailure(details.invitation.requestId(), owner.xidDocument())
366
+ .withError(errorBody)
367
+ .withState(nextResponseArid);
368
+ } else {
369
+ sealed = SealedResponse.newSuccess(details.invitation.requestId(), owner.xidDocument())
370
+ .withResult(responseBody)
371
+ .withState(nextResponseArid);
372
+ }
373
+
374
+ // Add peer continuation if present
375
+ const peerContinuation = details.invitation.peerContinuation();
376
+ if (peerContinuation !== undefined) {
377
+ sealed = sealed.withPeerContinuation(peerContinuation);
378
+ }
379
+
380
+ // Handle output based on storage selection
381
+ if (options.storageSelection !== undefined) {
382
+ const responseEnvelope = sealed.toEnvelope(
383
+ details.invitation.validUntil(),
384
+ signerPrivateKeys,
385
+ details.invitation.sender(),
386
+ );
387
+
388
+ const responseTarget = details.invitation.responseArid();
389
+ const client = await createStorageClient(options.storageSelection);
390
+
391
+ await putWithIndicator(
392
+ client,
393
+ responseTarget,
394
+ responseEnvelope,
395
+ "Round 1 Response",
396
+ options.verbose ?? false,
397
+ );
398
+
399
+ if (options.verbose === true) {
400
+ console.log(`Sent round 1 response`);
401
+ console.log(`Listening at: ${nextResponseArid.urString()}`);
402
+ }
403
+
404
+ return {
405
+ accepted: options.rejectReason === undefined,
406
+ listeningArid: nextResponseArid.urString(),
407
+ };
408
+ } else if (options.preview === true) {
409
+ // Show the GSTP response structure without encryption
410
+ const unsealedEnvelope = sealed.toEnvelope(undefined, signerPrivateKeys, undefined);
411
+ const envelopeUr = unsealedEnvelope.urString();
412
+ console.log(envelopeUr);
413
+
414
+ return {
415
+ accepted: options.rejectReason === undefined,
416
+ envelopeUr,
417
+ };
418
+ } else {
419
+ // Print the sealed envelope
420
+ const responseEnvelope = sealed.toEnvelope(
421
+ details.invitation.validUntil(),
422
+ signerPrivateKeys,
423
+ details.invitation.sender(),
424
+ );
425
+ const envelopeUr = responseEnvelope.urString();
426
+ console.log(envelopeUr);
427
+
428
+ return {
429
+ accepted: options.rejectReason === undefined,
430
+ envelopeUr,
431
+ };
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Resolve a sender XID document from the registry by UR or pet name.
437
+ */
438
+ function resolveSenderXidDocument(registry: Registry, raw: string): XIDDocument {
439
+ // Try parsing as XID UR first
440
+ try {
441
+ const xid = XID.fromURString(raw.trim());
442
+ const record = registry.participant(xid);
443
+ if (record) {
444
+ return record.xidDocument();
445
+ }
446
+ const owner = registry.owner();
447
+ if (owner?.xid().urString() === xid.urString()) {
448
+ return owner.xidDocument();
449
+ }
450
+ throw new Error(`Sender with XID ${xid.urString()} not found in registry`);
451
+ } catch {
452
+ // Try looking up by pet name
453
+ const result = registry.participantByPetName(raw.trim());
454
+ if (result) {
455
+ const [, record] = result;
456
+ return record.xidDocument();
457
+ }
458
+ const owner = registry.owner();
459
+ if (owner?.petName() === raw.trim()) {
460
+ return owner.xidDocument();
461
+ }
462
+ throw new Error(`Sender '${raw}' not found in registry`);
463
+ }
464
+ }