@agentcash/telemetry 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/dist/builder.d.mts +58 -0
- package/dist/builder.d.ts +58 -0
- package/dist/builder.js +218 -0
- package/dist/builder.js.map +1 -0
- package/dist/builder.mjs +218 -0
- package/dist/builder.mjs.map +1 -0
- package/dist/chunk-FJ3YM6KO.js +55 -0
- package/dist/chunk-FJ3YM6KO.js.map +1 -0
- package/dist/chunk-GE7VBMQP.js +149 -0
- package/dist/chunk-GE7VBMQP.js.map +1 -0
- package/dist/chunk-JVLKV7CX.mjs +55 -0
- package/dist/chunk-JVLKV7CX.mjs.map +1 -0
- package/dist/chunk-O2PCP6KV.mjs +60 -0
- package/dist/chunk-O2PCP6KV.mjs.map +1 -0
- package/dist/chunk-P63MLKU3.js +60 -0
- package/dist/chunk-P63MLKU3.js.map +1 -0
- package/dist/chunk-VOY67KA4.mjs +149 -0
- package/dist/chunk-VOY67KA4.mjs.map +1 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +14 -0
- package/dist/index.mjs.map +1 -0
- package/dist/router-plugin.d.mts +109 -0
- package/dist/router-plugin.d.ts +109 -0
- package/dist/router-plugin.js +99 -0
- package/dist/router-plugin.js.map +1 -0
- package/dist/router-plugin.mjs +99 -0
- package/dist/router-plugin.mjs.map +1 -0
- package/dist/siwx.d.mts +29 -0
- package/dist/siwx.d.ts +29 -0
- package/dist/siwx.js +99 -0
- package/dist/siwx.js.map +1 -0
- package/dist/siwx.mjs +99 -0
- package/dist/siwx.mjs.map +1 -0
- package/dist/types-Bl8IwXin.d.mts +57 -0
- package/dist/types-Bl8IwXin.d.ts +57 -0
- package/package.json +88 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @merit-systems/x402-server-telemetry
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@merit-systems/x402-server-telemetry)
|
|
4
|
+
|
|
5
|
+
Shared telemetry for Merit Systems x402 servers. Extracts identity headers, logs invocations to ClickHouse, and auto-extracts verified wallets from x402 payments and SIWX auth.
|
|
6
|
+
|
|
7
|
+
[Telemetry spec](docs/telemetry-spec.md) | [npm](https://www.npmjs.com/package/@merit-systems/x402-server-telemetry) | [GitHub](https://github.com/Merit-Systems/x402-server-telemetry)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @merit-systems/x402-server-telemetry @clickhouse/client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// lib/telemetry.ts (or wherever your route wrappers live)
|
|
19
|
+
import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
|
|
20
|
+
|
|
21
|
+
initTelemetry({
|
|
22
|
+
clickhouse: {
|
|
23
|
+
url: process.env.TELEM_CLICKHOUSE_URL!,
|
|
24
|
+
database: process.env.TELEM_CLICKHOUSE_DATABASE,
|
|
25
|
+
username: process.env.TELEM_CLICKHOUSE_USERNAME,
|
|
26
|
+
password: process.env.TELEM_CLICKHOUSE_PASSWORD,
|
|
27
|
+
},
|
|
28
|
+
verify: true, // optional — pings ClickHouse on startup, logs success/failure
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { withTelemetry };
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// app/api/example/route.ts
|
|
36
|
+
import { withTelemetry } from '@/lib/telemetry';
|
|
37
|
+
|
|
38
|
+
export const POST = withTelemetry(async (request, ctx) => {
|
|
39
|
+
return NextResponse.json(await doWork(request));
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Three entrypoints
|
|
44
|
+
|
|
45
|
+
### Core (`@merit-systems/x402-server-telemetry`)
|
|
46
|
+
|
|
47
|
+
Requires: `@clickhouse/client`, `next`
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `initTelemetry(config)` — synchronous, call once at module level. Pass `verify: true` to ping ClickHouse on startup (fire-and-forget, never blocks)
|
|
54
|
+
- `withTelemetry(handler)` — wrap any Next.js route handler
|
|
55
|
+
- `extractVerifiedWallet(headers)` — extract wallet from x402 payment headers
|
|
56
|
+
|
|
57
|
+
### SIWX (`@merit-systems/x402-server-telemetry/siwx`)
|
|
58
|
+
|
|
59
|
+
Requires: `@x402/extensions`, `@x402/core`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { withSiwxTelemetry } from '@merit-systems/x402-server-telemetry/siwx';
|
|
63
|
+
|
|
64
|
+
export const GET = withSiwxTelemetry(async (request, ctx) => {
|
|
65
|
+
// ctx.verifiedWallet is guaranteed to be set
|
|
66
|
+
return NextResponse.json(await getJobs(ctx.verifiedWallet));
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Route Builder (`@merit-systems/x402-server-telemetry/builder`)
|
|
71
|
+
|
|
72
|
+
Requires: `@x402/next`, `zod` (^4), `@x402/extensions`
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { createRouteBuilder } from '@merit-systems/x402-server-telemetry/builder';
|
|
76
|
+
|
|
77
|
+
const route = createRouteBuilder({ x402Server });
|
|
78
|
+
|
|
79
|
+
export const POST = route
|
|
80
|
+
.price('0.05', 'base:8453')
|
|
81
|
+
.body(searchSchema)
|
|
82
|
+
.handler(async ({ body }) => searchPeople(body.query));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Next.js integration footguns
|
|
86
|
+
|
|
87
|
+
### `@clickhouse/client` must be externalized
|
|
88
|
+
|
|
89
|
+
The ClickHouse client uses Node.js native APIs that break when bundled by Next.js. Add to your `next.config`:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const nextConfig: NextConfig = {
|
|
93
|
+
serverExternalPackages: ['@clickhouse/client'],
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Do NOT call `initTelemetry` in `instrumentation.ts`
|
|
98
|
+
|
|
99
|
+
On Vercel serverless, `instrumentation.ts` runs in a **separate module scope** from route handlers. Singletons set there are invisible to your handlers.
|
|
100
|
+
|
|
101
|
+
Call `initTelemetry()` in the same module that imports your route wrappers:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// lib/telemetry.ts — CORRECT
|
|
105
|
+
import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
|
|
106
|
+
initTelemetry({ clickhouse: { ... } });
|
|
107
|
+
export { withTelemetry };
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// instrumentation.ts — WRONG: singleton won't be shared with handlers
|
|
112
|
+
import { initTelemetry } from '@merit-systems/x402-server-telemetry';
|
|
113
|
+
export async function register() {
|
|
114
|
+
initTelemetry({ clickhouse: { ... } }); // handlers can't see this
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Subpath exports isolate heavy deps
|
|
119
|
+
|
|
120
|
+
The `/siwx` and `/builder` entrypoints have additional peer dependencies. If you only use the core `withTelemetry`, you don't need `zod`, `@x402/next`, or `@x402/extensions` installed.
|
|
121
|
+
|
|
122
|
+
### After updating, commit both `package.json` and lockfile
|
|
123
|
+
|
|
124
|
+
`pnpm update @merit-systems/x402-server-telemetry` bumps the version specifier in **both** `package.json` and `pnpm-lock.yaml`. Vercel's `frozen-lockfile` mode will reject deploys if only the lockfile is committed. Always:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git add package.json pnpm-lock.yaml
|
|
128
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ZodType } from 'zod';
|
|
3
|
+
import { a as TelemetryContext } from './types-Bl8IwXin.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convenience route builder that composes telemetry + validation + x402 wrapping.
|
|
7
|
+
* This is optional — servers can use withTelemetry directly.
|
|
8
|
+
*
|
|
9
|
+
* Import from '@agentcash/telemetry/builder'.
|
|
10
|
+
* Requires peer deps: @x402/next, zod (^4), @x402/extensions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare class HttpError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
constructor(message: string, status: number);
|
|
16
|
+
}
|
|
17
|
+
type AcceptsOption = {
|
|
18
|
+
amount: string;
|
|
19
|
+
network: string;
|
|
20
|
+
asset?: string;
|
|
21
|
+
};
|
|
22
|
+
type BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {
|
|
23
|
+
accepts: AcceptsOption[];
|
|
24
|
+
bodySchema?: ZodType<TBody>;
|
|
25
|
+
querySchema?: ZodType<TQuery>;
|
|
26
|
+
outputSchema?: ZodType<TOutput>;
|
|
27
|
+
outputExample?: TOutput;
|
|
28
|
+
description?: string;
|
|
29
|
+
};
|
|
30
|
+
type HandlerContext<TBody, TQuery> = {
|
|
31
|
+
body: TBody;
|
|
32
|
+
query: TQuery;
|
|
33
|
+
request: NextRequest;
|
|
34
|
+
telemetry: TelemetryContext;
|
|
35
|
+
};
|
|
36
|
+
type HandlerFn<TBody, TQuery, TResponse> = (ctx: HandlerContext<TBody, TQuery>) => Promise<TResponse>;
|
|
37
|
+
interface RouteBuilderOptions {
|
|
38
|
+
/** The x402 resource server instance (from @x402/core/server). Required when using .price(). */
|
|
39
|
+
x402Server?: unknown;
|
|
40
|
+
}
|
|
41
|
+
declare class RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {
|
|
42
|
+
private config;
|
|
43
|
+
private options;
|
|
44
|
+
constructor(config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>, options?: RouteBuilderOptions);
|
|
45
|
+
price(amount: string, network: string, asset?: string): RouteBuilder<TBody, TQuery, TOutput>;
|
|
46
|
+
accepts(options: AcceptsOption[]): RouteBuilder<TBody, TQuery, TOutput>;
|
|
47
|
+
body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput>;
|
|
48
|
+
query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput>;
|
|
49
|
+
output<T>(schema: ZodType<T>, example?: T): RouteBuilder<TBody, TQuery, T>;
|
|
50
|
+
description(text: string): RouteBuilder<TBody, TQuery, TOutput>;
|
|
51
|
+
handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>): (request: NextRequest) => Promise<NextResponse>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a new route builder instance.
|
|
55
|
+
*/
|
|
56
|
+
declare function createRouteBuilder(options?: RouteBuilderOptions): RouteBuilder<unknown, unknown, unknown>;
|
|
57
|
+
|
|
58
|
+
export { HttpError, type RouteBuilderOptions, createRouteBuilder };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ZodType } from 'zod';
|
|
3
|
+
import { a as TelemetryContext } from './types-Bl8IwXin.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convenience route builder that composes telemetry + validation + x402 wrapping.
|
|
7
|
+
* This is optional — servers can use withTelemetry directly.
|
|
8
|
+
*
|
|
9
|
+
* Import from '@agentcash/telemetry/builder'.
|
|
10
|
+
* Requires peer deps: @x402/next, zod (^4), @x402/extensions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare class HttpError extends Error {
|
|
14
|
+
status: number;
|
|
15
|
+
constructor(message: string, status: number);
|
|
16
|
+
}
|
|
17
|
+
type AcceptsOption = {
|
|
18
|
+
amount: string;
|
|
19
|
+
network: string;
|
|
20
|
+
asset?: string;
|
|
21
|
+
};
|
|
22
|
+
type BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {
|
|
23
|
+
accepts: AcceptsOption[];
|
|
24
|
+
bodySchema?: ZodType<TBody>;
|
|
25
|
+
querySchema?: ZodType<TQuery>;
|
|
26
|
+
outputSchema?: ZodType<TOutput>;
|
|
27
|
+
outputExample?: TOutput;
|
|
28
|
+
description?: string;
|
|
29
|
+
};
|
|
30
|
+
type HandlerContext<TBody, TQuery> = {
|
|
31
|
+
body: TBody;
|
|
32
|
+
query: TQuery;
|
|
33
|
+
request: NextRequest;
|
|
34
|
+
telemetry: TelemetryContext;
|
|
35
|
+
};
|
|
36
|
+
type HandlerFn<TBody, TQuery, TResponse> = (ctx: HandlerContext<TBody, TQuery>) => Promise<TResponse>;
|
|
37
|
+
interface RouteBuilderOptions {
|
|
38
|
+
/** The x402 resource server instance (from @x402/core/server). Required when using .price(). */
|
|
39
|
+
x402Server?: unknown;
|
|
40
|
+
}
|
|
41
|
+
declare class RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {
|
|
42
|
+
private config;
|
|
43
|
+
private options;
|
|
44
|
+
constructor(config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>, options?: RouteBuilderOptions);
|
|
45
|
+
price(amount: string, network: string, asset?: string): RouteBuilder<TBody, TQuery, TOutput>;
|
|
46
|
+
accepts(options: AcceptsOption[]): RouteBuilder<TBody, TQuery, TOutput>;
|
|
47
|
+
body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput>;
|
|
48
|
+
query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput>;
|
|
49
|
+
output<T>(schema: ZodType<T>, example?: T): RouteBuilder<TBody, TQuery, T>;
|
|
50
|
+
description(text: string): RouteBuilder<TBody, TQuery, TOutput>;
|
|
51
|
+
handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>): (request: NextRequest) => Promise<NextResponse>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a new route builder instance.
|
|
55
|
+
*/
|
|
56
|
+
declare function createRouteBuilder(options?: RouteBuilderOptions): RouteBuilder<unknown, unknown, unknown>;
|
|
57
|
+
|
|
58
|
+
export { HttpError, type RouteBuilderOptions, createRouteBuilder };
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunkGE7VBMQPjs = require('./chunk-GE7VBMQP.js');
|
|
6
|
+
require('./chunk-P63MLKU3.js');
|
|
7
|
+
|
|
8
|
+
// src/route-builder.ts
|
|
9
|
+
var _server = require('next/server');
|
|
10
|
+
var _next = require('@x402/next');
|
|
11
|
+
var _zod = require('zod');
|
|
12
|
+
var _bazaar = require('@x402/extensions/bazaar');
|
|
13
|
+
var HttpError = class extends Error {
|
|
14
|
+
constructor(message, status) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = "HttpError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function formatValidationError(error) {
|
|
21
|
+
const issues = error.issues;
|
|
22
|
+
if (issues.length === 0) return "Validation failed";
|
|
23
|
+
if (issues.length === 1) {
|
|
24
|
+
const issue = issues[0];
|
|
25
|
+
const path = issue.path.join(".");
|
|
26
|
+
const field = path || "request";
|
|
27
|
+
if (issue.message) return `${field}: ${issue.message}`;
|
|
28
|
+
return `${field}: Invalid value`;
|
|
29
|
+
}
|
|
30
|
+
const errors = issues.map((issue) => {
|
|
31
|
+
const path = issue.path.join(".");
|
|
32
|
+
const field = path || "request";
|
|
33
|
+
return issue.message ? `${field}: ${issue.message}` : `${field}: Invalid`;
|
|
34
|
+
});
|
|
35
|
+
return `Validation failed: ${errors.join("; ")}`;
|
|
36
|
+
}
|
|
37
|
+
var RouteBuilder = class _RouteBuilder {
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
constructor(config, options) {
|
|
41
|
+
this.config = { accepts: [], ...config };
|
|
42
|
+
this.options = _nullishCoalesce(options, () => ( {}));
|
|
43
|
+
}
|
|
44
|
+
price(amount, network, asset) {
|
|
45
|
+
return new _RouteBuilder(
|
|
46
|
+
{ ...this.config, accepts: [{ amount, network, asset }] },
|
|
47
|
+
this.options
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
accepts(options) {
|
|
51
|
+
return new _RouteBuilder(
|
|
52
|
+
{ ...this.config, accepts: options },
|
|
53
|
+
this.options
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
body(schema) {
|
|
57
|
+
return new _RouteBuilder(
|
|
58
|
+
{ ...this.config, bodySchema: schema },
|
|
59
|
+
this.options
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
query(schema) {
|
|
63
|
+
return new _RouteBuilder(
|
|
64
|
+
{ ...this.config, querySchema: schema },
|
|
65
|
+
this.options
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
output(schema, example) {
|
|
69
|
+
return new _RouteBuilder(
|
|
70
|
+
{
|
|
71
|
+
...this.config,
|
|
72
|
+
outputSchema: schema,
|
|
73
|
+
outputExample: example
|
|
74
|
+
},
|
|
75
|
+
this.options
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
description(text) {
|
|
79
|
+
return new _RouteBuilder(
|
|
80
|
+
{ ...this.config, description: text },
|
|
81
|
+
this.options
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
handler(fn) {
|
|
85
|
+
const { accepts, bodySchema, querySchema, outputSchema, outputExample, description } = this.config;
|
|
86
|
+
const coreHandler = async (request) => {
|
|
87
|
+
const meta = _chunkGE7VBMQPjs.extractRequestMeta.call(void 0, request);
|
|
88
|
+
const ctx = _chunkGE7VBMQPjs.buildTelemetryContext.call(void 0, meta);
|
|
89
|
+
const log = (status, responseBody, resp) => {
|
|
90
|
+
_chunkGE7VBMQPjs.recordInvocation.call(void 0, meta, requestBodyString, {
|
|
91
|
+
status,
|
|
92
|
+
body: responseBody,
|
|
93
|
+
headers: JSON.stringify(Object.fromEntries(resp.headers.entries())),
|
|
94
|
+
contentType: _nullishCoalesce(resp.headers.get("content-type"), () => ( null))
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
let body = void 0;
|
|
98
|
+
let query = void 0;
|
|
99
|
+
let requestBodyString = null;
|
|
100
|
+
if (bodySchema) {
|
|
101
|
+
let rawBody;
|
|
102
|
+
try {
|
|
103
|
+
rawBody = await request.json();
|
|
104
|
+
requestBodyString = JSON.stringify(rawBody);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const errorResp = _server.NextResponse.json(
|
|
107
|
+
{ success: false, error: "Invalid JSON body" },
|
|
108
|
+
{ status: 400 }
|
|
109
|
+
);
|
|
110
|
+
log(400, JSON.stringify({ success: false, error: "Invalid JSON body" }), errorResp);
|
|
111
|
+
return errorResp;
|
|
112
|
+
}
|
|
113
|
+
const parsed = bodySchema.safeParse(rawBody);
|
|
114
|
+
if (!parsed.success) {
|
|
115
|
+
const message = formatValidationError(parsed.error);
|
|
116
|
+
const errorBody = {
|
|
117
|
+
success: false,
|
|
118
|
+
error: "Validation failed",
|
|
119
|
+
message,
|
|
120
|
+
details: parsed.error.flatten()
|
|
121
|
+
};
|
|
122
|
+
const errorResp = _server.NextResponse.json(errorBody, { status: 400 });
|
|
123
|
+
log(400, JSON.stringify(errorBody), errorResp);
|
|
124
|
+
return errorResp;
|
|
125
|
+
}
|
|
126
|
+
body = parsed.data;
|
|
127
|
+
}
|
|
128
|
+
if (querySchema) {
|
|
129
|
+
const searchParams = Object.fromEntries(request.nextUrl.searchParams);
|
|
130
|
+
const parsed = querySchema.safeParse(searchParams);
|
|
131
|
+
if (!parsed.success) {
|
|
132
|
+
const message = formatValidationError(parsed.error);
|
|
133
|
+
const errorBody = {
|
|
134
|
+
success: false,
|
|
135
|
+
error: "Query validation failed",
|
|
136
|
+
message,
|
|
137
|
+
details: parsed.error.flatten()
|
|
138
|
+
};
|
|
139
|
+
const errorResp = _server.NextResponse.json(errorBody, { status: 400 });
|
|
140
|
+
log(400, JSON.stringify(errorBody), errorResp);
|
|
141
|
+
return errorResp;
|
|
142
|
+
}
|
|
143
|
+
query = parsed.data;
|
|
144
|
+
}
|
|
145
|
+
let response;
|
|
146
|
+
try {
|
|
147
|
+
response = await fn({ body, query, request, telemetry: ctx });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
150
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
151
|
+
const errorBody = { success: false, error: message };
|
|
152
|
+
const errorResp = _server.NextResponse.json(errorBody, { status });
|
|
153
|
+
log(status, JSON.stringify(errorBody), errorResp);
|
|
154
|
+
return errorResp;
|
|
155
|
+
}
|
|
156
|
+
if (response && typeof response === "object" && "success" in response && !response.success) {
|
|
157
|
+
const errorResp = _server.NextResponse.json(response, { status: 500 });
|
|
158
|
+
log(500, JSON.stringify(response), errorResp);
|
|
159
|
+
return errorResp;
|
|
160
|
+
}
|
|
161
|
+
const successResp = _server.NextResponse.json(response);
|
|
162
|
+
let responseBodyString = null;
|
|
163
|
+
try {
|
|
164
|
+
responseBodyString = JSON.stringify(response);
|
|
165
|
+
} catch (e2) {
|
|
166
|
+
}
|
|
167
|
+
log(200, responseBodyString, successResp);
|
|
168
|
+
return successResp;
|
|
169
|
+
};
|
|
170
|
+
if (accepts.length === 0) {
|
|
171
|
+
return coreHandler;
|
|
172
|
+
}
|
|
173
|
+
const X402_BYPASS = process.env.X402_BYPASS === "true";
|
|
174
|
+
if (X402_BYPASS) {
|
|
175
|
+
return coreHandler;
|
|
176
|
+
}
|
|
177
|
+
const X402_PAYEE_ADDRESS = process.env.X402_PAYEE_ADDRESS;
|
|
178
|
+
if (!X402_PAYEE_ADDRESS) {
|
|
179
|
+
throw new Error("X402_PAYEE_ADDRESS environment variable is required when using .price()");
|
|
180
|
+
}
|
|
181
|
+
const routeConfig = {
|
|
182
|
+
description,
|
|
183
|
+
accepts: accepts.map(({ amount, network, asset }) => ({
|
|
184
|
+
scheme: "exact",
|
|
185
|
+
network,
|
|
186
|
+
price: amount,
|
|
187
|
+
payTo: X402_PAYEE_ADDRESS,
|
|
188
|
+
...asset && { extra: { asset } }
|
|
189
|
+
})),
|
|
190
|
+
extensions: buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample)
|
|
191
|
+
};
|
|
192
|
+
if (!this.options.x402Server) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"x402Server is required when using .price(). Pass it to createRouteBuilder({ x402Server })."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return _next.withX402.call(void 0, coreHandler, routeConfig, this.options.x402Server);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample) {
|
|
201
|
+
const inputJsonSchema = bodySchema ? _zod.z.toJSONSchema(bodySchema, { target: "draft-2020-12" }) : querySchema ? _zod.z.toJSONSchema(querySchema, { target: "draft-2020-12" }) : void 0;
|
|
202
|
+
const outputJsonSchema = outputSchema ? _zod.z.toJSONSchema(outputSchema, { target: "draft-2020-12" }) : void 0;
|
|
203
|
+
if (!inputJsonSchema) return void 0;
|
|
204
|
+
const config = {
|
|
205
|
+
bodyType: bodySchema ? "json" : void 0,
|
|
206
|
+
inputSchema: inputJsonSchema,
|
|
207
|
+
output: outputJsonSchema ? { schema: outputJsonSchema, example: _nullishCoalesce(outputExample, () => ( {})) } : void 0
|
|
208
|
+
};
|
|
209
|
+
return { ..._bazaar.declareDiscoveryExtension.call(void 0, config) };
|
|
210
|
+
}
|
|
211
|
+
function createRouteBuilder(options) {
|
|
212
|
+
return new RouteBuilder(void 0, options);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
exports.HttpError = HttpError; exports.createRouteBuilder = createRouteBuilder;
|
|
218
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/samragsdale/Documents/Code/merit-systems/agentcash-telemetry/dist/builder.js","../src/route-builder.ts"],"names":[],"mappings":"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","file":"/Users/samragsdale/Documents/Code/merit-systems/agentcash-telemetry/dist/builder.js","sourcesContent":[null,"/**\n * Convenience route builder that composes telemetry + validation + x402 wrapping.\n * This is optional — servers can use withTelemetry directly.\n *\n * Import from '@agentcash/telemetry/builder'.\n * Requires peer deps: @x402/next, zod (^4), @x402/extensions\n */\n\nimport { type NextRequest, NextResponse } from 'next/server';\nimport { withX402 } from '@x402/next';\nimport { z, type ZodType } from 'zod';\nimport { declareDiscoveryExtension } from '@x402/extensions/bazaar';\nimport type { TelemetryContext } from './types';\nimport { extractRequestMeta, buildTelemetryContext, recordInvocation } from './telemetry-core';\n\nexport class HttpError extends Error {\n constructor(\n message: string,\n public status: number,\n ) {\n super(message);\n this.name = 'HttpError';\n }\n}\n\ntype AcceptsOption = {\n amount: string;\n network: string;\n asset?: string;\n};\n\ntype BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {\n accepts: AcceptsOption[];\n bodySchema?: ZodType<TBody>;\n querySchema?: ZodType<TQuery>;\n outputSchema?: ZodType<TOutput>;\n outputExample?: TOutput;\n description?: string;\n};\n\ntype HandlerContext<TBody, TQuery> = {\n body: TBody;\n query: TQuery;\n request: NextRequest;\n telemetry: TelemetryContext;\n};\n\ntype HandlerFn<TBody, TQuery, TResponse> = (\n ctx: HandlerContext<TBody, TQuery>,\n) => Promise<TResponse>;\n\nfunction formatValidationError(error: import('zod').ZodError): string {\n const issues = error.issues;\n if (issues.length === 0) return 'Validation failed';\n\n if (issues.length === 1) {\n const issue = issues[0];\n const path = issue.path.join('.');\n const field = path || 'request';\n if (issue.message) return `${field}: ${issue.message}`;\n return `${field}: Invalid value`;\n }\n\n const errors = issues.map((issue) => {\n const path = issue.path.join('.');\n const field = path || 'request';\n return issue.message ? `${field}: ${issue.message}` : `${field}: Invalid`;\n });\n\n return `Validation failed: ${errors.join('; ')}`;\n}\n\nexport interface RouteBuilderOptions {\n /** The x402 resource server instance (from @x402/core/server). Required when using .price(). */\n x402Server?: unknown;\n}\n\nclass RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {\n private config: BuilderConfig<TBody, TQuery, TOutput>;\n private options: RouteBuilderOptions;\n\n constructor(\n config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>,\n options?: RouteBuilderOptions,\n ) {\n this.config = { accepts: [], ...config };\n this.options = options ?? {};\n }\n\n price(amount: string, network: string, asset?: string) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, accepts: [{ amount, network, asset }] },\n this.options,\n );\n }\n\n accepts(options: AcceptsOption[]) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, accepts: options },\n this.options,\n );\n }\n\n body<T>(schema: ZodType<T>) {\n return new RouteBuilder<T, TQuery, TOutput>(\n { ...this.config, bodySchema: schema as ZodType<unknown> } as BuilderConfig<\n T,\n TQuery,\n TOutput\n >,\n this.options,\n );\n }\n\n query<T>(schema: ZodType<T>) {\n return new RouteBuilder<TBody, T, TOutput>(\n { ...this.config, querySchema: schema as ZodType<unknown> } as BuilderConfig<\n TBody,\n T,\n TOutput\n >,\n this.options,\n );\n }\n\n output<T>(schema: ZodType<T>, example?: T) {\n return new RouteBuilder<TBody, TQuery, T>(\n {\n ...this.config,\n outputSchema: schema as ZodType<unknown>,\n outputExample: example,\n } as BuilderConfig<TBody, TQuery, T>,\n this.options,\n );\n }\n\n description(text: string) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, description: text },\n this.options,\n );\n }\n\n handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>) {\n const { accepts, bodySchema, querySchema, outputSchema, outputExample, description } =\n this.config;\n\n const coreHandler = async (request: NextRequest): Promise<NextResponse> => {\n const meta = extractRequestMeta(request);\n const ctx = buildTelemetryContext(meta);\n\n const log = (status: number, responseBody: string | null, resp: NextResponse) => {\n recordInvocation(meta, requestBodyString, {\n status,\n body: responseBody,\n headers: JSON.stringify(Object.fromEntries(resp.headers.entries())),\n contentType: resp.headers.get('content-type') ?? null,\n });\n };\n\n // Parse and validate body\n let body: TBody = undefined as TBody;\n let query: TQuery = undefined as TQuery;\n let requestBodyString: string | null = null;\n\n if (bodySchema) {\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n requestBodyString = JSON.stringify(rawBody);\n } catch {\n const errorResp = NextResponse.json(\n { success: false, error: 'Invalid JSON body' },\n { status: 400 },\n );\n log(400, JSON.stringify({ success: false, error: 'Invalid JSON body' }), errorResp);\n return errorResp;\n }\n\n const parsed = bodySchema.safeParse(rawBody);\n if (!parsed.success) {\n const message = formatValidationError(parsed.error);\n const errorBody = {\n success: false,\n error: 'Validation failed',\n message,\n details: parsed.error.flatten(),\n };\n const errorResp = NextResponse.json(errorBody, { status: 400 });\n log(400, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n body = parsed.data;\n }\n\n // Parse and validate query\n if (querySchema) {\n const searchParams = Object.fromEntries(request.nextUrl.searchParams);\n const parsed = querySchema.safeParse(searchParams);\n if (!parsed.success) {\n const message = formatValidationError(parsed.error);\n const errorBody = {\n success: false,\n error: 'Query validation failed',\n message,\n details: parsed.error.flatten(),\n };\n const errorResp = NextResponse.json(errorBody, { status: 400 });\n log(400, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n query = parsed.data;\n }\n\n // Execute user handler\n let response: TResponse;\n try {\n response = await fn({ body, query, request, telemetry: ctx });\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n const status = error instanceof HttpError ? error.status : 500;\n const errorBody = { success: false, error: message };\n const errorResp = NextResponse.json(errorBody, { status });\n log(status, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n\n // Detect { success: false } to prevent x402 settlement\n if (\n response &&\n typeof response === 'object' &&\n 'success' in response &&\n !(response as { success: boolean }).success\n ) {\n const errorResp = NextResponse.json(response, { status: 500 });\n log(500, JSON.stringify(response), errorResp);\n return errorResp;\n }\n\n // Success\n const successResp = NextResponse.json(response);\n let responseBodyString: string | null = null;\n try {\n responseBodyString = JSON.stringify(response);\n } catch {\n // Not serializable — that's fine\n }\n log(200, responseBodyString, successResp);\n return successResp;\n };\n\n // If no pricing, return the core handler directly (no x402 wrapping)\n if (accepts.length === 0) {\n return coreHandler;\n }\n\n // Wrap with x402\n const X402_BYPASS = process.env.X402_BYPASS === 'true';\n if (X402_BYPASS) {\n return coreHandler;\n }\n\n const X402_PAYEE_ADDRESS = process.env.X402_PAYEE_ADDRESS;\n if (!X402_PAYEE_ADDRESS) {\n throw new Error('X402_PAYEE_ADDRESS environment variable is required when using .price()');\n }\n\n const routeConfig = {\n description,\n accepts: accepts.map(({ amount, network, asset }) => ({\n scheme: 'exact' as const,\n network: network as `${string}:${string}`,\n price: amount,\n payTo: X402_PAYEE_ADDRESS,\n ...(asset && { extra: { asset } }),\n })),\n extensions: buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample),\n };\n\n if (!this.options.x402Server) {\n throw new Error(\n 'x402Server is required when using .price(). Pass it to createRouteBuilder({ x402Server }).',\n );\n }\n\n return withX402(coreHandler, routeConfig, this.options.x402Server as never);\n }\n}\n\nfunction buildDiscoveryExtensions(\n bodySchema?: ZodType<unknown>,\n querySchema?: ZodType<unknown>,\n outputSchema?: ZodType<unknown>,\n outputExample?: unknown,\n): Record<string, unknown> | undefined {\n const inputJsonSchema = bodySchema\n ? z.toJSONSchema(bodySchema, { target: 'draft-2020-12' })\n : querySchema\n ? z.toJSONSchema(querySchema, { target: 'draft-2020-12' })\n : undefined;\n\n const outputJsonSchema = outputSchema\n ? z.toJSONSchema(outputSchema, { target: 'draft-2020-12' })\n : undefined;\n\n if (!inputJsonSchema) return undefined;\n\n const config = {\n bodyType: bodySchema ? 'json' : undefined,\n inputSchema: inputJsonSchema,\n output: outputJsonSchema\n ? { schema: outputJsonSchema, example: outputExample ?? {} }\n : undefined,\n };\n\n return { ...declareDiscoveryExtension(config as never) };\n}\n\n/**\n * Create a new route builder instance.\n */\nexport function createRouteBuilder(options?: RouteBuilderOptions) {\n return new RouteBuilder(undefined, options);\n}\n"]}
|