diff --git a/.github/workflows/weather-x402-image.yml b/.github/workflows/weather-x402-image.yml new file mode 100644 index 00000000..c764cf4f --- /dev/null +++ b/.github/workflows/weather-x402-image.yml @@ -0,0 +1,146 @@ +name: Weather x402 image + +# Builds the weather-x402 agent (http-simple-agent-ts/Dockerfile) and pushes it +# to Google Artifact Registry so ArgoCD can deploy it. Keyless auth via Workload +# Identity Federation (same SA/provider the other Nevermined repos use — no +# secrets). +# +# pull_request → verify only (install, self-checks, build); no credentials, no push. +# push to main → verify, then build & push. +# dispatch → verify, then build & push (optional extra semver tag). +# +# Versioning (immutable-first — the AR repo has immutableTags=true, so a moving +# `latest` tag would be rejected on the second build; we don't publish one): +# - sha- every build — the immutable tag ArgoCD should pin in production +# - optional, when run manually with a version input (e.g. 1.0.0) + +on: + pull_request: + paths: + - "http-simple-agent-ts/**" + - ".github/workflows/weather-x402-image.yml" + push: + branches: [main] + paths: + - "http-simple-agent-ts/**" + - ".github/workflows/weather-x402-image.yml" + workflow_dispatch: + inputs: + version: + description: "Optional explicit semver tag to also publish (e.g. 1.0.0)" + required: false + type: string + +# Superseded PR pushes shouldn't keep burning a runner. Cancel only in-flight PR +# runs (grouped per ref, so distinct PRs don't cancel each other); never cancel a +# push/dispatch run — those publish the immutable image and must finish. +concurrency: + group: weather-x402-image-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + IMAGE: europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402 + AR_LOCATION: europe-west3 + PROJECT_ID: nevermined-eu-dev + SERVICE_ACCOUNT: github-actions-service-account@nevermined-eu-dev.iam.gserviceaccount.com + WORKLOAD_IDENTITY_PROVIDER: projects/112425687177/locations/global/workloadIdentityPools/github/providers/github-actions + +permissions: + contents: read + id-token: write + +jobs: + # Runs on every PR and every push. `tsc` type-checks the agent, and the pure + # self-checks cover the pricing/request-validation logic — neither needs + # credentials, so a broken PR fails here instead of on main. + verify: + name: Verify (install + self-checks + build) + runs-on: ubuntu-latest + defaults: + run: + working-directory: http-simple-agent-ts + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: yarn + cache-dependency-path: http-simple-agent-ts/yarn.lock + + - name: Install + run: yarn install --frozen-lockfile + + - name: Pricing self-check + run: yarn pricing:selfcheck + + - name: Request validation self-check + run: yarn request:selfcheck + + - name: Build (type-checks the agent) + run: yarn build + + # Only publishes on main / dispatch — never on a pull request. + build-push: + name: Build & push weather-x402 image + runs-on: ubuntu-latest + needs: verify + if: github.event_name != 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker metadata (tags + labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=sha,prefix=sha-,format=short + type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + token_format: access_token + project_id: ${{ env.PROJECT_ID }} + service_account: ${{ env.SERVICE_ACCOUNT }} + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + + - name: Log in to Artifact Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.AR_LOCATION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.auth.outputs.access_token }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build & push + uses: docker/build-push-action@v5 + with: + context: ./http-simple-agent-ts + file: ./http-simple-agent-ts/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + sbom: false + + - name: Summary + env: + TAGS: ${{ steps.meta.outputs.tags }} + run: | + { + echo "### Pushed tutorials-weather-x402 image" + echo '```' + echo "$TAGS" + echo '```' + echo "Pin the immutable sha-* tag in ArgoCD for production." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/superpowers/plans/2026-09-04-weather-x402-mpp-backend.md b/docs/superpowers/plans/2026-09-04-weather-x402-mpp-backend.md new file mode 100644 index 00000000..7c7332b0 --- /dev/null +++ b/docs/superpowers/plans/2026-09-04-weather-x402-mpp-backend.md @@ -0,0 +1,597 @@ +# Weather x402 + MPP Backend — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn `http-simple-agent-ts` into a keyless weather service that exposes three payment-plan routes (fixed-credits, time-based, pay-as-you-go), each speaking **both** x402 and MPP, then containerize it and deploy it to the `agents` namespace as `weather-x402-agent.nevermined.app`. + +**Architecture:** One Express app, one `paymentMiddleware(payments, routes)` call. Each route names a different plan and sets `mpp: true`, so a single 402 advertises x402 *and* MPP and either buyer works against the same URL. Weather comes from Open-Meteo (no API key). Plans are registered once by a script; the service reads their IDs from env. The image builds from `http-simple-agent-ts/` and deploys through the shared `helm-charts/agent` chart, mirroring the existing `demo-finance-agent` / `tutorials-showcase` deploy paths. + +**Tech Stack:** TypeScript, Express 4, `@nevermined-io/payments` (x402 + MPP), Open-Meteo REST, Docker (node:20-alpine), GitHub Actions + GCP Artifact Registry + WIF, ArgoCD + Helm. + +**Spec:** in-conversation spec dated 2026-09-04 ("weather-x402 — a live dual-protocol paid backend for the tutorials"). Read it alongside this plan. + +## Global Constraints + +- **Language is TypeScript, not Python.** MPP exists only in the TS SDK + backend; `payments-py` has x402 only. A dual-protocol service must be TS. +- **`@nevermined-io/payments` must be bumped from `^1.1.0` to a version that ships `payments.mpp` and the `mpp` route option, and pinned to an exact version** (the SDK doc labels the `payments.mpp.*` buyer surface `@experimental` and says fields may move in a minor release — pin so a redeploy can't drift). Resolve the exact version from `~/Projects/Nevermined/payments/package.json`. +- **MPP single-use guards are in-process (in-memory), not shared across replicas.** The Deployment MUST stay `replicaCount: 1`. Horizontal scaling would require a shared store (Redis) and is out of scope. +- **Plans use a crypto (erc4337-payable) price**, because `payments.mpp.fetch` mints `nvm:erc4337` only this release. A fiat/card plan would be payable by x402 card-delegation but **not** by MPP — so one plan payable by both protocols must be crypto-priced. +- **Environment is `sandbox`.** No real money. +- **No OpenAI dependency.** Weather is keyless; the service must run with no `OPENAI_API_KEY`. +- Match the surrounding tutorial style: plain `tsx` scripts, assert-based self-checks (as in `showcase/lib/demo-agent.mjs`), no test framework unless one already exists in the dir. + +--- + +## File Structure + +Everything lives in `http-simple-agent-ts/` (the deployed image builds from here) except the deploy manifests, which live in the separate `argocd` repo. + +- `http-simple-agent-ts/src/agent.ts` — **modify**. The Express server: 3 dual-protocol weather routes + `/health`. This is the deployed entrypoint. +- `http-simple-agent-ts/src/services/weather.service.ts` — **create** (copied from `mcp-examples/weather-mcp/src/services/weather.service.ts`), extended with `getForecast(city, days)`. +- `http-simple-agent-ts/src/pricing.ts` — **create**. Pure `priceForRequest(body)` → credits, so pay-as-you-go pricing is unit-testable without HTTP or payments. +- `http-simple-agent-ts/src/pricing.selfcheck.ts` — **create**. Assert-based `node`/`tsx` self-check for `priceForRequest`. +- `http-simple-agent-ts/scripts/register-plans.ts` — **create**. One-shot: registers the three plans, prints their IDs. +- `http-simple-agent-ts/scripts/smoke.ts` — **create**. Runnable e2e: exercises x402 and MPP against a running server; the Plan 1 verification. +- `http-simple-agent-ts/package.json` — **modify**. Bump payments, add `start` + script entries, drop the OpenAI-only assumptions. +- `http-simple-agent-ts/.env.example` — **modify**. New env surface (plan IDs, no OpenAI). +- `http-simple-agent-ts/Dockerfile` — **create** (from `mcp-examples/weather-mcp/Dockerfile`), entrypoint fixed to run the server. +- `.github/workflows/weather-x402-image.yml` — **create** (from `.github/workflows/showcase-image.yml`). +- `argocd: eu/dev/argocd-apps/agents/weather-x402-agent.yaml` (+ `-staging.yaml`) — **create** (from `demo-finance-agent.yaml`). +- `argocd: helm-charts/agent/v1.0.0/.argocd-source-weather-x402-agent.yaml` — **create** (image-updater mirror; only if image-updater is used). + +--- + +## Phase A — Keyless weather foundation + +### Task 1: Dependencies, scripts, env surface + +**Files:** +- Modify: `http-simple-agent-ts/package.json` +- Modify: `http-simple-agent-ts/.env.example` + +**Interfaces:** +- Produces: a `yarn start` script (`node dist/agent.js`) that the Dockerfile entrypoint relies on; env vars `NVM_API_KEY`, `NVM_ENVIRONMENT`, `PLAN_ID_CREDITS`, `PLAN_ID_TIME`, `PLAN_ID_PAYG`, `PORT`. + +- [ ] **Step 1: Bump payments and add scripts.** In `package.json`, set `@nevermined-io/payments` to the exact version resolved from `~/Projects/Nevermined/payments/package.json` (e.g. `"1.19.0"` — verify, no caret), and add scripts: + +```json +"scripts": { + "agent": "tsx src/agent.ts", + "client": "tsx src/client.ts", + "register-plans": "tsx scripts/register-plans.ts", + "smoke": "tsx scripts/smoke.ts", + "pricing:selfcheck": "tsx src/pricing.selfcheck.ts", + "build": "tsc", + "start": "node dist/agent.js" +} +``` + +Remove `openai` from `dependencies` (the service no longer calls an LLM). Leave `agent-observability.ts` alone for now — it is out of scope; if `tsc` fails on it because it still imports `openai`, exclude it in `tsconfig.json` (`"exclude": ["node_modules","dist","src/agent-observability.ts"]`) rather than fixing it here. + +- [ ] **Step 2: Rewrite `.env.example`:** + +```bash +# Nevermined (required) +NVM_API_KEY=nvm:your-api-key +NVM_ENVIRONMENT=sandbox + +# Plan IDs — created by `yarn register-plans` (see scripts/register-plans.ts) +PLAN_ID_CREDITS= +PLAN_ID_TIME= +PLAN_ID_PAYG= + +# Server +PORT=3000 +``` + +- [ ] **Step 3: Install and verify it resolves.** + +Run: `cd http-simple-agent-ts && yarn install` +Expected: installs without error; `yarn list @nevermined-io/payments` shows the pinned version. + +- [ ] **Step 4: Verify MPP is present in the installed SDK.** + +Run: `node -e "const p=require('@nevermined-io/payments'); console.log(typeof require('@nevermined-io/payments/express').paymentMiddleware, Object.keys(require('@nevermined-io/payments/express')))"` +Expected: prints `function` and the export list includes `MPP_HEADERS` and `X402_HEADERS`. If `MPP_HEADERS` is absent, the version is too old — raise it. + +- [ ] **Step 5: Commit.** + +```bash +git add http-simple-agent-ts/package.json http-simple-agent-ts/.env.example http-simple-agent-ts/yarn.lock +git commit -m "chore(http-ts): bump payments for MPP, drop OpenAI, add plan-id env" +``` + +### Task 2: Weather service + pure pricing function + +**Files:** +- Create: `http-simple-agent-ts/src/services/weather.service.ts` +- Create: `http-simple-agent-ts/src/pricing.ts` +- Create: `http-simple-agent-ts/src/pricing.selfcheck.ts` + +**Interfaces:** +- Produces: + - `getTodayWeather(city: string): Promise` and `getForecast(city: string, days: number): Promise` from `weather.service.ts`. + - `priceForRequest(body: unknown): number` from `pricing.ts` — 1 credit for a current-weather/single-day request, `days` credits (capped 1..7) for a multi-day forecast. + +- [ ] **Step 1: Copy the weather service verbatim, then extend it.** Copy `mcp-examples/weather-mcp/src/services/weather.service.ts` to `http-simple-agent-ts/src/services/weather.service.ts`. Drop the `console.log` latency line (repo style forbids `console.log` in production code). Append a multi-day forecast reader: + +```typescript +export type ForecastDay = { + date: string; + tmaxC: number | null; + tminC: number | null; + precipitationMm: number | null; + weatherCode: number | null; + weatherText: string | null; +}; + +export type ForecastWeather = { + city: string; + country: string | null; + timezone: string; + updatedAt: string; + days: ForecastDay[]; +}; + +export async function getForecast(city: string, days: number): Promise { + const n = Math.max(1, Math.min(7, Math.trunc(days))); + const geo = await geocodeCity(city); + const url = new URL("https://api.open-meteo.com/v1/forecast"); + url.searchParams.set("latitude", String(geo.latitude)); + url.searchParams.set("longitude", String(geo.longitude)); + url.searchParams.set("forecast_days", String(n)); + url.searchParams.set("timezone", "auto"); + url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode"); + + let res: Response; + try { + res = await fetch(url); + } catch { + throw new DownstreamError("Failed to reach Open-Meteo forecast API"); + } + if (!res.ok) throw new DownstreamError(`Forecast API returned HTTP ${res.status}`); + + const data = (await res.json()) as any; + const d = data.daily ?? {}; + const time: string[] = Array.isArray(d.time) ? d.time : []; + const daysOut: ForecastDay[] = time.map((date: string, i: number) => { + const code = Array.isArray(d.weathercode) ? Number(d.weathercode[i]) : null; + return { + date, + tmaxC: Array.isArray(d.temperature_2m_max) ? Number(d.temperature_2m_max[i]) : null, + tminC: Array.isArray(d.temperature_2m_min) ? Number(d.temperature_2m_min[i]) : null, + precipitationMm: Array.isArray(d.precipitation_sum) ? Number(d.precipitation_sum[i]) : null, + weatherCode: code, + weatherText: weatherCodeToText(code), + }; + }); + + return { + city: geo.name, + country: geo.country, + timezone: data.timezone ?? geo.timezone ?? "unknown", + updatedAt: new Date().toISOString(), + days: daysOut, + }; +} +``` + +- [ ] **Step 2: Write the failing pricing self-check first.** Create `src/pricing.selfcheck.ts`: + +```typescript +import { priceForRequest } from "./pricing.js"; + +const assert = (c: boolean, m: string) => { if (!c) throw new Error("FAIL: " + m); }; + +assert(priceForRequest({ city: "Lisbon" }) === 1, "no days → 1 credit"); +assert(priceForRequest({ city: "Lisbon", days: 1 }) === 1, "1 day → 1 credit"); +assert(priceForRequest({ city: "Lisbon", days: 7 }) === 7, "7 days → 7 credits"); +assert(priceForRequest({ city: "Lisbon", days: 99 }) === 7, "days capped at 7"); +assert(priceForRequest({ city: "Lisbon", days: 0 }) === 1, "days floored at 1"); +assert(priceForRequest({ city: "Lisbon", days: "3" as any }) === 3, "numeric string coerced"); +assert(priceForRequest({}) === 1, "empty body → 1 credit"); +console.log("✓ pricing self-check passed"); +``` + +- [ ] **Step 3: Run it — must fail (module missing).** + +Run: `cd http-simple-agent-ts && yarn pricing:selfcheck` +Expected: FAIL — cannot find `./pricing.js` / `priceForRequest` is not a function. + +- [ ] **Step 4: Implement `src/pricing.ts` minimally.** + +```typescript +/** Pay-as-you-go price: 1 credit for current weather, `days` credits (1..7) for a forecast. */ +export function priceForRequest(body: unknown): number { + const raw = (body as { days?: unknown } | null | undefined)?.days; + const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN; + if (!Number.isFinite(n)) return 1; + return Math.max(1, Math.min(7, Math.trunc(n))); +} +``` + +- [ ] **Step 5: Run the self-check — must pass.** + +Run: `cd http-simple-agent-ts && yarn pricing:selfcheck` +Expected: `✓ pricing self-check passed` + +- [ ] **Step 6: Commit.** + +```bash +git add http-simple-agent-ts/src/services/weather.service.ts http-simple-agent-ts/src/pricing.ts http-simple-agent-ts/src/pricing.selfcheck.ts +git commit -m "feat(http-ts): keyless weather service + pay-as-you-go pricing fn" +``` + +--- + +## Phase B — Dual-protocol routes over three plans + +### Task 3: Rewrite `agent.ts` as the dual-protocol weather server + +**Files:** +- Modify: `http-simple-agent-ts/src/agent.ts` (full rewrite) + +**Interfaces:** +- Consumes: `getTodayWeather`, `getForecast` (Task 2); `priceForRequest` (Task 2); `paymentMiddleware`, `X402_HEADERS`, `MPP_HEADERS` from `@nevermined-io/payments/express`. +- Produces: routes `POST /weather/credits`, `POST /weather/subscription`, `POST /weather/payg`, `GET /health` on `PORT`. + +- [ ] **Step 1: Replace the file body.** Note the route→plan→credits mapping and that `mpp: true` is set on every paid route. + +```typescript +import "dotenv/config"; +import express, { Request, Response, RequestHandler } from "express"; +import { Payments, EnvironmentName } from "@nevermined-io/payments"; +import { paymentMiddleware, X402_HEADERS, MPP_HEADERS } from "@nevermined-io/payments/express"; +import { getTodayWeather, getForecast, CityNotFoundError } from "./services/weather.service.js"; +import { priceForRequest } from "./pricing.js"; + +const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; +const NVM_API_KEY = process.env.NVM_API_KEY ?? ""; +const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "sandbox") as EnvironmentName; +const PLAN_ID_CREDITS = process.env.PLAN_ID_CREDITS ?? ""; +const PLAN_ID_TIME = process.env.PLAN_ID_TIME ?? ""; +const PLAN_ID_PAYG = process.env.PLAN_ID_PAYG ?? ""; + +if (!NVM_API_KEY || !PLAN_ID_CREDITS || !PLAN_ID_TIME || !PLAN_ID_PAYG) { + console.error("NVM_API_KEY, PLAN_ID_CREDITS, PLAN_ID_TIME and PLAN_ID_PAYG are required."); + process.exit(1); +} + +const payments = Payments.getInstance({ nvmApiKey: NVM_API_KEY, environment: NVM_ENVIRONMENT }); + +const app = express(); +app.use(express.json()); + +// /health stays unprotected — mount it before the paywall. +app.get("/health", (_req: Request, res: Response) => res.json({ ok: true })); + +// One middleware, three plans, both protocols. `mpp: true` makes each 402 advertise +// x402 AND MPP, so either buyer works against the same URL. +app.use( + paymentMiddleware( + payments, + { + "POST /weather/credits": { planId: PLAN_ID_CREDITS, credits: 1, mpp: true }, + "POST /weather/subscription": { planId: PLAN_ID_TIME, credits: 1, mpp: true }, + "POST /weather/payg": { + planId: PLAN_ID_PAYG, + credits: (req: Request) => priceForRequest(req.body), + mpp: true, + }, + }, + { + onBeforeVerify: (req) => console.log(`[pay] verify ${req.method} ${req.path}`), + onAfterSettle: (req, credits) => console.log(`[pay] settled ${credits} on ${req.path}`), + }, + ) as RequestHandler, +); + +async function handleToday(req: Request, res: Response) { + try { + const { city } = req.body as { city?: string }; + if (!city) return res.status(400).json({ error: "Missing 'city'" }); + return res.json(await getTodayWeather(city)); + } catch (err) { + if (err instanceof CityNotFoundError) return res.status(404).json({ error: err.message }); + console.error(err); + return res.status(502).json({ error: "weather upstream failed" }); + } +} + +app.post("/weather/credits", handleToday); +app.post("/weather/subscription", handleToday); + +app.post("/weather/payg", async (req: Request, res: Response) => { + try { + const { city, days } = req.body as { city?: string; days?: number }; + if (!city) return res.status(400).json({ error: "Missing 'city'" }); + return res.json(days && days > 1 ? await getForecast(city, days) : await getTodayWeather(city)); + } catch (err) { + if (err instanceof CityNotFoundError) return res.status(404).json({ error: err.message }); + console.error(err); + return res.status(502).json({ error: "weather upstream failed" }); + } +}); + +app.listen(PORT, () => { + console.log(`weather-x402 agent on http://localhost:${PORT}`); + console.log(`x402 header: ${X402_HEADERS.PAYMENT_SIGNATURE} · MPP header: ${MPP_HEADERS.AUTHORIZATION}`); +}); +``` + +> If `MPP_HEADERS`'s field for the `Authorization` credential is named differently in the installed SDK, adjust the log line — read the export from `node_modules/@nevermined-io/payments/dist/.../express`. Do not invent a name. + +- [ ] **Step 2: Type-check.** + +Run: `cd http-simple-agent-ts && yarn build` +Expected: `tsc` exits 0. (If it fails on `agent-observability.ts`, confirm Task 1 Step 1's exclude is in place.) + +- [ ] **Step 3: Boot with dummy env to confirm the guard.** + +Run: `cd http-simple-agent-ts && NVM_API_KEY=x PLAN_ID_CREDITS=x PLAN_ID_TIME=x PLAN_ID_PAYG=x PORT=3999 timeout 5 yarn start || true` (after `yarn build`) +Expected: prints the boot banner (SDK init may warn on the fake key — acceptable; we only assert it reaches `listen`). + +- [ ] **Step 4: Commit.** + +```bash +git add http-simple-agent-ts/src/agent.ts http-simple-agent-ts/tsconfig.json +git commit -m "feat(http-ts): dual-protocol weather routes over three plans" +``` + +--- + +## Phase C — Plan registration + +### Task 4: `register-plans.ts` + +**Files:** +- Create: `http-simple-agent-ts/scripts/register-plans.ts` + +**Interfaces:** +- Produces: three plan IDs printed to stdout, to be pasted into `.env` / stored in Secret Manager. + +> Verify every builder/register name against `~/Projects/Nevermined/payments/src/plans.ts` and `src/api/plans-api.ts` before running — the SDK is the source of truth. The shape below matches the surface documented in the spec's evidence table. + +- [ ] **Step 1: Write the script.** + +```typescript +import "dotenv/config"; +import { Payments, EnvironmentName } from "@nevermined-io/payments"; +import { + getFixedCreditsConfig, + getExpirableDurationConfig, + getDynamicCreditsConfig, + getCryptoPriceConfig, + ONE_DAY_DURATION, +} from "@nevermined-io/payments"; + +const payments = Payments.getInstance({ + nvmApiKey: process.env.NVM_API_KEY!, + environment: (process.env.NVM_ENVIRONMENT || "sandbox") as EnvironmentName, +}); + +// Crypto price so BOTH x402 (erc4337) and MPP can pay it. Use the builder's +// signature from the SDK; `receivers` is the builder's wallet address. +const price = getCryptoPriceConfig(/* amount */ 0n, /* receiver */ process.env.NVM_RECEIVER ?? ""); + +async function main() { + const credits = await payments.plans.registerCreditsPlan( + { name: "Weather — Fixed Credits", description: "100 weather calls, 1 credit each" }, + price, + getFixedCreditsConfig(100n, 1n), + ); + const time = await payments.plans.registerTimePlan( + { name: "Weather — Day Pass", description: "24h of weather access" }, + price, + getExpirableDurationConfig(ONE_DAY_DURATION), + ); + const payg = await payments.plans.registerPlan( + { name: "Weather — Pay As You Go", description: "1 credit current, up to 7 for a forecast" }, + price, + getDynamicCreditsConfig(1000n, 1n, 7n), + ); + + console.log("PLAN_ID_CREDITS=" + (credits.planId ?? credits)); + console.log("PLAN_ID_TIME=" + (time.planId ?? time)); + console.log("PLAN_ID_PAYG=" + (payg.planId ?? payg)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); +``` + +- [ ] **Step 2: Run against sandbox with a real builder key.** + +Run: `cd http-simple-agent-ts && NVM_API_KEY= NVM_RECEIVER= yarn register-plans` +Expected: three `PLAN_ID_*=...` lines. If a builder name/signature mismatches, fix against `plans.ts` (this is the one task that can't be dry-run). + +- [ ] **Step 3: Record the IDs** in `.env` locally, and note them for the Secret Manager step (Task 8). Do **not** commit real IDs into `.env.example`. + +- [ ] **Step 4: Commit the script only.** + +```bash +git add http-simple-agent-ts/scripts/register-plans.ts +git commit -m "feat(http-ts): plan registration script (credits/time/payg)" +``` + +--- + +## Phase D — Local end-to-end verification + +### Task 5: `smoke.ts` — x402 and MPP round-trips + +**Files:** +- Create: `http-simple-agent-ts/scripts/smoke.ts` + +**Interfaces:** +- Consumes: a running server (Task 3) and the three plan IDs (Task 4); a **funded sandbox buyer** `NVM_API_KEY` with an `erc4337` delegation. + +- [ ] **Step 1: Write the smoke test.** It asserts: no-token → 402 advertising both protocols; x402 token → 200; MPP fetch → 200 + receipt. + +```typescript +import "dotenv/config"; +import { Payments, EnvironmentName } from "@nevermined-io/payments"; +import { X402_HEADERS } from "@nevermined-io/payments/express"; + +const BASE = process.env.SERVER_URL || "http://localhost:3000"; +const PLAN = process.env.PLAN_ID_CREDITS!; +const payments = Payments.getInstance({ + nvmApiKey: process.env.NVM_API_KEY!, + environment: (process.env.NVM_ENVIRONMENT || "sandbox") as EnvironmentName, +}); +const assert = (c: boolean, m: string) => { if (!c) throw new Error("FAIL: " + m); }; + +async function main() { + // 1) Unpaid → 402 advertising BOTH protocols + const r0 = await fetch(`${BASE}/weather/credits`, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ city: "Lisbon" }), + }); + assert(r0.status === 402, "unpaid → 402"); + assert(!!r0.headers.get(X402_HEADERS.PAYMENT_REQUIRED), "x402 challenge present"); + assert(!!r0.headers.get("www-authenticate"), "MPP WWW-Authenticate present"); + + // 2) x402 buyer → 200 + const { accessToken } = await payments.x402.getX402AccessToken(PLAN); + const r1 = await fetch(`${BASE}/weather/credits`, { + method: "POST", + headers: { "content-type": "application/json", [X402_HEADERS.PAYMENT_SIGNATURE]: accessToken }, + body: JSON.stringify({ city: "Lisbon" }), + }); + assert(r1.status === 200, `x402 paid → 200 (got ${r1.status})`); + const w = await r1.json(); + assert(typeof w.city === "string", "x402 returned real weather"); + + // 3) MPP buyer → 200 + receipt + const { delegationId } = await payments.delegation.createDelegation({ + provider: "erc4337", spendingLimitCents: 10000, durationSecs: 604800, currency: "usdc", + }); + const { response, receipt, paid } = await payments.mpp.fetch( + `${BASE}/weather/credits`, + { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ city: "Berlin" }) }, + { delegationConfig: { delegationId }, planId: PLAN }, + ); + assert(response.status === 200, `MPP paid → 200 (got ${response.status})`); + assert(paid && !!receipt, "MPP settled with a receipt"); + + console.log("✓ smoke: x402 + MPP both paid a real weather response"); +} +main().catch((e) => { console.error(e); process.exit(1); }); +``` + +- [ ] **Step 2: Run it against a locally running server.** + +Run (two shells): `yarn start` in one; `SERVER_URL=http://localhost:3000 NVM_API_KEY= PLAN_ID_CREDITS= yarn smoke` in the other. +Expected: `✓ smoke: x402 + MPP both paid a real weather response`. A `402` on step 2/3 usually means the buyer wallet is unfunded on sandbox — fund it, don't change the assert. + +- [ ] **Step 3: Commit.** + +```bash +git add http-simple-agent-ts/scripts/smoke.ts +git commit -m "test(http-ts): x402 + MPP smoke e2e" +``` + +--- + +## Phase E — Containerize and deploy + +### Task 6: Dockerfile + +**Files:** +- Create: `http-simple-agent-ts/Dockerfile` + +- [ ] **Step 1: Write it** (from the weather-mcp Dockerfile; entrypoint runs the server via the new `start` script, and the build needs `tsconfig.json`): + +```dockerfile +FROM node:20-alpine +LABEL maintainer="Nevermined " +RUN apk add --no-cache libc6-compat yarn +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --non-interactive +COPY src ./src +COPY tsconfig.json ./ +RUN yarn build +EXPOSE 3000 +ENTRYPOINT ["yarn", "start"] +``` + +- [ ] **Step 2: Build and run locally.** + +Run: `cd http-simple-agent-ts && docker build -t weather-x402:dev . && docker run --rm -p 3000:3000 -e NVM_API_KEY=x -e PLAN_ID_CREDITS=x -e PLAN_ID_TIME=x -e PLAN_ID_PAYG=x weather-x402:dev &` then `sleep 4 && curl -fsS localhost:3000/health` +Expected: `{"ok":true}`. Stop the container afterward. + +- [ ] **Step 3: Commit.** + +```bash +git add http-simple-agent-ts/Dockerfile +git commit -m "build(http-ts): Dockerfile for weather-x402 service" +``` + +### Task 7: CI workflow + +**Files:** +- Create: `.github/workflows/weather-x402-image.yml` + +- [ ] **Step 1: Copy `showcase-image.yml`** and change only: workflow `name`; the `paths` filters to `http-simple-agent-ts/**` and this workflow file; `env.IMAGE` to `europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402`; `defaults.run.working-directory` and the build `context`/`file` to `http-simple-agent-ts` / `http-simple-agent-ts/Dockerfile`. Keep the WIF `SERVICE_ACCOUNT` / `WORKLOAD_IDENTITY_PROVIDER` / `PROJECT_ID` blocks **unchanged** — this repo (`nevermined-io/tutorials`) is already allowlisted in the WIF pool (the showcase workflow proves it). Replace the `Sandbox agent self-check` step with `yarn pricing:selfcheck` (the smoke test needs live creds and does not run in CI). + +- [ ] **Step 2: Validate the YAML.** + +Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/weather-x402-image.yml'))"` (or `actionlint` if installed) +Expected: no error. + +- [ ] **Step 3: Commit.** (Push happens with the PR; the image builds on merge to `main`.) + +```bash +git add .github/workflows/weather-x402-image.yml +git commit -m "ci: build & push weather-x402 image to Artifact Registry" +``` + +### Task 8: ArgoCD Application + secrets (in the `argocd` repo) + +**Files (separate repo `~/Projects/Nevermined/argocd`, its own PR):** +- Create: `eu/dev/argocd-apps/agents/weather-x402-agent.yaml` +- Create: `eu/dev/argocd-apps/agents/weather-x402-agent-staging.yaml` +- Create: `helm-charts/agent/v1.0.0/.argocd-source-weather-x402-agent.yaml` (only if using image-updater) + +- [ ] **Step 1: Copy `demo-finance-agent.yaml`** and set: `image.repository=europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402`, `image.tag=sha-` (the immutable tag the CI prints), `service.hostname=weather-x402-agent.nevermined.app`, `service.ports.*=3000`, `initCommand=yarn start`, **`replicaCount: 1` (do not scale — MPP single-use is in-process)**, `ingress.rateLimitEnabled=true`. Set env params `NVM_ENVIRONMENT=sandbox` and the three `PLAN_ID_*`; set `secrets.nvmApiKey=gcp:secretmanager:projects/112425687177/secrets/` (create that Secret Manager secret first with the builder key from Task 4). Since the AR image is pinned by immutable `sha-*` tag, **prefer the pinned-tag path over image-updater** (drop the image-updater annotations and the `.argocd-source-*.yaml` mirror) — matches how the production apps pin AR tags. Repeat for `-staging` with `weather-x402-agent-staging.nevermined.app`. + +- [ ] **Step 2: Verify chart rendering locally.** + +Run: `helm template weather-x402-agent ~/Projects/Nevermined/argocd/helm-charts/agent/v1.0.0 --set image.repository=... --set service.hostname=weather-x402-agent.nevermined.app --set service.ports.port=3000 --set replicaCount=1` +Expected: renders a Deployment (1 replica), Service, and Ingress for `weather-x402-agent.nevermined.app` with no template errors. + +- [ ] **Step 3: Open the argocd PR**, merge after review; ArgoCD auto-syncs the `agents` namespace. + +- [ ] **Step 4: Verify the deployment is live.** + +Run: `curl -fsS https://weather-x402-agent.nevermined.app/health` and `kubectl -n agents get deploy weather-x402-agent` +Expected: `{"ok":true}`; deployment `1/1` ready. Then re-run Task 5's `smoke.ts` with `SERVER_URL=https://weather-x402-agent.nevermined.app` — both protocols pay against the live service. + +--- + +## Self-review (done against the spec) + +- **Spec coverage:** dual-protocol on one route ✅ (Task 3 `mpp: true`); three plan types ✅ (Tasks 3–4); keyless weather ✅ (Tasks 1–2); Docker ✅ (Task 6); agents-namespace deploy ✅ (Tasks 7–8); acceptance criteria 1–7 map to Tasks 3/5/8 (criterion 8, showcase, is Plan 2). Not covered here by design: the frontend/tutorial rewrite (open question #5) → Plan 2 below. +- **Consistency:** `priceForRequest` signature identical in Tasks 2/3/5; `PLAN_ID_*` names identical across `.env.example`, `agent.ts`, `register-plans.ts`, `smoke.ts`, and the Application. +- **Known soft spots the implementer must confirm against the SDK, not invent:** exact pinned payments version; `MPP_HEADERS` field name; `getCryptoPriceConfig`/`registerPlan` signatures; the shape of the `registerPlan` return (`.planId` vs raw). All flagged inline. + +--- + +## Roadmap: follow-on plans (2–4) + +Plan 1 delivers a live, dual-protocol, deployed backend. The remaining spec scope (open question #5 — "the frontend must be the tutorials") is a second plan, written once Plan 1's URL exists so its tasks can reference the real endpoint: + +- **Plan 2 — Tutorials consume the live backend.** + - Rewrite `http-simple-agent-ts/src/client.ts` to buy weather over x402 from the three routes (replaces the `/ask` + `{query}` flow). + - Rewrite `http-simple-agent-py` (server + client) as the **x402-only** weather service (Python has no MPP); its client can also target the deployed TS URL for x402, which is wire-compatible. + - New **MPP tutorial** (`http-mpp-agent-ts/` or an `mpp-client.ts` in the TS dir): the MPP buyer flow via `payments.mpp.fetch` against the same deployed dual-protocol server. TS-only; crypto (erc4337) delegation only — call this out, since `mpp.fetch` can't use card delegation this release. + - READMEs for each, kept in sync with code (repo rule). + +- **Plan 3 — Showcase live-run against the real backend.** + - Extend `showcase/app/api/agent/route.ts`: when a tutorial sets `endpointEnv` and the env var is present, do the buyer round-trip **server-side** (buyer NVM key + delegation held server-side, never in the browser — the route file's own header comment prescribes exactly this) instead of the `demo-agent.mjs` simulator; keep the simulator as the fallback. + - Set `LiveRun.endpointEnv` on the x402 + MPP tutorial entries in `showcase/content/tutorials.ts`; add MPP tutorial cards (new `protocol` value or reuse `x402` with an MPP tag — decide in Plan 3). + - Minor `LiveRunPanel` work to surface the real settlement receipt / MPP handshake. + +- **Plan 4 (optional) — Retire the fabricated weather answers** in `demo-agent.mjs` for the deployed tutorials, or leave them as the offline fallback. + +Dependencies: Plan 2 needs Plan 1's code; Plan 3 needs Plan 1's deployed URL and Plan 2's tutorial dirs. Recommend shipping Plan 1 as one PR, then Plan 2, then Plan 3. diff --git a/http-simple-agent-ts/.env.example b/http-simple-agent-ts/.env.example index 154196ce..beea3eab 100644 --- a/http-simple-agent-ts/.env.example +++ b/http-simple-agent-ts/.env.example @@ -1,29 +1,16 @@ -# ============================================================================= -# Nevermined Configuration (required for both agent and client) -# ============================================================================= +# Nevermined (required). The environment (sandbox/live) is derived from the key prefix. +NVM_API_KEY=sandbox:your-api-key -# Your Nevermined API key (get it from https://nevermined.app) -NVM_API_KEY=nvm:your-api-key +# Plan IDs — created by `yarn register-plans` (see scripts/register-plans.ts) +PLAN_ID_CREDITS= +PLAN_ID_TIME= +PLAN_ID_PAYG= -# Environment: 'sandbox' (Base Sepolia) or 'live' (Arbitrum One) -NVM_ENVIRONMENT=sandbox +# For `yarn register-plans` only: the builder wallet that receives payment. +# REQUIRED for crypto-priced (payable) plans — without it the plans register as FREE. +NVM_RECEIVER= +# Optional: on-chain price per plan purchase, in the token's smallest unit (default 0). +PLAN_PRICE_WEI=0 -# The Plan ID that protects the /ask endpoint -NVM_PLAN_ID=your-plan-id - -# ============================================================================= -# Agent Configuration (server) -# ============================================================================= - -# OpenAI API Key for the AI endpoint -OPENAI_API_KEY=sk-your-openai-api-key - -# Server port (optional, defaults to 3000) +# Server PORT=3000 - -# ============================================================================= -# Client Configuration -# ============================================================================= - -# URL of the agent server (optional, defaults to http://localhost:3000) -SERVER_URL=http://localhost:3000 diff --git a/http-simple-agent-ts/Dockerfile b/http-simple-agent-ts/Dockerfile new file mode 100644 index 00000000..0d1ff842 --- /dev/null +++ b/http-simple-agent-ts/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine +LABEL maintainer="Nevermined " +RUN apk add --no-cache libc6-compat yarn +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --non-interactive +COPY src ./src +COPY tsconfig.json ./ +RUN yarn build +EXPOSE 3000 +ENTRYPOINT ["yarn", "start"] diff --git a/http-simple-agent-ts/README.md b/http-simple-agent-ts/README.md index 5d482f52..052c5849 100644 --- a/http-simple-agent-ts/README.md +++ b/http-simple-agent-ts/README.md @@ -1,353 +1,196 @@ -# HTTP Simple Agent (TypeScript) with x402 Payment Protection +# HTTP Weather Agent (TypeScript) — x402 + MPP -A minimal Express server demonstrating the [x402 payment protocol](https://github.com/coinbase/x402) using Nevermined's payment middleware. This tutorial shows how to protect API endpoints with credit-based payments. +A minimal Express server whose weather endpoints are gated by Nevermined's payment middleware. One `paymentMiddleware` call protects three routes, each backed by a different payment plan and each accepting **both** payment protocols — [x402](https://github.com/coinbase/x402) and **MPP** (Machine Payments Protocol). Weather comes from the free, keyless [Open-Meteo](https://open-meteo.com) API, so the agent needs no third-party keys to run. -> **Note:** For a Python version of this tutorial, see [http-simple-agent-py](../http-simple-agent-py/). +> **Note:** For a Python version of the x402 flow, see [http-simple-agent-py](../http-simple-agent-py/). MPP is TypeScript-only. [![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289da?logo=discord&logoColor=white)](https://discord.com/invite/GZju2qScKq) ## Overview -This tutorial includes: +- **Agent** (`src/agent.ts`) — an Express server exposing three payment-protected weather routes plus an unprotected `/health`. +- **Weather service** (`src/services/weather.service.ts`) — current weather + multi-day forecast from Open-Meteo (no API key). +- **Plan registration** (`scripts/register-plans.ts`) — one-shot script that registers the three plans and prints their IDs. +- **Smoke test / buyer example** (`scripts/smoke.ts`) — drives the full x402 and MPP buyer round-trips against the agent. -- **Agent** (`src/agent.ts`) - An Express server with a payment-protected `/ask` endpoint -- **Agent with Observability** (`src/agent-observability.ts`) - Same agent with Nevermined observability for tracking OpenAI costs -- **Client** (`src/client.ts`) - A demo client showing the complete x402 payment flow +### Routes, plans and protocols -## x402 Payment Flow +| Route | Plan type | Cost | Protocols | +| --- | --- | --- | --- | +| `POST /weather/credits` | fixed credits | 1 credit / request | x402 + MPP | +| `POST /weather/subscription` | time-based (24h pass) | time-boxed access | x402 + MPP | +| `POST /weather/payg` | dynamic (pay-as-you-go) | 1 for today, up to 7 for a forecast | x402 + MPP | -``` -┌─────────┐ ┌─────────┐ -│ Client │ │ Agent │ -└────┬────┘ └────┬────┘ - │ │ - │ 1. POST /ask (no token) │ - │───────────────────────────────────────>│ - │ │ - │ 2. 402 Payment Required │ - │ Header: payment-required (base64) │ - │<───────────────────────────────────────│ - │ │ - │ 3. Generate x402 token via SDK │ - │ │ - │ 4. POST /ask │ - │ Header: payment-signature (token) │ - │───────────────────────────────────────>│ - │ │ - │ - Verify permissions │ - │ - Execute request │ - │ - Settle (burn credits) │ - │ │ - │ 5. 200 OK + AI response │ - │ Header: payment-response (base64) │ - │<───────────────────────────────────────│ - │ │ -``` +`mpp: true` on each route makes the `402` advertise **both** protocols, so an x402 buyer and an MPP buyer work against the same URL. + +## Payment headers + +| Protocol | Challenge (402) | Client → server | Receipt (200) | +| --- | --- | --- | --- | +| **x402** | `payment-required` | `payment-signature` | `payment-response` | +| **MPP** | `WWW-Authenticate: Payment` | `Authorization: Payment` | `Payment-Receipt` | -## Quick Start +## Quick start -### 1. Install dependencies +### 1. Install ```bash yarn install ``` -### 2. Configure environment +### 2. Register the plans + +Registers the three plans against your Nevermined account and prints their IDs. The environment (sandbox/live) is derived from your API-key prefix. Set `NVM_RECEIVER` to your builder wallet so the plans are **crypto-priced (payable)** — without it the script registers FREE plans and warns loudly. ```bash -cp .env.example .env +NVM_API_KEY=sandbox:your-api-key NVM_RECEIVER=0xYourBuilderWallet yarn register-plans +# -> PLAN_ID_CREDITS=... / PLAN_ID_TIME=... / PLAN_ID_PAYG=... ``` -Edit `.env` with your credentials: +### 3. Configure environment ```bash -# Nevermined (required) -NVM_API_KEY=nvm:your-api-key -NVM_ENVIRONMENT=sandbox -NVM_PLAN_ID=your-plan-id - -# Agent -OPENAI_API_KEY=sk-your-openai-api-key -PORT=3000 - -# Client -SERVER_URL=http://localhost:3000 +cp .env.example .env ``` -### 3. Run the agent (server) - ```bash -yarn agent +# Nevermined (required). Environment is derived from the key prefix (sandbox:/live:). +NVM_API_KEY=sandbox:your-api-key + +# Plan IDs from `yarn register-plans` +PLAN_ID_CREDITS=... +PLAN_ID_TIME=... +PLAN_ID_PAYG=... + +PORT=3000 ``` -### 4. Run the client (in another terminal) +### 4. Run the agent ```bash -yarn client +yarn agent # dev (tsx) +# or: yarn build && yarn start # compiled (dist/agent.js) — same as the Docker image ``` -## x402 Headers +### 5. Run the smoke test (buyer, in another terminal) -The middleware follows the [x402 HTTP transport spec](https://github.com/coinbase/x402/blob/main/specs/transports-v2/http.md): +Exercises the x402 and MPP buyer flows against the running agent. Needs a Nevermined key and an `erc4337` delegation (created by the script); point it at the server with `SERVER_URL`. -| Header | Direction | Description | -| ------------------- | --------------------- | ----------------------------------- | -| `payment-signature` | Client → Server | Base64-encoded x402 access token | -| `payment-required` | Server → Client (402) | Base64-encoded payment requirements | -| `payment-response` | Server → Client (200) | Base64-encoded settlement receipt | +```bash +SERVER_URL=http://localhost:3000 NVM_API_KEY=sandbox:your-api-key PLAN_ID_CREDITS=... yarn smoke +``` -## Agent Code +## Agent code -The agent uses the `paymentMiddleware` from `@nevermined-io/payments/express`: +One middleware protects all three routes; `mpp: true` opts each into MPP alongside x402: ```typescript import { Payments } from "@nevermined-io/payments"; import { paymentMiddleware } from "@nevermined-io/payments/express"; -const payments = Payments.getInstance({ - nvmApiKey: NVM_API_KEY, - environment: NVM_ENVIRONMENT, -}); +// Environment is derived from the API-key prefix — no `environment` option. +const payments = Payments.getInstance({ nvmApiKey: NVM_API_KEY }); -// Protect routes with one line app.use( paymentMiddleware(payments, { - "POST /ask": { - planId: NVM_PLAN_ID, - credits: 1, + "POST /weather/credits": { planId: PLAN_ID_CREDITS, credits: 1, mpp: true }, + "POST /weather/subscription": { planId: PLAN_ID_TIME, credits: 1, mpp: true }, + "POST /weather/payg": { + planId: PLAN_ID_PAYG, + credits: (req) => priceForRequest(req.body), // 1 for today, up to 7 for a forecast + mpp: true, }, - }) + }), ); -// Route handler - no payment logic needed! -app.post("/ask", async (req, res) => { - const response = await openai.chat.completions.create({ ... }); - res.json({ response: response.choices[0].message.content }); +app.post("/weather/credits", async (req, res) => { + const { city } = parseWeatherRequest(req.body); // 400 on invalid input + res.json(await getTodayWeather(city)); // keyless Open-Meteo }); ``` -## Client Code +## Buyer code -The client demonstrates the full x402 flow: +A delegation backs the buyer for both protocols (in 1.11.2 even the x402 token needs one): ```typescript -import { Payments } from "@nevermined-io/payments"; import { X402_HEADERS } from "@nevermined-io/payments/express"; -// Step 1: Request without token -> 402 -const response1 = await fetch(`${SERVER_URL}/ask`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ query: "What is 2+2?" }), +const { delegationId } = await payments.delegation.createDelegation({ + provider: "erc4337", spendingLimitCents: 10000, durationSecs: 604800, currency: "usdc", }); -// Status: 402, Header: payment-required -// Step 2: Decode payment requirements -const paymentRequired = JSON.parse( - Buffer.from(response1.headers.get("payment-required"), "base64").toString() +// --- x402 --- +const { accessToken } = await payments.x402.getX402AccessToken( + PLAN_ID_CREDITS, undefined, { delegationConfig: { delegationId } }, ); - -// Step 3: Generate x402 token -const { accessToken } = await payments.x402.getX402AccessToken(NVM_PLAN_ID); - -// Step 4: Request with token -> 200 -const response2 = await fetch(`${SERVER_URL}/ask`, { +const res = await fetch(SERVER_URL + "/weather/credits", { method: "POST", - headers: { - "Content-Type": "application/json", - [X402_HEADERS.PAYMENT_SIGNATURE]: accessToken, - }, - body: JSON.stringify({ query: "What is 2+2?" }), + headers: { "content-type": "application/json", [X402_HEADERS.PAYMENT_SIGNATURE]: accessToken }, + body: JSON.stringify({ city: "Lisbon" }), }); -// Status: 200, Header: payment-response - -// Step 5: Decode settlement receipt -const settlement = JSON.parse( - Buffer.from(response2.headers.get("payment-response"), "base64").toString() +// -> 200 + weather, with a `payment-response` settlement receipt + +// --- MPP (same route; one call runs the challenge -> credential handshake) --- +const { response, receipt } = await payments.mpp.fetch( + SERVER_URL + "/weather/payg", + { method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ city: "Berlin", days: 5 }) }, + { delegationConfig: { delegationId }, planId: PLAN_ID_PAYG, maxCredits: 7 }, ); ``` -## API Reference - -### POST /ask - -Send a query to the AI assistant (payment protected). - -**Request Headers:** - -``` -Content-Type: application/json -payment-signature: -``` - -**Request Body:** - -```json -{ - "query": "Your question here" -} -``` - -**Success Response (200):** +## API reference -``` -Header: payment-response: -``` +All weather routes take `{ "city": "", "days"?: <1-7> }` and return the weather as JSON. `days > 1` (on `/weather/payg`) returns a multi-day forecast. -```json -{ - "response": "AI's answer" -} -``` +| Method | Route | Auth | +| --- | --- | --- | +| `POST` | `/weather/credits` | payment (x402 or MPP) | +| `POST` | `/weather/subscription` | payment (x402 or MPP) | +| `POST` | `/weather/payg` | payment (x402 or MPP) | +| `GET` | `/health` | none — returns `{ "ok": true }` | -**Payment Required Response (402):** +An unpaid request returns `402` with the x402 `payment-required` header and the MPP `WWW-Authenticate: Payment` challenge. Invalid input (missing/short `city`) returns `400`; an unknown city returns `404`. -``` -Header: payment-required: -``` +## Scripts -```json -{ - "error": "Payment Required", - "message": "Missing x402 payment token. Send token in payment-signature header." -} -``` +| Script | Description | +| --- | --- | +| `yarn agent` | Run the agent (dev, tsx) | +| `yarn register-plans` | Register the credits / time / pay-as-you-go plans | +| `yarn smoke` | Buyer round-trips (x402 + MPP) against a running agent | +| `yarn pricing:selfcheck` | Unit self-check for the pay-as-you-go pricing function | +| `yarn request:selfcheck` | Unit self-check for request input validation | +| `yarn build` | Compile TypeScript to `dist/` | +| `yarn start` | Run the compiled agent (`node dist/agent.js`) | -## Project Structure +## Project structure ``` http-simple-agent-ts/ ├── src/ -│ ├── agent.ts # Express server with payment middleware -│ ├── agent-observability.ts # Agent with Nevermined observability -│ └── client.ts # x402 flow demo client +│ ├── agent.ts # Express server — three dual-protocol weather routes +│ ├── pricing.ts # priceForRequest (pay-as-you-go) +│ ├── request.ts # parseWeatherRequest (boundary validation) +│ └── services/weather.service.ts # Open-Meteo current weather + forecast (keyless) +├── scripts/ +│ ├── register-plans.ts # register the three plans +│ └── smoke.ts # x402 + MPP buyer smoke test +├── deploy/argocd/ # ArgoCD deploy manifests + runbook (agents namespace) +├── Dockerfile ├── package.json ├── tsconfig.json -├── .env.example -├── .gitignore -└── README.md -``` - -## Scripts - -| Script | Description | -| ------------------------ | ---------------------------------------- | -| `yarn agent` | Run the agent server (dev mode) | -| `yarn agent:observability` | Run the agent with observability logging | -| `yarn client` | Run the client demo | -| `yarn build` | Build TypeScript to JavaScript | -| `yarn start:agent` | Run built agent | -| `yarn start:client` | Run built client | - -## Middleware Options - -```typescript -paymentMiddleware(payments, routes, { - // Custom token header - default: 'payment-signature' (x402 v2) - tokenHeader: "payment-signature", - - // Hook before verification - onBeforeVerify: (req, paymentRequired) => { - console.log(`Verifying ${req.path}`); - }, - - // Hook after verification (for observability) - onAfterVerify: (req, verification) => { - // Access agentRequest for observability setup - const agentRequest = verification.agentRequest; - if (agentRequest) { - console.log(`Agent: ${agentRequest.agentName}`); - } - }, - - // Hook after settlement - onAfterSettle: (req, creditsUsed, settlement) => { - console.log(`Settled ${creditsUsed} credits`); - }, - - // Custom error handler - onPaymentError: (error, req, res) => { - res.status(402).json({ error: error.message }); - }, -}); -``` - -## Route Configuration - -```typescript -paymentMiddleware(payments, { - // Fixed credits - "POST /ask": { planId: PLAN_ID, credits: 1 }, - - // Dynamic credits based on request - "POST /generate": { - planId: PLAN_ID, - credits: (req, res) => req.body.tokens / 100, - }, - - // Path parameters - "GET /users/:id": { planId: PLAN_ID, credits: 1 }, - - // With agent ID - "POST /agent/task": { - planId: PLAN_ID, - agentId: AGENT_ID, - credits: 5, - }, -}); -``` - -## Observability - -The `agent-observability.ts` demonstrates how to integrate Nevermined observability for tracking OpenAI costs per agent and plan. - -### How it works - -1. The `paymentMiddleware` verifies the x402 token and returns an `agentRequest` object -2. The `agentRequest` is available via `req.paymentContext.agentRequest` in route handlers -3. Pass `agentRequest` to `payments.observability.withOpenAI()` to route calls through Nevermined observability - -```typescript -// In your route handler -const agentRequest = req.paymentContext?.agentRequest; - -if (agentRequest) { - // Route OpenAI calls through Nevermined observability - const config = payments.observability.withOpenAI( - OPENAI_API_KEY, - agentRequest, - { sessionid: randomUUID() } - ); - openai = new OpenAI(config); -} +└── .env.example ``` -### agentRequest contents - -The `agentRequest` object contains: +## Deployment -| Field | Description | -| ----- | ----------- | -| `agentRequestId` | Unique identifier for this request | -| `agentName` | Name of the AI agent | -| `agentId` | ID of the AI agent | -| `balance.planId` | Payment plan ID | -| `balance.planName` | Payment plan name | -| `balance.balance` | Subscriber's remaining credits | -| `balance.pricePerCredit` | Cost per credit in USD | -| `urlMatching` | The matched endpoint URL | -| `verbMatching` | The matched HTTP verb | - -### Running with observability - -```bash -yarn agent:observability -``` +Deployment reference (Dockerfile → GCP Artifact Registry via the CI workflow → ArgoCD in the `agents` namespace) lives in [`deploy/argocd/`](./deploy/argocd/README.md), including the required chart env patch, the secret, `replicaCount: 1` (MPP single-use is in-process), and the startup-probe timing. -## Learn More +## Learn more - [Nevermined Documentation](https://nevermined.ai/docs) -- [Nevermined Observability Guide](https://nevermined.ai/docs/development-guide/observability) - [Nevermined x402 Smart Accounts Spec](https://nevermined.ai/docs/specs/x402-smart-accounts) - [x402 Protocol Specification](https://github.com/coinbase/x402) - [@nevermined-io/payments SDK](https://github.com/nevermined-io/payments) diff --git a/http-simple-agent-ts/deploy/argocd/README.md b/http-simple-agent-ts/deploy/argocd/README.md new file mode 100644 index 00000000..db9b88b8 --- /dev/null +++ b/http-simple-agent-ts/deploy/argocd/README.md @@ -0,0 +1,191 @@ +# ArgoCD deploy manifests — weather-x402-agent + +Reference manifests for deploying the `http-simple-agent-ts` weather x402/MPP +service to the `agents` namespace. **This directory is a reference only** — +these files are not consumed by ArgoCD from here. A human must copy them into +the separate `argocd` repo and open a PR there. + +## Where each file goes + +| This directory | Target in `nevermined-io/argocd` | +|---|---| +| `weather-x402-agent.yaml` | `eu/dev/argocd-apps/agents/weather-x402-agent.yaml` | +| `weather-x402-agent-staging.yaml` | `eu/dev/argocd-apps/agents/weather-x402-agent-staging.yaml` | + +Modeled on `eu/dev/argocd-apps/agents/demo-finance-agent.yaml` and the shared +chart `helm-charts/agent/v1.0.0/`. + +## Chart env-key finding (REQUIRED reading before deploying) + +`helm-charts/agent/v1.0.0/templates/deployment.yaml` renders the container's +`env:` block as a **fixed, hardcoded list** of `- name: X / value: +{{ .Values.env.X }}` lines — there is no generic `env.*` passthrough. Verified +by rendering the chart with `--set env.PLAN_ID_CREDITS=...` etc.: the values +land in `.Values` but **do not appear anywhere in the rendered Deployment**, +because the template has no line for them. + +- **`NVM_API_KEY` needs no chart change.** The chart already has a + `- name: NVM_API_KEY / value: {{ .Values.env.NVM_API_KEY }}` line, so the + `helm.parameters` entry `env.NVM_API_KEY: gcp:secretmanager:...` in both + manifests here renders straight through and is resolved at container start + by the `secrets-init` init container/wrapper (the same mechanism the + template uses for `secrets.openaiApiKey` → `OPENAI_API_KEY` on + demo-finance-agent — any env value matching the `gcp:secretmanager:...` + form is fetched and substituted before `initCommand` runs, regardless of + which `.Values` path it came from). Confirmed in the `helm template` output + below. +- **`PLAN_ID_CREDITS`, `PLAN_ID_TIME`, `PLAN_ID_PAYG` need a chart patch.** + These three names don't exist anywhere in `templates/deployment.yaml`, so + no `helm.parameters` value can make them appear as container env vars. + **Required precondition before merging either Application manifest**: add + three lines to `helm-charts/agent/v1.0.0/templates/deployment.yaml`, in the + `env:` block of the container spec (next to the existing `NVM_PLAN_ID` / + `PLAN_ID` lines is a natural spot): + + ```yaml + - name: PLAN_ID_CREDITS + value: "{{ .Values.env.PLAN_ID_CREDITS }}" + - name: PLAN_ID_TIME + value: "{{ .Values.env.PLAN_ID_TIME }}" + - name: PLAN_ID_PAYG + value: "{{ .Values.env.PLAN_ID_PAYG }}" + ``` + + This is additive only — it doesn't touch any existing line, so it's safe + for the other apps using this chart version. Ship it as its own tiny PR (or + as the first commit of the same PR that adds the two Application files), + merged *before* the Applications sync, otherwise the pods will start with + empty plan IDs and `agent.ts`'s startup check + (`if (!NVM_API_KEY || !PLAN_ID_CREDITS || !PLAN_ID_TIME || !PLAN_ID_PAYG)`) + will crash-loop the container. + +## `replicaCount: "1"` — required, not just default + +`values.yaml` already defaults `replicaCount: 1`, and it *is* a settable Helm +param (`{{ .Values.replicaCount }}` is used directly in `deployment.yaml` +when `autoscaling.enabled` is false, which is also the default). Both +manifests set it explicitly anyway for visibility: **do not raise it and do +not enable `autoscaling`**. The MPP single-use-token guard lives inside +`paymentMiddleware` (`@nevermined-io/payments/express`) — the +`inFlightMppCredentials` / `spentMppCredentials` in-process, in-memory sets, +not anything in `agent.ts` — so a second replica (or HPA scale-out) would let +the same MPP credential be redeemed twice, defeating the guard. (The SDK's own +comment on `spentMppCredentials` says the same: it does not span processes or +horizontally-scaled instances.) + +## Startup: probes must tolerate a ~15–20s boot + +The image takes ~15–20s to start (the payments SDK import is heavy) before +`GET /health` responds. Ensure the pod's readiness/liveness probe allows for +this — a startup probe, or a `readinessProbe` with enough `initialDelaySeconds` +/ `failureThreshold`. A liveness probe that fires sooner will kill the pod +mid-boot and crash-loop it. + +## Viewer API keys in access logs (showcase Connect flow) + +The tutorials showcase's "Connect with Nevermined" flow returns the viewer's +API key to the site as a **query param** (`?nvm_api_key=…`, the App's +`/auth/cli` shape), so it lands in the showcase server's (and any proxy/CDN's) +access logs on the redirect's request line. If you deploy the showcase, either +scrub that param at the edge, or track the App-side fix to return the key in +the URL **fragment** (`#…`, never sent to the server — the showcase capture +code already reads both). + +## `helm template` render summary (validated locally, no cluster contact) + +``` +cd ~/Projects/Nevermined/argocd/helm-charts/agent/v1.0.0 +helm template weather-x402-agent . -n agents \ + --set image.repository=europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402 \ + --set image.tag=sha-testsha \ + --set image.pullPolicy=Always \ + --set service.hostname=weather-x402-agent.nevermined.app \ + --set service.ports.port=3000 \ + --set service.ports.targetPort=3000 \ + --set service.ports.containerPort=3000 \ + --set replicaCount=1 \ + --set initCommand="yarn start" \ + --set ingress.rateLimitEnabled=true \ + --set env.NVM_API_KEY="gcp:secretmanager:projects/112425687177/secrets/tutorials-weather-nvm-api-key" +``` + +Rendered (helm v3.10.1), no template errors, four resources: + +- **ServiceAccount** `weather-x402-agent`, namespace `agents`, GKE Workload + Identity annotation present. +- **Service** `weather-x402-agent`, namespace `agents`, `ClusterIP`, port + `3000` → targetPort `3000`. +- **Deployment** `weather-x402-agent`, namespace `agents`, `replicas: 1`, + image `europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402:sha-testsha`, + `imagePullPolicy: Always`, command wrapped by `/secrets/secrets-init`, + `initCommand` = `yarn start`. Rendered env block (fixed list — this is what + confirms finding (b) above): + ``` + - name: NVM_API_KEY + value: gcp:secretmanager:projects/112425687177/secrets/tutorials-weather-nvm-api-key + - name: OPENAI_API_KEY + value: + - name: AGENT_DID + value: + ... (unrelated fixed keys for other agents, all empty here) ... + - name: BASE_URL + value: https://weather-x402-agent.nevermined.app + ``` + `PLAN_ID_CREDITS` / `PLAN_ID_TIME` / `PLAN_ID_PAYG` are **absent** from this + list, confirming the chart patch above is required. +- **Ingress** `weather-x402-agent`, host `weather-x402-agent.nevermined.app`, + rate-limit annotations present (`rateLimitEnabled=true`), TLS via + `letsencrypt-prod` cluster issuer, backend service port `3000`. + +`helm` was available locally (`v3.10.1`); no cluster contact was made — this +is template rendering only. + +## Human deploy steps (gated — not run by the agent that authored this) + +1. **Patch the chart** (see above): add the three `PLAN_ID_*` env lines to + `helm-charts/agent/v1.0.0/templates/deployment.yaml` in the `argocd` repo. + Bump the chart's `Chart.yaml` `version`/`appVersion` if that repo's + convention expects it for template-only changes (check recent chart + commits for precedent). +2. **Create the GCP Secret Manager secret** referenced by both manifests: + `projects/112425687177/secrets/tutorials-weather-nvm-api-key`, containing + the builder's `NVM_API_KEY` (the same key created in Task 4 / + `register-plans.ts`). Grant the GKE node/Workload Identity service account + (`112425687177-compute@developer.gserviceaccount.com`, per the rendered + ServiceAccount annotation above) `roles/secretmanager.secretAccessor` on + it, matching how `openai-api-key` is already granted for + demo-finance-agent. + - Prod and staging manifests point at the **same** secret path here + (single sandbox builder key). Split into a second secret first if the + two environments should not share credentials/spending. +3. **Run `yarn register-plans`** in `http-simple-agent-ts/` against the + target environment to obtain the three real plan IDs, then replace + `REPLACE_WITH_PLAN_ID` for `env.PLAN_ID_CREDITS` / `PLAN_ID_TIME` / + `PLAN_ID_PAYG` in both manifests (staging can reuse the same plan IDs or + register its own — decide based on whether staging spend should be + isolated). +4. **Replace the image tag.** After the first CI build on `main` (see + `.github/workflows/weather-x402-image.yml`), copy the printed `sha-` + tag into `image.tag` in both manifests, replacing + `sha-REPLACE_AFTER_FIRST_CI_BUILD`. +5. **Copy the two Application files** from this directory into + `eu/dev/argocd-apps/agents/` in the `argocd` repo (paths in the table + above), alongside the chart patch from step 1, in one PR. +6. **Commit, push, open the PR** in the `argocd` repo, get it reviewed, merge + to `main`. +7. **ArgoCD auto-syncs** the `agents` namespace (`syncPolicy.automated` with + `prune`/`selfHeal`) — no manual `kubectl apply` needed once merged. +8. **Verify the deployment is live**: + ```bash + curl -fsS https://weather-x402-agent.nevermined.app/health + kubectl -n agents get deploy weather-x402-agent + ``` + Expect `{"ok":true}` and `1/1` ready. Then re-run Task 5's `smoke.ts` with + `SERVER_URL=https://weather-x402-agent.nevermined.app` so both protocols + pay against the live service. Repeat steps 3, 4 and 8 (with the `-staging` + names/hostname) for the staging Application. + +None of the above (chart patch, secret creation, plan registration against a +real deployment target, `argocd` repo commit/push/PR, `kubectl`) was +performed by the agent that authored these files — this task is gated to +author + validate only. diff --git a/http-simple-agent-ts/deploy/argocd/weather-x402-agent-staging.yaml b/http-simple-agent-ts/deploy/argocd/weather-x402-agent-staging.yaml new file mode 100644 index 00000000..21109f4e --- /dev/null +++ b/http-simple-agent-ts/deploy/argocd/weather-x402-agent-staging.yaml @@ -0,0 +1,64 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: weather-x402-agent-staging + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io + # No argocd-image-updater.* annotations: the image tag is pinned to an + # immutable `sha-` tag printed by CI on push to main (Artifact + # Registry has immutableTags=true), not a moving semver tag. Bump + # image.tag below by hand (or via PR) after each build you want deployed. +spec: + project: default + source: + path: helm-charts/agent/v1.0.0 + repoURL: git@github.com:nevermined-io/argocd.git + targetRevision: main + helm: + parameters: + - name: image.repository + value: europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402 + - name: image.tag + value: sha-REPLACE_AFTER_FIRST_CI_BUILD + - name: image.pullPolicy + value: Always + - name: replicaCount + value: "1" + - name: env.NVM_API_KEY + value: gcp:secretmanager:projects/112425687177/secrets/tutorials-weather-nvm-api-key + - name: env.PLAN_ID_CREDITS + value: "77894004416396861654845539877107044009493609640794154706672817393635324531204" + - name: env.PLAN_ID_TIME + value: "10952064442830220931222688026195788696776029458594089784747035900055527856473" + - name: env.PLAN_ID_PAYG + value: "8310544419921962213409780763224169468183360879970520182660325277034660762576" + - name: service.hostname + value: weather-x402-agent-staging.nevermined.app + - name: service.ports.port + value: "3000" + - name: service.ports.targetPort + value: "3000" + - name: service.ports.containerPort + value: "3000" + - name: initCommand + value: yarn start + - name: ingress.rateLimitEnabled + value: "true" + destination: + namespace: agents + server: https://kubernetes.default.svc + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - PrunePropagationPolicy=foreground + - PruneLast=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m diff --git a/http-simple-agent-ts/deploy/argocd/weather-x402-agent.yaml b/http-simple-agent-ts/deploy/argocd/weather-x402-agent.yaml new file mode 100644 index 00000000..78f55f2f --- /dev/null +++ b/http-simple-agent-ts/deploy/argocd/weather-x402-agent.yaml @@ -0,0 +1,64 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: weather-x402-agent + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io + # No argocd-image-updater.* annotations: the image tag is pinned to an + # immutable `sha-` tag printed by CI on push to main (Artifact + # Registry has immutableTags=true), not a moving semver tag. Bump + # image.tag below by hand (or via PR) after each build you want deployed. +spec: + project: default + source: + path: helm-charts/agent/v1.0.0 + repoURL: git@github.com:nevermined-io/argocd.git + targetRevision: main + helm: + parameters: + - name: image.repository + value: europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-weather-x402 + - name: image.tag + value: sha-REPLACE_AFTER_FIRST_CI_BUILD + - name: image.pullPolicy + value: Always + - name: replicaCount + value: "1" + - name: env.NVM_API_KEY + value: gcp:secretmanager:projects/112425687177/secrets/tutorials-weather-nvm-api-key + - name: env.PLAN_ID_CREDITS + value: "77894004416396861654845539877107044009493609640794154706672817393635324531204" + - name: env.PLAN_ID_TIME + value: "10952064442830220931222688026195788696776029458594089784747035900055527856473" + - name: env.PLAN_ID_PAYG + value: "8310544419921962213409780763224169468183360879970520182660325277034660762576" + - name: service.hostname + value: weather-x402-agent.nevermined.app + - name: service.ports.port + value: "3000" + - name: service.ports.targetPort + value: "3000" + - name: service.ports.containerPort + value: "3000" + - name: initCommand + value: yarn start + - name: ingress.rateLimitEnabled + value: "true" + destination: + namespace: agents + server: https://kubernetes.default.svc + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - PrunePropagationPolicy=foreground + - PruneLast=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m diff --git a/http-simple-agent-ts/package.json b/http-simple-agent-ts/package.json index c28d59cc..4e817e6d 100644 --- a/http-simple-agent-ts/package.json +++ b/http-simple-agent-ts/package.json @@ -7,17 +7,17 @@ "type": "module", "scripts": { "agent": "tsx src/agent.ts", - "agent:observability": "tsx src/agent-observability.ts", - "client": "tsx src/client.ts", + "register-plans": "tsx scripts/register-plans.ts", + "smoke": "tsx scripts/smoke.ts", + "pricing:selfcheck": "tsx src/pricing.selfcheck.ts", + "request:selfcheck": "tsx src/request.selfcheck.ts", "build": "tsc", - "start:agent": "node dist/agent.js", - "start:client": "node dist/client.js" + "start": "node dist/agent.js" }, "dependencies": { - "@nevermined-io/payments": "^1.1.0", + "@nevermined-io/payments": "1.11.2", "dotenv": "^16.4.5", - "express": "^4.19.2", - "openai": "^4.77.0" + "express": "^4.19.2" }, "devDependencies": { "@types/express": "^4.17.21", diff --git a/http-simple-agent-ts/scripts/register-plans.ts b/http-simple-agent-ts/scripts/register-plans.ts new file mode 100644 index 00000000..47bc50e9 --- /dev/null +++ b/http-simple-agent-ts/scripts/register-plans.ts @@ -0,0 +1,38 @@ +import "dotenv/config"; +import { Payments } from "@nevermined-io/payments"; + +// Environment is derived from the API-key prefix (sandbox:/live:); no `environment` option. +const payments = Payments.getInstance({ nvmApiKey: process.env.NVM_API_KEY! }); +const plans = payments.plans; + +// Crypto price so BOTH x402 (erc4337) and MPP can pay. Needs the builder's receiver wallet. +// If NVM_RECEIVER is unset, fall back to a free plan so the script still runs for a demo. +const receiver = process.env.NVM_RECEIVER ?? ""; +const amount = BigInt(process.env.PLAN_PRICE_WEI ?? "0"); +if (!receiver) { + console.warn( + "⚠ NVM_RECEIVER is unset — registering FREE plans (getFreePriceConfig); NO payment will be charged. " + + "Set NVM_RECEIVER= (and optionally PLAN_PRICE_WEI) to register crypto-priced plans " + + "that x402 and MPP buyers actually pay.", + ); +} +const price = receiver + ? plans.getCryptoPriceConfig(amount, receiver as `0x${string}`) + : plans.getFreePriceConfig(); + +async function main() { + const { planId: credits } = await plans.registerCreditsPlan( + { name: "Weather — Fixed Credits", description: "100 weather calls, 1 credit each" }, + price, plans.getFixedCreditsConfig(100n, 1n)); + const { planId: time } = await plans.registerTimePlan( + { name: "Weather — Day Pass", description: "24h of weather access" }, + price, plans.getExpirableDurationConfig(86400n)); // 86400n = ONE_DAY (const not importable) + const { planId: payg } = await plans.registerPlan( + { name: "Weather — Pay As You Go", description: "1 credit current, up to 7 for a forecast" }, + price, plans.getDynamicCreditsConfig(1000n, 1n, 7n)); + + console.log("PLAN_ID_CREDITS=" + credits); + console.log("PLAN_ID_TIME=" + time); + console.log("PLAN_ID_PAYG=" + payg); +} +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/http-simple-agent-ts/scripts/smoke.ts b/http-simple-agent-ts/scripts/smoke.ts new file mode 100644 index 00000000..b80a8a24 --- /dev/null +++ b/http-simple-agent-ts/scripts/smoke.ts @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { Payments } from "@nevermined-io/payments"; +import { X402_HEADERS } from "@nevermined-io/payments/express"; + +const BASE = process.env.SERVER_URL || "http://localhost:3000"; +const PLAN = process.env.PLAN_ID_CREDITS!; +// Environment is derived from the API-key prefix (sandbox:/live:); no `environment` option. +const payments = Payments.getInstance({ nvmApiKey: process.env.NVM_API_KEY! }); +const assert = (c: boolean, m: string) => { if (!c) throw new Error("FAIL: " + m); }; + +async function main() { + // A delegation backs the buyer for BOTH protocols (erc4337). Create it once — in 1.11.2 even + // the x402 token needs a delegationConfig, so this is not MPP-only. + const { delegationId } = await payments.delegation.createDelegation({ + provider: "erc4337", spendingLimitCents: 10000, durationSecs: 604800, currency: "usdc", + }); + + // 1) Unpaid → 402 advertising BOTH protocols + const r0 = await fetch(`${BASE}/weather/credits`, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ city: "Lisbon" }), + }); + assert(r0.status === 402, "unpaid → 402"); + assert(!!r0.headers.get(X402_HEADERS.PAYMENT_REQUIRED), "x402 challenge present"); + assert(!!r0.headers.get("www-authenticate"), "MPP WWW-Authenticate present"); + + // 2) x402 buyer → 200 + settlement receipt (payment-response header) + const { accessToken } = await payments.x402.getX402AccessToken(PLAN, undefined, { + delegationConfig: { delegationId }, + }); + const r1 = await fetch(`${BASE}/weather/credits`, { + method: "POST", + headers: { "content-type": "application/json", [X402_HEADERS.PAYMENT_SIGNATURE]: accessToken }, + body: JSON.stringify({ city: "Lisbon" }), + }); + assert(r1.status === 200, `x402 paid → 200 (got ${r1.status})`); + const w = await r1.json(); + assert(typeof w.city === "string", "x402 returned real weather"); + assert(!!r1.headers.get(X402_HEADERS.PAYMENT_RESPONSE), "x402 settlement receipt present"); + console.log(`✓ x402: 200 ${w.city} ${w.tmaxC}°C — settled (payment-response header present)`); + + // 3) MPP buyer → 200 + real weather. mpp.fetch runs the challenge→credential handshake. + const { response, receipt, paid, settled, credentialsPresented } = await payments.mpp.fetch( + `${BASE}/weather/credits`, + { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ city: "Berlin" }) }, + { delegationConfig: { delegationId }, planId: PLAN }, + ); + assert(response.status === 200, `MPP served → 200 (got ${response.status})`); + const mw = await response.json(); + assert(typeof mw.city === "string", "MPP returned real weather"); + assert(credentialsPresented >= 1, "MPP presented a credential"); + // Settlement completes server-side after the response is sent. Report it rather than hard-fail: + // the verify+serve path is what the tutorial demonstrates; settlement outcome is backend-side + // (`paid` = response.ok && settled; a served-but-unsettled response reports paid=false). + const tag = paid ? "✓" : "⚠"; + console.log( + `${tag} MPP: 200 ${mw.city} ${mw.tmaxC}°C — credentialsPresented=${credentialsPresented}, settled=${settled}, receipt=${!!receipt}` + + (paid ? "" : " (served; settlement not confirmed — check the server's onAfterSettle / MPP settlement log)") + ); + + console.log( + `\n✓ smoke: x402 settled end-to-end; MPP verify+serve OK${paid ? " and settled" : " (settlement pending — see note above)"}`, + ); +} +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/http-simple-agent-ts/src/agent-observability.ts b/http-simple-agent-ts/src/agent-observability.ts deleted file mode 100644 index cdc52a9b..00000000 --- a/http-simple-agent-ts/src/agent-observability.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * HTTP Agent with Observability - Express server with Nevermined payment middleware and observability logging. - * - * This demonstrates how to: - * 1. Protect an endpoint using the x402 protocol with the standard paymentMiddleware - * 2. Access agentRequest from the payment context for observability - * 3. Route OpenAI calls through Nevermined observability for logging and analytics - * - * The agentRequest (available via req.paymentContext.agentRequest) contains: - * - agentRequestId: Unique identifier for this request - * - agentName: Name of the AI agent - * - agentId: ID of the AI agent - * - balance: Subscriber's credit balance info - * - urlMatching: The matched endpoint pattern - * - verbMatching: The matched HTTP verb - */ -import "dotenv/config"; -import express, { Request, Response, RequestHandler } from "express"; -import OpenAI from "openai"; -import { - Payments, - EnvironmentName, - StartAgentRequest, -} from "@nevermined-io/payments"; -import { - paymentMiddleware, - X402_HEADERS, - PaymentContext, -} from "@nevermined-io/payments/express"; -import { randomUUID } from "crypto"; - -const app = express(); -app.use(express.json()); - -const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; -const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; -const NVM_API_KEY = process.env.NVM_API_KEY ?? ""; -const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "staging_sandbox") as EnvironmentName; -const NVM_PLAN_ID = process.env.NVM_PLAN_ID ?? ""; -const NVM_AGENT_ID = process.env.NVM_AGENT_ID ?? ""; - -if (!OPENAI_API_KEY) { - console.error("OPENAI_API_KEY is required. Set it in .env file."); - process.exit(1); -} - -if (!NVM_API_KEY || !NVM_PLAN_ID) { - console.error("NVM_API_KEY and NVM_PLAN_ID are required for payment protection."); - process.exit(1); -} - -// Initialize Nevermined Payments SDK -const payments = Payments.getInstance({ - nvmApiKey: NVM_API_KEY, - environment: NVM_ENVIRONMENT, -}); - -// Extend Express Request to include payment context -declare global { - namespace Express { - interface Request { - paymentContext?: PaymentContext; - } - } -} - -// Apply payment middleware with observability hooks -app.use( - paymentMiddleware(payments, { - "POST /ask": { - planId: NVM_PLAN_ID, - agentId: NVM_AGENT_ID || undefined, - credits: 1, - }, - }, { - onAfterVerify: (req, verification) => { - // Log observability info after verification - const agentRequest = verification.agentRequest; - if (agentRequest) { - console.log("[Observability] Verification successful"); - console.log(` Agent: ${agentRequest.agentName} (${agentRequest.agentId})`); - console.log(` Request ID: ${agentRequest.agentRequestId}`); - console.log(` Plan: ${agentRequest.balance.planName} (${agentRequest.balance.planId})`); - console.log(` Balance: ${agentRequest.balance.balance} credits`); - } - }, - onAfterSettle: (req, creditsUsed, result) => { - console.log(`[Payment] Settled ${creditsUsed} credits`); - }, - }) as RequestHandler -); - -/** - * POST /ask - Send a query to the AI with Nevermined observability - * - * When agentRequest is available from payment verification, OpenAI calls - * are routed through Nevermined observability for: - * - Cost tracking per agent/plan - * - Usage analytics - * - Request tracing - */ -app.post("/ask", async (req: Request, res: Response) => { - try { - const { query } = req.body as { query?: string }; - - if (!query || typeof query !== "string") { - return res.status(400).json({ error: "Missing or invalid 'query' field" }); - } - - // Get agentRequest from payment context (set by middleware) - const agentRequest = req.paymentContext?.agentRequest; - - let openai: OpenAI; - - if (agentRequest) { - // With observability - routes through Nevermined observability for logging - const config = payments.observability.withOpenAI( - OPENAI_API_KEY, - agentRequest, - { sessionid: randomUUID() } - ); - openai = new OpenAI(config); - - console.log("[Observability] Using Nevermined observability logging"); - } else { - // Fallback without observability - openai = new OpenAI({ apiKey: OPENAI_API_KEY }); - console.log("[Observability] Nevermined observability not available, using direct OpenAI"); - } - - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [ - { - role: "system", - content: "You are a helpful assistant. Be concise and informative.", - }, - { role: "user", content: query }, - ], - max_tokens: 500, - }); - - const response = completion.choices[0]?.message?.content ?? "No response generated"; - - // Settlement happens automatically via middleware after res.json() - res.json({ - response, - observability: agentRequest ? { - agentRequestId: agentRequest.agentRequestId, - agentName: agentRequest.agentName, - planId: agentRequest.balance.planId, - } : null, - }); - } catch (error) { - console.error("Error in /ask:", error); - res.status(500).json({ error: "Internal server error" }); - } -}); - -/** - * GET /health - Health check endpoint (unprotected) - */ -app.get("/health", (_req: Request, res: Response) => { - res.json({ status: "ok", observability: "enabled" }); -}); - -app.listen(PORT, () => { - console.log(`HTTP Agent with Observability running on http://localhost:${PORT}`); - console.log(`\nPayment protection enabled for POST /ask`); - console.log(`Plan ID: ${NVM_PLAN_ID}`); - if (NVM_AGENT_ID) { - console.log(`Agent ID: ${NVM_AGENT_ID}`); - } - console.log(`\nObservability: OpenAI calls are logged to Nevermined when agentRequest is available.`); - console.log(`\nTo test, send requests with x402 token in '${X402_HEADERS.PAYMENT_SIGNATURE}' header.`); -}); diff --git a/http-simple-agent-ts/src/agent.ts b/http-simple-agent-ts/src/agent.ts index 3c9bee92..db968707 100644 --- a/http-simple-agent-ts/src/agent.ts +++ b/http-simple-agent-ts/src/agent.ts @@ -1,100 +1,94 @@ /** - * Protected HTTP Agent - Express server with Nevermined payment middleware. + * Weather HTTP Agent - Express server with dual-protocol Nevermined payment middleware. * - * This demonstrates how to protect an endpoint using the x402 protocol. - * The /ask endpoint requires a valid x402 access token and burns credits. + * Three plans, three routes, both protocols. `mpp: true` on every paid route makes + * each 402 advertise x402 AND MPP, so either buyer works against the same URL. * * x402 HTTP Transport Headers: * - Client sends token in: `payment-signature` header * - Server returns 402 with: `payment-required` header (base64-encoded) + * + * MPP HTTP Transport Headers: + * - Client sends credential in: `authorization` header + * - Server returns 402 with: `www-authenticate` header */ import "dotenv/config"; import express, { Request, Response, RequestHandler } from "express"; -import OpenAI from "openai"; -import { Payments, EnvironmentName } from "@nevermined-io/payments"; -import { paymentMiddleware, X402_HEADERS } from "@nevermined-io/payments/express"; - -const app = express(); -app.use(express.json()); +import { Payments } from "@nevermined-io/payments"; +import { paymentMiddleware, X402_HEADERS, MPP_HEADERS } from "@nevermined-io/payments/express"; +import { getTodayWeather, getForecast, CityNotFoundError } from "./services/weather.service.js"; +import { priceForRequest } from "./pricing.js"; +import { parseWeatherRequest, BadRequestError } from "./request.js"; const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; -const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? ""; const NVM_API_KEY = process.env.NVM_API_KEY ?? ""; -const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "testing") as EnvironmentName; -const NVM_PLAN_ID = process.env.NVM_PLAN_ID ?? ""; +const PLAN_ID_CREDITS = process.env.PLAN_ID_CREDITS ?? ""; +const PLAN_ID_TIME = process.env.PLAN_ID_TIME ?? ""; +const PLAN_ID_PAYG = process.env.PLAN_ID_PAYG ?? ""; -if (!OPENAI_API_KEY) { - console.error("OPENAI_API_KEY is required. Set it in .env file."); +if (!NVM_API_KEY || !PLAN_ID_CREDITS || !PLAN_ID_TIME || !PLAN_ID_PAYG) { + console.error("NVM_API_KEY, PLAN_ID_CREDITS, PLAN_ID_TIME and PLAN_ID_PAYG are required."); process.exit(1); } -if (!NVM_API_KEY || !NVM_PLAN_ID) { - console.error("NVM_API_KEY and NVM_PLAN_ID are required for payment protection."); - process.exit(1); -} +// Environment is derived from the API-key prefix (sandbox:/live:); no `environment` option. +const payments = Payments.getInstance({ nvmApiKey: NVM_API_KEY }); -const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); +const app = express(); +app.use(express.json()); -// Initialize Nevermined Payments SDK -const payments = Payments.getInstance({ - nvmApiKey: NVM_API_KEY, - environment: NVM_ENVIRONMENT, -}); +// /health stays unprotected — mount it before the paywall. +app.get("/health", (_req: Request, res: Response) => res.json({ ok: true })); -// Apply payment middleware to protect the /ask endpoint -// x402-compliant: expects `payment-signature` header with access token -// Note: Type cast needed due to Express type overload resolution +// One middleware, three plans, both protocols. `mpp: true` makes each 402 advertise +// x402 AND MPP, so either buyer works against the same URL. app.use( - paymentMiddleware(payments, { - "POST /ask": { - planId: NVM_PLAN_ID, - credits: 1, + paymentMiddleware( + payments, + { + "POST /weather/credits": { planId: PLAN_ID_CREDITS, credits: 1, mpp: true }, + "POST /weather/subscription": { planId: PLAN_ID_TIME, credits: 1, mpp: true }, + "POST /weather/payg": { + planId: PLAN_ID_PAYG, + credits: (req: Request) => priceForRequest(req.body), + mpp: true, + }, }, - }, { - onBeforeVerify: (req) => { - console.log(`[Payment] Verifying request to ${req.path}`); + { + onBeforeVerify: (req) => console.log(`[pay] verify ${req.method} ${req.path}`), + onAfterSettle: (req, credits) => console.log(`[pay] settled ${credits} on ${req.path}`), }, - onAfterSettle: (req, creditsUsed) => { - console.log(`[Payment] Settled ${creditsUsed} credits for ${req.path}`); - }, - }) as RequestHandler + ) as RequestHandler, ); -/** - * POST /ask - Send a query to the AI (protected by payment middleware) - */ -app.post("/ask", async (req: Request, res: Response) => { +async function handleToday(req: Request, res: Response) { try { - const { query } = req.body as { query?: string }; - - if (!query || typeof query !== "string") { - return res.status(400).json({ error: "Missing or invalid 'query' field" }); - } - - const completion = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [ - { - role: "system", - content: "You are a helpful assistant. Be concise and informative.", - }, - { role: "user", content: query }, - ], - max_tokens: 500, - }); + const { city } = parseWeatherRequest(req.body); + return res.json(await getTodayWeather(city)); + } catch (err) { + if (err instanceof BadRequestError) return res.status(400).json({ error: err.message }); + if (err instanceof CityNotFoundError) return res.status(404).json({ error: err.message }); + console.error(err); + return res.status(502).json({ error: "weather upstream failed" }); + } +} - const response = completion.choices[0]?.message?.content ?? "No response generated"; +app.post("/weather/credits", handleToday); +app.post("/weather/subscription", handleToday); - res.json({ response }); - } catch (error) { - console.error("Error in /ask:", error); - res.status(500).json({ error: "Internal server error" }); +app.post("/weather/payg", async (req: Request, res: Response) => { + try { + const { city, days } = parseWeatherRequest(req.body); + return res.json(days && days > 1 ? await getForecast(city, days) : await getTodayWeather(city)); + } catch (err) { + if (err instanceof BadRequestError) return res.status(400).json({ error: err.message }); + if (err instanceof CityNotFoundError) return res.status(404).json({ error: err.message }); + console.error(err); + return res.status(502).json({ error: "weather upstream failed" }); } }); app.listen(PORT, () => { - console.log(`Protected HTTP Agent running on http://localhost:${PORT}`); - console.log(`\nPayment protection enabled for POST /ask`); - console.log(`Plan ID: ${NVM_PLAN_ID}`); - console.log(`\nTo test, send requests with x402 token in '${X402_HEADERS.PAYMENT_SIGNATURE}' header.`); + console.log(`weather-x402 agent on http://localhost:${PORT}`); + console.log(`x402 header: ${X402_HEADERS.PAYMENT_SIGNATURE} · MPP header: ${MPP_HEADERS.CREDENTIAL}`); }); diff --git a/http-simple-agent-ts/src/client.ts b/http-simple-agent-ts/src/client.ts deleted file mode 100644 index 8f152e3a..00000000 --- a/http-simple-agent-ts/src/client.ts +++ /dev/null @@ -1,242 +0,0 @@ -/** - * x402 Client - Demonstrates the full payment flow. - * - * This client shows the complete x402 HTTP protocol flow: - * 1. Request without token -> 402 Payment Required - * 2. Decode payment requirements from header - * 3. Generate x402 access token - * 4. Request with token -> Success - * 5. Decode settlement response from header - */ -import "dotenv/config"; -import { Payments, EnvironmentName } from "@nevermined-io/payments"; -import { X402_HEADERS } from "@nevermined-io/payments/express"; -import type { X402SchemeType } from "@nevermined-io/payments"; - -const SERVER_URL = process.env.SERVER_URL || "http://localhost:3000"; -const NVM_API_KEY = process.env.NVM_API_KEY ?? ""; -const NVM_ENVIRONMENT = (process.env.NVM_ENVIRONMENT || "testing") as EnvironmentName; -const NVM_PLAN_ID = process.env.NVM_PLAN_ID ?? ""; - -if (!NVM_API_KEY || !NVM_PLAN_ID) { - console.error("NVM_API_KEY and NVM_PLAN_ID are required."); - process.exit(1); -} - -// Initialize Nevermined Payments SDK -const payments = Payments.getInstance({ - nvmApiKey: NVM_API_KEY, - environment: NVM_ENVIRONMENT, -}); - -/** - * Helper to decode base64 JSON from headers - */ -function decodeBase64Json(base64: string): unknown { - const json = Buffer.from(base64, "base64").toString("utf-8"); - return JSON.parse(json); -} - -/** - * Helper to format JSON for console output - */ -function prettyJson(obj: unknown): string { - return JSON.stringify(obj, null, 2); -} - -async function main() { - console.log("=".repeat(60)); - console.log("x402 Payment Flow Demo"); - console.log("=".repeat(60)); - console.log(`\nServer: ${SERVER_URL}`); - console.log(`Plan ID: ${NVM_PLAN_ID}`); - - // ============================================================ - // Step 1: Request without token -> Expect 402 - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("STEP 1: Request without payment token"); - console.log("=".repeat(60)); - - const response1 = await fetch(`${SERVER_URL}/ask`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ query: "What is 2+2?" }), - }); - - console.log(`\nStatus: ${response1.status} ${response1.statusText}`); - - if (response1.status !== 402) { - console.error("Expected 402 Payment Required, got:", response1.status); - process.exit(1); - } - - // ============================================================ - // Step 2: Decode payment requirements from header - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("STEP 2: Decode payment requirements from header"); - console.log("=".repeat(60)); - - const paymentRequiredHeader = response1.headers.get(X402_HEADERS.PAYMENT_REQUIRED); - - if (!paymentRequiredHeader) { - console.error(`Missing '${X402_HEADERS.PAYMENT_REQUIRED}' header in 402 response`); - process.exit(1); - } - - console.log(`\nHeader '${X402_HEADERS.PAYMENT_REQUIRED}' (base64):`); - console.log(paymentRequiredHeader.substring(0, 80) + "..."); - - const paymentRequired = decodeBase64Json(paymentRequiredHeader); - console.log("\nDecoded Payment Requirements:"); - console.log(prettyJson(paymentRequired)); - - // Also print the JSON body - const body1 = await response1.json(); - console.log("\nResponse body:"); - console.log(prettyJson(body1)); - - // ============================================================ - // Step 3: Generate x402 access token (scheme-aware) - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("STEP 3: Generate x402 access token"); - console.log("=".repeat(60)); - - // Read scheme from the 402 response to determine payment type - const paymentRequiredObj = paymentRequired as any; - const scheme: X402SchemeType = - paymentRequiredObj?.accepts?.[0]?.scheme || "nvm:erc4337"; - console.log(`\nDetected scheme: ${scheme}`); - - let tokenResult; - if (scheme === "nvm:card-delegation") { - // Fiat flow: list enrolled cards and create a card-delegation token - console.log("\nFiat plan detected — listing enrolled payment methods..."); - const paymentMethods = await payments.delegation.listPaymentMethods(); - if (paymentMethods.length === 0) { - console.error( - "No enrolled payment methods. Enroll a card in the Nevermined App first." - ); - process.exit(1); - } - const pm = paymentMethods[0]; - console.log(`Using payment method: ${pm.brand} ****${pm.last4}`); - - tokenResult = await payments.x402.getX402AccessToken( - NVM_PLAN_ID, - undefined, - undefined, - undefined, - undefined, - { - scheme: "nvm:card-delegation", - delegationConfig: { - providerPaymentMethodId: pm.id, - spendingLimitCents: 10000, - durationSecs: 604800, - currency: "usd", - }, - } - ); - } else { - // Crypto flow: standard ERC-4337 token - console.log("\nCalling payments.x402.getX402AccessToken()..."); - tokenResult = await payments.x402.getX402AccessToken(NVM_PLAN_ID); - } - - const accessToken = tokenResult.accessToken; - - console.log("\nToken generated successfully!"); - console.log(`Token length: ${accessToken.length} characters`); - console.log(`Token preview: ${accessToken.substring(0, 50)}...`); - - // Decode and show token structure (it's base64-encoded JSON) - try { - const decodedToken = decodeBase64Json(accessToken); - console.log("\nDecoded token structure:"); - console.log(prettyJson(decodedToken)); - } catch { - console.log("\n(Token is not base64 JSON - showing raw)"); - } - - // ============================================================ - // Step 4: Request with token -> Expect success - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("STEP 4: Request with payment token"); - console.log("=".repeat(60)); - - console.log(`\nSending request with '${X402_HEADERS.PAYMENT_SIGNATURE}' header...`); - - const response2 = await fetch(`${SERVER_URL}/ask`, { - method: "POST", - headers: { - "Content-Type": "application/json", - [X402_HEADERS.PAYMENT_SIGNATURE]: accessToken, - }, - body: JSON.stringify({ query: "What is 2+2?" }), - }); - - console.log(`\nStatus: ${response2.status} ${response2.statusText}`); - - if (response2.status !== 200) { - console.error("Expected 200 OK, got:", response2.status); - const errorBody = await response2.text(); - console.error("Response:", errorBody); - process.exit(1); - } - - const body2 = await response2.json(); - console.log("\nResponse body:"); - console.log(prettyJson(body2)); - - // ============================================================ - // Step 5: Check for settlement response header - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("STEP 5: Check settlement response header"); - console.log("=".repeat(60)); - - const paymentResponseHeader = response2.headers.get(X402_HEADERS.PAYMENT_RESPONSE); - - if (paymentResponseHeader) { - console.log(`\nHeader '${X402_HEADERS.PAYMENT_RESPONSE}' found!`); - console.log(`(base64): ${paymentResponseHeader.substring(0, 80)}...`); - - try { - const settlementResponse = decodeBase64Json(paymentResponseHeader); - console.log("\nDecoded Settlement Response:"); - console.log(prettyJson(settlementResponse)); - } catch { - console.log("\n(Could not decode as JSON)"); - console.log(paymentResponseHeader); - } - } else { - console.log(`\nNo '${X402_HEADERS.PAYMENT_RESPONSE}' header in response.`); - console.log("(Settlement happens asynchronously after response is sent)"); - } - - // ============================================================ - // Summary - // ============================================================ - console.log("\n" + "=".repeat(60)); - console.log("FLOW COMPLETE!"); - console.log("=".repeat(60)); - console.log(` -x402 Payment Flow Summary: -1. Request without token -> 402 Payment Required -2. Decoded payment-required -> Plan ID, scheme, network -3. Generated access token -> Using Nevermined SDK -4. Request with token -> 200 OK + AI response -5. Settlement -> Credits burned asynchronously -`); -} - -main().catch((error) => { - console.error("Error:", error); - process.exit(1); -}); diff --git a/http-simple-agent-ts/src/pricing.selfcheck.ts b/http-simple-agent-ts/src/pricing.selfcheck.ts new file mode 100644 index 00000000..8c64b4c8 --- /dev/null +++ b/http-simple-agent-ts/src/pricing.selfcheck.ts @@ -0,0 +1,12 @@ +import { priceForRequest } from "./pricing.js"; + +const assert = (c: boolean, m: string) => { if (!c) throw new Error("FAIL: " + m); }; + +assert(priceForRequest({ city: "Lisbon" }) === 1, "no days → 1 credit"); +assert(priceForRequest({ city: "Lisbon", days: 1 }) === 1, "1 day → 1 credit"); +assert(priceForRequest({ city: "Lisbon", days: 7 }) === 7, "7 days → 7 credits"); +assert(priceForRequest({ city: "Lisbon", days: 99 }) === 7, "days capped at 7"); +assert(priceForRequest({ city: "Lisbon", days: 0 }) === 1, "days floored at 1"); +assert(priceForRequest({ city: "Lisbon", days: "3" as any }) === 3, "numeric string coerced"); +assert(priceForRequest({}) === 1, "empty body → 1 credit"); +console.log("✓ pricing self-check passed"); diff --git a/http-simple-agent-ts/src/pricing.ts b/http-simple-agent-ts/src/pricing.ts new file mode 100644 index 00000000..c2db43fa --- /dev/null +++ b/http-simple-agent-ts/src/pricing.ts @@ -0,0 +1,7 @@ +/** Pay-as-you-go price: 1 credit for current weather, `days` credits (1..7) for a forecast. */ +export function priceForRequest(body: unknown): number { + const raw = (body as { days?: unknown } | null | undefined)?.days; + const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN; + if (!Number.isFinite(n)) return 1; + return Math.max(1, Math.min(7, Math.trunc(n))); +} diff --git a/http-simple-agent-ts/src/request.selfcheck.ts b/http-simple-agent-ts/src/request.selfcheck.ts new file mode 100644 index 00000000..7fd6b6e9 --- /dev/null +++ b/http-simple-agent-ts/src/request.selfcheck.ts @@ -0,0 +1,20 @@ +import { parseWeatherRequest, BadRequestError } from "./request.js"; + +const assert = (c: boolean, m: string) => { if (!c) throw new Error("FAIL: " + m); }; +const assertThrows = (fn: () => unknown, m: string) => { + try { + fn(); + throw new Error("FAIL: " + m + " (did not throw)"); + } catch (err) { + if (!(err instanceof BadRequestError)) throw new Error("FAIL: " + m + " (wrong error type)"); + } +}; + +assert(parseWeatherRequest({ city: "Lisbon" }).city === "Lisbon", "valid city passes through"); +assert(parseWeatherRequest({ city: "Lisbon", days: "7" }).days === 7, "numeric string days coerced"); +assertThrows(() => parseWeatherRequest({}), "missing city throws"); +assertThrows(() => parseWeatherRequest({ city: "x" }), "too-short city throws"); +assertThrows(() => parseWeatherRequest({ city: 123 }), "non-string city throws"); +assertThrows(() => parseWeatherRequest({ city: "Lisbon", days: "abc" }), "non-numeric days throws"); + +console.log("✓ request self-check passed"); diff --git a/http-simple-agent-ts/src/request.ts b/http-simple-agent-ts/src/request.ts new file mode 100644 index 00000000..d8172c4e --- /dev/null +++ b/http-simple-agent-ts/src/request.ts @@ -0,0 +1,19 @@ +export class BadRequestError extends Error { + constructor(message: string) { super(message); this.name = "BadRequestError"; } +} + +/** Validate the weather request body at the trust boundary. Throws BadRequestError on invalid input. */ +export function parseWeatherRequest(body: unknown): { city: string; days?: number } { + const b = (body ?? {}) as Record; + const city = b.city; + if (typeof city !== "string" || city.trim().length < 2 || city.trim().length > 80) { + throw new BadRequestError("'city' must be a string between 2 and 80 characters"); + } + let days: number | undefined; + if (b.days !== undefined) { + const n = typeof b.days === "number" ? b.days : typeof b.days === "string" ? Number(b.days) : NaN; + if (!Number.isFinite(n)) throw new BadRequestError("'days' must be a number"); + days = n; // getForecast already clamps to 1..7 + } + return { city: city.trim(), days }; +} diff --git a/http-simple-agent-ts/src/services/weather.service.ts b/http-simple-agent-ts/src/services/weather.service.ts new file mode 100644 index 00000000..4a669861 --- /dev/null +++ b/http-simple-agent-ts/src/services/weather.service.ts @@ -0,0 +1,238 @@ +/** + * Weather service: geocoding and current forecast using Open-Meteo. + * All functions use the built-in fetch available in Node.js >= 18. + */ + +export type TodayWeather = { + city: string; + country: string | null; + latitude: number; + longitude: number; + timezone: string; + updatedAt: string; + tmaxC: number | null; + tminC: number | null; + precipitationMm: number | null; + weatherCode: number | null; + weatherText: string | null; +}; + +export class CityNotFoundError extends Error { + constructor(public readonly city: string) { + super(`City not found: ${city}`); + this.name = "CityNotFoundError"; + } +} + +export class DownstreamError extends Error { + constructor(message: string) { + super(message); + this.name = "DownstreamError"; + } +} + +export function sanitizeCity(rawCity: string): string { + const trimmed = rawCity.trim(); + if (trimmed.length < 2 || trimmed.length > 80) { + throw new Error("City must be between 2 and 80 characters long"); + } + return trimmed; +} + +export async function geocodeCity(city: string): Promise<{ + name: string; + country: string | null; + latitude: number; + longitude: number; + timezone: string | null; +}> { + const q = sanitizeCity(city); + const url = new URL("https://geocoding-api.open-meteo.com/v1/search"); + url.searchParams.set("name", q); + url.searchParams.set("count", "1"); + url.searchParams.set("language", "en"); + + let res: Response; + try { + res = await fetch(url); + } catch (err) { + throw new DownstreamError("Failed to reach Open-Meteo geocoding API"); + } + + if (!res.ok) { + throw new DownstreamError(`Geocoding API returned HTTP ${res.status}`); + } + + const data = (await res.json()) as any; + if (!data || !Array.isArray(data.results) || data.results.length === 0) { + throw new CityNotFoundError(q); + } + const first = data.results[0]; + return { + name: String(first.name), + country: first.country ? String(first.country) : null, + latitude: Number(first.latitude), + longitude: Number(first.longitude), + timezone: first.timezone ? String(first.timezone) : null, + }; +} + +export function weatherCodeToText( + code: number | null | undefined +): string | null { + if (code === null || code === undefined || Number.isNaN(code)) return null; + const mapping: Record = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Fog", + 48: "Depositing rime fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 56: "Light freezing drizzle", + 57: "Dense freezing drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 66: "Light freezing rain", + 67: "Heavy freezing rain", + 71: "Slight snow fall", + 73: "Moderate snow fall", + 75: "Heavy snow fall", + 77: "Snow grains", + 80: "Slight rain showers", + 81: "Moderate rain showers", + 82: "Violent rain showers", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm with slight hail", + 99: "Thunderstorm with heavy hail", + }; + return mapping[code] ?? "Unknown"; +} + +export async function getTodayWeather(city: string): Promise { + const geo = await geocodeCity(city); + + const url = new URL("https://api.open-meteo.com/v1/forecast"); + url.searchParams.set("latitude", String(geo.latitude)); + url.searchParams.set("longitude", String(geo.longitude)); + url.searchParams.set("current_weather", "true"); + url.searchParams.set("timezone", "auto"); + url.searchParams.set( + "daily", + "temperature_2m_max,temperature_2m_min,precipitation_sum" + ); + + let res: Response; + try { + res = await fetch(url); + } catch (err) { + throw new DownstreamError("Failed to reach Open-Meteo forecast API"); + } + + if (!res.ok) { + throw new DownstreamError(`Forecast API returned HTTP ${res.status}`); + } + + const data = (await res.json()) as any; + const tz: string = data.timezone ?? geo.timezone ?? "unknown"; + const daily = data.daily ?? {}; + const tmax: number | null = + Array.isArray(daily.temperature_2m_max) && + daily.temperature_2m_max.length > 0 + ? Number(daily.temperature_2m_max[0]) + : null; + const tmin: number | null = + Array.isArray(daily.temperature_2m_min) && + daily.temperature_2m_min.length > 0 + ? Number(daily.temperature_2m_min[0]) + : null; + const precip: number | null = + Array.isArray(daily.precipitation_sum) && daily.precipitation_sum.length > 0 + ? Number(daily.precipitation_sum[0]) + : null; + const current = data.current_weather ?? {}; + const code: number | null = + typeof current.weathercode === "number" + ? Number(current.weathercode) + : null; + + const result: TodayWeather = { + city: geo.name, + country: geo.country, + latitude: geo.latitude, + longitude: geo.longitude, + timezone: tz, + updatedAt: new Date().toISOString(), + tmaxC: tmax, + tminC: tmin, + precipitationMm: precip, + weatherCode: code, + weatherText: weatherCodeToText(code), + }; + + return result; +} + +export type ForecastDay = { + date: string; + tmaxC: number | null; + tminC: number | null; + precipitationMm: number | null; + weatherCode: number | null; + weatherText: string | null; +}; + +export type ForecastWeather = { + city: string; + country: string | null; + timezone: string; + updatedAt: string; + days: ForecastDay[]; +}; + +export async function getForecast(city: string, days: number): Promise { + const n = Math.max(1, Math.min(7, Math.trunc(days))); + const geo = await geocodeCity(city); + const url = new URL("https://api.open-meteo.com/v1/forecast"); + url.searchParams.set("latitude", String(geo.latitude)); + url.searchParams.set("longitude", String(geo.longitude)); + url.searchParams.set("forecast_days", String(n)); + url.searchParams.set("timezone", "auto"); + url.searchParams.set("daily", "temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode"); + + let res: Response; + try { + res = await fetch(url); + } catch { + throw new DownstreamError("Failed to reach Open-Meteo forecast API"); + } + if (!res.ok) throw new DownstreamError(`Forecast API returned HTTP ${res.status}`); + + const data = (await res.json()) as any; + const d = data.daily ?? {}; + const time: string[] = Array.isArray(d.time) ? d.time : []; + const daysOut: ForecastDay[] = time.map((date: string, i: number) => { + const code = Array.isArray(d.weathercode) ? Number(d.weathercode[i]) : null; + return { + date, + tmaxC: Array.isArray(d.temperature_2m_max) ? Number(d.temperature_2m_max[i]) : null, + tminC: Array.isArray(d.temperature_2m_min) ? Number(d.temperature_2m_min[i]) : null, + precipitationMm: Array.isArray(d.precipitation_sum) ? Number(d.precipitation_sum[i]) : null, + weatherCode: code, + weatherText: weatherCodeToText(code), + }; + }); + + return { + city: geo.name, + country: geo.country, + timezone: data.timezone ?? geo.timezone ?? "unknown", + updatedAt: new Date().toISOString(), + days: daysOut, + }; +} diff --git a/http-simple-agent-ts/yarn.lock b/http-simple-agent-ts/yarn.lock index d92f1db7..d2a884db 100644 --- a/http-simple-agent-ts/yarn.lock +++ b/http-simple-agent-ts/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@a2a-js/sdk@^0.3.4": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@a2a-js/sdk/-/sdk-0.3.7.tgz#3e7f65ba29cda90d81a70001f5391cace8431d4c" - integrity sha512-1WBghkOjgiKt4rPNje8jlB9VateVQXqyjlc887bY/H8yM82Hlf0+5JW8zB98BPExKAplI5XqtXVH980J6vqi+w== +"@a2a-js/sdk@^0.3.13": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@a2a-js/sdk/-/sdk-0.3.14.tgz#e807b4e6516ea8c4af621ad9789d8dad6173058d" + integrity sha512-F6Ew1AtPzCLhTn8h9yiqTe7DiDf6XVrSnq9V1YqSl9eWqPm6anMveTiKdCSb/76cW0YiJc24rNaUrVezFFHbqQ== dependencies: uuid "^11.1.0" @@ -144,6 +144,32 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== +"@google-cloud/opentelemetry-cloud-trace-exporter@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-3.1.0.tgz#1c34bb1c9b4dd50040afd298f21e2653189a5674" + integrity sha512-O7tpsMYLcZA7eE+y5ZpLBYHZfruvnYhmh4RLI2xTelVMsmR2JJolQCi3gk6dlDo58QDh7UzmX48X8TeNG1oG2g== + dependencies: + "@google-cloud/opentelemetry-resource-util" "^3.1.0" + "@grpc/grpc-js" "^1.1.8" + "@grpc/proto-loader" "^0.8.0" + google-auth-library "^10.0.0" + +"@google-cloud/opentelemetry-resource-util@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@google-cloud/opentelemetry-resource-util/-/opentelemetry-resource-util-3.1.0.tgz#d255fd66acc6fe2e49eec30611fc53812bae9464" + integrity sha512-dTvbFl+q9ac0Me8+GaDrFdwDBia/L8Qxxi/NY/jeHaUogDIpqOtLoqcXV9HdgiLxy5vjEGaJMqir1nVra1qG7w== + dependencies: + "@opentelemetry/semantic-conventions" "^1.22.0" + gcp-metadata "^9.0.0" + +"@grpc/grpc-js@^1.1.8": + version "1.14.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.4.tgz#e73ff57d97802f063999545f43ebb2b1eca65d9d" + integrity sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ== + dependencies: + "@grpc/proto-loader" "^0.8.0" + "@js-sdsl/ordered-map" "^4.4.2" + "@grpc/grpc-js@^1.10.9", "@grpc/grpc-js@^1.7.1": version "1.14.3" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz#4c9b817a900ae4020ddc28515ae4b52c78cfb8da" @@ -182,34 +208,29 @@ resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== -"@langchain/core@^0.3.58": - version "0.3.80" - resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.80.tgz#c494a6944e53ab28bf32dc531e257b17cfc8f797" - integrity sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA== +"@langchain/core@>=1.0.0 <2.0.0": + version "1.2.9" + resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.2.9.tgz#4f5fb27ba07c51ce4fb8e1dc32eb36945737afa1" + integrity sha512-conzSEj9Zu1AyXJLXsSbgrtxtxinmI1yGqQ5CIJZSoV5rvv+yvQE/vgBnoySpBQ/bl3YPgj2FL/gbDjWykLSfg== dependencies: "@cfworker/json-schema" "^4.0.2" - ansi-styles "^5.0.0" - camelcase "6" - decamelize "1.2.0" + "@standard-schema/spec" "^1.1.0" js-tiktoken "^1.0.12" - langsmith "^0.3.67" + langsmith ">=0.5.0 <1.0.0" mustache "^4.2.0" p-queue "^6.6.2" - p-retry "4" - uuid "^10.0.0" - zod "^3.25.32" - zod-to-json-schema "^3.22.3" + zod "^3.25.76 || ^4" -"@nevermined-io/payments@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@nevermined-io/payments/-/payments-1.1.0.tgz#5257c4c870a77915ecef7f7687955177298bcd21" - integrity sha512-grRz660Ds15n9+hamI6ZfvOp3CvLXrUBRdc0kSa9XuZiFCwmDQcErXGvrvyB0UUtAVJ51I7sKpBgS3Heq88CLw== +"@nevermined-io/payments@1.11.2": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@nevermined-io/payments/-/payments-1.11.2.tgz#9d803db4ef630085c0ecd02383cd0e7ebea0d04b" + integrity sha512-BdD3zErRzri3/++KHl7YdpbImH2UEpsJvCQGZIATwwbI9ApvTxoIWjAR3HabMyU9VkA6PxzE0T50d1WrB++LKQ== dependencies: - "@a2a-js/sdk" "^0.3.4" + "@a2a-js/sdk" "^0.3.13" "@helicone/helpers" "^1.6.0" "@opentelemetry/api" "^1.9.0" - "@opentelemetry/exporter-trace-otlp-http" "^0.203.0" - "@traceloop/node-server-sdk" "^0.14.6" + "@opentelemetry/exporter-trace-otlp-http" "^0.215.0" + "@traceloop/node-server-sdk" "^0.26.0" axios "^1.13.1" express "4.21.2" jose "^5.2.4" @@ -225,6 +246,13 @@ dependencies: "@opentelemetry/api" "^1.3.0" +"@opentelemetry/api-logs@0.215.0": + version "0.215.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.215.0.tgz#4670ee4ac65ad386bb2d955af9e96e6766aa1195" + integrity sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA== + dependencies: + "@opentelemetry/api" "^1.3.0" + "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" @@ -254,6 +282,13 @@ dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" +"@opentelemetry/core@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.7.0.tgz#41ebcd3033f69e48e719266524877f8fef75bc03" + integrity sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ== + dependencies: + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/exporter-logs-otlp-grpc@0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.203.0.tgz#2f1254bea8f0dab0c974d791af23e0a8e9f5eecc" @@ -349,7 +384,7 @@ "@opentelemetry/resources" "2.0.1" "@opentelemetry/sdk-trace-base" "2.0.1" -"@opentelemetry/exporter-trace-otlp-http@0.203.0", "@opentelemetry/exporter-trace-otlp-http@^0.203.0": +"@opentelemetry/exporter-trace-otlp-http@0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.203.0.tgz#29908aac3ee4d085a17bb653dcee2778a9ba3bb9" integrity sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw== @@ -360,6 +395,17 @@ "@opentelemetry/resources" "2.0.1" "@opentelemetry/sdk-trace-base" "2.0.1" +"@opentelemetry/exporter-trace-otlp-http@^0.215.0": + version "0.215.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.215.0.tgz#e4c40ae641f33216826a4550836ec3967c5d3084" + integrity sha512-k4J9ISeGpb0Bm/wCrlcrbroMFTkiWMrdhNxQGrlktxLy127Yzd4/7nrTawn5d/ApktYTknvdixsE6++34Qfi1w== + dependencies: + "@opentelemetry/core" "2.7.0" + "@opentelemetry/otlp-exporter-base" "0.215.0" + "@opentelemetry/otlp-transformer" "0.215.0" + "@opentelemetry/resources" "2.7.0" + "@opentelemetry/sdk-trace-base" "2.7.0" + "@opentelemetry/exporter-trace-otlp-proto@0.203.0", "@opentelemetry/exporter-trace-otlp-proto@^0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.203.0.tgz#c8b15b7a9088e2580a8dd2c3e057845dfe990100" @@ -398,6 +444,14 @@ "@opentelemetry/core" "2.0.1" "@opentelemetry/otlp-transformer" "0.203.0" +"@opentelemetry/otlp-exporter-base@0.215.0": + version "0.215.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.215.0.tgz#a42eb76242dcc08bb472f4b16477f6f30d6daa5a" + integrity sha512-lHrfbmeLSmesGSkkHiqDwOzfaEMSWXdc7q6UoLfbW8byONCb+bE/zkAr0kapN4US1baT/2nbpNT7Cn9XoB96Vg== + dependencies: + "@opentelemetry/core" "2.7.0" + "@opentelemetry/otlp-transformer" "0.215.0" + "@opentelemetry/otlp-grpc-exporter-base@0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.203.0.tgz#0600151f3eb70f05923f621afaf9ac72ae213261" @@ -421,6 +475,19 @@ "@opentelemetry/sdk-trace-base" "2.0.1" protobufjs "^7.3.0" +"@opentelemetry/otlp-transformer@0.215.0": + version "0.215.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.215.0.tgz#69de6ae54d1db685a8cf85472be36d9099919fff" + integrity sha512-cWwBvaV+vkXHkSoTYR8hGw+AW03UlgTr6xtrUKOMeum3T+8vffYXIfXu6KY5MLu8O9QtoBKqaKWw9I5xoOepng== + dependencies: + "@opentelemetry/api-logs" "0.215.0" + "@opentelemetry/core" "2.7.0" + "@opentelemetry/resources" "2.7.0" + "@opentelemetry/sdk-logs" "0.215.0" + "@opentelemetry/sdk-metrics" "2.7.0" + "@opentelemetry/sdk-trace-base" "2.7.0" + protobufjs "^8.0.1" + "@opentelemetry/propagator-b3@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-2.0.1.tgz#0b2875724a9c3f8353366e05cc2f701763faa940" @@ -451,6 +518,14 @@ "@opentelemetry/core" "2.3.0" "@opentelemetry/semantic-conventions" "^1.29.0" +"@opentelemetry/resources@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.7.0.tgz#82abff22253c06a73bd392e218645d1ccf197c91" + integrity sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A== + dependencies: + "@opentelemetry/core" "2.7.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/sdk-logs@0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.203.0.tgz#01bc7c0549929d2864af2ab0ba23fd5ce02b5b0a" @@ -460,6 +535,16 @@ "@opentelemetry/core" "2.0.1" "@opentelemetry/resources" "2.0.1" +"@opentelemetry/sdk-logs@0.215.0": + version "0.215.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.215.0.tgz#cf8ca6fd558484eb4b2366d604b9e1a2a77775f2" + integrity sha512-y3ucOmphzc4vgBTyIGchs+N/1rkACmoka8QalT2z1LBNM232Z17zMYayHcMl+dgMoOadZ0b72UZv7mDtqy1cFA== + dependencies: + "@opentelemetry/api-logs" "0.215.0" + "@opentelemetry/core" "2.7.0" + "@opentelemetry/resources" "2.7.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/sdk-metrics@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz#efb6e9349e8a9038ac622e172692bfcdcad8010b" @@ -468,6 +553,14 @@ "@opentelemetry/core" "2.0.1" "@opentelemetry/resources" "2.0.1" +"@opentelemetry/sdk-metrics@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz#0bcfa214e85eb78c882d109fda3c2f09eb9e051c" + integrity sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w== + dependencies: + "@opentelemetry/core" "2.7.0" + "@opentelemetry/resources" "2.7.0" + "@opentelemetry/sdk-node@^0.203.0": version "0.203.0" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-node/-/sdk-node-0.203.0.tgz#16f4a77031834c32ff1b6864873db7169791fd14" @@ -514,6 +607,15 @@ "@opentelemetry/resources" "2.3.0" "@opentelemetry/semantic-conventions" "^1.29.0" +"@opentelemetry/sdk-trace-base@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.0.tgz#b2290b43fe9252f85fc37a243852a14bff110a85" + integrity sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A== + dependencies: + "@opentelemetry/core" "2.7.0" + "@opentelemetry/resources" "2.7.0" + "@opentelemetry/semantic-conventions" "^1.29.0" + "@opentelemetry/sdk-trace-node@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.1.tgz#bbb9bdb4985d7930941b3d4023e1661ba46f60c1" @@ -532,18 +634,16 @@ "@opentelemetry/core" "2.3.0" "@opentelemetry/sdk-trace-base" "2.3.0" -"@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.36.0": +"@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.38.0", "@opentelemetry/semantic-conventions@^1.40.0": + version "1.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz#f3f467e36c27332f0e735ec86cdcd78dd6f27865" + integrity sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg== + +"@opentelemetry/semantic-conventions@^1.29.0": version "1.38.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz#8b5f415395a7ddb7c8e0c7932171deb9278df1a3" integrity sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg== -"@posthog/core@1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.9.1.tgz#bcc5392359b8413c2de19181c081fcc0475ab784" - integrity sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw== - dependencies: - cross-spawn "^7.0.6" - "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -597,159 +697,203 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@standard-schema/spec@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@traceloop/ai-semantic-conventions@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@traceloop/ai-semantic-conventions/-/ai-semantic-conventions-0.14.0.tgz#1bb37b72fba84d8cacc14a34c5d82974c47f629e" - integrity sha512-0hpu19rwFNXeYp7B3s0s+9Jo4TqYiDvbQYpzIU5Xu0tx4HUhbocaJEVwWrLJMwFDx7PMPkMu2M1Vte09OtipWw== +"@traceloop/ai-semantic-conventions@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/ai-semantic-conventions/-/ai-semantic-conventions-0.26.0.tgz#b5d106ef4639e311872037e689a6205c27d9c014" + integrity sha512-Ict2OXBDw58Ij3WgHZqtloHkDsjVmm6MRsrehsfPfSg4rJk5HQc1ZIRwssFsjSbly6k6WXE//1U9lA61tZj4mg== dependencies: "@opentelemetry/api" "^1.9.0" + "@opentelemetry/semantic-conventions" "^1.40.0" -"@traceloop/instrumentation-anthropic@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-anthropic/-/instrumentation-anthropic-0.14.6.tgz#4931a2a8ddb15fc3905b69531faf017f0a908fbd" - integrity sha512-7AmPZJVh2THCumGjCq8FcVDw1YQR3K0xM6woTODZlfxRwnr3bdMtbTkZs7IwFFxF1932n47cIFl6AfX3cTwSSA== +"@traceloop/instrumentation-anthropic@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-anthropic/-/instrumentation-anthropic-0.26.0.tgz#003af80304ecbb7071567af6fde29cbaf4b380ce" + integrity sha512-esZbo2F3uEanIs6SU6hyt1sRlHRUnWIuTu29b72QXWT7XzS8l2TAY1usMNUT86W7CGVWZOHR4BT0B/tdoIijVA== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-bedrock@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-bedrock/-/instrumentation-bedrock-0.14.6.tgz#c5597f69c45f1b54c7b041f733359dc023be0eb2" - integrity sha512-1vQj0moJZjrwk6h6GU17OgjN9MFLmO0jMV2uaaBJAUT9dZuNTBbwhQN20zQssWXI5vPHCtok+JQM1DG2rI14cg== +"@traceloop/instrumentation-bedrock@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-bedrock/-/instrumentation-bedrock-0.26.0.tgz#8fbf6f5b5c5406741ada63762ea05922272c895d" + integrity sha512-SNTRyqmQHpH2CN7R3m3+aanYOqOzAhr0Orbbzx+mOonsUuXY+Yo7IiejwOu0exPudx+7czbUV1JbvDC1hjYZKQ== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-chromadb@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-chromadb/-/instrumentation-chromadb-0.14.6.tgz#1f1c14250c9a7b66f98e8178402723bbb7c588ec" - integrity sha512-/GfMEIOaz+Za1E2Ef0lhMmBADnZdDZgY16uescNxole4IKjPV7/VYIzd6ZZcDq5/YebpHCXyJWeg6cAbFrfRUw== +"@traceloop/instrumentation-chromadb@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-chromadb/-/instrumentation-chromadb-0.26.0.tgz#55511620d0b7ebcd828e12b361b7e3498cec1f1b" + integrity sha512-eFu/Zwj5IblMRWzQblXO0mtUXWSylB08RSJzfj0gM2zU7TlHk7iz+NZHhvldTee39+wIqHIVPc6//d0UwB8+kQ== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-cohere@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-cohere/-/instrumentation-cohere-0.14.6.tgz#be0e6ea88e78aa43cdae36c89ff6763bff001395" - integrity sha512-y9cjDZDS2o0yL7ei1H+wzl8ZgzjxDyzuUrAW18O5KXvX9W2we7RrJAyqdLQ48q5pb2uK9+W9qwTsdv8PkHBHkQ== +"@traceloop/instrumentation-cohere@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-cohere/-/instrumentation-cohere-0.26.0.tgz#19abacc417fe883b8200ab449b30e2b4cd0e2d70" + integrity sha512-fdPq8kiL2gzHzJIwJXF2Ffw6J3jm5V9L1m6gqNHCQ8tSBT5WPwzHmN+OPCknq1/oaBaymWyepSYRn/8wOzov6g== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-langchain@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-langchain/-/instrumentation-langchain-0.14.6.tgz#322ec1e0417dfb2f6f7abeef1d0069b7f70bb944" - integrity sha512-yxtpLsYCE+hqeV3J3F7HvBls4OZ3XUzhISXQK1gMzXKRPwn8hB0frPivZ3mNtnqI4i+Lq6hALO3jl/Pqel7hMA== +"@traceloop/instrumentation-google-generativeai@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-google-generativeai/-/instrumentation-google-generativeai-0.26.0.tgz#8f1ab4dccea0a2d4efda97525cb8697b0964fd2b" + integrity sha512-26aiuE+e7hMol/vJDYVEynB86PFPZVdAdHHwxNZdEba6ASGo7EZvJYcvjHImP8dJVgOVP2en7YquJ0+TXUd0Yg== dependencies: - "@langchain/core" "^0.3.58" "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-llamaindex@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-llamaindex/-/instrumentation-llamaindex-0.14.6.tgz#80c739702056ec2bfdca39127d0f89fc743cbae2" - integrity sha512-Pp519YHEilHjMJviNxS6NTlN9aZGXNQgDCpbaWv0cPp116arR6LlMB+K57wwaDDRmk7QzYsQgtEN0BuuuOjCnQ== +"@traceloop/instrumentation-langchain@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-langchain/-/instrumentation-langchain-0.26.0.tgz#d6c8f3c20b34bfa6b04bf9b7d0cddd57381b9918" + integrity sha512-WY8ZUXP51ZM0NwYiCupzEYyA/D2lw4LZi3bQ3NQRGjNTCaj/BEF1Ho9R8HpYSvBn/kJnbwsnGsxqgu1+fex6ig== dependencies: + "@langchain/core" ">=1.0.0 <2.0.0" "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + tslib "^2.8.1" + +"@traceloop/instrumentation-llamaindex@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-llamaindex/-/instrumentation-llamaindex-0.26.0.tgz#8355e798ec064be9084ee1fff4f56f06f82ee202" + integrity sha512-hT9eB288hyhNmNoxXT7C+jzGq9W/nKe4qAZw8tsWZoxvNUapUda1ppd9aacVhJky2sW9++rSgxZPmoCVgqzpWA== + dependencies: + "@opentelemetry/api" "^1.9.0" + "@opentelemetry/core" "^2.0.1" + "@opentelemetry/instrumentation" "^0.203.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" lodash "^4.17.21" tslib "^2.8.1" -"@traceloop/instrumentation-openai@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-openai/-/instrumentation-openai-0.14.6.tgz#35e8c77ab31fb9cf45335cc531115b6cb4de81de" - integrity sha512-WGFxbRf1Rn6+Pl+sOuLSJVGSyD/g7XhRFJ2X9qdapu+XRThcPboxwFMYaG7j6rLj5fxXDLqfuzwZJ0IRzmdIYA== +"@traceloop/instrumentation-mcp@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-mcp/-/instrumentation-mcp-0.26.0.tgz#9f930c01b85ef15620099ca74429c596cec4796d" + integrity sha512-1xZZvAryZU/lWhagpnqQ+PULfpRnUc0mupp1BVso2fqJaISxBbJnUtWSUxHSZwcsHaWMz4nQ9NeZue/dnPBswQ== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + tslib "^2.8.1" + +"@traceloop/instrumentation-openai@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-openai/-/instrumentation-openai-0.26.0.tgz#4c36dff3460a358f98f4005f826b9c24c4e02b4a" + integrity sha512-ekFOgNLlb6VlxD30nCEH0dl2OgiWTWovZzMJJP80hH5hOgU9N20RJ468OtxVjlyw3PoU6iPVtU9UBsDsLhTfgQ== + dependencies: + "@opentelemetry/api" "^1.9.0" + "@opentelemetry/core" "^2.0.1" + "@opentelemetry/instrumentation" "^0.203.0" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" js-tiktoken "^1.0.20" tslib "^2.8.1" -"@traceloop/instrumentation-pinecone@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-pinecone/-/instrumentation-pinecone-0.14.6.tgz#027fd6386998deea15e257fb2fbf86ac711e165c" - integrity sha512-ztEbjSGcaiRL56QXknJoBSCts19fNXzuMvRpb5eMgNxzMif0kCqZnurrjky6dqDJxtxI0rzQ5mCOpXLqd+D9mA== +"@traceloop/instrumentation-pinecone@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-pinecone/-/instrumentation-pinecone-0.26.0.tgz#2ad6c0890a4055212016e4eb92b48ae8ef2d188a" + integrity sha512-2Dbhf1RvWFb/9gTf76c4bSbgTVP6yZBAb5qiIujfvIm3YTRC7coLfxKxzXaHGH3RF2fCdPxV6IIVFVHOAtCO/w== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-qdrant@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-qdrant/-/instrumentation-qdrant-0.14.6.tgz#660ebe2c23fc5305f06caf1edc86a6f5f021b75a" - integrity sha512-jdi9BW4O9Yjc79T4UJBnsp041xS6QbgQssmvRZ3LlZ4g1jfk+QBGM6FFBGbgx9m4lWV0tys4SP5q7p9aFENtqw== +"@traceloop/instrumentation-qdrant@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-qdrant/-/instrumentation-qdrant-0.26.0.tgz#63e7d8567a3af232448df1e6daf20360c45596d5" + integrity sha512-ZDU8C1wNrYhIVaAcwcm/ukDUSgLWvfZjYhCZxtXlOjeWMjodBUGR2/OrtYgjN68IbfM8dJukXHXj6uzS2S2/tw== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@traceloop/ai-semantic-conventions" "0.26.0" tslib "^2.8.1" -"@traceloop/instrumentation-together@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-together/-/instrumentation-together-0.14.6.tgz#4312878d6265f51080bf54146b994ec07614910e" - integrity sha512-P/YtcIT8c8Ua26ksL3Vn5/a3KgYBCx6aUonWdzR5bkDWyJClLAxmbgrbj+Qx/Pad19U9BlpiZBhGCdT8upOpcg== +"@traceloop/instrumentation-together@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-together/-/instrumentation-together-0.26.0.tgz#4311fccc5e2c739c5c0d9d8236c6d09cbb30bc63" + integrity sha512-pLkQI8WUAEeJmWxILhZNGpn6NBtfrFqXzIa+TWxJVlxdUtUCQ1XLbc2f5KOHuwGCATlmCO1uFs+GlVur8mqcDw== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" js-tiktoken "^1.0.20" tslib "^2.8.1" -"@traceloop/instrumentation-vertexai@0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-vertexai/-/instrumentation-vertexai-0.14.6.tgz#61813187f9596a68ad731974eb31529d414bad0b" - integrity sha512-OsxAsAz8+068M1N3CAdKxS2RkizcdKFaKagIP+lE94ANeWdlD7M7IIZ/d7yP/wdDWlQg6l5IodybnQVEyzTD/A== +"@traceloop/instrumentation-utils@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-utils/-/instrumentation-utils-0.26.0.tgz#e858162c4f902c0ae9e19dff910897fde5bfaa4a" + integrity sha512-9uPCUVeZzJ0xh9DogLWKXd+bZsjwh/XJfYeLG3fkoUgmj/4XaTVf3W3F/z3yjDBcUI0vHwOwe2vpg+nWBHXZFQ== + dependencies: + "@opentelemetry/semantic-conventions" "^1.40.0" + tslib "^2.8.1" + +"@traceloop/instrumentation-vertexai@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/instrumentation-vertexai/-/instrumentation-vertexai-0.26.0.tgz#7a2203f7ab3fa502366346a671daef04230c263f" + integrity sha512-ithoN+3GuVVgue1Dgaj3fad0lxMj9JZqirvrjgHaI92L1rrJhlsYghX4ak965/4Jd3Uc9VGjnJm+Pz+OENPeBw== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/instrumentation" "^0.203.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" + "@opentelemetry/semantic-conventions" "^1.38.0" + "@traceloop/ai-semantic-conventions" "0.26.0" google-gax "^4.0.0" tslib "^2.8.1" -"@traceloop/node-server-sdk@^0.14.6": - version "0.14.6" - resolved "https://registry.yarnpkg.com/@traceloop/node-server-sdk/-/node-server-sdk-0.14.6.tgz#a7f3210c887c17f6f32b7e2620e6c02541776175" - integrity sha512-RRNsXCh+3Qy4lE/e7b3LmXpyeVASBUkWw5TTm4HxWJu9pHsyEcEKFV7EAmkh+s6gQomB03ZA4ifT9Pem6bR+vA== +"@traceloop/node-server-sdk@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@traceloop/node-server-sdk/-/node-server-sdk-0.26.0.tgz#74f247760d7050e6611d8846e6070dad7ecbc08c" + integrity sha512-0m1fD58kl73n62IodjOx8rjz3hE5mOyy/kv5S7Fq/DS09lyrH7N7VcR2PCQpzDXKLm2Mv0PnYNqVBaVTK2WRfA== dependencies: + "@google-cloud/opentelemetry-cloud-trace-exporter" "^3.0.0" "@opentelemetry/api" "^1.9.0" "@opentelemetry/core" "^2.0.1" "@opentelemetry/exporter-trace-otlp-proto" "^0.203.0" @@ -758,25 +902,31 @@ "@opentelemetry/sdk-node" "^0.203.0" "@opentelemetry/sdk-trace-base" "^2.0.1" "@opentelemetry/sdk-trace-node" "^2.0.1" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@traceloop/ai-semantic-conventions" "0.14.0" - "@traceloop/instrumentation-anthropic" "0.14.6" - "@traceloop/instrumentation-bedrock" "0.14.6" - "@traceloop/instrumentation-chromadb" "0.14.6" - "@traceloop/instrumentation-cohere" "0.14.6" - "@traceloop/instrumentation-langchain" "0.14.6" - "@traceloop/instrumentation-llamaindex" "0.14.6" - "@traceloop/instrumentation-openai" "0.14.6" - "@traceloop/instrumentation-pinecone" "0.14.6" - "@traceloop/instrumentation-qdrant" "0.14.6" - "@traceloop/instrumentation-together" "0.14.6" - "@traceloop/instrumentation-vertexai" "0.14.6" + "@opentelemetry/semantic-conventions" "^1.40.0" + "@traceloop/ai-semantic-conventions" "0.26.0" + "@traceloop/instrumentation-anthropic" "0.26.0" + "@traceloop/instrumentation-bedrock" "0.26.0" + "@traceloop/instrumentation-chromadb" "0.26.0" + "@traceloop/instrumentation-cohere" "0.26.0" + "@traceloop/instrumentation-google-generativeai" "0.26.0" + "@traceloop/instrumentation-langchain" "0.26.0" + "@traceloop/instrumentation-llamaindex" "0.26.0" + "@traceloop/instrumentation-mcp" "0.26.0" + "@traceloop/instrumentation-openai" "0.26.0" + "@traceloop/instrumentation-pinecone" "0.26.0" + "@traceloop/instrumentation-qdrant" "0.26.0" + "@traceloop/instrumentation-together" "0.26.0" + "@traceloop/instrumentation-utils" "0.26.0" + "@traceloop/instrumentation-vertexai" "0.26.0" "@types/nunjucks" "^3.2.6" cross-fetch "^4.1.0" + eventsource "^3.0.2" fetch-retry "^6.0.0" + mime-types "^3.0.2" nunjucks "^3.2.4" - posthog-node "^5.5.1" + papaparse "^5.5.3" supports-color "^10.0.0" + tslib "^2.8.1" uuid "^11.1.0" "@types/body-parser@*": @@ -834,14 +984,6 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/node-fetch@^2.6.4": - version "2.6.13" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee" - integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw== - dependencies: - "@types/node" "*" - form-data "^4.0.4" - "@types/node@*", "@types/node@>=13.7.0": version "25.0.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.8.tgz#e54e00f94fe1db2497b3e42d292b8376a2678c8d" @@ -849,13 +991,6 @@ dependencies: undici-types "~7.16.0" -"@types/node@^18.11.18": - version "18.19.130" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" - integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg== - dependencies: - undici-types "~5.26.4" - "@types/node@^22.7.4": version "22.19.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.6.tgz#0e9d80ebcd2dfce03265768c17a1212d4eb07e82" @@ -888,11 +1023,6 @@ "@types/tough-cookie" "*" form-data "^2.5.5" -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - "@types/send@*": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" @@ -922,11 +1052,6 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== -"@types/uuid@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" - integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== - a-sync-waterfall@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" @@ -969,30 +1094,18 @@ agent-base@^7.1.2: resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== -agentkeepalive@^4.2.1: - version "4.6.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" - integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== - dependencies: - humanize-ms "^1.2.1" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1089,19 +1202,6 @@ call-bound@^1.0.2: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" -camelcase@6: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - cjs-module-lexer@^1.2.2: version "1.4.3" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" @@ -1140,13 +1240,6 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -console-table-printer@^2.12.1: - version "2.15.0" - resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.15.0.tgz#5c808204640b8f024d545bde8aabe5d344dfadc1" - integrity sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw== - dependencies: - simple-wcswidth "^1.1.2" - content-disposition@0.5.4, content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -1186,14 +1279,10 @@ cross-fetch@^4.1.0: dependencies: node-fetch "^2.7.0" -cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== debug@2.6.9: version "2.6.9" @@ -1209,11 +1298,6 @@ debug@4, debug@^4.3.5: dependencies: ms "^2.1.3" -decamelize@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1371,6 +1455,18 @@ eventemitter3@^4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventsource-parser@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.1.1.tgz#b96cbb7dace4f3774f58a9e3b1ae9a4a524872e2" + integrity sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + express@4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -1450,6 +1546,14 @@ extend@^3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + fetch-retry@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-6.0.0.tgz#4ffdf92c834d72ae819e42a4ee2a63f1e9454426" @@ -1486,11 +1590,6 @@ follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== -form-data-encoder@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" - integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== - form-data@^2.5.5: version "2.5.5" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.5.tgz#a5f6364ad7e4e67e95b4a07e2d8c6f711c74f624" @@ -1514,13 +1613,12 @@ form-data@^4.0.4: hasown "^2.0.2" mime-types "^2.1.12" -formdata-node@^4.3.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" - integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== dependencies: - node-domexception "1.0.0" - web-streams-polyfill "4.0.0-beta.3" + fetch-blob "^3.1.2" forwarded@0.2.0: version "0.2.0" @@ -1553,6 +1651,24 @@ gaxios@^6.0.0, gaxios@^6.1.1: node-fetch "^2.6.9" uuid "^9.0.1" +gaxios@^7.0.0, gaxios@^7.1.3, gaxios@^7.1.4: + version "7.3.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-7.3.1.tgz#b26223db05856dfa5e2393ea376abad00e479c62" + integrity sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + node-fetch "^3.3.2" + +gcp-metadata@8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz#e62e3373ddf41fc727ccc31c55c687b798bee898" + integrity sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg== + dependencies: + gaxios "^7.0.0" + google-logging-utils "^1.0.0" + json-bigint "^1.0.0" + gcp-metadata@^6.1.0: version "6.1.1" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" @@ -1562,6 +1678,15 @@ gcp-metadata@^6.1.0: google-logging-utils "^0.0.2" json-bigint "^1.0.0" +gcp-metadata@^9.0.0: + version "9.0.3" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-9.0.3.tgz#d8ed78f75b3c842d8cf5c2b8e73e28e2fd80f17f" + integrity sha512-2YYnIlHaKBGT2IPg3G2M57hia9Galz15zsEOvw9T3oRf0lSn6KN6VcHQLqby7x8ksYKnjXvp3rp2KJyLCN6zfQ== + dependencies: + gaxios "^7.1.3" + google-logging-utils "^2.0.0" + json-bigint "^1.0.0" + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -1598,6 +1723,18 @@ get-tsconfig@^4.7.5: dependencies: resolve-pkg-maps "^1.0.0" +google-auth-library@^10.0.0: + version "10.9.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-10.9.1.tgz#af9852a328a6077e8ffbe776681b4ca9420a1714" + integrity sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^7.1.4" + gcp-metadata "8.1.2" + google-logging-utils "1.1.3" + jws "^4.0.0" + google-auth-library@^9.3.0: version "9.15.1" resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" @@ -1628,11 +1765,26 @@ google-gax@^4.0.0: retry-request "^7.0.0" uuid "^9.0.1" +google-logging-utils@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz#17b71f1f95d266d2ddd356b8f00178433f041b17" + integrity sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA== + google-logging-utils@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== +google-logging-utils@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-1.2.0.tgz#89dd643f986c765e61bfa2d01cada2292fe5bfd7" + integrity sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A== + +google-logging-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-2.0.1.tgz#d78b34f4250db9ee816f29fee3321939296bcb8a" + integrity sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A== + gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -1646,11 +1798,6 @@ gtoken@^7.0.0: gaxios "^6.0.0" jws "^4.0.0" -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" @@ -1717,13 +1864,6 @@ https-proxy-agent@^7.0.1: agent-base "^7.1.2" debug "4" -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - iconv-lite@0.4.24, iconv-lite@~0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1768,11 +1908,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - jose@^5.2.4: version "5.10.0" resolved "https://registry.yarnpkg.com/jose/-/jose-5.10.0.tgz#c37346a099d6467c401351a9a0c2161e0f52c4be" @@ -1814,17 +1949,12 @@ jws@^4.0.0: jwa "^2.0.1" safe-buffer "^5.0.1" -langsmith@^0.3.67: - version "0.3.87" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.87.tgz#f1c991c93a5d4d226a31671be7e4443b4b8673b1" - integrity sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q== +"langsmith@>=0.5.0 <1.0.0": + version "0.10.1" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.10.1.tgz#944d16ee0e43d621f11449c0deb6ee0edab0efce" + integrity sha512-zRDCnLznGdzx1VottX4CWr8v9ZZLRoSql2pbjEXYA1Jeg+NMDdq87x/v0Dk4GNkNZYhE+ZxFX3vTxwWq6W6gVA== dependencies: - "@types/uuid" "^10.0.0" - chalk "^4.1.2" - console-table-printer "^2.12.1" - p-queue "^6.6.2" - semver "^7.6.3" - uuid "^10.0.0" + p-queue "6.6.2" lodash.camelcase@^4.3.0: version "4.3.0" @@ -1836,7 +1966,7 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -long@^5.0.0: +long@^5.0.0, long@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== @@ -1866,6 +1996,11 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -1873,6 +2008,13 @@ mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: dependencies: mime-db "1.52.0" +mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -1888,7 +2030,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.3, ms@^2.0.0, ms@^2.1.3: +ms@2.1.3, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -1903,18 +2045,27 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -node-domexception@1.0.0: +node-domexception@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: +node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + nunjucks@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.4.tgz#f0878eef528ce7b0aa35d67cc6898635fd74649e" @@ -1948,25 +2099,12 @@ once@^1.4.0: dependencies: wrappy "1" -openai@^4.77.0: - version "4.104.0" - resolved "https://registry.yarnpkg.com/openai/-/openai-4.104.0.tgz#c489765dc051b95019845dab64b0e5207cae4d30" - integrity sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA== - dependencies: - "@types/node" "^18.11.18" - "@types/node-fetch" "^2.6.4" - abort-controller "^3.0.0" - agentkeepalive "^4.2.1" - form-data-encoder "1.7.2" - formdata-node "^4.3.2" - node-fetch "^2.6.7" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== -p-queue@^6.6.2: +p-queue@6.6.2, p-queue@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== @@ -1974,14 +2112,6 @@ p-queue@^6.6.2: eventemitter3 "^4.0.4" p-timeout "^3.2.0" -p-retry@4: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -1989,16 +2119,16 @@ p-timeout@^3.2.0: dependencies: p-finally "^1.0.0" +papaparse@^5.5.3: + version "5.7.0" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.7.0.tgz#791209b95dd3a0ca0b6ed9b64f853bc491a45a65" + integrity sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -2009,13 +2139,6 @@ path-to-regexp@0.1.12, path-to-regexp@~0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== -posthog-node@^5.5.1: - version "5.20.0" - resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-5.20.0.tgz#9244a270ccf6a64db26a64fab65499990356f307" - integrity sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg== - dependencies: - "@posthog/core" "1.9.1" - proto3-json-serializer@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd" @@ -2041,6 +2164,13 @@ protobufjs@^7.2.5, protobufjs@^7.3.0, protobufjs@^7.3.2, protobufjs@^7.5.3: "@types/node" ">=13.7.0" long "^5.0.0" +protobufjs@^8.0.1: + version "8.8.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-8.8.0.tgz#7ed071d94b6755f63cc9936ecd82e4deac7cf624" + integrity sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg== + dependencies: + long "^5.3.2" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -2139,11 +2269,6 @@ retry-request@^7.0.0: extend "^3.0.2" teeny-request "^9.0.0" -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2154,11 +2279,6 @@ safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^7.6.3: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== - send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -2222,18 +2342,6 @@ setprototypeof@1.2.0, setprototypeof@~1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" @@ -2274,11 +2382,6 @@ side-channel@^1.0.6, side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -simple-wcswidth@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b" - integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw== - statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -2334,13 +2437,6 @@ supports-color@^10.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-10.2.2.tgz#466c2978cc5cd0052d542a0b576461c2b802ebb4" integrity sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g== -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -2395,11 +2491,6 @@ typescript@^5.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" @@ -2445,10 +2536,10 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -web-streams-polyfill@4.0.0-beta.3: - version "4.0.0-beta.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" - integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== webidl-conversions@^3.0.0: version "3.0.1" @@ -2463,13 +2554,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -2507,15 +2591,15 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" -zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.25.0: +zod-to-json-schema@^3.25.0: version "3.25.1" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba" integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA== -zod@^3.25.32: - version "3.25.76" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" - integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== +"zod@^3.25.76 || ^4": + version "4.5.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.5.4.tgz#e215c62420c528dd7951e31fb52c5438f1fd184a" + integrity sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA== zod@^4.0.17: version "4.3.5" diff --git a/showcase/app/api/agent/route.ts b/showcase/app/api/agent/route.ts index f762a2e9..3184fa17 100644 --- a/showcase/app/api/agent/route.ts +++ b/showcase/app/api/agent/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; // plain-JS sandbox logic (unit-tested via `node lib/demo-agent.mjs`) import { respond } from "@/lib/demo-agent.mjs"; +// real x402/MPP buyer against a deployed agent, enabled per-slug when env is present +import { isLiveSlug, liveRespond } from "@/lib/live-agent.mjs"; const COOKIE = "nvm_demo"; @@ -17,7 +19,7 @@ type Sub = { authorized: boolean; balance: number }; // and unsigned (httpOnly is not integrity), so it must never gate real spend — a raw // `Cookie: nvm_demo={"x":{"authorized":true,"balance":1e9}}` would otherwise pass. export async function POST(req: NextRequest) { - let payload: { slug?: string; action?: string; message?: string }; + let payload: { slug?: string; action?: string; message?: string; apiKey?: string }; try { payload = await req.json(); } catch { @@ -39,11 +41,20 @@ export async function POST(req: NextRequest) { all = {}; } - const result = respond(all[slug], { + const agentReq = { slug, action: (payload.action ?? "ask") as "intro" | "ask" | "authorize" | "reset", message: payload.message, - }); + // The viewer's Nevermined key (from localStorage via the Connect flow); used server-side to + // mint tokens on their behalf. Only forwarded to the live buyer, never to the simulator. + apiKey: payload.apiKey, + }; + // Live slugs hit the real deployed agent (server-side buyer); everything else uses the + // in-process simulator. isLiveSlug is false unless WEATHER_AGENT_URL + a plan id are configured, + // so the site stays a pure simulator when unconfigured. + const result = isLiveSlug(slug) + ? await liveRespond(all[slug], agentReq) + : respond(all[slug], agentReq); const res = NextResponse.json(result.body, { status: result.status }); res.cookies.set(COOKIE, JSON.stringify({ ...all, [slug]: result.state }), { diff --git a/showcase/app/globals.css b/showcase/app/globals.css index c329739f..e1ce1190 100644 --- a/showcase/app/globals.css +++ b/showcase/app/globals.css @@ -254,6 +254,13 @@ button { font-family: inherit; } width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center; } .block .lead { font-family: var(--disp); font-size: 24px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.22; margin-bottom: 14px; color: var(--ink); max-width: 22ch; } +/* numbered sub-sections inside a block (e.g. 3.1 Client, 3.2 Agent) */ +.subblock { margin-top: 24px; } +.subblock .h3 { font-family: var(--disp); font-weight: 800; font-size: 12px; color: var(--ink-soft); + letter-spacing: 0.02em; margin: 0 0 12px; display: flex; align-items: center; gap: 9px; } +.subblock .h3 .num { font-family: var(--mono); font-weight: 500; color: var(--ink); background: var(--bg-tint); + border: 1px solid var(--border); font-size: 10.5px; padding: 3px 7px; border-radius: 5px; } +.subblock .sublead { color: var(--ink-soft); font-size: 14px; margin: 0 0 12px; } .block p { color: var(--ink-soft); font-size: 15.5px; } .block p + p { margin-top: 12px; } ul.learn { list-style: none; padding: 0; margin: 14px 0 0; display: flex; flex-direction: column; gap: 11px; } @@ -349,8 +356,44 @@ table.dt tr.total td:last-child { color: var(--paid); } .settle.err { border-color: var(--danger); background: var(--danger-wash); } .settle.err .stamp { background: var(--danger); } .settle.err .txt { color: var(--danger-ink); } +/* compact 402 row — same footprint as .settle (the PAID line), not the big paycard */ +.payline { align-self: stretch; display: flex; align-items: center; gap: 11px; border: 1.5px solid var(--pay); + background: var(--pay-wash); border-radius: 12px; padding: 12px 14px; } +.payline .stamp { font-family: var(--mono); font-weight: 500; font-size: 12px; color: #fff; background: var(--pay); + padding: 4px 9px; border-radius: 6px; flex: none; } +.payline .txt { font-family: var(--mono); font-size: 12.5px; color: var(--ink-soft); } +.cta.sm { margin-left: auto; padding: 6px 12px; font-size: 12.5px; border-radius: 9px; flex: none; } .notice { align-self: stretch; font-size: 12.5px; color: var(--muted); border: 1px solid var(--border); background: var(--bg); border-radius: 10px; padding: 10px 13px; } +/* spinner for in-flight requests */ +.spinner { display: inline-block; width: 13px; height: 13px; border: 2px solid var(--border-strong); + border-top-color: var(--accent-ink); border-radius: 50%; animation: spin 0.7s linear infinite; } +.cta .spinner, .rp-input button .spinner { border-color: rgba(0,0,0,0.25); border-top-color: currentColor; } +.working { align-self: flex-start; display: flex; align-items: center; gap: 9px; color: var(--muted); font-size: 13px; + padding: 4px 2px; } +@keyframes spin { to { transform: rotate(360deg); } } + +/* "Connect" button — signal variant, from the Nevermined Button Studio design + (artifact f119d119). Ribbon logo + pulsing signal rings on hover. Vars are --nc-* + prefixed so they don't collide with the showcase's own --accent/--lime. */ +.nvm-connect { --nc-brand: #0d3f48; --nc-brand-ink: #ffffff; --nc-accent: #4ac6bf; + position: relative; isolation: isolate; z-index: 0; display: inline-flex; align-items: center; gap: 11px; + padding: 13px 22px; border: 0; border-radius: 12px; background: var(--nc-brand); color: var(--nc-brand-ink); + font-weight: 600; font-size: 15px; line-height: 1; cursor: pointer; } +.nvm-connect:disabled { opacity: 0.7; cursor: default; } +.nvm-connect:focus-visible { outline: 2px solid var(--nc-accent); outline-offset: 3px; } +.nvm-connect .ico { position: relative; display: grid; place-items: center; color: var(--nc-accent); } +.nvm-connect .ribbon { height: 17px; width: auto; display: block; } +.nvm-connect .ribbon path { fill: currentColor; } +.nvm-connect .lbl { position: relative; z-index: 1; } +.nvm-connect .ico::before, .nvm-connect .ico::after { content: ""; position: absolute; top: 50%; left: 50%; + width: 32px; height: 32px; margin: -16px 0 0 -16px; border-radius: 50%; border: 2px solid var(--nc-accent); + opacity: 0; z-index: -1; } +.nvm-connect:hover .ico::before, .nvm-connect:focus-visible .ico::before { animation: sig 1.4s ease-out infinite; } +.nvm-connect:hover .ico::after, .nvm-connect:focus-visible .ico::after { animation: sig 1.4s ease-out 0.7s infinite; } +.nvm-connect:hover .ico, .nvm-connect:focus-visible .ico { + filter: drop-shadow(0 0 6px color-mix(in srgb, var(--nc-accent) 75%, transparent)); } +@keyframes sig { from { transform: scale(0.45); opacity: 0.95; } to { transform: scale(2.6); opacity: 0; } } .rp-suggest { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 18px 14px; background: var(--bg-tint); } .rp-suggest .schip { cursor: pointer; background: var(--bg); } @@ -364,6 +407,9 @@ table.dt tr.total td:last-child { color: var(--paid); } font-weight: 700; font-size: 13.5px; cursor: pointer; } .rp-input button:disabled { opacity: 0.5; cursor: default; } .runnote { font-size: 12.5px; color: var(--faint); margin-top: 14px; } +.codelinks { font-size: 12px; color: var(--faint); margin: 12px 0 0; } +.codelinks a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; } +.runnote a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; } .linkbtn { background: none; border: none; padding: 0; font: inherit; color: var(--accent-ink); cursor: pointer; text-decoration: underline; } .linkbtn:hover { color: var(--accent); } diff --git a/showcase/app/t/[slug]/page.tsx b/showcase/app/t/[slug]/page.tsx index ed579461..3519adfb 100644 --- a/showcase/app/t/[slug]/page.tsx +++ b/showcase/app/t/[slug]/page.tsx @@ -132,11 +132,27 @@ export default async function TutorialPage({ params }: { params: Promise<{ slug: ))} - {t.tech.samples.map((s, i) => ( -
- -
- ))} + {t.tech.groups?.length ? ( + t.tech.groups.map((g, gi) => ( +
+
+ 3.{gi + 1} {g.title} +
+ {g.lead ?

{g.lead}

: null} + {g.samples.map((s, i) => ( +
+ +
+ ))} +
+ )) + ) : ( + t.tech.samples.map((s, i) => ( +
+ +
+ )) + )} {t.tech.files?.length ? ( diff --git a/showcase/components/AppShell.tsx b/showcase/components/AppShell.tsx index 5257aafa..daf4c2ef 100644 --- a/showcase/components/AppShell.tsx +++ b/showcase/components/AppShell.tsx @@ -12,6 +12,7 @@ import { External, Spark, Bolt, + Signal, Plug, Layers, Book, @@ -44,6 +45,7 @@ export interface NavGroup { const GLYPH: Record = { catalog: , x402: , + mpp: , mcp: , langchain: , }; diff --git a/showcase/components/CodeBlock.tsx b/showcase/components/CodeBlock.tsx index dc58edfb..aeccf080 100644 --- a/showcase/components/CodeBlock.tsx +++ b/showcase/components/CodeBlock.tsx @@ -58,7 +58,10 @@ export default function CodeBlock({ {copied ? : } {copied ? "Copied" : "Copy"} -
+        {/* suppressHydrationWarning: Prism (and some browser extensions) mutate the
+            highlighted 
/ after SSR — adding a language class / tabindex — which
+            is a benign attribute-only diff React would otherwise warn about. */}
+        
           {html ? (
             // Safe: `code` is our own static content (content/tutorials.ts), and
             // Prism.highlight HTML-escapes its input before emitting token spans —
@@ -66,6 +69,7 @@ export default function CodeBlock({
             
           ) : (
             {code}
diff --git a/showcase/components/LiveRunPanel.tsx b/showcase/components/LiveRunPanel.tsx
index e1c4400f..b66aff6c 100644
--- a/showcase/components/LiveRunPanel.tsx
+++ b/showcase/components/LiveRunPanel.tsx
@@ -6,15 +6,44 @@ import { ArrowRight } from "./icons";
 
 type Item =
   | { type: "msg"; role: "user" | "agent"; text: string; tag?: "free" | "paid" }
-  | { type: "pay"; credits: number; pending: string; resolved?: boolean }
+  | { type: "pay"; credits: number; pending: string; resolved?: boolean; payg?: boolean }
   | { type: "settle"; text: string; error?: boolean }
   | { type: "notice"; text: string };
 
+interface CodeLink {
+  label: string;
+  url: string;
+}
+
 interface Intro {
   greeting: string;
   suggestions: string[];
   authorized: boolean;
-  balance: number;
+  balance?: number;
+  live?: boolean;
+  payg?: boolean;
+  code?: CodeLink[];
+}
+
+// The Nevermined App host for the "Connect" (CLI-auth) flow. Sandbox vs live is chosen inside the
+// App; this is just the frontend host. Override per-deployment via NEXT_PUBLIC_NVM_APP_URL.
+const APP_URL = process.env.NEXT_PUBLIC_NVM_APP_URL || "https://nevermined.app";
+const KEY_STORAGE = "nvm_api_key";
+
+function readStoredKey(): string | null {
+  try {
+    return localStorage.getItem(KEY_STORAGE);
+  } catch {
+    return null;
+  }
+}
+
+// Current page URL with any nvm_api_key stripped from BOTH query and fragment — the callback target.
+function cleanCallbackUrl(): string {
+  const u = new URL(window.location.href);
+  u.searchParams.delete(KEY_STORAGE);
+  u.hash = "";
+  return u.toString();
 }
 
 export default function LiveRunPanel({
@@ -33,13 +62,41 @@ export default function LiveRunPanel({
   const [flash, setFlash] = useState(false);
   const [input, setInput] = useState("");
   const [busy, setBusy] = useState(false);
+  const [live, setLive] = useState(false);
+  const [apiKey, setApiKey] = useState(null);
+  const [connecting, setConnecting] = useState(false);
+  const [codeLinks, setCodeLinks] = useState([]);
+  const [payg, setPayg] = useState(false);
   const logRef = useRef(null);
 
+  // On mount: capture an nvm_api_key returned by the Nevermined App callback, persist it, and clean
+  // the URL — otherwise fall back to a previously stored key.
+  //
+  // SECURITY NOTE: the App's /auth/cli flow currently returns the key as a QUERY param
+  // (?nvm_api_key=…), which lands in the showcase server's (and any proxy/CDN's) access logs on the
+  // redirect's request line — a live key in plaintext logs. The real fix is App-side: return it in
+  // the URL FRAGMENT (#nvm_api_key=…), which browsers never send to the server. We read both here so
+  // that change needs no update on this side; until then, see deploy/argocd README for the log-scrub note.
+  useEffect(() => {
+    try {
+      const params = new URLSearchParams(window.location.search);
+      const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, ""));
+      const returned = params.get(KEY_STORAGE) || hashParams.get(KEY_STORAGE);
+      if (returned) {
+        localStorage.setItem(KEY_STORAGE, returned);
+        window.history.replaceState({}, "", cleanCallbackUrl());
+      }
+    } catch {
+      /* private mode / no storage — connect flow just won't persist */
+    }
+    setApiKey(readStoredKey());
+  }, []);
+
   async function call(action: string, message?: string) {
     const res = await fetch("/api/agent", {
       method: "POST",
       headers: { "content-type": "application/json" },
-      body: JSON.stringify({ slug, action, message }),
+      body: JSON.stringify({ slug, action, message, apiKey: readStoredKey() ?? undefined }),
     });
     return { status: res.status, body: await res.json() };
   }
@@ -51,17 +108,20 @@ export default function LiveRunPanel({
   }
 
   useEffect(() => {
-    let live = true;
+    let alive = true;
     call("intro").then(({ body }) => {
-      if (!live) return;
+      if (!alive) return;
       const intro = body as Intro;
       setItems([{ type: "msg", role: "agent", text: intro.greeting }]);
       setSuggestions(intro.suggestions ?? []);
       setAuthorized(intro.authorized);
-      setBalance(intro.balance);
+      setBalance(typeof intro.balance === "number" ? intro.balance : null);
+      setLive(!!intro.live);
+      setCodeLinks(intro.code ?? []);
+      setPayg(!!intro.payg);
     });
     return () => {
-      live = false;
+      alive = false;
     };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [slug]);
@@ -70,6 +130,25 @@ export default function LiveRunPanel({
     logRef.current?.scrollTo({ top: logRef.current.scrollHeight });
   }, [items]);
 
+  function connect() {
+    setConnecting(true);
+    const url = `${APP_URL}/auth/cli?callback_url=${encodeURIComponent(
+      cleanCallbackUrl(),
+    )}&key_name=${encodeURIComponent("Nevermined Tutorials")}`;
+    window.location.href = url;
+  }
+
+  function disconnect() {
+    try {
+      localStorage.removeItem(KEY_STORAGE);
+    } catch {
+      /* ignore */
+    }
+    setApiKey(null);
+    setAuthorized(false);
+    setItems((x) => [...x, { type: "notice", text: "Disconnected — your Nevermined key was removed from this browser." }]);
+  }
+
   async function ask(text: string) {
     const message = text.trim();
     if (!message || busy) return;
@@ -77,8 +156,13 @@ export default function LiveRunPanel({
     setItems((x) => [...x, { type: "msg", role: "user", text: message }]);
     try {
       const { status, body } = await call("ask", message);
-      if (status === 402 && body.kind === "payment_required") {
-        setItems((x) => [...x, { type: "pay", credits: body.credits, pending: message }]);
+      if (status === 401 && body.kind === "not_connected") {
+        setItems((x) => [...x, { type: "notice", text: "Connect with Nevermined first to make paid requests." }]);
+      } else if (status === 402 && body.kind === "payment_required") {
+        setItems((x) => [
+          ...x,
+          { type: "pay", credits: body.credits, pending: message, payg: !!body.payg },
+        ]);
       } else if (status === 402 && body.kind === "insufficient") {
         setBalance(body.balance);
         setItems((x) => [
@@ -88,14 +172,22 @@ export default function LiveRunPanel({
       } else if (body.kind === "free") {
         setItems((x) => [...x, { type: "msg", role: "agent", text: body.answer, tag: "free" }]);
       } else if (body.kind === "paid") {
-        tickBalance(body.balance);
+        if (typeof body.balance === "number") tickBalance(body.balance);
         setItems((x) => [
           ...x,
-          { type: "settle", text: `200 OK · settled ${body.credits} credit(s) · balance ${body.balance}` },
+          {
+            type: "settle",
+            text: body.payg
+              ? "200 OK · paid · pay-as-you-go"
+              : `200 OK · settled ${body.credits} credit(s) · balance ${body.balance}`,
+          },
           { type: "msg", role: "agent", text: body.answer, tag: "paid" },
         ]);
       } else {
-        setItems((x) => [...x, { type: "settle", text: "the agent returned an error", error: true }]);
+        setItems((x) => [
+          ...x,
+          { type: "settle", text: body.error || "the agent returned an error", error: true },
+        ]);
       }
     } catch {
       setItems((x) => [
@@ -124,16 +216,27 @@ export default function LiveRunPanel({
   async function reset() {
     await call("reset");
     setAuthorized(false);
-    setBalance(100);
-    setItems((x) => [...x, { type: "notice", text: "Card delegation reset · balance back to 100." }]);
+    setBalance(live ? balance : 100);
+    setItems((x) => [...x, { type: "notice", text: "Delegation reset." }]);
   }
 
+  // Live tutorial, not yet connected → show the Connect gate instead of the input.
+  const needsConnect = live && !apiKey;
+
   return (
     <>
       
- {suggestions.length > 0 && items.length <= 1 ? ( -
- {suggestions.map((s) => ( - - ))} + {needsConnect ? ( +
+

+ This tutorial makes real x402 / MPP requests to a live sandbox agent. Connect your + Nevermined account to get a sandbox API key — you'll be sent to the Nevermined App to log + in and returned here automatically. +

+ + {codeLinks.length > 0 ? ( +

+ Server code:{" "} + {codeLinks.map((c, i) => ( + + {i > 0 ? " · " : ""} + + {c.label} + + + ))} +

+ ) : null}
- ) : null} + ) : ( + <> + {suggestions.length > 0 && items.length <= 1 ? ( +
+ {suggestions.map((s) => ( + + ))} +
+ ) : null} -
{ - e.preventDefault(); - const t = input; - setInput(""); - ask(t); - }} - > - setInput(e.target.value)} - placeholder={busy ? "…" : "Send a request to the agent"} - disabled={busy} - aria-label="Message the agent" - /> - - +
{ + e.preventDefault(); + const t = input; + setInput(""); + ask(t); + }} + > + setInput(e.target.value)} + placeholder={busy ? "…" : "Send a request to the agent"} + disabled={busy} + aria-label="Message the agent" + /> + + + + )}

- {authorized ? ( + {live ? ( + apiKey ? ( + payg ? ( + <>Live agent — pay-as-you-go: each request is paid on its own against Nevermined sandbox, no credit balance. + ) : ( + <>Live agent — real x402/MPP payments on Nevermined sandbox, paid with your connected key. + ) + ) : ( + <>Live agent — connect once to pay real x402/MPP requests on Nevermined sandbox. + ) + ) : authorized ? ( <> Sandbox agent — real payment round-trips and a real per-session credit balance, no real money.{" "}