@dodopayments/opencode-plugin 0.2.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,591 @@
1
+ ---
2
+ name: license-keys
3
+ description: Guide for implementing license key management with Dodo Payments - activation, validation, and access control for software products.
4
+ ---
5
+
6
+ # Dodo Payments License Keys
7
+
8
+ **Reference: [docs.dodopayments.com/features/license-keys](https://docs.dodopayments.com/features/license-keys)**
9
+
10
+ License keys authorize access to your digital products. Use them for software licensing, per-seat controls, and gating premium features.
11
+
12
+ ---
13
+
14
+ ## Overview
15
+
16
+ License keys are unique tokens that:
17
+ - Authorize access to software, plugins, CLIs
18
+ - Limit activations per user or device
19
+ - Gate downloads, updates, or premium features
20
+ - Can be linked to subscriptions or one-time purchases
21
+
22
+ ---
23
+
24
+ ## Creating License Keys
25
+
26
+ ### In Dashboard
27
+
28
+ 1. Go to Dashboard → License Keys
29
+ 2. Click "Create License Key"
30
+ 3. Configure settings:
31
+ - **Expiry Date**: Duration or "no expiry" for perpetual
32
+ - **Activation Limit**: Max concurrent activations (1, 5, unlimited)
33
+ - **Activation Instructions**: Steps for customers
34
+
35
+ 4. Save the license key configuration
36
+
37
+ ### Auto-Generation on Purchase
38
+
39
+ License keys can be automatically generated when a product is purchased:
40
+
41
+ 1. Configure your product with license key settings
42
+ 2. When purchased, a key is generated and emailed to customer
43
+ 3. `license_key.created` webhook is fired
44
+
45
+ ---
46
+
47
+ ## API Reference
48
+
49
+ ### Public Endpoints (No API Key Required)
50
+
51
+ These endpoints can be called directly from client applications:
52
+
53
+ | Endpoint | Description |
54
+ |----------|-------------|
55
+ | `POST /licenses/activate` | Activate a license key |
56
+ | `POST /licenses/deactivate` | Deactivate an instance |
57
+ | `POST /licenses/validate` | Check if key is valid |
58
+
59
+ ### Authenticated Endpoints (API Key Required)
60
+
61
+ | Endpoint | Description |
62
+ |----------|-------------|
63
+ | `GET /license_keys` | List all license keys |
64
+ | `GET /license_keys/:id` | Get license key details |
65
+ | `PATCH /license_keys/:id` | Update license key |
66
+ | `GET /license_key_instances` | List activation instances |
67
+
68
+ ---
69
+
70
+ ## Implementation Examples
71
+
72
+ ### Activate a License Key
73
+
74
+ ```typescript
75
+ import DodoPayments from 'dodopayments';
76
+
77
+ // No API key needed for public endpoints
78
+ const client = new DodoPayments();
79
+
80
+ async function activateLicense(licenseKey: string, deviceName: string) {
81
+ try {
82
+ const response = await client.licenses.activate({
83
+ license_key: licenseKey,
84
+ name: deviceName, // e.g., "John's MacBook Pro"
85
+ });
86
+
87
+ return {
88
+ success: true,
89
+ instanceId: response.id,
90
+ message: 'License activated successfully',
91
+ };
92
+ } catch (error: any) {
93
+ return {
94
+ success: false,
95
+ message: error.message || 'Activation failed',
96
+ };
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Validate a License Key
102
+
103
+ ```typescript
104
+ import DodoPayments from 'dodopayments';
105
+
106
+ const client = new DodoPayments();
107
+
108
+ async function validateLicense(licenseKey: string) {
109
+ try {
110
+ const response = await client.licenses.validate({
111
+ license_key: licenseKey,
112
+ });
113
+
114
+ return {
115
+ valid: response.valid,
116
+ activations: response.activations_count,
117
+ maxActivations: response.activations_limit,
118
+ expiresAt: response.expires_at,
119
+ };
120
+ } catch (error) {
121
+ return { valid: false };
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### Deactivate a License
127
+
128
+ ```typescript
129
+ import DodoPayments from 'dodopayments';
130
+
131
+ const client = new DodoPayments();
132
+
133
+ async function deactivateLicense(licenseKey: string, instanceId: string) {
134
+ try {
135
+ await client.licenses.deactivate({
136
+ license_key: licenseKey,
137
+ license_key_instance_id: instanceId,
138
+ });
139
+
140
+ return { success: true, message: 'License deactivated' };
141
+ } catch (error: any) {
142
+ return { success: false, message: error.message };
143
+ }
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Desktop App Integration
150
+
151
+ ### Electron App Example
152
+
153
+ ```typescript
154
+ // main/license.ts
155
+ import Store from 'electron-store';
156
+ import DodoPayments from 'dodopayments';
157
+
158
+ const store = new Store();
159
+ const client = new DodoPayments();
160
+
161
+ interface LicenseInfo {
162
+ key: string;
163
+ instanceId: string;
164
+ activatedAt: string;
165
+ }
166
+
167
+ export async function activateLicense(licenseKey: string): Promise<boolean> {
168
+ try {
169
+ // Get device identifier
170
+ const deviceName = `${os.hostname()} - ${os.platform()}`;
171
+
172
+ const response = await client.licenses.activate({
173
+ license_key: licenseKey,
174
+ name: deviceName,
175
+ });
176
+
177
+ // Store license info locally
178
+ const licenseInfo: LicenseInfo = {
179
+ key: licenseKey,
180
+ instanceId: response.id,
181
+ activatedAt: new Date().toISOString(),
182
+ };
183
+
184
+ store.set('license', licenseInfo);
185
+ return true;
186
+ } catch (error) {
187
+ console.error('Activation failed:', error);
188
+ return false;
189
+ }
190
+ }
191
+
192
+ export async function checkLicense(): Promise<boolean> {
193
+ const license = store.get('license') as LicenseInfo | undefined;
194
+
195
+ if (!license) {
196
+ return false;
197
+ }
198
+
199
+ try {
200
+ const response = await client.licenses.validate({
201
+ license_key: license.key,
202
+ });
203
+
204
+ return response.valid;
205
+ } catch (error) {
206
+ // If offline, trust local license (with optional grace period)
207
+ const activatedAt = new Date(license.activatedAt);
208
+ const daysSinceActivation = (Date.now() - activatedAt.getTime()) / (1000 * 60 * 60 * 24);
209
+
210
+ // Allow 30-day offline grace period
211
+ return daysSinceActivation < 30;
212
+ }
213
+ }
214
+
215
+ export async function deactivateLicense(): Promise<boolean> {
216
+ const license = store.get('license') as LicenseInfo | undefined;
217
+
218
+ if (!license) {
219
+ return true;
220
+ }
221
+
222
+ try {
223
+ await client.licenses.deactivate({
224
+ license_key: license.key,
225
+ license_key_instance_id: license.instanceId,
226
+ });
227
+
228
+ store.delete('license');
229
+ return true;
230
+ } catch (error) {
231
+ console.error('Deactivation failed:', error);
232
+ return false;
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### React Component for License Input
238
+
239
+ ```tsx
240
+ // components/LicenseActivation.tsx
241
+ import { useState } from 'react';
242
+
243
+ interface Props {
244
+ onActivated: () => void;
245
+ }
246
+
247
+ export function LicenseActivation({ onActivated }: Props) {
248
+ const [licenseKey, setLicenseKey] = useState('');
249
+ const [loading, setLoading] = useState(false);
250
+ const [error, setError] = useState<string | null>(null);
251
+
252
+ const handleActivate = async () => {
253
+ setLoading(true);
254
+ setError(null);
255
+
256
+ try {
257
+ // Call main process (Electron IPC)
258
+ const success = await window.electronAPI.activateLicense(licenseKey);
259
+
260
+ if (success) {
261
+ onActivated();
262
+ } else {
263
+ setError('Invalid license key. Please check and try again.');
264
+ }
265
+ } catch (err) {
266
+ setError('Activation failed. Please try again.');
267
+ } finally {
268
+ setLoading(false);
269
+ }
270
+ };
271
+
272
+ return (
273
+ <div className="license-form">
274
+ <h2>Activate Your License</h2>
275
+ <p>Enter your license key to unlock all features.</p>
276
+
277
+ <input
278
+ type="text"
279
+ value={licenseKey}
280
+ onChange={(e) => setLicenseKey(e.target.value)}
281
+ placeholder="XXXX-XXXX-XXXX-XXXX"
282
+ disabled={loading}
283
+ />
284
+
285
+ {error && <p className="error">{error}</p>}
286
+
287
+ <button onClick={handleActivate} disabled={loading || !licenseKey}>
288
+ {loading ? 'Activating...' : 'Activate License'}
289
+ </button>
290
+
291
+ <a href="https://yoursite.com/purchase" target="_blank">
292
+ Don't have a license? Purchase here
293
+ </a>
294
+ </div>
295
+ );
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## CLI Tool Integration
302
+
303
+ ### Node.js CLI Example
304
+
305
+ ```typescript
306
+ // src/license.ts
307
+ import Conf from 'conf';
308
+ import DodoPayments from 'dodopayments';
309
+ import { machineIdSync } from 'node-machine-id';
310
+
311
+ const config = new Conf({ projectName: 'your-cli' });
312
+ const client = new DodoPayments();
313
+
314
+ export async function activate(licenseKey: string): Promise<void> {
315
+ const machineId = machineIdSync();
316
+ const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;
317
+
318
+ try {
319
+ const response = await client.licenses.activate({
320
+ license_key: licenseKey,
321
+ name: deviceName,
322
+ });
323
+
324
+ config.set('license', {
325
+ key: licenseKey,
326
+ instanceId: response.id,
327
+ machineId,
328
+ });
329
+
330
+ console.log('License activated successfully!');
331
+ } catch (error: any) {
332
+ if (error.status === 400) {
333
+ console.error('Invalid license key.');
334
+ } else if (error.status === 403) {
335
+ console.error('Activation limit reached. Deactivate another device first.');
336
+ } else {
337
+ console.error('Activation failed:', error.message);
338
+ }
339
+ process.exit(1);
340
+ }
341
+ }
342
+
343
+ export async function checkLicense(): Promise<boolean> {
344
+ const license = config.get('license') as any;
345
+
346
+ if (!license) {
347
+ return false;
348
+ }
349
+
350
+ try {
351
+ const response = await client.licenses.validate({
352
+ license_key: license.key,
353
+ });
354
+
355
+ return response.valid;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ export async function deactivate(): Promise<void> {
362
+ const license = config.get('license') as any;
363
+
364
+ if (!license) {
365
+ console.log('No active license found.');
366
+ return;
367
+ }
368
+
369
+ try {
370
+ await client.licenses.deactivate({
371
+ license_key: license.key,
372
+ license_key_instance_id: license.instanceId,
373
+ });
374
+
375
+ config.delete('license');
376
+ console.log('License deactivated.');
377
+ } catch (error: any) {
378
+ console.error('Deactivation failed:', error.message);
379
+ }
380
+ }
381
+
382
+ // Middleware to check license before commands
383
+ export function requireLicense() {
384
+ return async () => {
385
+ const valid = await checkLicense();
386
+ if (!valid) {
387
+ console.error('This command requires a valid license.');
388
+ console.error('Run: your-cli activate <license-key>');
389
+ process.exit(1);
390
+ }
391
+ };
392
+ }
393
+ ```
394
+
395
+ ### CLI Commands
396
+
397
+ ```typescript
398
+ // src/cli.ts
399
+ import { Command } from 'commander';
400
+ import { activate, deactivate, checkLicense, requireLicense } from './license';
401
+
402
+ const program = new Command();
403
+
404
+ program
405
+ .command('activate <license-key>')
406
+ .description('Activate your license')
407
+ .action(activate);
408
+
409
+ program
410
+ .command('deactivate')
411
+ .description('Deactivate license on this device')
412
+ .action(deactivate);
413
+
414
+ program
415
+ .command('status')
416
+ .description('Check license status')
417
+ .action(async () => {
418
+ const valid = await checkLicense();
419
+ console.log(valid ? 'License: Active' : 'License: Not activated');
420
+ });
421
+
422
+ // Protected command example
423
+ program
424
+ .command('generate')
425
+ .description('Generate something (requires license)')
426
+ .hook('preAction', requireLicense())
427
+ .action(async () => {
428
+ // Premium feature
429
+ });
430
+
431
+ program.parse();
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Webhook Integration
437
+
438
+ ### Handle License Key Creation
439
+
440
+ ```typescript
441
+ // app/api/webhooks/dodo/route.ts
442
+ export async function POST(req: NextRequest) {
443
+ const event = await req.json();
444
+
445
+ if (event.type === 'license_key.created') {
446
+ const { id, key, product_id, customer_id, expires_at } = event.data;
447
+
448
+ // Store in your database
449
+ await prisma.license.create({
450
+ data: {
451
+ externalId: id,
452
+ key: key,
453
+ productId: product_id,
454
+ customerId: customer_id,
455
+ expiresAt: expires_at ? new Date(expires_at) : null,
456
+ status: 'active',
457
+ },
458
+ });
459
+
460
+ // Optional: Send custom email with activation instructions
461
+ await sendLicenseEmail(customer_id, key, product_id);
462
+ }
463
+
464
+ return NextResponse.json({ received: true });
465
+ }
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Server-Side Validation
471
+
472
+ For sensitive operations, validate server-side with your API key:
473
+
474
+ ```typescript
475
+ // app/api/validate-license/route.ts
476
+ import { NextRequest, NextResponse } from 'next/server';
477
+ import DodoPayments from 'dodopayments';
478
+
479
+ const client = new DodoPayments({
480
+ bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
481
+ });
482
+
483
+ export async function POST(req: NextRequest) {
484
+ const { licenseKey } = await req.json();
485
+
486
+ try {
487
+ // Get detailed license info (requires API key)
488
+ const licenses = await client.licenseKeys.list({
489
+ license_key: licenseKey,
490
+ });
491
+
492
+ if (licenses.items.length === 0) {
493
+ return NextResponse.json({ valid: false, error: 'License not found' });
494
+ }
495
+
496
+ const license = licenses.items[0];
497
+
498
+ // Check various conditions
499
+ const valid =
500
+ license.status === 'active' &&
501
+ (!license.expires_at || new Date(license.expires_at) > new Date());
502
+
503
+ return NextResponse.json({
504
+ valid,
505
+ status: license.status,
506
+ activationsUsed: license.activations_count,
507
+ activationsLimit: license.activations_limit,
508
+ expiresAt: license.expires_at,
509
+ });
510
+ } catch (error: any) {
511
+ return NextResponse.json({ valid: false, error: error.message }, { status: 500 });
512
+ }
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ## Best Practices
519
+
520
+ ### 1. Keep Limits Clear
521
+ Choose sensible defaults for expiry and activations based on your product type.
522
+
523
+ ### 2. Guide Users
524
+ Provide precise activation instructions:
525
+ - "Paste the key in Settings → License"
526
+ - "Run: `mycli activate <key>`"
527
+ - Include self-serve documentation links
528
+
529
+ ### 3. Validate Server-Side
530
+ For critical access control, always validate on your server before granting access.
531
+
532
+ ### 4. Handle Offline Gracefully
533
+ Allow a grace period for offline use in desktop/CLI apps.
534
+
535
+ ### 5. Monitor Events
536
+ Use webhooks to detect abuse patterns and automate revocations.
537
+
538
+ ### 6. Provide Easy Deactivation
539
+ Let users deactivate devices themselves to manage their activation slots.
540
+
541
+ ---
542
+
543
+ ## Common Patterns
544
+
545
+ ### Feature Gating
546
+
547
+ ```typescript
548
+ async function canAccessFeature(feature: string, licenseKey: string) {
549
+ const { valid } = await validateLicense(licenseKey);
550
+
551
+ if (!valid) return false;
552
+
553
+ // Map features to license tiers
554
+ const featureTiers = {
555
+ 'basic-export': ['starter', 'pro', 'enterprise'],
556
+ 'advanced-export': ['pro', 'enterprise'],
557
+ 'api-access': ['enterprise'],
558
+ };
559
+
560
+ const license = await getLicenseDetails(licenseKey);
561
+ return featureTiers[feature]?.includes(license.tier);
562
+ }
563
+ ```
564
+
565
+ ### Subscription-Linked Licenses
566
+
567
+ When license is linked to a subscription:
568
+
569
+ ```typescript
570
+ // Handle subscription.cancelled webhook
571
+ if (event.type === 'subscription.cancelled') {
572
+ const { customer_id } = event.data;
573
+
574
+ // Disable associated license keys
575
+ const licenses = await client.licenseKeys.list({ customer_id });
576
+
577
+ for (const license of licenses.items) {
578
+ await client.licenseKeys.update(license.id, {
579
+ status: 'disabled',
580
+ });
581
+ }
582
+ }
583
+ ```
584
+
585
+ ---
586
+
587
+ ## Resources
588
+
589
+ - [License Keys Documentation](https://docs.dodopayments.com/features/license-keys)
590
+ - [API Reference](https://docs.dodopayments.com/api-reference/license-keys)
591
+ - [Video Tutorial](https://www.youtube.com/watch?v=BNuLTXok8dQ)