@forgeintel/sdk 0.4.0-beta.1 → 0.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/client-signals.d.ts +4 -0
- package/dist/client-signals.js +45 -0
- package/dist/context.d.ts +3 -8
- package/dist/context.js +15 -10
- package/dist/core.d.ts +5 -3
- package/dist/core.js +31 -18
- package/dist/index.d.ts +1 -0
- package/dist/openapi.d.ts +2 -0
- package/dist/openapi.js +9 -0
- package/dist/reporter.d.ts +12 -1
- package/dist/x402.d.ts +2 -0
- package/dist/x402.js +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,13 +86,14 @@ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared compone
|
|
|
86
86
|
| `apiKey` | required | Merchant key. Also the HMAC key for IDs. |
|
|
87
87
|
| `backendUrl` | required | Forge backend origin. |
|
|
88
88
|
| `publicUrl` | required | This service's public origin. Used in rating URLs. |
|
|
89
|
+
| `feedback` | `true` | Master switch for rating prompts, IDs, response additions and feedback routes. `false` leaves telemetry, client headers and independent agent context on. |
|
|
89
90
|
| `basePath` | `/feedback` | Feedback routes (`/feedback`, `/feedback/rate`, `/feedback/summary`). |
|
|
90
91
|
| `tone` | `"soft"` | `"lifecycle"` describes this service's flow as four steps (402, pay, response, rate). Both stay soft asks. |
|
|
91
92
|
| `describeChallenges` | `true` | Append the sentence to 402 challenges. |
|
|
92
93
|
| `challengeSentence` | built-in | Override for wording experiments. `{rate_url}` and `{summary_url}` are substituted. Wording that presents the rating as required, makes anything depend on it, or asks for user data is refused (with a warning). |
|
|
93
94
|
| `challengeExtension` | `true` | Add the `forge-feedback` extension to x402 v2 challenges. |
|
|
94
95
|
| `receiptExtension` | `true` | Add it, with the real `feedback_id`, to the x402 v2 payment receipt (`PAYMENT-RESPONSE`). |
|
|
95
|
-
| `agentContext` | `true` | Ask for self-reported `agent_context` (agent
|
|
96
|
+
| `agentContext` | `true` | Ask for self-reported `agent_context` (agent name, search query), record it, and remove it before your code runs. `{ searchQuery: false }` or `false` to limit. |
|
|
96
97
|
| `injectBody` | `true` | Add `feedback_id` / `feedback_url` to paid JSON bodies. |
|
|
97
98
|
| `rateHint` | built-in | The `rate_this_call` field. A string overrides it (`{feedback_url}` is substituted); `false` removes it. |
|
|
98
99
|
| `injectText` | `false` | Append a two-line trailer to paid `text/plain` bodies. |
|
|
@@ -105,3 +106,5 @@ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared compone
|
|
|
105
106
|
`forge.diagnostics()` returns counters and the last OpenAPI report. `forge.enrichOpenApi(doc)` is available for build-time use. Call `await forge.shutdown()` in your shutdown handler to flush pending events.
|
|
106
107
|
|
|
107
108
|
Source, examples and design notes: [github.com/ClawCash/forge-feedback](https://github.com/ClawCash/forge-feedback).
|
|
109
|
+
|
|
110
|
+
Client headers are captured automatically at discovery, challenge, payment, and rating stages, including unknown clients. Forge identifies awal, AgentCash, pay.sh and x402scan-mcp from known headers; other clients remain unknown. There is no client-signals switch. Legacy `agent_context.client` inputs remain accepted but are no longer requested.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
declare const HEADERS: readonly ["user-agent", "x-client-id", "x-session-id", "x-wallet-address", "x-solana-wallet-address", "referer", "access-control-expose-headers", "payment-retry", "cf-worker", "signature-agent"];
|
|
2
|
+
export type ClientHeaders = Partial<Record<(typeof HEADERS)[number] | "sentry-release", string>>;
|
|
3
|
+
export declare function captureClientHeaders(header: (name: string) => string | undefined): ClientHeaders | undefined;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Attribution hints only. Iterate named headers, never enumerate or serialize a request's headers.
|
|
2
|
+
const HEADERS = [
|
|
3
|
+
"user-agent", "x-client-id", "x-session-id", "x-wallet-address", "x-solana-wallet-address",
|
|
4
|
+
"referer", "access-control-expose-headers", "payment-retry", "cf-worker", "signature-agent",
|
|
5
|
+
];
|
|
6
|
+
const VALUE_LIMIT = 512;
|
|
7
|
+
const BAGGAGE_LIMIT = 8192;
|
|
8
|
+
export function captureClientHeaders(header) {
|
|
9
|
+
const captured = {};
|
|
10
|
+
const read = (name) => {
|
|
11
|
+
try {
|
|
12
|
+
return header(name);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const clean = (value) => value.slice(0, VALUE_LIMIT).replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
19
|
+
for (const name of HEADERS) {
|
|
20
|
+
const raw = read(name);
|
|
21
|
+
if (typeof raw !== "string")
|
|
22
|
+
continue;
|
|
23
|
+
const value = clean(raw);
|
|
24
|
+
if (value)
|
|
25
|
+
captured[name] = value;
|
|
26
|
+
}
|
|
27
|
+
// Baggage may contain unrelated tracing data. Keep only the exact Sentry release member,
|
|
28
|
+
// excluding its optional metadata. Oversized baggage is dropped, never partially parsed.
|
|
29
|
+
const baggage = read("baggage");
|
|
30
|
+
if (typeof baggage === "string" && baggage.length <= BAGGAGE_LIMIT) {
|
|
31
|
+
for (const member of baggage.split(",")) {
|
|
32
|
+
const eq = member.indexOf("=");
|
|
33
|
+
if (eq < 0 || member.slice(0, eq).trim() !== "sentry-release")
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
const value = clean(decodeURIComponent(member.slice(eq + 1).split(";", 1)[0].trim()));
|
|
37
|
+
if (value)
|
|
38
|
+
captured["sentry-release"] = value;
|
|
39
|
+
}
|
|
40
|
+
catch { /* Malformed encoding is not a usable release. */ }
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Object.keys(captured).length ? captured : undefined;
|
|
45
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Suggested agent names; anything else is kept as given. */
|
|
2
|
-
export declare const AGENT_TYPES: readonly ["Claude Code", "Codex", "Cursor", "Grok", "
|
|
2
|
+
export declare const AGENT_TYPES: readonly ["Claude Code", "Codex", "Cursor", "Grok Bot", "Muse", "Hermes", "Instinct", "OpenClaw", "Others"];
|
|
3
3
|
/** Suggested x402 clients; anything else is kept as given. */
|
|
4
4
|
export declare const CLIENTS: readonly ["agentcash", "awal", "pay.sh", "other", "unknown"];
|
|
5
5
|
/** JSON body field (POST, PUT, …). */
|
|
@@ -45,7 +45,7 @@ export declare function agentContextSchema({ searchQuery }?: {
|
|
|
45
45
|
} | undefined;
|
|
46
46
|
agent_type: {
|
|
47
47
|
type: string;
|
|
48
|
-
enum: ("Claude Code" | "Codex" | "Cursor" | "Grok" | "
|
|
48
|
+
enum: ("Claude Code" | "Codex" | "Cursor" | "Grok Bot" | "Muse" | "Hermes" | "Instinct" | "OpenClaw" | "Others")[];
|
|
49
49
|
description: string;
|
|
50
50
|
};
|
|
51
51
|
agent_type_other: {
|
|
@@ -53,11 +53,6 @@ export declare function agentContextSchema({ searchQuery }?: {
|
|
|
53
53
|
maxLength: number;
|
|
54
54
|
description: string;
|
|
55
55
|
};
|
|
56
|
-
client: {
|
|
57
|
-
type: string;
|
|
58
|
-
enum: ("agentcash" | "awal" | "pay.sh" | "other" | "unknown")[];
|
|
59
|
-
description: string;
|
|
60
|
-
};
|
|
61
56
|
};
|
|
62
57
|
};
|
|
63
58
|
/** The optional query parameters, for operations without a request body. */
|
|
@@ -71,4 +66,4 @@ export declare function agentContextParameters({ searchQuery }?: {
|
|
|
71
66
|
/** One line for the forge-feedback extension, so clients that only inspect the 402 learn about it too. */
|
|
72
67
|
export declare function agentContextAsk({ searchQuery }?: {
|
|
73
68
|
searchQuery?: boolean;
|
|
74
|
-
}): "Please add agent_context {agent_type,
|
|
69
|
+
}): "Please add agent_context {agent_type, search_query} to your paid request's JSON body (without a body: agent_type, agent_search_query query parameters). search_query: share the query you used to find this service, or \"direct\" if you did not search." | "Please add agent_context {agent_type} to your paid request's JSON body (without a body: agent_type query parameter). Self-reported; omit what you don't know.";
|
package/dist/context.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// API contract never changes. Never required: a required field breaks strict validators and makes
|
|
4
4
|
// agents invent answers.
|
|
5
5
|
/** Suggested agent names; anything else is kept as given. */
|
|
6
|
-
export const AGENT_TYPES = ["Claude Code", "Codex", "Cursor", "Grok", "
|
|
6
|
+
export const AGENT_TYPES = ["Claude Code", "Codex", "Cursor", "Grok Bot", "Muse", "Hermes", "Instinct", "OpenClaw", "Others"];
|
|
7
7
|
/** Suggested x402 clients; anything else is kept as given. */
|
|
8
8
|
export const CLIENTS = ["agentcash", "awal", "pay.sh", "other", "unknown"];
|
|
9
9
|
/** JSON body field (POST, PUT, …). */
|
|
@@ -28,9 +28,17 @@ export function parseAgentContext(input, { searchQuery = true } = {}) {
|
|
|
28
28
|
const raw = Array.isArray(input[key]) ? input[key][0] : input[key];
|
|
29
29
|
if (typeof raw !== "string")
|
|
30
30
|
continue;
|
|
31
|
-
|
|
31
|
+
let text = raw.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, LIMITS[key]);
|
|
32
32
|
if (!text)
|
|
33
33
|
continue;
|
|
34
|
+
if (key === "agent_type") {
|
|
35
|
+
if (/^grok(?: bot)?$/i.test(text))
|
|
36
|
+
text = "Grok Bot";
|
|
37
|
+
if (/^(?:others?(?: \(specify\))?|unknown)$/i.test(text))
|
|
38
|
+
text = "Others";
|
|
39
|
+
}
|
|
40
|
+
if (key === "search_query" && /^direct$/i.test(text))
|
|
41
|
+
text = "direct";
|
|
34
42
|
const known = key === "agent_type" ? AGENT_TYPES : key === "client" ? CLIENTS : null;
|
|
35
43
|
context[key] = known?.find((v) => v.toLowerCase() === text.toLowerCase()) ?? text;
|
|
36
44
|
}
|
|
@@ -80,10 +88,9 @@ export function takeFromUrl(url) {
|
|
|
80
88
|
}
|
|
81
89
|
const DESCRIPTIONS = {
|
|
82
90
|
object: "Self-reported context about the calling agent, so the merchant can see which agents use this service. Not needed for the result; omit anything you don't know.",
|
|
83
|
-
agent_type: "Which agent are you?
|
|
84
|
-
agent_type_other: "Your agent's name, when agent_type is
|
|
85
|
-
|
|
86
|
-
search_query: "The search terms you used to find this service, for example in a marketplace or search tool. Omit if you didn't search; don't invent one, and never include your user's request.",
|
|
91
|
+
agent_type: "Which agent are you? Choose Others if yours is not listed and share its name in agent_type_other.",
|
|
92
|
+
agent_type_other: "Your agent's name, when agent_type is Others.",
|
|
93
|
+
search_query: "The search query you used to find this service, or \"direct\" if you did not search.",
|
|
87
94
|
};
|
|
88
95
|
/** JSON Schema for the optional `agent_context` body property. */
|
|
89
96
|
export function agentContextSchema({ searchQuery = true } = {}) {
|
|
@@ -93,7 +100,6 @@ export function agentContextSchema({ searchQuery = true } = {}) {
|
|
|
93
100
|
properties: {
|
|
94
101
|
agent_type: { type: "string", enum: [...AGENT_TYPES], description: DESCRIPTIONS.agent_type },
|
|
95
102
|
agent_type_other: { type: "string", maxLength: LIMITS.agent_type_other, description: DESCRIPTIONS.agent_type_other },
|
|
96
|
-
client: { type: "string", enum: [...CLIENTS], description: DESCRIPTIONS.client },
|
|
97
103
|
...(searchQuery ? { search_query: { type: "string", maxLength: LIMITS.search_query, description: DESCRIPTIONS.search_query } } : {}),
|
|
98
104
|
},
|
|
99
105
|
};
|
|
@@ -103,7 +109,6 @@ export function agentContextParameters({ searchQuery = true } = {}) {
|
|
|
103
109
|
const params = [
|
|
104
110
|
{ name: CONTEXT_QUERY.agent_type, description: DESCRIPTIONS.agent_type, schema: { type: "string", enum: [...AGENT_TYPES] } },
|
|
105
111
|
{ name: CONTEXT_QUERY.agent_type_other, description: DESCRIPTIONS.agent_type_other, schema: { type: "string", maxLength: LIMITS.agent_type_other } },
|
|
106
|
-
{ name: CONTEXT_QUERY.client, description: DESCRIPTIONS.client, schema: { type: "string", enum: [...CLIENTS] } },
|
|
107
112
|
];
|
|
108
113
|
if (searchQuery)
|
|
109
114
|
params.push({ name: CONTEXT_QUERY.search_query, description: DESCRIPTIONS.search_query, schema: { type: "string", maxLength: LIMITS.search_query } });
|
|
@@ -112,6 +117,6 @@ export function agentContextParameters({ searchQuery = true } = {}) {
|
|
|
112
117
|
/** One line for the forge-feedback extension, so clients that only inspect the 402 learn about it too. */
|
|
113
118
|
export function agentContextAsk({ searchQuery = true } = {}) {
|
|
114
119
|
return searchQuery
|
|
115
|
-
? "Please add agent_context {agent_type,
|
|
116
|
-
: "Please add agent_context {agent_type
|
|
120
|
+
? "Please add agent_context {agent_type, search_query} to your paid request's JSON body (without a body: agent_type, agent_search_query query parameters). search_query: share the query you used to find this service, or \"direct\" if you did not search."
|
|
121
|
+
: "Please add agent_context {agent_type} to your paid request's JSON body (without a body: agent_type query parameter). Self-reported; omit what you don't know.";
|
|
117
122
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ForgeOptions {
|
|
|
22
22
|
backendUrl: string;
|
|
23
23
|
/** This service's public origin, e.g. https://pixels.gateway.clawca.sh. Never derived from the Host header. */
|
|
24
24
|
publicUrl: string;
|
|
25
|
+
/** Enable rating prompts, feedback IDs and feedback routes. Default true. Telemetry and agent context are independent. */
|
|
26
|
+
feedback?: boolean;
|
|
25
27
|
/** Path for the feedback routes. Default "/feedback" (quick rating at "/feedback/rate"). */
|
|
26
28
|
basePath?: string;
|
|
27
29
|
/**
|
|
@@ -48,8 +50,8 @@ export interface ForgeOptions {
|
|
|
48
50
|
*/
|
|
49
51
|
receiptExtension?: boolean;
|
|
50
52
|
/**
|
|
51
|
-
* Agent context: optional, self-reported `agent_context` {agent_type,
|
|
52
|
-
* (JSON body, or agent_type /
|
|
53
|
+
* Agent context: optional, self-reported `agent_context` {agent_type, search_query} on the paid request
|
|
54
|
+
* (JSON body, or agent_type / agent_search_query query parameters). Forge documents it in the
|
|
53
55
|
* extension and OpenAPI, reads it, reports it with the call, and removes it before your validators and handlers
|
|
54
56
|
* run. Default true. `{ searchQuery: false }` stops asking for (and recording) the search query; `false` stops
|
|
55
57
|
* asking and recording altogether (the fields are still removed if an agent sends them).
|
|
@@ -145,7 +147,7 @@ export interface ForgeCore {
|
|
|
145
147
|
readonly enabled: boolean;
|
|
146
148
|
/** The sentence appended to x402 challenges and OpenAPI guidance. */
|
|
147
149
|
readonly challengeSentence: string;
|
|
148
|
-
/**
|
|
150
|
+
/** Call once per request: records OpenAPI discovery and handles Forge's own routes. Null when not ours. */
|
|
149
151
|
route(request: ForgeRequest): Promise<ForgeResponse | null>;
|
|
150
152
|
/** Whether this is a GET for the merchant's own OpenAPI route, which the adapter should buffer and pass to enrichOpenApi. */
|
|
151
153
|
isSpecRequest(method: string, path: string): boolean;
|
package/dist/core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
// Framework-free Forge logic. Adapters (express.ts, later fetch.ts) translate their request/response
|
|
2
3
|
// objects to the small interfaces below and write out what the core returns.
|
|
3
4
|
import { DEFAULT_TTL_MS, deriveSigningKey, mintFeedbackId, verifyFeedbackId } from "./id.js";
|
|
@@ -5,6 +6,7 @@ import { createOperationIndex, enrichOpenApi } from "./openapi.js";
|
|
|
5
6
|
import { ASK, TONES, checkAskText } from "./ask.js";
|
|
6
7
|
import { agentContextAsk, parseAgentContext, takeFromBody, takeFromUrl } from "./context.js";
|
|
7
8
|
import { EventReporter } from "./reporter.js";
|
|
9
|
+
import { captureClientHeaders } from "./client-signals.js";
|
|
8
10
|
import { ISSUES, NOTE_MAX_LENGTH, OUTCOMES, PROTOCOL, parseSubmission } from "./values.js";
|
|
9
11
|
import { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, readPaymentHeader, readReceipt, receiptExtension } from "./x402.js";
|
|
10
12
|
export const FEEDBACK_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
|
|
@@ -60,7 +62,7 @@ export function checkOptions(input) {
|
|
|
60
62
|
fallback("challengeSentence", typeof raw.challengeSentence === "string" && raw.challengeSentence.trim() !== "", "must be a non-empty string");
|
|
61
63
|
fallback("rateHint", boolean(raw.rateHint) || typeof raw.rateHint === "string", "must be a boolean or a string");
|
|
62
64
|
fallback("agentContext", boolean(raw.agentContext) || (!!raw.agentContext && typeof raw.agentContext === "object" && !Array.isArray(raw.agentContext)), "must be true, false or { searchQuery }");
|
|
63
|
-
for (const key of ["describeChallenges", "challengeExtension", "receiptExtension", "injectBody", "injectText", "strict"])
|
|
65
|
+
for (const key of ["feedback", "describeChallenges", "challengeExtension", "receiptExtension", "injectBody", "injectText", "strict"])
|
|
64
66
|
fallback(key, boolean(raw[key]), "must be true or false");
|
|
65
67
|
// The lines no wording may cross, whatever the merchant configures (see ask.ts).
|
|
66
68
|
for (const key of ["challengeSentence", "rateHint"]) {
|
|
@@ -155,7 +157,8 @@ function enabledCore(options, configWarnings) {
|
|
|
155
157
|
const summaryUrl = new URL(summaryPath, publicUrl).href;
|
|
156
158
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
157
159
|
const fetchImpl = options.fetch ?? fetch;
|
|
158
|
-
const
|
|
160
|
+
const feedback = options.feedback !== false;
|
|
161
|
+
const describe = feedback && (options.describeChallenges ?? true);
|
|
159
162
|
const injectBody = options.injectBody ?? true;
|
|
160
163
|
const injectText = options.injectText ?? false;
|
|
161
164
|
const tone = options.tone ?? "soft";
|
|
@@ -186,20 +189,23 @@ function enabledCore(options, configWarnings) {
|
|
|
186
189
|
lastLogged = message;
|
|
187
190
|
});
|
|
188
191
|
const reporter = new EventReporter(backend("events"), apiKey, fetchImpl, onError, options.flushIntervalMs ?? 2000);
|
|
189
|
-
const challengeSentence = (options.challengeSentence ?? ASK[tone].challengeSentence).replaceAll("{rate_url}", rateUrl).replaceAll("{summary_url}", summaryUrl);
|
|
192
|
+
const challengeSentence = feedback ? (options.challengeSentence ?? ASK[tone].challengeSentence).replaceAll("{rate_url}", rateUrl).replaceAll("{summary_url}", summaryUrl) : "";
|
|
190
193
|
// Idempotency marker: the rate URL when the sentence contains it, otherwise the sentence itself.
|
|
191
194
|
const challengeMarker = challengeSentence.includes(rateUrl) ? rateUrl : challengeSentence;
|
|
192
195
|
const challengeAdditions = {
|
|
193
196
|
...(describe ? { sentence: challengeSentence, marker: challengeMarker } : {}),
|
|
194
|
-
...(options.challengeExtension === false ? {} : { extension: feedbackExtension(rateUrl, tone, collectContext ? agentContextAsk({ searchQuery }) : undefined) }),
|
|
197
|
+
...(!feedback || options.challengeExtension === false ? {} : { extension: feedbackExtension(rateUrl, tone, collectContext ? agentContextAsk({ searchQuery }) : undefined) }),
|
|
195
198
|
};
|
|
196
|
-
|
|
199
|
+
if (!feedback && collectContext)
|
|
200
|
+
challengeAdditions.contextExtension = { info: { agent_context: agentContextAsk({ searchQuery }), optional: true } };
|
|
201
|
+
const touchChallenges = Boolean(challengeAdditions.sentence || challengeAdditions.extension || challengeAdditions.contextExtension);
|
|
197
202
|
// Set once a document has been enriched; lets body injection respect strict response schemas.
|
|
198
203
|
let allowInjection = null;
|
|
199
204
|
let lastWarnings = "";
|
|
200
205
|
function enrich(document) {
|
|
201
206
|
const result = enrichOpenApi(document, {
|
|
202
207
|
publicUrl,
|
|
208
|
+
feedback,
|
|
203
209
|
basePath,
|
|
204
210
|
sentence: challengeSentence,
|
|
205
211
|
marker: challengeMarker,
|
|
@@ -258,7 +264,7 @@ function enabledCore(options, configWarnings) {
|
|
|
258
264
|
const upstream = await fetchImpl(backend("feedback"), {
|
|
259
265
|
method: "POST",
|
|
260
266
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
261
|
-
body: JSON.stringify({ ...parsed.value, via, user_agent: request.header("user-agent") ?? null }),
|
|
267
|
+
body: JSON.stringify({ ...parsed.value, via, user_agent: request.header("user-agent") ?? null, request_headers: captureClientHeaders((name) => request.header(name)) }),
|
|
262
268
|
signal: AbortSignal.timeout(3000),
|
|
263
269
|
});
|
|
264
270
|
const body = await upstream.json().catch(() => ({ recorded: false, error: "feedback_unavailable" }));
|
|
@@ -299,7 +305,7 @@ function enabledCore(options, configWarnings) {
|
|
|
299
305
|
}
|
|
300
306
|
async function handleRoute(request) {
|
|
301
307
|
const { method, path } = request;
|
|
302
|
-
if (path === basePath) {
|
|
308
|
+
if (feedback && path === basePath) {
|
|
303
309
|
if (method === "GET" || method === "HEAD")
|
|
304
310
|
return reply(200, form);
|
|
305
311
|
if (method === "POST") {
|
|
@@ -315,18 +321,22 @@ function enabledCore(options, configWarnings) {
|
|
|
315
321
|
return submit(request, body, "POST");
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
|
-
if (path === summaryPath && (method === "GET" || method === "HEAD"))
|
|
324
|
+
if (feedback && path === summaryPath && (method === "GET" || method === "HEAD"))
|
|
319
325
|
return summary();
|
|
320
|
-
if (path === ratePath) {
|
|
326
|
+
if (feedback && path === ratePath) {
|
|
321
327
|
// HEAD and other methods must never record a rating (link checkers send HEAD).
|
|
322
328
|
if (method !== "GET")
|
|
323
329
|
return reply(405, undefined, { ...FEEDBACK_HEADERS, Allow: "GET" });
|
|
324
330
|
const first = (v) => (Array.isArray(v) ? v[0] : v);
|
|
325
331
|
return submit(request, { feedback_id: first(request.query("feedback_id")), outcome: first(request.query("outcome")), issue: first(request.query("issue")) }, "GET");
|
|
326
332
|
}
|
|
327
|
-
if (openapi &&
|
|
328
|
-
const
|
|
329
|
-
|
|
333
|
+
if (openapi && method === "GET" && specPaths.has(path)) {
|
|
334
|
+
const requestHeaders = captureClientHeaders((name) => request.header(name));
|
|
335
|
+
reporter.push({ type: "discovery", route: `${method} ${path}`, ts: Date.now(), user_agent: requestHeaders?.["user-agent"], request_headers: requestHeaders });
|
|
336
|
+
if (openapi.document !== undefined) {
|
|
337
|
+
const source = typeof openapi.document === "function" ? await openapi.document() : openapi.document;
|
|
338
|
+
return reply(200, enrich(source).document, { "Cache-Control": "no-store" });
|
|
339
|
+
}
|
|
330
340
|
}
|
|
331
341
|
return null;
|
|
332
342
|
}
|
|
@@ -352,8 +362,10 @@ function enabledCore(options, configWarnings) {
|
|
|
352
362
|
const started = Date.now();
|
|
353
363
|
const route = `${request.method} ${request.path}`;
|
|
354
364
|
const userAgent = request.header("user-agent") ?? undefined;
|
|
365
|
+
const requestHeaders = captureClientHeaders((name) => request.header(name));
|
|
355
366
|
const paymentHeader = request.header("payment-signature") ?? request.header("x-payment"); // x402 v2 / v1
|
|
356
|
-
const feedbackId = paymentHeader ? mintFeedbackId(signingKey) : undefined;
|
|
367
|
+
const feedbackId = feedback && paymentHeader ? mintFeedbackId(signingKey) : undefined;
|
|
368
|
+
const interactionId = paymentHeader && !feedback ? randomUUID() : undefined;
|
|
357
369
|
if (feedbackId)
|
|
358
370
|
stats.minted++;
|
|
359
371
|
const feedbackUrl = feedbackId ? `${rateUrl}?feedback_id=${feedbackId}&outcome=` : "";
|
|
@@ -424,10 +436,10 @@ function enabledCore(options, configWarnings) {
|
|
|
424
436
|
stats.challengesDescribed++;
|
|
425
437
|
}
|
|
426
438
|
}
|
|
439
|
+
if (paymentHeader && ok(status) && paymentResponse)
|
|
440
|
+
receiptFacts = readReceipt(paymentResponse);
|
|
427
441
|
if (feedbackId && ok(status)) {
|
|
428
442
|
set["Forge-Feedback-Id"] = feedbackId;
|
|
429
|
-
if (paymentResponse)
|
|
430
|
-
receiptFacts = readReceipt(paymentResponse);
|
|
431
443
|
const receipt = receipts && paymentResponse ? describeReceipt(paymentResponse, receiptExtension(rateUrl, feedbackId, tone)) : undefined;
|
|
432
444
|
if (receipt)
|
|
433
445
|
set["PAYMENT-RESPONSE"] = receipt;
|
|
@@ -475,19 +487,20 @@ function enabledCore(options, configWarnings) {
|
|
|
475
487
|
const ts = Date.now();
|
|
476
488
|
if (status === 402 && (hadChallengeHeader || headerChallenge || bodyChallenge)) {
|
|
477
489
|
// v2 carries the challenge in a header; v1 in the body. Both count as a challenge.
|
|
478
|
-
reporter.push({ type: "challenge", route, ts, user_agent: userAgent, ...(context ? { agent_context: context } : {}) });
|
|
490
|
+
reporter.push({ type: "challenge", route, ts, user_agent: userAgent, request_headers: requestHeaders, ...(context ? { agent_context: context } : {}) });
|
|
479
491
|
}
|
|
480
|
-
if (feedbackId) {
|
|
492
|
+
if (feedbackId || interactionId) {
|
|
481
493
|
const facts = { ...readPaymentHeader(paymentHeader), ...receiptFacts };
|
|
482
494
|
reporter.push({
|
|
483
495
|
type: "interaction",
|
|
484
|
-
feedback_id: feedbackId,
|
|
496
|
+
...(feedbackId ? { feedback_id: feedbackId } : { interaction_id: interactionId }),
|
|
485
497
|
route,
|
|
486
498
|
status,
|
|
487
499
|
latency_ms: ts - started,
|
|
488
500
|
// Only report the payer once the call succeeded (i.e. settlement went through).
|
|
489
501
|
...(ok(status) ? facts : { network: facts.network, amount: facts.amount }),
|
|
490
502
|
user_agent: userAgent,
|
|
503
|
+
request_headers: requestHeaders,
|
|
491
504
|
...(context ? { agent_context: context } : {}),
|
|
492
505
|
ts,
|
|
493
506
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/openapi.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ type Json = Record<string, any>;
|
|
|
2
2
|
export type SpecVersion = "2.0" | "3.0" | "3.1" | "3.2";
|
|
3
3
|
export type ResponseSupport = "extended" | "incompatible" | "undocumented";
|
|
4
4
|
export interface EnrichOptions {
|
|
5
|
+
/** Disable all feedback additions while retaining optional agent context. */
|
|
6
|
+
feedback?: boolean;
|
|
5
7
|
/** Merchant public origin, e.g. https://api.example.com */
|
|
6
8
|
publicUrl: string;
|
|
7
9
|
/** Public path of the feedback routes, e.g. /feedback */
|
package/dist/openapi.js
CHANGED
|
@@ -287,6 +287,11 @@ export function enrichOpenApi(input, options) {
|
|
|
287
287
|
continue;
|
|
288
288
|
const opReport = { method: method.toUpperCase(), path, responses: {}, reasons: [] };
|
|
289
289
|
report.operations.push(opReport);
|
|
290
|
+
if (options.feedback === false) {
|
|
291
|
+
if (options.agentContext)
|
|
292
|
+
addAgentContext(doc, version, item, method, op, options.agentContext, opReport);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
290
295
|
if (options.describeOperations ?? true)
|
|
291
296
|
op.description = appendSentence(op.description, options.sentence, options.marker);
|
|
292
297
|
const produces = (op.produces ?? doc.produces);
|
|
@@ -344,6 +349,10 @@ export function enrichOpenApi(input, options) {
|
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
351
|
}
|
|
352
|
+
if (options.feedback === false) {
|
|
353
|
+
report.enriched = Boolean(options.agentContext);
|
|
354
|
+
return { document: doc, report };
|
|
355
|
+
}
|
|
347
356
|
// Feedback routes. Paths are relative to the server prefix when it's this origin;
|
|
348
357
|
// otherwise (3.x) the path items carry their own servers entry.
|
|
349
358
|
const inPrefix = sameOrigin && (base === prefix || base.startsWith(`${prefix}/`));
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import type { AgentContext } from "./context.js";
|
|
2
|
+
import type { ClientHeaders } from "./client-signals.js";
|
|
2
3
|
export type ForgeEvent = {
|
|
4
|
+
type: "discovery";
|
|
5
|
+
route: string;
|
|
6
|
+
ts: number;
|
|
7
|
+
user_agent?: string;
|
|
8
|
+
request_headers?: ClientHeaders;
|
|
9
|
+
} | {
|
|
3
10
|
type: "challenge";
|
|
4
11
|
route: string;
|
|
5
12
|
ts: number;
|
|
6
13
|
user_agent?: string;
|
|
14
|
+
request_headers?: ClientHeaders;
|
|
7
15
|
agent_context?: AgentContext;
|
|
8
16
|
} | {
|
|
9
17
|
type: "interaction";
|
|
10
|
-
feedback_id
|
|
18
|
+
feedback_id?: string;
|
|
19
|
+
/** Independent telemetry identifier when feedback is disabled. */
|
|
20
|
+
interaction_id?: string;
|
|
11
21
|
route: string;
|
|
12
22
|
status: number;
|
|
13
23
|
latency_ms: number;
|
|
@@ -19,6 +29,7 @@ export type ForgeEvent = {
|
|
|
19
29
|
/** Settlement transaction reference from the receipt. */
|
|
20
30
|
transaction?: string;
|
|
21
31
|
user_agent?: string;
|
|
32
|
+
request_headers?: ClientHeaders;
|
|
22
33
|
agent_context?: AgentContext;
|
|
23
34
|
ts: number;
|
|
24
35
|
};
|
package/dist/x402.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface ChallengeAdditions {
|
|
|
50
50
|
marker?: string;
|
|
51
51
|
/** Added as extensions["forge-feedback"] (v2 only; v1 challenges have no extensions). */
|
|
52
52
|
extension?: unknown;
|
|
53
|
+
/** Independent context prompt when feedback is disabled. */
|
|
54
|
+
contextExtension?: unknown;
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
57
|
* Add the rating sentence and/or the forge-feedback extension to a base64 PAYMENT-REQUIRED header (x402 v2).
|
package/dist/x402.js
CHANGED
|
@@ -72,15 +72,17 @@ function addToV2(challenge, add) {
|
|
|
72
72
|
changed = true;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
for (const [key, extension] of [[FEEDBACK_EXTENSION, add.extension], ["forge-agent-context", add.contextExtension]]) {
|
|
76
|
+
if (!extension)
|
|
77
|
+
continue;
|
|
76
78
|
const extensions = challenge.extensions;
|
|
77
79
|
if (extensions === undefined || extensions === null) {
|
|
78
|
-
challenge.extensions = { [
|
|
80
|
+
challenge.extensions = { [key]: extension };
|
|
79
81
|
changed = true;
|
|
80
82
|
}
|
|
81
|
-
else if (typeof extensions === "object" && !Array.isArray(extensions) && !(
|
|
83
|
+
else if (typeof extensions === "object" && !Array.isArray(extensions) && !(key in extensions)) {
|
|
82
84
|
// Added last, so the merchant's own extensions (e.g. bazaar) keep their order and content.
|
|
83
|
-
challenge.extensions = { ...extensions, [
|
|
85
|
+
challenge.extensions = { ...extensions, [key]: extension };
|
|
84
86
|
changed = true;
|
|
85
87
|
}
|
|
86
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeintel/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-beta.1",
|
|
4
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",
|