@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,146 @@
1
+ export const SVS_ADAPTER_CATALOG_VERSION = "svs.agent-framework-adapter-catalog.v1";
2
+
3
+ const FRAMEWORK_ADAPTERS = [
4
+ {
5
+ id: "custom-adapter-core",
6
+ runtime: "Custom agent runtime",
7
+ category: "adapter-core",
8
+ packageExport: "@svsprotocol/solana/adapter-core",
9
+ exportPath: "./adapter-core",
10
+ adapterVersion: "svs.adapter-core.v1",
11
+ readinessHelper: "requireSvsAdapterProductionReady",
12
+ submitHelper: "verifyAndSubmitSvsAdapterSolanaAction",
13
+ toolName: null,
14
+ pluginName: null,
15
+ actionName: null,
16
+ demoCommand: "npm run example:typescript:agent",
17
+ docsPath: "examples/host-apps/custom-adapter-core/README.md",
18
+ hostValidationTarget: "custom-adapter-core",
19
+ bestFit: "Any Solana agent framework that wants SVS without a first-party adapter.",
20
+ primary: false
21
+ },
22
+ {
23
+ id: "solana-agent-kit",
24
+ runtime: "Solana Agent Kit",
25
+ category: "framework-adapter",
26
+ packageExport: "@svsprotocol/solana/solana-agent-kit",
27
+ exportPath: "./solana-agent-kit",
28
+ adapterVersion: "svs.solana-agent-kit-adapter.v1",
29
+ readinessHelper: "requireSvsProductionReady",
30
+ submitHelper: "verifyAndSubmitSolanaAction",
31
+ toolName: "svs_verify_and_submit_solana_action",
32
+ pluginName: null,
33
+ actionName: null,
34
+ demoCommand: "npm run example:agent:solana-kit",
35
+ docsPath: "integrations/solana-agent-kit/README.md",
36
+ hostValidationTarget: "solana-agent-kit-minimal",
37
+ bestFit: "Solana-native agent kits that register executable tools.",
38
+ primary: true
39
+ },
40
+ {
41
+ id: "elizaos",
42
+ runtime: "ElizaOS",
43
+ category: "framework-adapter",
44
+ packageExport: "@svsprotocol/solana/elizaos",
45
+ exportPath: "./elizaos",
46
+ adapterVersion: "svs.elizaos-adapter.v1",
47
+ readinessHelper: "requireSvsElizaOsProductionReady",
48
+ submitHelper: "verifyAndSubmitElizaOsSolanaAction",
49
+ toolName: null,
50
+ pluginName: "svs-solana-verification",
51
+ actionName: "SVS_VERIFY_AND_SUBMIT_SOLANA_ACTION",
52
+ demoCommand: "npm run example:agent:elizaos",
53
+ docsPath: "integrations/elizaos/README.md",
54
+ hostValidationTarget: "elizaos-plugin-install",
55
+ bestFit: "ElizaOS plugins/actions with fail-closed handler results.",
56
+ primary: true
57
+ },
58
+ {
59
+ id: "goat",
60
+ runtime: "GOAT",
61
+ category: "framework-adapter",
62
+ packageExport: "@svsprotocol/solana/goat",
63
+ exportPath: "./goat",
64
+ adapterVersion: "svs.goat-adapter.v1",
65
+ readinessHelper: "requireSvsGoatProductionReady",
66
+ submitHelper: "verifyAndSubmitGoatSolanaAction",
67
+ toolName: "svs_verify_and_submit_solana_action",
68
+ pluginName: "svs-solana-verification",
69
+ actionName: null,
70
+ demoCommand: "npm run example:agent:goat",
71
+ docsPath: "integrations/goat/README.md",
72
+ hostValidationTarget: "goat-sdk-plugin-install",
73
+ bestFit: "GOAT finance agents and Solana transaction tools.",
74
+ primary: true
75
+ },
76
+ {
77
+ id: "vercel-ai-sdk",
78
+ runtime: "Vercel AI SDK",
79
+ category: "framework-adapter",
80
+ packageExport: "@svsprotocol/solana/vercel-ai",
81
+ exportPath: "./vercel-ai",
82
+ adapterVersion: "svs.vercel-ai-sdk-adapter.v1",
83
+ readinessHelper: "requireSvsVercelAiSdkProductionReady",
84
+ submitHelper: "verifyAndSubmitVercelAiSdkSolanaAction",
85
+ toolName: "svsVerifyAndSubmitSolanaAction",
86
+ pluginName: null,
87
+ actionName: null,
88
+ demoCommand: "npm run example:agent:vercel-ai",
89
+ docsPath: "integrations/vercel-ai/README.md",
90
+ hostValidationTarget: "vercel-ai-sdk-tool-install",
91
+ bestFit: "AI SDK generateText and streamText tool maps.",
92
+ primary: true
93
+ }
94
+ ];
95
+
96
+ export const SVS_AGENT_FRAMEWORK_ADAPTERS = Object.freeze(
97
+ FRAMEWORK_ADAPTERS.map((adapter) => Object.freeze({ ...adapter }))
98
+ );
99
+
100
+ export function getSvsAgentFrameworkAdapters({
101
+ includeCustom = true,
102
+ primaryOnly = false
103
+ } = {}) {
104
+ return SVS_AGENT_FRAMEWORK_ADAPTERS
105
+ .filter((adapter) => includeCustom || adapter.id !== "custom-adapter-core")
106
+ .filter((adapter) => !primaryOnly || adapter.primary === true)
107
+ .map((adapter) => ({ ...adapter }));
108
+ }
109
+
110
+ export function findSvsAgentFrameworkAdapter(identifier) {
111
+ if (!identifier) {
112
+ return null;
113
+ }
114
+
115
+ const raw = String(identifier);
116
+ const foundExact = SVS_AGENT_FRAMEWORK_ADAPTERS.find((adapter) => {
117
+ const values = getAdapterIdentifierValues(adapter);
118
+
119
+ return values.includes(raw);
120
+ });
121
+
122
+ if (foundExact) {
123
+ return { ...foundExact };
124
+ }
125
+
126
+ const normalized = raw.toLowerCase();
127
+ const foundNormalized = SVS_AGENT_FRAMEWORK_ADAPTERS.find((adapter) => {
128
+ const values = getAdapterIdentifierValues(adapter).map((value) => value.toLowerCase());
129
+
130
+ return values.includes(normalized);
131
+ });
132
+
133
+ return foundNormalized ? { ...foundNormalized } : null;
134
+ }
135
+
136
+ function getAdapterIdentifierValues(adapter) {
137
+ return [
138
+ adapter.id,
139
+ adapter.runtime,
140
+ adapter.packageExport,
141
+ adapter.exportPath,
142
+ adapter.toolName,
143
+ adapter.pluginName,
144
+ adapter.actionName
145
+ ].filter(Boolean).map((value) => String(value));
146
+ }
@@ -0,0 +1,48 @@
1
+ import type {
2
+ SvsAdapterActionInput,
3
+ SvsAdapterActionResult,
4
+ SvsAdapterOptions,
5
+ SvsAdapterReadinessResult,
6
+ SvsClientLike
7
+ } from "./common.js";
8
+
9
+ export class SvsAdapterCoreError extends Error {
10
+ details: unknown;
11
+ constructor(message: string, details?: unknown);
12
+ }
13
+
14
+ export interface SvsAdapterClientOptions {
15
+ client?: SvsAdapterOptions["client"];
16
+ baseUrl?: string;
17
+ apiKey?: string;
18
+ requestSigningSecret?: string;
19
+ expectedIntegrationContractHash?: string;
20
+ }
21
+
22
+ export type SvsAdapterProductionReadinessOptions = SvsAdapterOptions & {
23
+ runSelfTest?: boolean;
24
+ pendingProofMaxAgeMs?: number;
25
+ requireNoExpiredPreviousSigningSecrets?: boolean;
26
+ readinessVersion?: string;
27
+ readyMessage?: string;
28
+ adapterLabel?: string;
29
+ ErrorClass?: typeof SvsAdapterCoreError;
30
+ };
31
+
32
+ export type SvsAdapterSolanaActionSubmitOptions = SvsAdapterOptions & SvsAdapterActionInput & {
33
+ adapterVersion: string;
34
+ agentFramework: string;
35
+ resultVersion: string;
36
+ readinessVersion?: string;
37
+ readyMessage?: string;
38
+ adapterLabel?: string;
39
+ ErrorClass?: typeof SvsAdapterCoreError;
40
+ };
41
+
42
+ export function createSvsAdapterClient(options?: SvsAdapterClientOptions): SvsClientLike | unknown;
43
+
44
+ export function requireSvsAdapterProductionReady(options?: SvsAdapterProductionReadinessOptions): Promise<SvsAdapterReadinessResult>;
45
+
46
+ export function verifyAndSubmitSvsAdapterSolanaAction(options?: SvsAdapterSolanaActionSubmitOptions): Promise<SvsAdapterActionResult>;
47
+
48
+ export function extractSubmittedRecordId(result: unknown): string | null;
@@ -0,0 +1,249 @@
1
+ import {
2
+ SolanaVerificationClient,
3
+ productionCertificationIsReady
4
+ } from "./index.js";
5
+
6
+ export class SvsAdapterCoreError extends Error {
7
+ constructor(message, details = {}) {
8
+ super(message);
9
+ this.name = "SvsAdapterCoreError";
10
+ this.details = details;
11
+ }
12
+ }
13
+
14
+ export function createSvsAdapterClient({
15
+ client = null,
16
+ baseUrl,
17
+ apiKey,
18
+ requestSigningSecret,
19
+ expectedIntegrationContractHash
20
+ } = {}) {
21
+ return client ?? new SolanaVerificationClient({
22
+ baseUrl,
23
+ apiKey,
24
+ requestSigningSecret,
25
+ expectedIntegrationContractHash
26
+ });
27
+ }
28
+
29
+ export async function requireSvsAdapterProductionReady({
30
+ client = null,
31
+ baseUrl,
32
+ apiKey,
33
+ requestSigningSecret,
34
+ expectedIntegrationContractHash,
35
+ botId,
36
+ certificationStaleAfterMs = undefined,
37
+ requireCurrentIntegrationContract = true,
38
+ runSelfTest = true,
39
+ pendingProofMaxAgeMs = undefined,
40
+ requireNoExpiredPreviousSigningSecrets = true,
41
+ readinessVersion = "svs.adapter-readiness.v1",
42
+ readyMessage = "SVS adapter readiness and production certification passed.",
43
+ adapterLabel = "SVS adapter",
44
+ ErrorClass = SvsAdapterCoreError
45
+ } = {}) {
46
+ if (!botId) {
47
+ throw new Error("botId is required.");
48
+ }
49
+
50
+ const svs = createSvsAdapterClient({
51
+ client,
52
+ baseUrl,
53
+ apiKey,
54
+ requestSigningSecret,
55
+ expectedIntegrationContractHash
56
+ });
57
+ const readiness = await svs.checkBotReadiness({
58
+ botId,
59
+ runSelfTest,
60
+ pendingProofMaxAgeMs,
61
+ requireNoExpiredPreviousSigningSecrets
62
+ });
63
+
64
+ if (readiness.ok !== true) {
65
+ throw new ErrorClass(
66
+ `SVS bot readiness failed for ${botId}: ${readiness.nextAction?.message ?? readiness.status ?? "not ready"}`,
67
+ { readiness }
68
+ );
69
+ }
70
+
71
+ const certification = await svs.requireProductionCertification({
72
+ botId,
73
+ staleAfterMs: certificationStaleAfterMs,
74
+ requireCurrentIntegrationContract
75
+ });
76
+
77
+ if (!productionCertificationIsReady(certification)) {
78
+ throw new ErrorClass(
79
+ `SVS production certification failed for ${botId}: ${certification.nextAction?.message ?? certification.status ?? "not certified"}`,
80
+ {
81
+ readiness,
82
+ certification
83
+ }
84
+ );
85
+ }
86
+
87
+ return {
88
+ version: readinessVersion,
89
+ ok: true,
90
+ status: "ready",
91
+ botId: certification.botId ?? readiness.botId ?? botId,
92
+ readiness,
93
+ certification,
94
+ nextAction: {
95
+ code: "none",
96
+ message: readyMessage || `${adapterLabel} readiness and production certification passed.`
97
+ }
98
+ };
99
+ }
100
+
101
+ export async function verifyAndSubmitSvsAdapterSolanaAction({
102
+ client = null,
103
+ baseUrl,
104
+ apiKey,
105
+ requestSigningSecret,
106
+ expectedIntegrationContractHash,
107
+ botId,
108
+ requestId = null,
109
+ idempotencyKey = requestId,
110
+ intent,
111
+ transaction = null,
112
+ serializedTransaction = transaction?.serializedTransaction ?? transaction,
113
+ policyId,
114
+ rpcUrl,
115
+ simulation = null,
116
+ metadata = null,
117
+ source = {},
118
+ certificationStaleAfterMs = undefined,
119
+ requireCurrentIntegrationContract = true,
120
+ waitForProof = false,
121
+ waitAttempts = 12,
122
+ waitIntervalMs = 5000,
123
+ fetchProof = true,
124
+ checkReceiptRegistryChain = true,
125
+ requireReadinessOnSubmit = true,
126
+ adapterVersion,
127
+ agentFramework,
128
+ resultVersion,
129
+ readinessVersion = "svs.adapter-readiness.v1",
130
+ readyMessage,
131
+ adapterLabel,
132
+ ErrorClass = SvsAdapterCoreError
133
+ } = {}) {
134
+ if (!botId) {
135
+ throw new Error("botId is required.");
136
+ }
137
+
138
+ if (!intent || typeof intent !== "object") {
139
+ throw new Error("intent is required.");
140
+ }
141
+
142
+ if (!policyId) {
143
+ throw new Error("policyId is required.");
144
+ }
145
+
146
+ if (!serializedTransaction) {
147
+ throw new Error("serializedTransaction is required.");
148
+ }
149
+
150
+ if (!adapterVersion) {
151
+ throw new Error("adapterVersion is required.");
152
+ }
153
+
154
+ if (!agentFramework) {
155
+ throw new Error("agentFramework is required.");
156
+ }
157
+
158
+ if (!resultVersion) {
159
+ throw new Error("resultVersion is required.");
160
+ }
161
+
162
+ const svs = createSvsAdapterClient({
163
+ client,
164
+ baseUrl,
165
+ apiKey,
166
+ requestSigningSecret,
167
+ expectedIntegrationContractHash
168
+ });
169
+ let readiness = null;
170
+
171
+ if (requireReadinessOnSubmit !== false) {
172
+ readiness = await requireSvsAdapterProductionReady({
173
+ client: svs,
174
+ botId,
175
+ certificationStaleAfterMs,
176
+ requireCurrentIntegrationContract,
177
+ readinessVersion,
178
+ readyMessage,
179
+ adapterLabel,
180
+ ErrorClass
181
+ });
182
+ }
183
+
184
+ const actionPayload = {
185
+ requestId,
186
+ idempotencyKey,
187
+ intent,
188
+ policyId,
189
+ simulation,
190
+ serializedTransaction,
191
+ rpcUrl,
192
+ metadata,
193
+ source: {
194
+ ...source,
195
+ submittedBy: source.submittedBy ?? botId,
196
+ adapter: adapterVersion,
197
+ agentFramework: source.agentFramework ?? agentFramework
198
+ }
199
+ };
200
+ const result = waitForProof
201
+ ? await svs.submitCertifiedActionAndWaitForProof(actionPayload, {
202
+ idempotencyKey,
203
+ botId,
204
+ certificationStaleAfterMs,
205
+ requireCurrentIntegrationContract,
206
+ waitAttempts,
207
+ waitIntervalMs,
208
+ fetchProof,
209
+ checkReceiptRegistryChain
210
+ })
211
+ : await svs.submitCertifiedAction(actionPayload, {
212
+ idempotencyKey,
213
+ botId,
214
+ certificationStaleAfterMs,
215
+ requireCurrentIntegrationContract
216
+ });
217
+
218
+ return {
219
+ version: resultVersion,
220
+ ok: result.ok !== false,
221
+ status: waitForProof ? result.status : "submitted",
222
+ botId: result.botId ?? botId,
223
+ requestId,
224
+ idempotencyKey,
225
+ policyId,
226
+ adapter: adapterVersion,
227
+ readiness,
228
+ result,
229
+ recordId: waitForProof
230
+ ? result.recordId ?? null
231
+ : extractSubmittedRecordId(result),
232
+ productionProofReady: waitForProof
233
+ ? result.ok === true && result.status === "production_proof_ready"
234
+ : false,
235
+ nextAction: result.nextAction ?? {
236
+ code: "wait_for_human_approval",
237
+ message: "Wait for human approval, broadcast, registry write, and production proof verification."
238
+ }
239
+ };
240
+ }
241
+
242
+ export function extractSubmittedRecordId(result) {
243
+ return result?.submitted?.record?.id ??
244
+ result?.submitted?.queue?.record?.id ??
245
+ result?.submitted?.recordId ??
246
+ result?.submitted?.id ??
247
+ result?.submitted?.action?.id ??
248
+ null;
249
+ }
@@ -0,0 +1,197 @@
1
+ import { createHash, createPublicKey, sign, verify } from "node:crypto";
2
+ import { decodeBase58, encodeBase58 } from "./base58.js";
3
+
4
+ export const APPROVAL_MESSAGE_VERSION = "svs.approval.v1";
5
+ export const APPROVAL_MESSAGE_DOMAIN = "svs-solana-verification-system";
6
+ export const LEGACY_APPROVAL_MESSAGE_DOMAIN = "solana-human-verification-system";
7
+
8
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
9
+ const VALID_OUTCOMES = new Set(["approved", "rejected", "acknowledged"]);
10
+ const VALID_DOMAINS = new Set([APPROVAL_MESSAGE_DOMAIN, LEGACY_APPROVAL_MESSAGE_DOMAIN]);
11
+
12
+ export function createApprovalPayload(record, {
13
+ outcome,
14
+ reviewedAt,
15
+ domain = APPROVAL_MESSAGE_DOMAIN
16
+ }) {
17
+ assertValidOutcome(outcome);
18
+ assertValidDomain(domain);
19
+
20
+ if (!reviewedAt) {
21
+ throw new Error("reviewedAt is required to create an approval payload.");
22
+ }
23
+
24
+ return {
25
+ domain,
26
+ version: APPROVAL_MESSAGE_VERSION,
27
+ chain: "solana-devnet",
28
+ recordId: record.id,
29
+ outcome,
30
+ controllerWallet: record.intent.controllerWallet,
31
+ botId: record.intent.botId,
32
+ txType: record.intent.txType,
33
+ amountSol: record.intent.amountSol,
34
+ destinationWallet: record.intent.destinationWallet ?? null,
35
+ serviceFee: record.serviceFee ?? null,
36
+ receiptHash: record.receipt.receiptHash,
37
+ policyHash: record.receipt.policyHash,
38
+ intentHash: record.receipt.intentHash,
39
+ simulationHash: record.receipt.simulationHash,
40
+ reviewedAt
41
+ };
42
+ }
43
+
44
+ export function createApprovalMessage(record, { outcome, reviewedAt, domain }) {
45
+ return JSON.stringify(createApprovalPayload(record, { outcome, reviewedAt, domain }), null, 2);
46
+ }
47
+
48
+ export function signApproval({
49
+ record,
50
+ outcome,
51
+ reviewedAt = new Date().toISOString(),
52
+ wallet,
53
+ domain = APPROVAL_MESSAGE_DOMAIN
54
+ }) {
55
+ const message = createApprovalMessage(record, { outcome, reviewedAt, domain });
56
+ const signature = sign(null, Buffer.from(message, "utf8"), wallet.privateKey);
57
+
58
+ return {
59
+ version: APPROVAL_MESSAGE_VERSION,
60
+ scheme: "ed25519",
61
+ signer: wallet.publicKey,
62
+ message,
63
+ messageHash: hashMessage(message),
64
+ signature: encodeBase58(signature)
65
+ };
66
+ }
67
+
68
+ export function verifyApprovalSignature(record, signedApproval) {
69
+ if (!signedApproval || typeof signedApproval !== "object") {
70
+ throw new Error("A signed approval is required.");
71
+ }
72
+
73
+ const payload = parseApprovalMessage(signedApproval.message);
74
+ const expectedMessage = createApprovalMessage(record, {
75
+ outcome: payload.outcome,
76
+ reviewedAt: payload.reviewedAt,
77
+ domain: payload.domain
78
+ });
79
+ const signer = signedApproval.signer ?? payload.controllerWallet;
80
+
81
+ if (signedApproval.version && signedApproval.version !== APPROVAL_MESSAGE_VERSION) {
82
+ throw new Error(`Unsupported approval version: ${signedApproval.version}`);
83
+ }
84
+
85
+ if (signedApproval.scheme && signedApproval.scheme !== "ed25519") {
86
+ throw new Error(`Unsupported approval signature scheme: ${signedApproval.scheme}`);
87
+ }
88
+
89
+ if (signedApproval.message !== expectedMessage) {
90
+ throw new Error("Signed approval message does not match this approval record.");
91
+ }
92
+
93
+ if (signedApproval.messageHash && signedApproval.messageHash !== hashMessage(signedApproval.message)) {
94
+ throw new Error("Signed approval message hash does not match the approval message.");
95
+ }
96
+
97
+ if (signer !== record.intent.controllerWallet) {
98
+ throw new Error("Signed approval must come from the controller wallet.");
99
+ }
100
+
101
+ const publicKey = createSolanaPublicKey(signer);
102
+ const signature = decodeBase58(signedApproval.signature);
103
+ const verified = verify(null, Buffer.from(signedApproval.message, "utf8"), publicKey, signature);
104
+
105
+ if (!verified) {
106
+ throw new Error("Approval signature verification failed.");
107
+ }
108
+
109
+ return {
110
+ version: APPROVAL_MESSAGE_VERSION,
111
+ scheme: "ed25519",
112
+ signer,
113
+ message: signedApproval.message,
114
+ messageHash: hashMessage(signedApproval.message),
115
+ signature: signedApproval.signature,
116
+ verified: true,
117
+ payload
118
+ };
119
+ }
120
+
121
+ export function summarizeApprovalDomain(signatureOrPayload) {
122
+ const domain = extractApprovalDomain(signatureOrPayload);
123
+ const status = domain === APPROVAL_MESSAGE_DOMAIN
124
+ ? "current"
125
+ : domain === LEGACY_APPROVAL_MESSAGE_DOMAIN
126
+ ? "legacy_compatible"
127
+ : domain
128
+ ? "unsupported"
129
+ : "missing";
130
+
131
+ return {
132
+ domain,
133
+ status,
134
+ current: status === "current",
135
+ legacyCompatible: status === "legacy_compatible",
136
+ supported: status === "current" || status === "legacy_compatible"
137
+ };
138
+ }
139
+
140
+ function parseApprovalMessage(message) {
141
+ if (typeof message !== "string" || message.length === 0) {
142
+ throw new Error("Signed approval message is required.");
143
+ }
144
+
145
+ const payload = JSON.parse(message);
146
+
147
+ if (payload.version !== APPROVAL_MESSAGE_VERSION) {
148
+ throw new Error(`Unsupported approval message version: ${payload.version}`);
149
+ }
150
+
151
+ assertValidOutcome(payload.outcome);
152
+ assertValidDomain(payload.domain);
153
+
154
+ return payload;
155
+ }
156
+
157
+ function extractApprovalDomain(signatureOrPayload) {
158
+ if (!signatureOrPayload || typeof signatureOrPayload !== "object") return null;
159
+ if (signatureOrPayload.domain) return signatureOrPayload.domain;
160
+ if (!signatureOrPayload.message) return null;
161
+
162
+ try {
163
+ return JSON.parse(signatureOrPayload.message)?.domain ?? null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ function createSolanaPublicKey(publicKey) {
170
+ const publicKeyBytes = decodeBase58(publicKey);
171
+
172
+ if (publicKeyBytes.length !== 32) {
173
+ throw new Error("Controller wallet public key must decode to 32 bytes.");
174
+ }
175
+
176
+ return createPublicKey({
177
+ key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKeyBytes)]),
178
+ format: "der",
179
+ type: "spki"
180
+ });
181
+ }
182
+
183
+ function hashMessage(message) {
184
+ return createHash("sha256").update(message).digest("hex");
185
+ }
186
+
187
+ function assertValidOutcome(outcome) {
188
+ if (!VALID_OUTCOMES.has(outcome)) {
189
+ throw new Error("Review outcome must be approved, rejected, or acknowledged.");
190
+ }
191
+ }
192
+
193
+ function assertValidDomain(domain) {
194
+ if (!VALID_DOMAINS.has(domain)) {
195
+ throw new Error(`Unsupported approval message domain: ${domain ?? "missing"}.`);
196
+ }
197
+ }
package/dist/base58.js ADDED
@@ -0,0 +1,69 @@
1
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2
+ const BASE = BigInt(ALPHABET.length);
3
+ const LOOKUP = new Map([...ALPHABET].map((char, index) => [char, BigInt(index)]));
4
+
5
+ export function encodeBase58(bytes) {
6
+ if (!(bytes instanceof Uint8Array)) {
7
+ throw new TypeError("encodeBase58 expects a Uint8Array.");
8
+ }
9
+
10
+ let value = 0n;
11
+
12
+ for (const byte of bytes) {
13
+ value = value * 256n + BigInt(byte);
14
+ }
15
+
16
+ let encoded = "";
17
+
18
+ while (value > 0n) {
19
+ const remainder = value % BASE;
20
+ value /= BASE;
21
+ encoded = ALPHABET[Number(remainder)] + encoded;
22
+ }
23
+
24
+ for (const byte of bytes) {
25
+ if (byte !== 0) {
26
+ break;
27
+ }
28
+
29
+ encoded = "1" + encoded;
30
+ }
31
+
32
+ return encoded || "1";
33
+ }
34
+
35
+ export function decodeBase58(value) {
36
+ if (typeof value !== "string" || value.length === 0) {
37
+ throw new TypeError("decodeBase58 expects a non-empty string.");
38
+ }
39
+
40
+ let decoded = 0n;
41
+
42
+ for (const char of value) {
43
+ const index = LOOKUP.get(char);
44
+
45
+ if (index === undefined) {
46
+ throw new Error(`Invalid base58 character: ${char}`);
47
+ }
48
+
49
+ decoded = decoded * BASE + index;
50
+ }
51
+
52
+ const bytes = [];
53
+
54
+ while (decoded > 0n) {
55
+ bytes.unshift(Number(decoded % 256n));
56
+ decoded /= 256n;
57
+ }
58
+
59
+ for (const char of value) {
60
+ if (char !== "1") {
61
+ break;
62
+ }
63
+
64
+ bytes.unshift(0);
65
+ }
66
+
67
+ return Uint8Array.from(bytes);
68
+ }
69
+