@forgeintel/sdk 0.3.0-beta.0 → 0.4.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/README.md CHANGED
@@ -10,8 +10,8 @@ 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_FEEDBACK_KEY, // from the Forge backend
14
- backendUrl: process.env.FORGE_BACKEND_URL, // your Forge backend
13
+ apiKey: process.env.FORGE_API_KEY, // from your Forge project's Agents page
14
+ backendUrl: process.env.FORGE_BACKEND_URL, // Forge: <API origin>/api/sdk/v2
15
15
  publicUrl: "https://api.example.com", // your public origin (never taken from the Host header)
16
16
  });
17
17
 
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Suggested agent names; anything else is kept as given. */
2
- export declare const AGENT_TYPES: readonly ["Claude Code", "Codex", "Cursor", "Grok", "Hermes", "OpenClaw", "Other", "Unknown"];
2
+ export declare const AGENT_TYPES: readonly ["Claude Code", "Codex", "Cursor", "Grok", "Hermes", "Instinct", "OpenClaw", "Other", "Unknown"];
3
3
  /** Suggested x402 clients; anything else is kept as given. */
4
4
  export declare const CLIENTS: readonly ["agentcash", "awal", "pay.sh", "other", "unknown"];
5
5
  /** JSON body field (POST, PUT, …). */
@@ -45,7 +45,7 @@ export declare function agentContextSchema({ searchQuery }?: {
45
45
  } | undefined;
46
46
  agent_type: {
47
47
  type: string;
48
- enum: ("Claude Code" | "Codex" | "Cursor" | "Grok" | "Hermes" | "OpenClaw" | "Other" | "Unknown")[];
48
+ enum: ("Claude Code" | "Codex" | "Cursor" | "Grok" | "Hermes" | "Instinct" | "OpenClaw" | "Other" | "Unknown")[];
49
49
  description: string;
50
50
  };
51
51
  agent_type_other: {
package/dist/context.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // API contract never changes. Never required: a required field breaks strict validators and makes
4
4
  // agents invent answers.
5
5
  /** Suggested agent names; anything else is kept as given. */
6
- export const AGENT_TYPES = ["Claude Code", "Codex", "Cursor", "Grok", "Hermes", "OpenClaw", "Other", "Unknown"];
6
+ export const AGENT_TYPES = ["Claude Code", "Codex", "Cursor", "Grok", "Hermes", "Instinct", "OpenClaw", "Other", "Unknown"];
7
7
  /** Suggested x402 clients; anything else is kept as given. */
8
8
  export const CLIENTS = ["agentcash", "awal", "pay.sh", "other", "unknown"];
9
9
  /** JSON body field (POST, PUT, …). */
package/dist/core.d.ts CHANGED
@@ -15,7 +15,10 @@ export type ForgeFeedbackOptions = ForgeOptions;
15
15
  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
- /** Forge backend origin, e.g. https://forge-feedback.up.railway.app */
18
+ /**
19
+ * Where events and ratings go. An origin (https://backend.example.com) uses its /v1/events, /v1/feedback and
20
+ * /v1/summary routes; a URL with a path (https://api.forgeintel.co/api/sdk/v2) uses {path}/events, and so on.
21
+ */
19
22
  backendUrl: string;
20
23
  /** This service's public origin, e.g. https://pixels.gateway.clawca.sh. Never derived from the Host header. */
21
24
  publicUrl: string;
package/dist/core.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // Framework-free Forge logic. Adapters (express.ts, later fetch.ts) translate their request/response
2
2
  // objects to the small interfaces below and write out what the core returns.
3
- import { DEFAULT_TTL_MS, mintFeedbackId, verifyFeedbackId } from "./id.js";
3
+ import { DEFAULT_TTL_MS, deriveSigningKey, mintFeedbackId, verifyFeedbackId } from "./id.js";
4
4
  import { createOperationIndex, enrichOpenApi } from "./openapi.js";
5
5
  import { ASK, TONES, checkAskText } from "./ask.js";
6
6
  import { agentContextAsk, parseAgentContext, takeFromBody, takeFromUrl } from "./context.js";
7
7
  import { EventReporter } from "./reporter.js";
8
8
  import { ISSUES, NOTE_MAX_LENGTH, OUTCOMES, PROTOCOL, parseSubmission } from "./values.js";
9
- import { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, readPaymentHeader, receiptExtension } from "./x402.js";
9
+ import { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, readPaymentHeader, readReceipt, receiptExtension } from "./x402.js";
10
10
  export const FEEDBACK_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
11
11
  /** Max JSON body for POST {basePath}. */
12
12
  export const BODY_LIMIT = 8 * 1024;
@@ -143,6 +143,10 @@ export function createForgeCore(input) {
143
143
  }
144
144
  function enabledCore(options, configWarnings) {
145
145
  const { apiKey, backendUrl, publicUrl } = options;
146
+ // Feedback IDs are signed with a key derived from the API key, never with the API key itself.
147
+ const signingKey = deriveSigningKey(apiKey);
148
+ const backendPath = new URL(backendUrl).pathname.replace(/\/+$/, "") || "/v1";
149
+ const backend = (name) => new URL(`${backendPath}/${name}`, backendUrl).href;
146
150
  const basePath = (options.basePath ?? "/feedback").replace(/\/+$/, "");
147
151
  const ratePath = `${basePath}/rate`;
148
152
  const rateUrl = new URL(ratePath, publicUrl).href;
@@ -181,7 +185,7 @@ function enabledCore(options, configWarnings) {
181
185
  console.warn(`[forge-feedback] ${message}`);
182
186
  lastLogged = message;
183
187
  });
184
- const reporter = new EventReporter(new URL("/v1/events", backendUrl).href, apiKey, fetchImpl, onError, options.flushIntervalMs ?? 2000);
188
+ const reporter = new EventReporter(backend("events"), apiKey, fetchImpl, onError, options.flushIntervalMs ?? 2000);
185
189
  const challengeSentence = (options.challengeSentence ?? ASK[tone].challengeSentence).replaceAll("{rate_url}", rateUrl).replaceAll("{summary_url}", summaryUrl);
186
190
  // Idempotency marker: the rate URL when the sentence contains it, otherwise the sentence itself.
187
191
  const challengeMarker = challengeSentence.includes(rateUrl) ? rateUrl : challengeSentence;
@@ -247,11 +251,11 @@ function enabledCore(options, configWarnings) {
247
251
  if (!parsed.ok)
248
252
  return badRequest(parsed.error);
249
253
  // Cheap local check so garbage and expired IDs never reach the backend.
250
- if (!verifyFeedbackId(apiKey, parsed.value.feedback_id, { ttlMs }).valid) {
254
+ if (!verifyFeedbackId(signingKey, parsed.value.feedback_id, { ttlMs }).valid) {
251
255
  return reply(404, { recorded: false, error: "unknown_or_expired_feedback_id" });
252
256
  }
253
257
  try {
254
- const upstream = await fetchImpl(new URL("/v1/feedback", backendUrl).href, {
258
+ const upstream = await fetchImpl(backend("feedback"), {
255
259
  method: "POST",
256
260
  headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
257
261
  body: JSON.stringify({ ...parsed.value, via, user_agent: request.header("user-agent") ?? null }),
@@ -270,7 +274,7 @@ function enabledCore(options, configWarnings) {
270
274
  async function summary() {
271
275
  if (!summaryCache || Date.now() - summaryCache.at > 30_000) {
272
276
  try {
273
- const upstream = await fetchImpl(new URL("/v1/summary", backendUrl).href, {
277
+ const upstream = await fetchImpl(backend("summary"), {
274
278
  headers: { authorization: `Bearer ${apiKey}` },
275
279
  signal: AbortSignal.timeout(3000),
276
280
  });
@@ -349,13 +353,14 @@ function enabledCore(options, configWarnings) {
349
353
  const route = `${request.method} ${request.path}`;
350
354
  const userAgent = request.header("user-agent") ?? undefined;
351
355
  const paymentHeader = request.header("payment-signature") ?? request.header("x-payment"); // x402 v2 / v1
352
- const feedbackId = paymentHeader ? mintFeedbackId(apiKey) : undefined;
356
+ const feedbackId = paymentHeader ? mintFeedbackId(signingKey) : undefined;
353
357
  if (feedbackId)
354
358
  stats.minted++;
355
359
  const feedbackUrl = feedbackId ? `${rateUrl}?feedback_id=${feedbackId}&outcome=` : "";
356
360
  let bodyChallenge = false;
357
361
  let headerChallenge = false;
358
362
  let context;
363
+ let receiptFacts = {};
359
364
  const remember = (raw) => {
360
365
  if (!collectContext)
361
366
  return;
@@ -421,6 +426,8 @@ function enabledCore(options, configWarnings) {
421
426
  }
422
427
  if (feedbackId && ok(status)) {
423
428
  set["Forge-Feedback-Id"] = feedbackId;
429
+ if (paymentResponse)
430
+ receiptFacts = readReceipt(paymentResponse);
424
431
  const receipt = receipts && paymentResponse ? describeReceipt(paymentResponse, receiptExtension(rateUrl, feedbackId, tone)) : undefined;
425
432
  if (receipt)
426
433
  set["PAYMENT-RESPONSE"] = receipt;
@@ -471,7 +478,7 @@ function enabledCore(options, configWarnings) {
471
478
  reporter.push({ type: "challenge", route, ts, user_agent: userAgent, ...(context ? { agent_context: context } : {}) });
472
479
  }
473
480
  if (feedbackId) {
474
- const facts = readPaymentHeader(paymentHeader);
481
+ const facts = { ...readPaymentHeader(paymentHeader), ...receiptFacts };
475
482
  reporter.push({
476
483
  type: "interaction",
477
484
  feedback_id: feedbackId,
package/dist/id.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  export declare const FEEDBACK_ID_PATTERN: RegExp;
2
2
  export declare const DEFAULT_TTL_MS: number;
3
+ /**
4
+ * The key feedback IDs are signed with, derived from the merchant's API key. Backends keep this (encrypted)
5
+ * instead of the API key itself, or derive it from the Bearer key on each request.
6
+ */
7
+ export declare function deriveSigningKey(apiKey: string): string;
3
8
  export declare function mintFeedbackId(key: string, now?: number): string;
4
9
  export type FeedbackIdCheck = {
5
10
  valid: true;
package/dist/id.js CHANGED
@@ -5,6 +5,13 @@ export const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
5
5
  const HEAD_BYTES = 10;
6
6
  const TAG_BYTES = 6;
7
7
  const CLOCK_SKEW_MS = 5 * 60 * 1000;
8
+ /**
9
+ * The key feedback IDs are signed with, derived from the merchant's API key. Backends keep this (encrypted)
10
+ * instead of the API key itself, or derive it from the Bearer key on each request.
11
+ */
12
+ export function deriveSigningKey(apiKey) {
13
+ return createHmac("sha256", apiKey).update("forge-feedback-id/v1").digest("base64url");
14
+ }
8
15
  function tag(key, head) {
9
16
  return createHmac("sha256", key).update(head).digest().subarray(0, TAG_BYTES);
10
17
  }
package/dist/index.d.ts CHANGED
@@ -4,11 +4,11 @@ export { createForgeCore, checkOptions, BODY_LIMIT, SPEC_LIMIT } from "./core.js
4
4
  export type { ForgeCall, ForgeCore, ForgeDiagnostics, ForgeFeedbackOptions, ForgeOptions, ForgeRequest, ForgeResponse, OpenApiOptions } from "./core.js";
5
5
  export { enrichOpenApi, detectVersion } from "./openapi.js";
6
6
  export type { EnrichOptions, EnrichReport, OperationReport, ResponseSupport, SpecVersion } from "./openapi.js";
7
- export { mintFeedbackId, verifyFeedbackId, FEEDBACK_ID_PATTERN, DEFAULT_TTL_MS } from "./id.js";
7
+ export { deriveSigningKey, mintFeedbackId, verifyFeedbackId, FEEDBACK_ID_PATTERN, DEFAULT_TTL_MS } from "./id.js";
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 } from "./x402.js";
11
+ export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, 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";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export { createForge, createForgeFeedback } from "./express.js";
2
2
  export { createForgeCore, checkOptions, BODY_LIMIT, SPEC_LIMIT } from "./core.js";
3
3
  export { enrichOpenApi, detectVersion } from "./openapi.js";
4
- export { mintFeedbackId, verifyFeedbackId, FEEDBACK_ID_PATTERN, DEFAULT_TTL_MS } from "./id.js";
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 } from "./x402.js";
6
+ export { describeChallenge, describeChallengeBody, describeReceipt, feedbackExtension, receiptExtension, FEEDBACK_EXTENSION, 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";
@@ -14,6 +14,10 @@ export type ForgeEvent = {
14
14
  payer?: string;
15
15
  network?: string;
16
16
  amount?: string;
17
+ scheme?: string;
18
+ asset?: string;
19
+ /** Settlement transaction reference from the receipt. */
20
+ transaction?: string;
17
21
  user_agent?: string;
18
22
  agent_context?: AgentContext;
19
23
  ts: number;
package/dist/x402.d.ts CHANGED
@@ -60,6 +60,12 @@ export interface PaymentFacts {
60
60
  payer?: string;
61
61
  network?: string;
62
62
  amount?: string;
63
+ scheme?: string;
64
+ asset?: string;
65
+ /** Settlement transaction reference, from the receipt (see readReceipt). */
66
+ transaction?: string;
63
67
  }
64
- /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
68
+ /** Best-effort read of the payer, network, amount, scheme and asset from an x402 v1/v2 payment header. Never throws. */
65
69
  export declare function readPaymentHeader(headerValue: string): PaymentFacts;
70
+ /** Best-effort read of a successful settlement receipt (base64 PAYMENT-RESPONSE): transaction, payer, network. Never throws. */
71
+ export declare function readReceipt(headerValue: string): Pick<PaymentFacts, "transaction" | "payer" | "network">;
package/dist/x402.js CHANGED
@@ -129,7 +129,7 @@ export function describeChallengeBody(body, additions) {
129
129
  }
130
130
  return undefined;
131
131
  }
132
- /** Best-effort read of the payer, network and amount from an x402 v1/v2 payment header. Never throws. */
132
+ /** Best-effort read of the payer, network, amount, scheme and asset from an x402 v1/v2 payment header. Never throws. */
133
133
  export function readPaymentHeader(headerValue) {
134
134
  try {
135
135
  const p = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
@@ -138,6 +138,8 @@ export function readPaymentHeader(headerValue) {
138
138
  payer: authorization?.from,
139
139
  network: p?.accepted?.network ?? p?.network,
140
140
  amount: p?.accepted?.amount ?? authorization?.value,
141
+ scheme: p?.accepted?.scheme ?? p?.scheme,
142
+ asset: p?.accepted?.asset,
141
143
  };
142
144
  for (const key of Object.keys(facts)) {
143
145
  if (typeof facts[key] !== "string")
@@ -149,3 +151,20 @@ export function readPaymentHeader(headerValue) {
149
151
  return {};
150
152
  }
151
153
  }
154
+ /** Best-effort read of a successful settlement receipt (base64 PAYMENT-RESPONSE): transaction, payer, network. Never throws. */
155
+ export function readReceipt(headerValue) {
156
+ try {
157
+ const r = JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
158
+ if (r?.success !== true)
159
+ return {};
160
+ const facts = { transaction: r.transaction, payer: r.payer, network: r.network };
161
+ for (const key of Object.keys(facts)) {
162
+ if (typeof facts[key] !== "string" || !facts[key])
163
+ delete facts[key];
164
+ }
165
+ return facts;
166
+ }
167
+ catch {
168
+ return {};
169
+ }
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeintel/sdk",
3
- "version": "0.3.0-beta.0",
3
+ "version": "0.4.0-beta.0",
4
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.",
5
5
  "license": "MIT",
6
6
  "type": "module",