@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.
- package/LICENSE +158 -0
- package/README.md +365 -0
- package/dist/action-production-proof-evidence.js +553 -0
- package/dist/adapter-catalog.d.ts +29 -0
- package/dist/adapter-catalog.js +146 -0
- package/dist/adapter-core.d.ts +48 -0
- package/dist/adapter-core.js +249 -0
- package/dist/approval-signature.js +197 -0
- package/dist/base58.js +69 -0
- package/dist/bot-auth.js +50 -0
- package/dist/bot-certification-evidence.js +342 -0
- package/dist/bot-first-action-runbook.js +299 -0
- package/dist/bot-integration-contract.js +41 -0
- package/dist/certified-submit-status.js +176 -0
- package/dist/common.d.ts +1135 -0
- package/dist/elizaos.d.ts +43 -0
- package/dist/elizaos.js +227 -0
- package/dist/goat.d.ts +47 -0
- package/dist/goat.js +261 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +128 -0
- package/dist/protocol.d.ts +205 -0
- package/dist/protocol.js +900 -0
- package/dist/receipt.js +51 -0
- package/dist/signed-proof-read-protection.js +495 -0
- package/dist/solana-agent-kit.d.ts +35 -0
- package/dist/solana-agent-kit.js +151 -0
- package/dist/svs-client.js +1232 -0
- package/dist/vercel-ai.d.ts +47 -0
- package/dist/vercel-ai.js +266 -0
- package/dist/verified-agent-adoption-kit.js +471 -0
- package/dist/verified-agent-profile.js +329 -0
- package/dist/verified-agent-registry-consumer.js +421 -0
- package/dist/verified-agent-registry.d.ts +36 -0
- package/dist/verified-agent-registry.js +826 -0
- package/dist/verified-agent-trust-score.js +335 -0
- package/dist/webhooks.js +834 -0
- 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
|
+
}
|