@oracle-agent/oracle 0.4.0 → 0.4.1

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.
@@ -263,7 +263,7 @@ const tools = [
263
263
  {
264
264
  name: "nft_prepare_list",
265
265
  description:
266
- "Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
266
+ "Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Magic Eden Solana also requires confirmation exactly: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
267
267
  inputSchema: {
268
268
  type: "object",
269
269
  required: ["marketplace", "userConfirmed"],
@@ -284,7 +284,8 @@ const tools = [
284
284
  tokenATA: { type: "string" },
285
285
  auctionHouse: { type: "string" },
286
286
  priceSol: { type: "string" },
287
- expiry: { description: "Future ISO 8601 timestamp or Unix seconds." },
287
+ expiry: { description: "Future ISO 8601 timestamp or Unix seconds. Magic Eden Solana listings require finite future expiry; no-expiry is refused." },
288
+ confirmation: { type: "string", description: "Required for Magic Eden Solana listings. Exact phrase: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>." },
288
289
  inscriptionId: { type: "string" },
289
290
  runesOutput: { type: "string" },
290
291
  ordAddress: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oracle-agent/oracle",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default — the public package never takes your key. Built for Hermes; no model key required.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/data/http.mjs CHANGED
@@ -37,6 +37,35 @@ const IDENTITY_HEADERS = [
37
37
  "proxy-authorization",
38
38
  ];
39
39
 
40
+ const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
41
+ const MAX_REDIRECTS = 3;
42
+
43
+ function credentialHeaderName(name) {
44
+ const h = String(name || "").toLowerCase();
45
+ return IDENTITY_HEADERS.includes(h) || /(^|-)(api[-_]?key|key|token|secret|auth|authorization|session|cookie|signature|access)(-|$)/.test(h);
46
+ }
47
+
48
+ function stripCredentialHeaders(headers) {
49
+ const safe = {};
50
+ for (const [name, value] of Object.entries(headers || {})) {
51
+ if (!credentialHeaderName(name)) safe[name] = value;
52
+ }
53
+ return safe;
54
+ }
55
+
56
+ function redirectedUrl(location, currentUrl) {
57
+ if (!location) throw new Error(`HTTP redirect from ${currentUrl} missing Location`);
58
+ const next = new URL(String(location), String(currentUrl));
59
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
60
+ throw new Error(`HTTP redirect from ${currentUrl} used unsupported protocol ${next.protocol}`);
61
+ }
62
+ return next.toString();
63
+ }
64
+
65
+ function sameOrigin(a, b) {
66
+ return new URL(String(a)).origin === new URL(String(b)).origin;
67
+ }
68
+
40
69
  const inflight = new Map();
41
70
 
42
71
  /**
@@ -77,24 +106,48 @@ async function once(url, { fetchImpl, method, headers, body, timeoutMs }) {
77
106
  const ac = new AbortController();
78
107
  const t = setTimeout(() => ac.abort(), timeoutMs);
79
108
  try {
80
- const res = await fetchImpl(url, { method, headers, body, signal: ac.signal });
81
- const text = await res.text();
82
- let json = null;
83
- if (text) {
84
- try {
85
- json = JSON.parse(text);
86
- } catch {
87
- json = null;
109
+ let currentUrl = String(url);
110
+ let currentHeaders = { ...(headers || {}) };
111
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
112
+ const res = await fetchImpl(currentUrl, {
113
+ method,
114
+ headers: currentHeaders,
115
+ body,
116
+ signal: ac.signal,
117
+ redirect: "manual",
118
+ });
119
+ const text = await res.text();
120
+ let json = null;
121
+ if (text) {
122
+ try {
123
+ json = JSON.parse(text);
124
+ } catch {
125
+ json = null;
126
+ }
88
127
  }
128
+ if (REDIRECT_STATUS.has(res.status)) {
129
+ if (method !== "GET" && method !== "HEAD") {
130
+ const err = new Error(`HTTP ${res.status} ${method} ${currentUrl} redirected non-idempotent request`);
131
+ err.status = res.status;
132
+ err.body = json ?? text.slice(0, 300);
133
+ throw err;
134
+ }
135
+ if (redirects === MAX_REDIRECTS) throw new Error(`HTTP redirect loop for ${url}`);
136
+ const nextUrl = redirectedUrl(res.headers?.get?.("location"), currentUrl);
137
+ if (!sameOrigin(nextUrl, currentUrl)) currentHeaders = stripCredentialHeaders(currentHeaders);
138
+ currentUrl = nextUrl;
139
+ continue;
140
+ }
141
+ if (!res.ok) {
142
+ const err = new Error(`HTTP ${res.status} ${method} ${currentUrl}`);
143
+ err.status = res.status;
144
+ err.retryAfter = res.headers?.get?.("retry-after") ?? null;
145
+ err.body = json ?? text.slice(0, 300);
146
+ throw err;
147
+ }
148
+ return json ?? text;
89
149
  }
90
- if (!res.ok) {
91
- const err = new Error(`HTTP ${res.status} ${method} ${url}`);
92
- err.status = res.status;
93
- err.retryAfter = res.headers?.get?.("retry-after") ?? null;
94
- err.body = json ?? text.slice(0, 300);
95
- throw err;
96
- }
97
- return json ?? text;
150
+ throw new Error(`HTTP redirect loop for ${url}`);
98
151
  } finally {
99
152
  clearTimeout(t);
100
153
  }
@@ -90,17 +90,33 @@ function lamports(sol) {
90
90
  }
91
91
 
92
92
  function listingExpiry(value) {
93
- if (value == null || value === "") return null;
93
+ if (value == null || value === "") throw new Error("magiceden: listing expiry is required and must be in the future");
94
94
  const expiry = Number(value);
95
- if (!Number.isSafeInteger(expiry) || expiry < 0) {
96
- throw new Error("magiceden: expiry must be a whole Unix timestamp in seconds or 0");
95
+ if (!Number.isSafeInteger(expiry) || expiry <= 0) {
96
+ throw new Error("magiceden: expiry must be a future Unix timestamp in seconds");
97
97
  }
98
- if (expiry !== 0 && expiry <= Math.floor(Date.now() / 1000)) {
99
- throw new Error("magiceden: expiry must be in the future or 0 for no expiry");
98
+ if (expiry <= Math.floor(Date.now() / 1000)) {
99
+ throw new Error("magiceden: expiry must be in the future");
100
100
  }
101
101
  return expiry;
102
102
  }
103
103
 
104
+ function listConfirmation({ tokenMint, priceSol, expiry }) {
105
+ return `list ${tokenMint} on magiceden-sol for ${priceSol} SOL until ${expiry}`;
106
+ }
107
+
108
+ function requireListingConfirmation(args, fields) {
109
+ if (args.userConfirmed !== true) {
110
+ throw new Error("magiceden: listing requires userConfirmed=true after reviewing tokenMint, priceSol, and expiry");
111
+ }
112
+ const expected = listConfirmation(fields);
113
+ const actual = String(args.confirmation || args.confirmationPhrase || "").trim();
114
+ if (actual !== expected) {
115
+ throw new Error(`magiceden: confirmation must equal ${JSON.stringify(expected)}`);
116
+ }
117
+ return expected;
118
+ }
119
+
104
120
  export async function magicEdenSolHealth(opts = {}) {
105
121
  try {
106
122
  const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
@@ -269,6 +285,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
269
285
  const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
270
286
  const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
271
287
  const expiry = listingExpiry(args.expiry);
288
+ const confirmation = requireListingConfirmation(args, { tokenMint, priceSol, expiry });
272
289
  const url = new URL(`${base(opts)}/instructions/sell`);
273
290
  url.searchParams.set("seller", seller);
274
291
  url.searchParams.set("auctionHouseAddress", auctionHouse);
@@ -301,6 +318,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
301
318
  tokenMint,
302
319
  priceSol,
303
320
  expiry,
321
+ confirmation,
304
322
  transaction,
305
323
  transactionEncoding: "base64",
306
324
  raw,