dexe-mcp 0.5.7 → 0.5.8
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/CHANGELOG.md +39 -0
- package/README.md +270 -200
- package/SECURITY.md +46 -0
- package/dist/lib/ipfs.d.ts +7 -0
- package/dist/lib/ipfs.d.ts.map +1 -1
- package/dist/lib/ipfs.js +32 -1
- package/dist/lib/ipfs.js.map +1 -1
- package/dist/tools/ipfs.d.ts.map +1 -1
- package/dist/tools/ipfs.js +437 -16
- package/dist/tools/ipfs.js.map +1 -1
- package/package.json +10 -2
package/dist/lib/ipfs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipfs.d.ts","sourceRoot":"","sources":["../../src/lib/ipfs.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAO,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,gEAAgE;IAChE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"ipfs.d.ts","sourceRoot":"","sources":["../../src/lib/ipfs.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAO,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,gEAAgE;IAChE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,eAAe,CAAC,CAkD1B;AAID,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoB/C;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,iEAAiE;AACjE,wBAAsB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAIhE;AAED,iEAAiE;AACjE,wBAAsB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAGpE;AAyBD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAIxC,4DAA4D;IACtD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrB,OAAO,CACX,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAC3D,OAAO,CAAC,eAAe,CAAC;IA0BrB,OAAO,CACX,KAAK,EAAE,UAAU,EACjB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAC7F,OAAO,CAAC,eAAe,CAAC;CAiC5B"}
|
package/dist/lib/ipfs.js
CHANGED
|
@@ -19,14 +19,24 @@ export async function fetchIpfs(cid, cfg) {
|
|
|
19
19
|
const timeout = cfg.perRequestTimeoutMs ?? 4000;
|
|
20
20
|
const errors = [];
|
|
21
21
|
let attempts = 0;
|
|
22
|
+
const pinataGatewayToken = process.env.DEXE_PINATA_GATEWAY_TOKEN?.trim();
|
|
22
23
|
for (const gw of cfg.gateways) {
|
|
23
24
|
attempts++;
|
|
24
25
|
const base = gw.replace(/\/+$/, "").replace(/\/ipfs$/, "");
|
|
25
26
|
const url = `${base}/ipfs/${cidStr}`;
|
|
26
27
|
const controller = new AbortController();
|
|
27
28
|
const t = setTimeout(() => controller.abort(), timeout);
|
|
29
|
+
// Pinata "dedicated gateways" (`*.mypinata.cloud`) in Restricted mode
|
|
30
|
+
// reject anonymous GETs with HTTP 403. They authenticate via a separate
|
|
31
|
+
// Gateway Key (NOT the API JWT used for pinning); pass it as either
|
|
32
|
+
// `?pinataGatewayToken=…` or the `x-pinata-gateway-token` header. We use
|
|
33
|
+
// the header form. Public gateways receive no auth header.
|
|
34
|
+
const headers = {};
|
|
35
|
+
if (pinataGatewayToken && /\.mypinata\.cloud(\/|$)/i.test(base)) {
|
|
36
|
+
headers["x-pinata-gateway-token"] = pinataGatewayToken;
|
|
37
|
+
}
|
|
28
38
|
try {
|
|
29
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
39
|
+
const res = await fetch(url, { signal: controller.signal, headers });
|
|
30
40
|
if (!res.ok) {
|
|
31
41
|
errors.push(`${gw} → HTTP ${res.status}`);
|
|
32
42
|
continue;
|
|
@@ -77,6 +87,18 @@ export function parseCid(input) {
|
|
|
77
87
|
export function stripIpfsPrefix(s) {
|
|
78
88
|
return s.replace(/^ipfs:\/\//, "").replace(/^\/?ipfs\//, "");
|
|
79
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Convert a CID string to its v1 base32 form (idempotent for v1 inputs).
|
|
92
|
+
* Frontend uses subdomain gateway (`<cid>.ipfs.4everland.io`), which only
|
|
93
|
+
* resolves CID v1 base32. Passing a v0 (Qm...) here produces a dead link.
|
|
94
|
+
*/
|
|
95
|
+
export function toCidV1(input) {
|
|
96
|
+
const s = stripIpfsPrefix(input);
|
|
97
|
+
const cid = CID.parse(s);
|
|
98
|
+
if (cid.version === 1)
|
|
99
|
+
return cid.toString(base32);
|
|
100
|
+
return cid.toV1().toString(base32);
|
|
101
|
+
}
|
|
80
102
|
/** Compute the CIDv1 for arbitrary JSON locally — no network. */
|
|
81
103
|
export async function cidForJson(value) {
|
|
82
104
|
const bytes = json.encode(value);
|
|
@@ -149,10 +171,19 @@ export class PinataClient {
|
|
|
149
171
|
const blob = new Blob([bytes], {
|
|
150
172
|
type: opts?.contentType ?? "application/octet-stream",
|
|
151
173
|
});
|
|
174
|
+
// Default: wrap-with-directory so the returned CID is a directory whose
|
|
175
|
+
// single child is `fileName`. That's what lets subdomain gateways serve
|
|
176
|
+
// `<cid>.ipfs.<host>/<fileName>` — without the wrapper, the CID is a raw
|
|
177
|
+
// file and any path suffix returns 404, breaking every consumer that
|
|
178
|
+
// builds URLs from `cid + fileName` (DeXe frontend + ipfs-cache.dexe.io).
|
|
179
|
+
const wrap = opts?.wrapWithDirectory ?? true;
|
|
152
180
|
form.append("file", blob, opts?.fileName ?? "file");
|
|
153
181
|
if (opts?.name) {
|
|
154
182
|
form.append("pinataMetadata", JSON.stringify({ name: opts.name }));
|
|
155
183
|
}
|
|
184
|
+
if (wrap) {
|
|
185
|
+
form.append("pinataOptions", JSON.stringify({ wrapWithDirectory: true }));
|
|
186
|
+
}
|
|
156
187
|
const res = await fetch(PINATA_PIN_FILE_URL, {
|
|
157
188
|
method: "POST",
|
|
158
189
|
headers: { Authorization: `Bearer ${this.jwt}` },
|
package/dist/lib/ipfs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipfs.js","sourceRoot":"","sources":["../../src/lib/ipfs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,0BAA0B,CAAC;AACjD,OAAO,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAsB,EAAE,CAAC;AAmBzD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,GAAoB;IAEpB,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,GAAG,CAAC,mBAAmB,IAAI,IAAI,CAAC;IAChD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,QAAQ,EAAE,CAAC;QACX,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,GAAG,GAAG,GAAG,IAAI,SAAS,MAAM,EAAE,CAAC;QACrC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"ipfs.js","sourceRoot":"","sources":["../../src/lib/ipfs.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/ipfs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipfs.d.ts","sourceRoot":"","sources":["../../src/tools/ipfs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,WAAW,
|
|
1
|
+
{"version":3,"file":"ipfs.d.ts","sourceRoot":"","sources":["../../src/tools/ipfs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,WAAW,EAMZ,MAAM,gBAAgB,CAAC;AAGxB,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,CAY3E;AAmhBD,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/tools/ipfs.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { cidForBytes, cidForJson, fetchIpfs, parseCid, PinataClient, } from "../lib/ipfs.js";
|
|
2
|
+
import { cidForBytes, cidForJson, fetchIpfs, parseCid, PinataClient, toCidV1, } from "../lib/ipfs.js";
|
|
3
3
|
import { markdownToSlate } from "../lib/markdownToSlate.js";
|
|
4
4
|
export function registerIpfsTools(server, ctx) {
|
|
5
5
|
const gateways = resolveGateways(ctx);
|
|
6
6
|
registerUploadProposalMetadata(server, ctx);
|
|
7
7
|
registerUploadDaoMetadata(server, ctx);
|
|
8
8
|
registerUploadFile(server, ctx);
|
|
9
|
+
registerUploadAvatar(server, ctx);
|
|
10
|
+
registerGenerateAvatar(server, ctx);
|
|
11
|
+
registerUpdateDaoMetadata(server, ctx, gateways);
|
|
9
12
|
registerFetch(server, gateways);
|
|
10
13
|
registerCidInfo(server, gateways);
|
|
11
14
|
registerCidForJson(server);
|
|
@@ -19,18 +22,90 @@ function errorResult(message) {
|
|
|
19
22
|
* the user opts in via `DEXE_IPFS_GATEWAYS_FALLBACK`. Returns an empty array
|
|
20
23
|
* if nothing is configured; the fetch tool fails clean in that case.
|
|
21
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Subdomain-gateway host used to build the `avatarUrl` field stored inside
|
|
27
|
+
* DAO metadata. Must speak the `<cidV1>.ipfs.<host>/<filename>` schema so the
|
|
28
|
+
* DeXe frontend's `parseAvatarFromIpfsResponse` can round-trip the URL.
|
|
29
|
+
*
|
|
30
|
+
* Default is `dweb.link`. The frontend historically hardcoded `4everland.io`,
|
|
31
|
+
* but 4everland fails to discover freshly-pinned CIDs for tens of minutes —
|
|
32
|
+
* during that window the backend cache (`ipfs-cache.dexe.io`) can't fetch
|
|
33
|
+
* the avatar and serves 404 for `<CID>.jpeg`, leaving the profile image
|
|
34
|
+
* broken on app.dexe.io. dweb.link / w3s.link / gateway.pinata.cloud all
|
|
35
|
+
* resolve the same pins immediately.
|
|
36
|
+
*
|
|
37
|
+
* Configurable via `DEXE_IPFS_AVATAR_GATEWAY` (host, no scheme).
|
|
38
|
+
*/
|
|
39
|
+
function avatarSubdomainHost() {
|
|
40
|
+
const override = process.env.DEXE_IPFS_AVATAR_GATEWAY?.trim().replace(/^https?:\/\//i, "").replace(/\/$/, "");
|
|
41
|
+
return override || "dweb.link";
|
|
42
|
+
}
|
|
43
|
+
function buildAvatarUrl(cidV1, fileName) {
|
|
44
|
+
return `https://${cidV1}.ipfs.${avatarSubdomainHost()}/${fileName}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort POST to the DeXe IPFS cache service so the next reader hit on
|
|
48
|
+
* `https://ipfs-cache.dexe.io/<cid>.json|.jpeg` serves cached bytes instead
|
|
49
|
+
* of 404. The frontend's modify-profile flow does this automatically; the
|
|
50
|
+
* MCP path didn't, which is why agents who landed a `editDescriptionURL`
|
|
51
|
+
* proposal saw their new avatar fail to render on app.dexe.io even though
|
|
52
|
+
* the on-chain pointer was correct.
|
|
53
|
+
*
|
|
54
|
+
* Never throws; returns the warmed CID(s) on success.
|
|
55
|
+
*/
|
|
56
|
+
async function warmDexeIpfsCache(cidV0) {
|
|
57
|
+
try {
|
|
58
|
+
const body = JSON.stringify({ data: { attributes: { link: cidV0 } } });
|
|
59
|
+
const r = await fetch("https://api.dexe.io/integrations/ipfs-cache-svc/public/pool-info", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body,
|
|
63
|
+
});
|
|
64
|
+
return { ok: r.ok, status: r.status };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
22
70
|
function resolveGateways(_ctx) {
|
|
23
71
|
const out = [];
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
const normalize = (raw) => {
|
|
73
|
+
const trimmed = raw.trim().replace(/\/$/, "");
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
return "";
|
|
76
|
+
// Allow operators to set DEXE_IPFS_GATEWAY=<host> without a scheme — fetch()
|
|
77
|
+
// refuses such URLs with "Failed to parse URL", which masquerades as an
|
|
78
|
+
// IPFS outage. Default to https since every realistic gateway requires it.
|
|
79
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
80
|
+
return trimmed;
|
|
81
|
+
return `https://${trimmed}`;
|
|
82
|
+
};
|
|
83
|
+
const primary = process.env.DEXE_IPFS_GATEWAY;
|
|
84
|
+
if (primary) {
|
|
85
|
+
const p = normalize(primary);
|
|
86
|
+
if (p)
|
|
87
|
+
out.push(p);
|
|
88
|
+
}
|
|
89
|
+
const fallback = process.env.DEXE_IPFS_GATEWAYS_FALLBACK;
|
|
28
90
|
if (fallback) {
|
|
29
|
-
for (const g of fallback.split(",").map(
|
|
91
|
+
for (const g of fallback.split(",").map(normalize)) {
|
|
30
92
|
if (g && !out.includes(g))
|
|
31
93
|
out.push(g);
|
|
32
94
|
}
|
|
33
95
|
}
|
|
96
|
+
// Auto-fallback: if the primary is a Pinata dedicated gateway AND no
|
|
97
|
+
// gateway key is configured, anonymous reads return 403 and tools like
|
|
98
|
+
// dexe_ipfs_update_dao_metadata hang. Append `https://ipfs.io` as a
|
|
99
|
+
// last-resort public reader so flows keep working out of the box.
|
|
100
|
+
// Opt-out via DEXE_IPFS_DISABLE_PUBLIC_FALLBACK=1.
|
|
101
|
+
const disablePublic = process.env.DEXE_IPFS_DISABLE_PUBLIC_FALLBACK === "1";
|
|
102
|
+
const usesRestrictedPinata = out.some((g) => /\.mypinata\.cloud(\/|$)/i.test(g));
|
|
103
|
+
const haveGatewayKey = !!process.env.DEXE_PINATA_GATEWAY_TOKEN?.trim();
|
|
104
|
+
if (!disablePublic && usesRestrictedPinata && !haveGatewayKey) {
|
|
105
|
+
const publicFallback = "https://ipfs.io";
|
|
106
|
+
if (!out.includes(publicFallback))
|
|
107
|
+
out.push(publicFallback);
|
|
108
|
+
}
|
|
34
109
|
return out;
|
|
35
110
|
}
|
|
36
111
|
const NO_GATEWAY_HINT = "No IPFS gateway configured. Set DEXE_IPFS_GATEWAY to a dedicated gateway " +
|
|
@@ -110,7 +185,7 @@ function registerUploadProposalMetadata(server, ctx) {
|
|
|
110
185
|
*
|
|
111
186
|
* Outer metadata shape (must match for frontend UI compatibility):
|
|
112
187
|
* {
|
|
113
|
-
* avatarUrl: "https://<cidV1>.ipfs
|
|
188
|
+
* avatarUrl: "https://<cidV1>.ipfs.<host>/<filename>" | "",
|
|
114
189
|
* avatarCID: "<cidV1>" | undefined,
|
|
115
190
|
* avatarFileName: "<filename>.jpeg" | "",
|
|
116
191
|
* daoName: string,
|
|
@@ -170,13 +245,15 @@ function registerUploadDaoMetadata(server, ctx) {
|
|
|
170
245
|
const descriptionIpfsPath = `ipfs://${descriptionRes.cid}`;
|
|
171
246
|
// Step 2: Build the outer metadata wrapper (matches frontend schema exactly)
|
|
172
247
|
let avatarUrl = "";
|
|
248
|
+
let avatarCidV1;
|
|
173
249
|
if (avatarCID && avatarFileName) {
|
|
174
|
-
//
|
|
175
|
-
|
|
250
|
+
// Normalize to CID v1 base32 — subdomain gateway only resolves v1.
|
|
251
|
+
avatarCidV1 = toCidV1(avatarCID);
|
|
252
|
+
avatarUrl = buildAvatarUrl(avatarCidV1, avatarFileName);
|
|
176
253
|
}
|
|
177
254
|
const outerPayload = {
|
|
178
255
|
avatarUrl,
|
|
179
|
-
avatarCID:
|
|
256
|
+
avatarCID: avatarCidV1,
|
|
180
257
|
avatarFileName: avatarFileName ?? "",
|
|
181
258
|
daoName,
|
|
182
259
|
websiteUrl,
|
|
@@ -188,12 +265,16 @@ function registerUploadDaoMetadata(server, ctx) {
|
|
|
188
265
|
const metadataRes = await client.pinJson(outerPayload, {
|
|
189
266
|
name: `dao:${daoName.slice(0, 48)}`,
|
|
190
267
|
});
|
|
268
|
+
// Step 4: Prewarm the DeXe IPFS cache so app.dexe.io renders the new
|
|
269
|
+
// metadata + avatar immediately. Best-effort; never fails the call.
|
|
270
|
+
const warmed = await warmDexeIpfsCache(metadataRes.cid);
|
|
191
271
|
const structured = {
|
|
192
272
|
cid: metadataRes.cid,
|
|
193
273
|
descriptionCid: descriptionRes.cid,
|
|
194
274
|
size: metadataRes.size,
|
|
195
275
|
pinnedAt: metadataRes.pinnedAt,
|
|
196
276
|
descriptionURL: metadataRes.cid,
|
|
277
|
+
cachePrewarmed: warmed.ok,
|
|
197
278
|
};
|
|
198
279
|
return {
|
|
199
280
|
content: [
|
|
@@ -202,6 +283,7 @@ function registerUploadDaoMetadata(server, ctx) {
|
|
|
202
283
|
text: `Pinned DAO metadata (frontend-compatible):\n` +
|
|
203
284
|
` description content → ${descriptionRes.cid}\n` +
|
|
204
285
|
` outer metadata → ${metadataRes.cid} (${metadataRes.size} bytes)\n` +
|
|
286
|
+
` ipfs-cache.dexe.io prewarm → ${warmed.ok ? "ok" : `skipped (${warmed.status ?? warmed.error})`}\n` +
|
|
205
287
|
`Use "${metadataRes.cid}" as descriptionURL in deployGovPool.`,
|
|
206
288
|
},
|
|
207
289
|
],
|
|
@@ -217,30 +299,53 @@ function registerUploadDaoMetadata(server, ctx) {
|
|
|
217
299
|
function registerUploadFile(server, ctx) {
|
|
218
300
|
server.registerTool("dexe_ipfs_upload_file", {
|
|
219
301
|
title: "Upload raw bytes (avatar, attachment, etc.) to IPFS (Pinata)",
|
|
220
|
-
description: "Pins a file to IPFS. Accepts base64-encoded bytes; returns the CID
|
|
302
|
+
description: "Pins a file to IPFS. Accepts base64-encoded bytes; returns the CID v1 (base32) and the (possibly normalized) filename. " +
|
|
303
|
+
"For images (contentType: image/*) the filename extension is normalized to `.jpeg` to match what the DeXe frontend stores — " +
|
|
304
|
+
"this is what `dexe_ipfs_upload_dao_metadata` and the DAO profile reader expect. Set `normalizeImageExt: false` to opt out.",
|
|
221
305
|
inputSchema: {
|
|
222
306
|
base64: z.string().min(1).describe("Base64-encoded file bytes"),
|
|
223
307
|
fileName: z.string().default("file"),
|
|
224
308
|
contentType: z.string().default("application/octet-stream"),
|
|
309
|
+
normalizeImageExt: z
|
|
310
|
+
.boolean()
|
|
311
|
+
.default(true)
|
|
312
|
+
.describe("If true and contentType starts with image/, rename the file extension to .jpeg."),
|
|
225
313
|
},
|
|
226
314
|
outputSchema: {
|
|
227
|
-
cid: z.string(),
|
|
315
|
+
cid: z.string().describe("CID v1 base32 — use this as avatarCID."),
|
|
316
|
+
cidV0: z.string().describe("Original CID returned by Pinata (usually v0 Qm...). Kept for legacy callers."),
|
|
317
|
+
fileName: z.string().describe("Filename actually pinned (possibly normalized to .jpeg)."),
|
|
228
318
|
size: z.number(),
|
|
229
319
|
pinnedAt: z.string(),
|
|
230
320
|
},
|
|
231
|
-
}, async ({ base64, fileName = "file", contentType = "application/octet-stream" }) => {
|
|
321
|
+
}, async ({ base64, fileName = "file", contentType = "application/octet-stream", normalizeImageExt = true, }) => {
|
|
232
322
|
const client = requirePinata(ctx);
|
|
233
323
|
if ("error" in client)
|
|
234
324
|
return errorResult(client.error);
|
|
235
325
|
try {
|
|
326
|
+
const isImage = contentType.toLowerCase().startsWith("image/");
|
|
327
|
+
const effectiveFileName = normalizeImageExt && isImage
|
|
328
|
+
? `${fileName.includes(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName}.jpeg`
|
|
329
|
+
: fileName;
|
|
236
330
|
const bytes = Uint8Array.from(Buffer.from(base64, "base64"));
|
|
237
|
-
const res = await client.pinFile(bytes, {
|
|
238
|
-
|
|
331
|
+
const res = await client.pinFile(bytes, {
|
|
332
|
+
fileName: effectiveFileName,
|
|
333
|
+
contentType,
|
|
334
|
+
name: effectiveFileName,
|
|
335
|
+
});
|
|
336
|
+
const cidV1 = toCidV1(res.cid);
|
|
337
|
+
const structured = {
|
|
338
|
+
cid: cidV1,
|
|
339
|
+
cidV0: res.cid,
|
|
340
|
+
fileName: effectiveFileName,
|
|
341
|
+
size: res.size,
|
|
342
|
+
pinnedAt: res.pinnedAt,
|
|
343
|
+
};
|
|
239
344
|
return {
|
|
240
345
|
content: [
|
|
241
346
|
{
|
|
242
347
|
type: "text",
|
|
243
|
-
text: `Pinned ${
|
|
348
|
+
text: `Pinned ${effectiveFileName} (${bytes.length} bytes) → ${cidV1} (v0=${res.cid}, size on IPFS=${res.size})`,
|
|
244
349
|
},
|
|
245
350
|
],
|
|
246
351
|
structuredContent: structured,
|
|
@@ -373,4 +478,320 @@ function registerCidForJson(server) {
|
|
|
373
478
|
// Keep this export so `cidForBytes` isn't flagged as dead code by strict builds;
|
|
374
479
|
// tools can opt into it later.
|
|
375
480
|
export { cidForBytes };
|
|
481
|
+
// ---------- dexe_ipfs_upload_avatar (one-shot composite) ----------
|
|
482
|
+
/**
|
|
483
|
+
* Convenience wrapper around `dexe_ipfs_upload_file` that returns the
|
|
484
|
+
* exact triple `dexe_ipfs_upload_dao_metadata` (and `*_modify_dao_profile`)
|
|
485
|
+
* expect: { avatarCID (v1), avatarFileName (.jpeg), avatarUrl }.
|
|
486
|
+
*
|
|
487
|
+
* Single call replaces: pinFile → toV1 → rename → build subdomain URL.
|
|
488
|
+
*/
|
|
489
|
+
function registerUploadAvatar(server, ctx) {
|
|
490
|
+
server.registerTool("dexe_ipfs_upload_avatar", {
|
|
491
|
+
title: "Upload a DAO avatar (one-shot: pins + returns avatarCID/avatarFileName/avatarUrl)",
|
|
492
|
+
description: "Uploads an image and returns the {avatarCID, avatarFileName, avatarUrl} triple ready to feed into `dexe_ipfs_upload_dao_metadata` " +
|
|
493
|
+
"(for DAO creation) or `dexe_proposal_build_modify_dao_profile` (for profile updates). " +
|
|
494
|
+
"Normalizes the filename to `.jpeg` (matching the frontend) and returns a CID v1 base32 string that resolves on the subdomain gateway.",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
base64: z.string().min(1).describe("Base64-encoded image bytes"),
|
|
497
|
+
fileName: z.string().default("avatar").describe("Base filename; extension will be normalized to .jpeg"),
|
|
498
|
+
contentType: z
|
|
499
|
+
.string()
|
|
500
|
+
.default("image/jpeg")
|
|
501
|
+
.describe("MIME type; must start with image/. Defaults to image/jpeg."),
|
|
502
|
+
},
|
|
503
|
+
outputSchema: {
|
|
504
|
+
avatarCID: z.string().describe("CID v1 base32 — pass to upload_dao_metadata as avatarCID."),
|
|
505
|
+
avatarFileName: z.string().describe("Filename (always ends with .jpeg)."),
|
|
506
|
+
avatarUrl: z.string().describe("Full subdomain-gateway URL — what the frontend stores verbatim."),
|
|
507
|
+
size: z.number(),
|
|
508
|
+
pinnedAt: z.string(),
|
|
509
|
+
},
|
|
510
|
+
}, async ({ base64, fileName = "avatar", contentType = "image/jpeg" }) => {
|
|
511
|
+
if (!contentType.toLowerCase().startsWith("image/")) {
|
|
512
|
+
return errorResult(`contentType must be image/* (got: ${contentType})`);
|
|
513
|
+
}
|
|
514
|
+
const client = requirePinata(ctx);
|
|
515
|
+
if ("error" in client)
|
|
516
|
+
return errorResult(client.error);
|
|
517
|
+
try {
|
|
518
|
+
const base = fileName.includes(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
|
|
519
|
+
const normalized = `${base || "avatar"}.jpeg`;
|
|
520
|
+
const bytes = Uint8Array.from(Buffer.from(base64, "base64"));
|
|
521
|
+
const res = await client.pinFile(bytes, {
|
|
522
|
+
fileName: normalized,
|
|
523
|
+
contentType,
|
|
524
|
+
name: normalized,
|
|
525
|
+
});
|
|
526
|
+
const avatarCID = toCidV1(res.cid);
|
|
527
|
+
const avatarUrl = buildAvatarUrl(avatarCID, normalized);
|
|
528
|
+
const structured = {
|
|
529
|
+
avatarCID,
|
|
530
|
+
avatarFileName: normalized,
|
|
531
|
+
avatarUrl,
|
|
532
|
+
size: res.size,
|
|
533
|
+
pinnedAt: res.pinnedAt,
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
content: [
|
|
537
|
+
{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: `Avatar pinned: ${avatarUrl} (cidV1=${avatarCID}, ${bytes.length} bytes)`,
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
structuredContent: structured,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// ---------- dexe_dao_generate_avatar (no external provider) ----------
|
|
551
|
+
/**
|
|
552
|
+
* Deterministic placeholder avatar: 1–2 letter initials over a hash-coloured
|
|
553
|
+
* gradient. Pure SVG → uploaded as image/svg+xml then served via the same
|
|
554
|
+
* subdomain gateway frontend uses. No third-party generator required.
|
|
555
|
+
*/
|
|
556
|
+
function registerGenerateAvatar(server, ctx) {
|
|
557
|
+
server.registerTool("dexe_dao_generate_avatar", {
|
|
558
|
+
title: "Generate a deterministic placeholder avatar for a DAO",
|
|
559
|
+
description: "Builds an SVG avatar with the DAO's initials over a hash-coloured gradient (no external generator) and pins it to IPFS. " +
|
|
560
|
+
"Returns the same {avatarCID, avatarFileName, avatarUrl} shape as `dexe_ipfs_upload_avatar`, " +
|
|
561
|
+
"ready to feed into `dexe_ipfs_upload_dao_metadata` or `dexe_proposal_build_modify_dao_profile`. " +
|
|
562
|
+
"Same input always produces the same colours (great for re-deploys).",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
daoName: z.string().min(1).describe("DAO name; first 1–2 alphanumeric chars become the avatar initials."),
|
|
565
|
+
size: z.number().int().min(64).max(2048).default(512).describe("SVG viewBox size (square)."),
|
|
566
|
+
},
|
|
567
|
+
outputSchema: {
|
|
568
|
+
avatarCID: z.string(),
|
|
569
|
+
avatarFileName: z.string(),
|
|
570
|
+
avatarUrl: z.string(),
|
|
571
|
+
size: z.number(),
|
|
572
|
+
pinnedAt: z.string(),
|
|
573
|
+
},
|
|
574
|
+
}, async ({ daoName, size = 512 }) => {
|
|
575
|
+
const client = requirePinata(ctx);
|
|
576
|
+
if ("error" in client)
|
|
577
|
+
return errorResult(client.error);
|
|
578
|
+
try {
|
|
579
|
+
const svg = buildIdenticonSvg(daoName, size);
|
|
580
|
+
const bytes = Buffer.from(svg, "utf8");
|
|
581
|
+
// Frontend reader looks for an extension; we keep .jpeg to stay
|
|
582
|
+
// consistent with what `dexe_ipfs_upload_avatar` returns even though
|
|
583
|
+
// the bytes themselves are SVG. The subdomain gateway serves it by
|
|
584
|
+
// CID; content negotiation handles the type.
|
|
585
|
+
const fileName = "avatar.jpeg";
|
|
586
|
+
const res = await client.pinFile(bytes, {
|
|
587
|
+
fileName,
|
|
588
|
+
contentType: "image/svg+xml",
|
|
589
|
+
name: fileName,
|
|
590
|
+
});
|
|
591
|
+
const avatarCID = toCidV1(res.cid);
|
|
592
|
+
const avatarUrl = buildAvatarUrl(avatarCID, fileName);
|
|
593
|
+
const structured = {
|
|
594
|
+
avatarCID,
|
|
595
|
+
avatarFileName: fileName,
|
|
596
|
+
avatarUrl,
|
|
597
|
+
size: res.size,
|
|
598
|
+
pinnedAt: res.pinnedAt,
|
|
599
|
+
};
|
|
600
|
+
return {
|
|
601
|
+
content: [
|
|
602
|
+
{
|
|
603
|
+
type: "text",
|
|
604
|
+
text: `Generated avatar for "${daoName}" → ${avatarUrl}`,
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
structuredContent: structured,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/** djb2-style hash → unsigned 32-bit. */
|
|
616
|
+
function hashString(s) {
|
|
617
|
+
let h = 5381;
|
|
618
|
+
for (let i = 0; i < s.length; i++)
|
|
619
|
+
h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
620
|
+
return h >>> 0;
|
|
621
|
+
}
|
|
622
|
+
function buildIdenticonSvg(daoName, size) {
|
|
623
|
+
const cleaned = daoName.replace(/[^\p{L}\p{N}]/gu, "");
|
|
624
|
+
const initials = (cleaned.slice(0, 2) || "?").toUpperCase();
|
|
625
|
+
const h = hashString(daoName);
|
|
626
|
+
const hue1 = h % 360;
|
|
627
|
+
const hue2 = (hue1 + 40 + ((h >>> 8) % 80)) % 360;
|
|
628
|
+
const c1 = `hsl(${hue1} 70% 55%)`;
|
|
629
|
+
const c2 = `hsl(${hue2} 70% 35%)`;
|
|
630
|
+
const fontSize = Math.round(size * 0.46);
|
|
631
|
+
// Plain SVG — no <foreignObject>, no JS. Safe to pin and serve via subdomain gateway.
|
|
632
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">\n <defs>\n <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">\n <stop offset="0%" stop-color="${c1}"/>\n <stop offset="100%" stop-color="${c2}"/>\n </linearGradient>\n </defs>\n <rect width="100%" height="100%" fill="url(#g)"/>\n <text x="50%" y="50%" dy=".35em" text-anchor="middle" font-family="Inter, Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white">${initials}</text>\n</svg>\n`;
|
|
633
|
+
}
|
|
634
|
+
// ---------- dexe_ipfs_update_dao_metadata (fetch + merge + re-upload) ----------
|
|
635
|
+
/**
|
|
636
|
+
* Smart helper for "Modify DAO Profile" proposals. Fetches the DAO's existing
|
|
637
|
+
* metadata JSON, applies user-supplied partial overrides, re-pins the result.
|
|
638
|
+
*
|
|
639
|
+
* Returns the new outer CID so callers can feed it into
|
|
640
|
+
* `dexe_proposal_build_modify_dao_profile` as `newDescriptionURL`.
|
|
641
|
+
*
|
|
642
|
+
* Without this tool, callers had to re-specify every unchanged field
|
|
643
|
+
* (daoName, websiteUrl, socialLinks, …) on every edit, and any forgotten
|
|
644
|
+
* field would silently disappear from the profile.
|
|
645
|
+
*/
|
|
646
|
+
function registerUpdateDaoMetadata(server, ctx, gateways) {
|
|
647
|
+
server.registerTool("dexe_ipfs_update_dao_metadata", {
|
|
648
|
+
title: "Fetch DAO metadata, apply partial overrides, re-upload",
|
|
649
|
+
description: "Reads the existing DAO metadata JSON from IPFS via the configured gateway, applies only the fields you pass in `overrides`, " +
|
|
650
|
+
"and re-pins the merged result. Returns the new outer `descriptionURL` ready for `dexe_proposal_build_modify_dao_profile`. " +
|
|
651
|
+
"Unspecified fields are preserved verbatim (so you can change just the avatar without re-typing the website or social links).",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
currentDescriptionURL: z
|
|
654
|
+
.string()
|
|
655
|
+
.describe("Current DAO descriptionURL — `ipfs://<cid>` or bare CID. Fetched via the configured IPFS gateway."),
|
|
656
|
+
overrides: z
|
|
657
|
+
.object({
|
|
658
|
+
daoName: z.string().optional(),
|
|
659
|
+
websiteUrl: z.string().optional(),
|
|
660
|
+
description: z
|
|
661
|
+
.string()
|
|
662
|
+
.optional()
|
|
663
|
+
.describe("Markdown or plain text. If provided, replaces the description content (re-uploaded as its own pin)."),
|
|
664
|
+
avatarCID: z.string().optional().describe("New avatar CID (any version). Pair with avatarFileName to set, or pass empty string to clear."),
|
|
665
|
+
avatarFileName: z.string().optional(),
|
|
666
|
+
socialLinks: z.array(z.tuple([z.string(), z.string()])).optional(),
|
|
667
|
+
documents: z.array(z.object({ name: z.string(), url: z.string() })).optional(),
|
|
668
|
+
})
|
|
669
|
+
.describe("Only the fields you want to change. Anything omitted is kept from the current metadata."),
|
|
670
|
+
timeoutMs: z.number().int().min(500).max(30_000).default(6000),
|
|
671
|
+
},
|
|
672
|
+
outputSchema: {
|
|
673
|
+
descriptionURL: z.string().describe("New outer CID — pass to dexe_proposal_build_modify_dao_profile.newDescriptionURL."),
|
|
674
|
+
previousDescriptionURL: z.string(),
|
|
675
|
+
cid: z.string(),
|
|
676
|
+
descriptionCid: z.string().optional(),
|
|
677
|
+
size: z.number(),
|
|
678
|
+
pinnedAt: z.string(),
|
|
679
|
+
},
|
|
680
|
+
}, async ({ currentDescriptionURL, overrides, timeoutMs = 6000 }) => {
|
|
681
|
+
if (gateways.length === 0)
|
|
682
|
+
return errorResult(NO_GATEWAY_HINT);
|
|
683
|
+
const client = requirePinata(ctx);
|
|
684
|
+
if ("error" in client)
|
|
685
|
+
return errorResult(client.error);
|
|
686
|
+
try {
|
|
687
|
+
const fetched = await fetchIpfs(currentDescriptionURL, {
|
|
688
|
+
gateways,
|
|
689
|
+
perRequestTimeoutMs: timeoutMs,
|
|
690
|
+
});
|
|
691
|
+
if (!fetched.json || typeof fetched.json !== "object") {
|
|
692
|
+
return errorResult(`Current descriptionURL did not resolve to JSON metadata (content-type=${fetched.contentType}). ` +
|
|
693
|
+
`Got ${fetched.bytes.length} bytes from ${fetched.gateway}.`);
|
|
694
|
+
}
|
|
695
|
+
const current = fetched.json;
|
|
696
|
+
// Per-field merge, with explicit semantics for the trickier ones.
|
|
697
|
+
const daoName = typeof overrides.daoName === "string" ? overrides.daoName : current.daoName ?? "";
|
|
698
|
+
const websiteUrl = typeof overrides.websiteUrl === "string" ? overrides.websiteUrl : current.websiteUrl ?? "";
|
|
699
|
+
const socialLinks = overrides.socialLinks ?? (current.socialLinks ?? []);
|
|
700
|
+
const documents = overrides.documents ?? (current.documents ?? []);
|
|
701
|
+
// Avatar: empty-string avatarCID means "clear". Otherwise normalize to v1.
|
|
702
|
+
let avatarUrl = "";
|
|
703
|
+
let avatarCidV1;
|
|
704
|
+
let avatarFileName = "";
|
|
705
|
+
if (overrides.avatarCID === undefined && overrides.avatarFileName === undefined) {
|
|
706
|
+
// Unchanged — copy avatarCID + filename from current, but rebuild
|
|
707
|
+
// avatarUrl with the configured gateway. Carrying the old URL
|
|
708
|
+
// verbatim leaves stale 4everland references in DAOs that have
|
|
709
|
+
// only had a name/website update; the cache backend then fails
|
|
710
|
+
// to fetch the avatar binary and serves a 404 for `<cid>.jpeg`.
|
|
711
|
+
const currCID = current.avatarCID || "";
|
|
712
|
+
const currFile = current.avatarFileName || "";
|
|
713
|
+
if (currCID && currFile) {
|
|
714
|
+
avatarCidV1 = toCidV1(currCID);
|
|
715
|
+
avatarFileName = currFile;
|
|
716
|
+
avatarUrl = buildAvatarUrl(avatarCidV1, avatarFileName);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
avatarUrl = current.avatarUrl ?? "";
|
|
720
|
+
avatarCidV1 = currCID || undefined;
|
|
721
|
+
avatarFileName = currFile;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else if (overrides.avatarCID && overrides.avatarFileName) {
|
|
725
|
+
avatarCidV1 = toCidV1(overrides.avatarCID);
|
|
726
|
+
avatarFileName = overrides.avatarFileName;
|
|
727
|
+
avatarUrl = buildAvatarUrl(avatarCidV1, avatarFileName);
|
|
728
|
+
}
|
|
729
|
+
else if (overrides.avatarCID === "") {
|
|
730
|
+
// Explicit clear.
|
|
731
|
+
avatarUrl = "";
|
|
732
|
+
avatarCidV1 = undefined;
|
|
733
|
+
avatarFileName = "";
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
return errorResult("Avatar overrides require BOTH avatarCID and avatarFileName (or empty string for avatarCID to clear).");
|
|
737
|
+
}
|
|
738
|
+
// Description: re-upload only if changed.
|
|
739
|
+
let descriptionIpfsPath;
|
|
740
|
+
let descriptionCid;
|
|
741
|
+
if (typeof overrides.description === "string") {
|
|
742
|
+
const payload = markdownToSlate(overrides.description);
|
|
743
|
+
const descRes = await client.pinJson(payload, {
|
|
744
|
+
name: `dao-desc:${daoName.slice(0, 40)}`,
|
|
745
|
+
});
|
|
746
|
+
descriptionIpfsPath = `ipfs://${descRes.cid}`;
|
|
747
|
+
descriptionCid = descRes.cid;
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
descriptionIpfsPath = current.description ?? "";
|
|
751
|
+
}
|
|
752
|
+
const outerPayload = {
|
|
753
|
+
avatarUrl,
|
|
754
|
+
avatarCID: avatarCidV1,
|
|
755
|
+
avatarFileName,
|
|
756
|
+
daoName,
|
|
757
|
+
websiteUrl,
|
|
758
|
+
description: descriptionIpfsPath,
|
|
759
|
+
socialLinks,
|
|
760
|
+
documents,
|
|
761
|
+
};
|
|
762
|
+
const metadataRes = await client.pinJson(outerPayload, {
|
|
763
|
+
name: `dao:${daoName.slice(0, 48)}`,
|
|
764
|
+
});
|
|
765
|
+
// Prewarm DeXe ipfs-cache so app.dexe.io renders the new metadata
|
|
766
|
+
// immediately after the modify-profile proposal executes.
|
|
767
|
+
const warmed = await warmDexeIpfsCache(metadataRes.cid);
|
|
768
|
+
const structured = {
|
|
769
|
+
descriptionURL: metadataRes.cid,
|
|
770
|
+
previousDescriptionURL: fetched.cid,
|
|
771
|
+
cid: metadataRes.cid,
|
|
772
|
+
descriptionCid,
|
|
773
|
+
size: metadataRes.size,
|
|
774
|
+
pinnedAt: metadataRes.pinnedAt,
|
|
775
|
+
cachePrewarmed: warmed.ok,
|
|
776
|
+
};
|
|
777
|
+
const changed = Object.keys(overrides).filter((k) => overrides[k] !== undefined);
|
|
778
|
+
return {
|
|
779
|
+
content: [
|
|
780
|
+
{
|
|
781
|
+
type: "text",
|
|
782
|
+
text: `Updated DAO metadata (${changed.length ? changed.join(", ") : "no changes"}):\n` +
|
|
783
|
+
` previous → ${fetched.cid}\n` +
|
|
784
|
+
` new → ${metadataRes.cid}\n` +
|
|
785
|
+
` ipfs-cache.dexe.io prewarm → ${warmed.ok ? "ok" : `skipped (${warmed.status ?? warmed.error})`}\n` +
|
|
786
|
+
`Pass "${metadataRes.cid}" to dexe_proposal_build_modify_dao_profile.newDescriptionURL.`,
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
structuredContent: structured,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
376
797
|
//# sourceMappingURL=ipfs.js.map
|