@forgeintel/sdk 0.2.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/dist/x402.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /** Key of the Forge extension in an x402 v2 challenge's `extensions`, next to e.g. `bazaar`. */
2
+ export declare const FEEDBACK_EXTENSION = "forge-feedback";
3
+ /**
4
+ * The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
5
+ * the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
6
+ */
7
+ export declare function feedbackExtension(rateUrl: string): {
8
+ info: {
9
+ protocol: string;
10
+ ask: string;
11
+ rate: string;
12
+ outcome: string[];
13
+ feedback_id: string;
14
+ payment: string;
15
+ };
16
+ };
17
+ /** What the SDK adds to a challenge. Each part is skipped when already present. */
18
+ export interface ChallengeAdditions {
19
+ /** Appended to the description (v2 resource.description, v1 accepts[].description). */
20
+ sentence?: string;
21
+ /** Idempotency marker for the sentence. Default: the sentence itself. */
22
+ marker?: string;
23
+ /** Added as extensions["forge-feedback"] (v2 only; v1 challenges have no extensions). */
24
+ extension?: unknown;
25
+ }
26
+ /**
27
+ * Add the rating sentence and/or the forge-feedback extension to a base64 PAYMENT-REQUIRED header (x402 v2).
28
+ * `accepts` is untouched: v2 matches payments on `accepts`, and @x402/core only checks echoed extensions
29
+ * the server itself advertised, so an added extension doesn't affect payment.
30
+ * Returns undefined when the header can't be parsed or already has everything.
31
+ */
32
+ export declare function describeChallenge(headerValue: string, additions: ChallengeAdditions): string | undefined;
33
+ /**
34
+ * Same for a JSON-body challenge: x402 v1 ({x402Version: 1, accepts: [{description}]}, sentence only)
35
+ * or a v2 PaymentRequired echoed in the body. v1 servers match payments against their own requirements,
36
+ * so descriptions in the response are informational. Returns undefined when nothing applies.
37
+ */
38
+ export declare function describeChallengeBody(body: unknown, additions: ChallengeAdditions): unknown;
39
+ export interface PaymentFacts {
40
+ payer?: string;
41
+ network?: string;
42
+ amount?: string;
43
+ }
44
+ /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
45
+ export declare function readPaymentHeader(headerValue: string): PaymentFacts;
package/dist/x402.js ADDED
@@ -0,0 +1,113 @@
1
+ import { PROTOCOL } from "./values.js";
2
+ /** Key of the Forge extension in an x402 v2 challenge's `extensions`, next to e.g. `bazaar`. */
3
+ export const FEEDBACK_EXTENSION = "forge-feedback";
4
+ /**
5
+ * The `forge-feedback` challenge extension: how to rate the call, as structured data. Clients that inspect
6
+ * the 402 (e.g. `awal x402 details`) print every extension, so this reaches agents before they pay.
7
+ */
8
+ export function feedbackExtension(rateUrl) {
9
+ return {
10
+ info: {
11
+ protocol: PROTOCOL,
12
+ ask: "After your call, please rate this service for other agents. Free, one request.",
13
+ rate: `GET ${rateUrl}?feedback_id=FEEDBACK_ID&outcome=fully`,
14
+ outcome: ["fully", "partially", "no"],
15
+ feedback_id: "In the paid response body (feedback_id) and the Forge-Feedback-Id header.",
16
+ payment: "None. Plain GET, not an x402 endpoint.",
17
+ },
18
+ };
19
+ }
20
+ function appendSentence(description, sentence, marker) {
21
+ const current = typeof description === "string" ? description.trim() : "";
22
+ return current.includes(marker) ? current : current ? `${current} ${sentence}` : sentence;
23
+ }
24
+ /** Apply additions to a v2 PaymentRequired in place. Returns whether anything changed. */
25
+ function addToV2(challenge, add) {
26
+ let changed = false;
27
+ const resource = challenge.resource;
28
+ if (add.sentence && resource && typeof resource === "object") {
29
+ const next = appendSentence(resource.description, add.sentence, add.marker ?? add.sentence);
30
+ if (next !== resource.description) {
31
+ resource.description = next;
32
+ changed = true;
33
+ }
34
+ }
35
+ if (add.extension) {
36
+ const extensions = challenge.extensions;
37
+ if (extensions === undefined || extensions === null) {
38
+ challenge.extensions = { [FEEDBACK_EXTENSION]: add.extension };
39
+ changed = true;
40
+ }
41
+ else if (typeof extensions === "object" && !Array.isArray(extensions) && !(FEEDBACK_EXTENSION in extensions)) {
42
+ // Added last, so the merchant's own extensions (e.g. bazaar) keep their order and content.
43
+ challenge.extensions = { ...extensions, [FEEDBACK_EXTENSION]: add.extension };
44
+ changed = true;
45
+ }
46
+ }
47
+ return changed;
48
+ }
49
+ /**
50
+ * Add the rating sentence and/or the forge-feedback extension to a base64 PAYMENT-REQUIRED header (x402 v2).
51
+ * `accepts` is untouched: v2 matches payments on `accepts`, and @x402/core only checks echoed extensions
52
+ * the server itself advertised, so an added extension doesn't affect payment.
53
+ * Returns undefined when the header can't be parsed or already has everything.
54
+ */
55
+ export function describeChallenge(headerValue, additions) {
56
+ let challenge;
57
+ try {
58
+ challenge = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ if (!challenge || typeof challenge !== "object" || !challenge.resource || typeof challenge.resource !== "object")
64
+ return undefined;
65
+ if (!addToV2(challenge, additions))
66
+ return undefined;
67
+ return Buffer.from(JSON.stringify(challenge), "utf8").toString("base64");
68
+ }
69
+ /**
70
+ * Same for a JSON-body challenge: x402 v1 ({x402Version: 1, accepts: [{description}]}, sentence only)
71
+ * or a v2 PaymentRequired echoed in the body. v1 servers match payments against their own requirements,
72
+ * so descriptions in the response are informational. Returns undefined when nothing applies.
73
+ */
74
+ export function describeChallengeBody(body, additions) {
75
+ if (!body || typeof body !== "object" || Array.isArray(body))
76
+ return undefined;
77
+ const b = body;
78
+ const { sentence } = additions;
79
+ if (b.x402Version === 1 && Array.isArray(b.accepts)) {
80
+ if (!sentence)
81
+ return undefined;
82
+ const marker = additions.marker ?? sentence;
83
+ return {
84
+ ...b,
85
+ accepts: b.accepts.map((a) => a && typeof a === "object" ? { ...a, description: appendSentence(a.description, sentence, marker) } : a),
86
+ };
87
+ }
88
+ if (b.x402Version === 2 && b.resource && typeof b.resource === "object") {
89
+ const copy = { ...b, resource: { ...b.resource } };
90
+ return addToV2(copy, additions) ? copy : undefined;
91
+ }
92
+ return undefined;
93
+ }
94
+ /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
95
+ export function readPaymentHeader(headerValue) {
96
+ try {
97
+ const p = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
98
+ const authorization = p?.payload?.authorization;
99
+ const facts = {
100
+ payer: authorization?.from,
101
+ network: p?.accepted?.network ?? p?.network,
102
+ amount: p?.accepted?.amount ?? authorization?.value,
103
+ };
104
+ for (const key of Object.keys(facts)) {
105
+ if (typeof facts[key] !== "string")
106
+ delete facts[key];
107
+ }
108
+ return facts;
109
+ }
110
+ catch {
111
+ return {};
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@forgeintel/sdk",
3
+ "version": "0.2.0-beta.0",
4
+ "description": "The Forge SDK for x402 paid APIs: agent feedback (feedback IDs, one-request GET ratings), OpenAPI and challenge enrichment, and passive call signals. Express adapter plus a framework-free core. Never on your critical path.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20.19"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./express": {
17
+ "types": "./dist/express.d.ts",
18
+ "import": "./dist/express.js",
19
+ "default": "./dist/express.js"
20
+ },
21
+ "./core": {
22
+ "types": "./dist/core.d.ts",
23
+ "import": "./dist/core.js",
24
+ "default": "./dist/core.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "sideEffects": false,
35
+ "keywords": [
36
+ "x402",
37
+ "feedback",
38
+ "reputation",
39
+ "agents",
40
+ "openapi",
41
+ "express",
42
+ "payments",
43
+ "forge"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/ClawCash/forge-feedback.git",
48
+ "directory": "packages/sdk"
49
+ },
50
+ "scripts": {
51
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
52
+ "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\"",
53
+ "prepublishOnly": "npm run build && npm test"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "tag": "beta"
58
+ },
59
+ "peerDependencies": {
60
+ "express": ">=4.21 <6"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "express": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "devDependencies": {
68
+ "@apidevtools/swagger-parser": "^12.1.0",
69
+ "@types/express": "^5.0.3",
70
+ "@types/node": "^22.18.0",
71
+ "@x402/core": "~2.25.0",
72
+ "@x402/evm": "~2.25.0",
73
+ "@x402/express": "~2.25.0",
74
+ "ajv": "^8.20.0",
75
+ "ajv-formats": "^3.0.1",
76
+ "express": "^5.2.1",
77
+ "typescript": "^5.9.2",
78
+ "viem": "^2.56.3",
79
+ "x402-express": "^1.2.0"
80
+ }
81
+ }