@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forge Intel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @forgeintel/sdk
2
+
3
+ > **Beta.** Install with `npm install @forgeintel/sdk@beta`. The API may change before 1.0.
4
+
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
+
7
+ ## Express
8
+
9
+ ```js
10
+ import { createForge } from "@forgeintel/sdk";
11
+
12
+ const forge = createForge({
13
+ apiKey: process.env.FORGE_FEEDBACK_KEY, // from the Forge backend
14
+ backendUrl: process.env.FORGE_BACKEND_URL, // your Forge backend
15
+ publicUrl: "https://api.example.com", // your public origin (never taken from the Host header)
16
+ });
17
+
18
+ app.use(forge.middleware()); // 1. first: before payments and your /openapi.json route
19
+ app.use(paymentMiddleware(routes, resourceServer)); // 2. your existing x402 setup, unchanged
20
+ ```
21
+
22
+ Works with `@x402/express` (v2) and `x402-express` (v1), on Express 4.21+ or 5.
23
+
24
+ ## Any other framework
25
+
26
+ `@forgeintel/sdk/core` is the same logic without a framework. Call `forge.route()` first, then `forge.call()` per paid request, and `call.headers()` / `call.json()` / `call.finish()` where you build the 402 and the paid response. See the [core integration guide](https://github.com/ClawCash/forge-feedback/tree/main/examples/node-http-core) for a complete `node:http` + `@x402/core` example.
27
+
28
+ Zero runtime dependencies. Node ≥ 20.19. Works from both `import` and `require()`.
29
+
30
+ ## What it does
31
+
32
+ | When | What happens |
33
+ | --- | --- |
34
+ | 402 challenge | Appends a soft ask to the description: *After your call, please rate this service for other agents: GET …/feedback/rate?feedback_id=FEEDBACK_ID&outcome=fully (or partially, no)…*. On x402 v2 it also adds an `extensions["forge-feedback"]` block. Payment terms and your own extensions are never touched. |
35
+ | 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 them. |
36
+ | `GET /openapi.json` | Your document is served enriched: feedback routes documented, feedback fields added to paid response schemas, one sentence in `x-guidance`. |
37
+ | `GET /feedback/rate` | Quick rating: `feedback_id`, `outcome`, optional `issue`. |
38
+ | `POST /feedback` | Same fields, plus an optional `note` (≤ 280 chars). |
39
+ | `GET /feedback`, `GET /feedback/summary` | The form description, and public aggregate ratings. |
40
+ | Every 402 and paid call | Sends a challenge or interaction event to the backend in the background. |
41
+
42
+ IDs are minted locally with an HMAC, so the paid path makes no network call. If the backend is down, paid calls still work, and ratings return `503 feedback_unavailable`.
43
+
44
+ ## Compatibility
45
+
46
+ | | x402 v1 (`x402-express` 1.x) | x402 v2 (`@x402/express` 2.x, `@x402/core`) |
47
+ | --- | --- | --- |
48
+ | Challenge | JSON body, `accepts[].description` | `PAYMENT-REQUIRED` header: `resource.description` and `extensions` |
49
+ | Payment header | `X-PAYMENT` | `PAYMENT-SIGNATURE` |
50
+ | Why the changes are safe | Servers match payments against their own requirements | Payments match on `accepts`; `@x402/core` only validates echoed extensions the server advertised |
51
+
52
+ OpenAPI: Swagger 2.0 and OpenAPI 3.0, 3.1 and 3.2. Additive only; shared components are never edited; strict schemas get the header only; idempotent; YAML and anything unexpected pass through byte-for-byte.
53
+
54
+ ## Options
55
+
56
+ | Option | Default | |
57
+ | --- | --- | --- |
58
+ | `apiKey` | required | Merchant key. Also the HMAC key for IDs. |
59
+ | `backendUrl` | required | Forge backend origin. |
60
+ | `publicUrl` | required | This service's public origin. Used in rating URLs. |
61
+ | `basePath` | `/feedback` | Feedback routes (`/feedback`, `/feedback/rate`, `/feedback/summary`). |
62
+ | `describeChallenges` | `true` | Append the sentence to 402 challenges. |
63
+ | `challengeSentence` | built-in | Override for wording experiments. `{rate_url}` and `{summary_url}` are substituted. |
64
+ | `challengeExtension` | `true` | Add the `forge-feedback` extension to x402 v2 challenges. |
65
+ | `injectBody` | `true` | Add `feedback_id` / `feedback_url` to paid JSON bodies. |
66
+ | `rateHint` | built-in | The `rate_this_call` field. A string overrides it (`{feedback_url}` is substituted); `false` removes it. |
67
+ | `injectText` | `false` | Append a two-line trailer to paid `text/plain` bodies. |
68
+ | `openapi` | intercept `/openapi.json` | `false` to disable, or `{ paths, document, isPaidOperation, describeOperations }`. |
69
+ | `ttlMs` | 24h | Local pre-check; keep in sync with the backend. |
70
+ | `flushIntervalMs` | `2000` | Event batching interval. |
71
+ | `onError` | `console.warn` | Called for internal errors. Business requests are never affected. |
72
+
73
+ `forge.diagnostics()` returns counters and the last OpenAPI report. `forge.enrichOpenApi(doc)` is available for build-time use. Call `await forge.shutdown()` in your shutdown handler to flush pending events.
74
+
75
+ Source, examples and design notes: [github.com/ClawCash/forge-feedback](https://github.com/ClawCash/forge-feedback).
package/dist/core.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { type EnrichReport } from "./openapi.js";
2
+ export interface OpenApiOptions {
3
+ /** Paths where your OpenAPI JSON is served. Default ["/openapi.json"]. */
4
+ paths?: string[];
5
+ /** Serve this document (enriched) instead of intercepting your own route. May be an async provider. */
6
+ document?: unknown | (() => unknown | Promise<unknown>);
7
+ /** Override paid-operation detection. Default: the operation declares a 402 response or x-payment-info. */
8
+ isPaidOperation?: (method: string, path: string, operation: Record<string, any>) => boolean;
9
+ /** Append the rating sentence to paid operation descriptions. Default true. */
10
+ describeOperations?: boolean;
11
+ }
12
+ /** @deprecated Renamed to ForgeOptions. */
13
+ export type ForgeFeedbackOptions = ForgeOptions;
14
+ export interface ForgeOptions {
15
+ /** Merchant API key issued by the Forge backend. Also the HMAC key for feedback IDs. */
16
+ apiKey: string;
17
+ /** Forge backend origin, e.g. https://forge-feedback.up.railway.app */
18
+ backendUrl: string;
19
+ /** This service's public origin, e.g. https://pixels.gateway.clawca.sh. Never derived from the Host header. */
20
+ publicUrl: string;
21
+ /** Path for the feedback routes. Default "/feedback" (quick rating at "/feedback/rate"). */
22
+ basePath?: string;
23
+ /** Append the rating sentence to x402 challenges (v2 header, v1 JSON body). Default true. */
24
+ describeChallenges?: boolean;
25
+ /** Override the appended sentence, for wording experiments. "{rate_url}" and "{summary_url}" are substituted. */
26
+ challengeSentence?: string;
27
+ /**
28
+ * Add a structured `forge-feedback` extension to x402 v2 challenges (next to e.g. `bazaar`), which clients
29
+ * that inspect the 402 print as its own block. Default true; `false` turns it off. v1 challenges have no extensions.
30
+ */
31
+ challengeExtension?: boolean;
32
+ /** Add feedback_id and feedback_url to JSON object bodies of paid responses. Default true. */
33
+ injectBody?: boolean;
34
+ /**
35
+ * The `rate_this_call` sentence added to paid JSON bodies: a soft, one-request ask next to the data the agent paid for.
36
+ * On by default; a string overrides the wording ("{feedback_url}" and "{summary_url}" are substituted); `false` turns it off.
37
+ */
38
+ rateHint?: boolean | string;
39
+ /**
40
+ * Append a two-line "feedback_id: … / feedback_url: …" trailer to text/plain paid responses. Default false.
41
+ * Changes the business payload, so enable it only when agents can't otherwise see the ID (e.g. awal shows
42
+ * the model the body but never headers).
43
+ */
44
+ injectText?: boolean;
45
+ /** Enrich your OpenAPI document. Default: intercept GET /openapi.json. `false` disables. */
46
+ openapi?: false | OpenApiOptions;
47
+ /** Must match the backend's FEEDBACK_TTL_HOURS; only used to reject stale IDs before a network call. */
48
+ ttlMs?: number;
49
+ flushIntervalMs?: number;
50
+ onError?: (error: unknown) => void;
51
+ fetch?: typeof fetch;
52
+ /**
53
+ * Throw on invalid options instead of warning and running disabled. Default false: a misconfigured Forge
54
+ * logs a warning, reports it in diagnostics(), and leaves your API running unchanged. Use true in CI.
55
+ */
56
+ strict?: boolean;
57
+ }
58
+ /** An incoming request, as the core needs to see it. */
59
+ export interface ForgeRequest {
60
+ method: string;
61
+ /** Path relative to where Forge is mounted, without the query string. */
62
+ path: string;
63
+ header(name: string): string | undefined;
64
+ /** A query parameter; arrays are fine (the first value is used). */
65
+ query(name: string): unknown;
66
+ /** Parse the JSON body. Only called for POST {basePath}. Throw on invalid JSON or bodies over BODY_LIMIT. */
67
+ json(): Promise<unknown>;
68
+ }
69
+ /** A response the core produces itself (rating routes, a served spec). `body` is JSON; absent for 405. */
70
+ export interface ForgeResponse {
71
+ status: number;
72
+ headers: Record<string, string>;
73
+ body?: unknown;
74
+ }
75
+ /** One request to a merchant route. Every method is fail-safe: on any error it returns the input unchanged. */
76
+ export interface ForgeCall {
77
+ /** Set when the request carried a payment header (x402 v2 PAYMENT-SIGNATURE or v1 X-PAYMENT). */
78
+ readonly feedbackId: string | undefined;
79
+ /** A JSON body about to be sent: 402 challenges get the rating ask, paid 2xx objects get the feedback fields. */
80
+ json(status: number, body: unknown): unknown;
81
+ /** A text body about to be sent: gets the two-line trailer on paid 2xx text/plain when injectText is on. */
82
+ text(status: number, contentType: string, body: string): string;
83
+ /** Headers to set once the final status is known: a rewritten PAYMENT-REQUIRED on 402, Forge-Feedback-Id on paid 2xx. */
84
+ headers(status: number, paymentRequired: string | undefined): Record<string, string>;
85
+ /**
86
+ * Report the challenge / interaction events after the response is sent. A 402 counts as a challenge when
87
+ * headers() saw a PAYMENT-REQUIRED value or json() saw a challenge body; pass true if you know of one otherwise.
88
+ */
89
+ finish(status: number, hadChallengeHeader?: boolean): void;
90
+ }
91
+ export interface ForgeDiagnostics {
92
+ /** False when invalid options turned Forge off; the API runs unchanged. */
93
+ enabled: boolean;
94
+ /** Why Forge is disabled (invalid required options). */
95
+ configErrors: string[];
96
+ /** Invalid optional values that were replaced by their defaults. */
97
+ configWarnings: string[];
98
+ minted: number;
99
+ challengesDescribed: number;
100
+ eventsSent: number;
101
+ eventsDropped: number;
102
+ openapi: EnrichReport | null;
103
+ lastError?: string;
104
+ }
105
+ export interface ForgeCore {
106
+ /** False when invalid options turned Forge off. Every method then passes everything through. */
107
+ readonly enabled: boolean;
108
+ /** The sentence appended to x402 challenges and OpenAPI guidance. */
109
+ readonly challengeSentence: string;
110
+ /** Handle Forge's own routes ({basePath}, /rate, /summary, a static openapi.document). Null when not ours. */
111
+ route(request: ForgeRequest): Promise<ForgeResponse | null>;
112
+ /** Whether this is a GET for the merchant's own OpenAPI route, which the adapter should buffer and pass to enrichOpenApi. */
113
+ isSpecRequest(method: string, path: string): boolean;
114
+ /** Enrich an OpenAPI document (pure; the input is never mutated). Also lets body injection respect its schemas. */
115
+ enrichOpenApi(document: unknown): {
116
+ document: unknown;
117
+ report: EnrichReport;
118
+ };
119
+ /** Start observing a request to a merchant route. `path` is the full request path (used for route names). */
120
+ call(request: Pick<ForgeRequest, "method" | "path" | "header">): ForgeCall;
121
+ onError(error: unknown): void;
122
+ diagnostics(): ForgeDiagnostics;
123
+ /** Flush pending events. Call from your shutdown handler. */
124
+ shutdown(): Promise<void>;
125
+ }
126
+ export declare const FEEDBACK_HEADERS: {
127
+ "Cache-Control": string;
128
+ "X-Robots-Tag": string;
129
+ };
130
+ /** Max JSON body for POST {basePath}. */
131
+ export declare const BODY_LIMIT: number;
132
+ /** Max OpenAPI document an adapter should buffer for enrichment. */
133
+ export declare const SPEC_LIMIT: number;
134
+ /**
135
+ * Check options without throwing. Invalid required options are errors (Forge runs disabled);
136
+ * invalid optional values are warnings and fall back to their defaults.
137
+ */
138
+ export declare function checkOptions(input: unknown): {
139
+ errors: string[];
140
+ warnings: string[];
141
+ options: ForgeOptions;
142
+ };
143
+ /**
144
+ * Never throws (unless `strict`): invalid required options log one warning and return a disabled core
145
+ * that passes everything through, so a misconfigured Forge can't take down your API or server.
146
+ */
147
+ export declare function createForgeCore(input: ForgeOptions): ForgeCore;