@svsprotocol/solana 0.1.0

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 (38) hide show
  1. package/LICENSE +158 -0
  2. package/README.md +365 -0
  3. package/dist/action-production-proof-evidence.js +553 -0
  4. package/dist/adapter-catalog.d.ts +29 -0
  5. package/dist/adapter-catalog.js +146 -0
  6. package/dist/adapter-core.d.ts +48 -0
  7. package/dist/adapter-core.js +249 -0
  8. package/dist/approval-signature.js +197 -0
  9. package/dist/base58.js +69 -0
  10. package/dist/bot-auth.js +50 -0
  11. package/dist/bot-certification-evidence.js +342 -0
  12. package/dist/bot-first-action-runbook.js +299 -0
  13. package/dist/bot-integration-contract.js +41 -0
  14. package/dist/certified-submit-status.js +176 -0
  15. package/dist/common.d.ts +1135 -0
  16. package/dist/elizaos.d.ts +43 -0
  17. package/dist/elizaos.js +227 -0
  18. package/dist/goat.d.ts +47 -0
  19. package/dist/goat.js +261 -0
  20. package/dist/index.d.ts +330 -0
  21. package/dist/index.js +128 -0
  22. package/dist/protocol.d.ts +205 -0
  23. package/dist/protocol.js +900 -0
  24. package/dist/receipt.js +51 -0
  25. package/dist/signed-proof-read-protection.js +495 -0
  26. package/dist/solana-agent-kit.d.ts +35 -0
  27. package/dist/solana-agent-kit.js +151 -0
  28. package/dist/svs-client.js +1232 -0
  29. package/dist/vercel-ai.d.ts +47 -0
  30. package/dist/vercel-ai.js +266 -0
  31. package/dist/verified-agent-adoption-kit.js +471 -0
  32. package/dist/verified-agent-profile.js +329 -0
  33. package/dist/verified-agent-registry-consumer.js +421 -0
  34. package/dist/verified-agent-registry.d.ts +36 -0
  35. package/dist/verified-agent-registry.js +826 -0
  36. package/dist/verified-agent-trust-score.js +335 -0
  37. package/dist/webhooks.js +834 -0
  38. package/package.json +72 -0
@@ -0,0 +1,834 @@
1
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
4
+ import { hashObject } from "./receipt.js";
5
+
6
+ export const WEBHOOK_EVENT_VERSION = "svs.webhook-event.v1";
7
+ export const WEBHOOK_SIGNATURE_VERSION = "v1";
8
+ export const WEBHOOK_REPLAY_STORE_VERSION = "svs.webhook-replay-store.v1";
9
+ export const DEFAULT_WEBHOOK_REPLAY_TTL_MS = 7 * 24 * 60 * 60 * 1000;
10
+ export const DEFAULT_WEBHOOK_RETRY_DELAYS_MS = [
11
+ 60 * 1000,
12
+ 5 * 60 * 1000,
13
+ 15 * 60 * 1000,
14
+ 60 * 60 * 1000,
15
+ 6 * 60 * 60 * 1000
16
+ ];
17
+
18
+ export async function emitWebhookEvent({
19
+ outboxDir = "./data/webhook-outbox",
20
+ botRegistryPath = "./.devnet/bots.json",
21
+ eventType,
22
+ record,
23
+ previousStatus = null,
24
+ eventData = null,
25
+ fetchImpl = globalThis.fetch,
26
+ deliver = true,
27
+ createdAt = new Date().toISOString()
28
+ } = {}) {
29
+ if (!record) {
30
+ return null;
31
+ }
32
+
33
+ const bot = await findWebhookBot({
34
+ botRegistryPath,
35
+ botId: record.source?.authenticatedBotId ?? record.intent?.botId
36
+ });
37
+
38
+ if (!bot?.webhookUrl) {
39
+ return null;
40
+ }
41
+
42
+ const event = createWebhookEvent({
43
+ eventType,
44
+ record,
45
+ previousStatus,
46
+ bot,
47
+ eventData,
48
+ createdAt
49
+ });
50
+ const outboxEntry = {
51
+ ...event,
52
+ delivery: {
53
+ mode: "outbox",
54
+ url: bot.webhookUrl,
55
+ attemptedAt: null,
56
+ deliveredAt: null,
57
+ statusCode: null,
58
+ ok: false,
59
+ error: null
60
+ }
61
+ };
62
+ const path = join(outboxDir, `${event.createdAt.replaceAll(":", "-")}-${event.eventId}.json`);
63
+
64
+ await writeWebhookEvent(path, outboxEntry);
65
+
66
+ if (!deliver) {
67
+ return {
68
+ path,
69
+ event: outboxEntry
70
+ };
71
+ }
72
+
73
+ const delivered = await deliverWebhookEvent({
74
+ path,
75
+ event: outboxEntry,
76
+ webhookSecret: bot.webhookSecret,
77
+ fetchImpl
78
+ });
79
+
80
+ return {
81
+ path,
82
+ event: delivered
83
+ };
84
+ }
85
+
86
+ export async function emitBotWebhookTest({
87
+ outboxDir = "./data/webhook-outbox",
88
+ botRegistryPath = "./.devnet/bots.json",
89
+ botId,
90
+ fetchImpl = globalThis.fetch,
91
+ deliver = true,
92
+ createdAt = new Date().toISOString()
93
+ } = {}) {
94
+ if (!botId) {
95
+ throw new Error("botId is required.");
96
+ }
97
+
98
+ const bot = await findWebhookBot({ botRegistryPath, botId });
99
+
100
+ if (!bot?.webhookUrl) {
101
+ return null;
102
+ }
103
+
104
+ const record = {
105
+ id: `webhook-test-${randomUUID()}`,
106
+ status: "webhook_test",
107
+ authorizationType: "webhook_test",
108
+ intent: {
109
+ botId,
110
+ controllerWallet: null
111
+ },
112
+ source: {
113
+ authenticatedBotId: botId,
114
+ actionRequestHash: null,
115
+ idempotencyKey: null
116
+ },
117
+ policySummary: {
118
+ id: null
119
+ },
120
+ receipt: {
121
+ receiptId: null,
122
+ receiptHash: null
123
+ }
124
+ };
125
+ const event = createWebhookEvent({
126
+ eventType: "bot.webhook_test",
127
+ record,
128
+ previousStatus: null,
129
+ bot,
130
+ createdAt
131
+ });
132
+ const outboxEntry = {
133
+ ...event,
134
+ delivery: {
135
+ mode: "outbox",
136
+ url: bot.webhookUrl,
137
+ attemptedAt: null,
138
+ deliveredAt: null,
139
+ statusCode: null,
140
+ ok: false,
141
+ error: null
142
+ }
143
+ };
144
+ const path = join(outboxDir, `${event.createdAt.replaceAll(":", "-")}-${event.eventId}.json`);
145
+
146
+ await writeWebhookEvent(path, outboxEntry);
147
+
148
+ if (!deliver) {
149
+ return {
150
+ path,
151
+ event: outboxEntry
152
+ };
153
+ }
154
+
155
+ const delivered = await deliverWebhookEvent({
156
+ path,
157
+ event: outboxEntry,
158
+ webhookSecret: bot.webhookSecret,
159
+ fetchImpl
160
+ });
161
+
162
+ return {
163
+ path,
164
+ event: delivered
165
+ };
166
+ }
167
+
168
+ export async function listWebhookEvents({
169
+ outboxDir = "./data/webhook-outbox"
170
+ } = {}) {
171
+ let files;
172
+
173
+ try {
174
+ files = await readdir(outboxDir);
175
+ } catch (error) {
176
+ if (error.code === "ENOENT") {
177
+ return [];
178
+ }
179
+
180
+ throw error;
181
+ }
182
+
183
+ return Promise.all(
184
+ files
185
+ .filter((file) => file.endsWith(".json"))
186
+ .sort()
187
+ .map(async (file) => {
188
+ const path = join(outboxDir, file);
189
+
190
+ return {
191
+ path,
192
+ event: JSON.parse(await readFile(path, "utf8"))
193
+ };
194
+ })
195
+ );
196
+ }
197
+
198
+ export async function retryWebhookEvents({
199
+ outboxDir = "./data/webhook-outbox",
200
+ botRegistryPath = "./.devnet/bots.json",
201
+ eventId = null,
202
+ onlyFailed = true,
203
+ force = false,
204
+ limit = 25,
205
+ fetchImpl = globalThis.fetch,
206
+ now = new Date()
207
+ } = {}) {
208
+ const entries = await listWebhookEvents({ outboxDir });
209
+ const hasMatchingEvent = !eventId || entries.some(({ event }) => event.eventId === eventId);
210
+ const retryable = entries
211
+ .filter(({ event }) => !eventId || event.eventId === eventId)
212
+ .filter(({ event }) => !onlyFailed || !event.delivery?.ok)
213
+ .filter(({ event }) => force || isWebhookRetryDue(event, now))
214
+ .slice(0, limit);
215
+ const results = [];
216
+
217
+ for (const entry of retryable) {
218
+ const bot = await findWebhookBot({
219
+ botRegistryPath,
220
+ botId: entry.event.authenticatedBotId ?? entry.event.botId
221
+ });
222
+
223
+ if (!bot?.webhookUrl) {
224
+ results.push({
225
+ path: entry.path,
226
+ event: entry.event,
227
+ retried: false,
228
+ error: "No active bot webhook URL is configured."
229
+ });
230
+ continue;
231
+ }
232
+
233
+ const event = {
234
+ ...entry.event,
235
+ webhookUrl: bot.webhookUrl,
236
+ delivery: {
237
+ ...(entry.event.delivery ?? {}),
238
+ url: bot.webhookUrl
239
+ }
240
+ };
241
+ const delivered = await deliverWebhookEvent({
242
+ path: entry.path,
243
+ event,
244
+ webhookSecret: bot.webhookSecret,
245
+ fetchImpl,
246
+ now
247
+ });
248
+
249
+ results.push({
250
+ path: entry.path,
251
+ event: delivered,
252
+ retried: true,
253
+ error: null
254
+ });
255
+ }
256
+
257
+ if (eventId && !hasMatchingEvent) {
258
+ throw new Error(`Webhook event ${eventId} was not found or is not retryable.`);
259
+ }
260
+
261
+ return {
262
+ version: "svs.webhook-retry-result.v1",
263
+ retriedAt: now.toISOString(),
264
+ eventId,
265
+ onlyFailed,
266
+ force,
267
+ count: results.length,
268
+ delivered: results.filter((result) => result.event.delivery?.ok).length,
269
+ failed: results.filter((result) => result.retried && !result.event.delivery?.ok).length,
270
+ skipped: results.filter((result) => !result.retried).length,
271
+ results
272
+ };
273
+ }
274
+
275
+ function createWebhookEvent({
276
+ eventType,
277
+ record,
278
+ previousStatus,
279
+ bot,
280
+ eventData,
281
+ createdAt
282
+ }) {
283
+ const payload = {
284
+ version: WEBHOOK_EVENT_VERSION,
285
+ eventId: randomUUID(),
286
+ eventType,
287
+ createdAt,
288
+ botId: record.intent?.botId ?? null,
289
+ authenticatedBotId: record.source?.authenticatedBotId ?? null,
290
+ recordId: record.id,
291
+ previousStatus,
292
+ status: record.status,
293
+ authorizationType: record.authorizationType,
294
+ controllerWallet: record.intent?.controllerWallet ?? null,
295
+ policyId: record.policySummary?.id ?? record.receipt?.policyId ?? null,
296
+ receiptHash: record.receipt?.receiptHash ?? null,
297
+ receiptId: record.receipt?.receiptId ?? record.id,
298
+ broadcastSignature: record.broadcast?.signature ?? null,
299
+ anchorSignature: record.anchor?.signature ?? null,
300
+ feePaymentSignature: record.feePayment?.signature ?? null,
301
+ idempotencyKey: record.source?.idempotencyKey ?? null,
302
+ actionRequestHash: record.source?.actionRequestHash ?? null,
303
+ links: {
304
+ action: `/api/actions/${encodeURIComponent(record.id)}`,
305
+ receipt: `/api/actions/${encodeURIComponent(record.id)}/receipt`,
306
+ report: `/api/actions/${encodeURIComponent(record.id)}/report`
307
+ }
308
+ };
309
+
310
+ if (eventData !== null && eventData !== undefined) {
311
+ payload.data = eventData;
312
+ }
313
+
314
+ return {
315
+ ...payload,
316
+ webhookUrl: bot.webhookUrl,
317
+ payloadHashAlgorithm: "sha256",
318
+ payloadHash: hashObject(payload)
319
+ };
320
+ }
321
+
322
+ async function deliverWebhookEvent({
323
+ path,
324
+ event,
325
+ webhookSecret,
326
+ fetchImpl,
327
+ now = new Date()
328
+ }) {
329
+ const attemptedAt = now.toISOString();
330
+
331
+ if (!fetchImpl) {
332
+ const failed = withDeliveryAttempt(event, {
333
+ attemptedAt,
334
+ error: "fetch is not available."
335
+ });
336
+
337
+ await writeWebhookEvent(path, failed);
338
+ return failed;
339
+ }
340
+
341
+ try {
342
+ const body = JSON.stringify(event);
343
+ const signatureHeaders = webhookSecret
344
+ ? createWebhookSignatureHeaders({ body, webhookSecret, timestamp: attemptedAt })
345
+ : {};
346
+ const response = await fetchImpl(event.webhookUrl, {
347
+ method: "POST",
348
+ headers: {
349
+ "content-type": "application/json",
350
+ "svs-event-type": event.eventType,
351
+ "svs-event-id": event.eventId,
352
+ "svs-payload-hash": event.payloadHash,
353
+ ...signatureHeaders
354
+ },
355
+ body
356
+ });
357
+ const delivered = withDeliveryAttempt(event, {
358
+ attemptedAt,
359
+ deliveredAt: response.ok ? now.toISOString() : null,
360
+ statusCode: response.status,
361
+ ok: Boolean(response.ok),
362
+ error: response.ok ? null : `HTTP ${response.status}`,
363
+ signature: signatureHeaders["svs-webhook-signature"] ?? null,
364
+ signatureTimestamp: signatureHeaders["svs-webhook-timestamp"] ?? null
365
+ });
366
+
367
+ await writeWebhookEvent(path, delivered);
368
+ return delivered;
369
+ } catch (error) {
370
+ const failed = withDeliveryAttempt(event, {
371
+ attemptedAt,
372
+ error: error.message
373
+ });
374
+
375
+ await writeWebhookEvent(path, failed);
376
+ return failed;
377
+ }
378
+ }
379
+
380
+ export function createWebhookSignatureHeaders({
381
+ body,
382
+ webhookSecret,
383
+ timestamp = new Date().toISOString()
384
+ }) {
385
+ if (!webhookSecret) {
386
+ return {};
387
+ }
388
+
389
+ return {
390
+ "svs-webhook-timestamp": timestamp,
391
+ "svs-webhook-signature": createWebhookSignature({ body, webhookSecret, timestamp })
392
+ };
393
+ }
394
+
395
+ export function createWebhookSignature({ body, webhookSecret, timestamp }) {
396
+ if (!webhookSecret) {
397
+ throw new Error("webhookSecret is required.");
398
+ }
399
+
400
+ if (!timestamp) {
401
+ throw new Error("timestamp is required.");
402
+ }
403
+
404
+ const digest = createHmac("sha256", webhookSecret)
405
+ .update(`${timestamp}.${body}`)
406
+ .digest("hex");
407
+
408
+ return `${WEBHOOK_SIGNATURE_VERSION}=${digest}`;
409
+ }
410
+
411
+ export function verifyWebhookSignature({
412
+ body,
413
+ webhookSecret,
414
+ timestamp,
415
+ signature,
416
+ toleranceMs = 5 * 60 * 1000,
417
+ now = new Date()
418
+ }) {
419
+ if (!webhookSecret || !timestamp || !signature) {
420
+ return {
421
+ ok: false,
422
+ reason: "missing_signature_fields"
423
+ };
424
+ }
425
+
426
+ const signedAt = new Date(timestamp);
427
+
428
+ if (Number.isNaN(signedAt.getTime())) {
429
+ return {
430
+ ok: false,
431
+ reason: "invalid_timestamp"
432
+ };
433
+ }
434
+
435
+ if (toleranceMs !== null && Math.abs(now.getTime() - signedAt.getTime()) > toleranceMs) {
436
+ return {
437
+ ok: false,
438
+ reason: "timestamp_outside_tolerance"
439
+ };
440
+ }
441
+
442
+ const expected = createWebhookSignature({ body, webhookSecret, timestamp });
443
+
444
+ if (!constantTimeEqual(signature, expected)) {
445
+ return {
446
+ ok: false,
447
+ reason: "signature_mismatch"
448
+ };
449
+ }
450
+
451
+ return {
452
+ ok: true,
453
+ reason: "valid",
454
+ scheme: "hmac-sha256",
455
+ signatureVersion: WEBHOOK_SIGNATURE_VERSION
456
+ };
457
+ }
458
+
459
+ export function verifyWebhookRequest({
460
+ body,
461
+ headers = {},
462
+ webhookSecret,
463
+ seenEventIds = null,
464
+ toleranceMs = 5 * 60 * 1000,
465
+ now = new Date()
466
+ } = {}) {
467
+ const rawBody = normalizeWebhookBody(body);
468
+ const eventType = getHeader(headers, "svs-event-type");
469
+ const eventId = getHeader(headers, "svs-event-id");
470
+ const payloadHash = getHeader(headers, "svs-payload-hash");
471
+ const timestamp = getHeader(headers, "svs-webhook-timestamp");
472
+ const signature = getHeader(headers, "svs-webhook-signature");
473
+
474
+ let event;
475
+
476
+ try {
477
+ event = JSON.parse(rawBody);
478
+ } catch (_error) {
479
+ return webhookRequestResult(false, "invalid_json", null);
480
+ }
481
+
482
+ if (event.version !== WEBHOOK_EVENT_VERSION) {
483
+ return webhookRequestResult(false, "unsupported_event_version", event);
484
+ }
485
+
486
+ if (eventType && eventType !== event.eventType) {
487
+ return webhookRequestResult(false, "event_type_header_mismatch", event);
488
+ }
489
+
490
+ if (eventId && eventId !== event.eventId) {
491
+ return webhookRequestResult(false, "event_id_header_mismatch", event);
492
+ }
493
+
494
+ if (payloadHash && payloadHash !== event.payloadHash) {
495
+ return webhookRequestResult(false, "payload_hash_header_mismatch", event);
496
+ }
497
+
498
+ const expectedPayloadHash = hashObject(createWebhookPayloadFromEvent(event));
499
+
500
+ if (event.payloadHash !== expectedPayloadHash) {
501
+ return webhookRequestResult(false, "payload_hash_mismatch", event, { expectedPayloadHash });
502
+ }
503
+
504
+ const signatureResult = verifyWebhookSignature({
505
+ body: rawBody,
506
+ webhookSecret,
507
+ timestamp,
508
+ signature,
509
+ toleranceMs,
510
+ now
511
+ });
512
+
513
+ if (!signatureResult.ok) {
514
+ return webhookRequestResult(false, signatureResult.reason, event, { signature: signatureResult });
515
+ }
516
+
517
+ if (seenEventIds?.has?.(event.eventId)) {
518
+ return webhookRequestResult(false, "replayed_event", event, { signature: signatureResult });
519
+ }
520
+
521
+ seenEventIds?.add?.(event.eventId);
522
+
523
+ return {
524
+ ok: true,
525
+ reason: "valid",
526
+ event,
527
+ checks: {
528
+ eventVersion: WEBHOOK_EVENT_VERSION,
529
+ payloadHash: event.payloadHash,
530
+ signature: signatureResult
531
+ }
532
+ };
533
+ }
534
+
535
+ export async function verifyWebhookRequestWithReplayStore({
536
+ body,
537
+ headers = {},
538
+ webhookSecret,
539
+ replayStorePath = "./data/webhook-receiver-replays.json",
540
+ replayTtlMs = DEFAULT_WEBHOOK_REPLAY_TTL_MS,
541
+ maxReplayEvents = 10000,
542
+ toleranceMs = 5 * 60 * 1000,
543
+ now = new Date()
544
+ } = {}) {
545
+ const store = await loadWebhookReplayStore({
546
+ path: replayStorePath,
547
+ maxAgeMs: replayTtlMs,
548
+ maxEntries: maxReplayEvents,
549
+ now
550
+ });
551
+ const replayGuard = createWebhookReplayGuard(store, now);
552
+ const result = verifyWebhookRequest({
553
+ body,
554
+ headers,
555
+ webhookSecret,
556
+ seenEventIds: replayGuard,
557
+ toleranceMs,
558
+ now
559
+ });
560
+
561
+ if (result.ok || store.pruned) {
562
+ await writeWebhookReplayStore(replayStorePath, store);
563
+ }
564
+
565
+ return {
566
+ ...result,
567
+ replayStore: result.ok
568
+ ? {
569
+ path: replayStorePath,
570
+ eventCount: store.acceptedEvents.length
571
+ }
572
+ : undefined
573
+ };
574
+ }
575
+
576
+ function withDeliveryAttempt(event, delivery) {
577
+ const priorAttempts = event.deliveryAttempts ?? [];
578
+ const attemptNumber = priorAttempts.length + 1;
579
+ const ok = Boolean(delivery.ok);
580
+ const nextAttemptAt = ok
581
+ ? null
582
+ : calculateNextWebhookAttemptAt({
583
+ attemptedAt: delivery.attemptedAt,
584
+ failedAttemptCount: priorAttempts.filter((attempt) => !attempt.ok).length + 1
585
+ });
586
+ const attempt = {
587
+ attempt: attemptNumber,
588
+ mode: "http",
589
+ url: event.webhookUrl ?? event.delivery?.url ?? null,
590
+ attemptedAt: delivery.attemptedAt,
591
+ deliveredAt: delivery.deliveredAt ?? null,
592
+ statusCode: delivery.statusCode ?? null,
593
+ ok,
594
+ error: delivery.error ?? null,
595
+ signature: delivery.signature ?? null,
596
+ signatureTimestamp: delivery.signatureTimestamp ?? null,
597
+ nextAttemptAt
598
+ };
599
+
600
+ return {
601
+ ...event,
602
+ deliveryAttempts: [
603
+ ...priorAttempts,
604
+ attempt
605
+ ],
606
+ delivery: {
607
+ ...event.delivery,
608
+ ...attempt
609
+ }
610
+ };
611
+ }
612
+
613
+ function isWebhookRetryDue(event, now) {
614
+ if (event.delivery?.ok) return false;
615
+ if (!event.delivery?.nextAttemptAt) return true;
616
+
617
+ return new Date(event.delivery.nextAttemptAt).getTime() <= now.getTime();
618
+ }
619
+
620
+ function calculateNextWebhookAttemptAt({
621
+ attemptedAt,
622
+ failedAttemptCount
623
+ }) {
624
+ const delay = DEFAULT_WEBHOOK_RETRY_DELAYS_MS[
625
+ Math.min(failedAttemptCount - 1, DEFAULT_WEBHOOK_RETRY_DELAYS_MS.length - 1)
626
+ ];
627
+
628
+ return new Date(new Date(attemptedAt).getTime() + delay).toISOString();
629
+ }
630
+
631
+ async function loadWebhookReplayStore({
632
+ path,
633
+ maxAgeMs,
634
+ maxEntries,
635
+ now
636
+ }) {
637
+ let store;
638
+
639
+ try {
640
+ store = normalizeWebhookReplayStore(JSON.parse(await readFile(path, "utf8")));
641
+ } catch (error) {
642
+ if (error.code !== "ENOENT") {
643
+ throw error;
644
+ }
645
+
646
+ store = createEmptyWebhookReplayStore();
647
+ }
648
+
649
+ return pruneWebhookReplayStore(store, { maxAgeMs, maxEntries, now });
650
+ }
651
+
652
+ function createEmptyWebhookReplayStore() {
653
+ return {
654
+ version: WEBHOOK_REPLAY_STORE_VERSION,
655
+ updatedAt: null,
656
+ acceptedEvents: []
657
+ };
658
+ }
659
+
660
+ function normalizeWebhookReplayStore(store) {
661
+ if (store?.version !== WEBHOOK_REPLAY_STORE_VERSION) {
662
+ return createEmptyWebhookReplayStore();
663
+ }
664
+
665
+ return {
666
+ version: WEBHOOK_REPLAY_STORE_VERSION,
667
+ updatedAt: store.updatedAt ?? null,
668
+ acceptedEvents: Array.isArray(store.acceptedEvents)
669
+ ? store.acceptedEvents
670
+ .filter((event) => event?.eventId)
671
+ .map((event) => ({
672
+ eventId: String(event.eventId),
673
+ acceptedAt: event.acceptedAt ?? null
674
+ }))
675
+ : []
676
+ };
677
+ }
678
+
679
+ function pruneWebhookReplayStore(store, { maxAgeMs, maxEntries, now }) {
680
+ const cutoff = maxAgeMs === null ? null : now.getTime() - maxAgeMs;
681
+ const originalCount = store.acceptedEvents.length;
682
+ const acceptedEvents = store.acceptedEvents
683
+ .filter((event) => {
684
+ if (cutoff === null) return true;
685
+ const acceptedAt = new Date(event.acceptedAt);
686
+
687
+ return !Number.isNaN(acceptedAt.getTime()) && acceptedAt.getTime() >= cutoff;
688
+ })
689
+ .slice(-maxEntries);
690
+
691
+ return {
692
+ ...store,
693
+ acceptedEvents,
694
+ pruned: acceptedEvents.length !== originalCount
695
+ };
696
+ }
697
+
698
+ function createWebhookReplayGuard(store, now) {
699
+ const seen = new Set(store.acceptedEvents.map((event) => event.eventId));
700
+
701
+ return {
702
+ has(eventId) {
703
+ return seen.has(eventId);
704
+ },
705
+ add(eventId) {
706
+ if (seen.has(eventId)) return;
707
+
708
+ seen.add(eventId);
709
+ store.acceptedEvents.push({
710
+ eventId,
711
+ acceptedAt: now.toISOString()
712
+ });
713
+ store.updatedAt = now.toISOString();
714
+ }
715
+ };
716
+ }
717
+
718
+ async function writeWebhookReplayStore(path, store) {
719
+ const persisted = {
720
+ version: WEBHOOK_REPLAY_STORE_VERSION,
721
+ updatedAt: store.updatedAt ?? null,
722
+ acceptedEvents: store.acceptedEvents
723
+ };
724
+
725
+ await mkdir(dirname(path), { recursive: true });
726
+ await writeFile(path, `${JSON.stringify(persisted, null, 2)}\n`, { mode: 0o600 });
727
+ }
728
+
729
+ function createWebhookPayloadFromEvent(event) {
730
+ const payload = {
731
+ version: event.version,
732
+ eventId: event.eventId,
733
+ eventType: event.eventType,
734
+ createdAt: event.createdAt,
735
+ botId: event.botId,
736
+ authenticatedBotId: event.authenticatedBotId,
737
+ recordId: event.recordId,
738
+ previousStatus: event.previousStatus,
739
+ status: event.status,
740
+ authorizationType: event.authorizationType,
741
+ controllerWallet: event.controllerWallet,
742
+ policyId: event.policyId,
743
+ receiptHash: event.receiptHash,
744
+ receiptId: event.receiptId,
745
+ broadcastSignature: event.broadcastSignature,
746
+ anchorSignature: event.anchorSignature,
747
+ feePaymentSignature: event.feePaymentSignature,
748
+ idempotencyKey: event.idempotencyKey,
749
+ actionRequestHash: event.actionRequestHash,
750
+ links: event.links
751
+ };
752
+
753
+ if (event.data !== undefined) {
754
+ payload.data = event.data;
755
+ }
756
+
757
+ return payload;
758
+ }
759
+
760
+ function getHeader(headers, name) {
761
+ if (headers?.get) {
762
+ return headers.get(name);
763
+ }
764
+
765
+ const lowerName = name.toLowerCase();
766
+
767
+ for (const [key, value] of Object.entries(headers ?? {})) {
768
+ if (key.toLowerCase() === lowerName) {
769
+ return Array.isArray(value) ? value[0] : value;
770
+ }
771
+ }
772
+
773
+ return null;
774
+ }
775
+
776
+ function normalizeWebhookBody(body) {
777
+ if (Buffer.isBuffer(body)) {
778
+ return body.toString("utf8");
779
+ }
780
+
781
+ if (body instanceof Uint8Array) {
782
+ return Buffer.from(body).toString("utf8");
783
+ }
784
+
785
+ if (typeof body === "string") {
786
+ return body;
787
+ }
788
+
789
+ return JSON.stringify(body ?? {});
790
+ }
791
+
792
+ function webhookRequestResult(ok, reason, event, checks = {}) {
793
+ return {
794
+ ok,
795
+ reason,
796
+ event,
797
+ checks
798
+ };
799
+ }
800
+
801
+ function constantTimeEqual(left, right) {
802
+ const leftBuffer = Buffer.from(String(left));
803
+ const rightBuffer = Buffer.from(String(right));
804
+
805
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
806
+ }
807
+
808
+ async function findWebhookBot({ botRegistryPath, botId }) {
809
+ if (!botId) {
810
+ return null;
811
+ }
812
+
813
+ try {
814
+ const registry = JSON.parse(await readFile(botRegistryPath, "utf8"));
815
+ const bot = (registry.bots ?? []).find((candidate) => candidate.id === botId);
816
+
817
+ if (!bot || bot.status === "disabled") {
818
+ return null;
819
+ }
820
+
821
+ return bot;
822
+ } catch (error) {
823
+ if (error.code === "ENOENT") {
824
+ return null;
825
+ }
826
+
827
+ throw error;
828
+ }
829
+ }
830
+
831
+ async function writeWebhookEvent(path, event) {
832
+ await mkdir(dirname(path), { recursive: true });
833
+ await writeFile(path, `${JSON.stringify(event, null, 2)}\n`);
834
+ }