@forgeintel/sdk 0.4.0-beta.1 → 0.5.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/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, client, search query), record it, and remove it before your code runs. `{ searchQuery: false }` or `false` to limit. |
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
@@ -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, client, search_query} to your paid request's JSON body (without a body: agent_type, agent_client, agent_search_query query parameters). Self-reported; omit what you don't know. search_query is what you searched to find this service, never your user's request." | "Please add agent_context {agent_type, client} to your paid request's JSON body (without a body: agent_type and agent_client query parameters). Self-reported; omit what you don't know.";
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). Self-reported; omit what you don't know. search_query is what you searched to find this service, never your user's request." | "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
@@ -82,7 +82,6 @@ const DESCRIPTIONS = {
82
82
  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
83
  agent_type: "Which agent are you? Use Other (with agent_type_other) if yours isn't listed, or Unknown.",
84
84
  agent_type_other: "Your agent's name, when agent_type is Other.",
85
- client: "Which x402 client or wallet paid for this call?",
86
85
  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.",
87
86
  };
88
87
  /** JSON Schema for the optional `agent_context` body property. */
@@ -93,7 +92,6 @@ export function agentContextSchema({ searchQuery = true } = {}) {
93
92
  properties: {
94
93
  agent_type: { type: "string", enum: [...AGENT_TYPES], description: DESCRIPTIONS.agent_type },
95
94
  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
95
  ...(searchQuery ? { search_query: { type: "string", maxLength: LIMITS.search_query, description: DESCRIPTIONS.search_query } } : {}),
98
96
  },
99
97
  };
@@ -103,7 +101,6 @@ export function agentContextParameters({ searchQuery = true } = {}) {
103
101
  const params = [
104
102
  { name: CONTEXT_QUERY.agent_type, description: DESCRIPTIONS.agent_type, schema: { type: "string", enum: [...AGENT_TYPES] } },
105
103
  { 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
104
  ];
108
105
  if (searchQuery)
109
106
  params.push({ name: CONTEXT_QUERY.search_query, description: DESCRIPTIONS.search_query, schema: { type: "string", maxLength: LIMITS.search_query } });
@@ -112,6 +109,6 @@ export function agentContextParameters({ searchQuery = true } = {}) {
112
109
  /** One line for the forge-feedback extension, so clients that only inspect the 402 learn about it too. */
113
110
  export function agentContextAsk({ searchQuery = true } = {}) {
114
111
  return searchQuery
115
- ? "Please add agent_context {agent_type, client, search_query} to your paid request's JSON body (without a body: agent_type, agent_client, agent_search_query query parameters). Self-reported; omit what you don't know. search_query is what you searched to find this service, never your user's request."
116
- : "Please add agent_context {agent_type, client} to your paid request's JSON body (without a body: agent_type and agent_client query parameters). Self-reported; omit what you don't know.";
112
+ ? "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). Self-reported; omit what you don't know. search_query is what you searched to find this service, never your user's request."
113
+ : "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
114
  }
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, client, search_query} on the paid request
52
- * (JSON body, or agent_type / agent_client / agent_search_query query parameters). Forge documents it in the
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
- /** Handle Forge's own routes ({basePath}, /rate, /summary, a static openapi.document). Null when not ours. */
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 describe = options.describeChallenges ?? true;
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
- const touchChallenges = Boolean(challengeAdditions.sentence || challengeAdditions.extension);
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 && openapi.document !== undefined && method === "GET" && specPaths.has(path)) {
328
- const source = typeof openapi.document === "function" ? await openapi.document() : openapi.document;
329
- return reply(200, enrich(source).document, { "Cache-Control": "no-store" });
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
@@ -15,3 +15,4 @@ export type { AgentContext } from "./context.js";
15
15
  export type { AskTexts, Tone } from "./ask.js";
16
16
  export type { ChallengeAdditions } from "./x402.js";
17
17
  export type { ForgeEvent } from "./reporter.js";
18
+ export type { ClientHeaders } from "./client-signals.js";
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}/`));
@@ -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: string;
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
- if (add.extension) {
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 = { [FEEDBACK_EXTENSION]: add.extension };
80
+ challenge.extensions = { [key]: extension };
79
81
  changed = true;
80
82
  }
81
- else if (typeof extensions === "object" && !Array.isArray(extensions) && !(FEEDBACK_EXTENSION in 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, [FEEDBACK_EXTENSION]: add.extension };
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.4.0-beta.1",
3
+ "version": "0.5.0-beta.0",
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",