@drip-sdk/node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +603 -0
- package/dist/express.cjs +3 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +150 -0
- package/dist/express.d.ts +150 -0
- package/dist/express.js +3 -0
- package/dist/express.js.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1248 -0
- package/dist/index.d.ts +1246 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.cjs +3 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +92 -0
- package/dist/middleware.d.ts +92 -0
- package/dist/middleware.js +3 -0
- package/dist/middleware.js.map +1 -0
- package/dist/next.cjs +3 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +122 -0
- package/dist/next.d.ts +122 -0
- package/dist/next.js +3 -0
- package/dist/next.js.map +1 -0
- package/dist/types-92iVqLtE.d.cts +167 -0
- package/dist/types-D8mMON4v.d.ts +167 -0
- package/package.json +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Drip Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
# @drip-sdk/node
|
|
2
|
+
|
|
3
|
+
The official Node.js SDK for **Drip** - Usage-based billing for AI agents.
|
|
4
|
+
|
|
5
|
+
Drip enables real-time, per-request billing using USDC on blockchain. Perfect for AI APIs, compute platforms, and any service with variable usage patterns.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@drip-sdk/node)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @drip-sdk/node
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
yarn add @drip-sdk/node
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add @drip-sdk/node
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### One-Liner Integration (Recommended)
|
|
27
|
+
|
|
28
|
+
The fastest way to add billing to your API:
|
|
29
|
+
|
|
30
|
+
#### Next.js App Router
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// app/api/generate/route.ts
|
|
34
|
+
import { withDrip } from '@drip-sdk/node/next';
|
|
35
|
+
|
|
36
|
+
export const POST = withDrip({
|
|
37
|
+
meter: 'api_calls',
|
|
38
|
+
quantity: 1,
|
|
39
|
+
}, async (req, { charge, customerId }) => {
|
|
40
|
+
// Your handler - payment already verified!
|
|
41
|
+
console.log(`Charged ${charge.charge.amountUsdc} USDC to ${customerId}`);
|
|
42
|
+
return Response.json({ result: 'success' });
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### Express
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import express from 'express';
|
|
50
|
+
import { dripMiddleware } from '@drip-sdk/node/express';
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
|
|
54
|
+
app.use('/api/paid', dripMiddleware({
|
|
55
|
+
meter: 'api_calls',
|
|
56
|
+
quantity: 1,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
app.post('/api/paid/generate', (req, res) => {
|
|
60
|
+
console.log(`Charged: ${req.drip.charge.charge.amountUsdc} USDC`);
|
|
61
|
+
res.json({ success: true });
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Manual Integration
|
|
66
|
+
|
|
67
|
+
For more control, use the SDK directly:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { Drip } from '@drip-sdk/node';
|
|
71
|
+
|
|
72
|
+
// Initialize the client
|
|
73
|
+
const drip = new Drip({
|
|
74
|
+
apiKey: process.env.DRIP_API_KEY!,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Create a customer
|
|
78
|
+
const customer = await drip.createCustomer({
|
|
79
|
+
onchainAddress: '0x1234567890abcdef...',
|
|
80
|
+
externalCustomerId: 'user_123',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Record usage and charge
|
|
84
|
+
const result = await drip.charge({
|
|
85
|
+
customerId: customer.id,
|
|
86
|
+
meter: 'api_calls',
|
|
87
|
+
quantity: 100,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`Charged ${result.charge.amountUsdc} USDC`);
|
|
91
|
+
console.log(`TX: ${result.charge.txHash}`);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const drip = new Drip({
|
|
98
|
+
// Required: Your Drip API key
|
|
99
|
+
apiKey: 'drip_live_abc123...',
|
|
100
|
+
|
|
101
|
+
// Optional: API base URL (for staging/development)
|
|
102
|
+
baseUrl: 'https://api.drip.dev/v1',
|
|
103
|
+
|
|
104
|
+
// Optional: Request timeout in milliseconds (default: 30000)
|
|
105
|
+
timeout: 30000,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### Customer Management
|
|
112
|
+
|
|
113
|
+
#### Create a Customer
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const customer = await drip.createCustomer({
|
|
117
|
+
onchainAddress: '0x1234567890abcdef...',
|
|
118
|
+
externalCustomerId: 'user_123', // Your internal user ID
|
|
119
|
+
metadata: { plan: 'pro' },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Get a Customer
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const customer = await drip.getCustomer('cust_abc123');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### List Customers
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// List all customers
|
|
133
|
+
const { data: customers } = await drip.listCustomers();
|
|
134
|
+
|
|
135
|
+
// With filters
|
|
136
|
+
const { data: activeCustomers } = await drip.listCustomers({
|
|
137
|
+
status: 'ACTIVE',
|
|
138
|
+
limit: 50,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Get Customer Balance
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const balance = await drip.getBalance('cust_abc123');
|
|
146
|
+
console.log(`Balance: ${balance.balanceUSDC} USDC`);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Meters (Usage Types)
|
|
150
|
+
|
|
151
|
+
#### List Available Meters
|
|
152
|
+
|
|
153
|
+
Discover what meter names are valid for charging. Meters are defined by your pricing plans.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const { data: meters } = await drip.listMeters();
|
|
157
|
+
|
|
158
|
+
console.log('Available meters:');
|
|
159
|
+
for (const meter of meters) {
|
|
160
|
+
console.log(` ${meter.meter}: $${meter.unitPriceUsd}/unit`);
|
|
161
|
+
}
|
|
162
|
+
// Output:
|
|
163
|
+
// api_calls: $0.001/unit
|
|
164
|
+
// tokens: $0.00001/unit
|
|
165
|
+
// compute_seconds: $0.01/unit
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Charging & Usage
|
|
169
|
+
|
|
170
|
+
#### Record Usage and Charge
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const result = await drip.charge({
|
|
174
|
+
customerId: 'cust_abc123',
|
|
175
|
+
meter: 'api_calls',
|
|
176
|
+
quantity: 100,
|
|
177
|
+
idempotencyKey: 'req_unique_123', // Prevents duplicate charges
|
|
178
|
+
metadata: { endpoint: '/v1/chat' },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (result.success) {
|
|
182
|
+
console.log(`Charge ID: ${result.charge.id}`);
|
|
183
|
+
console.log(`Amount: ${result.charge.amountUsdc} USDC`);
|
|
184
|
+
console.log(`TX Hash: ${result.charge.txHash}`);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### Get Charge Details
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const charge = await drip.getCharge('chg_abc123');
|
|
192
|
+
console.log(`Status: ${charge.status}`);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### List Charges
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// List all charges
|
|
199
|
+
const { data: charges } = await drip.listCharges();
|
|
200
|
+
|
|
201
|
+
// Filter by customer and status
|
|
202
|
+
const { data: customerCharges } = await drip.listCharges({
|
|
203
|
+
customerId: 'cust_abc123',
|
|
204
|
+
status: 'CONFIRMED',
|
|
205
|
+
limit: 50,
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Check Charge Status
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const status = await drip.getChargeStatus('chg_abc123');
|
|
213
|
+
if (status.status === 'CONFIRMED') {
|
|
214
|
+
console.log('Charge confirmed on-chain!');
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Run Tracking (Simplified API)
|
|
219
|
+
|
|
220
|
+
Track agent executions with a single API call instead of multiple separate calls.
|
|
221
|
+
|
|
222
|
+
#### Record a Complete Run
|
|
223
|
+
|
|
224
|
+
The `recordRun()` method combines workflow creation, run tracking, event emission, and completion into one call:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Before: 4+ separate API calls
|
|
228
|
+
const workflow = await drip.createWorkflow({ name: 'My Agent', slug: 'my_agent' });
|
|
229
|
+
const run = await drip.startRun({ customerId, workflowId: workflow.id });
|
|
230
|
+
await drip.emitEvent({ runId: run.id, eventType: 'step1', ... });
|
|
231
|
+
await drip.emitEvent({ runId: run.id, eventType: 'step2', ... });
|
|
232
|
+
await drip.endRun(run.id, { status: 'COMPLETED' });
|
|
233
|
+
|
|
234
|
+
// After: 1 call with recordRun()
|
|
235
|
+
const result = await drip.recordRun({
|
|
236
|
+
customerId: 'cust_123',
|
|
237
|
+
workflow: 'my_agent', // Auto-creates workflow if it doesn't exist
|
|
238
|
+
events: [
|
|
239
|
+
{ eventType: 'agent.start', description: 'Started processing' },
|
|
240
|
+
{ eventType: 'tool.ocr', quantity: 3, units: 'pages', costUnits: 0.15 },
|
|
241
|
+
{ eventType: 'tool.validate', quantity: 1, costUnits: 0.05 },
|
|
242
|
+
{ eventType: 'agent.complete', description: 'Finished successfully' },
|
|
243
|
+
],
|
|
244
|
+
status: 'COMPLETED',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log(result.summary);
|
|
248
|
+
// Output: "✓ My Agent: 4 events recorded (250ms)"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Record a Failed Run
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const result = await drip.recordRun({
|
|
255
|
+
customerId: 'cust_123',
|
|
256
|
+
workflow: 'prescription_intake',
|
|
257
|
+
events: [
|
|
258
|
+
{ eventType: 'agent.start', description: 'Started processing' },
|
|
259
|
+
{ eventType: 'error', description: 'OCR failed: image too blurry' },
|
|
260
|
+
],
|
|
261
|
+
status: 'FAILED',
|
|
262
|
+
errorMessage: 'OCR processing failed',
|
|
263
|
+
errorCode: 'OCR_QUALITY_ERROR',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log(result.summary);
|
|
267
|
+
// Output: "✗ Prescription Intake: 2 events recorded (150ms)"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Webhooks
|
|
271
|
+
|
|
272
|
+
#### Create a Webhook
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const webhook = await drip.createWebhook({
|
|
276
|
+
url: 'https://api.yourapp.com/webhooks/drip',
|
|
277
|
+
events: ['charge.succeeded', 'charge.failed', 'customer.balance.low'],
|
|
278
|
+
description: 'Main webhook endpoint',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// IMPORTANT: Save the secret securely!
|
|
282
|
+
console.log(`Webhook secret: ${webhook.secret}`);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### List Webhooks
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const { data: webhooks } = await drip.listWebhooks();
|
|
289
|
+
webhooks.forEach((wh) => {
|
|
290
|
+
console.log(`${wh.url}: ${wh.stats?.successfulDeliveries} successful`);
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Delete a Webhook
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
await drip.deleteWebhook('wh_abc123');
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Verify Webhook Signatures
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import express from 'express';
|
|
304
|
+
import { Drip } from '@drip-sdk/node';
|
|
305
|
+
|
|
306
|
+
const app = express();
|
|
307
|
+
|
|
308
|
+
app.post(
|
|
309
|
+
'/webhooks/drip',
|
|
310
|
+
express.raw({ type: 'application/json' }),
|
|
311
|
+
(req, res) => {
|
|
312
|
+
const isValid = Drip.verifyWebhookSignature(
|
|
313
|
+
req.body.toString(),
|
|
314
|
+
req.headers['x-drip-signature'] as string,
|
|
315
|
+
process.env.DRIP_WEBHOOK_SECRET!,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!isValid) {
|
|
319
|
+
return res.status(401).send('Invalid signature');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const event = JSON.parse(req.body.toString());
|
|
323
|
+
|
|
324
|
+
switch (event.type) {
|
|
325
|
+
case 'charge.succeeded':
|
|
326
|
+
console.log('Charge succeeded:', event.data.charge_id);
|
|
327
|
+
break;
|
|
328
|
+
case 'charge.failed':
|
|
329
|
+
console.log('Charge failed:', event.data.failure_reason);
|
|
330
|
+
break;
|
|
331
|
+
case 'customer.balance.low':
|
|
332
|
+
console.log('Low balance alert for:', event.data.customer_id);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
res.status(200).send('OK');
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Available Webhook Events
|
|
342
|
+
|
|
343
|
+
| Event | Description |
|
|
344
|
+
| ---------------------------- | ------------------------------------ |
|
|
345
|
+
| `charge.succeeded` | Charge confirmed on-chain |
|
|
346
|
+
| `charge.failed` | Charge failed |
|
|
347
|
+
| `customer.balance.low` | Customer balance below threshold |
|
|
348
|
+
| `customer.deposit.confirmed` | Deposit confirmed on-chain |
|
|
349
|
+
| `customer.withdraw.confirmed`| Withdrawal confirmed |
|
|
350
|
+
| `customer.usage_cap.reached` | Usage cap hit |
|
|
351
|
+
| `customer.created` | New customer created |
|
|
352
|
+
| `usage.recorded` | Usage event recorded |
|
|
353
|
+
| `transaction.created` | Transaction initiated |
|
|
354
|
+
| `transaction.confirmed` | Transaction confirmed on-chain |
|
|
355
|
+
| `transaction.failed` | Transaction failed |
|
|
356
|
+
|
|
357
|
+
## TypeScript Usage
|
|
358
|
+
|
|
359
|
+
The SDK is written in TypeScript and includes full type definitions.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import {
|
|
363
|
+
Drip,
|
|
364
|
+
DripConfig,
|
|
365
|
+
DripError,
|
|
366
|
+
Customer,
|
|
367
|
+
Charge,
|
|
368
|
+
ChargeResult,
|
|
369
|
+
ChargeStatus,
|
|
370
|
+
Webhook,
|
|
371
|
+
WebhookEventType,
|
|
372
|
+
} from '@drip-sdk/node';
|
|
373
|
+
|
|
374
|
+
// All types are available for use
|
|
375
|
+
const config: DripConfig = {
|
|
376
|
+
apiKey: process.env.DRIP_API_KEY!,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const drip = new Drip(config);
|
|
380
|
+
|
|
381
|
+
// Type-safe responses
|
|
382
|
+
const customer: Customer = await drip.getCustomer('cust_abc123');
|
|
383
|
+
const result: ChargeResult = await drip.charge({
|
|
384
|
+
customerId: customer.id,
|
|
385
|
+
meter: 'api_calls',
|
|
386
|
+
quantity: 100,
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Error Handling
|
|
391
|
+
|
|
392
|
+
The SDK throws `DripError` for API errors:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { Drip, DripError } from '@drip-sdk/node';
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = await drip.charge({
|
|
399
|
+
customerId: 'cust_abc123',
|
|
400
|
+
meter: 'api_calls',
|
|
401
|
+
quantity: 100,
|
|
402
|
+
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error instanceof DripError) {
|
|
405
|
+
console.error(`API Error: ${error.message}`);
|
|
406
|
+
console.error(`Status Code: ${error.statusCode}`);
|
|
407
|
+
console.error(`Error Code: ${error.code}`);
|
|
408
|
+
|
|
409
|
+
switch (error.code) {
|
|
410
|
+
case 'INSUFFICIENT_BALANCE':
|
|
411
|
+
// Handle low balance
|
|
412
|
+
break;
|
|
413
|
+
case 'CUSTOMER_NOT_FOUND':
|
|
414
|
+
// Handle missing customer
|
|
415
|
+
break;
|
|
416
|
+
case 'RATE_LIMITED':
|
|
417
|
+
// Handle rate limiting
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Common Error Codes
|
|
425
|
+
|
|
426
|
+
| Code | Description |
|
|
427
|
+
| ---------------------- | ---------------------------------------- |
|
|
428
|
+
| `INSUFFICIENT_BALANCE` | Customer doesn't have enough balance |
|
|
429
|
+
| `CUSTOMER_NOT_FOUND` | Customer ID doesn't exist |
|
|
430
|
+
| `DUPLICATE_CUSTOMER` | Customer already exists |
|
|
431
|
+
| `INVALID_API_KEY` | API key is invalid or revoked |
|
|
432
|
+
| `RATE_LIMITED` | Too many requests |
|
|
433
|
+
| `TIMEOUT` | Request timed out |
|
|
434
|
+
|
|
435
|
+
## Idempotency
|
|
436
|
+
|
|
437
|
+
Use idempotency keys to safely retry requests:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
const result = await drip.charge({
|
|
441
|
+
customerId: 'cust_abc123',
|
|
442
|
+
meter: 'api_calls',
|
|
443
|
+
quantity: 100,
|
|
444
|
+
idempotencyKey: `req_${requestId}`, // Unique per request
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Retrying with the same key returns the original result
|
|
448
|
+
const retry = await drip.charge({
|
|
449
|
+
customerId: 'cust_abc123',
|
|
450
|
+
meter: 'api_calls',
|
|
451
|
+
quantity: 100,
|
|
452
|
+
idempotencyKey: `req_${requestId}`, // Same key = same result
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## CommonJS Usage
|
|
457
|
+
|
|
458
|
+
The SDK supports both ESM and CommonJS:
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
// ESM
|
|
462
|
+
import { Drip } from '@drip-sdk/node';
|
|
463
|
+
|
|
464
|
+
// CommonJS
|
|
465
|
+
const { Drip } = require('@drip-sdk/node');
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Requirements
|
|
469
|
+
|
|
470
|
+
- Node.js 18.0.0 or higher
|
|
471
|
+
- Native `fetch` support (included in Node.js 18+)
|
|
472
|
+
|
|
473
|
+
## Middleware Reference (withDrip)
|
|
474
|
+
|
|
475
|
+
### Configuration Options
|
|
476
|
+
|
|
477
|
+
| Option | Type | Default | Description |
|
|
478
|
+
|--------|------|---------|-------------|
|
|
479
|
+
| `meter` | `string` | **required** | Usage meter to charge (must match pricing plan) |
|
|
480
|
+
| `quantity` | `number \| (req) => number` | **required** | Quantity to charge (static or dynamic) |
|
|
481
|
+
| `apiKey` | `string` | `DRIP_API_KEY` | Drip API key |
|
|
482
|
+
| `baseUrl` | `string` | `DRIP_API_URL` | Drip API base URL |
|
|
483
|
+
| `customerResolver` | `'header' \| 'query' \| function` | `'header'` | How to identify customers |
|
|
484
|
+
| `skipInDevelopment` | `boolean` | `false` | Skip charging in dev mode |
|
|
485
|
+
| `metadata` | `object \| function` | `undefined` | Custom metadata for charges |
|
|
486
|
+
| `onCharge` | `function` | `undefined` | Callback after successful charge |
|
|
487
|
+
| `onError` | `function` | `undefined` | Custom error handler |
|
|
488
|
+
|
|
489
|
+
### How It Works
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
493
|
+
│ Your API │ │ withDrip │ │ Drip Backend │
|
|
494
|
+
│ (Next/Express)│───▶│ Middleware │───▶│ API │
|
|
495
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
496
|
+
│ │ │
|
|
497
|
+
▼ ▼ ▼
|
|
498
|
+
1. Request 2. Resolve 3. Check balance
|
|
499
|
+
arrives customer & charge
|
|
500
|
+
│ │ │
|
|
501
|
+
▼ ▼ ▼
|
|
502
|
+
6. Response 5. Pass to 4. Return result
|
|
503
|
+
returned handler or 402
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### x402 Payment Flow
|
|
507
|
+
|
|
508
|
+
When a customer has insufficient balance, the middleware returns `402 Payment Required`:
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
HTTP/1.1 402 Payment Required
|
|
512
|
+
X-Payment-Required: true
|
|
513
|
+
X-Payment-Amount: 0.01
|
|
514
|
+
X-Payment-Recipient: 0x...
|
|
515
|
+
X-Payment-Usage-Id: 0x...
|
|
516
|
+
X-Payment-Expires: 1704110400
|
|
517
|
+
X-Payment-Nonce: abc123
|
|
518
|
+
|
|
519
|
+
{
|
|
520
|
+
"error": "Payment required",
|
|
521
|
+
"code": "PAYMENT_REQUIRED",
|
|
522
|
+
"paymentRequest": { ... },
|
|
523
|
+
"instructions": {
|
|
524
|
+
"step1": "Sign the payment request with your session key using EIP-712",
|
|
525
|
+
"step2": "Retry the request with X-Payment-* headers"
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Advanced Usage
|
|
531
|
+
|
|
532
|
+
#### Dynamic Quantity
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
export const POST = withDrip({
|
|
536
|
+
meter: 'tokens',
|
|
537
|
+
quantity: async (req) => {
|
|
538
|
+
const body = await req.json();
|
|
539
|
+
return body.maxTokens ?? 100;
|
|
540
|
+
},
|
|
541
|
+
}, handler);
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### Custom Customer Resolution
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
export const POST = withDrip({
|
|
548
|
+
meter: 'api_calls',
|
|
549
|
+
quantity: 1,
|
|
550
|
+
customerResolver: (req) => {
|
|
551
|
+
const token = req.headers.get('authorization')?.split(' ')[1];
|
|
552
|
+
return decodeJWT(token).customerId;
|
|
553
|
+
},
|
|
554
|
+
}, handler);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### Factory Pattern
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// lib/drip.ts
|
|
561
|
+
import { createWithDrip } from '@drip-sdk/node/next';
|
|
562
|
+
|
|
563
|
+
export const withDrip = createWithDrip({
|
|
564
|
+
apiKey: process.env.DRIP_API_KEY,
|
|
565
|
+
baseUrl: process.env.DRIP_API_URL,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// app/api/generate/route.ts
|
|
569
|
+
import { withDrip } from '@/lib/drip';
|
|
570
|
+
export const POST = withDrip({ meter: 'api_calls', quantity: 1 }, handler);
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### What's Included vs. Missing
|
|
574
|
+
|
|
575
|
+
| Feature | Status | Description |
|
|
576
|
+
|---------|--------|-------------|
|
|
577
|
+
| Next.js App Router | ✅ | `withDrip` wrapper |
|
|
578
|
+
| Express Middleware | ✅ | `dripMiddleware` |
|
|
579
|
+
| x402 Payment Flow | ✅ | Automatic 402 handling |
|
|
580
|
+
| Dynamic Quantity | ✅ | Function-based pricing |
|
|
581
|
+
| Customer Resolution | ✅ | Header, query, or custom |
|
|
582
|
+
| Idempotency | ✅ | Built-in or custom keys |
|
|
583
|
+
| Dev Mode Skip | ✅ | Skip in development |
|
|
584
|
+
| Metadata | ✅ | Attach to charges |
|
|
585
|
+
| TypeScript | ✅ | Full type definitions |
|
|
586
|
+
| Fastify Adapter | ❌ | Coming soon |
|
|
587
|
+
| Rate Limiting | ❌ | Planned |
|
|
588
|
+
| Balance Caching | ❌ | Planned |
|
|
589
|
+
|
|
590
|
+
## Contributing
|
|
591
|
+
|
|
592
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
593
|
+
|
|
594
|
+
## License
|
|
595
|
+
|
|
596
|
+
MIT - see [LICENSE](./LICENSE)
|
|
597
|
+
|
|
598
|
+
## Links
|
|
599
|
+
|
|
600
|
+
- [GitHub Repository](https://github.com/MichaelLevin5908/drip-sdk)
|
|
601
|
+
- [Issue Tracker](https://github.com/MichaelLevin5908/drip-sdk/issues)
|
|
602
|
+
- [npm Package](https://www.npmjs.com/package/@drip-sdk/node)
|
|
603
|
+
- [Documentation](https://docs.drip.dev)
|
package/dist/express.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';var crypto=require('crypto');var f=class t extends Error{constructor(r,s,n){super(r);this.statusCode=s;this.code=n;this.name="DripError",Object.setPrototypeOf(this,t.prototype);}},x=class{apiKey;baseUrl;timeout;constructor(e){if(!e.apiKey)throw new Error("Drip API key is required");this.apiKey=e.apiKey,this.baseUrl=e.baseUrl||"https://api.drip.dev/v1",this.timeout=e.timeout||3e4;}async request(e,r={}){let s=new AbortController,n=setTimeout(()=>s.abort(),this.timeout);try{let i=await fetch(`${this.baseUrl}${e}`,{...r,signal:s.signal,headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`,...r.headers}});if(i.status===204)return {success:!0};let o=await i.json();if(!i.ok)throw new f(o.message||o.error||"Request failed",i.status,o.code);return o}catch(i){throw i instanceof f?i:i instanceof Error&&i.name==="AbortError"?new f("Request timed out",408,"TIMEOUT"):new f(i instanceof Error?i.message:"Unknown error",0,"UNKNOWN")}finally{clearTimeout(n);}}async createCustomer(e){return this.request("/customers",{method:"POST",body:JSON.stringify(e)})}async getCustomer(e){return this.request(`/customers/${e}`)}async listCustomers(e){let r=new URLSearchParams;e?.limit&&r.set("limit",e.limit.toString()),e?.status&&r.set("status",e.status);let s=r.toString(),n=s?`/customers?${s}`:"/customers";return this.request(n)}async getBalance(e){return this.request(`/customers/${e}/balance`)}async charge(e){return this.request("/usage",{method:"POST",body:JSON.stringify({customerId:e.customerId,usageType:e.meter,quantity:e.quantity,idempotencyKey:e.idempotencyKey,metadata:e.metadata})})}async getCharge(e){return this.request(`/charges/${e}`)}async listCharges(e){let r=new URLSearchParams;e?.customerId&&r.set("customerId",e.customerId),e?.status&&r.set("status",e.status),e?.limit&&r.set("limit",e.limit.toString()),e?.offset&&r.set("offset",e.offset.toString());let s=r.toString(),n=s?`/charges?${s}`:"/charges";return this.request(n)}async getChargeStatus(e){return this.request(`/charges/${e}/status`)}async checkout(e){let r=await this.request("/checkout",{method:"POST",body:JSON.stringify({customer_id:e.customerId,external_customer_id:e.externalCustomerId,amount:e.amount,return_url:e.returnUrl,cancel_url:e.cancelUrl,metadata:e.metadata})});return {id:r.id,url:r.url,expiresAt:r.expires_at,amountUsd:r.amount_usd}}async createWebhook(e){return this.request("/webhooks",{method:"POST",body:JSON.stringify(e)})}async listWebhooks(){return this.request("/webhooks")}async getWebhook(e){return this.request(`/webhooks/${e}`)}async deleteWebhook(e){return this.request(`/webhooks/${e}`,{method:"DELETE"})}async testWebhook(e){return this.request(`/webhooks/${e}/test`,{method:"POST"})}async rotateWebhookSecret(e){return this.request(`/webhooks/${e}/rotate-secret`,{method:"POST"})}async createWorkflow(e){return this.request("/workflows",{method:"POST",body:JSON.stringify(e)})}async listWorkflows(){return this.request("/workflows")}async startRun(e){return this.request("/runs",{method:"POST",body:JSON.stringify(e)})}async endRun(e,r){return this.request(`/runs/${e}`,{method:"PATCH",body:JSON.stringify(r)})}async getRunTimeline(e){return this.request(`/runs/${e}`)}async emitEvent(e){return this.request("/events",{method:"POST",body:JSON.stringify(e)})}async emitEventsBatch(e){return this.request("/events/batch",{method:"POST",body:JSON.stringify({events:e})})}async listMeters(){let e=await this.request("/pricing-plans");return {data:e.data.map(r=>({id:r.id,name:r.name,meter:r.unitType,unitPriceUsd:r.unitPriceUsd,isActive:r.isActive})),count:e.count}}async recordRun(e){let r=Date.now(),s=e.workflow,n=e.workflow;if(!e.workflow.startsWith("wf_"))try{let y=(await this.listWorkflows()).data.find(d=>d.slug===e.workflow||d.id===e.workflow);if(y)s=y.id,n=y.name;else {let d=await this.createWorkflow({name:e.workflow.replace(/[_-]/g," ").replace(/\b\w/g,E=>E.toUpperCase()),slug:e.workflow,productSurface:"AGENT"});s=d.id,n=d.name;}}catch{s=e.workflow;}let i=await this.startRun({customerId:e.customerId,workflowId:s,externalRunId:e.externalRunId,correlationId:e.correlationId,metadata:e.metadata}),o=0,c=0;if(e.events.length>0){let l=e.events.map((d,E)=>({runId:i.id,eventType:d.eventType,quantity:d.quantity,units:d.units,description:d.description,costUnits:d.costUnits,metadata:d.metadata,idempotencyKey:e.externalRunId?`${e.externalRunId}:${d.eventType}:${E}`:void 0})),y=await this.emitEventsBatch(l);o=y.created,c=y.duplicates;}let m=await this.endRun(i.id,{status:e.status,errorMessage:e.errorMessage,errorCode:e.errorCode}),g=Date.now()-r,u=e.events.length>0?`${o} events recorded`:"no events",h=`${e.status==="COMPLETED"?"\u2713":e.status==="FAILED"?"\u2717":"\u25CB"} ${n}: ${u} (${m.durationMs??g}ms)`;return {run:{id:i.id,workflowId:s,workflowName:n,status:e.status,durationMs:m.durationMs},events:{created:o,duplicates:c},totalCostUnits:m.totalCostUnits,summary:h}}static generateIdempotencyKey(e){let r=[e.customerId,e.runId??"no_run",e.stepName,String(e.sequence??0)],s=0,n=r.join("|");for(let i=0;i<n.length;i++){let o=n.charCodeAt(i);s=(s<<5)-s+o,s=s&s;}return `drip_${Math.abs(s).toString(36)}_${e.stepName.slice(0,16)}`}static verifyWebhookSignature(e,r,s){let n=new TextEncoder,i=n.encode(e),o=n.encode(s),c=0;for(let g=0;g<i.length;g++)c=(c<<5)-c+i[g]+o[g%o.length]|0;let m=Math.abs(c).toString(16);return r===m||r.includes(m)}};var p=class t extends Error{constructor(r,s,n,i){super(r);this.code=s;this.statusCode=n;this.details=i;this.name="DripMiddlewareError",Object.setPrototypeOf(this,t.prototype);}};var v=300,A=300,S=["x-payment-signature","x-payment-session-key","x-payment-smart-account","x-payment-timestamp","x-payment-amount","x-payment-recipient","x-payment-usage-id","x-payment-nonce"];function N(t){return t.toLowerCase()}function R(t,e){let r=N(e);if(t[r]!==void 0){let s=t[r];return Array.isArray(s)?s[0]:s}for(let[s,n]of Object.entries(t))if(s.toLowerCase()===r)return Array.isArray(n)?n[0]:n}function P(t){return S.every(e=>R(t,e)!==void 0)}function O(t){let e=R(t,"x-payment-signature"),r=R(t,"x-payment-session-key"),s=R(t,"x-payment-smart-account"),n=R(t,"x-payment-timestamp"),i=R(t,"x-payment-amount"),o=R(t,"x-payment-recipient"),c=R(t,"x-payment-usage-id"),m=R(t,"x-payment-nonce");if(!e||!r||!s||!n||!i||!o||!c||!m)return null;let g=parseInt(n,10);if(isNaN(g)||Math.floor(Date.now()/1e3)-g>A)return null;let a=(h,l)=>{if(!h.startsWith("0x"))return false;let y=h.slice(2);return y.length<l?false:/^[a-fA-F0-9]+$/.test(y)};return !a(e,130)||!a(r,64)||!a(s,40)?null:{signature:e,sessionKeyId:r,smartAccount:s,timestamp:g,amount:i,recipient:o,usageId:c,nonce:m}}function U(t){let e=Math.floor(Date.now()/1e3),r=e+(t.expiresInSec??v),s=`${e}-${crypto.randomBytes(16).toString("hex")}`,n=t.usageId;n.startsWith("0x")||(n=b(n));let i={"X-Payment-Required":"true","X-Payment-Amount":t.amount,"X-Payment-Recipient":t.recipient,"X-Payment-Usage-Id":n,"X-Payment-Description":t.description??"API usage charge","X-Payment-Expires":String(r),"X-Payment-Nonce":s,"X-Payment-Timestamp":String(e)},o={amount:t.amount,recipient:t.recipient,usageId:n,description:t.description??"API usage charge",expiresAt:r,nonce:s,timestamp:e};return {headers:i,paymentRequest:o}}function b(t){let e=5381,r=52711;for(let i=0;i<t.length;i++){let o=t.charCodeAt(i);e=(e<<5)+e^o,r=(r<<5)+r^o;}return `0x${Math.abs(e*31+r).toString(16).padStart(16,"0").slice(0,16).padEnd(64,"0")}`}async function M(t,e){let r=e.customerResolver??"header";if(typeof r=="function")return r(t);if(r==="header"){let s=R(t.headers,"x-drip-customer-id")??R(t.headers,"x-customer-id");if(!s)throw new p("Missing customer ID. Include X-Drip-Customer-Id header.","CUSTOMER_RESOLUTION_FAILED",400);return s}if(r==="query"){let s=t.query??{},n=s.drip_customer_id??s.customer_id,i=Array.isArray(n)?n[0]:n;if(!i)throw new p("Missing customer ID. Include drip_customer_id query parameter.","CUSTOMER_RESOLUTION_FAILED",400);return i}throw new p(`Invalid customer resolver: ${r}`,"CONFIGURATION_ERROR",500)}async function _(t,e){return typeof e.quantity=="function"?e.quantity(t):e.quantity}async function W(t,e,r){if(r.idempotencyKey)return r.idempotencyKey(t);let s=Date.now(),n=[t.method,t.url,e,s];return `drip_${b(n.join("|")).slice(2,18)}`}function C(t){let e=t.apiKey??process.env.DRIP_API_KEY;if(!e)throw new p("Missing Drip API key. Set DRIP_API_KEY environment variable or pass apiKey in config.","CONFIGURATION_ERROR",500);return new x({apiKey:e,baseUrl:t.baseUrl??process.env.DRIP_API_URL})}async function I(t,e){if(e.skipInDevelopment&&process.env.NODE_ENV==="development"){console.warn("[Drip] Skipping billing in development mode. Set skipInDevelopment: false or NODE_ENV to production to enable billing.");let r=C(e),s={success:true,usageEventId:"dev_usage_event",isReplay:false,charge:{id:"dev_charge",amountUsdc:"0.00",amountToken:"0",txHash:"0x0",status:"CONFIRMED"}};return {success:true,state:{customerId:"dev_customer",quantity:typeof e.quantity=="number"?e.quantity:1,idempotencyKey:"dev_idempotency",hasPaymentProof:false},charge:s,drip:r,isReplay:false}}try{let r=C(e),s=await M(t,e),n=await _(t,e),i=await W(t,s,e),o=P(t.headers),c=o?O(t.headers):void 0,m={customerId:s,quantity:n,idempotencyKey:i,hasPaymentProof:o,paymentProof:c??void 0},g=typeof e.metadata=="function"?e.metadata(t):e.metadata;try{let u=await r.charge({customerId:s,meter:e.meter,quantity:n,idempotencyKey:i,metadata:g});return e.onCharge&&await e.onCharge(u,t),{success:!0,state:m,charge:u,drip:r,isReplay:u.isReplay??!1}}catch(u){if(u instanceof f){if(u.statusCode===402){let a=process.env.DRIP_RECIPIENT_ADDRESS;if(!a)throw new p("DRIP_RECIPIENT_ADDRESS environment variable must be configured for x402 payment flow.","CONFIGURATION_ERROR",500);let h="0.01",l=u.message.match(/amount[:\s]+([0-9.]+)/i);l?h=l[1]:h=(n*1e-4).toFixed(6);let{headers:y,paymentRequest:d}=U({amount:h,recipient:a,usageId:i,description:`${e.meter} usage charge`});return {success:!1,error:new p("Insufficient balance. Payment required.","PAYMENT_REQUIRED",402),paymentRequired:{headers:y,paymentRequest:d}}}throw e.onError&&await e.onError(u,t),new p(u.message,"CHARGE_FAILED",u.statusCode,{code:u.code})}throw u}}catch(r){if(r instanceof p)return {success:false,error:r};let s=r instanceof Error?r.message:"Unknown error";return {success:false,error:new p(s,"INTERNAL_ERROR",500)}}}function q(t){let e={};for(let[r,s]of Object.entries(t))e[r.toLowerCase()]=Array.isArray(s)?s[0]:s;return e}function L(t,e,r){t.status(402).set(e).json({error:"Payment required",code:"PAYMENT_REQUIRED",paymentRequest:r,instructions:{step1:"Sign the payment request with your session key using EIP-712",step2:"Retry the request with X-Payment-* headers",documentation:"https://docs.drip.dev/x402"}});}function X(t,e,r,s,n){t.status(s).json({error:e,code:r,...n&&{details:n}});}function D(t){let e=t.attachToRequest??true;return async(r,s,n)=>{let i={method:r.method,url:r.originalUrl||r.url,headers:q(r.headers),query:r.query},o=typeof t.quantity=="function"?await t.quantity(r):t.quantity,c;if(typeof t.customerResolver=="function"){let l=t.customerResolver;c=async()=>l(r);}else c=t.customerResolver;let m;if(typeof t.idempotencyKey=="function"){let l=t.idempotencyKey;m=async()=>l(r);}let g=typeof t.metadata=="function"?t.metadata(r):t.metadata,u={meter:t.meter,quantity:o,apiKey:t.apiKey,baseUrl:t.baseUrl,customerResolver:c,idempotencyKey:m,metadata:g,skipInDevelopment:t.skipInDevelopment,onCharge:void 0,onError:void 0},a=await I(i,u);if(!a.success){if(t.errorHandler&&await t.errorHandler(a.error,r,s))return;if(a.paymentRequired){L(s,a.paymentRequired.headers,a.paymentRequired.paymentRequest);return}X(s,a.error.message,a.error.code,a.error.statusCode,a.error.details);return}t.onCharge&&await t.onCharge(a.charge,r);let h={drip:a.drip,customerId:a.state.customerId,charge:a.charge,isReplay:a.isReplay};e&&(r.drip=h),n();}}function K(t){return e=>D({...t,...e})}function $(t){return P(q(t.headers))}function k(t){return "drip"in t&&typeof t.drip=="object"}function F(t){if(!k(t))throw new Error("Drip context not found on request. Ensure dripMiddleware is applied before this route.");return t.drip}
|
|
2
|
+
exports.Drip=x;exports.DripError=f;exports.DripMiddlewareError=p;exports.createDripMiddleware=K;exports.dripMiddleware=D;exports.getDripContext=F;exports.hasDripContext=k;exports.hasPaymentProofHeaders=$;//# sourceMappingURL=express.cjs.map
|
|
3
|
+
//# sourceMappingURL=express.cjs.map
|