@forgeintel/sdk 0.5.0-beta.1 → 0.5.0-beta.11
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 +29 -13
- package/dist/ask.d.ts +2 -0
- package/dist/ask.js +5 -3
- package/dist/bazaar.d.ts +7 -0
- package/dist/bazaar.js +58 -0
- package/dist/context.d.ts +33 -16
- package/dist/context.js +62 -27
- package/dist/core.d.ts +16 -7
- package/dist/core.js +154 -55
- package/dist/express.js +25 -4
- package/dist/fetch.d.ts +3 -1
- package/dist/fetch.js +74 -6
- package/dist/hono.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next.js +4 -0
- package/dist/openapi.d.ts +6 -5
- package/dist/openapi.js +38 -21
- package/dist/x402.d.ts +25 -7
- package/dist/x402.js +64 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @forgeintel/sdk
|
|
2
2
|
|
|
3
|
-
> **Beta.** Install with `npm install @forgeintel/sdk
|
|
3
|
+
> **Beta.** Install with `npm install @forgeintel/sdk`. The API may change before 1.0.
|
|
4
4
|
|
|
5
5
|
The Forge SDK for x402 paid APIs. Paid responses carry a `feedback_id`, the 402 challenge asks agents to rate the call, a rating is one free GET, and every call is reported to your Forge dashboard in the background. Never on your critical path: no network calls while your API serves a request, and it fails open. Aware of x402 v1 and v2, and of OpenAPI 2.0 through 3.2. Every change is additive, and anything it doesn't understand passes through untouched.
|
|
6
6
|
|
|
@@ -10,9 +10,7 @@ The Forge SDK for x402 paid APIs. Paid responses carry a `feedback_id`, the 402
|
|
|
10
10
|
import { createForge } from "@forgeintel/sdk";
|
|
11
11
|
|
|
12
12
|
const forge = createForge({
|
|
13
|
-
apiKey: process.env.FORGE_API_KEY,
|
|
14
|
-
backendUrl: process.env.FORGE_BACKEND_URL, // Forge: <API origin>/api/sdk/v2
|
|
15
|
-
publicUrl: "https://api.example.com", // your public origin (never taken from the Host header)
|
|
13
|
+
apiKey: process.env.FORGE_API_KEY,
|
|
16
14
|
});
|
|
17
15
|
|
|
18
16
|
app.use(forge.middleware()); // 1. first: before payments and your /openapi.json route
|
|
@@ -26,7 +24,7 @@ Works with `@x402/express` (v2) and `x402-express` (v1), on Express 4.21+ or 5.
|
|
|
26
24
|
```ts
|
|
27
25
|
import { createForge } from "@forgeintel/sdk/hono";
|
|
28
26
|
|
|
29
|
-
const forge = createForge({ apiKey
|
|
27
|
+
const forge = createForge({ apiKey });
|
|
30
28
|
app.use(forge.middleware()); // before paymentMiddleware from @x402/hono
|
|
31
29
|
app.use(paymentMiddleware(routes, resourceServer));
|
|
32
30
|
```
|
|
@@ -36,7 +34,7 @@ app.use(paymentMiddleware(routes, resourceServer));
|
|
|
36
34
|
```ts
|
|
37
35
|
import { createForge } from "@forgeintel/sdk/next";
|
|
38
36
|
|
|
39
|
-
export const forge = createForge({ apiKey
|
|
37
|
+
export const forge = createForge({ apiKey });
|
|
40
38
|
// app/api/…/route.ts: Forge outermost, around @x402/next's withX402
|
|
41
39
|
export const POST = forge.withForge(withX402(handler, route, resourceServer));
|
|
42
40
|
// app/feedback/[[...path]]/route.ts: Forge's own routes
|
|
@@ -57,8 +55,8 @@ Zero runtime dependencies. Node ≥ 20.19. Works from both `import` and `require
|
|
|
57
55
|
|
|
58
56
|
| When | What happens |
|
|
59
57
|
| --- | --- |
|
|
60
|
-
| 402 challenge | Appends
|
|
61
|
-
| Paid 2xx response | Adds the `Forge-Feedback-Id` header. JSON object bodies also get `feedback_id`, `feedback_url` (with `outcome=` left blank) and `rate_this_call`, unless your OpenAPI schema for that response couldn't safely take
|
|
58
|
+
| 402 challenge | Appends one line to the description: *After your call, please rate this service for other agents: GET https://…/feedback/rate?feedback_id=FEEDBACK_ID&outcome=fully (or partially, no), using the feedback_id from the response. Free, no payment: any HTTP client works, including your x402 one. …* On x402 v2 it also adds an `extensions["forge-feedback"]` block with the `ask`, the rating link, and the outcome and issue values. The description never exceeds 500 characters (the CDP facilitator rejects longer ones): a shorter line is used when the full one doesn't fit, and none when neither does. Payment terms are never touched. |
|
|
59
|
+
| Paid 2xx response | Adds the `Forge-Feedback-Id` header. JSON object bodies also get one `forge_feedback` object: `feedback_id`, `feedback_url` (with `outcome=` left blank) and `rate_this_call`, unless your body already has a `forge_feedback` key or your OpenAPI schema for that response couldn't safely take it. Rating links are absolute: your origin registered in Forge, fetched in the background, or the request's own x402 resource origin. |
|
|
62
60
|
| `GET /openapi.json` | Your document is served enriched: feedback routes documented, feedback fields added to paid response schemas, one sentence in `x-guidance`. |
|
|
63
61
|
| `GET /feedback/rate` | Quick rating: `feedback_id`, `outcome`, optional `issue`. |
|
|
64
62
|
| `POST /feedback` | Same fields, plus an optional `note` (≤ 280 chars). |
|
|
@@ -84,8 +82,8 @@ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared compone
|
|
|
84
82
|
| Option | Default | |
|
|
85
83
|
| --- | --- | --- |
|
|
86
84
|
| `apiKey` | required | Merchant key. Also the HMAC key for IDs. |
|
|
87
|
-
| `backendUrl` |
|
|
88
|
-
| `publicUrl` |
|
|
85
|
+
| `backendUrl` | `https://app-api.forgeintel.co/api/sdk/v2` | Optional collector override. |
|
|
86
|
+
| `publicUrl` | Same-origin paths | Optional public origin for absolute rating URLs. Never infer it from request headers. |
|
|
89
87
|
| `feedback` | `true` | Master switch for rating prompts, IDs, response additions and feedback routes. `false` leaves telemetry, client headers and independent agent context on. |
|
|
90
88
|
| `basePath` | `/feedback` | Feedback routes (`/feedback`, `/feedback/rate`, `/feedback/summary`). |
|
|
91
89
|
| `tone` | `"soft"` | `"lifecycle"` describes this service's flow as four steps (402, pay, response, rate). Both stay soft asks. |
|
|
@@ -93,9 +91,9 @@ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared compone
|
|
|
93
91
|
| `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). |
|
|
94
92
|
| `challengeExtension` | `true` | Add the `forge-feedback` extension to x402 v2 challenges. |
|
|
95
93
|
| `receiptExtension` | `true` | Add it, with the real `feedback_id`, to the x402 v2 payment receipt (`PAYMENT-RESPONSE`). |
|
|
96
|
-
| `agentContext` |
|
|
97
|
-
| `injectBody` | `true` | Add `
|
|
98
|
-
| `rateHint` | built-in | The `rate_this_call` field. A string overrides it (`{feedback_url}` is substituted); `false` removes it. |
|
|
94
|
+
| `agentContext` | optional | Ask for self-reported `agent_context` (agent name, search query) on paid requests; record and strip it before your code runs. Unset, it is optional and never rejects a request. `true` (or `{ required: true }`) requires it before payment processing. `{ searchQuery: false }` stops asking for the search query; `false` disables collection. |
|
|
95
|
+
| `injectBody` | `true` | Add the `forge_feedback` object to paid JSON bodies. |
|
|
96
|
+
| `rateHint` | built-in | The `forge_feedback.rate_this_call` field. A string overrides it (`{feedback_url}` is substituted); `false` removes it. |
|
|
99
97
|
| `injectText` | `false` | Append a two-line trailer to paid `text/plain` bodies. |
|
|
100
98
|
| `openapi` | intercept `/openapi.json` | `false` to disable, or `{ paths, document, isPaidOperation, describeOperations }`. |
|
|
101
99
|
| `ttlMs` | 24h | Local pre-check; keep in sync with the backend. |
|
|
@@ -108,3 +106,21 @@ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared compone
|
|
|
108
106
|
Source, examples and design notes: [github.com/ClawCash/forge-feedback](https://github.com/ClawCash/forge-feedback).
|
|
109
107
|
|
|
110
108
|
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.
|
|
109
|
+
|
|
110
|
+
### Agent context on paid requests
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
createForge({ apiKey }); // default: asked for, optional
|
|
114
|
+
createForge({ apiKey, agentContext: true }); // required before payment
|
|
115
|
+
createForge({ apiKey, agentContext: { searchQuery: false } }); // required, agent name only
|
|
116
|
+
createForge({ apiKey, agentContext: { required: false, searchQuery: false } }); // optional, agent name only
|
|
117
|
+
createForge({ apiKey, agentContext: false }); // off
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
By default Forge asks for context and records it whenever an agent sends it, but a paid request without it is never rejected, so existing paying clients keep working. Forge documents `agent_context` and its `agent_type` and `search_query` fields as optional. Any agent name is accepted; known spellings are normalized (`Claude` and `claude-code` become `Claude Code`). Send the search query, or `"direct"`. When the merchant declares a `bazaar` extension, Forge adds the same field there (schema and example), so agents that discover the service through Bazaar see it too. Context is self-reported, not authenticated identity.
|
|
121
|
+
|
|
122
|
+
`agentContext: true`, or an object without `required: false`, makes context required: Forge documents the fields as required, and a request carrying `PAYMENT-SIGNATURE` or `X-PAYMENT` gets HTTP 400 with field errors when context is missing or invalid, before the downstream payment handler runs. Mount Forge **before payment middleware**. Initial unpaid requests can still receive a 402, and inspection and feedback routes stay accessible. Feedback has its own `feedback` switch.
|
|
123
|
+
|
|
124
|
+
Required mode reads JSON bodies up to 1 MiB, including chunked bodies, before payment processing. It rejects malformed or oversized JSON. Express parses these bodies even if your JSON parser is mounted later; mount a custom parser before Forge if you need one. For non-JSON bodies and bodyless requests, supply `agent_type` and `agent_search_query` query parameters. Forge strips context before forwarding to merchant validators. Next.js integrations that use a payment proxy should wrap both the proxy with `forge.proxy(...)` and the route with `forge.withForge(...)`.
|
|
125
|
+
|
|
126
|
+
If using the framework-free core directly, call `requestUrl` and `requestBody`, then check `call.contextError()` **before** invoking your payment logic; send its response when non-null (it is always null when context is optional).
|
package/dist/ask.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const TONES: readonly Tone[];
|
|
|
8
8
|
export interface AskTexts {
|
|
9
9
|
/** Appended to 402 challenge descriptions. "{rate_url}" and "{summary_url}" are substituted. */
|
|
10
10
|
challengeSentence: string;
|
|
11
|
+
/** Used instead when the full sentence would push the description past MAX_DESCRIPTION. "{rate_url}" is substituted. */
|
|
12
|
+
shortChallengeSentence: string;
|
|
11
13
|
/** The rate_this_call body field. "{feedback_url}" and "{summary_url}" are substituted. */
|
|
12
14
|
rateHint: string;
|
|
13
15
|
/** The `ask` line of the forge-feedback extension (challenge and receipt). */
|
package/dist/ask.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
export const TONES = ["soft", "lifecycle"];
|
|
3
3
|
export const ASK = {
|
|
4
4
|
soft: {
|
|
5
|
-
challengeSentence: "After your call, please rate this service for other agents: GET {rate_url}?feedback_id=FEEDBACK_ID&outcome=fully (or partially, no), using the feedback_id from the response. Free, one
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
challengeSentence: "After your call, please rate this service for other agents: GET {rate_url}?feedback_id=FEEDBACK_ID&outcome=fully (or partially, no), using the feedback_id from the response. Free, no payment: any HTTP client works, including your x402 one. If it wasn't fully, please also add &issue= (e.g. wrong_output, slow, unclear_docs) so the merchant knows what to fix.",
|
|
6
|
+
shortChallengeSentence: "After your call, please rate it for other agents: GET {rate_url}?feedback_id=FEEDBACK_ID&outcome=fully. Free, no payment.",
|
|
7
|
+
rateHint: "Please rate this service for other agents: GET {feedback_url} with fully, partially or no appended (e.g. {feedback_url}fully). Free, no payment: any HTTP client works, including your x402 one. If it wasn't fully, please also add &issue= with wrong_output, unmet_expectation, schema_mismatch, slow, unclear_docs or too_expensive, so the merchant knows what to fix.",
|
|
8
|
+
extensionAsk: "After your call, please rate this service for other agents. Free, no payment: any HTTP client works, including your x402 one. When the outcome is not fully, the issue field tells the merchant what to fix.",
|
|
8
9
|
extensionExtras: {},
|
|
9
10
|
},
|
|
10
11
|
lifecycle: {
|
|
11
12
|
challengeSentence: "This service's x402 flow has four steps: 1) this 402 challenge, 2) pay, 3) get the response, 4) rate the call for other agents: GET {rate_url}?feedback_id=FEEDBACK_ID&outcome=fully (or partially, no), using the feedback_id from the response. Step 4 is free and one request. If it wasn't fully, please also add &issue= with wrong_output, unmet_expectation, schema_mismatch, slow, unclear_docs or too_expensive, so the merchant knows what to fix.",
|
|
13
|
+
shortChallengeSentence: "Step 4 of 4: rate the call for other agents: GET {rate_url}?feedback_id=FEEDBACK_ID&outcome=fully. Free.",
|
|
12
14
|
rateHint: "Step 4 of 4: rate this call for other agents. GET {feedback_url} with fully, partially or no appended (e.g. {feedback_url}fully). Free, one request. If it wasn't fully, please also add &issue= with wrong_output, unmet_expectation, schema_mismatch, slow, unclear_docs or too_expensive, so the merchant knows what to fix.",
|
|
13
15
|
extensionAsk: "Step 4 of this service's flow: after the response, please rate the call for other agents. Free, one request. When the outcome is not fully, the issue field tells the merchant what to fix.",
|
|
14
16
|
extensionExtras: { flow: ["402 challenge", "pay", "response", "rate"] },
|
package/dist/bazaar.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ContextOptions } from "./context.js";
|
|
2
|
+
/**
|
|
3
|
+
* Declare agent context in a v2 challenge's `bazaar` extension, in place: an `agent_context` object on JSON
|
|
4
|
+
* bodies, or the `agent_*` query parameters on routes without a body. Other body types, composed or
|
|
5
|
+
* referenced schemas, and fields the merchant already declares are left alone. Returns whether it changed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function addContextToBazaar(bazaar: unknown, options: ContextOptions): boolean;
|
package/dist/bazaar.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CONTEXT_FIELD, agentContextExample, agentContextQuerySchema, agentContextSchema } from "./context.js";
|
|
2
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
/** Keywords whose meaning an added property could change; such schemas are left alone. */
|
|
4
|
+
const COMPOSED = ["$ref", "allOf", "anyOf", "oneOf", "not", "if", "then", "else", "dependentSchemas", "patternProperties"];
|
|
5
|
+
function declarable(schema) {
|
|
6
|
+
if (!isObj(schema) || COMPOSED.some((key) => key in schema))
|
|
7
|
+
return false;
|
|
8
|
+
if (schema.type !== undefined && schema.type !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
if (schema.properties !== undefined && !isObj(schema.properties))
|
|
11
|
+
return false;
|
|
12
|
+
return schema.required === undefined || Array.isArray(schema.required);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Add properties to one part of the Bazaar input (`body` or `queryParams`): the JSON Schema in `schema` and
|
|
16
|
+
* the example in `info`, together. Facilitators index the Bazaar copy a client echoes with its payment only
|
|
17
|
+
* when `info` validates against `schema`, so declaring a required field without an example drops the listing.
|
|
18
|
+
* `@x402/core` checks the echo as a superset of what the server advertised, so adding keys keeps payments valid.
|
|
19
|
+
*/
|
|
20
|
+
function extend(schemas, info, key, properties, required, example) {
|
|
21
|
+
const schema = schemas[key];
|
|
22
|
+
if (!declarable(schema))
|
|
23
|
+
return false;
|
|
24
|
+
const current = info[key];
|
|
25
|
+
if (current !== undefined && !isObj(current))
|
|
26
|
+
return false;
|
|
27
|
+
const declared = isObj(schema.properties) ? schema.properties : {};
|
|
28
|
+
const listed = (schema.required ?? []);
|
|
29
|
+
// The merchant already describes these fields: their declaration wins.
|
|
30
|
+
if (Object.keys(properties).some((name) => name in declared || listed.includes(name) || (isObj(current) && name in current)))
|
|
31
|
+
return false;
|
|
32
|
+
schema.properties = { ...declared, ...properties };
|
|
33
|
+
if (required.length)
|
|
34
|
+
schema.required = [...new Set([...listed, ...required])];
|
|
35
|
+
info[key] = { ...current, ...example };
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Declare agent context in a v2 challenge's `bazaar` extension, in place: an `agent_context` object on JSON
|
|
40
|
+
* bodies, or the `agent_*` query parameters on routes without a body. Other body types, composed or
|
|
41
|
+
* referenced schemas, and fields the merchant already declares are left alone. Returns whether it changed.
|
|
42
|
+
*/
|
|
43
|
+
export function addContextToBazaar(bazaar, options) {
|
|
44
|
+
if (!isObj(bazaar) || !isObj(bazaar.info) || !isObj(bazaar.info.input) || !isObj(bazaar.schema))
|
|
45
|
+
return false;
|
|
46
|
+
const inputSchema = bazaar.schema.properties?.input;
|
|
47
|
+
if (!isObj(inputSchema) || !isObj(inputSchema.properties))
|
|
48
|
+
return false;
|
|
49
|
+
const info = bazaar.info.input;
|
|
50
|
+
const required = options.required !== false;
|
|
51
|
+
if (info.bodyType !== undefined || "body" in inputSchema.properties) {
|
|
52
|
+
if (info.bodyType !== "json")
|
|
53
|
+
return false;
|
|
54
|
+
return extend(inputSchema.properties, info, "body", { [CONTEXT_FIELD]: agentContextSchema(options) }, required ? [CONTEXT_FIELD] : [], { [CONTEXT_FIELD]: agentContextExample(options) });
|
|
55
|
+
}
|
|
56
|
+
const query = agentContextQuerySchema(options);
|
|
57
|
+
return extend(inputSchema.properties, info, "queryParams", query.properties, query.required, agentContextExample(options, "query"));
|
|
58
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Suggested agent names; anything else is kept as given. */
|
|
2
2
|
export declare const AGENT_TYPES: readonly ["Claude Code", "Codex", "Cursor", "Grok Bot", "Muse", "Hermes", "Instinct", "OpenClaw", "Others"];
|
|
3
|
+
/**
|
|
4
|
+
* Example values for discovery documents that need one (Bazaar's `info`). Agents sometimes copy examples
|
|
5
|
+
* verbatim, so they are placeholders, and an unedited placeholder is recorded as missing rather than as a name.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONTEXT_PLACEHOLDERS: {
|
|
8
|
+
readonly agent_type: "your agent name";
|
|
9
|
+
readonly search_query: "your search query, or direct";
|
|
10
|
+
};
|
|
3
11
|
/** Suggested x402 clients; anything else is kept as given. */
|
|
4
12
|
export declare const CLIENTS: readonly ["agentcash", "awal", "pay.sh", "other", "unknown"];
|
|
5
13
|
/** JSON body field (POST, PUT, …). */
|
|
@@ -31,31 +39,31 @@ export declare function takeFromUrl(url: string): {
|
|
|
31
39
|
url: string;
|
|
32
40
|
raw?: Record<string, string>;
|
|
33
41
|
};
|
|
34
|
-
|
|
35
|
-
export declare function agentContextSchema({ searchQuery }?: {
|
|
42
|
+
export interface ContextOptions {
|
|
36
43
|
searchQuery?: boolean;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
required?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** JSON Schema for the `agent_context` body property. */
|
|
47
|
+
export declare function agentContextSchema({ searchQuery, required }?: ContextOptions): {
|
|
40
48
|
properties: {
|
|
41
49
|
search_query?: {
|
|
50
|
+
description: string;
|
|
51
|
+
minLength?: number | undefined;
|
|
42
52
|
type: string;
|
|
43
53
|
maxLength: number;
|
|
44
|
-
description: string;
|
|
45
54
|
} | undefined;
|
|
46
55
|
agent_type: {
|
|
47
|
-
type: string;
|
|
48
|
-
enum: ("Claude Code" | "Codex" | "Cursor" | "Grok Bot" | "Muse" | "Hermes" | "Instinct" | "OpenClaw" | "Others")[];
|
|
49
56
|
description: string;
|
|
50
|
-
|
|
51
|
-
agent_type_other: {
|
|
57
|
+
minLength?: number | undefined;
|
|
52
58
|
type: string;
|
|
53
59
|
maxLength: number;
|
|
54
|
-
description: string;
|
|
55
60
|
};
|
|
56
61
|
};
|
|
62
|
+
required?: string[] | undefined;
|
|
63
|
+
type: string;
|
|
64
|
+
description: string;
|
|
57
65
|
};
|
|
58
|
-
/** The
|
|
66
|
+
/** The query parameters, for operations without a request body. */
|
|
59
67
|
export declare function agentContextParameters({ searchQuery }?: {
|
|
60
68
|
searchQuery?: boolean;
|
|
61
69
|
}): {
|
|
@@ -63,7 +71,16 @@ export declare function agentContextParameters({ searchQuery }?: {
|
|
|
63
71
|
description: string;
|
|
64
72
|
schema: Record<string, unknown>;
|
|
65
73
|
}[];
|
|
66
|
-
/**
|
|
67
|
-
export declare function
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
/** The same parameters as JSON Schema properties, for discovery documents that describe a query object (Bazaar). */
|
|
75
|
+
export declare function agentContextQuerySchema({ searchQuery, required }?: ContextOptions): {
|
|
76
|
+
properties: Record<string, Record<string, unknown>>;
|
|
77
|
+
required: string[];
|
|
78
|
+
};
|
|
79
|
+
/** Placeholder values matching agentContextSchema (body) or agentContextQuerySchema (query). */
|
|
80
|
+
export declare function agentContextExample({ searchQuery }?: ContextOptions, where?: "body" | "query"): {
|
|
81
|
+
[x: string]: "your agent name" | "your search query, or direct";
|
|
82
|
+
};
|
|
83
|
+
/** One line for the forge-agent-context extension, so clients that only inspect the 402 learn about it too. */
|
|
84
|
+
export declare function agentContextAsk({ searchQuery, required }?: ContextOptions): string;
|
|
85
|
+
/** Validate the original values, before lenient parsing can truncate or discard them. */
|
|
86
|
+
export declare function contextIssues(raw: Record<string, unknown>, { searchQuery }: ContextOptions): string[];
|
package/dist/context.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// API contract never changes. Never required: a required field breaks strict validators and makes
|
|
4
|
-
// agents invent answers.
|
|
1
|
+
// Self-reported facts about the calling agent, removed before merchant validation.
|
|
2
|
+
// Optional by default; merchants can require context on payment-bearing requests.
|
|
5
3
|
/** Suggested agent names; anything else is kept as given. */
|
|
6
4
|
export const AGENT_TYPES = ["Claude Code", "Codex", "Cursor", "Grok Bot", "Muse", "Hermes", "Instinct", "OpenClaw", "Others"];
|
|
5
|
+
/** Spellings agents use for the suggested names, compared lowercase with only letters and digits kept. */
|
|
6
|
+
const AGENT_ALIASES = {
|
|
7
|
+
claude: "Claude Code", claudecode: "Claude Code", claudecodecli: "Claude Code", anthropicclaudecode: "Claude Code",
|
|
8
|
+
codex: "Codex", codexcli: "Codex", openaicodex: "Codex",
|
|
9
|
+
cursor: "Cursor", cursoragent: "Cursor", cursorai: "Cursor",
|
|
10
|
+
grok: "Grok Bot", grokbot: "Grok Bot",
|
|
11
|
+
muse: "Muse", hermes: "Hermes", hermesagent: "Hermes", instinct: "Instinct", openclaw: "OpenClaw",
|
|
12
|
+
other: "Others", others: "Others", othersspecify: "Others", unknown: "Others",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Example values for discovery documents that need one (Bazaar's `info`). Agents sometimes copy examples
|
|
16
|
+
* verbatim, so they are placeholders, and an unedited placeholder is recorded as missing rather than as a name.
|
|
17
|
+
*/
|
|
18
|
+
export const CONTEXT_PLACEHOLDERS = { agent_type: "your agent name", search_query: "your search query, or direct" };
|
|
7
19
|
/** Suggested x402 clients; anything else is kept as given. */
|
|
8
20
|
export const CLIENTS = ["agentcash", "awal", "pay.sh", "other", "unknown"];
|
|
9
21
|
/** JSON body field (POST, PUT, …). */
|
|
@@ -31,15 +43,13 @@ export function parseAgentContext(input, { searchQuery = true } = {}) {
|
|
|
31
43
|
let text = raw.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, LIMITS[key]);
|
|
32
44
|
if (!text)
|
|
33
45
|
continue;
|
|
34
|
-
if (key === "agent_type")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
text = "Others";
|
|
39
|
-
}
|
|
46
|
+
if ((key === "agent_type" || key === "search_query") && text.toLowerCase() === CONTEXT_PLACEHOLDERS[key])
|
|
47
|
+
continue;
|
|
48
|
+
if (key === "agent_type")
|
|
49
|
+
text = AGENT_ALIASES[text.toLowerCase().replace(/[^a-z0-9]/g, "")] ?? text;
|
|
40
50
|
if (key === "search_query" && /^direct$/i.test(text))
|
|
41
51
|
text = "direct";
|
|
42
|
-
const known = key === "
|
|
52
|
+
const known = key === "client" ? CLIENTS : null;
|
|
43
53
|
context[key] = known?.find((v) => v.toLowerCase() === text.toLowerCase()) ?? text;
|
|
44
54
|
}
|
|
45
55
|
return Object.keys(context).length ? context : undefined;
|
|
@@ -87,36 +97,61 @@ export function takeFromUrl(url) {
|
|
|
87
97
|
return { url: `${url.slice(0, q)}${rest ? `?${rest}` : ""}${hash < 0 ? "" : url.slice(hash)}`, raw };
|
|
88
98
|
}
|
|
89
99
|
const DESCRIPTIONS = {
|
|
90
|
-
object: "
|
|
91
|
-
agent_type:
|
|
92
|
-
agent_type_other: "Your agent's name, when agent_type is Others.",
|
|
100
|
+
object: "information about the calling agent and how it found this service.",
|
|
101
|
+
agent_type: `Your agent's name, for example ${AGENT_TYPES.filter((name) => name !== "Others").join(", ")}. Any name works.`,
|
|
93
102
|
search_query: "The search query you used to find this service, or \"direct\" if you did not search.",
|
|
94
103
|
};
|
|
95
|
-
/** JSON Schema for the
|
|
96
|
-
export function agentContextSchema({ searchQuery = true } = {}) {
|
|
104
|
+
/** JSON Schema for the `agent_context` body property. */
|
|
105
|
+
export function agentContextSchema({ searchQuery = true, required = true } = {}) {
|
|
97
106
|
return {
|
|
98
107
|
type: "object",
|
|
99
108
|
description: DESCRIPTIONS.object,
|
|
109
|
+
...(required ? { required: ["agent_type", ...(searchQuery ? ["search_query"] : [])] } : {}),
|
|
100
110
|
properties: {
|
|
101
|
-
agent_type: { type: "string",
|
|
102
|
-
|
|
103
|
-
...(searchQuery ? { search_query: { type: "string", maxLength: LIMITS.search_query, description: DESCRIPTIONS.search_query } } : {}),
|
|
111
|
+
agent_type: { type: "string", maxLength: LIMITS.agent_type, ...(required ? { minLength: 1 } : {}), description: DESCRIPTIONS.agent_type },
|
|
112
|
+
...(searchQuery ? { search_query: { type: "string", maxLength: LIMITS.search_query, ...(required ? { minLength: 1 } : {}), description: DESCRIPTIONS.search_query } } : {}),
|
|
104
113
|
},
|
|
105
114
|
};
|
|
106
115
|
}
|
|
107
|
-
/** The
|
|
116
|
+
/** The query parameters, for operations without a request body. */
|
|
108
117
|
export function agentContextParameters({ searchQuery = true } = {}) {
|
|
109
118
|
const params = [
|
|
110
|
-
{ name: CONTEXT_QUERY.agent_type, description: DESCRIPTIONS.agent_type, schema: { type: "string",
|
|
111
|
-
{ name: CONTEXT_QUERY.agent_type_other, description: DESCRIPTIONS.agent_type_other, schema: { type: "string", maxLength: LIMITS.agent_type_other } },
|
|
119
|
+
{ name: CONTEXT_QUERY.agent_type, description: DESCRIPTIONS.agent_type, schema: { type: "string", maxLength: LIMITS.agent_type } },
|
|
112
120
|
];
|
|
113
121
|
if (searchQuery)
|
|
114
122
|
params.push({ name: CONTEXT_QUERY.search_query, description: DESCRIPTIONS.search_query, schema: { type: "string", maxLength: LIMITS.search_query } });
|
|
115
123
|
return params;
|
|
116
124
|
}
|
|
117
|
-
/**
|
|
118
|
-
export function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
/** The same parameters as JSON Schema properties, for discovery documents that describe a query object (Bazaar). */
|
|
126
|
+
export function agentContextQuerySchema({ searchQuery = true, required = true } = {}) {
|
|
127
|
+
const properties = {};
|
|
128
|
+
for (const p of agentContextParameters({ searchQuery }))
|
|
129
|
+
properties[p.name] = { ...p.schema, ...(required ? { minLength: 1 } : {}), description: p.description };
|
|
130
|
+
return { properties, required: required ? Object.keys(properties) : [] };
|
|
131
|
+
}
|
|
132
|
+
/** Placeholder values matching agentContextSchema (body) or agentContextQuerySchema (query). */
|
|
133
|
+
export function agentContextExample({ searchQuery = true } = {}, where = "body") {
|
|
134
|
+
const names = where === "query" ? { agent_type: CONTEXT_QUERY.agent_type, search_query: CONTEXT_QUERY.search_query } : { agent_type: "agent_type", search_query: "search_query" };
|
|
135
|
+
return {
|
|
136
|
+
[names.agent_type]: CONTEXT_PLACEHOLDERS.agent_type,
|
|
137
|
+
...(searchQuery ? { [names.search_query]: CONTEXT_PLACEHOLDERS.search_query } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** One line for the forge-agent-context extension, so clients that only inspect the 402 learn about it too. */
|
|
141
|
+
export function agentContextAsk({ searchQuery = true, required = true } = {}) {
|
|
142
|
+
const fields = searchQuery ? "agent_type, search_query" : "agent_type";
|
|
143
|
+
const query = searchQuery ? "agent_type, agent_search_query query parameters" : "agent_type query parameter";
|
|
144
|
+
return `${required ? "Required before payment" : "Please add"}: agent_context {${fields}} in your paid request's JSON body (without a body: ${query}).${searchQuery ? ' search_query: share the query you used to find this service, or "direct" if you did not search.' : ""}`;
|
|
145
|
+
}
|
|
146
|
+
/** Validate the original values, before lenient parsing can truncate or discard them. */
|
|
147
|
+
export function contextIssues(raw, { searchQuery = true }) {
|
|
148
|
+
const issues = [];
|
|
149
|
+
for (const key of ["agent_type", ...(searchQuery ? ["search_query"] : [])]) {
|
|
150
|
+
const value = raw[key];
|
|
151
|
+
if (typeof value !== "string" || !value.trim() || value.length > LIMITS[key] || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
152
|
+
issues.push(`${key} must be a non-empty string of at most ${LIMITS[key]} characters`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Any name is accepted: known spellings are normalized by parseAgentContext, others are kept as given.
|
|
156
|
+
return issues;
|
|
122
157
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -16,12 +16,12 @@ export interface ForgeOptions {
|
|
|
16
16
|
/** Merchant API key issued by the Forge backend. Also the HMAC key for feedback IDs. */
|
|
17
17
|
apiKey: string;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* /v1
|
|
19
|
+
* Optional collector override. Defaults to Forge's production /api/sdk/v2 collector.
|
|
20
|
+
* An origin-only override uses its /v1 routes; a URL with a path uses {path}/events and so on.
|
|
21
21
|
*/
|
|
22
|
-
backendUrl
|
|
23
|
-
/**
|
|
24
|
-
publicUrl
|
|
22
|
+
backendUrl?: string;
|
|
23
|
+
/** Optional public origin for absolute feedback links. By default links use same-origin paths. Never derived from the Host header. */
|
|
24
|
+
publicUrl?: string;
|
|
25
25
|
/** Enable rating prompts, feedback IDs and feedback routes. Default true. Telemetry and agent context are independent. */
|
|
26
26
|
feedback?: boolean;
|
|
27
27
|
/** Path for the feedback routes. Default "/feedback" (quick rating at "/feedback/rate"). */
|
|
@@ -50,14 +50,18 @@ export interface ForgeOptions {
|
|
|
50
50
|
*/
|
|
51
51
|
receiptExtension?: boolean;
|
|
52
52
|
/**
|
|
53
|
-
* Agent context:
|
|
53
|
+
* Agent context: self-reported `agent_context` {agent_type, search_query} on the paid request
|
|
54
54
|
* (JSON body, or agent_type / agent_search_query query parameters). Forge documents it in the
|
|
55
55
|
* extension and OpenAPI, reads it, reports it with the call, and removes it before your validators and handlers
|
|
56
|
-
* run.
|
|
56
|
+
* run. By default it is asked for but optional: a paid request without it is never rejected.
|
|
57
|
+
* `true` or `{ required: true }` requires it: payment-bearing requests with missing/invalid context get a 400
|
|
58
|
+
* before payment middleware (an object is required unless it sets `required: false`).
|
|
59
|
+
* `{ searchQuery: false }` stops asking for (and recording) the search query; `false` stops
|
|
57
60
|
* asking and recording altogether (the fields are still removed if an agent sends them).
|
|
58
61
|
*/
|
|
59
62
|
agentContext?: boolean | {
|
|
60
63
|
searchQuery?: boolean;
|
|
64
|
+
required?: boolean;
|
|
61
65
|
};
|
|
62
66
|
/** Add feedback_id and feedback_url to JSON object bodies of paid responses. Default true. */
|
|
63
67
|
injectBody?: boolean;
|
|
@@ -106,6 +110,10 @@ export interface ForgeResponse {
|
|
|
106
110
|
export interface ForgeCall {
|
|
107
111
|
/** Set when the request carried a payment header (x402 v2 PAYMENT-SIGNATURE or v1 X-PAYMENT). */
|
|
108
112
|
readonly feedbackId: string | undefined;
|
|
113
|
+
/** Whether this payment-bearing request must provide agent context. */
|
|
114
|
+
readonly contextRequired: boolean;
|
|
115
|
+
/** Call after requestUrl/requestBody and BEFORE payment processing. Null means context is acceptable. */
|
|
116
|
+
contextError(): ForgeResponse | null;
|
|
109
117
|
/** A JSON body about to be sent: 402 challenges get the rating ask, paid 2xx objects get the feedback fields. */
|
|
110
118
|
json(status: number, body: unknown): unknown;
|
|
111
119
|
/** A text body about to be sent: gets the two-line trailer on paid 2xx text/plain when injectText is on. */
|
|
@@ -171,6 +179,7 @@ export declare const FEEDBACK_HEADERS: {
|
|
|
171
179
|
export declare const BODY_LIMIT: number;
|
|
172
180
|
/** Max OpenAPI document an adapter should buffer for enrichment. */
|
|
173
181
|
export declare const SPEC_LIMIT: number;
|
|
182
|
+
export declare const DEFAULT_BACKEND_URL = "https://app-api.forgeintel.co/api/sdk/v2";
|
|
174
183
|
/**
|
|
175
184
|
* Check options without throwing. Invalid required options are errors (Forge runs disabled);
|
|
176
185
|
* invalid optional values are warnings and fall back to their defaults.
|