@forgeintel/sdk 0.4.0-beta.1 → 0.5.0-alpha.oldfeedback.1.b13
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 +36 -17
- package/dist/ask.d.ts +2 -0
- package/dist/ask.js +2 -0
- package/dist/bazaar.d.ts +7 -0
- package/dist/bazaar.js +58 -0
- package/dist/client-signals.d.ts +4 -0
- package/dist/client-signals.js +45 -0
- package/dist/context.d.ts +34 -22
- package/dist/context.js +67 -27
- package/dist/core.d.ts +20 -9
- package/dist/core.js +186 -71
- package/dist/express.js +37 -6
- package/dist/fetch.d.ts +3 -1
- package/dist/fetch.js +77 -8
- package/dist/hono.js +1 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/next.js +4 -0
- package/dist/openapi.d.ts +7 -4
- package/dist/openapi.js +53 -22
- package/dist/reporter.d.ts +12 -1
- package/dist/x402.d.ts +26 -6
- package/dist/x402.js +70 -16
- package/package.json +4 -8
package/dist/fetch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Web-standard adapter: Request in, Response out. Works wherever handlers are (request) => Response:
|
|
2
2
|
// Hono, Next.js route handlers, Cloudflare Workers, Bun, Deno. The Hono and Next adapters build on this.
|
|
3
3
|
import { BODY_LIMIT, SPEC_LIMIT, createForgeCore } from "./core.js";
|
|
4
|
-
/**
|
|
4
|
+
/** Buffer limit; required context rejects oversized paid JSON before payment processing. */
|
|
5
5
|
const JSON_LIMIT = 1024 * 1024;
|
|
6
6
|
const isJson = (type) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(type ?? "");
|
|
7
7
|
export function toResponse(response) {
|
|
@@ -20,6 +20,40 @@ const smallEnough = (headers, limit) => {
|
|
|
20
20
|
const length = Number(headers.get("content-length"));
|
|
21
21
|
return Number.isFinite(length) && length > 0 && length <= limit;
|
|
22
22
|
};
|
|
23
|
+
/** Bound reads even when a client sends chunked JSON or an inaccurate Content-Length. */
|
|
24
|
+
async function boundedJson(request) {
|
|
25
|
+
const reader = request.clone().body.getReader();
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let size = 0;
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const { value, done } = await reader.read();
|
|
31
|
+
if (done)
|
|
32
|
+
break;
|
|
33
|
+
size += value.length;
|
|
34
|
+
if (size > JSON_LIMIT)
|
|
35
|
+
throw new Error("body_too_large");
|
|
36
|
+
chunks.push(value);
|
|
37
|
+
}
|
|
38
|
+
const data = new Uint8Array(size);
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const chunk of chunks) {
|
|
41
|
+
data.set(chunk, offset);
|
|
42
|
+
offset += chunk.length;
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
// A clone is a tee: awaiting cancellation would wait for the untouched original stream.
|
|
48
|
+
void reader.cancel().catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function unreadableContext() {
|
|
52
|
+
return toResponse({ status: 400, headers: {}, body: {
|
|
53
|
+
error: "agent_context_invalid",
|
|
54
|
+
message: "Required agent context could not be read. Send valid JSON up to 1 MiB, or use agent_type and agent_search_query query parameters with a non-JSON body.",
|
|
55
|
+
} });
|
|
56
|
+
}
|
|
23
57
|
export function createForge(options) {
|
|
24
58
|
const core = createForgeCore(options);
|
|
25
59
|
const forgeRequest = (request, url) => ({
|
|
@@ -35,6 +69,24 @@ export function createForge(options) {
|
|
|
35
69
|
const own = await core.route(forgeRequest(request, new URL(request.url)));
|
|
36
70
|
return own ? toResponse(own) : null;
|
|
37
71
|
}
|
|
72
|
+
async function validate(request) {
|
|
73
|
+
if (!core.enabled || options.agentContext === false)
|
|
74
|
+
return null;
|
|
75
|
+
const url = new URL(request.url);
|
|
76
|
+
const call = core.call(forgeRequest(request, url));
|
|
77
|
+
if (!call.contextRequired)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
call.requestUrl(`${url.pathname}${url.search}`);
|
|
81
|
+
if (request.body && isJson(request.headers.get("content-type")))
|
|
82
|
+
call.requestBody(await boundedJson(request));
|
|
83
|
+
const error = call.contextError();
|
|
84
|
+
return error ? toResponse(error) : null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return unreadableContext();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
38
90
|
/** The merchant's own OpenAPI route: fetch it without validators, enrich JSON 200s, pass anything else through. */
|
|
39
91
|
async function spec(request, next) {
|
|
40
92
|
const headers = new Headers(request.headers);
|
|
@@ -61,9 +113,7 @@ export function createForge(options) {
|
|
|
61
113
|
}
|
|
62
114
|
}
|
|
63
115
|
async function handle(request, next) {
|
|
64
|
-
|
|
65
|
-
return next(request);
|
|
66
|
-
// Before the handler: our own routes, the spec, and agent context. Any failure here: the original request goes on.
|
|
116
|
+
// Before the handler: our own routes, the spec, and agent context. Required-context errors stop paid attempts.
|
|
67
117
|
let call;
|
|
68
118
|
let forwarded = request;
|
|
69
119
|
try {
|
|
@@ -77,15 +127,26 @@ export function createForge(options) {
|
|
|
77
127
|
const stripped = call.requestUrl(`${url.pathname}${url.search}`);
|
|
78
128
|
const nextUrl = stripped === `${url.pathname}${url.search}` ? request.url : new URL(stripped, url).href;
|
|
79
129
|
let body;
|
|
80
|
-
if (request.body && isJson(request.headers.get("content-type"))
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
130
|
+
if (call.contextRequired && request.body && isJson(request.headers.get("content-type"))) {
|
|
131
|
+
const parsed = await boundedJson(request);
|
|
132
|
+
const without = call.requestBody(parsed);
|
|
133
|
+
if (without !== parsed)
|
|
134
|
+
body = JSON.stringify(without);
|
|
135
|
+
}
|
|
136
|
+
else if (request.body && isJson(request.headers.get("content-type")) && (smallEnough(request.headers, JSON_LIMIT) || !request.headers.has("content-length"))) {
|
|
137
|
+
// Optional context, best effort: a body that is too large or not JSON goes to the handler untouched.
|
|
138
|
+
const parsed = await boundedJson(request).catch(() => undefined);
|
|
139
|
+
if (parsed !== undefined) {
|
|
84
140
|
const without = call.requestBody(parsed);
|
|
85
141
|
if (without !== parsed)
|
|
86
142
|
body = JSON.stringify(without);
|
|
87
143
|
}
|
|
88
144
|
}
|
|
145
|
+
const contextError = call.contextError();
|
|
146
|
+
if (contextError) {
|
|
147
|
+
call.finish(contextError.status);
|
|
148
|
+
return toResponse(contextError);
|
|
149
|
+
}
|
|
89
150
|
if (nextUrl !== request.url || body !== undefined) {
|
|
90
151
|
const headers = new Headers(request.headers);
|
|
91
152
|
if (body !== undefined)
|
|
@@ -99,8 +160,15 @@ export function createForge(options) {
|
|
|
99
160
|
}
|
|
100
161
|
catch (error) {
|
|
101
162
|
core.onError(error);
|
|
163
|
+
if (call?.contextRequired) {
|
|
164
|
+
call.finish(400);
|
|
165
|
+
return unreadableContext();
|
|
166
|
+
}
|
|
102
167
|
return next(request);
|
|
103
168
|
}
|
|
169
|
+
// Disabled by invalid options: agent context is removed (above), nothing else.
|
|
170
|
+
if (!core.enabled)
|
|
171
|
+
return next(forwarded);
|
|
104
172
|
const response = await next(forwarded);
|
|
105
173
|
// After the handler: decorate the 402 or the paid response. Any failure before the body is read: the original
|
|
106
174
|
// response goes out. Once read, the body is always sent from what was read (decorated or not).
|
|
@@ -165,6 +233,7 @@ export function createForge(options) {
|
|
|
165
233
|
diagnostics: core.diagnostics,
|
|
166
234
|
shutdown: core.shutdown,
|
|
167
235
|
handle,
|
|
236
|
+
validate,
|
|
168
237
|
route,
|
|
169
238
|
wrap: (handler) => (request, ...rest) => handle(request, (r) => handler(r, ...rest)),
|
|
170
239
|
};
|
package/dist/hono.js
CHANGED
|
@@ -8,9 +8,8 @@ export function createForge(options) {
|
|
|
8
8
|
enrichOpenApi: forge.enrichOpenApi,
|
|
9
9
|
diagnostics: forge.diagnostics,
|
|
10
10
|
shutdown: forge.shutdown,
|
|
11
|
+
validate: forge.validate,
|
|
11
12
|
middleware() {
|
|
12
|
-
if (!forge.enabled)
|
|
13
|
-
return async (_c, next) => next();
|
|
14
13
|
return async (c, next) => {
|
|
15
14
|
let ranNext = false;
|
|
16
15
|
const response = await forge.handle(c.req.raw, async (request) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,10 +8,11 @@ export { deriveSigningKey, mintFeedbackId, verifyFeedbackId, FEEDBACK_ID_PATTERN
|
|
|
8
8
|
export type { FeedbackIdCheck } from "./id.js";
|
|
9
9
|
export { OUTCOMES, ISSUES, NOTE_MAX_LENGTH, PROTOCOL, parseSubmission } from "./values.js";
|
|
10
10
|
export type { Outcome, Issue, Submission } from "./values.js";
|
|
11
|
-
export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, readPaymentHeader, readReceipt } from "./x402.js";
|
|
11
|
+
export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, FEEDBACK_FIELD, readPaymentHeader, readReceipt } from "./x402.js";
|
|
12
12
|
export { ASK, TONES, checkAskText } from "./ask.js";
|
|
13
13
|
export { AGENT_TYPES, CLIENTS, CONTEXT_FIELD, CONTEXT_QUERY, agentContextSchema, parseAgentContext } from "./context.js";
|
|
14
14
|
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/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { createForgeCore, checkOptions, BODY_LIMIT, SPEC_LIMIT } from "./core.js
|
|
|
3
3
|
export { enrichOpenApi, detectVersion } from "./openapi.js";
|
|
4
4
|
export { deriveSigningKey, mintFeedbackId, verifyFeedbackId, FEEDBACK_ID_PATTERN, DEFAULT_TTL_MS } from "./id.js";
|
|
5
5
|
export { OUTCOMES, ISSUES, NOTE_MAX_LENGTH, PROTOCOL, parseSubmission } from "./values.js";
|
|
6
|
-
export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, readPaymentHeader, readReceipt } from "./x402.js";
|
|
6
|
+
export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, FEEDBACK_FIELD, readPaymentHeader, readReceipt } from "./x402.js";
|
|
7
7
|
export { ASK, TONES, checkAskText } from "./ask.js";
|
|
8
8
|
export { AGENT_TYPES, CLIENTS, CONTEXT_FIELD, CONTEXT_QUERY, agentContextSchema, parseAgentContext } from "./context.js";
|
package/dist/next.js
CHANGED
|
@@ -17,9 +17,13 @@ export function createForge(options) {
|
|
|
17
17
|
diagnostics: forge.diagnostics,
|
|
18
18
|
shutdown: forge.shutdown,
|
|
19
19
|
handle: forge.handle,
|
|
20
|
+
validate: forge.validate,
|
|
20
21
|
withForge: (handler) => (request, context) => forge.handle(request, (forwarded) => handler(forwarded === request ? request : sameKind(request, forwarded), context)),
|
|
21
22
|
routes: { GET: own, POST: own, HEAD: own },
|
|
22
23
|
proxy: (proxy) => async (request) => {
|
|
24
|
+
const invalid = await forge.validate(request);
|
|
25
|
+
if (invalid)
|
|
26
|
+
return invalid;
|
|
23
27
|
const response = await proxy(request);
|
|
24
28
|
// Only the 402: the proxy passes paid requests on to the route, where withForge mints the feedback ID.
|
|
25
29
|
if (response.status !== 402 || !forge.enabled)
|
package/dist/openapi.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ 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
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** Disable all feedback additions while retaining agent context. */
|
|
6
|
+
feedback?: boolean;
|
|
7
|
+
/** Optional merchant public origin. Without it, feedback links use same-origin paths. */
|
|
8
|
+
publicUrl?: string;
|
|
7
9
|
/** Public path of the feedback routes, e.g. /feedback */
|
|
8
10
|
basePath: string;
|
|
9
11
|
/** Sentence appended to x-guidance and paid operation descriptions. */
|
|
@@ -16,9 +18,10 @@ export interface EnrichOptions {
|
|
|
16
18
|
describeOperations?: boolean;
|
|
17
19
|
/** Also document the optional rate_this_call body field (when the SDK's rateHint is on). */
|
|
18
20
|
hintField?: boolean;
|
|
19
|
-
/** Document
|
|
21
|
+
/** Document agent context on paid operations: `agent_context` in JSON request bodies, agent_* query parameters otherwise. */
|
|
20
22
|
agentContext?: {
|
|
21
23
|
searchQuery: boolean;
|
|
24
|
+
required?: boolean;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
export interface OperationReport {
|
|
@@ -27,7 +30,7 @@ export interface OperationReport {
|
|
|
27
30
|
path: string;
|
|
28
31
|
/** Per 2xx status code: whether feedback_id was added to its JSON schema. */
|
|
29
32
|
responses: Record<string, ResponseSupport>;
|
|
30
|
-
/** Where
|
|
33
|
+
/** Where agent context was documented, if asked to. */
|
|
31
34
|
agentContext?: "body" | "query" | "not_added";
|
|
32
35
|
reasons: string[];
|
|
33
36
|
}
|
package/dist/openapi.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// and on any unexpected input return the original document unchanged.
|
|
4
4
|
import { CONTEXT_FIELD, agentContextParameters, agentContextSchema } from "./context.js";
|
|
5
5
|
import { FEEDBACK_ID_PATTERN } from "./id.js";
|
|
6
|
+
import { FEEDBACK_FIELD } from "./x402.js";
|
|
6
7
|
import { ISSUES, NOTE_MAX_LENGTH, OUTCOMES, PROTOCOL } from "./values.js";
|
|
7
8
|
const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
8
9
|
const ANNOTATIONS = new Set(["$ref", "description", "title", "summary", "example", "examples", "deprecated", "readOnly", "writeOnly", "$comment"]);
|
|
@@ -38,21 +39,30 @@ function resolveRef(doc, ref) {
|
|
|
38
39
|
}
|
|
39
40
|
function feedbackProperties(rateUrl, hintField = false) {
|
|
40
41
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
[FEEDBACK_FIELD]: {
|
|
43
|
+
type: "object",
|
|
44
|
+
description: "Added by Forge to paid responses: how to rate this call. See x-forge-feedback.",
|
|
45
|
+
required: ["feedback_id", "feedback_url"],
|
|
46
|
+
properties: {
|
|
47
|
+
feedback_id: {
|
|
48
|
+
type: "string",
|
|
49
|
+
pattern: FEEDBACK_ID_PATTERN.source,
|
|
50
|
+
description: "Rating ID for this paid call (also sent as the Forge-Feedback-Id header).",
|
|
51
|
+
},
|
|
52
|
+
feedback_url: {
|
|
53
|
+
type: "string",
|
|
54
|
+
format: "uri-reference",
|
|
55
|
+
description: `Quick-rating URL with outcome left blank; outcome values: ${Object.keys(OUTCOMES).join(", ")}. Base: ${rateUrl}`,
|
|
56
|
+
},
|
|
57
|
+
...(hintField ? { rate_this_call: { type: "string", description: "How to rate this paid call in one free request." } } : {}),
|
|
58
|
+
},
|
|
51
59
|
},
|
|
52
60
|
};
|
|
53
61
|
}
|
|
54
62
|
/** Returns an extended copy of an object schema, or a reason it can't be extended safely. */
|
|
55
63
|
function extendSchema(doc, schema, props) {
|
|
64
|
+
if (schema === true)
|
|
65
|
+
schema = {}; // `true` accepts any value, like {}
|
|
56
66
|
if (!isObj(schema))
|
|
57
67
|
return { reason: "no_schema" };
|
|
58
68
|
let target = schema;
|
|
@@ -75,7 +85,10 @@ function extendSchema(doc, schema, props) {
|
|
|
75
85
|
if (COMPOSITION.some((k) => k in copy))
|
|
76
86
|
return { reason: "composition" };
|
|
77
87
|
const types = Array.isArray(copy.type) ? copy.type : [copy.type];
|
|
78
|
-
|
|
88
|
+
// {} (annotations only) accepts any value, e.g. FastAPI's default response schema. `properties` only constrains
|
|
89
|
+
// objects, so documenting the field there narrows nothing.
|
|
90
|
+
const anyValue = Object.keys(copy).every((k) => ANNOTATIONS.has(k));
|
|
91
|
+
const objectish = anyValue || types.includes("object") || (copy.type === undefined && (isObj(copy.properties) || Array.isArray(copy.allOf)));
|
|
79
92
|
if (!objectish)
|
|
80
93
|
return { reason: "not_object" };
|
|
81
94
|
if ("propertyNames" in copy || "maxProperties" in copy)
|
|
@@ -100,7 +113,7 @@ function extendSchema(doc, schema, props) {
|
|
|
100
113
|
return { schema: copy };
|
|
101
114
|
}
|
|
102
115
|
const BODYLESS = new Set(["get", "head", "delete", "options", "trace"]);
|
|
103
|
-
/** Document
|
|
116
|
+
/** Document agent context on a paid operation; skip schemas that cannot be extended safely. */
|
|
104
117
|
function addAgentContext(doc, version, item, method, op, opts, report) {
|
|
105
118
|
const resolve = (v) => (isObj(v) && typeof v.$ref === "string" ? resolveRef(doc, v.$ref) : v);
|
|
106
119
|
const listed = [...(Array.isArray(item.parameters) ? item.parameters : []), ...(Array.isArray(op.parameters) ? op.parameters : [])].map(resolve).filter(isObj);
|
|
@@ -115,8 +128,8 @@ function addAgentContext(doc, version, item, method, op, opts, report) {
|
|
|
115
128
|
if (!params.length)
|
|
116
129
|
return notAdded("parameter_collision");
|
|
117
130
|
const documented = params.map((p) => version === "2.0"
|
|
118
|
-
? { name: p.name, in: "query", required:
|
|
119
|
-
: { name: p.name, in: "query", required:
|
|
131
|
+
? { name: p.name, in: "query", required: !!opts.required && p.name !== "agent_type_other", description: p.description, ...p.schema }
|
|
132
|
+
: { name: p.name, in: "query", required: !!opts.required && p.name !== "agent_type_other", description: p.description, schema: p.schema });
|
|
120
133
|
op.parameters = [...(Array.isArray(op.parameters) ? op.parameters : []), ...documented];
|
|
121
134
|
report.agentContext = "query";
|
|
122
135
|
return;
|
|
@@ -130,6 +143,10 @@ function addAgentContext(doc, version, item, method, op, opts, report) {
|
|
|
130
143
|
const result = extendSchema(doc, param.schema, props);
|
|
131
144
|
if ("reason" in result)
|
|
132
145
|
return notAdded(result.reason);
|
|
146
|
+
if (opts.required) {
|
|
147
|
+
result.schema.required = [...new Set([...(Array.isArray(result.schema.required) ? result.schema.required : []), CONTEXT_FIELD])];
|
|
148
|
+
param.required = true;
|
|
149
|
+
}
|
|
133
150
|
param.schema = result.schema;
|
|
134
151
|
op.parameters = params.map((p, i) => (i === index ? param : p)); // inline copy if it was a shared $ref
|
|
135
152
|
report.agentContext = "body";
|
|
@@ -146,8 +163,12 @@ function addAgentContext(doc, version, item, method, op, opts, report) {
|
|
|
146
163
|
const result = extendSchema(doc, entry.schema, props);
|
|
147
164
|
if ("reason" in result)
|
|
148
165
|
return notAdded(result.reason);
|
|
166
|
+
if (opts.required)
|
|
167
|
+
result.schema.required = [...new Set([...(Array.isArray(result.schema.required) ? result.schema.required : []), CONTEXT_FIELD])];
|
|
149
168
|
entry.schema = result.schema;
|
|
150
169
|
}
|
|
170
|
+
if (opts.required)
|
|
171
|
+
copy.required = true;
|
|
151
172
|
op.requestBody = copy; // inline copy if it was a shared $ref
|
|
152
173
|
report.agentContext = "body";
|
|
153
174
|
}
|
|
@@ -161,20 +182,21 @@ function appendSentence(text, sentence, marker) {
|
|
|
161
182
|
}
|
|
162
183
|
/** Work out the path prefix document paths are relative to, and whether it's this service's origin. */
|
|
163
184
|
function serverBase(doc, version, publicUrl) {
|
|
164
|
-
const origin = new URL(publicUrl).origin;
|
|
185
|
+
const origin = publicUrl ? new URL(publicUrl).origin : undefined;
|
|
186
|
+
const baseUrl = publicUrl ?? "https://forge.invalid";
|
|
165
187
|
try {
|
|
166
188
|
if (version === "2.0") {
|
|
167
|
-
const scheme = Array.isArray(doc.schemes) && doc.schemes[0] ? doc.schemes[0] : new URL(
|
|
189
|
+
const scheme = Array.isArray(doc.schemes) && doc.schemes[0] ? doc.schemes[0] : new URL(baseUrl).protocol.slice(0, -1);
|
|
168
190
|
const base = typeof doc.basePath === "string" ? doc.basePath : "";
|
|
169
|
-
const url = typeof doc.host === "string" ? new URL(`${scheme}://${doc.host}${base}`) : new URL(base || "/",
|
|
170
|
-
return { prefix: url.pathname.replace(/\/+$/, ""), sameOrigin: url.origin === origin };
|
|
191
|
+
const url = typeof doc.host === "string" ? new URL(`${scheme}://${doc.host}${base}`) : new URL(base || "/", baseUrl);
|
|
192
|
+
return { prefix: url.pathname.replace(/\/+$/, ""), sameOrigin: !origin || url.origin === origin };
|
|
171
193
|
}
|
|
172
194
|
const server = Array.isArray(doc.servers) ? doc.servers[0] : undefined;
|
|
173
195
|
if (!isObj(server) || typeof server.url !== "string")
|
|
174
196
|
return { prefix: "", sameOrigin: true };
|
|
175
197
|
const raw = server.url.replace(/\{([^}]+)\}/g, (_, name) => server.variables?.[name]?.default ?? "");
|
|
176
|
-
const url = new URL(raw,
|
|
177
|
-
return { prefix: url.pathname.replace(/\/+$/, ""), sameOrigin: url.origin === origin };
|
|
198
|
+
const url = new URL(raw, baseUrl);
|
|
199
|
+
return { prefix: url.pathname.replace(/\/+$/, ""), sameOrigin: !origin || url.origin === origin };
|
|
178
200
|
}
|
|
179
201
|
catch {
|
|
180
202
|
return { prefix: "", sameOrigin: false };
|
|
@@ -253,7 +275,7 @@ export function enrichOpenApi(input, options) {
|
|
|
253
275
|
}
|
|
254
276
|
try {
|
|
255
277
|
const doc = clone(original);
|
|
256
|
-
const origin = new URL(options.publicUrl).origin;
|
|
278
|
+
const origin = options.publicUrl ? new URL(options.publicUrl).origin : "";
|
|
257
279
|
const base = options.basePath.replace(/\/+$/, "");
|
|
258
280
|
const rateUrl = `${origin}${base}/rate`;
|
|
259
281
|
const formUrl = `${origin}${base}`;
|
|
@@ -287,6 +309,11 @@ export function enrichOpenApi(input, options) {
|
|
|
287
309
|
continue;
|
|
288
310
|
const opReport = { method: method.toUpperCase(), path, responses: {}, reasons: [] };
|
|
289
311
|
report.operations.push(opReport);
|
|
312
|
+
if (options.feedback === false) {
|
|
313
|
+
if (options.agentContext)
|
|
314
|
+
addAgentContext(doc, version, item, method, op, options.agentContext, opReport);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
290
317
|
if (options.describeOperations ?? true)
|
|
291
318
|
op.description = appendSentence(op.description, options.sentence, options.marker);
|
|
292
319
|
const produces = (op.produces ?? doc.produces);
|
|
@@ -344,6 +371,10 @@ export function enrichOpenApi(input, options) {
|
|
|
344
371
|
}
|
|
345
372
|
}
|
|
346
373
|
}
|
|
374
|
+
if (options.feedback === false) {
|
|
375
|
+
report.enriched = Boolean(options.agentContext);
|
|
376
|
+
return { document: doc, report };
|
|
377
|
+
}
|
|
347
378
|
// Feedback routes. Paths are relative to the server prefix when it's this origin;
|
|
348
379
|
// otherwise (3.x) the path items carry their own servers entry.
|
|
349
380
|
const inPrefix = sameOrigin && (base === prefix || base.startsWith(`${prefix}/`));
|
|
@@ -359,7 +390,7 @@ export function enrichOpenApi(input, options) {
|
|
|
359
390
|
}
|
|
360
391
|
else {
|
|
361
392
|
const ops = feedbackOperations(version, formUrl, rateUrl, operationIds);
|
|
362
|
-
const servers = inPrefix ? {} : { servers: [{ url: origin }] };
|
|
393
|
+
const servers = inPrefix ? {} : { servers: [{ url: origin || "/" }] };
|
|
363
394
|
paths[docBase] = { ...servers, get: ops.form, post: ops.submit };
|
|
364
395
|
paths[`${docBase}/rate`] = { ...servers, get: ops.rate };
|
|
365
396
|
paths[`${docBase}/summary`] = { ...servers, get: ops.summary };
|
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
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { type Tone } from "./ask.js";
|
|
2
|
+
import type { ContextOptions } from "./context.js";
|
|
2
3
|
/** Key of the Forge extension in x402 v2 `extensions`: in the 402 challenge (next to e.g. `bazaar`) and in the payment receipt. */
|
|
3
4
|
export declare const FEEDBACK_EXTENSION = "forge-feedback";
|
|
5
|
+
/** Key of the object the SDK adds to paid JSON response bodies: feedback_id, feedback_url, and optionally rate_this_call. */
|
|
6
|
+
export declare const FEEDBACK_FIELD = "forge_feedback";
|
|
7
|
+
/**
|
|
8
|
+
* The service origin a challenge was issued for: v2 `resource.url` (header or body), or v1 `accepts[].resource`.
|
|
9
|
+
* Used for absolute rating links when no origin is configured or registered. Never throws.
|
|
10
|
+
*/
|
|
11
|
+
export declare function challengeOrigin(challenge: string | unknown): string | undefined;
|
|
12
|
+
/** The service origin a payment was made for (x402 v2 `resource.url` in the payment header). Never throws. */
|
|
13
|
+
export declare function paymentOrigin(headerValue: string): string | undefined;
|
|
4
14
|
/**
|
|
5
15
|
* The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
|
|
6
16
|
* the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
|
|
7
|
-
*
|
|
17
|
+
* Agent context is a separate switch with its own extension (`forge-agent-context`).
|
|
8
18
|
*/
|
|
9
|
-
export declare function feedbackExtension(rateUrl: string, tone?: Tone
|
|
19
|
+
export declare function feedbackExtension(rateUrl: string, tone?: Tone): {
|
|
10
20
|
info: {
|
|
11
|
-
agent_context?: string | undefined;
|
|
12
21
|
rate: string;
|
|
13
22
|
outcome: string[];
|
|
14
23
|
issue: {
|
|
@@ -42,19 +51,30 @@ export declare function receiptExtension(rateUrl: string, feedbackId: string, to
|
|
|
42
51
|
* Returns undefined when the header can't be parsed, settlement failed, or the extension is already there.
|
|
43
52
|
*/
|
|
44
53
|
export declare function describeReceipt(headerValue: string, extension: unknown): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The longest challenge description the SDK produces. Clients copy the description into the payment payload,
|
|
56
|
+
* and the CDP facilitator rejects a payload whose resource.description exceeds 500 characters, failing the payment.
|
|
57
|
+
*/
|
|
58
|
+
export declare const MAX_DESCRIPTION = 500;
|
|
45
59
|
/** What the SDK adds to a challenge. Each part is skipped when already present. */
|
|
46
60
|
export interface ChallengeAdditions {
|
|
47
61
|
/** Appended to the description (v2 resource.description, v1 accepts[].description). */
|
|
48
62
|
sentence?: string;
|
|
63
|
+
/** Appended instead when `sentence` would pass MAX_DESCRIPTION; the description is left alone when this doesn't fit either. */
|
|
64
|
+
shortSentence?: string;
|
|
49
65
|
/** Idempotency marker for the sentence. Default: the sentence itself. */
|
|
50
66
|
marker?: string;
|
|
51
67
|
/** Added as extensions["forge-feedback"] (v2 only; v1 challenges have no extensions). */
|
|
52
68
|
extension?: unknown;
|
|
69
|
+
/** Added as extensions["forge-agent-context"] whenever agent context is enabled, independent of feedback. */
|
|
70
|
+
contextExtension?: unknown;
|
|
71
|
+
/** Declare agent context in the merchant's `bazaar` extension (schema and example together). */
|
|
72
|
+
bazaarContext?: ContextOptions;
|
|
53
73
|
}
|
|
54
74
|
/**
|
|
55
|
-
* Add the rating sentence and
|
|
56
|
-
* `accepts` is untouched: v2 matches payments on `accepts
|
|
57
|
-
* the server
|
|
75
|
+
* Add the rating sentence, Forge's extensions and Bazaar agent context to a base64 PAYMENT-REQUIRED header (x402 v2).
|
|
76
|
+
* `accepts` is untouched: v2 matches payments on `accepts`. @x402/core checks that each echoed extension's `info`
|
|
77
|
+
* contains what the server advertised, so added extensions and added Bazaar fields don't affect payment.
|
|
58
78
|
* Returns undefined when the header can't be parsed or already has everything.
|
|
59
79
|
*/
|
|
60
80
|
export declare function describeChallenge(headerValue: string, additions: ChallengeAdditions): string | undefined;
|
package/dist/x402.js
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
import { ASK } from "./ask.js";
|
|
2
|
+
import { addContextToBazaar } from "./bazaar.js";
|
|
2
3
|
import { ISSUES, PROTOCOL } from "./values.js";
|
|
3
4
|
/** Key of the Forge extension in x402 v2 `extensions`: in the 402 challenge (next to e.g. `bazaar`) and in the payment receipt. */
|
|
4
5
|
export const FEEDBACK_EXTENSION = "forge-feedback";
|
|
6
|
+
/** Key of the object the SDK adds to paid JSON response bodies: feedback_id, feedback_url, and optionally rate_this_call. */
|
|
7
|
+
export const FEEDBACK_FIELD = "forge_feedback";
|
|
8
|
+
const originOf = (url) => {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(String(url));
|
|
11
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.origin : undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The service origin a challenge was issued for: v2 `resource.url` (header or body), or v1 `accepts[].resource`.
|
|
19
|
+
* Used for absolute rating links when no origin is configured or registered. Never throws.
|
|
20
|
+
*/
|
|
21
|
+
export function challengeOrigin(challenge) {
|
|
22
|
+
try {
|
|
23
|
+
const value = typeof challenge === "string" ? JSON.parse(Buffer.from(challenge, "base64").toString("utf8")) : challenge;
|
|
24
|
+
const resource = value?.resource?.url ?? (Array.isArray(value?.accepts) ? value.accepts[0]?.resource : undefined);
|
|
25
|
+
return originOf(resource);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The service origin a payment was made for (x402 v2 `resource.url` in the payment header). Never throws. */
|
|
32
|
+
export function paymentOrigin(headerValue) {
|
|
33
|
+
try {
|
|
34
|
+
return originOf(JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"))?.resource?.url);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
5
40
|
/**
|
|
6
41
|
* The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
|
|
7
42
|
* the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
|
|
8
|
-
*
|
|
43
|
+
* Agent context is a separate switch with its own extension (`forge-agent-context`).
|
|
9
44
|
*/
|
|
10
|
-
export function feedbackExtension(rateUrl, tone = "soft"
|
|
45
|
+
export function feedbackExtension(rateUrl, tone = "soft") {
|
|
11
46
|
return {
|
|
12
47
|
info: {
|
|
13
48
|
protocol: PROTOCOL,
|
|
@@ -16,9 +51,8 @@ export function feedbackExtension(rateUrl, tone = "soft", agentContext) {
|
|
|
16
51
|
rate: `GET ${rateUrl}?feedback_id=FEEDBACK_ID&outcome=fully`,
|
|
17
52
|
outcome: ["fully", "partially", "no"],
|
|
18
53
|
issue: { when: "The outcome is not fully. Tells the merchant what to fix.", values: Object.keys(ISSUES) },
|
|
19
|
-
feedback_id:
|
|
54
|
+
feedback_id: `In the paid response body (${FEEDBACK_FIELD}.feedback_id) and the Forge-Feedback-Id header.`,
|
|
20
55
|
payment: "None. Plain GET, not an x402 endpoint.",
|
|
21
|
-
...(agentContext ? { agent_context: agentContext } : {}),
|
|
22
56
|
},
|
|
23
57
|
};
|
|
24
58
|
}
|
|
@@ -57,39 +91,59 @@ export function describeReceipt(headerValue, extension) {
|
|
|
57
91
|
receipt.extensions = { ...extensions, [FEEDBACK_EXTENSION]: extension };
|
|
58
92
|
return Buffer.from(JSON.stringify(receipt), "utf8").toString("base64");
|
|
59
93
|
}
|
|
60
|
-
|
|
94
|
+
/**
|
|
95
|
+
* The longest challenge description the SDK produces. Clients copy the description into the payment payload,
|
|
96
|
+
* and the CDP facilitator rejects a payload whose resource.description exceeds 500 characters, failing the payment.
|
|
97
|
+
*/
|
|
98
|
+
export const MAX_DESCRIPTION = 500;
|
|
99
|
+
function appendSentence(description, sentence, marker, shortSentence) {
|
|
61
100
|
const current = typeof description === "string" ? description.trim() : "";
|
|
62
|
-
|
|
101
|
+
if (current.includes(marker))
|
|
102
|
+
return current;
|
|
103
|
+
for (const candidate of [sentence, shortSentence]) {
|
|
104
|
+
if (!candidate)
|
|
105
|
+
continue;
|
|
106
|
+
const next = current ? `${current} ${candidate}` : candidate;
|
|
107
|
+
if (next.length <= MAX_DESCRIPTION)
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
// Nothing fits: the forge-feedback extension still carries the ask.
|
|
111
|
+
return current;
|
|
63
112
|
}
|
|
64
113
|
/** Apply additions to a v2 PaymentRequired in place. Returns whether anything changed. */
|
|
65
114
|
function addToV2(challenge, add) {
|
|
66
115
|
let changed = false;
|
|
67
116
|
const resource = challenge.resource;
|
|
68
117
|
if (add.sentence && resource && typeof resource === "object") {
|
|
69
|
-
const next = appendSentence(resource.description, add.sentence, add.marker ?? add.sentence);
|
|
118
|
+
const next = appendSentence(resource.description, add.sentence, add.marker ?? add.sentence, add.shortSentence);
|
|
70
119
|
if (next !== resource.description) {
|
|
71
120
|
resource.description = next;
|
|
72
121
|
changed = true;
|
|
73
122
|
}
|
|
74
123
|
}
|
|
75
|
-
|
|
124
|
+
for (const [key, extension] of [[FEEDBACK_EXTENSION, add.extension], ["forge-agent-context", add.contextExtension]]) {
|
|
125
|
+
if (!extension)
|
|
126
|
+
continue;
|
|
76
127
|
const extensions = challenge.extensions;
|
|
77
128
|
if (extensions === undefined || extensions === null) {
|
|
78
|
-
challenge.extensions = { [
|
|
129
|
+
challenge.extensions = { [key]: extension };
|
|
79
130
|
changed = true;
|
|
80
131
|
}
|
|
81
|
-
else if (typeof extensions === "object" && !Array.isArray(extensions) && !(
|
|
82
|
-
// Added last, so the merchant's own extensions (e.g. bazaar) keep their order
|
|
83
|
-
challenge.extensions = { ...extensions, [
|
|
132
|
+
else if (typeof extensions === "object" && !Array.isArray(extensions) && !(key in extensions)) {
|
|
133
|
+
// Added last, so the merchant's own extensions (e.g. bazaar) keep their order.
|
|
134
|
+
challenge.extensions = { ...extensions, [key]: extension };
|
|
84
135
|
changed = true;
|
|
85
136
|
}
|
|
86
137
|
}
|
|
138
|
+
const extensions = challenge.extensions;
|
|
139
|
+
if (add.bazaarContext && extensions && typeof extensions === "object" && addContextToBazaar(extensions.bazaar, add.bazaarContext))
|
|
140
|
+
changed = true;
|
|
87
141
|
return changed;
|
|
88
142
|
}
|
|
89
143
|
/**
|
|
90
|
-
* Add the rating sentence and
|
|
91
|
-
* `accepts` is untouched: v2 matches payments on `accepts
|
|
92
|
-
* the server
|
|
144
|
+
* Add the rating sentence, Forge's extensions and Bazaar agent context to a base64 PAYMENT-REQUIRED header (x402 v2).
|
|
145
|
+
* `accepts` is untouched: v2 matches payments on `accepts`. @x402/core checks that each echoed extension's `info`
|
|
146
|
+
* contains what the server advertised, so added extensions and added Bazaar fields don't affect payment.
|
|
93
147
|
* Returns undefined when the header can't be parsed or already has everything.
|
|
94
148
|
*/
|
|
95
149
|
export function describeChallenge(headerValue, additions) {
|
|
@@ -122,7 +176,7 @@ export function describeChallengeBody(body, additions) {
|
|
|
122
176
|
const marker = additions.marker ?? sentence;
|
|
123
177
|
return {
|
|
124
178
|
...b,
|
|
125
|
-
accepts: b.accepts.map((a) => a && typeof a === "object" ? { ...a, description: appendSentence(a.description, sentence, marker) } : a),
|
|
179
|
+
accepts: b.accepts.map((a) => a && typeof a === "object" ? { ...a, description: appendSentence(a.description, sentence, marker, additions.shortSentence) } : a),
|
|
126
180
|
};
|
|
127
181
|
}
|
|
128
182
|
if (b.x402Version === 2 && b.resource && typeof b.resource === "object") {
|