@metr-sdk/express 0.1.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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @metr/express - One line of middleware to monetize any Express.js API
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { metr } from '@metr/express';
7
+ *
8
+ * app.post('/api/summarize', metr({ price: 0.001 }), (req, res) => {
9
+ * res.json({ summary: 'Your text summary...' });
10
+ * });
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ import { Request, Response, NextFunction } from 'express';
16
+ export interface MetrConfig {
17
+ /** Price per unit in USD (e.g., 0.001 = $0.001 per request) */
18
+ price: number;
19
+ /** Unit type for metering. Default: "request" */
20
+ unitType?: 'request' | 'token' | 'byte' | 'second';
21
+ /** Your metr API key. Falls back to METR_API_KEY env var */
22
+ apiKey?: string;
23
+ /** Your registered endpoint ID. Falls back to METR_ENDPOINT_ID env var */
24
+ endpointId?: string;
25
+ /** metr gateway URL. Falls back to METR_GATEWAY_URL env var */
26
+ gatewayUrl?: string;
27
+ }
28
+ export interface MetrPaymentInfo {
29
+ sessionToken: string;
30
+ buyerId: string;
31
+ balanceUsd: number;
32
+ }
33
+ declare global {
34
+ namespace Express {
35
+ interface Request {
36
+ metr?: MetrPaymentInfo;
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * metr middleware - add to any Express route to require payment.
42
+ *
43
+ * Checks for a valid billing session token in the `Authorization` header.
44
+ * If missing or invalid, returns 402 Payment Required with a checkout URL.
45
+ * If valid, meters the usage and passes control to your handler.
46
+ */
47
+ export declare function metr(config: MetrConfig): (req: Request, res: Response, next: NextFunction) => Promise<void>;
48
+ export default metr;
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * @metr/express - One line of middleware to monetize any Express.js API
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * import { metr } from '@metr/express';
8
+ *
9
+ * app.post('/api/summarize', metr({ price: 0.001 }), (req, res) => {
10
+ * res.json({ summary: 'Your text summary...' });
11
+ * });
12
+ * ```
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.metr = metr;
18
+ const DEFAULT_GATEWAY_URL = 'https://api.metr.dev';
19
+ /**
20
+ * metr middleware - add to any Express route to require payment.
21
+ *
22
+ * Checks for a valid billing session token in the `Authorization` header.
23
+ * If missing or invalid, returns 402 Payment Required with a checkout URL.
24
+ * If valid, meters the usage and passes control to your handler.
25
+ */
26
+ function metr(config) {
27
+ const apiKey = config.apiKey || process.env.METR_API_KEY;
28
+ const endpointId = config.endpointId || process.env.METR_ENDPOINT_ID;
29
+ const gatewayUrl = config.gatewayUrl || process.env.METR_GATEWAY_URL || DEFAULT_GATEWAY_URL;
30
+ if (!apiKey) {
31
+ throw new Error('metr: API key required. Set config.apiKey or METR_API_KEY env var.');
32
+ }
33
+ return async (req, res, next) => {
34
+ // Extract session token from Authorization header
35
+ const authHeader = req.headers.authorization;
36
+ const sessionToken = authHeader?.startsWith('Bearer metr_sess_')
37
+ ? authHeader.slice(7)
38
+ : null;
39
+ if (!sessionToken) {
40
+ // No valid session — return 402 with checkout URL
41
+ try {
42
+ const checkoutRes = await fetch(`${gatewayUrl}/api/v1/checkout/create`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'X-Metr-Api-Key': apiKey,
47
+ },
48
+ body: JSON.stringify({
49
+ endpoint_id: endpointId,
50
+ amount_usd: config.price,
51
+ }),
52
+ });
53
+ const checkout = await checkoutRes.json();
54
+ res.status(402).json({
55
+ error: 'payment_required',
56
+ message: 'This API requires payment. Complete checkout to get access.',
57
+ checkout_url: checkout.checkout_url,
58
+ price: config.price,
59
+ unit_type: config.unitType || 'request',
60
+ });
61
+ }
62
+ catch (err) {
63
+ res.status(500).json({ error: 'Failed to create checkout session' });
64
+ }
65
+ return;
66
+ }
67
+ // Verify session is valid
68
+ try {
69
+ const verifyRes = await fetch(`${gatewayUrl}/api/v1/verify`, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'X-Metr-Api-Key': apiKey,
74
+ },
75
+ body: JSON.stringify({ session_token: sessionToken }),
76
+ });
77
+ const session = await verifyRes.json();
78
+ if (!session.valid) {
79
+ res.status(402).json({
80
+ error: 'session_expired',
81
+ message: 'Your billing session has expired. Please create a new checkout.',
82
+ });
83
+ return;
84
+ }
85
+ // Attach payment info to request
86
+ req.metr = {
87
+ sessionToken,
88
+ buyerId: session.buyer_id || '',
89
+ balanceUsd: session.remaining_balance_usd || 0,
90
+ };
91
+ // Let the handler execute
92
+ next();
93
+ // After handler completes, record usage (fire and forget)
94
+ fetch(`${gatewayUrl}/api/v1/meter`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'X-Metr-Api-Key': apiKey,
99
+ },
100
+ body: JSON.stringify({
101
+ endpoint_id: endpointId,
102
+ session_token: sessionToken,
103
+ units: 1,
104
+ unit_type: config.unitType || 'request',
105
+ }),
106
+ }).catch((err) => {
107
+ console.error('[metr] Failed to record usage:', err);
108
+ });
109
+ }
110
+ catch (err) {
111
+ res.status(500).json({ error: 'Failed to verify session' });
112
+ }
113
+ };
114
+ }
115
+ exports.default = metr;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@metr-sdk/express",
3
+ "version": "0.1.0",
4
+ "description": "One line of middleware to monetize any Express.js API",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "dev": "tsc --watch",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "keywords": ["metr", "api", "monetization", "metering", "billing", "stripe", "middleware", "express"],
14
+ "license": "MIT",
15
+ "peerDependencies": {
16
+ "express": "^4.0.0 || ^5.0.0"
17
+ },
18
+ "dependencies": {},
19
+ "devDependencies": {
20
+ "@types/express": "^4.17.21",
21
+ "@types/jest": "^29.5.0",
22
+ "@types/node": "^20.0.0",
23
+ "express": "^4.18.0",
24
+ "jest": "^29.7.0",
25
+ "ts-jest": "^29.1.0",
26
+ "typescript": "^5.3.0"
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @metr/express - One line of middleware to monetize any Express.js API
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { metr } from '@metr/express';
7
+ *
8
+ * app.post('/api/summarize', metr({ price: 0.001 }), (req, res) => {
9
+ * res.json({ summary: 'Your text summary...' });
10
+ * });
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ import { Request, Response, NextFunction } from 'express';
17
+
18
+ export interface MetrConfig {
19
+ /** Price per unit in USD (e.g., 0.001 = $0.001 per request) */
20
+ price: number;
21
+
22
+ /** Unit type for metering. Default: "request" */
23
+ unitType?: 'request' | 'token' | 'byte' | 'second';
24
+
25
+ /** Your metr API key. Falls back to METR_API_KEY env var */
26
+ apiKey?: string;
27
+
28
+ /** Your registered endpoint ID. Falls back to METR_ENDPOINT_ID env var */
29
+ endpointId?: string;
30
+
31
+ /** metr gateway URL. Falls back to METR_GATEWAY_URL env var */
32
+ gatewayUrl?: string;
33
+ }
34
+
35
+ export interface MetrPaymentInfo {
36
+ sessionToken: string;
37
+ buyerId: string;
38
+ balanceUsd: number;
39
+ }
40
+
41
+ declare global {
42
+ namespace Express {
43
+ interface Request {
44
+ metr?: MetrPaymentInfo;
45
+ }
46
+ }
47
+ }
48
+
49
+ const DEFAULT_GATEWAY_URL = 'https://api.metr.dev';
50
+
51
+ /**
52
+ * metr middleware - add to any Express route to require payment.
53
+ *
54
+ * Checks for a valid billing session token in the `Authorization` header.
55
+ * If missing or invalid, returns 402 Payment Required with a checkout URL.
56
+ * If valid, meters the usage and passes control to your handler.
57
+ */
58
+ export function metr(config: MetrConfig) {
59
+ const apiKey = config.apiKey || process.env.METR_API_KEY;
60
+ const endpointId = config.endpointId || process.env.METR_ENDPOINT_ID;
61
+ const gatewayUrl = config.gatewayUrl || process.env.METR_GATEWAY_URL || DEFAULT_GATEWAY_URL;
62
+
63
+ if (!apiKey) {
64
+ throw new Error('metr: API key required. Set config.apiKey or METR_API_KEY env var.');
65
+ }
66
+
67
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
68
+ // Extract session token from Authorization header
69
+ const authHeader = req.headers.authorization;
70
+ const sessionToken = authHeader?.startsWith('Bearer metr_sess_')
71
+ ? authHeader.slice(7)
72
+ : null;
73
+
74
+ if (!sessionToken) {
75
+ // No valid session — return 402 with checkout URL
76
+ try {
77
+ const checkoutRes = await fetch(`${gatewayUrl}/api/v1/checkout/create`, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'X-Metr-Api-Key': apiKey,
82
+ },
83
+ body: JSON.stringify({
84
+ endpoint_id: endpointId,
85
+ amount_usd: config.price,
86
+ }),
87
+ });
88
+
89
+ const checkout = await checkoutRes.json() as { checkout_url: string; session_token: string };
90
+
91
+ res.status(402).json({
92
+ error: 'payment_required',
93
+ message: 'This API requires payment. Complete checkout to get access.',
94
+ checkout_url: checkout.checkout_url,
95
+ price: config.price,
96
+ unit_type: config.unitType || 'request',
97
+ });
98
+ } catch (err) {
99
+ res.status(500).json({ error: 'Failed to create checkout session' });
100
+ }
101
+ return;
102
+ }
103
+
104
+ // Verify session is valid
105
+ try {
106
+ const verifyRes = await fetch(`${gatewayUrl}/api/v1/verify`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'X-Metr-Api-Key': apiKey,
111
+ },
112
+ body: JSON.stringify({ session_token: sessionToken }),
113
+ });
114
+
115
+ const session = await verifyRes.json() as {
116
+ valid: boolean;
117
+ buyer_id?: string;
118
+ remaining_balance_usd?: number;
119
+ };
120
+
121
+ if (!session.valid) {
122
+ res.status(402).json({
123
+ error: 'session_expired',
124
+ message: 'Your billing session has expired. Please create a new checkout.',
125
+ });
126
+ return;
127
+ }
128
+
129
+ // Attach payment info to request
130
+ req.metr = {
131
+ sessionToken,
132
+ buyerId: session.buyer_id || '',
133
+ balanceUsd: session.remaining_balance_usd || 0,
134
+ };
135
+
136
+ // Let the handler execute
137
+ next();
138
+
139
+ // After handler completes, record usage (fire and forget)
140
+ fetch(`${gatewayUrl}/api/v1/meter`, {
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ 'X-Metr-Api-Key': apiKey,
145
+ },
146
+ body: JSON.stringify({
147
+ endpoint_id: endpointId,
148
+ session_token: sessionToken,
149
+ units: 1,
150
+ unit_type: config.unitType || 'request',
151
+ }),
152
+ }).catch((err) => {
153
+ console.error('[metr] Failed to record usage:', err);
154
+ });
155
+ } catch (err) {
156
+ res.status(500).json({ error: 'Failed to verify session' });
157
+ }
158
+ };
159
+ }
160
+
161
+ export default metr;
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "strict": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
17
+ }