@l4yercak3/cli 1.0.6 → 1.1.1

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,532 @@
1
+ # L4YERCAK3 CLI - Reference Implementation Analysis
2
+
3
+ **Source:** HaffNet L4YerCak3 Frontend (`/.kiro/haffnet-l4yercak3/`)
4
+
5
+ This document analyzes the real-world HaffNet integration to inform CLI code generation.
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ HaffNet is a **medical education platform** (CME courses) built on:
12
+ - **Next.js 16** + TypeScript
13
+ - **Convex** (frontend real-time database + auth)
14
+ - **L4YERCAK3 Backend** (CRM, events, checkout, invoicing)
15
+
16
+ This is the **gold standard** for how external apps should connect to L4YERCAK3.
17
+
18
+ ---
19
+
20
+ ## Architecture Pattern: Dual Database
21
+
22
+ ### The Key Insight
23
+
24
+ HaffNet uses **TWO databases**:
25
+
26
+ 1. **Convex (Frontend)** - Fast, real-time, local to the app
27
+ - User authentication (sessions, passwords)
28
+ - CMS content (page configs, checkout instances)
29
+ - Local user profiles
30
+
31
+ 2. **L4YERCAK3 Backend** - Business logic, shared data
32
+ - CRM contacts
33
+ - Events & products
34
+ - Checkout & transactions
35
+ - Invoicing
36
+ - Workflows
37
+
38
+ ### Why This Pattern?
39
+
40
+ | Concern | Convex (Frontend) | L4YERCAK3 (Backend) |
41
+ |---------|-------------------|---------------------|
42
+ | **Auth speed** | ✅ < 50ms | ❌ ~200ms |
43
+ | **Real-time** | ✅ Native | ❌ Polling |
44
+ | **CRM data** | ❌ Duplication | ✅ Source of truth |
45
+ | **Business logic** | ❌ None | ✅ Workflows |
46
+ | **Invoicing** | ❌ None | ✅ Full system |
47
+ | **Multi-app sharing** | ❌ Isolated | ✅ Central |
48
+
49
+ ### User Sync Pattern
50
+
51
+ ```typescript
52
+ // STEP 1: Create Convex auth user (fast)
53
+ const convexUser = await convexAuth.register({
54
+ email, password, firstName, lastName
55
+ });
56
+
57
+ // STEP 2: Create Backend user (links to CRM)
58
+ const backendUser = await userApi.registerUser({
59
+ email, firstName, lastName,
60
+ convexUserId: convexUser._id // Link for sync
61
+ });
62
+
63
+ // Result: User exists in both systems, linked by IDs
64
+ // convexUser._id ↔ backendUser.frontendUserId
65
+ // backendUser.crmContactId → CRM contact
66
+ ```
67
+
68
+ ---
69
+
70
+ ## API Client Structure
71
+
72
+ The CLI should generate an API client similar to HaffNet's `src/lib/api-client.ts`:
73
+
74
+ ### Core Pattern
75
+
76
+ ```typescript
77
+ // Environment variables
78
+ const API_URL = process.env.NEXT_PUBLIC_API_URL;
79
+ const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
80
+ const ORG_ID = process.env.NEXT_PUBLIC_ORG_ID;
81
+
82
+ // Base fetch wrapper
83
+ async function apiFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
84
+ const response = await fetch(`${API_URL}${endpoint}`, {
85
+ ...options,
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ 'Authorization': `Bearer ${API_KEY}`,
89
+ ...options.headers,
90
+ },
91
+ });
92
+
93
+ if (!response.ok) {
94
+ const error = await response.json();
95
+ throw new Error(error.message || `API Error: ${response.status}`);
96
+ }
97
+
98
+ return response.json();
99
+ }
100
+ ```
101
+
102
+ ### Module Organization
103
+
104
+ ```typescript
105
+ // Event API
106
+ export const eventApi = {
107
+ getEvents(params?: { status?, upcoming?, limit? }),
108
+ getEvent(eventId: string),
109
+ getEventProducts(eventId: string),
110
+ };
111
+
112
+ // Form API
113
+ export const formApi = {
114
+ getPublicForm(formId: string),
115
+ submitForm({ formId, responses, metadata }),
116
+ };
117
+
118
+ // Checkout API (main registration flow)
119
+ export const checkoutApi = {
120
+ submitRegistration(data: RegistrationInput, checkoutInstanceId: string),
121
+ };
122
+
123
+ // Ticket API
124
+ export const ticketApi = {
125
+ getTicket(ticketId: string),
126
+ verifyTicket(ticketId: string),
127
+ };
128
+
129
+ // Transaction API
130
+ export const transactionApi = {
131
+ getTransaction(transactionId: string),
132
+ getTicketByTransaction(transactionId: string),
133
+ };
134
+
135
+ // User API (sync with Backend)
136
+ export const userApi = {
137
+ registerUser(userData: {
138
+ email, firstName, lastName, convexUserId
139
+ }),
140
+ };
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Universal Event Pattern
146
+
147
+ The most important pattern from HaffNet is the **workflow trigger API**:
148
+
149
+ ```typescript
150
+ POST /api/v1/workflows/trigger
151
+ Authorization: Bearer {API_KEY}
152
+
153
+ {
154
+ "trigger": "registration_complete",
155
+ "inputData": {
156
+ "eventType": "seminar_registration",
157
+ "source": "haffnet_website",
158
+ "eventId": "event_xxx",
159
+
160
+ "customerData": {
161
+ "email": "user@example.com",
162
+ "firstName": "John",
163
+ "lastName": "Doe",
164
+ "phone": "+49123456789",
165
+ "organization": "Hospital Name"
166
+ },
167
+
168
+ "formResponses": {
169
+ "specialty": "Cardiology",
170
+ "dietary_requirements": "vegetarian"
171
+ },
172
+
173
+ "transactionData": {
174
+ "productId": "product_xxx",
175
+ "price": 0,
176
+ "currency": "EUR"
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ **Response:**
183
+ ```typescript
184
+ {
185
+ "success": true,
186
+ "ticketId": "ticket_xxx",
187
+ "invoiceId": "invoice_xxx",
188
+ "crmContactId": "contact_xxx",
189
+ "frontendUserId": "user_xxx",
190
+ "isGuestRegistration": true,
191
+ "downloadUrls": {
192
+ "tickets": "https://...",
193
+ "invoice": "https://..."
194
+ }
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Type Definitions to Generate
201
+
202
+ ### Event Type
203
+ ```typescript
204
+ interface Event {
205
+ id: string;
206
+ name: string;
207
+ description?: string;
208
+ subtype: string; // "symposium", "workshop", etc.
209
+ status: string; // "draft", "published", "completed"
210
+
211
+ // Flattened from customProperties
212
+ startDate?: number;
213
+ endDate?: number;
214
+ location?: string;
215
+ capacity?: number;
216
+ registrations?: number;
217
+ registrationFormId?: string;
218
+ checkoutInstanceId?: string;
219
+ agenda?: AgendaItem[];
220
+
221
+ // Legacy nested format
222
+ customProperties?: {
223
+ startDate?: number;
224
+ endDate?: number;
225
+ location?: string;
226
+ venue?: string;
227
+ address?: Address;
228
+ registration?: {
229
+ enabled: boolean;
230
+ openDate: number;
231
+ closeDate: number;
232
+ };
233
+ };
234
+ }
235
+ ```
236
+
237
+ ### Product Type
238
+ ```typescript
239
+ interface Product {
240
+ id: string;
241
+ name: string;
242
+ description: string;
243
+ type: string;
244
+ subtype: string;
245
+ status: string;
246
+ customProperties: {
247
+ price: number;
248
+ currency: string;
249
+ sold: number;
250
+ categoryCode: string;
251
+ categoryLabel: string;
252
+ invoiceConfig?: {
253
+ employerSourceField: string;
254
+ employerMapping: Record<string, string>;
255
+ defaultPaymentTerms: string;
256
+ };
257
+ addons: ProductAddon[];
258
+ };
259
+ }
260
+ ```
261
+
262
+ ### Registration Input
263
+ ```typescript
264
+ interface RegistrationInput {
265
+ eventId: string;
266
+ formId: string;
267
+
268
+ products: Array<{
269
+ productId: string;
270
+ quantity: number;
271
+ }>;
272
+
273
+ customerData: {
274
+ email: string;
275
+ firstName: string;
276
+ lastName: string;
277
+ phone?: string;
278
+ salutation?: string;
279
+ title?: string;
280
+ organization?: string;
281
+ };
282
+
283
+ formResponses: Record<string, unknown>;
284
+
285
+ transactionData: {
286
+ currency: string;
287
+ breakdown: {
288
+ basePrice: number;
289
+ addons?: Array<{
290
+ id: string;
291
+ name: string;
292
+ quantity: number;
293
+ pricePerUnit: number;
294
+ total: number;
295
+ }>;
296
+ subtotal: number;
297
+ tax?: number;
298
+ total: number;
299
+ };
300
+ };
301
+
302
+ metadata?: {
303
+ source: string;
304
+ ipAddress?: string;
305
+ userAgent?: string;
306
+ };
307
+ }
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Files CLI Should Generate
313
+
314
+ Based on HaffNet, the CLI `spread` command should generate:
315
+
316
+ ### 1. API Client (`src/lib/layercake.ts`)
317
+ ```
318
+ - Base fetch wrapper with auth
319
+ - Event API module
320
+ - Form API module
321
+ - Checkout API module
322
+ - Ticket API module
323
+ - Transaction API module
324
+ - User API module (if auth feature selected)
325
+ - Full TypeScript types
326
+ ```
327
+
328
+ ### 2. Environment File (`.env.local`)
329
+ ```bash
330
+ # L4YERCAK3 Backend
331
+ NEXT_PUBLIC_API_URL=https://agreeable-lion-828.convex.site/api/v1
332
+ NEXT_PUBLIC_API_KEY=org_xxx_yyy
333
+ NEXT_PUBLIC_ORG_ID=xxx
334
+
335
+ # Optional: Convex (if dual-db pattern)
336
+ NEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud
337
+ CONVEX_DEPLOYMENT=dev:xxx
338
+ ```
339
+
340
+ ### 3. Auth Integration (`src/lib/layercake-auth.ts`)
341
+ ```typescript
342
+ // Only if OAuth feature selected
343
+ // User sync between frontend auth and Backend CRM
344
+ ```
345
+
346
+ ### 4. Hooks (`src/hooks/useLayercake.ts`)
347
+ ```typescript
348
+ // React hooks for common operations
349
+ useEvents()
350
+ useEvent(id)
351
+ useCheckout()
352
+ ```
353
+
354
+ ### 5. Types (`src/types/layercake.ts`)
355
+ ```typescript
356
+ // All type definitions
357
+ Event, Product, Ticket, Transaction, Form, etc.
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Feature Flags from HaffNet
363
+
364
+ Features that can be enabled/disabled:
365
+
366
+ | Feature | Files Generated |
367
+ |---------|-----------------|
368
+ | **events** | Event API, Event types |
369
+ | **forms** | Form API, Form types |
370
+ | **checkout** | Checkout API, Registration types |
371
+ | **tickets** | Ticket API, Ticket types |
372
+ | **invoicing** | Invoice types (accessed via checkout) |
373
+ | **user-sync** | User API, Auth sync hooks |
374
+ | **crm** | Contact API, CRM types |
375
+
376
+ ---
377
+
378
+ ## Convex CMS Pattern
379
+
380
+ HaffNet uses Convex for CMS content. The CLI could generate:
381
+
382
+ ### convex-client.ts
383
+ ```typescript
384
+ import { useQuery } from 'convex/react';
385
+ import { api } from '@convex/_generated/api';
386
+
387
+ /**
388
+ * Get published page content from CMS
389
+ */
390
+ export async function getPageContent(orgSlug: string, pagePath: string) {
391
+ // Fetch from Convex CMS
392
+ }
393
+
394
+ /**
395
+ * Extract checkout instance ID from page content
396
+ */
397
+ export function getCheckoutInstanceId(pageContent: PageContent): string | null {
398
+ return pageContent?.content?.checkout?.checkoutInstanceId || null;
399
+ }
400
+
401
+ /**
402
+ * Check if page has checkout configured
403
+ */
404
+ export function hasCheckoutConfigured(pageContent: PageContent): boolean {
405
+ return !!getCheckoutInstanceId(pageContent);
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Error Handling Patterns
412
+
413
+ From HaffNet:
414
+
415
+ ```typescript
416
+ // API Error class
417
+ export class ApiError extends Error {
418
+ constructor(
419
+ message: string,
420
+ public code?: string,
421
+ public errors?: Array<{ field: string; message: string }>
422
+ ) {
423
+ super(message);
424
+ this.name = 'ApiError';
425
+ }
426
+ }
427
+
428
+ // Response handling
429
+ async function apiFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
430
+ // Check content type before parsing
431
+ const contentType = response.headers.get('content-type');
432
+ const isJson = contentType?.includes('application/json');
433
+
434
+ if (!response.ok) {
435
+ if (isJson) {
436
+ const errorData = await response.json();
437
+ throw new Error(errorData.message || `API Error: ${response.status}`);
438
+ } else {
439
+ const errorText = await response.text();
440
+ console.error('API Error Response (non-JSON):', errorText.substring(0, 500));
441
+ throw new Error(`API Error: ${response.status}`);
442
+ }
443
+ }
444
+
445
+ if (!isJson) {
446
+ throw new Error('API returned non-JSON response');
447
+ }
448
+
449
+ return response.json();
450
+ }
451
+ ```
452
+
453
+ ---
454
+
455
+ ## Security Practices
456
+
457
+ From HaffNet:
458
+
459
+ 1. **API key in server-side only**
460
+ - Use `NEXT_PUBLIC_` prefix carefully
461
+ - Server actions for sensitive operations
462
+
463
+ 2. **Honeypot spam protection**
464
+ ```typescript
465
+ body: JSON.stringify({
466
+ responses: data.responses,
467
+ bot_trap: '', // Must be empty for legitimate submissions
468
+ })
469
+ ```
470
+
471
+ 3. **Rate limiting awareness**
472
+ - Document: 5 submissions/hour per IP for public forms
473
+
474
+ 4. **Input validation**
475
+ - Zod schemas before API calls
476
+ - Field-level error messages
477
+
478
+ ---
479
+
480
+ ## Documentation Structure
481
+
482
+ HaffNet has 25+ documentation files. Key ones:
483
+
484
+ | Document | Purpose |
485
+ |----------|---------|
486
+ | `README.md` | Overview & navigation |
487
+ | `QUICK_REFERENCE.md` | One-page cheat sheet |
488
+ | `API_SPECIFICATION.md` | Complete API reference |
489
+ | `UNIVERSAL_EVENT_PAYLOAD.md` | Event structure |
490
+ | `FRONTEND_INTEGRATION.md` | Code examples |
491
+ | `USER_SYNC_ARCHITECTURE.md` | Dual-DB pattern |
492
+
493
+ **CLI should generate similar docs for each project.**
494
+
495
+ ---
496
+
497
+ ## CLI Generation Templates
498
+
499
+ Based on HaffNet, create templates:
500
+
501
+ ### Template: `api-client.template.ts`
502
+ Full API client with all modules
503
+
504
+ ### Template: `types.template.ts`
505
+ All TypeScript type definitions
506
+
507
+ ### Template: `hooks.template.ts`
508
+ React hooks for data fetching
509
+
510
+ ### Template: `auth-sync.template.ts`
511
+ User sync between auth systems
512
+
513
+ ### Template: `README.template.md`
514
+ Project-specific documentation
515
+
516
+ ---
517
+
518
+ ## Summary: What CLI Should Do
519
+
520
+ 1. **Detect** existing project structure (Next.js, Convex, etc.)
521
+ 2. **Ask** which features to enable (events, forms, checkout, etc.)
522
+ 3. **Generate** API client with only needed modules
523
+ 4. **Generate** TypeScript types for enabled features
524
+ 5. **Generate** hooks for common operations
525
+ 6. **Generate** auth sync if user-sync enabled
526
+ 7. **Create** environment template
527
+ 8. **Create** documentation for the integration
528
+ 9. **Register** the connection in L4YERCAK3 backend
529
+
530
+ ---
531
+
532
+ *This analysis based on production code from HaffNet L4YerCak3 Frontend*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l4yercak3/cli",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Icing on the L4yercak3 - The sweet finishing touch for your Layer Cake integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -167,6 +167,68 @@ class BackendClient {
167
167
  name,
168
168
  });
169
169
  }
170
+
171
+ // ============================================
172
+ // Connected Applications API
173
+ // ============================================
174
+
175
+ /**
176
+ * Check if an application already exists for this project path
177
+ * @param {string} organizationId - The organization ID
178
+ * @param {string} projectPathHash - SHA256 hash of the project path
179
+ * @returns {Promise<{found: boolean, application?: object}>}
180
+ */
181
+ async checkExistingApplication(organizationId, projectPathHash) {
182
+ try {
183
+ return await this.request(
184
+ 'GET',
185
+ `/api/v1/cli/applications/by-path?organizationId=${organizationId}&hash=${projectPathHash}`
186
+ );
187
+ } catch (error) {
188
+ // If 404, no existing app found
189
+ if (error.status === 404) {
190
+ return { found: false };
191
+ }
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Register a new connected application
198
+ * @param {object} data - Application registration data
199
+ * @returns {Promise<{applicationId: string, apiKey?: object, backendUrl: string}>}
200
+ */
201
+ async registerApplication(data) {
202
+ return await this.request('POST', '/api/v1/cli/applications', data);
203
+ }
204
+
205
+ /**
206
+ * Update an existing connected application
207
+ * @param {string} applicationId - The application ID
208
+ * @param {object} updates - Fields to update
209
+ * @returns {Promise<object>}
210
+ */
211
+ async updateApplication(applicationId, updates) {
212
+ return await this.request('PATCH', `/api/v1/cli/applications/${applicationId}`, updates);
213
+ }
214
+
215
+ /**
216
+ * Get application details
217
+ * @param {string} applicationId - The application ID
218
+ * @returns {Promise<object>}
219
+ */
220
+ async getApplication(applicationId) {
221
+ return await this.request('GET', `/api/v1/cli/applications/${applicationId}`);
222
+ }
223
+
224
+ /**
225
+ * List all connected applications for an organization
226
+ * @param {string} organizationId - The organization ID
227
+ * @returns {Promise<{applications: object[]}>}
228
+ */
229
+ async listApplications(organizationId) {
230
+ return await this.request('GET', `/api/v1/cli/applications?organizationId=${organizationId}`);
231
+ }
170
232
  }
171
233
 
172
234
  module.exports = new BackendClient();