@cascade-fyi/sati-sdk 0.5.0 → 0.7.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/dist/index.cjs CHANGED
@@ -4641,6 +4641,36 @@ function deserializeEncryptedPayload(bytes) {
4641
4641
 
4642
4642
  //#endregion
4643
4643
  //#region src/uploaders.ts
4644
+ const SATI_UPLOAD_URL = "https://sati.cascade.fyi/api/upload-metadata";
4645
+ /**
4646
+ * Create a hosted SATI metadata uploader.
4647
+ *
4648
+ * Uploads JSON to the SATI Identity Service which pins it to IPFS via Pinata.
4649
+ * No API keys needed - zero-config alternative to `createPinataUploader()`.
4650
+ *
4651
+ * @returns MetadataUploader that uploads via sati.cascade.fyi
4652
+ *
4653
+ * @example
4654
+ * ```typescript
4655
+ * const uploader = createSatiUploader();
4656
+ * const uri = await uploader.upload({ name: "MyAgent" });
4657
+ * // "ipfs://QmXyz..."
4658
+ * ```
4659
+ */
4660
+ function createSatiUploader() {
4661
+ return { async upload(data) {
4662
+ const response = await fetch(SATI_UPLOAD_URL, {
4663
+ method: "POST",
4664
+ headers: { "Content-Type": "application/json" },
4665
+ body: JSON.stringify(data)
4666
+ });
4667
+ if (!response.ok) {
4668
+ const text = await response.text();
4669
+ throw new Error(`Metadata upload failed (${response.status}): ${text}`);
4670
+ }
4671
+ return (await response.json()).uri;
4672
+ } };
4673
+ }
4644
4674
  /**
4645
4675
  * Create a Pinata IPFS uploader using the v3 Files API.
4646
4676
  *
@@ -4677,8 +4707,7 @@ function createPinataUploader(jwt) {
4677
4707
  const verifyResponse = await fetch(`https://gateway.pinata.cloud/ipfs/${cid}`, { signal: AbortSignal.timeout(5e3) });
4678
4708
  if (!verifyResponse.ok && verifyResponse.status !== 429) throw new Error(`Pinata returned CID ${cid} but content not accessible on gateway (HTTP ${verifyResponse.status})`);
4679
4709
  } catch (verifyError) {
4680
- if (verifyError instanceof Error && !verifyError.message.includes("not accessible")) console.warn(`[SATI] Pinata gateway verification skipped for ${cid}: ${verifyError.message}`);
4681
- else throw verifyError;
4710
+ if (verifyError instanceof Error && verifyError.message.includes("not accessible")) throw verifyError;
4682
4711
  }
4683
4712
  return `ipfs://${cid}`;
4684
4713
  } };
@@ -4872,8 +4901,8 @@ function buildRegistrationFile(params) {
4872
4901
  * Fetch and parse a registration file from URI.
4873
4902
  *
4874
4903
  * - Returns null on network errors or invalid URIs
4875
- * - Validates structure, logs warnings for non-conforming files
4876
- * - Never throws
4904
+ * - Validates structure, returns raw data for non-conforming files
4905
+ * - Never throws, never logs to console
4877
4906
  */
4878
4907
  async function fetchRegistrationFile(uri) {
4879
4908
  if (!uri) return null;
@@ -4883,19 +4912,12 @@ async function fetchRegistrationFile(uri) {
4883
4912
  else if (!uri.startsWith("http://") && !uri.startsWith("https://")) return null;
4884
4913
  try {
4885
4914
  const response = await fetch(url);
4886
- if (!response.ok) {
4887
- console.warn(`[SATI] Failed to fetch metadata from ${url}: ${response.status}`);
4888
- return null;
4889
- }
4915
+ if (!response.ok) return null;
4890
4916
  const data = await response.json();
4891
4917
  const result = RegistrationFileSchema.safeParse(data);
4892
- if (!result.success) {
4893
- console.warn(`[SATI] Registration file validation issues:`, result.error.issues);
4894
- return data;
4895
- }
4918
+ if (!result.success) return data;
4896
4919
  return result.data;
4897
- } catch (error) {
4898
- console.warn(`[SATI] Failed to fetch metadata from ${uri}:`, error);
4920
+ } catch {
4899
4921
  return null;
4900
4922
  }
4901
4923
  }
@@ -4961,6 +4983,211 @@ function getSatiAgentIds(file) {
4961
4983
  return file.registrations?.filter((r) => typeof r.agentRegistry === "string" && r.agentRegistry.startsWith(SATI_CHAIN_ID)).map((r) => String(r.agentId)) ?? [];
4962
4984
  }
4963
4985
 
4986
+ //#endregion
4987
+ //#region src/cache.ts
4988
+ var FeedbackCache = class {
4989
+ cache = /* @__PURE__ */ new Map();
4990
+ constructor(ttlMs = 3e4) {
4991
+ this.ttlMs = ttlMs;
4992
+ }
4993
+ /** Get cached data or null if expired/missing. */
4994
+ get(key) {
4995
+ const entry = this.cache.get(key);
4996
+ if (!entry || Date.now() > entry.expires) {
4997
+ if (entry) this.cache.delete(key);
4998
+ return null;
4999
+ }
5000
+ return entry.data;
5001
+ }
5002
+ /** Store data with TTL. */
5003
+ set(key, data) {
5004
+ this.cache.set(key, {
5005
+ data,
5006
+ expires: Date.now() + this.ttlMs
5007
+ });
5008
+ }
5009
+ /** Invalidate a specific key, or all entries if no key given. */
5010
+ invalidate(key) {
5011
+ if (key) this.cache.delete(key);
5012
+ else this.cache.clear();
5013
+ }
5014
+ /** Build a cache key from schema and optional agent mint. */
5015
+ static cacheKey(sasSchema, agentMint) {
5016
+ return `${sasSchema}:${agentMint ?? "*"}`;
5017
+ }
5018
+ };
5019
+
5020
+ //#endregion
5021
+ //#region src/agent-builder.ts
5022
+ var SatiAgentBuilder = class {
5023
+ _params;
5024
+ _identity;
5025
+ _sati;
5026
+ constructor(sati, name, description, image) {
5027
+ this._sati = sati;
5028
+ this._params = {
5029
+ name,
5030
+ description,
5031
+ image,
5032
+ endpoints: [],
5033
+ active: true
5034
+ };
5035
+ }
5036
+ /** Current registration file parameters. */
5037
+ get params() {
5038
+ return this._params;
5039
+ }
5040
+ /** On-chain identity (available after register/load). */
5041
+ get identity() {
5042
+ return this._identity;
5043
+ }
5044
+ /** Set a generic endpoint. */
5045
+ setEndpoint(endpoint) {
5046
+ if (!this._params.endpoints) this._params.endpoints = [];
5047
+ this._params.endpoints = this._params.endpoints.filter((ep) => ep.name !== endpoint.name);
5048
+ this._params.endpoints.push(endpoint);
5049
+ return this;
5050
+ }
5051
+ /**
5052
+ * Set MCP endpoint.
5053
+ *
5054
+ * Unlike agent0-sdk's `SatiAgent.setMCP()`, this does NOT auto-fetch capabilities.
5055
+ * Pass tools/prompts/resources explicitly via the `meta` parameter.
5056
+ */
5057
+ setMCP(url, version, meta) {
5058
+ return this.setEndpoint({
5059
+ name: "MCP",
5060
+ endpoint: url,
5061
+ ...version && { version },
5062
+ ...meta?.tools?.length && { mcpTools: meta.tools },
5063
+ ...meta?.prompts?.length && { mcpPrompts: meta.prompts },
5064
+ ...meta?.resources?.length && { mcpResources: meta.resources }
5065
+ });
5066
+ }
5067
+ /** Set A2A endpoint. */
5068
+ setA2A(url, version, meta) {
5069
+ return this.setEndpoint({
5070
+ name: "A2A",
5071
+ endpoint: url,
5072
+ ...version && { version },
5073
+ ...meta?.skills?.length && { a2aSkills: meta.skills }
5074
+ });
5075
+ }
5076
+ /** Set wallet endpoint. */
5077
+ setWallet(address$5) {
5078
+ return this.setEndpoint({
5079
+ name: "agentWallet",
5080
+ endpoint: address$5
5081
+ });
5082
+ }
5083
+ /** Remove an endpoint by name. */
5084
+ removeEndpoint(name) {
5085
+ if (this._params.endpoints) this._params.endpoints = this._params.endpoints.filter((ep) => ep.name !== name);
5086
+ return this;
5087
+ }
5088
+ /** Set agent active status. */
5089
+ setActive(active) {
5090
+ this._params.active = active;
5091
+ return this;
5092
+ }
5093
+ /** Set x402 payment support. */
5094
+ setX402Support(x402) {
5095
+ this._params.x402support = x402;
5096
+ return this;
5097
+ }
5098
+ /** Set supported trust mechanisms. */
5099
+ setSupportedTrust(trusts) {
5100
+ this._params.supportedTrust = trusts;
5101
+ return this;
5102
+ }
5103
+ /** Set external URL. */
5104
+ setExternalUrl(url) {
5105
+ this._params.externalUrl = url;
5106
+ return this;
5107
+ }
5108
+ /** Update basic info. */
5109
+ updateInfo(opts) {
5110
+ if (opts.name !== void 0) this._params.name = opts.name;
5111
+ if (opts.description !== void 0) this._params.description = opts.description;
5112
+ if (opts.image !== void 0) this._params.image = opts.image;
5113
+ return this;
5114
+ }
5115
+ /**
5116
+ * Upload registration file and register agent on-chain.
5117
+ *
5118
+ * @returns Registration result with mint address, member number, and signature
5119
+ */
5120
+ async register(opts) {
5121
+ const uri = await this._sati.uploadRegistrationFile(this._params, opts.uploader);
5122
+ return this._registerWithUri(uri, opts);
5123
+ }
5124
+ /**
5125
+ * Register agent on-chain with a pre-existing URI.
5126
+ *
5127
+ * Use when you already have a hosted registration file.
5128
+ */
5129
+ async registerWithUri(opts) {
5130
+ return this._registerWithUri(opts.uri, opts);
5131
+ }
5132
+ /**
5133
+ * Re-upload registration file and update the on-chain URI.
5134
+ *
5135
+ * Use after modifying the builder via fluent setters.
5136
+ */
5137
+ async update(opts) {
5138
+ if (!this._identity) throw new Error("Agent not registered on-chain. Call register() first.");
5139
+ const uri = await this._sati.uploadRegistrationFile(this._params, opts.uploader);
5140
+ return this._updateUri(uri, opts);
5141
+ }
5142
+ /**
5143
+ * Update the on-chain URI to point to a new registration file.
5144
+ */
5145
+ async updateUri(opts) {
5146
+ if (!this._identity) throw new Error("Agent not registered on-chain. Call register() first.");
5147
+ return this._updateUri(opts.uri, opts);
5148
+ }
5149
+ /**
5150
+ * Load existing on-chain identity into this builder.
5151
+ * Useful for wrapping an already-registered agent.
5152
+ */
5153
+ setIdentity(identity) {
5154
+ this._identity = identity;
5155
+ return this;
5156
+ }
5157
+ async _registerWithUri(uri, opts) {
5158
+ if (this._identity) throw new Error("Agent is already registered on-chain. Use update() instead.");
5159
+ const result = await this._sati.registerAgent({
5160
+ payer: opts.payer,
5161
+ name: this._params.name,
5162
+ uri,
5163
+ nonTransferable: opts.nonTransferable,
5164
+ owner: opts.owner
5165
+ });
5166
+ this._identity = {
5167
+ mint: result.mint,
5168
+ owner: opts.owner ?? opts.payer.address,
5169
+ name: this._params.name,
5170
+ uri,
5171
+ memberNumber: result.memberNumber,
5172
+ additionalMetadata: {},
5173
+ nonTransferable: opts.nonTransferable ?? false
5174
+ };
5175
+ return result;
5176
+ }
5177
+ async _updateUri(uri, opts) {
5178
+ const identity = this._identity;
5179
+ if (!identity) throw new Error("Agent not registered on-chain. Call register() first.");
5180
+ const result = await this._sati.updateAgentMetadata({
5181
+ payer: opts.payer,
5182
+ owner: opts.owner,
5183
+ mint: identity.mint,
5184
+ updates: { uri }
5185
+ });
5186
+ identity.uri = uri;
5187
+ return { signature: result.signature };
5188
+ }
5189
+ };
5190
+
4964
5191
  //#endregion
4965
5192
  //#region src/client.ts
4966
5193
  /**
@@ -4987,6 +5214,11 @@ const WS_URLS = {
4987
5214
  devnet: "wss://api.devnet.solana.com",
4988
5215
  localnet: "ws://127.0.0.1:8900"
4989
5216
  };
5217
+ const PHOTON_URLS = {
5218
+ mainnet: "https://sati.cascade.fyi/api/photon/mainnet",
5219
+ devnet: "https://sati.cascade.fyi/api/photon/devnet",
5220
+ localnet: "http://127.0.0.1:8899"
5221
+ };
4990
5222
  /**
4991
5223
  * Convert an Address to Base58EncodedBytes for RPC memcmp filters.
4992
5224
  */
@@ -5047,6 +5279,9 @@ var Sati = class {
5047
5279
  rpcSubscriptions;
5048
5280
  sendAndConfirm;
5049
5281
  lightClient;
5282
+ _deployedConfig;
5283
+ _feedbackCache = new FeedbackCache();
5284
+ _onWarning;
5050
5285
  /** Network configuration */
5051
5286
  network;
5052
5287
  constructor(options) {
@@ -5059,7 +5294,9 @@ var Sati = class {
5059
5294
  rpc: this.rpc,
5060
5295
  rpcSubscriptions: this.rpcSubscriptions
5061
5296
  });
5062
- this.lightClient = createSATILightClient(options.photonRpcUrl ?? rpcUrl);
5297
+ this.lightClient = createSATILightClient(options.photonRpcUrl ?? PHOTON_URLS[options.network]);
5298
+ this._deployedConfig = loadDeployedConfig(options.network);
5299
+ this._onWarning = options.onWarning;
5063
5300
  }
5064
5301
  /** @internal */
5065
5302
  getRpc() {
@@ -6179,6 +6416,432 @@ var Sati = class {
6179
6416
  await this.sendAndConfirm(signedTx, { commitment: "confirmed" });
6180
6417
  return signature;
6181
6418
  }
6419
+ /** Deployed SAS configuration for this network (null for localnet unless deployed). */
6420
+ get deployedConfig() {
6421
+ return this._deployedConfig;
6422
+ }
6423
+ /** FeedbackPublic schema address (CounterpartySigned mode). */
6424
+ get feedbackPublicSchema() {
6425
+ return this._deployedConfig?.schemas.feedbackPublic;
6426
+ }
6427
+ /** Feedback schema address (DualSignature mode). */
6428
+ get feedbackSchema() {
6429
+ return this._deployedConfig?.schemas.feedback;
6430
+ }
6431
+ /** Validation schema address. */
6432
+ get validationSchema() {
6433
+ return this._deployedConfig?.schemas.validation;
6434
+ }
6435
+ /** Address Lookup Table for transaction compression. */
6436
+ get lookupTable() {
6437
+ return this._deployedConfig?.lookupTable;
6438
+ }
6439
+ /**
6440
+ * Give feedback to an agent (simplified).
6441
+ *
6442
+ * Uses FeedbackPublicV1 schema (CounterpartySigned mode).
6443
+ * Automatically handles SIWS message construction and signing.
6444
+ *
6445
+ * @example
6446
+ * ```typescript
6447
+ * const result = await sati.giveFeedback({
6448
+ * payer: myKeypair,
6449
+ * agentMint: address("Agent..."),
6450
+ * score: 85,
6451
+ * tags: ["quality", "speed"],
6452
+ * message: "Great response time",
6453
+ * });
6454
+ * ```
6455
+ */
6456
+ async giveFeedback(params) {
6457
+ const schema = this._requireFeedbackPublicSchema();
6458
+ const payer = params.payer;
6459
+ if (params.score !== void 0 && (!Number.isFinite(params.score) || params.score < 0 || params.score > 100)) throw new Error(`Feedback score must be a finite number between 0 and 100, got: ${params.score}`);
6460
+ const contentObj = {};
6461
+ if (params.score !== void 0) contentObj.score = params.score;
6462
+ if (params.tags?.length) contentObj.tags = params.tags;
6463
+ if (params.endpoint) contentObj.endpoint = params.endpoint;
6464
+ if (params.message) contentObj.m = params.message;
6465
+ const content = Object.keys(contentObj).length > 0 ? new TextEncoder().encode(JSON.stringify(contentObj)) : new Uint8Array(0);
6466
+ const contentType = content.length > 0 ? ContentType.JSON : ContentType.None;
6467
+ const taskRef = params.taskRef ?? globalThis.crypto.getRandomValues(new Uint8Array(32));
6468
+ const outcome = params.outcome ?? Outcome.Neutral;
6469
+ const { messageBytes } = buildCounterpartyMessage({
6470
+ schemaName: "FeedbackPublicV1",
6471
+ data: serializeFeedback({
6472
+ taskRef,
6473
+ agentMint: params.agentMint,
6474
+ counterparty: payer.address,
6475
+ dataHash: zeroDataHash$1(),
6476
+ outcome,
6477
+ contentType,
6478
+ content
6479
+ })
6480
+ });
6481
+ const sig = await (0, __solana_kit.signBytes)(payer.keyPair.privateKey, messageBytes);
6482
+ const result = await this.createFeedback({
6483
+ payer,
6484
+ sasSchema: schema,
6485
+ taskRef,
6486
+ agentMint: params.agentMint,
6487
+ counterparty: payer.address,
6488
+ dataHash: zeroDataHash$1(),
6489
+ outcome,
6490
+ contentType,
6491
+ content,
6492
+ agentSignature: {
6493
+ pubkey: payer.address,
6494
+ signature: new Uint8Array(sig)
6495
+ },
6496
+ counterpartyMessage: messageBytes,
6497
+ lookupTableAddress: this._deployedConfig?.lookupTable
6498
+ });
6499
+ this._feedbackCache.invalidate();
6500
+ return {
6501
+ signature: result.signature,
6502
+ attestationAddress: result.address
6503
+ };
6504
+ }
6505
+ /**
6506
+ * Prepare feedback for browser wallet signing.
6507
+ *
6508
+ * Returns SIWS message bytes that the counterparty must sign externally.
6509
+ * Pass the result + signature to `submitPreparedFeedback()`.
6510
+ */
6511
+ async prepareFeedback(params) {
6512
+ const schema = this._requireFeedbackPublicSchema();
6513
+ if (params.score !== void 0 && (!Number.isFinite(params.score) || params.score < 0 || params.score > 100)) throw new Error(`Feedback score must be a finite number between 0 and 100, got: ${params.score}`);
6514
+ const contentObj = {};
6515
+ if (params.score !== void 0) contentObj.score = params.score;
6516
+ if (params.tags?.length) contentObj.tags = params.tags;
6517
+ if (params.endpoint) contentObj.endpoint = params.endpoint;
6518
+ if (params.message) contentObj.m = params.message;
6519
+ const content = Object.keys(contentObj).length > 0 ? new TextEncoder().encode(JSON.stringify(contentObj)) : new Uint8Array(0);
6520
+ const contentType = content.length > 0 ? ContentType.JSON : ContentType.None;
6521
+ const taskRef = params.taskRef ?? globalThis.crypto.getRandomValues(new Uint8Array(32));
6522
+ const outcome = params.outcome ?? Outcome.Neutral;
6523
+ const { messageBytes } = buildCounterpartyMessage({
6524
+ schemaName: "FeedbackPublicV1",
6525
+ data: serializeFeedback({
6526
+ taskRef,
6527
+ agentMint: params.agentMint,
6528
+ counterparty: params.counterparty,
6529
+ dataHash: zeroDataHash$1(),
6530
+ outcome,
6531
+ contentType,
6532
+ content
6533
+ })
6534
+ });
6535
+ return {
6536
+ messageBytes,
6537
+ agentMint: params.agentMint,
6538
+ counterparty: params.counterparty,
6539
+ taskRef,
6540
+ dataHash: zeroDataHash$1(),
6541
+ outcome,
6542
+ contentType,
6543
+ content,
6544
+ sasSchema: schema,
6545
+ lookupTable: this._deployedConfig?.lookupTable,
6546
+ meta: {
6547
+ score: params.score,
6548
+ tags: params.tags ? [...params.tags] : void 0,
6549
+ message: params.message,
6550
+ endpoint: params.endpoint
6551
+ }
6552
+ };
6553
+ }
6554
+ /**
6555
+ * Submit prepared feedback with an externally-obtained wallet signature.
6556
+ *
6557
+ * The payer signs the transaction and pays gas. The counterparty's SIWS
6558
+ * signature (from wallet) proves consent.
6559
+ */
6560
+ async submitPreparedFeedback(params) {
6561
+ const { payer, prepared, counterpartySignature } = params;
6562
+ const result = await this.createFeedback({
6563
+ payer,
6564
+ sasSchema: prepared.sasSchema,
6565
+ taskRef: prepared.taskRef,
6566
+ agentMint: prepared.agentMint,
6567
+ counterparty: prepared.counterparty,
6568
+ dataHash: prepared.dataHash,
6569
+ outcome: prepared.outcome,
6570
+ contentType: prepared.contentType,
6571
+ content: prepared.content,
6572
+ agentSignature: {
6573
+ pubkey: prepared.counterparty,
6574
+ signature: new Uint8Array(counterpartySignature)
6575
+ },
6576
+ counterpartyMessage: prepared.messageBytes,
6577
+ lookupTableAddress: prepared.lookupTable
6578
+ });
6579
+ this._feedbackCache.invalidate();
6580
+ return {
6581
+ signature: result.signature,
6582
+ attestationAddress: result.address
6583
+ };
6584
+ }
6585
+ /**
6586
+ * Revoke (close) a feedback attestation by its compressed account address.
6587
+ *
6588
+ * The payer must be the counterparty who originally submitted the feedback.
6589
+ * Closed attestations are permanently deleted.
6590
+ */
6591
+ async revokeFeedback(params) {
6592
+ const schema = this._deployedConfig?.schemas.feedbackPublic ?? this._deployedConfig?.schemas.feedback;
6593
+ if (!schema) throw new Error(`No feedback schema deployed for network "${this.network}"`);
6594
+ const result = await this.closeCompressedAttestation({
6595
+ payer: params.payer,
6596
+ counterparty: params.payer,
6597
+ sasSchema: schema,
6598
+ attestationAddress: params.attestationAddress,
6599
+ lookupTableAddress: this._deployedConfig?.lookupTable
6600
+ });
6601
+ this._feedbackCache.invalidate();
6602
+ return { signature: result.signature };
6603
+ }
6604
+ /**
6605
+ * Search feedback attestations with client-side filtering.
6606
+ *
6607
+ * Note: `createdAt` timestamps are approximate - derived from Solana slot
6608
+ * numbers using ~400ms/slot estimate.
6609
+ *
6610
+ * @example
6611
+ * ```typescript
6612
+ * const feedbacks = await sati.searchFeedback({
6613
+ * agentMint: address("Agent..."),
6614
+ * tags: ["quality"],
6615
+ * minScore: 70,
6616
+ * });
6617
+ * ```
6618
+ */
6619
+ async searchFeedback(options) {
6620
+ const schema = this._deployedConfig?.schemas.feedbackPublic ?? this._deployedConfig?.schemas.feedback;
6621
+ if (!schema) throw new Error(`No feedback schema deployed for network "${this.network}"`);
6622
+ const filter = { sasSchema: schema };
6623
+ if (options?.agentMint) filter.agentMint = options.agentMint;
6624
+ if (options?.counterparty) filter.counterparty = options.counterparty;
6625
+ const cacheKey = FeedbackCache.cacheKey(schema, options?.agentMint);
6626
+ const cached = this._feedbackCache.get(cacheKey);
6627
+ const result = cached ?? await this.listFeedbacks(filter);
6628
+ if (!cached) this._feedbackCache.set(cacheKey, result);
6629
+ const currentSlot = await this.rpc.getSlot({ commitment: "confirmed" }).send();
6630
+ const nowSec = Math.floor(Date.now() / 1e3);
6631
+ const feedbacks = [];
6632
+ for (const item of result.items) {
6633
+ const rawContent = this._parseContentJson(item.data.content, item.data.contentType);
6634
+ const score = rawContent?.score;
6635
+ const tags = rawContent?.tags ?? [];
6636
+ const message = rawContent?.m;
6637
+ const endpoint = rawContent?.endpoint;
6638
+ if (options?.tags?.length) {
6639
+ if (!options.tags.every((t) => tags.includes(t))) continue;
6640
+ }
6641
+ if (options?.minScore !== void 0 && (score === void 0 || score < options.minScore)) continue;
6642
+ if (options?.maxScore !== void 0 && (score === void 0 || score > options.maxScore)) continue;
6643
+ const slotDiff = Number(BigInt(currentSlot) - item.raw.slotCreated);
6644
+ const createdAt = nowSec - Math.floor(slotDiff * .4);
6645
+ const [compressedAddress] = (0, __solana_kit.getAddressDecoder)().read(item.address, 0);
6646
+ feedbacks.push({
6647
+ compressedAddress,
6648
+ agentMint: item.data.agentMint,
6649
+ counterparty: item.data.counterparty,
6650
+ outcome: item.data.outcome,
6651
+ score,
6652
+ tags,
6653
+ message,
6654
+ endpoint,
6655
+ createdAt
6656
+ });
6657
+ }
6658
+ if (options?.includeTxHash) {
6659
+ const photon = this.getLightClient().getRpc();
6660
+ await Promise.all(feedbacks.map(async (fb) => {
6661
+ try {
6662
+ fb.txSignature = (await photon.getCompressionSignaturesForAddress(fb.compressedAddress, { limit: 1 })).items[0]?.signature;
6663
+ } catch (error) {
6664
+ this._warn({
6665
+ code: "SIGNATURE_LOOKUP_FAILED",
6666
+ message: "Failed to fetch tx signature",
6667
+ context: fb.compressedAddress,
6668
+ cause: error
6669
+ });
6670
+ }
6671
+ }));
6672
+ }
6673
+ return feedbacks;
6674
+ }
6675
+ /**
6676
+ * Get reputation summary for an agent.
6677
+ *
6678
+ * Aggregates feedback scores, optionally filtered by tags.
6679
+ *
6680
+ * @example
6681
+ * ```typescript
6682
+ * const summary = await sati.getReputationSummary(address("Agent..."));
6683
+ * console.log(`${summary.count} reviews, avg ${summary.averageScore}`);
6684
+ * ```
6685
+ */
6686
+ async getReputationSummary(agentMint, tags) {
6687
+ const schema = this._deployedConfig?.schemas.feedbackPublic ?? this._deployedConfig?.schemas.feedback;
6688
+ if (!schema) throw new Error(`No feedback schema deployed for network "${this.network}"`);
6689
+ const cacheKey = FeedbackCache.cacheKey(schema, agentMint);
6690
+ const cached = this._feedbackCache.get(cacheKey);
6691
+ const result = cached ?? await this.listFeedbacks({
6692
+ sasSchema: schema,
6693
+ agentMint
6694
+ });
6695
+ if (!cached) this._feedbackCache.set(cacheKey, result);
6696
+ if (result.items.length === 0) return {
6697
+ count: 0,
6698
+ averageScore: 0
6699
+ };
6700
+ let sum = 0;
6701
+ let count = 0;
6702
+ for (const item of result.items) {
6703
+ const rawContent = this._parseContentJson(item.data.content, item.data.contentType);
6704
+ const score = rawContent?.score;
6705
+ const itemTags = rawContent?.tags ?? [];
6706
+ if (tags?.length && !tags.every((t) => itemTags.includes(t))) continue;
6707
+ if (score !== void 0) {
6708
+ sum += score;
6709
+ count++;
6710
+ }
6711
+ }
6712
+ return {
6713
+ count,
6714
+ averageScore: count > 0 ? sum / count : 0
6715
+ };
6716
+ }
6717
+ /**
6718
+ * Search validation attestations for an agent.
6719
+ *
6720
+ * Note: `createdAt` timestamps are approximate.
6721
+ */
6722
+ async searchValidations(agentMint) {
6723
+ const validationSchema = this._deployedConfig?.schemas.validation;
6724
+ if (!validationSchema) throw new Error(`No validation schema deployed for network "${this.network}"`);
6725
+ const result = await this.listValidations({
6726
+ sasSchema: validationSchema,
6727
+ agentMint
6728
+ });
6729
+ const currentSlot = await this.rpc.getSlot({ commitment: "confirmed" }).send();
6730
+ const nowSec = Math.floor(Date.now() / 1e3);
6731
+ return result.items.map((item) => {
6732
+ const slotDiff = Number(BigInt(currentSlot) - item.raw.slotCreated);
6733
+ const createdAt = nowSec - Math.floor(slotDiff * .4);
6734
+ const [compressedAddress] = (0, __solana_kit.getAddressDecoder)().read(item.address, 0);
6735
+ return {
6736
+ compressedAddress,
6737
+ agentMint: item.data.agentMint,
6738
+ counterparty: item.data.counterparty,
6739
+ outcome: item.data.outcome,
6740
+ createdAt
6741
+ };
6742
+ });
6743
+ }
6744
+ /**
6745
+ * Search registered agents with filtering and optional feedback stats.
6746
+ *
6747
+ * @example
6748
+ * ```typescript
6749
+ * const agents = await sati.searchAgents({
6750
+ * endpointTypes: ["MCP"],
6751
+ * active: true,
6752
+ * includeFeedbackStats: true,
6753
+ * });
6754
+ * ```
6755
+ */
6756
+ async searchAgents(options) {
6757
+ const limit = options?.limit ?? 100;
6758
+ const offset = options?.offset;
6759
+ let agents;
6760
+ if (options?.owner) agents = await this.listAgentsByOwner(options.owner);
6761
+ else agents = await this.listAllAgents({
6762
+ limit,
6763
+ offset
6764
+ });
6765
+ if (options?.name) {
6766
+ const lower = options.name.toLowerCase();
6767
+ agents = agents.filter((a) => a.name.toLowerCase().includes(lower));
6768
+ }
6769
+ const regFiles = !!(options?.active !== void 0 || options?.endpointTypes?.length) || options?.includeFeedbackStats ? await Promise.all(agents.map(async (a) => {
6770
+ try {
6771
+ return await fetchRegistrationFile(a.uri);
6772
+ } catch {
6773
+ return null;
6774
+ }
6775
+ })) : agents.map(() => null);
6776
+ const filtered = [];
6777
+ for (let i = 0; i < agents.length; i++) {
6778
+ const identity = agents[i];
6779
+ const regFile = regFiles[i];
6780
+ const endpoints = regFile?.endpoints ?? [];
6781
+ if (options?.active !== void 0 && (regFile?.active ?? true) !== options.active) continue;
6782
+ if (options?.endpointTypes?.length) {
6783
+ if (!options.endpointTypes.every((type) => endpoints.some((e) => e.name.toUpperCase() === type.toUpperCase()))) continue;
6784
+ }
6785
+ filtered.push({
6786
+ identity,
6787
+ regFile
6788
+ });
6789
+ }
6790
+ let statsMap = null;
6791
+ if (options?.includeFeedbackStats && this._deployedConfig && filtered.length > 0) {
6792
+ statsMap = /* @__PURE__ */ new Map();
6793
+ await Promise.all(filtered.map(async ({ identity }) => {
6794
+ try {
6795
+ const summary = await this.getReputationSummary(identity.mint);
6796
+ statsMap?.set(identity.mint, summary);
6797
+ } catch (error) {
6798
+ this._warn({
6799
+ code: "RPC_ERROR",
6800
+ message: "Failed to fetch feedback stats",
6801
+ context: identity.mint,
6802
+ cause: error
6803
+ });
6804
+ }
6805
+ }));
6806
+ }
6807
+ return filtered.map(({ identity, regFile }) => ({
6808
+ identity,
6809
+ registrationFile: regFile,
6810
+ ...statsMap && { feedbackStats: statsMap.get(identity.mint) }
6811
+ }));
6812
+ }
6813
+ /**
6814
+ * Create a fluent builder for agent registration.
6815
+ *
6816
+ * @example
6817
+ * ```typescript
6818
+ * const builder = sati.createAgentBuilder("MyAgent", "AI assistant", "https://example.com/avatar.png");
6819
+ * builder.setMCP("https://mcp.example.com").setActive(true);
6820
+ * const result = await builder.register({ payer, uploader: createSatiUploader() });
6821
+ * ```
6822
+ */
6823
+ createAgentBuilder(name, description, image) {
6824
+ return new SatiAgentBuilder(this, name, description, image);
6825
+ }
6826
+ /** Safely parse JSON content from an attestation. */
6827
+ _parseContentJson(content, contentType) {
6828
+ if (contentType !== ContentType.JSON || content.length === 0) return null;
6829
+ try {
6830
+ return JSON.parse(new TextDecoder().decode(content));
6831
+ } catch {
6832
+ return null;
6833
+ }
6834
+ }
6835
+ /** Fire a non-fatal warning. */
6836
+ _warn(warning) {
6837
+ this._onWarning?.(warning);
6838
+ }
6839
+ /** Get the FeedbackPublic schema address or throw. */
6840
+ _requireFeedbackPublicSchema() {
6841
+ const schema = this._deployedConfig?.schemas.feedbackPublic;
6842
+ if (!schema) throw new Error(`No FeedbackPublic schema deployed for network "${this.network}". Pass sasSchema explicitly via the low-level createFeedback() method.`);
6843
+ return schema;
6844
+ }
6182
6845
  };
6183
6846
 
6184
6847
  //#endregion
@@ -6363,6 +7026,7 @@ exports.DuplicateAttestationError = DuplicateAttestationError;
6363
7026
  exports.ED25519_PROGRAM_ADDRESS = ED25519_PROGRAM_ADDRESS;
6364
7027
  exports.ENCRYPTION_VERSION = ENCRYPTION_VERSION;
6365
7028
  exports.FEEDBACK_OFFSETS = FEEDBACK_OFFSETS;
7029
+ exports.FeedbackCache = FeedbackCache;
6366
7030
  exports.INITIALIZE_DISCRIMINATOR = INITIALIZE_DISCRIMINATOR;
6367
7031
  exports.LIGHT_ERROR_CODES = LIGHT_ERROR_CODES;
6368
7032
  exports.LINK_EVM_ADDRESS_DISCRIMINATOR = LINK_EVM_ADDRESS_DISCRIMINATOR;
@@ -6446,6 +7110,7 @@ exports.SCHEMA_SEED = SCHEMA_SEED;
6446
7110
  exports.SOLANA_CHAIN_REFS = SOLANA_CHAIN_REFS;
6447
7111
  exports.Sati = Sati;
6448
7112
  exports.SatiAccount = SatiAccount;
7113
+ exports.SatiAgentBuilder = SatiAgentBuilder;
6449
7114
  exports.SatiError = SatiError;
6450
7115
  exports.SatiInstruction = SatiInstruction;
6451
7116
  exports.SchemaNotFoundError = SchemaNotFoundError;
@@ -6480,6 +7145,7 @@ exports.createEd25519Instruction = createEd25519Instruction;
6480
7145
  exports.createJsonContent = createJsonContent;
6481
7146
  exports.createPinataUploader = createPinataUploader;
6482
7147
  exports.createSATILightClient = createSATILightClient;
7148
+ exports.createSatiUploader = createSatiUploader;
6483
7149
  exports.decodeAgentIndex = decodeAgentIndex;
6484
7150
  exports.decodeRegistryConfig = decodeRegistryConfig;
6485
7151
  exports.decodeSchemaConfig = decodeSchemaConfig;