@l4yercak3/cli 1.2.15 → 1.2.18

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,377 @@
1
+ /**
2
+ * Webhooks Generator
3
+ * Generates webhook handling utilities for the L4YERCAK3 API
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { ensureDir, writeFileWithBackup, checkFileOverwrite } = require('../../utils/file-utils');
9
+
10
+ class WebhooksGenerator {
11
+ /**
12
+ * Generate the webhooks utility file
13
+ * @param {Object} options - Generation options
14
+ * @returns {Promise<string|null>} - Path to generated file or null if skipped
15
+ */
16
+ async generate(options) {
17
+ const { projectPath, isTypeScript } = options;
18
+
19
+ // Determine output directory
20
+ let outputDir;
21
+ if (fs.existsSync(path.join(projectPath, 'src'))) {
22
+ outputDir = path.join(projectPath, 'src', 'lib', 'l4yercak3');
23
+ } else {
24
+ outputDir = path.join(projectPath, 'lib', 'l4yercak3');
25
+ }
26
+
27
+ ensureDir(outputDir);
28
+
29
+ const extension = isTypeScript ? 'ts' : 'js';
30
+ const outputPath = path.join(outputDir, `webhooks.${extension}`);
31
+
32
+ const action = await checkFileOverwrite(outputPath);
33
+ if (action === 'skip') {
34
+ return null;
35
+ }
36
+
37
+ const content = isTypeScript
38
+ ? this.generateTypeScriptWebhooks()
39
+ : this.generateJavaScriptWebhooks();
40
+
41
+ return writeFileWithBackup(outputPath, content, action);
42
+ }
43
+
44
+ generateTypeScriptWebhooks() {
45
+ return `/**
46
+ * L4YERCAK3 Webhook Utilities
47
+ * Auto-generated by @l4yercak3/cli
48
+ *
49
+ * Utilities for handling webhook events from L4YERCAK3.
50
+ */
51
+
52
+ import { createHmac, timingSafeEqual } from 'crypto';
53
+
54
+ export interface WebhookEvent<T = Record<string, unknown>> {
55
+ id: string;
56
+ type: WebhookEventType;
57
+ timestamp: string;
58
+ data: T;
59
+ organizationId: string;
60
+ }
61
+
62
+ export type WebhookEventType =
63
+ | 'contact.created'
64
+ | 'contact.updated'
65
+ | 'contact.deleted'
66
+ | 'organization.created'
67
+ | 'organization.updated'
68
+ | 'event.created'
69
+ | 'event.updated'
70
+ | 'event.published'
71
+ | 'event.cancelled'
72
+ | 'attendee.registered'
73
+ | 'attendee.checked_in'
74
+ | 'attendee.cancelled'
75
+ | 'form.submitted'
76
+ | 'order.created'
77
+ | 'order.paid'
78
+ | 'order.refunded'
79
+ | 'invoice.created'
80
+ | 'invoice.sent'
81
+ | 'invoice.paid'
82
+ | 'invoice.overdue'
83
+ | 'benefit_claim.submitted'
84
+ | 'benefit_claim.approved'
85
+ | 'benefit_claim.rejected'
86
+ | 'benefit_claim.paid'
87
+ | 'commission.calculated'
88
+ | 'commission.paid'
89
+ | 'certificate.issued';
90
+
91
+ export interface VerifyOptions {
92
+ /** The raw request body as a string or Buffer */
93
+ payload: string | Buffer;
94
+ /** The signature from the X-L4yercak3-Signature header */
95
+ signature: string;
96
+ /** Your webhook secret from the L4YERCAK3 dashboard */
97
+ secret: string;
98
+ /** Tolerance in seconds for timestamp validation (default: 300 = 5 minutes) */
99
+ tolerance?: number;
100
+ }
101
+
102
+ export class WebhookVerificationError extends Error {
103
+ constructor(message: string) {
104
+ super(message);
105
+ this.name = 'WebhookVerificationError';
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Verify a webhook signature and parse the event
111
+ *
112
+ * @example
113
+ * \`\`\`ts
114
+ * // In your Next.js API route
115
+ * import { verifyWebhookSignature, WebhookEvent } from '@/lib/l4yercak3';
116
+ *
117
+ * export async function POST(req: Request) {
118
+ * const payload = await req.text();
119
+ * const signature = req.headers.get('x-l4yercak3-signature') || '';
120
+ *
121
+ * try {
122
+ * const event = verifyWebhookSignature({
123
+ * payload,
124
+ * signature,
125
+ * secret: process.env.L4YERCAK3_WEBHOOK_SECRET!,
126
+ * });
127
+ *
128
+ * switch (event.type) {
129
+ * case 'contact.created':
130
+ * // Handle new contact
131
+ * break;
132
+ * case 'order.paid':
133
+ * // Handle paid order
134
+ * break;
135
+ * }
136
+ *
137
+ * return new Response('OK', { status: 200 });
138
+ * } catch (error) {
139
+ * console.error('Webhook error:', error);
140
+ * return new Response('Invalid signature', { status: 400 });
141
+ * }
142
+ * }
143
+ * \`\`\`
144
+ */
145
+ export function verifyWebhookSignature<T = Record<string, unknown>>(
146
+ options: VerifyOptions
147
+ ): WebhookEvent<T> {
148
+ const { payload, signature, secret, tolerance = 300 } = options;
149
+
150
+ if (!signature) {
151
+ throw new WebhookVerificationError('Missing signature header');
152
+ }
153
+
154
+ // Parse the signature header (format: t=timestamp,v1=signature)
155
+ const parts = signature.split(',').reduce((acc, part) => {
156
+ const [key, value] = part.split('=');
157
+ acc[key] = value;
158
+ return acc;
159
+ }, {} as Record<string, string>);
160
+
161
+ const timestamp = parts['t'];
162
+ const signatureHash = parts['v1'];
163
+
164
+ if (!timestamp || !signatureHash) {
165
+ throw new WebhookVerificationError('Invalid signature format');
166
+ }
167
+
168
+ // Check timestamp tolerance
169
+ const timestampNum = parseInt(timestamp, 10);
170
+ const now = Math.floor(Date.now() / 1000);
171
+ if (Math.abs(now - timestampNum) > tolerance) {
172
+ throw new WebhookVerificationError('Timestamp outside tolerance window');
173
+ }
174
+
175
+ // Compute expected signature
176
+ const payloadStr = typeof payload === 'string' ? payload : payload.toString('utf8');
177
+ const signedPayload = \`\${timestamp}.\${payloadStr}\`;
178
+ const expectedSignature = createHmac('sha256', secret)
179
+ .update(signedPayload)
180
+ .digest('hex');
181
+
182
+ // Constant-time comparison
183
+ const expectedBuffer = Buffer.from(expectedSignature);
184
+ const receivedBuffer = Buffer.from(signatureHash);
185
+
186
+ if (expectedBuffer.length !== receivedBuffer.length) {
187
+ throw new WebhookVerificationError('Invalid signature');
188
+ }
189
+
190
+ if (!timingSafeEqual(expectedBuffer, receivedBuffer)) {
191
+ throw new WebhookVerificationError('Invalid signature');
192
+ }
193
+
194
+ // Parse and return the event
195
+ try {
196
+ return JSON.parse(payloadStr) as WebhookEvent<T>;
197
+ } catch {
198
+ throw new WebhookVerificationError('Invalid JSON payload');
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Type guard helpers for specific webhook events
204
+ */
205
+ export function isContactEvent(event: WebhookEvent): event is WebhookEvent<{ contact: unknown }> {
206
+ return event.type.startsWith('contact.');
207
+ }
208
+
209
+ export function isEventEvent(event: WebhookEvent): event is WebhookEvent<{ event: unknown }> {
210
+ return event.type.startsWith('event.');
211
+ }
212
+
213
+ export function isOrderEvent(event: WebhookEvent): event is WebhookEvent<{ order: unknown }> {
214
+ return event.type.startsWith('order.');
215
+ }
216
+
217
+ export function isInvoiceEvent(event: WebhookEvent): event is WebhookEvent<{ invoice: unknown }> {
218
+ return event.type.startsWith('invoice.');
219
+ }
220
+
221
+ export function isBenefitClaimEvent(event: WebhookEvent): event is WebhookEvent<{ claim: unknown }> {
222
+ return event.type.startsWith('benefit_claim.');
223
+ }
224
+ `;
225
+ }
226
+
227
+ generateJavaScriptWebhooks() {
228
+ return `/**
229
+ * L4YERCAK3 Webhook Utilities
230
+ * Auto-generated by @l4yercak3/cli
231
+ *
232
+ * Utilities for handling webhook events from L4YERCAK3.
233
+ */
234
+
235
+ const { createHmac, timingSafeEqual } = require('crypto');
236
+
237
+ class WebhookVerificationError extends Error {
238
+ constructor(message) {
239
+ super(message);
240
+ this.name = 'WebhookVerificationError';
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Verify a webhook signature and parse the event
246
+ *
247
+ * @param {Object} options
248
+ * @param {string|Buffer} options.payload - The raw request body
249
+ * @param {string} options.signature - The signature from X-L4yercak3-Signature header
250
+ * @param {string} options.secret - Your webhook secret
251
+ * @param {number} [options.tolerance=300] - Timestamp tolerance in seconds
252
+ * @returns {Object} The verified webhook event
253
+ * @throws {WebhookVerificationError} If verification fails
254
+ *
255
+ * @example
256
+ * // In your Next.js API route
257
+ * const { verifyWebhookSignature } = require('@/lib/l4yercak3');
258
+ *
259
+ * export default async function handler(req, res) {
260
+ * const payload = req.body;
261
+ * const signature = req.headers['x-l4yercak3-signature'];
262
+ *
263
+ * try {
264
+ * const event = verifyWebhookSignature({
265
+ * payload: JSON.stringify(payload),
266
+ * signature,
267
+ * secret: process.env.L4YERCAK3_WEBHOOK_SECRET,
268
+ * });
269
+ *
270
+ * switch (event.type) {
271
+ * case 'contact.created':
272
+ * // Handle new contact
273
+ * break;
274
+ * case 'order.paid':
275
+ * // Handle paid order
276
+ * break;
277
+ * }
278
+ *
279
+ * res.status(200).json({ received: true });
280
+ * } catch (error) {
281
+ * console.error('Webhook error:', error);
282
+ * res.status(400).json({ error: 'Invalid signature' });
283
+ * }
284
+ * }
285
+ */
286
+ function verifyWebhookSignature(options) {
287
+ const { payload, signature, secret, tolerance = 300 } = options;
288
+
289
+ if (!signature) {
290
+ throw new WebhookVerificationError('Missing signature header');
291
+ }
292
+
293
+ // Parse the signature header (format: t=timestamp,v1=signature)
294
+ const parts = signature.split(',').reduce((acc, part) => {
295
+ const [key, value] = part.split('=');
296
+ acc[key] = value;
297
+ return acc;
298
+ }, {});
299
+
300
+ const timestamp = parts['t'];
301
+ const signatureHash = parts['v1'];
302
+
303
+ if (!timestamp || !signatureHash) {
304
+ throw new WebhookVerificationError('Invalid signature format');
305
+ }
306
+
307
+ // Check timestamp tolerance
308
+ const timestampNum = parseInt(timestamp, 10);
309
+ const now = Math.floor(Date.now() / 1000);
310
+ if (Math.abs(now - timestampNum) > tolerance) {
311
+ throw new WebhookVerificationError('Timestamp outside tolerance window');
312
+ }
313
+
314
+ // Compute expected signature
315
+ const payloadStr = typeof payload === 'string' ? payload : payload.toString('utf8');
316
+ const signedPayload = \`\${timestamp}.\${payloadStr}\`;
317
+ const expectedSignature = createHmac('sha256', secret)
318
+ .update(signedPayload)
319
+ .digest('hex');
320
+
321
+ // Constant-time comparison
322
+ const expectedBuffer = Buffer.from(expectedSignature);
323
+ const receivedBuffer = Buffer.from(signatureHash);
324
+
325
+ if (expectedBuffer.length !== receivedBuffer.length) {
326
+ throw new WebhookVerificationError('Invalid signature');
327
+ }
328
+
329
+ if (!timingSafeEqual(expectedBuffer, receivedBuffer)) {
330
+ throw new WebhookVerificationError('Invalid signature');
331
+ }
332
+
333
+ // Parse and return the event
334
+ try {
335
+ return JSON.parse(payloadStr);
336
+ } catch {
337
+ throw new WebhookVerificationError('Invalid JSON payload');
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Type guard helpers for specific webhook events
343
+ */
344
+ function isContactEvent(event) {
345
+ return event.type && event.type.startsWith('contact.');
346
+ }
347
+
348
+ function isEventEvent(event) {
349
+ return event.type && event.type.startsWith('event.');
350
+ }
351
+
352
+ function isOrderEvent(event) {
353
+ return event.type && event.type.startsWith('order.');
354
+ }
355
+
356
+ function isInvoiceEvent(event) {
357
+ return event.type && event.type.startsWith('invoice.');
358
+ }
359
+
360
+ function isBenefitClaimEvent(event) {
361
+ return event.type && event.type.startsWith('benefit_claim.');
362
+ }
363
+
364
+ module.exports = {
365
+ verifyWebhookSignature,
366
+ WebhookVerificationError,
367
+ isContactEvent,
368
+ isEventEvent,
369
+ isOrderEvent,
370
+ isInvoiceEvent,
371
+ isBenefitClaimEvent,
372
+ };
373
+ `;
374
+ }
375
+ }
376
+
377
+ module.exports = new WebhooksGenerator();
@@ -5,6 +5,11 @@
5
5
  * Supports multiple frameworks:
6
6
  * - nextjs: Next.js (App Router and Pages Router)
7
7
  * - expo: Expo/React Native
8
+ *
9
+ * Supports multiple integration paths:
10
+ * - quickstart: Full-stack with UI components & database
11
+ * - api-only: Just the typed API client
12
+ * - mcp-assisted: AI-powered custom generation
8
13
  */
9
14
 
10
15
  const apiClientGenerator = require('./api-client-generator');
@@ -12,6 +17,9 @@ const envGenerator = require('./env-generator');
12
17
  const nextauthGenerator = require('./nextauth-generator');
13
18
  const oauthGuideGenerator = require('./oauth-guide-generator');
14
19
  const gitignoreGenerator = require('./gitignore-generator');
20
+ const apiOnlyGenerator = require('./api-only');
21
+ const mcpGuideGenerator = require('./mcp-guide-generator');
22
+ const quickstartGenerator = require('./quickstart');
15
23
 
16
24
  class FileGenerator {
17
25
  /**
@@ -32,6 +40,86 @@ class FileGenerator {
32
40
  * Generate all files based on configuration
33
41
  */
34
42
  async generate(options) {
43
+ const integrationPath = options.integrationPath || 'api-only';
44
+
45
+ // Route to the appropriate generator based on integration path
46
+ switch (integrationPath) {
47
+ case 'api-only':
48
+ return this.generateApiOnly(options);
49
+ case 'mcp-assisted':
50
+ return this.generateMcpAssisted(options);
51
+ case 'quickstart':
52
+ return this.generateQuickStart(options);
53
+ default:
54
+ return this.generateLegacy(options);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * API-Only path: Generate typed API client and types
60
+ */
61
+ async generateApiOnly(options) {
62
+ const results = {
63
+ apiClient: null,
64
+ types: null,
65
+ webhooks: null,
66
+ index: null,
67
+ envFile: null,
68
+ gitignore: null,
69
+ };
70
+
71
+ // Generate the full typed API client package
72
+ const apiOnlyResults = await apiOnlyGenerator.generate(options);
73
+ results.apiClient = apiOnlyResults.client;
74
+ results.types = apiOnlyResults.types;
75
+ results.webhooks = apiOnlyResults.webhooks;
76
+ results.index = apiOnlyResults.index;
77
+
78
+ // Generate environment file
79
+ results.envFile = envGenerator.generate(options);
80
+
81
+ // Update .gitignore
82
+ results.gitignore = gitignoreGenerator.generate(options);
83
+
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * MCP-Assisted path: Generate MCP config and guide
89
+ */
90
+ async generateMcpAssisted(options) {
91
+ const results = {
92
+ mcpConfig: null,
93
+ mcpGuide: null,
94
+ envFile: null,
95
+ gitignore: null,
96
+ };
97
+
98
+ // Generate MCP configuration and guide
99
+ const mcpResults = await mcpGuideGenerator.generate(options);
100
+ results.mcpConfig = mcpResults.config;
101
+ results.mcpGuide = mcpResults.guide;
102
+
103
+ // Generate environment file
104
+ results.envFile = envGenerator.generate(options);
105
+
106
+ // Update .gitignore
107
+ results.gitignore = gitignoreGenerator.generate(options);
108
+
109
+ return results;
110
+ }
111
+
112
+ /**
113
+ * Quick Start path: Full-stack generation with database, hooks, and components
114
+ */
115
+ async generateQuickStart(options) {
116
+ return quickstartGenerator.generate(options);
117
+ }
118
+
119
+ /**
120
+ * Legacy generation (backward compatibility)
121
+ */
122
+ async generateLegacy(options) {
35
123
  const results = {
36
124
  apiClient: null,
37
125
  envFile: null,
@@ -57,12 +145,10 @@ class FileGenerator {
57
145
  if (isNextJs) {
58
146
  results.nextauth = await nextauthGenerator.generate(options);
59
147
  }
60
- // For mobile, OAuth is handled differently (expo-auth-session, etc.)
61
148
  }
62
149
 
63
150
  // Generate OAuth guide if OAuth is enabled
64
151
  if (options.features && options.features.includes('oauth') && options.oauthProviders) {
65
- // Pass framework info so guide can be customized
66
152
  results.oauthGuide = oauthGuideGenerator.generate({
67
153
  ...options,
68
154
  isMobile,