@forgeintel/sdk 0.2.0-beta.0 → 0.4.0-beta.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/next.js ADDED
@@ -0,0 +1,30 @@
1
+ import { createForge as createForgeFetch } from "./fetch.js";
2
+ /**
3
+ * Rebuild a forwarded Request as the original's class (NextRequest), so handlers and withX402 still get
4
+ * `nextUrl` and friends. Next isn't imported: the constructor comes from the request itself.
5
+ */
6
+ function sameKind(original, forwarded) {
7
+ const Kind = original.constructor;
8
+ return Kind === Request ? forwarded : new Kind(forwarded);
9
+ }
10
+ export function createForge(options) {
11
+ const forge = createForgeFetch(options);
12
+ const own = async (request) => (await forge.route(request)) ?? new Response(JSON.stringify({ error: "not_found" }), { status: 404, headers: { "Content-Type": "application/json" } });
13
+ return {
14
+ enabled: forge.enabled,
15
+ challengeSentence: forge.challengeSentence,
16
+ enrichOpenApi: forge.enrichOpenApi,
17
+ diagnostics: forge.diagnostics,
18
+ shutdown: forge.shutdown,
19
+ handle: forge.handle,
20
+ withForge: (handler) => (request, context) => forge.handle(request, (forwarded) => handler(forwarded === request ? request : sameKind(request, forwarded), context)),
21
+ routes: { GET: own, POST: own, HEAD: own },
22
+ proxy: (proxy) => async (request) => {
23
+ const response = await proxy(request);
24
+ // Only the 402: the proxy passes paid requests on to the route, where withForge mints the feedback ID.
25
+ if (response.status !== 402 || !forge.enabled)
26
+ return response;
27
+ return forge.handle(request, () => response);
28
+ },
29
+ };
30
+ }
package/dist/openapi.d.ts CHANGED
@@ -16,6 +16,10 @@ export interface EnrichOptions {
16
16
  describeOperations?: boolean;
17
17
  /** Also document the optional rate_this_call body field (when the SDK's rateHint is on). */
18
18
  hintField?: boolean;
19
+ /** Document optional agent context on paid operations: `agent_context` in JSON request bodies, agent_* query parameters otherwise. */
20
+ agentContext?: {
21
+ searchQuery: boolean;
22
+ };
19
23
  }
20
24
  export interface OperationReport {
21
25
  method: string;
@@ -23,6 +27,8 @@ export interface OperationReport {
23
27
  path: string;
24
28
  /** Per 2xx status code: whether feedback_id was added to its JSON schema. */
25
29
  responses: Record<string, ResponseSupport>;
30
+ /** Where optional agent context was documented, if asked to. */
31
+ agentContext?: "body" | "query" | "not_added";
26
32
  reasons: string[];
27
33
  }
28
34
  export interface EnrichReport {
package/dist/openapi.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Additive, idempotent OpenAPI enrichment for Swagger 2.0 and OpenAPI 3.0 / 3.1 / 3.2.
2
2
  // Rules: never mutate the input, never modify a shared $ref target, never override merchant paths,
3
3
  // and on any unexpected input return the original document unchanged.
4
+ import { CONTEXT_FIELD, agentContextParameters, agentContextSchema } from "./context.js";
4
5
  import { FEEDBACK_ID_PATTERN } from "./id.js";
5
6
  import { ISSUES, NOTE_MAX_LENGTH, OUTCOMES, PROTOCOL } from "./values.js";
6
7
  const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
@@ -98,6 +99,58 @@ function extendSchema(doc, schema, props) {
98
99
  copy.properties = { ...(isObj(copy.properties) ? copy.properties : {}), ...clone(props) };
99
100
  return { schema: copy };
100
101
  }
102
+ const BODYLESS = new Set(["get", "head", "delete", "options", "trace"]);
103
+ /** Document optional agent context on one paid operation. Never required; skipped when a schema can't take it safely. */
104
+ function addAgentContext(doc, version, item, method, op, opts, report) {
105
+ const resolve = (v) => (isObj(v) && typeof v.$ref === "string" ? resolveRef(doc, v.$ref) : v);
106
+ const listed = [...(Array.isArray(item.parameters) ? item.parameters : []), ...(Array.isArray(op.parameters) ? op.parameters : [])].map(resolve).filter(isObj);
107
+ const props = { [CONTEXT_FIELD]: agentContextSchema(opts) };
108
+ const notAdded = (reason) => {
109
+ report.agentContext = "not_added";
110
+ report.reasons.push(`request: ${reason}`);
111
+ };
112
+ if (BODYLESS.has(method)) {
113
+ const taken = new Set(listed.filter((p) => p.in === "query").map((p) => p.name));
114
+ const params = agentContextParameters(opts).filter((p) => !taken.has(p.name));
115
+ if (!params.length)
116
+ return notAdded("parameter_collision");
117
+ const documented = params.map((p) => version === "2.0"
118
+ ? { name: p.name, in: "query", required: false, description: p.description, ...p.schema }
119
+ : { name: p.name, in: "query", required: false, description: p.description, schema: p.schema });
120
+ op.parameters = [...(Array.isArray(op.parameters) ? op.parameters : []), ...documented];
121
+ report.agentContext = "query";
122
+ return;
123
+ }
124
+ if (version === "2.0") {
125
+ const params = Array.isArray(op.parameters) ? op.parameters : [];
126
+ const index = params.findIndex((p) => resolve(p)?.in === "body");
127
+ if (index < 0)
128
+ return notAdded("no_request_body");
129
+ const param = clone(resolve(params[index]));
130
+ const result = extendSchema(doc, param.schema, props);
131
+ if ("reason" in result)
132
+ return notAdded(result.reason);
133
+ param.schema = result.schema;
134
+ op.parameters = params.map((p, i) => (i === index ? param : p)); // inline copy if it was a shared $ref
135
+ report.agentContext = "body";
136
+ return;
137
+ }
138
+ const body = resolve(op.requestBody);
139
+ if (!isObj(body) || !isObj(body.content))
140
+ return notAdded("no_request_body");
141
+ const copy = clone(body);
142
+ const media = Object.entries(copy.content).filter(([type, entry]) => isJsonMedia(type) && isObj(entry) && "schema" in entry);
143
+ if (!media.length)
144
+ return notAdded("no_json_request_body");
145
+ for (const [, entry] of media) {
146
+ const result = extendSchema(doc, entry.schema, props);
147
+ if ("reason" in result)
148
+ return notAdded(result.reason);
149
+ entry.schema = result.schema;
150
+ }
151
+ op.requestBody = copy; // inline copy if it was a shared $ref
152
+ report.agentContext = "body";
153
+ }
101
154
  const isJsonMedia = (type) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(type);
102
155
  const is2xx = (code) => /^2(\d\d|XX)$/i.test(code);
103
156
  function appendSentence(text, sentence, marker) {
@@ -280,6 +333,8 @@ export function enrichOpenApi(input, options) {
280
333
  op.responses[code] = response; // inline copy if it was a shared $ref
281
334
  opReport.responses[code] = "extended";
282
335
  }
336
+ if (options.agentContext)
337
+ addAgentContext(doc, version, item, method, op, options.agentContext, opReport);
283
338
  // agentcash-style payment metadata carries its own output schema.
284
339
  const info = op["x-payment-info"];
285
340
  if (isObj(info) && isObj(info.outputSchema)) {
@@ -1,8 +1,10 @@
1
+ import type { AgentContext } from "./context.js";
1
2
  export type ForgeEvent = {
2
3
  type: "challenge";
3
4
  route: string;
4
5
  ts: number;
5
6
  user_agent?: string;
7
+ agent_context?: AgentContext;
6
8
  } | {
7
9
  type: "interaction";
8
10
  feedback_id: string;
@@ -12,7 +14,12 @@ export type ForgeEvent = {
12
14
  payer?: string;
13
15
  network?: string;
14
16
  amount?: string;
17
+ scheme?: string;
18
+ asset?: string;
19
+ /** Settlement transaction reference from the receipt. */
20
+ transaction?: string;
15
21
  user_agent?: string;
22
+ agent_context?: AgentContext;
16
23
  ts: number;
17
24
  };
18
25
  /** Batches events to the backend in the background. Drops the oldest events if the backend stays down. */
package/dist/x402.d.ts CHANGED
@@ -1,19 +1,39 @@
1
- /** Key of the Forge extension in an x402 v2 challenge's `extensions`, next to e.g. `bazaar`. */
1
+ import { type Tone } from "./ask.js";
2
+ /** Key of the Forge extension in x402 v2 `extensions`: in the 402 challenge (next to e.g. `bazaar`) and in the payment receipt. */
2
3
  export declare const FEEDBACK_EXTENSION = "forge-feedback";
3
4
  /**
4
5
  * The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
5
6
  * the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
7
+ * With `agentContext`, it also says how to report agent context with the paid request.
6
8
  */
7
- export declare function feedbackExtension(rateUrl: string): {
9
+ export declare function feedbackExtension(rateUrl: string, tone?: Tone, agentContext?: string): {
8
10
  info: {
11
+ agent_context?: string | undefined;
12
+ rate: string;
13
+ outcome: string[];
14
+ feedback_id: string;
15
+ payment: string;
9
16
  protocol: string;
10
17
  ask: string;
18
+ };
19
+ };
20
+ /** The receipt extension: the same ask, with the real feedback ID, in the settlement response after payment. */
21
+ export declare function receiptExtension(rateUrl: string, feedbackId: string, tone?: Tone): {
22
+ info: {
23
+ protocol: string;
24
+ ask: string;
25
+ feedback_id: string;
11
26
  rate: string;
12
27
  outcome: string[];
13
- feedback_id: string;
14
28
  payment: string;
15
29
  };
16
30
  };
31
+ /**
32
+ * Add the receipt extension to a base64 PAYMENT-RESPONSE header (x402 v2 settlement response), only when
33
+ * settlement succeeded. The receipt is informational for the client: facilitator fields are untouched.
34
+ * Returns undefined when the header can't be parsed, settlement failed, or the extension is already there.
35
+ */
36
+ export declare function describeReceipt(headerValue: string, extension: unknown): string | undefined;
17
37
  /** What the SDK adds to a challenge. Each part is skipped when already present. */
18
38
  export interface ChallengeAdditions {
19
39
  /** Appended to the description (v2 resource.description, v1 accepts[].description). */
@@ -40,6 +60,12 @@ export interface PaymentFacts {
40
60
  payer?: string;
41
61
  network?: string;
42
62
  amount?: string;
63
+ scheme?: string;
64
+ asset?: string;
65
+ /** Settlement transaction reference, from the receipt (see readReceipt). */
66
+ transaction?: string;
43
67
  }
44
- /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
68
+ /** Best-effort read of the payer, network, amount, scheme and asset from an x402 v1/v2 payment header. Never throws. */
45
69
  export declare function readPaymentHeader(headerValue: string): PaymentFacts;
70
+ /** Best-effort read of a successful settlement receipt (base64 PAYMENT-RESPONSE): transaction, payer, network. Never throws. */
71
+ export declare function readReceipt(headerValue: string): Pick<PaymentFacts, "transaction" | "payer" | "network">;
package/dist/x402.js CHANGED
@@ -1,22 +1,60 @@
1
+ import { ASK } from "./ask.js";
1
2
  import { PROTOCOL } from "./values.js";
2
- /** Key of the Forge extension in an x402 v2 challenge's `extensions`, next to e.g. `bazaar`. */
3
+ /** Key of the Forge extension in x402 v2 `extensions`: in the 402 challenge (next to e.g. `bazaar`) and in the payment receipt. */
3
4
  export const FEEDBACK_EXTENSION = "forge-feedback";
4
5
  /**
5
6
  * The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
6
7
  * the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
8
+ * With `agentContext`, it also says how to report agent context with the paid request.
7
9
  */
8
- export function feedbackExtension(rateUrl) {
10
+ export function feedbackExtension(rateUrl, tone = "soft", agentContext) {
9
11
  return {
10
12
  info: {
11
13
  protocol: PROTOCOL,
12
- ask: "After your call, please rate this service for other agents. Free, one request.",
14
+ ask: ASK[tone].extensionAsk,
15
+ ...ASK[tone].extensionExtras,
13
16
  rate: `GET ${rateUrl}?feedback_id=FEEDBACK_ID&outcome=fully`,
14
17
  outcome: ["fully", "partially", "no"],
15
18
  feedback_id: "In the paid response body (feedback_id) and the Forge-Feedback-Id header.",
16
19
  payment: "None. Plain GET, not an x402 endpoint.",
20
+ ...(agentContext ? { agent_context: agentContext } : {}),
17
21
  },
18
22
  };
19
23
  }
24
+ /** The receipt extension: the same ask, with the real feedback ID, in the settlement response after payment. */
25
+ export function receiptExtension(rateUrl, feedbackId, tone = "soft") {
26
+ return {
27
+ info: {
28
+ protocol: PROTOCOL,
29
+ ask: ASK[tone].extensionAsk,
30
+ feedback_id: feedbackId,
31
+ rate: `GET ${rateUrl}?feedback_id=${feedbackId}&outcome=fully`,
32
+ outcome: ["fully", "partially", "no"],
33
+ payment: "None. Plain GET, not an x402 endpoint.",
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * Add the receipt extension to a base64 PAYMENT-RESPONSE header (x402 v2 settlement response), only when
39
+ * settlement succeeded. The receipt is informational for the client: facilitator fields are untouched.
40
+ * Returns undefined when the header can't be parsed, settlement failed, or the extension is already there.
41
+ */
42
+ export function describeReceipt(headerValue, extension) {
43
+ let receipt;
44
+ try {
45
+ receipt = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ if (!receipt || typeof receipt !== "object" || receipt.success !== true)
51
+ return undefined;
52
+ const extensions = receipt.extensions;
53
+ if (extensions !== undefined && extensions !== null && (typeof extensions !== "object" || Array.isArray(extensions) || FEEDBACK_EXTENSION in extensions))
54
+ return undefined;
55
+ receipt.extensions = { ...extensions, [FEEDBACK_EXTENSION]: extension };
56
+ return Buffer.from(JSON.stringify(receipt), "utf8").toString("base64");
57
+ }
20
58
  function appendSentence(description, sentence, marker) {
21
59
  const current = typeof description === "string" ? description.trim() : "";
22
60
  return current.includes(marker) ? current : current ? `${current} ${sentence}` : sentence;
@@ -91,7 +129,7 @@ export function describeChallengeBody(body, additions) {
91
129
  }
92
130
  return undefined;
93
131
  }
94
- /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
132
+ /** Best-effort read of the payer, network, amount, scheme and asset from an x402 v1/v2 payment header. Never throws. */
95
133
  export function readPaymentHeader(headerValue) {
96
134
  try {
97
135
  const p = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
@@ -100,6 +138,8 @@ export function readPaymentHeader(headerValue) {
100
138
  payer: authorization?.from,
101
139
  network: p?.accepted?.network ?? p?.network,
102
140
  amount: p?.accepted?.amount ?? authorization?.value,
141
+ scheme: p?.accepted?.scheme ?? p?.scheme,
142
+ asset: p?.accepted?.asset,
103
143
  };
104
144
  for (const key of Object.keys(facts)) {
105
145
  if (typeof facts[key] !== "string")
@@ -111,3 +151,20 @@ export function readPaymentHeader(headerValue) {
111
151
  return {};
112
152
  }
113
153
  }
154
+ /** Best-effort read of a successful settlement receipt (base64 PAYMENT-RESPONSE): transaction, payer, network. Never throws. */
155
+ export function readReceipt(headerValue) {
156
+ try {
157
+ const r = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
158
+ if (r?.success !== true)
159
+ return {};
160
+ const facts = { transaction: r.transaction, payer: r.payer, network: r.network };
161
+ for (const key of Object.keys(facts)) {
162
+ if (typeof facts[key] !== "string" || !facts[key])
163
+ delete facts[key];
164
+ }
165
+ return facts;
166
+ }
167
+ catch {
168
+ return {};
169
+ }
170
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forgeintel/sdk",
3
- "version": "0.2.0-beta.0",
4
- "description": "The Forge SDK for x402 paid APIs: agent feedback (feedback IDs, one-request GET ratings), OpenAPI and challenge enrichment, and passive call signals. Express adapter plus a framework-free core. Never on your critical path.",
3
+ "version": "0.4.0-beta.0",
4
+ "description": "The Forge SDK for x402 paid APIs: agent feedback (feedback IDs, one-request GET ratings), agent context, OpenAPI and challenge enrichment, and passive call signals. Express, Hono, Next.js and any fetch handler. Never on your critical path.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
@@ -23,6 +23,21 @@
23
23
  "import": "./dist/core.js",
24
24
  "default": "./dist/core.js"
25
25
  },
26
+ "./fetch": {
27
+ "types": "./dist/fetch.d.ts",
28
+ "import": "./dist/fetch.js",
29
+ "default": "./dist/fetch.js"
30
+ },
31
+ "./hono": {
32
+ "types": "./dist/hono.d.ts",
33
+ "import": "./dist/hono.js",
34
+ "default": "./dist/hono.js"
35
+ },
36
+ "./next": {
37
+ "types": "./dist/next.d.ts",
38
+ "import": "./dist/next.js",
39
+ "default": "./dist/next.js"
40
+ },
26
41
  "./package.json": "./package.json"
27
42
  },
28
43
  "types": "./dist/index.d.ts",
@@ -40,7 +55,11 @@
40
55
  "openapi",
41
56
  "express",
42
57
  "payments",
43
- "forge"
58
+ "forge",
59
+ "hono",
60
+ "nextjs",
61
+ "cloudflare-workers",
62
+ "bun"
44
63
  ],
45
64
  "repository": {
46
65
  "type": "git",
@@ -57,25 +76,24 @@
57
76
  "tag": "beta"
58
77
  },
59
78
  "peerDependencies": {
60
- "express": ">=4.21 <6"
79
+ "express": ">=4.21 <6",
80
+ "hono": ">=4"
61
81
  },
62
82
  "peerDependenciesMeta": {
63
83
  "express": {
64
84
  "optional": true
85
+ },
86
+ "hono": {
87
+ "optional": true
65
88
  }
66
89
  },
67
90
  "devDependencies": {
68
91
  "@apidevtools/swagger-parser": "^12.1.0",
69
92
  "@types/express": "^5.0.3",
70
93
  "@types/node": "^22.18.0",
71
- "@x402/core": "~2.25.0",
72
- "@x402/evm": "~2.25.0",
73
- "@x402/express": "~2.25.0",
74
94
  "ajv": "^8.20.0",
75
95
  "ajv-formats": "^3.0.1",
76
- "express": "^5.2.1",
77
- "typescript": "^5.9.2",
78
- "viem": "^2.56.3",
79
- "x402-express": "^1.2.0"
96
+ "hono": "^4.13.7",
97
+ "typescript": "^5.9.2"
80
98
  }
81
99
  }