@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,1232 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import { createBotRequestSignature } from "./bot-auth.js";
5
+ import {
6
+ BOT_INTEGRATION_CONTRACT_VERSION,
7
+ BOT_INTEGRATION_QUICKSTART_VERSION,
8
+ hashBotIntegrationContract
9
+ } from "./bot-integration-contract.js";
10
+
11
+ export const BOT_CLIENT_COMPATIBILITY_VERSION = "svs.bot-client-compatibility.v1";
12
+ export const BOT_CLIENT_RETRY_SAFETY_VERSION = "svs.bot-client-retry-safety.v1";
13
+ export const SOLANA_VERIFICATION_CLIENT_NAME = "SolanaVerificationClient";
14
+ export const SOLANA_VERIFICATION_CLIENT_VERSION = "svs-js-client.v1";
15
+ export const SOLANA_VERIFICATION_CLIENT_CAPABILITIES = Object.freeze([
16
+ "fetchIntegrationContract",
17
+ "createSignedBotRequest",
18
+ "submitSignedAction",
19
+ "autoNonceForSignedAction",
20
+ "retrySafeIdempotentSubmit",
21
+ "signedSelfTest",
22
+ "credentialReadiness",
23
+ "credentialRotationReceipt",
24
+ "credentialRotationReadinessCheck",
25
+ "combinedReadinessCheck",
26
+ "certificationStatus",
27
+ "certificationQualityTarget",
28
+ "productionCertificationGuard",
29
+ "certifiedSubmitStatus",
30
+ "certifiedActionSubmit",
31
+ "certifiedActionSubmitAndWaitForProof",
32
+ "actionProductionProofStatus",
33
+ "actionProductionProofFetch"
34
+ ]);
35
+
36
+ const DEFAULT_RETRYABLE_STATUSES = Object.freeze([408, 429, 500, 502, 503, 504]);
37
+ const DEFAULT_RETRY_OPTIONS = Object.freeze({
38
+ maxRetries: 2,
39
+ baseDelayMs: 100,
40
+ maxDelayMs: 1000,
41
+ retryableStatuses: DEFAULT_RETRYABLE_STATUSES
42
+ });
43
+
44
+ export function createSignedBotRequest({
45
+ body,
46
+ requestSigningSecret,
47
+ timestamp = new Date().toISOString(),
48
+ headers = {}
49
+ } = {}) {
50
+ if (!requestSigningSecret) {
51
+ throw new Error("requestSigningSecret is required.");
52
+ }
53
+
54
+ const rawBody = body === undefined || body === null
55
+ ? ""
56
+ : typeof body === "string"
57
+ ? body
58
+ : JSON.stringify(body);
59
+
60
+ return {
61
+ body: rawBody,
62
+ headers: {
63
+ ...headers,
64
+ "svs-request-timestamp": timestamp,
65
+ "svs-request-signature": createBotRequestSignature({
66
+ body: rawBody,
67
+ timestamp,
68
+ secret: requestSigningSecret
69
+ })
70
+ }
71
+ };
72
+ }
73
+
74
+ export class SolanaVerificationClient {
75
+ constructor({
76
+ baseUrl = "http://127.0.0.1:4173",
77
+ apiKey = null,
78
+ requestSigningSecret = null,
79
+ expectedIntegrationContractHash = null,
80
+ timestampProvider = () => new Date().toISOString(),
81
+ fetchImpl = globalThis.fetch,
82
+ retry = {},
83
+ sleep = defaultSleep
84
+ } = {}) {
85
+ if (!fetchImpl) {
86
+ throw new Error("fetch is required.");
87
+ }
88
+
89
+ this.baseUrl = baseUrl.replace(/\/$/, "");
90
+ this.apiKey = apiKey;
91
+ this.requestSigningSecret = requestSigningSecret;
92
+ this.expectedIntegrationContractHash = expectedIntegrationContractHash;
93
+ this.timestampProvider = timestampProvider;
94
+ this.fetchImpl = fetchImpl;
95
+ this.usesDefaultFetch = fetchImpl === globalThis.fetch;
96
+ this.retry = normalizeRetryOptions(retry);
97
+ this.sleep = sleep;
98
+ }
99
+
100
+ submitAction(payload, {
101
+ idempotencyKey = payload?.idempotencyKey ?? payload?.requestId,
102
+ requestSigningSecret = this.requestSigningSecret,
103
+ requestTimestamp = null,
104
+ requestNonce = null,
105
+ retry = undefined
106
+ } = {}) {
107
+ const shouldSign = Boolean(requestSigningSecret);
108
+ const body = shouldSign && payload && typeof payload === "object" && !payload.requestNonce
109
+ ? { ...payload, requestNonce: requestNonce ?? randomUUID() }
110
+ : payload;
111
+
112
+ return this.request("/api/actions", {
113
+ method: "POST",
114
+ body,
115
+ headers: idempotencyKey
116
+ ? { "idempotency-key": idempotencyKey }
117
+ : undefined,
118
+ requestSigningSecret,
119
+ requestTimestamp,
120
+ signRequest: shouldSign,
121
+ retry,
122
+ retrySafe: Boolean(idempotencyKey)
123
+ });
124
+ }
125
+
126
+ getRetrySafetyProof() {
127
+ return createSolanaVerificationClientRetrySafetyProof({
128
+ retry: this.retry
129
+ });
130
+ }
131
+
132
+ getIntegrationContract() {
133
+ return this.request("/api/bots/integration-contract");
134
+ }
135
+
136
+ async checkClientCompatibility({ contract = null } = {}) {
137
+ const liveContract = contract ?? await this.getIntegrationContract();
138
+
139
+ return createSolanaVerificationClientCompatibilityReport({ contract: liveContract });
140
+ }
141
+
142
+ getCredentialReadiness(options = {}) {
143
+ const params = new URLSearchParams();
144
+
145
+ for (const [key, value] of Object.entries(options)) {
146
+ if (value !== undefined && value !== null) {
147
+ params.set(key, String(value));
148
+ }
149
+ }
150
+
151
+ const query = params.toString();
152
+
153
+ return this.request(`/api/bots/credential-readiness${query ? `?${query}` : ""}`);
154
+ }
155
+
156
+ getCredentialRotationReceipt({
157
+ botId,
158
+ pendingProofMaxAgeMs = undefined
159
+ } = {}) {
160
+ if (!botId) {
161
+ throw new Error("botId is required.");
162
+ }
163
+
164
+ const params = new URLSearchParams();
165
+
166
+ if (pendingProofMaxAgeMs !== undefined && pendingProofMaxAgeMs !== null) {
167
+ params.set("pendingProofMaxAgeMs", String(pendingProofMaxAgeMs));
168
+ }
169
+
170
+ const query = params.toString();
171
+
172
+ return this.request(`/api/bots/${encodeURIComponent(botId)}/request-signing-rotation-receipt${query ? `?${query}` : ""}`);
173
+ }
174
+
175
+ async checkCredentialRotationReadiness({
176
+ botId,
177
+ pendingProofMaxAgeMs = undefined
178
+ } = {}) {
179
+ const checkedAt = this.timestampProvider();
180
+ const receipt = await this.getCredentialRotationReceipt({
181
+ botId,
182
+ pendingProofMaxAgeMs
183
+ });
184
+ const missingProof = Array.isArray(receipt.missingProof) ? receipt.missingProof : [];
185
+ const firstMissing = missingProof[0] ?? null;
186
+ const readyToPromote = receipt.readyToPromote === true;
187
+ const nextAction = receipt.nextAction ?? firstMissing?.nextAction ?? {
188
+ code: readyToPromote ? "operator_promote_pending_secret" : "rotation_not_ready",
189
+ message: readyToPromote
190
+ ? "Send this non-secret rotation receipt to the operator so they can promote the pending signing secret."
191
+ : "Complete the missing credential-rotation proof before promotion."
192
+ };
193
+ const operatorSteps = firstMissing && Array.isArray(firstMissing.operatorSteps)
194
+ ? firstMissing.operatorSteps
195
+ : readyToPromote
196
+ ? [
197
+ "Send this receipt to the operator.",
198
+ "Ask the operator to import the receipt in the dashboard.",
199
+ "Wait for the operator to promote the pending signing secret."
200
+ ]
201
+ : [];
202
+
203
+ return {
204
+ version: "svs.bot-credential-rotation-readiness-check.v1",
205
+ checkedAt,
206
+ ok: readyToPromote,
207
+ status: readyToPromote ? "ready_to_promote" : receipt.status ?? "not_ready",
208
+ botId: receipt.botId ?? botId ?? null,
209
+ readyToPromote,
210
+ receiptHash: receipt.receiptHash ?? null,
211
+ receipt,
212
+ missingProof,
213
+ nextAction,
214
+ operatorSteps
215
+ };
216
+ }
217
+
218
+ getIntegrationCertificationStatus({
219
+ botId,
220
+ staleAfterMs = undefined
221
+ } = {}) {
222
+ if (!botId) {
223
+ throw new Error("botId is required.");
224
+ }
225
+
226
+ const params = new URLSearchParams();
227
+
228
+ if (staleAfterMs !== undefined && staleAfterMs !== null) {
229
+ params.set("staleAfterMs", String(staleAfterMs));
230
+ }
231
+
232
+ const query = params.toString();
233
+
234
+ return this.request(`/api/bots/${encodeURIComponent(botId)}/integration-certification/status${query ? `?${query}` : ""}`);
235
+ }
236
+
237
+ selfTest({
238
+ botId = null,
239
+ requestNonce = null,
240
+ requestSigningSecret = this.requestSigningSecret,
241
+ requestTimestamp = null
242
+ } = {}) {
243
+ return this.request("/api/bots/self-test", {
244
+ method: "POST",
245
+ body: {
246
+ kind: "svs.bot-self-test-request.v1",
247
+ botId,
248
+ requestNonce: requestNonce ?? randomUUID()
249
+ },
250
+ requestSigningSecret,
251
+ requestTimestamp,
252
+ signRequest: Boolean(requestSigningSecret)
253
+ });
254
+ }
255
+
256
+ async checkBotReadiness({
257
+ botId = null,
258
+ runSelfTest = true,
259
+ pendingProofMaxAgeMs = undefined,
260
+ requireNoExpiredPreviousSigningSecrets = true,
261
+ requestSigningSecret = this.requestSigningSecret,
262
+ requestTimestamp = null,
263
+ requestNonce = null
264
+ } = {}) {
265
+ const checkedAt = this.timestampProvider();
266
+ let selfTest = null;
267
+
268
+ if (runSelfTest) {
269
+ try {
270
+ selfTest = await this.selfTest({
271
+ botId,
272
+ requestNonce,
273
+ requestSigningSecret,
274
+ requestTimestamp
275
+ });
276
+ } catch (error) {
277
+ selfTest = {
278
+ ok: false,
279
+ status: "failed",
280
+ error: error.message
281
+ };
282
+ }
283
+ }
284
+
285
+ const readiness = await this.getCredentialReadiness({
286
+ pendingProofMaxAgeMs,
287
+ requireNoExpiredPreviousSigningSecrets
288
+ });
289
+ const bot = selectReadinessBot(readiness, botId);
290
+ const selfTestOk = runSelfTest ? selfTest?.ok === true : true;
291
+ const readinessOk = bot ? bot.productionReady === true : readiness.productionReady === true;
292
+ const ok = selfTestOk && readinessOk;
293
+
294
+ return {
295
+ version: "svs.bot-readiness-check.v1",
296
+ checkedAt,
297
+ ok,
298
+ status: ok ? "ready" : "not_ready",
299
+ botId: bot?.botId ?? botId ?? null,
300
+ selfTest: runSelfTest ? selfTest : null,
301
+ readiness: {
302
+ version: readiness.version ?? null,
303
+ productionReady: readiness.productionReady === true,
304
+ activeBotCount: readiness.summary?.activeBotCount ?? null,
305
+ adoptionAverageScore: readiness.summary?.adoptionAverageScore ?? null,
306
+ adoptionNeedsActionCount: readiness.summary?.adoptionNeedsActionCount ?? null
307
+ },
308
+ bot: bot ? summarizeReadinessBot(bot) : null,
309
+ adoption: bot?.adoption ?? null,
310
+ nextAction: createReadinessNextAction({ bot, readiness, selfTest, runSelfTest })
311
+ };
312
+ }
313
+
314
+ async checkProductionCertification({
315
+ botId,
316
+ staleAfterMs = undefined,
317
+ requireCurrentIntegrationContract = true,
318
+ expectedIntegrationContractHash = this.expectedIntegrationContractHash
319
+ } = {}) {
320
+ const checkedAt = this.timestampProvider();
321
+ const certification = await this.getIntegrationCertificationStatus({
322
+ botId,
323
+ staleAfterMs
324
+ });
325
+ const integrationContract = await this.checkProductionCertificationContractBinding({
326
+ certification,
327
+ required: requireCurrentIntegrationContract,
328
+ expectedIntegrationContractHash
329
+ });
330
+ const ok = certification.ok === true && integrationContract.ok === true;
331
+ const nextAction = certification.nextAction ?? {
332
+ code: certification.ok === true ? "none" : "certification_not_ready",
333
+ message: certification.ok === true
334
+ ? "Bot certification is current."
335
+ : "Ask the operator to persist fresh bot integration certification evidence."
336
+ };
337
+
338
+ return {
339
+ version: "svs.bot-production-certification-check.v1",
340
+ checkedAt,
341
+ ok,
342
+ status: ok ? "ready" : "not_ready",
343
+ botId: certification.botId ?? botId ?? null,
344
+ certification,
345
+ integrationContract,
346
+ quality: certification.quality ?? certification.certification?.quality ?? null,
347
+ qualityTarget: certification.qualityTarget ?? null,
348
+ nextAction: integrationContract.ok === false
349
+ ? {
350
+ code: "integration_contract_mismatch",
351
+ message: integrationContract.nextActionMessage
352
+ }
353
+ : nextAction
354
+ };
355
+ }
356
+
357
+ async checkProductionCertificationContractBinding({
358
+ certification,
359
+ required = true,
360
+ expectedIntegrationContractHash = this.expectedIntegrationContractHash
361
+ } = {}) {
362
+ if (!required) {
363
+ return {
364
+ version: "svs.production-certification-contract-binding.v1",
365
+ required: false,
366
+ ok: true,
367
+ status: "not_required",
368
+ localContractHash: null,
369
+ certifiedContractHash: certification?.proofs?.integrationContractHash ??
370
+ certification?.contract?.certifiedHash ??
371
+ certification?.certification?.evidence?.integrationContractHash ??
372
+ null,
373
+ currentContractHash: certification?.proofs?.currentIntegrationContractHash ??
374
+ certification?.contract?.currentHash ??
375
+ null,
376
+ drifted: certification?.contract?.drifted === true,
377
+ nextActionMessage: "Integration contract binding check was not required."
378
+ };
379
+ }
380
+
381
+ try {
382
+ const contract = await this.getIntegrationContract();
383
+ const dashboardContractHash = hashBotIntegrationContract(contract);
384
+ const localContractHash = expectedIntegrationContractHash ?? dashboardContractHash;
385
+ const certifiedContractHash = certification?.proofs?.integrationContractHash ??
386
+ certification?.contract?.certifiedHash ??
387
+ certification?.certification?.evidence?.integrationContractHash ??
388
+ null;
389
+ const currentContractHash = certification?.proofs?.currentIntegrationContractHash ??
390
+ certification?.contract?.currentHash ??
391
+ null;
392
+ const drifted = certification?.contract?.drifted === true ||
393
+ certification?.proofs?.integrationContractCurrent === false;
394
+ const ok = Boolean(localContractHash) &&
395
+ dashboardContractHash === localContractHash &&
396
+ certifiedContractHash === localContractHash &&
397
+ (!currentContractHash || currentContractHash === localContractHash) &&
398
+ !drifted;
399
+ const status = ok
400
+ ? "current"
401
+ : dashboardContractHash !== localContractHash
402
+ ? "client_contract_mismatch"
403
+ : drifted
404
+ ? "drifted"
405
+ : "mismatch";
406
+
407
+ return {
408
+ version: "svs.production-certification-contract-binding.v1",
409
+ required: true,
410
+ ok,
411
+ status,
412
+ localContractHash,
413
+ dashboardContractHash,
414
+ certifiedContractHash,
415
+ currentContractHash,
416
+ drifted,
417
+ contractVersion: contract?.version ?? null,
418
+ nextActionMessage: ok
419
+ ? "Bot integration certification matches the current integration contract."
420
+ : status === "client_contract_mismatch"
421
+ ? `Update the bot client integration contract before production submit. local=${shortHash(localContractHash)} dashboard=${shortHash(dashboardContractHash)} certified=${shortHash(certifiedContractHash)}.`
422
+ : `Refresh bot integration certification before production submit. local=${shortHash(localContractHash)} certified=${shortHash(certifiedContractHash)} current=${shortHash(currentContractHash)}.`
423
+ };
424
+ } catch (error) {
425
+ return {
426
+ version: "svs.production-certification-contract-binding.v1",
427
+ required: true,
428
+ ok: false,
429
+ status: "unavailable",
430
+ localContractHash: null,
431
+ certifiedContractHash: certification?.proofs?.integrationContractHash ??
432
+ certification?.contract?.certifiedHash ??
433
+ certification?.certification?.evidence?.integrationContractHash ??
434
+ null,
435
+ currentContractHash: certification?.proofs?.currentIntegrationContractHash ??
436
+ certification?.contract?.currentHash ??
437
+ null,
438
+ drifted: certification?.contract?.drifted === true,
439
+ error: error.message,
440
+ nextActionMessage: `Fetch the live bot integration contract before production submit: ${error.message}`
441
+ };
442
+ }
443
+ }
444
+
445
+ async requireProductionCertification({
446
+ botId,
447
+ staleAfterMs = undefined,
448
+ requireCurrentIntegrationContract = true
449
+ } = {}) {
450
+ const result = await this.checkProductionCertification({
451
+ botId,
452
+ staleAfterMs,
453
+ requireCurrentIntegrationContract
454
+ });
455
+
456
+ if (!productionCertificationIsReady(result)) {
457
+ throw new SolanaVerificationProductionCertificationError(
458
+ createProductionCertificationFailureMessage(result),
459
+ { certificationCheck: result }
460
+ );
461
+ }
462
+
463
+ return result;
464
+ }
465
+
466
+ async submitCertifiedAction(payload, {
467
+ botId = payload?.source?.submittedBy ?? payload?.intent?.botId ?? null,
468
+ certificationStaleAfterMs = undefined,
469
+ requireCurrentIntegrationContract = true,
470
+ ...submitOptions
471
+ } = {}) {
472
+ const certification = await this.requireProductionCertification({
473
+ botId,
474
+ staleAfterMs: certificationStaleAfterMs,
475
+ requireCurrentIntegrationContract
476
+ });
477
+ const submitted = await this.submitAction({
478
+ ...payload,
479
+ source: {
480
+ ...(payload?.source ?? {}),
481
+ submittedBy: payload?.source?.submittedBy ?? botId ?? payload?.intent?.botId ?? null,
482
+ productionCertification: createProductionCertificationSourceProof({
483
+ certification,
484
+ staleAfterMs: certificationStaleAfterMs,
485
+ integrationContract: certification.integrationContract
486
+ })
487
+ }
488
+ }, submitOptions);
489
+
490
+ return {
491
+ version: "svs.certified-action-submission.v1",
492
+ ok: true,
493
+ status: "submitted",
494
+ botId: certification.botId ?? botId ?? null,
495
+ certification,
496
+ submitted
497
+ };
498
+ }
499
+
500
+ getCertifiedSubmitStatus({
501
+ staleAfterMs = undefined
502
+ } = {}) {
503
+ const params = new URLSearchParams();
504
+
505
+ if (staleAfterMs !== undefined && staleAfterMs !== null) {
506
+ params.set("staleAfterMs", String(staleAfterMs));
507
+ }
508
+
509
+ const query = params.toString();
510
+
511
+ return this.request(`/api/bots/certified-submit-status${query ? `?${query}` : ""}`);
512
+ }
513
+
514
+ async submitCertifiedActionAndWaitForProof(payload, {
515
+ botId = payload?.source?.submittedBy ?? payload?.intent?.botId ?? null,
516
+ certificationStaleAfterMs = undefined,
517
+ requireCurrentIntegrationContract = true,
518
+ waitAttempts = 12,
519
+ waitIntervalMs = 5000,
520
+ fetchProof = true,
521
+ checkReceiptRegistryChain = false,
522
+ ...submitOptions
523
+ } = {}) {
524
+ const submission = await this.submitCertifiedAction(payload, {
525
+ botId,
526
+ certificationStaleAfterMs,
527
+ requireCurrentIntegrationContract,
528
+ ...submitOptions
529
+ });
530
+ const recordId = getCertifiedSubmissionRecordId(submission);
531
+
532
+ if (!recordId) {
533
+ return {
534
+ version: "svs.certified-action-production-proof-wait.v1",
535
+ ok: false,
536
+ status: "submitted_record_missing",
537
+ botId: submission.botId ?? botId ?? null,
538
+ recordId: null,
539
+ submission,
540
+ productionProof: null,
541
+ nextAction: {
542
+ code: "record_id_missing",
543
+ message: "SVS accepted the certified action, but the response did not include a queued record id to poll."
544
+ }
545
+ };
546
+ }
547
+
548
+ const productionProof = await this.waitForActionProductionProof(recordId, {
549
+ attempts: waitAttempts,
550
+ intervalMs: waitIntervalMs,
551
+ fetchProof,
552
+ checkReceiptRegistryChain
553
+ });
554
+ const proofStatus = fetchProof ? productionProof?.status : productionProof;
555
+ const ready = proofStatus?.ready === true;
556
+ const proofFetched = !fetchProof || Boolean(productionProof?.proof);
557
+
558
+ return {
559
+ version: "svs.certified-action-production-proof-wait.v1",
560
+ ok: ready && proofFetched,
561
+ status: ready && proofFetched ? "production_proof_ready" : "production_proof_not_ready",
562
+ botId: submission.botId ?? botId ?? null,
563
+ recordId,
564
+ submission,
565
+ productionProof,
566
+ nextAction: ready && proofFetched
567
+ ? {
568
+ code: "none",
569
+ message: "Production proof is ready."
570
+ }
571
+ : proofStatus?.nextAction ?? {
572
+ code: "wait_for_human_approval",
573
+ message: "Wait for human approval, broadcast, custom registry registration, and production proof readiness."
574
+ }
575
+ };
576
+ }
577
+
578
+ getAction(id) {
579
+ return this.request(`/api/actions/${encodeURIComponent(id)}`);
580
+ }
581
+
582
+ getActionProductionProofStatus(id, {
583
+ signRequest = Boolean(this.requestSigningSecret),
584
+ requestSigningSecret = this.requestSigningSecret,
585
+ requestTimestamp = null
586
+ } = {}) {
587
+ if (!id) {
588
+ throw new Error("id is required.");
589
+ }
590
+
591
+ return this.request(`/api/actions/${encodeURIComponent(id)}/production-proof-status`, {
592
+ signRequest,
593
+ requestSigningSecret,
594
+ requestTimestamp
595
+ });
596
+ }
597
+
598
+ getActionProductionProof(id, {
599
+ checkReceiptRegistryChain = false,
600
+ signRequest = Boolean(this.requestSigningSecret),
601
+ requestSigningSecret = this.requestSigningSecret,
602
+ requestTimestamp = null
603
+ } = {}) {
604
+ if (!id) {
605
+ throw new Error("id is required.");
606
+ }
607
+
608
+ const params = new URLSearchParams();
609
+ params.set("checkReceiptRegistryChain", checkReceiptRegistryChain ? "true" : "false");
610
+
611
+ return this.request(`/api/actions/${encodeURIComponent(id)}/production-proof?${params.toString()}`, {
612
+ signRequest,
613
+ requestSigningSecret,
614
+ requestTimestamp
615
+ });
616
+ }
617
+
618
+ async waitForActionProductionProof(id, {
619
+ attempts = 12,
620
+ intervalMs = 5000,
621
+ fetchProof = false,
622
+ checkReceiptRegistryChain = false
623
+ } = {}) {
624
+ if (!id) {
625
+ throw new Error("id is required.");
626
+ }
627
+
628
+ let lastStatus = null;
629
+
630
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
631
+ lastStatus = await this.getActionProductionProofStatus(id);
632
+
633
+ if (lastStatus.ready === true) {
634
+ return fetchProof
635
+ ? {
636
+ status: lastStatus,
637
+ proof: await this.getActionProductionProof(id, { checkReceiptRegistryChain })
638
+ }
639
+ : lastStatus;
640
+ }
641
+
642
+ if (attempt < attempts) {
643
+ await this.sleep(intervalMs);
644
+ }
645
+ }
646
+
647
+ return fetchProof
648
+ ? {
649
+ status: lastStatus,
650
+ proof: null
651
+ }
652
+ : lastStatus;
653
+ }
654
+
655
+ getReceipt(id) {
656
+ return this.request(`/api/actions/${encodeURIComponent(id)}/receipt`);
657
+ }
658
+
659
+ verifyReceipt(receipt, options = {}) {
660
+ return this.request("/api/verify-receipt", {
661
+ method: "POST",
662
+ body: {
663
+ receipt,
664
+ ...options
665
+ }
666
+ });
667
+ }
668
+
669
+ verifyActionReceipt(id, options = {}) {
670
+ return this.request(`/api/actions/${encodeURIComponent(id)}/verify-receipt`, {
671
+ method: "POST",
672
+ body: options
673
+ });
674
+ }
675
+
676
+ async request(path, {
677
+ method = "GET",
678
+ body = null,
679
+ headers: extraHeaders = {},
680
+ requestSigningSecret = this.requestSigningSecret,
681
+ requestTimestamp = null,
682
+ signRequest = false,
683
+ retry = undefined,
684
+ retrySafe = false
685
+ } = {}) {
686
+ const headers = { ...extraHeaders };
687
+ const hasBody = body !== null && body !== undefined;
688
+ let rawBody = hasBody ? JSON.stringify(body) : undefined;
689
+
690
+ if (hasBody) {
691
+ headers["content-type"] = "application/json";
692
+ }
693
+
694
+ if (this.apiKey) {
695
+ headers.authorization = `Bearer ${this.apiKey}`;
696
+ }
697
+
698
+ if (signRequest) {
699
+ const signed = createSignedBotRequest({
700
+ body: rawBody ?? "",
701
+ requestSigningSecret,
702
+ timestamp: requestTimestamp ?? this.timestampProvider(),
703
+ headers
704
+ });
705
+
706
+ if (hasBody) {
707
+ rawBody = signed.body;
708
+ }
709
+ Object.assign(headers, signed.headers);
710
+ }
711
+
712
+ const requestOptions = {
713
+ method,
714
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
715
+ body: rawBody
716
+ };
717
+ const retryOptions = normalizeRetryOptions(retry, this.retry);
718
+ const maxAttempts = Math.max(1, retryOptions.maxRetries + 1);
719
+ let lastError = null;
720
+
721
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
722
+ try {
723
+ const requestUrl = `${this.baseUrl}${path}`;
724
+ const response = await this.fetchWithLocalFallback(requestUrl, requestOptions);
725
+ const text = await response.text();
726
+ const data = text ? JSON.parse(text) : null;
727
+
728
+ if (response.ok) {
729
+ return data;
730
+ }
731
+
732
+ const error = new SolanaVerificationRequestError(data?.error ?? `SVS request failed with HTTP ${response.status}`, {
733
+ status: response.status,
734
+ data,
735
+ attempt,
736
+ retryable: retryOptions.retryableStatuses.includes(response.status)
737
+ });
738
+
739
+ if (!shouldRetryRequest({
740
+ error,
741
+ attempt,
742
+ maxAttempts,
743
+ retrySafe
744
+ })) {
745
+ throw decorateUnsafeRetryError(error, { retrySafe });
746
+ }
747
+
748
+ lastError = error;
749
+ } catch (error) {
750
+ const requestError = error instanceof SolanaVerificationRequestError
751
+ ? error
752
+ : new SolanaVerificationRequestError(error.message ?? "SVS request failed.", {
753
+ cause: error,
754
+ attempt,
755
+ retryable: true
756
+ });
757
+
758
+ if (!shouldRetryRequest({
759
+ error: requestError,
760
+ attempt,
761
+ maxAttempts,
762
+ retrySafe
763
+ })) {
764
+ throw decorateUnsafeRetryError(requestError, { retrySafe });
765
+ }
766
+
767
+ lastError = requestError;
768
+ }
769
+
770
+ await this.sleep(calculateRetryDelayMs({
771
+ attempt,
772
+ baseDelayMs: retryOptions.baseDelayMs,
773
+ maxDelayMs: retryOptions.maxDelayMs
774
+ }));
775
+ }
776
+
777
+ throw lastError;
778
+ }
779
+
780
+ async fetchWithLocalFallback(url, requestOptions) {
781
+ try {
782
+ return await this.fetchImpl(url, requestOptions);
783
+ } catch (error) {
784
+ if (!this.usesDefaultFetch || !shouldUseNodeHttpFallback(url)) {
785
+ throw error;
786
+ }
787
+
788
+ return nodeHttpFetch(url, requestOptions);
789
+ }
790
+ }
791
+ }
792
+
793
+ export class SolanaVerificationRequestError extends Error {
794
+ constructor(message, {
795
+ status = null,
796
+ data = null,
797
+ attempt = null,
798
+ attempts = null,
799
+ retryable = false,
800
+ retrySkippedReason = null,
801
+ cause = null
802
+ } = {}) {
803
+ super(message, cause ? { cause } : undefined);
804
+ this.name = "SolanaVerificationRequestError";
805
+ this.status = status;
806
+ this.data = data;
807
+ this.attempt = attempt;
808
+ this.attempts = attempts ?? attempt;
809
+ this.retryable = retryable;
810
+ this.retrySkippedReason = retrySkippedReason;
811
+ }
812
+ }
813
+
814
+ export class SolanaVerificationProductionCertificationError extends Error {
815
+ constructor(message, { certificationCheck = null } = {}) {
816
+ super(message);
817
+ this.name = "SolanaVerificationProductionCertificationError";
818
+ this.certificationCheck = certificationCheck;
819
+ this.qualityTarget = certificationCheck?.qualityTarget ?? null;
820
+ this.nextAction = certificationCheck?.nextAction ?? null;
821
+ }
822
+ }
823
+
824
+ export function productionCertificationIsReady(result) {
825
+ const target = result?.qualityTarget;
826
+ const integrationContractOk = result?.integrationContract
827
+ ? result.integrationContract.ok === true
828
+ : true;
829
+
830
+ return result?.ok === true &&
831
+ integrationContractOk &&
832
+ target?.ready === true &&
833
+ Number.isInteger(target.score) &&
834
+ Number.isInteger(target.targetScore) &&
835
+ target.score === target.targetScore &&
836
+ target.status === "excellent";
837
+ }
838
+
839
+ export function createSolanaVerificationClientRetrySafetyProof({
840
+ retry = DEFAULT_RETRY_OPTIONS
841
+ } = {}) {
842
+ const retryOptions = normalizeRetryOptions(retry);
843
+ const preservedAcrossRetries = [
844
+ "rawJsonBody",
845
+ "requestTimestamp",
846
+ "requestNonce",
847
+ "hmacSignature",
848
+ "idempotencyKey"
849
+ ];
850
+ const supported = SOLANA_VERIFICATION_CLIENT_CAPABILITIES.includes("retrySafeIdempotentSubmit") &&
851
+ retryOptions.maxRetries > 0 &&
852
+ retryOptions.retryableStatuses.length > 0;
853
+
854
+ return {
855
+ version: BOT_CLIENT_RETRY_SAFETY_VERSION,
856
+ supported,
857
+ status: supported ? "ready" : "disabled",
858
+ clientName: SOLANA_VERIFICATION_CLIENT_NAME,
859
+ clientVersion: SOLANA_VERIFICATION_CLIENT_VERSION,
860
+ capability: "retrySafeIdempotentSubmit",
861
+ maxRetries: retryOptions.maxRetries,
862
+ retryableStatuses: retryOptions.retryableStatuses,
863
+ mutationPolicy: {
864
+ requiresIdempotencyKey: true,
865
+ idempotencySources: [
866
+ "Idempotency-Key header",
867
+ "idempotencyKey body field",
868
+ "requestId body field"
869
+ ],
870
+ unsafeRetrySkippedReason: "idempotency_key_required",
871
+ preservedAcrossRetries
872
+ },
873
+ nextAction: supported
874
+ ? {
875
+ code: "none",
876
+ message: "Client retries transient submit failures only when a stable idempotency key is present."
877
+ }
878
+ : {
879
+ code: "retry_safety_disabled",
880
+ message: "Enable client retry options and use stable requestId or idempotencyKey values before production bot submissions."
881
+ }
882
+ };
883
+ }
884
+
885
+ function createProductionCertificationFailureMessage(result) {
886
+ const target = result?.qualityTarget;
887
+ const score = Number.isInteger(target?.score) ? target.score : "missing";
888
+ const targetScore = Number.isInteger(target?.targetScore)
889
+ ? target.targetScore
890
+ : Number.isInteger(target?.maxScore)
891
+ ? target.maxScore
892
+ : "missing";
893
+ const status = target?.status ?? result?.status ?? "missing";
894
+ const ready = target?.ready === true ? "ready" : "not ready";
895
+ const nextAction = result?.nextAction?.message ?? "Ask the operator to certify the bot integration path.";
896
+ const contract = result?.integrationContract?.required
897
+ ? ` contract=${result.integrationContract.status ?? "unknown"} local=${shortHash(result.integrationContract.localContractHash)} dashboard=${shortHash(result.integrationContract.dashboardContractHash)} certified=${shortHash(result.integrationContract.certifiedContractHash)}`
898
+ : "";
899
+
900
+ return `SVS production certification is required before submitting bot actions: quality=${score}/${targetScore} ${status} (${ready})${contract}. Next action: ${nextAction}`;
901
+ }
902
+
903
+ function createProductionCertificationSourceProof({
904
+ certification,
905
+ integrationContract = null,
906
+ staleAfterMs = undefined
907
+ } = {}) {
908
+ const target = certification?.qualityTarget ?? null;
909
+ const certificationArtifact = certification?.certification?.certification ?? certification?.certification ?? certification;
910
+
911
+ return {
912
+ version: "svs.production-certification-source-proof.v1",
913
+ clientGuard: "SolanaVerificationClient.submitCertifiedAction",
914
+ verified: true,
915
+ checkedAt: certification?.checkedAt ?? null,
916
+ botId: certification?.botId ?? null,
917
+ status: certification?.status ?? null,
918
+ certificationHash: certificationArtifact?.certificationHash ?? null,
919
+ recordId: certificationArtifact?.recordId ?? null,
920
+ staleAfterMs: staleAfterMs ?? null,
921
+ integrationContract: integrationContract
922
+ ? {
923
+ required: integrationContract.required === true,
924
+ ok: integrationContract.ok === true,
925
+ status: integrationContract.status ?? null,
926
+ localContractHash: integrationContract.localContractHash ?? null,
927
+ dashboardContractHash: integrationContract.dashboardContractHash ?? null,
928
+ certifiedContractHash: integrationContract.certifiedContractHash ?? null,
929
+ currentContractHash: integrationContract.currentContractHash ?? null,
930
+ drifted: integrationContract.drifted === true
931
+ }
932
+ : null,
933
+ qualityTarget: target
934
+ ? {
935
+ ready: target.ready === true,
936
+ score: Number.isInteger(target.score) ? target.score : null,
937
+ targetScore: Number.isInteger(target.targetScore) ? target.targetScore : null,
938
+ status: target.status ?? null
939
+ }
940
+ : null
941
+ };
942
+ }
943
+
944
+ function shortHash(value) {
945
+ return value ? `${String(value).slice(0, 12)}...` : "missing";
946
+ }
947
+
948
+ function normalizeRetryOptions(options = {}, fallback = DEFAULT_RETRY_OPTIONS) {
949
+ if (options === false) {
950
+ return {
951
+ ...fallback,
952
+ retryableStatuses: [...fallback.retryableStatuses],
953
+ maxRetries: 0
954
+ };
955
+ }
956
+
957
+ const source = options === true || options === undefined || options === null
958
+ ? {}
959
+ : options;
960
+ const fallbackStatuses = Array.isArray(fallback.retryableStatuses)
961
+ ? fallback.retryableStatuses
962
+ : DEFAULT_RETRYABLE_STATUSES;
963
+ const retryableStatuses = Array.isArray(source.retryableStatuses)
964
+ ? source.retryableStatuses
965
+ : fallbackStatuses;
966
+ const maxRetries = normalizeNonNegativeInteger(source.maxRetries, fallback.maxRetries ?? DEFAULT_RETRY_OPTIONS.maxRetries);
967
+ const baseDelayMs = normalizeNonNegativeInteger(source.baseDelayMs, fallback.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs);
968
+ const maxDelayMs = normalizeNonNegativeInteger(source.maxDelayMs, fallback.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs);
969
+
970
+ return {
971
+ maxRetries,
972
+ baseDelayMs,
973
+ maxDelayMs,
974
+ retryableStatuses: [...new Set(retryableStatuses.map((status) => Number(status)).filter(Number.isInteger))]
975
+ };
976
+ }
977
+
978
+ function normalizeNonNegativeInteger(value, fallback) {
979
+ const parsed = Number(value);
980
+
981
+ if (!Number.isFinite(parsed) || parsed < 0) {
982
+ return fallback;
983
+ }
984
+
985
+ return Math.floor(parsed);
986
+ }
987
+
988
+ function shouldRetryRequest({
989
+ error,
990
+ attempt,
991
+ maxAttempts,
992
+ retrySafe
993
+ } = {}) {
994
+ return retrySafe === true &&
995
+ error?.retryable === true &&
996
+ attempt < maxAttempts;
997
+ }
998
+
999
+ function decorateUnsafeRetryError(error, { retrySafe } = {}) {
1000
+ if (error?.retryable === true && retrySafe !== true && !error.retrySkippedReason) {
1001
+ error.retrySkippedReason = "idempotency_key_required";
1002
+ error.message = `${error.message} Retry skipped because this mutation is not retry-safe without an idempotency key.`;
1003
+ }
1004
+
1005
+ return error;
1006
+ }
1007
+
1008
+ function calculateRetryDelayMs({
1009
+ attempt,
1010
+ baseDelayMs,
1011
+ maxDelayMs
1012
+ } = {}) {
1013
+ const exponent = Math.max(0, Number(attempt ?? 1) - 1);
1014
+ const delay = Number(baseDelayMs ?? 0) * (2 ** exponent);
1015
+ const capped = Math.min(Number(maxDelayMs ?? delay), delay);
1016
+
1017
+ return Number.isFinite(capped) && capped > 0 ? capped : 0;
1018
+ }
1019
+
1020
+ function defaultSleep(ms) {
1021
+ return new Promise((resolve) => setTimeout(resolve, ms));
1022
+ }
1023
+
1024
+ function shouldUseNodeHttpFallback(url) {
1025
+ try {
1026
+ const parsed = new URL(url);
1027
+
1028
+ return ["127.0.0.1", "localhost", "::1"].includes(parsed.hostname) &&
1029
+ ["http:", "https:"].includes(parsed.protocol);
1030
+ } catch {
1031
+ return false;
1032
+ }
1033
+ }
1034
+
1035
+ function nodeHttpFetch(url, {
1036
+ method = "GET",
1037
+ headers = undefined,
1038
+ body = undefined
1039
+ } = {}) {
1040
+ const parsed = new URL(url);
1041
+ const transport = parsed.protocol === "https:" ? https : http;
1042
+
1043
+ return new Promise((resolve, reject) => {
1044
+ const request = transport.request(parsed, {
1045
+ method,
1046
+ headers
1047
+ }, (response) => {
1048
+ const chunks = [];
1049
+
1050
+ response.on("data", (chunk) => chunks.push(chunk));
1051
+ response.on("end", () => {
1052
+ const status = response.statusCode ?? 0;
1053
+ const text = Buffer.concat(chunks).toString("utf8");
1054
+
1055
+ resolve({
1056
+ ok: status >= 200 && status < 300,
1057
+ status,
1058
+ headers: response.headers,
1059
+ text: async () => text,
1060
+ json: async () => JSON.parse(text)
1061
+ });
1062
+ });
1063
+ });
1064
+
1065
+ request.on("error", reject);
1066
+
1067
+ if (body !== undefined && body !== null) {
1068
+ request.write(body);
1069
+ }
1070
+
1071
+ request.end();
1072
+ });
1073
+ }
1074
+
1075
+ function getCertifiedSubmissionRecordId(submission) {
1076
+ return submission?.submitted?.queue?.record?.id
1077
+ ?? submission?.submitted?.record?.id
1078
+ ?? submission?.submitted?.recordId
1079
+ ?? submission?.submitted?.id
1080
+ ?? submission?.submitted?.action?.id
1081
+ ?? null;
1082
+ }
1083
+
1084
+ export function createSolanaVerificationClientCompatibilityReport({ contract } = {}) {
1085
+ const quickstart = contract?.quickstart;
1086
+ const contractClient = quickstart?.client ?? contract?.client ?? {};
1087
+ const requiredCapabilities = Array.isArray(contractClient.requiredCapabilities)
1088
+ ? contractClient.requiredCapabilities
1089
+ : SOLANA_VERIFICATION_CLIENT_CAPABILITIES;
1090
+ const localCapabilities = [...SOLANA_VERIFICATION_CLIENT_CAPABILITIES];
1091
+ const missingLocalCapabilities = requiredCapabilities.filter((capability) => !localCapabilities.includes(capability));
1092
+ const checks = [
1093
+ {
1094
+ name: "contract_version_supported",
1095
+ ok: contract?.version === BOT_INTEGRATION_CONTRACT_VERSION,
1096
+ detail: `version=${contract?.version ?? "missing"}`
1097
+ },
1098
+ {
1099
+ name: "quickstart_version_supported",
1100
+ ok: quickstart?.version === BOT_INTEGRATION_QUICKSTART_VERSION,
1101
+ detail: `version=${quickstart?.version ?? "missing"}`
1102
+ },
1103
+ {
1104
+ name: "client_class_matches",
1105
+ ok: !contractClient.className || contractClient.className === SOLANA_VERIFICATION_CLIENT_NAME,
1106
+ detail: `expected=${contractClient.className ?? SOLANA_VERIFICATION_CLIENT_NAME} local=${SOLANA_VERIFICATION_CLIENT_NAME}`
1107
+ },
1108
+ {
1109
+ name: "request_signing_helper_available",
1110
+ ok: !contractClient.requestSigningHelper || contractClient.requestSigningHelper === "createSignedBotRequest",
1111
+ detail: `expected=${contractClient.requestSigningHelper ?? "createSignedBotRequest"}`
1112
+ },
1113
+ {
1114
+ name: "readiness_helper_available",
1115
+ ok: !contractClient.readinessHelper || /checkBotReadiness/.test(String(contractClient.readinessHelper)),
1116
+ detail: `expected=${contractClient.readinessHelper ?? "checkBotReadiness"}`
1117
+ },
1118
+ {
1119
+ name: "minimum_startup_check_available",
1120
+ ok: !contractClient.minimumStartupCheck || /checkBotReadiness/.test(String(contractClient.minimumStartupCheck)),
1121
+ detail: `expected=${contractClient.minimumStartupCheck ?? "checkBotReadiness"}`
1122
+ },
1123
+ {
1124
+ name: "required_capabilities_available",
1125
+ ok: missingLocalCapabilities.length === 0,
1126
+ detail: missingLocalCapabilities.length === 0
1127
+ ? `capabilities=${requiredCapabilities.join(", ")}`
1128
+ : `missing=${missingLocalCapabilities.join(", ")}`
1129
+ }
1130
+ ];
1131
+ const failed = checks.find((check) => !check.ok);
1132
+ const ok = !failed;
1133
+
1134
+ return {
1135
+ version: BOT_CLIENT_COMPATIBILITY_VERSION,
1136
+ ok,
1137
+ status: ok ? "compatible" : "incompatible",
1138
+ client: {
1139
+ name: SOLANA_VERIFICATION_CLIENT_NAME,
1140
+ version: SOLANA_VERIFICATION_CLIENT_VERSION,
1141
+ capabilities: localCapabilities
1142
+ },
1143
+ contract: {
1144
+ version: contract?.version ?? null,
1145
+ quickstartVersion: quickstart?.version ?? null,
1146
+ className: contractClient.className ?? null,
1147
+ requestSigningHelper: contractClient.requestSigningHelper ?? null,
1148
+ readinessHelper: contractClient.readinessHelper ?? null,
1149
+ minimumStartupCheck: contractClient.minimumStartupCheck ?? null,
1150
+ requiredCapabilities
1151
+ },
1152
+ missingLocalCapabilities,
1153
+ checks,
1154
+ nextAction: ok
1155
+ ? {
1156
+ code: "none",
1157
+ message: "Local bot client is compatible with the live integration contract."
1158
+ }
1159
+ : {
1160
+ code: "client_contract_incompatible",
1161
+ message: `Update the bot client or dashboard contract before live submissions. First failing check: ${failed.name}.`
1162
+ }
1163
+ };
1164
+ }
1165
+
1166
+ function selectReadinessBot(readiness, botId) {
1167
+ const bots = Array.isArray(readiness?.bots) ? readiness.bots : [];
1168
+
1169
+ if (botId) {
1170
+ return bots.find((bot) => bot.botId === botId) ?? null;
1171
+ }
1172
+
1173
+ return bots.length === 1 ? bots[0] : null;
1174
+ }
1175
+
1176
+ function summarizeReadinessBot(bot) {
1177
+ return {
1178
+ botId: bot.botId,
1179
+ name: bot.name ?? bot.botId,
1180
+ status: bot.status ?? null,
1181
+ productionReady: bot.productionReady === true,
1182
+ readinessStatus: bot.readinessStatus ?? null,
1183
+ credentialRotationStatus: bot.credentialRotationStatus ?? null,
1184
+ requestSigningSecretSlots: Array.isArray(bot.requestSigningSecretSlots)
1185
+ ? bot.requestSigningSecretSlots
1186
+ : [],
1187
+ lastSuccessfulSignedRequest: bot.lastSuccessfulSignedRequest ?? null,
1188
+ selfTest: bot.selfTest ?? null,
1189
+ blockingReasons: Array.isArray(bot.blockingReasons) ? bot.blockingReasons : []
1190
+ };
1191
+ }
1192
+
1193
+ function createReadinessNextAction({ bot, readiness, selfTest, runSelfTest }) {
1194
+ if (runSelfTest && selfTest?.ok !== true) {
1195
+ return {
1196
+ code: "self_test_failed",
1197
+ message: selfTest?.error ?? "Run the signed bot self-test successfully before live traffic."
1198
+ };
1199
+ }
1200
+
1201
+ if (!bot) {
1202
+ const botCount = Array.isArray(readiness?.bots) ? readiness.bots.length : 0;
1203
+
1204
+ return {
1205
+ code: botCount === 0 ? "no_bot_found" : "select_bot_id",
1206
+ message: botCount === 0
1207
+ ? "No bot credential readiness entry was returned."
1208
+ : "Pass botId so the client can report the exact bot readiness entry."
1209
+ };
1210
+ }
1211
+
1212
+ const blockingReason = Array.isArray(bot.blockingReasons) ? bot.blockingReasons[0] : null;
1213
+
1214
+ if (blockingReason) {
1215
+ return {
1216
+ code: blockingReason.code ?? "credential_readiness_blocked",
1217
+ message: blockingReason.message ?? "Resolve the first credential readiness blocker."
1218
+ };
1219
+ }
1220
+
1221
+ if (bot.adoption?.nextAction && bot.adoption.nextAction.code !== "none") {
1222
+ return {
1223
+ code: bot.adoption.nextAction.code,
1224
+ message: bot.adoption.nextAction.message
1225
+ };
1226
+ }
1227
+
1228
+ return {
1229
+ code: "none",
1230
+ message: "Bot readiness path is complete."
1231
+ };
1232
+ }