@forgeintel/sdk 0.4.0-beta.1 → 0.5.0-alpha.feedback030top.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/dist/x402.js CHANGED
@@ -1,13 +1,48 @@
1
1
  import { ASK } from "./ask.js";
2
- import { ISSUES, PROTOCOL } from "./values.js";
2
+ import { addContextToBazaar } from "./bazaar.js";
3
+ import { 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
- * With `agentContext`, it also says how to report agent context with the paid request.
43
+ * Agent context is a separate switch with its own extension (`forge-agent-context`).
9
44
  */
10
- export function feedbackExtension(rateUrl, tone = "soft", agentContext) {
45
+ export function feedbackExtension(rateUrl, tone = "soft") {
11
46
  return {
12
47
  info: {
13
48
  protocol: PROTOCOL,
@@ -15,10 +50,8 @@ export function feedbackExtension(rateUrl, tone = "soft", agentContext) {
15
50
  ...ASK[tone].extensionExtras,
16
51
  rate: `GET ${rateUrl}?feedback_id=FEEDBACK_ID&outcome=fully`,
17
52
  outcome: ["fully", "partially", "no"],
18
- issue: { when: "The outcome is not fully. Tells the merchant what to fix.", values: Object.keys(ISSUES) },
19
- feedback_id: "In the paid response body (feedback_id) and the Forge-Feedback-Id header.",
53
+ feedback_id: `In the paid response body (${FEEDBACK_FIELD}.feedback_id) and the Forge-Feedback-Id header.`,
20
54
  payment: "None. Plain GET, not an x402 endpoint.",
21
- ...(agentContext ? { agent_context: agentContext } : {}),
22
55
  },
23
56
  };
24
57
  }
@@ -31,7 +64,6 @@ export function receiptExtension(rateUrl, feedbackId, tone = "soft") {
31
64
  feedback_id: feedbackId,
32
65
  rate: `GET ${rateUrl}?feedback_id=${feedbackId}&outcome=fully`,
33
66
  outcome: ["fully", "partially", "no"],
34
- issue: { when: "The outcome is not fully. Tells the merchant what to fix.", values: Object.keys(ISSUES) },
35
67
  payment: "None. Plain GET, not an x402 endpoint.",
36
68
  },
37
69
  };
@@ -57,39 +89,59 @@ export function describeReceipt(headerValue, extension) {
57
89
  receipt.extensions = { ...extensions, [FEEDBACK_EXTENSION]: extension };
58
90
  return Buffer.from(JSON.stringify(receipt), "utf8").toString("base64");
59
91
  }
60
- function appendSentence(description, sentence, marker) {
92
+ /**
93
+ * The longest challenge description the SDK produces. Clients copy the description into the payment payload,
94
+ * and the CDP facilitator rejects a payload whose resource.description exceeds 500 characters, failing the payment.
95
+ */
96
+ export const MAX_DESCRIPTION = 500;
97
+ function appendSentence(description, sentence, marker, shortSentence) {
61
98
  const current = typeof description === "string" ? description.trim() : "";
62
- return current.includes(marker) ? current : current ? `${current} ${sentence}` : sentence;
99
+ if (current.includes(marker))
100
+ return current;
101
+ for (const candidate of [sentence, shortSentence]) {
102
+ if (!candidate)
103
+ continue;
104
+ const next = current ? `${current} ${candidate}` : candidate;
105
+ if (next.length <= MAX_DESCRIPTION)
106
+ return next;
107
+ }
108
+ // Nothing fits: the forge-feedback extension still carries the ask.
109
+ return current;
63
110
  }
64
111
  /** Apply additions to a v2 PaymentRequired in place. Returns whether anything changed. */
65
112
  function addToV2(challenge, add) {
66
113
  let changed = false;
67
114
  const resource = challenge.resource;
68
115
  if (add.sentence && resource && typeof resource === "object") {
69
- const next = appendSentence(resource.description, add.sentence, add.marker ?? add.sentence);
116
+ const next = appendSentence(resource.description, add.sentence, add.marker ?? add.sentence, add.shortSentence);
70
117
  if (next !== resource.description) {
71
118
  resource.description = next;
72
119
  changed = true;
73
120
  }
74
121
  }
75
- if (add.extension) {
122
+ for (const [key, extension] of [[FEEDBACK_EXTENSION, add.extension], ["forge-agent-context", add.contextExtension]]) {
123
+ if (!extension)
124
+ continue;
76
125
  const extensions = challenge.extensions;
77
126
  if (extensions === undefined || extensions === null) {
78
- challenge.extensions = { [FEEDBACK_EXTENSION]: add.extension };
127
+ challenge.extensions = { [key]: extension };
79
128
  changed = true;
80
129
  }
81
- else if (typeof extensions === "object" && !Array.isArray(extensions) && !(FEEDBACK_EXTENSION in extensions)) {
82
- // Added last, so the merchant's own extensions (e.g. bazaar) keep their order and content.
83
- challenge.extensions = { ...extensions, [FEEDBACK_EXTENSION]: add.extension };
130
+ else if (typeof extensions === "object" && !Array.isArray(extensions) && !(key in extensions)) {
131
+ // Added last, so the merchant's own extensions (e.g. bazaar) keep their order.
132
+ challenge.extensions = { ...extensions, [key]: extension };
84
133
  changed = true;
85
134
  }
86
135
  }
136
+ const extensions = challenge.extensions;
137
+ if (add.bazaarContext && extensions && typeof extensions === "object" && addContextToBazaar(extensions.bazaar, add.bazaarContext))
138
+ changed = true;
87
139
  return changed;
88
140
  }
89
141
  /**
90
- * Add the rating sentence and/or the forge-feedback extension to a base64 PAYMENT-REQUIRED header (x402 v2).
91
- * `accepts` is untouched: v2 matches payments on `accepts`, and @x402/core only checks echoed extensions
92
- * the server itself advertised, so an added extension doesn't affect payment.
142
+ * Add the rating sentence, Forge's extensions and Bazaar agent context to a base64 PAYMENT-REQUIRED header (x402 v2).
143
+ * `accepts` is untouched: v2 matches payments on `accepts`. @x402/core checks that each echoed extension's `info`
144
+ * contains what the server advertised, so added extensions and added Bazaar fields don't affect payment.
93
145
  * Returns undefined when the header can't be parsed or already has everything.
94
146
  */
95
147
  export function describeChallenge(headerValue, additions) {
@@ -122,7 +174,7 @@ export function describeChallengeBody(body, additions) {
122
174
  const marker = additions.marker ?? sentence;
123
175
  return {
124
176
  ...b,
125
- accepts: b.accepts.map((a) => a && typeof a === "object" ? { ...a, description: appendSentence(a.description, sentence, marker) } : a),
177
+ accepts: b.accepts.map((a) => a && typeof a === "object" ? { ...a, description: appendSentence(a.description, sentence, marker, additions.shortSentence) } : a),
126
178
  };
127
179
  }
128
180
  if (b.x402Version === 2 && b.resource && typeof b.resource === "object") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forgeintel/sdk",
3
- "version": "0.4.0-beta.1",
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.",
3
+ "version": "0.5.0-alpha.feedback030top.b13",
4
+ "description": "The Forge SDK for x402 paid APIs: agent feedback, agent context, OpenAPI and challenge enrichment, and passive call signals. Express, Hono, Next.js and fetch handlers. No Forge network request on the merchant response path.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
@@ -61,11 +61,7 @@
61
61
  "cloudflare-workers",
62
62
  "bun"
63
63
  ],
64
- "repository": {
65
- "type": "git",
66
- "url": "git+https://github.com/ClawCash/forge-feedback.git",
67
- "directory": "packages/sdk"
68
- },
64
+ "homepage": "https://docs.forgeintel.co",
69
65
  "scripts": {
70
66
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
71
67
  "test": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\" && tsc -p tsconfig.test.json && node --test --test-reporter=spec \"dist-test/test/**/*.test.js\"",
@@ -73,7 +69,7 @@
73
69
  },
74
70
  "publishConfig": {
75
71
  "access": "public",
76
- "tag": "beta"
72
+ "tag": "latest"
77
73
  },
78
74
  "peerDependencies": {
79
75
  "express": ">=4.21 <6",