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