@clawcash/forge 0.1.7 → 0.2.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/README.md +38 -39
- package/dist/index.cjs +320 -440
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -131
- package/dist/index.d.ts +75 -131
- package/dist/index.js +305 -429
- package/dist/index.js.map +1 -1
- package/package.json +2 -12
package/dist/index.js
CHANGED
|
@@ -36,33 +36,8 @@ function defineAgentService(config) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// src/
|
|
40
|
-
import
|
|
41
|
-
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
|
|
42
|
-
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
43
|
-
import { ExactEvmScheme } from "@x402/evm/exact/server";
|
|
44
|
-
|
|
45
|
-
// src/gateway.ts
|
|
46
|
-
async function heartbeatGateway(opts) {
|
|
47
|
-
const gateway = opts.gatewayUrl.replace(/\/$/, "");
|
|
48
|
-
const res = await fetch(`${gateway}/register/heartbeat`, {
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: {
|
|
51
|
-
"content-type": "application/json",
|
|
52
|
-
authorization: `Bearer ${opts.registerSecret}`
|
|
53
|
-
},
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
handle: opts.handle.trim().toLowerCase(),
|
|
56
|
-
baseUrl: opts.baseUrl.replace(/\/$/, "")
|
|
57
|
-
})
|
|
58
|
-
});
|
|
59
|
-
if (!res.ok) {
|
|
60
|
-
const text = await res.text();
|
|
61
|
-
throw new Error(
|
|
62
|
-
`gateway heartbeat failed (${res.status}): ${text.slice(0, 240)}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
39
|
+
// src/worker.ts
|
|
40
|
+
import os from "os";
|
|
66
41
|
|
|
67
42
|
// src/gateway-auth.ts
|
|
68
43
|
function getForgeApiKey(explicit) {
|
|
@@ -70,29 +45,7 @@ function getForgeApiKey(explicit) {
|
|
|
70
45
|
if (!value || value.length < 16) return null;
|
|
71
46
|
return value;
|
|
72
47
|
}
|
|
73
|
-
function getForgeGatewaySecret(explicit) {
|
|
74
|
-
return getForgeApiKey(explicit);
|
|
75
|
-
}
|
|
76
|
-
function isForgeGatewayRequest(req, secret) {
|
|
77
|
-
const expected = getForgeApiKey(secret);
|
|
78
|
-
if (!expected) return false;
|
|
79
|
-
const header = req.header("authorization") ?? "";
|
|
80
|
-
const bearer = header.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() : "";
|
|
81
|
-
const alt = req.header("x-forge-api-key")?.trim() || req.header("x-clawcash-gateway")?.trim() || "";
|
|
82
|
-
return bearer === expected || alt === expected;
|
|
83
|
-
}
|
|
84
48
|
var FORGE_GATEWAY_USER_ID = "forge_gateway_agent";
|
|
85
|
-
function requireForgeGateway(secret) {
|
|
86
|
-
return (req, res, next) => {
|
|
87
|
-
if (!isForgeGatewayRequest(req, secret)) {
|
|
88
|
-
res.status(401).json({
|
|
89
|
-
error: "Unauthorized \u2014 Forge API key required (FORGE_API_KEY from Forge dashboard)"
|
|
90
|
-
});
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
next();
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
49
|
function forgeGatewayLoopbackHeaders(secret) {
|
|
97
50
|
const expected = getForgeApiKey(secret);
|
|
98
51
|
if (!expected) return {};
|
|
@@ -102,138 +55,7 @@ function forgeGatewayLoopbackHeaders(secret) {
|
|
|
102
55
|
};
|
|
103
56
|
}
|
|
104
57
|
|
|
105
|
-
// src/
|
|
106
|
-
var ZERO = /^0x0{40}$/i;
|
|
107
|
-
function isZeroAddress(address) {
|
|
108
|
-
if (!address) return true;
|
|
109
|
-
return ZERO.test(address.trim());
|
|
110
|
-
}
|
|
111
|
-
function normalizeBaseUrl(url) {
|
|
112
|
-
return url.replace(/\/$/, "");
|
|
113
|
-
}
|
|
114
|
-
function pickForgeBaseUrl(source) {
|
|
115
|
-
const fromEnv = process.env.FORGE_URL?.trim();
|
|
116
|
-
if (fromEnv) return normalizeBaseUrl(fromEnv);
|
|
117
|
-
const fromOpts = source?.baseUrl?.trim();
|
|
118
|
-
if (!fromOpts) return null;
|
|
119
|
-
const normalized = normalizeBaseUrl(fromOpts);
|
|
120
|
-
if (/localhost|127\.0\.0\.1/i.test(normalized) && process.env.NODE_ENV === "production") {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
return normalized;
|
|
124
|
-
}
|
|
125
|
-
async function resolvePayTo(opts) {
|
|
126
|
-
const envOverride = process.env.PAY_TO?.trim();
|
|
127
|
-
if (envOverride && !isZeroAddress(envOverride)) {
|
|
128
|
-
return envOverride;
|
|
129
|
-
}
|
|
130
|
-
const explicit = opts.payTo?.trim();
|
|
131
|
-
if (explicit && !isZeroAddress(explicit) && !opts.forge) {
|
|
132
|
-
return explicit;
|
|
133
|
-
}
|
|
134
|
-
const baseUrl = pickForgeBaseUrl(opts.forge);
|
|
135
|
-
if (baseUrl && opts.forge) {
|
|
136
|
-
const url = `${baseUrl}/api/public/pay-to/${encodeURIComponent(opts.forge.owner)}/${encodeURIComponent(opts.forge.repo)}`;
|
|
137
|
-
try {
|
|
138
|
-
const controller = new AbortController();
|
|
139
|
-
const timer = setTimeout(
|
|
140
|
-
() => controller.abort(),
|
|
141
|
-
opts.timeoutMs ?? 2500
|
|
142
|
-
);
|
|
143
|
-
const res = await fetch(url, {
|
|
144
|
-
signal: controller.signal,
|
|
145
|
-
headers: { accept: "application/json" }
|
|
146
|
-
});
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
if (res.ok) {
|
|
149
|
-
const json = await res.json();
|
|
150
|
-
if (json.address && !isZeroAddress(json.address)) {
|
|
151
|
-
return json.address.trim();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
console.warn("[forge] live payTo fetch failed, using fallback:", err);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (explicit && !isZeroAddress(explicit)) return explicit;
|
|
159
|
-
if (opts.fallback && !isZeroAddress(opts.fallback)) return opts.fallback;
|
|
160
|
-
throw new Error(
|
|
161
|
-
"resolvePayTo: no usable address (set Forge wallet / PAY_TO, or re-run Forge PR)"
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// src/status.ts
|
|
166
|
-
var PACKAGE_VERSION = "0.1.7";
|
|
167
|
-
var lastHeartbeat = {
|
|
168
|
-
attempted: false,
|
|
169
|
-
ok: null,
|
|
170
|
-
at: null,
|
|
171
|
-
error: null
|
|
172
|
-
};
|
|
173
|
-
function recordHeartbeatResult(result) {
|
|
174
|
-
lastHeartbeat = {
|
|
175
|
-
attempted: true,
|
|
176
|
-
ok: result.ok,
|
|
177
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
178
|
-
error: result.ok ? null : result.error
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
function getLastHeartbeat() {
|
|
182
|
-
return { ...lastHeartbeat };
|
|
183
|
-
}
|
|
184
|
-
function buildForgeSurfaceStatus(opts) {
|
|
185
|
-
const fulfillPath = opts.fulfillPath.replace(/\/$/, "") || "/forge/fulfill";
|
|
186
|
-
const agentBasePath = opts.agentBasePath.replace(/\/$/, "") || "/agent";
|
|
187
|
-
const ready = opts.fulfillMounted && Boolean(opts.publicBaseUrl) && opts.actions.length > 0;
|
|
188
|
-
return {
|
|
189
|
-
ok: true,
|
|
190
|
-
forge: true,
|
|
191
|
-
version: PACKAGE_VERSION,
|
|
192
|
-
handle: opts.handle,
|
|
193
|
-
publicBaseUrl: opts.publicBaseUrl,
|
|
194
|
-
gatewayUrl: opts.gatewayUrl,
|
|
195
|
-
fulfillMounted: opts.fulfillMounted,
|
|
196
|
-
fulfillPath,
|
|
197
|
-
agentBasePath,
|
|
198
|
-
skipPayment: opts.skipPayment,
|
|
199
|
-
actions: opts.actions,
|
|
200
|
-
ready,
|
|
201
|
-
heartbeat: getLastHeartbeat(),
|
|
202
|
-
endpoints: {
|
|
203
|
-
health: "/forge/health",
|
|
204
|
-
status: "/forge/status",
|
|
205
|
-
wellKnown: "/.well-known/clawcash.json",
|
|
206
|
-
fulfill: fulfillPath,
|
|
207
|
-
agent: agentBasePath
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function buildWellKnownDocument(status) {
|
|
212
|
-
return {
|
|
213
|
-
forge: true,
|
|
214
|
-
version: status.version,
|
|
215
|
-
health: status.endpoints.health,
|
|
216
|
-
status: status.endpoints.status,
|
|
217
|
-
handle: status.handle,
|
|
218
|
-
ready: status.ready
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
function forgeSdkVersion() {
|
|
222
|
-
return PACKAGE_VERSION;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/mount.ts
|
|
226
|
-
var DEFAULT_NETWORK = "eip155:8453";
|
|
227
|
-
var DEFAULT_FACILITATOR_URL = "https://facilitator.xpay.sh";
|
|
228
|
-
function asNetwork(value) {
|
|
229
|
-
return value ?? DEFAULT_NETWORK;
|
|
230
|
-
}
|
|
231
|
-
function createFacilitatorClient(options) {
|
|
232
|
-
return new HTTPFacilitatorClient({
|
|
233
|
-
url: options.facilitatorUrl ?? DEFAULT_FACILITATOR_URL,
|
|
234
|
-
createAuthHeaders: options.createAuthHeaders
|
|
235
|
-
});
|
|
236
|
-
}
|
|
58
|
+
// src/validate.ts
|
|
237
59
|
function validateInput(body, schema) {
|
|
238
60
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
239
61
|
return { ok: false, error: "Request body must be a JSON object" };
|
|
@@ -279,289 +101,343 @@ function checkType(key, value, type) {
|
|
|
279
101
|
return null;
|
|
280
102
|
}
|
|
281
103
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
104
|
+
|
|
105
|
+
// src/worker.ts
|
|
106
|
+
var DEFAULT_GATEWAY_URL = "https://gateway.clawca.sh";
|
|
107
|
+
var DEFAULT_WAIT_MS = 2e4;
|
|
108
|
+
var BACKOFF_STEPS_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
109
|
+
function findService(services, action) {
|
|
110
|
+
const key = toKebabCase(action.trim());
|
|
111
|
+
return services.find((s) => s.path === key || toKebabCase(s.name) === key) ?? null;
|
|
112
|
+
}
|
|
113
|
+
async function postJson(url, apiKey, body, signal) {
|
|
114
|
+
const res = await fetch(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
accept: "application/json",
|
|
119
|
+
authorization: `Bearer ${apiKey}`
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
signal
|
|
123
|
+
});
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
let json2 = {};
|
|
126
|
+
try {
|
|
127
|
+
json2 = text ? JSON.parse(text) : {};
|
|
128
|
+
} catch {
|
|
129
|
+
json2 = { raw: text };
|
|
130
|
+
}
|
|
131
|
+
return { status: res.status, json: json2 };
|
|
285
132
|
}
|
|
286
|
-
function
|
|
287
|
-
const
|
|
288
|
-
if (!
|
|
289
|
-
|
|
290
|
-
"
|
|
133
|
+
function forgeWorker(options) {
|
|
134
|
+
const handle = (options.handle ?? process.env.FORGE_HANDLE ?? "").trim().toLowerCase();
|
|
135
|
+
if (!handle) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"forgeWorker: handle is required (option or FORGE_HANDLE env)"
|
|
291
138
|
);
|
|
292
|
-
return;
|
|
293
139
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
140
|
+
const apiKey = getForgeApiKey(options.apiKey);
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"forgeWorker: FORGE_API_KEY is required (per-project key from the Forge dashboard, min 16 chars)"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (!options.services?.length) {
|
|
147
|
+
throw new Error("forgeWorker: services must be a non-empty array");
|
|
148
|
+
}
|
|
149
|
+
const gatewayUrl = (options.gatewayUrl?.trim() || process.env.GATEWAY_URL?.trim() || DEFAULT_GATEWAY_URL).replace(/\/$/, "");
|
|
150
|
+
const workerId = options.workerId?.trim() || `${os.hostname()}-${process.pid}`;
|
|
151
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
152
|
+
let running = true;
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
let wakeSleeper = null;
|
|
155
|
+
const reportError = (err) => {
|
|
156
|
+
if (options.onError) options.onError(err);
|
|
157
|
+
else console.warn(`[forge-worker] ${err.message}`);
|
|
158
|
+
};
|
|
159
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
wakeSleeper = null;
|
|
162
|
+
resolve();
|
|
163
|
+
}, ms);
|
|
164
|
+
wakeSleeper = () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
wakeSleeper = null;
|
|
167
|
+
resolve();
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
async function runJob(job) {
|
|
171
|
+
const resultUrl = `${gatewayUrl}/jobs/${encodeURIComponent(job.id)}/result`;
|
|
172
|
+
const service = findService(options.services, job.action);
|
|
173
|
+
if (!service) {
|
|
174
|
+
await postJson(
|
|
175
|
+
resultUrl,
|
|
176
|
+
apiKey,
|
|
177
|
+
{ ok: false, error: `no service registered for action ${job.action}` },
|
|
178
|
+
controller.signal
|
|
179
|
+
);
|
|
180
|
+
reportError(new Error(`job ${job.id}: unknown action ${job.action}`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const validated = validateInput(job.input, service.input);
|
|
185
|
+
if (!validated.ok) {
|
|
186
|
+
await postJson(
|
|
187
|
+
resultUrl,
|
|
188
|
+
apiKey,
|
|
189
|
+
{ ok: false, error: validated.error },
|
|
190
|
+
controller.signal
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const output = await service.execute(validated.value, createContext());
|
|
195
|
+
const res = await postJson(
|
|
196
|
+
resultUrl,
|
|
197
|
+
apiKey,
|
|
198
|
+
{ ok: true, output },
|
|
199
|
+
controller.signal
|
|
200
|
+
);
|
|
201
|
+
if (res.status >= 400) {
|
|
202
|
+
reportError(
|
|
203
|
+
new Error(
|
|
204
|
+
`job ${job.id}: result rejected (${res.status}): ${JSON.stringify(res.json).slice(0, 300)}`
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`[forge-worker] completed job ${job.id} (${job.action})`);
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const message = err instanceof Error ? err.message : "execute failed";
|
|
212
|
+
try {
|
|
213
|
+
await postJson(
|
|
214
|
+
resultUrl,
|
|
215
|
+
apiKey,
|
|
216
|
+
{ ok: false, error: message },
|
|
217
|
+
controller.signal
|
|
218
|
+
);
|
|
219
|
+
} catch {
|
|
316
220
|
}
|
|
221
|
+
reportError(new Error(`job ${job.id} failed: ${message}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function loop() {
|
|
225
|
+
console.log(
|
|
226
|
+
`[forge-worker] ${handle} polling ${gatewayUrl}/jobs/claim as ${workerId} (${options.services.length} action(s))`
|
|
317
227
|
);
|
|
228
|
+
let backoffIndex = 0;
|
|
229
|
+
while (running) {
|
|
230
|
+
try {
|
|
231
|
+
const { status, json: json2 } = await postJson(
|
|
232
|
+
`${gatewayUrl}/jobs/claim`,
|
|
233
|
+
apiKey,
|
|
234
|
+
{ handle, workerId, waitMs },
|
|
235
|
+
controller.signal
|
|
236
|
+
);
|
|
237
|
+
if (status === 401 || status === 404 || status === 409) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`claim rejected (${status}): ${JSON.stringify(json2).slice(0, 300)} \u2014 check FORGE_API_KEY, handle, and that the project is registered with fulfillment.mode=worker`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (status >= 400) {
|
|
243
|
+
throw Object.assign(
|
|
244
|
+
new Error(`claim failed (${status})`),
|
|
245
|
+
{ transient: true }
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
backoffIndex = 0;
|
|
249
|
+
const job = json2.job;
|
|
250
|
+
if (job) {
|
|
251
|
+
await runJob(job);
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (!running) break;
|
|
255
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
256
|
+
if (error.name === "AbortError") break;
|
|
257
|
+
reportError(error);
|
|
258
|
+
const transient = error.transient || !/claim rejected/.test(error.message);
|
|
259
|
+
if (!transient) {
|
|
260
|
+
console.error(
|
|
261
|
+
"[forge-worker] fatal configuration error \u2014 stopping worker"
|
|
262
|
+
);
|
|
263
|
+
running = false;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const delay = BACKOFF_STEPS_MS[Math.min(backoffIndex, BACKOFF_STEPS_MS.length - 1)];
|
|
267
|
+
backoffIndex += 1;
|
|
268
|
+
await sleep(delay);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
console.log("[forge-worker] stopped");
|
|
318
272
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
273
|
+
void loop();
|
|
274
|
+
return {
|
|
275
|
+
stop() {
|
|
276
|
+
running = false;
|
|
277
|
+
controller.abort();
|
|
278
|
+
wakeSleeper?.();
|
|
279
|
+
},
|
|
280
|
+
get running() {
|
|
281
|
+
return running;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
323
284
|
}
|
|
324
|
-
|
|
325
|
-
|
|
285
|
+
|
|
286
|
+
// src/status.ts
|
|
287
|
+
var PACKAGE_VERSION = "0.2.0";
|
|
288
|
+
function buildForgeSurfaceStatus(opts) {
|
|
289
|
+
const fulfillPath = opts.fulfillPath.replace(/\/$/, "") || "/forge/fulfill";
|
|
290
|
+
const ready = opts.fulfillMounted && opts.actions.length > 0;
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
forge: true,
|
|
294
|
+
version: PACKAGE_VERSION,
|
|
326
295
|
handle: opts.handle,
|
|
327
296
|
publicBaseUrl: opts.publicBaseUrl,
|
|
328
297
|
gatewayUrl: opts.gatewayUrl,
|
|
329
298
|
fulfillMounted: opts.fulfillMounted,
|
|
330
|
-
fulfillPath
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
299
|
+
fulfillPath,
|
|
300
|
+
actions: opts.actions,
|
|
301
|
+
ready,
|
|
302
|
+
endpoints: {
|
|
303
|
+
health: "/forge/health",
|
|
304
|
+
status: "/forge/status",
|
|
305
|
+
wellKnown: "/.well-known/clawcash.json",
|
|
306
|
+
fulfill: fulfillPath
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function buildWellKnownDocument(status) {
|
|
311
|
+
return {
|
|
312
|
+
forge: true,
|
|
313
|
+
version: status.version,
|
|
314
|
+
health: status.endpoints.health,
|
|
315
|
+
status: status.endpoints.status,
|
|
316
|
+
handle: status.handle,
|
|
317
|
+
ready: status.ready
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function forgeSdkVersion() {
|
|
321
|
+
return PACKAGE_VERSION;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/handler.ts
|
|
325
|
+
function json(body, status = 200) {
|
|
326
|
+
return new Response(JSON.stringify(body), {
|
|
327
|
+
status,
|
|
328
|
+
headers: { "content-type": "application/json" }
|
|
356
329
|
});
|
|
357
|
-
console.log(
|
|
358
|
-
"[forge] liveliness: GET /forge/health, GET /forge/status, GET /.well-known/clawcash.json"
|
|
359
|
-
);
|
|
360
330
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
throw new Error(
|
|
372
|
-
"mountAgentServices: resolved payTo is the zero address. Re-run Forge to provision a wallet."
|
|
373
|
-
);
|
|
374
|
-
}
|
|
331
|
+
function isAuthorized(request, apiKey) {
|
|
332
|
+
if (!apiKey) return false;
|
|
333
|
+
const header = request.headers.get("authorization") ?? "";
|
|
334
|
+
const bearer = header.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() : "";
|
|
335
|
+
const alt = request.headers.get("x-forge-api-key")?.trim() || request.headers.get("x-clawcash-gateway")?.trim() || "";
|
|
336
|
+
return bearer === apiKey || alt === apiKey;
|
|
337
|
+
}
|
|
338
|
+
function createForgeRequestMatcher(services, options = {}) {
|
|
339
|
+
if (!services?.length) {
|
|
340
|
+
throw new Error("createForgeHandler: services must be a non-empty array");
|
|
375
341
|
}
|
|
376
|
-
const basePath = (options.basePath ?? "/agent").replace(/\/$/, "") || "/agent";
|
|
377
342
|
const fulfillPath = (options.fulfillPath ?? "/forge/fulfill").replace(/\/$/, "") || "/forge/fulfill";
|
|
378
|
-
const
|
|
343
|
+
const handle = options.handle?.trim().toLowerCase() || process.env.FORGE_HANDLE?.trim().toLowerCase() || null;
|
|
344
|
+
const gatewayUrl = options.gatewayUrl?.trim() || process.env.GATEWAY_URL?.trim() || "https://gateway.clawca.sh";
|
|
379
345
|
const context = createContext();
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
346
|
+
const getStatus = () => {
|
|
347
|
+
const apiKey = getForgeApiKey(options.apiKey);
|
|
348
|
+
return buildForgeSurfaceStatus({
|
|
349
|
+
handle,
|
|
350
|
+
publicBaseUrl: options.publicBaseUrl ?? null,
|
|
351
|
+
gatewayUrl,
|
|
352
|
+
fulfillMounted: Boolean(apiKey),
|
|
353
|
+
fulfillPath,
|
|
354
|
+
actions: services.map((s) => ({
|
|
355
|
+
name: s.name,
|
|
356
|
+
path: s.path,
|
|
357
|
+
price: s.payment.amount
|
|
358
|
+
}))
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
return async (request) => {
|
|
362
|
+
const url = new URL(request.url);
|
|
363
|
+
const pathname = url.pathname.replace(/\/$/, "") || "/";
|
|
364
|
+
if (request.method === "GET") {
|
|
365
|
+
if (pathname === "/forge/health") {
|
|
366
|
+
const status = getStatus();
|
|
367
|
+
return json({
|
|
368
|
+
ok: true,
|
|
369
|
+
forge: true,
|
|
370
|
+
ready: status.ready,
|
|
371
|
+
handle: status.handle,
|
|
372
|
+
version: status.version,
|
|
373
|
+
actions: status.actions.map((a) => a.name)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (pathname === "/forge/status") {
|
|
377
|
+
const status = getStatus();
|
|
378
|
+
return json(status, status.ready ? 200 : 503);
|
|
379
|
+
}
|
|
380
|
+
if (pathname === "/.well-known/clawcash.json") {
|
|
381
|
+
return json(buildWellKnownDocument(getStatus()));
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
396
384
|
}
|
|
397
|
-
|
|
385
|
+
if (request.method === "POST" && pathname.startsWith(`${fulfillPath}/`)) {
|
|
386
|
+
const actionPath = pathname.slice(fulfillPath.length + 1);
|
|
387
|
+
const service = services.find((s) => s.path === actionPath) ?? null;
|
|
388
|
+
if (!service) {
|
|
389
|
+
return json({ error: `Unknown action ${actionPath}` }, 404);
|
|
390
|
+
}
|
|
391
|
+
const apiKey = getForgeApiKey(options.apiKey);
|
|
392
|
+
if (!isAuthorized(request, apiKey)) {
|
|
393
|
+
return json(
|
|
394
|
+
{
|
|
395
|
+
error: "Unauthorized \u2014 Forge API key required (FORGE_API_KEY from Forge dashboard)"
|
|
396
|
+
},
|
|
397
|
+
401
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
let body;
|
|
401
|
+
try {
|
|
402
|
+
body = await request.json();
|
|
403
|
+
} catch {
|
|
404
|
+
return json({ error: "Request body must be JSON" }, 400);
|
|
405
|
+
}
|
|
406
|
+
const validated = validateInput(body, service.input);
|
|
407
|
+
if (!validated.ok) {
|
|
408
|
+
return json({ error: validated.error }, 400);
|
|
409
|
+
}
|
|
398
410
|
try {
|
|
399
|
-
const validated = validateInput(req.body, service.input);
|
|
400
|
-
if (!validated.ok) {
|
|
401
|
-
res.status(400).json({ error: validated.error });
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
411
|
const output = await service.execute(validated.value, context);
|
|
405
|
-
|
|
412
|
+
return json(output);
|
|
406
413
|
} catch (err) {
|
|
407
414
|
const message = err instanceof Error ? err.message : "Internal error";
|
|
408
|
-
console.error(`[forge] ${service.name} failed:`, err);
|
|
409
|
-
|
|
415
|
+
console.error(`[forge] fulfill ${service.name} failed:`, err);
|
|
416
|
+
return json({ error: message }, 500);
|
|
410
417
|
}
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
if (!options.skipPayment && Object.keys(routes).length > 0) {
|
|
414
|
-
const facilitator = createFacilitatorClient(options);
|
|
415
|
-
const networks = new Set(
|
|
416
|
-
services.map((s) => asNetwork(s.payment.network))
|
|
417
|
-
);
|
|
418
|
-
const resourceServer = new x402ResourceServer(facilitator);
|
|
419
|
-
for (const network of networks) {
|
|
420
|
-
resourceServer.register(network, new ExactEvmScheme());
|
|
421
418
|
}
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
app.use(basePath, router);
|
|
425
|
-
const fulfillMounted = Boolean(
|
|
426
|
-
getForgeApiKey(options.gateway?.registerSecret)
|
|
427
|
-
);
|
|
428
|
-
mountFulfillmentRouter(app, services, options);
|
|
429
|
-
const gatewayUrl = options.gateway?.url?.trim() || process.env.GATEWAY_URL?.trim() || null;
|
|
430
|
-
const handle = options.gateway?.handle?.trim() || process.env.FORGE_HANDLE?.trim() || null;
|
|
431
|
-
const registerSecret = options.gateway?.registerSecret?.trim() || process.env.FORGE_API_KEY?.trim() || process.env.GATEWAY_REGISTER_SECRET?.trim();
|
|
432
|
-
const publicBaseUrl = options.gateway?.publicBaseUrl?.trim() || process.env.PUBLIC_BASE_URL?.trim() || (process.env.RAILWAY_PUBLIC_DOMAIN ? `https://${process.env.RAILWAY_PUBLIC_DOMAIN}` : "") || null;
|
|
433
|
-
mountForgeStatusRoutes(app, services, options, {
|
|
434
|
-
fulfillMounted,
|
|
435
|
-
fulfillPath,
|
|
436
|
-
agentBasePath: basePath,
|
|
437
|
-
handle,
|
|
438
|
-
publicBaseUrl,
|
|
439
|
-
gatewayUrl
|
|
440
|
-
});
|
|
441
|
-
if (gatewayUrl && handle && registerSecret && publicBaseUrl) {
|
|
442
|
-
void heartbeatGateway({
|
|
443
|
-
gatewayUrl,
|
|
444
|
-
handle,
|
|
445
|
-
baseUrl: publicBaseUrl,
|
|
446
|
-
registerSecret
|
|
447
|
-
}).then(() => {
|
|
448
|
-
recordHeartbeatResult({ ok: true });
|
|
449
|
-
}).catch((err) => {
|
|
450
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
451
|
-
recordHeartbeatResult({ ok: false, error: message });
|
|
452
|
-
console.warn("[forge] gateway heartbeat skipped:", err);
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
return router;
|
|
456
|
-
}
|
|
457
|
-
function createForgeRouter(services, options) {
|
|
458
|
-
return (app) => mountAgentServices(app, services, options);
|
|
419
|
+
return null;
|
|
420
|
+
};
|
|
459
421
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const base = meta.baseUrl.replace(/\/$/, "");
|
|
464
|
-
const lines = [
|
|
465
|
-
`---`,
|
|
466
|
-
`name: ${meta.name}`,
|
|
467
|
-
`description: ${meta.description}`,
|
|
468
|
-
`---`,
|
|
469
|
-
``,
|
|
470
|
-
`# ${meta.name}`,
|
|
471
|
-
``,
|
|
472
|
-
meta.description,
|
|
473
|
-
``
|
|
474
|
-
];
|
|
475
|
-
if (meta.homepage) {
|
|
476
|
-
lines.push(`Homepage: ${meta.homepage}`, ``);
|
|
477
|
-
}
|
|
478
|
-
lines.push(
|
|
479
|
-
`## How to use`,
|
|
480
|
-
``,
|
|
481
|
-
`1. Read this skill.`,
|
|
482
|
-
`2. Call the agent endpoint with a JSON body matching the input schema.`,
|
|
483
|
-
`3. If you receive HTTP 402, pay via x402 (USDC) and retry with the payment header.`,
|
|
484
|
-
`4. Use the returned output fields.`,
|
|
485
|
-
``,
|
|
486
|
-
`## Services`,
|
|
487
|
-
``
|
|
488
|
-
);
|
|
489
|
-
for (const service of services) {
|
|
490
|
-
const path = `${base}/agent/${service.path}`;
|
|
491
|
-
const price = service.payment.amount.startsWith("$") ? service.payment.amount : `$${service.payment.amount}`;
|
|
492
|
-
lines.push(
|
|
493
|
-
`### \`${service.name}\``,
|
|
494
|
-
``,
|
|
495
|
-
service.description,
|
|
496
|
-
``,
|
|
497
|
-
`- **Method:** \`POST\``,
|
|
498
|
-
`- **URL:** \`${path}\``,
|
|
499
|
-
`- **Price:** ${price} ${service.payment.currency} via ${service.payment.protocols.join(", ")}`,
|
|
500
|
-
``,
|
|
501
|
-
`**Input**`,
|
|
502
|
-
``,
|
|
503
|
-
"```json",
|
|
504
|
-
JSON.stringify(
|
|
505
|
-
Object.fromEntries(
|
|
506
|
-
Object.entries(service.input).map(([k, t]) => [k, `<${t}>`])
|
|
507
|
-
),
|
|
508
|
-
null,
|
|
509
|
-
2
|
|
510
|
-
),
|
|
511
|
-
"```",
|
|
512
|
-
``,
|
|
513
|
-
`**Output**`,
|
|
514
|
-
``,
|
|
515
|
-
"```json",
|
|
516
|
-
JSON.stringify(
|
|
517
|
-
Object.fromEntries(
|
|
518
|
-
Object.entries(service.output).map(([k, t]) => [k, `<${t}>`])
|
|
519
|
-
),
|
|
520
|
-
null,
|
|
521
|
-
2
|
|
522
|
-
),
|
|
523
|
-
"```",
|
|
524
|
-
``,
|
|
525
|
-
`**Example**`,
|
|
526
|
-
``,
|
|
527
|
-
"```bash",
|
|
528
|
-
`curl -X POST ${path} \\`,
|
|
529
|
-
` -H "Content-Type: application/json" \\`,
|
|
530
|
-
` -d '${JSON.stringify(
|
|
531
|
-
Object.fromEntries(
|
|
532
|
-
Object.entries(service.input).map(([k, t]) => [
|
|
533
|
-
k,
|
|
534
|
-
t === "url" ? "https://example.com/image.png" : `example-${k}`
|
|
535
|
-
])
|
|
536
|
-
)
|
|
537
|
-
)}'`,
|
|
538
|
-
"```",
|
|
539
|
-
``
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
return lines.join("\n");
|
|
422
|
+
function createForgeHandler(services, options = {}) {
|
|
423
|
+
const match = createForgeRequestMatcher(services, options);
|
|
424
|
+
return async (request) => await match(request) ?? json({ error: "Not found" }, 404);
|
|
543
425
|
}
|
|
544
426
|
export {
|
|
545
427
|
FORGE_GATEWAY_USER_ID,
|
|
546
428
|
buildForgeSurfaceStatus,
|
|
547
429
|
buildWellKnownDocument,
|
|
430
|
+
checkType,
|
|
548
431
|
createContext,
|
|
549
|
-
|
|
432
|
+
createForgeHandler,
|
|
433
|
+
createForgeRequestMatcher,
|
|
550
434
|
defineAgentService,
|
|
551
435
|
forgeGatewayLoopbackHeaders,
|
|
552
436
|
forgeSdkVersion,
|
|
553
|
-
|
|
437
|
+
forgeWorker,
|
|
554
438
|
getForgeApiKey,
|
|
555
|
-
getForgeGatewaySecret,
|
|
556
|
-
getLastHeartbeat,
|
|
557
|
-
heartbeatGateway,
|
|
558
|
-
isForgeGatewayRequest,
|
|
559
|
-
isZeroAddress,
|
|
560
|
-
mountAgentServices,
|
|
561
|
-
recordHeartbeatResult,
|
|
562
|
-
requireForgeGateway,
|
|
563
|
-
resolvePayTo,
|
|
564
439
|
toKebabCase,
|
|
440
|
+
validateInput,
|
|
565
441
|
waitUntil
|
|
566
442
|
};
|
|
567
443
|
//# sourceMappingURL=index.js.map
|