@digilogiclabs/create-saas-app 2.1.0 → 2.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +975 -891
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/generators/template-generator.d.ts +11 -0
  5. package/dist/generators/template-generator.d.ts.map +1 -1
  6. package/dist/generators/template-generator.js +360 -16
  7. package/dist/generators/template-generator.js.map +1 -1
  8. package/dist/index.js +1837 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/templates/shared/admin/web/app/admin/layout.tsx +34 -0
  11. package/dist/templates/shared/admin/web/components/admin-nav.tsx +48 -0
  12. package/dist/templates/shared/audit/web/lib/audit.ts +24 -0
  13. package/dist/templates/shared/auth/keycloak/web/app/api/auth/federated-logout/route.ts +173 -0
  14. package/dist/templates/shared/auth/keycloak/web/auth.config.ts +84 -0
  15. package/dist/templates/shared/auth/keycloak/web/auth.ts +26 -0
  16. package/dist/templates/shared/beta/web/app/api/beta-settings/route.ts +25 -0
  17. package/dist/templates/shared/beta/web/app/api/validate-beta-code/route.ts +67 -0
  18. package/dist/templates/shared/beta/web/lib/beta/settings.ts +31 -0
  19. package/dist/templates/shared/cache/web/lib/cache.ts +44 -0
  20. package/dist/templates/shared/config/web/lib/config.ts +112 -0
  21. package/dist/templates/shared/config/web/next.config.mjs +62 -0
  22. package/dist/templates/shared/contact/web/app/api/contact/route.ts +113 -0
  23. package/dist/templates/shared/contact/web/app/contact/page.tsx +195 -0
  24. package/dist/templates/shared/cookie-consent/web/components/cookie-consent.tsx +54 -0
  25. package/dist/templates/shared/database/postgresql/web/drizzle.config.ts +16 -0
  26. package/dist/templates/shared/database/postgresql/web/lib/db/drizzle.ts +39 -0
  27. package/dist/templates/shared/database/postgresql/web/lib/db/schema.ts +33 -0
  28. package/dist/templates/shared/database/supabase/web/lib/supabase/client.ts +12 -0
  29. package/dist/templates/shared/database/supabase/web/lib/supabase/server.ts +31 -0
  30. package/dist/templates/shared/database/supabase/web/lib/supabase/service.ts +15 -0
  31. package/dist/templates/shared/email/web/lib/email/branding.ts +18 -0
  32. package/dist/templates/shared/email/web/lib/email/client.ts +96 -0
  33. package/dist/templates/shared/error-pages/web/app/error.tsx +70 -0
  34. package/dist/templates/shared/error-pages/web/app/global-error.tsx +102 -0
  35. package/dist/templates/shared/error-pages/web/app/not-found.tsx +39 -0
  36. package/dist/templates/shared/health/web/app/api/health/route.ts +68 -0
  37. package/dist/templates/shared/legal/web/app/(legal)/privacy/page.tsx +205 -0
  38. package/dist/templates/shared/legal/web/app/(legal)/terms/page.tsx +154 -0
  39. package/dist/templates/shared/legal/web/lib/legal-config.ts +50 -0
  40. package/dist/templates/shared/loading/web/app/loading.tsx +5 -0
  41. package/dist/templates/shared/loading/web/components/skeleton.tsx +95 -0
  42. package/dist/templates/shared/middleware/web/middleware.ts +68 -0
  43. package/dist/templates/shared/observability/web/lib/observability.ts +135 -0
  44. package/dist/templates/shared/payments/web/app/api/webhooks/stripe/route.ts +109 -0
  45. package/dist/templates/shared/platform/web/lib/platform.ts +37 -0
  46. package/dist/templates/shared/redis/web/lib/rate-limit-store.ts +18 -0
  47. package/dist/templates/shared/redis/web/lib/redis.ts +48 -0
  48. package/dist/templates/shared/security/web/lib/api-security.ts +318 -0
  49. package/dist/templates/shared/seo/web/app/api/og/route.tsx +97 -0
  50. package/dist/templates/shared/seo/web/app/robots.ts +53 -0
  51. package/dist/templates/shared/seo/web/app/sitemap.ts +53 -0
  52. package/dist/templates/shared/utils/web/lib/api-response.ts +71 -0
  53. package/dist/templates/shared/utils/web/lib/utils.ts +85 -0
  54. package/package.json +5 -4
  55. package/src/templates/shared/admin/web/app/admin/layout.tsx +34 -0
  56. package/src/templates/shared/admin/web/components/admin-nav.tsx +48 -0
  57. package/src/templates/shared/audit/web/lib/audit.ts +24 -0
  58. package/src/templates/shared/auth/keycloak/web/app/api/auth/federated-logout/route.ts +173 -0
  59. package/src/templates/shared/auth/keycloak/web/auth.config.ts +84 -0
  60. package/src/templates/shared/auth/keycloak/web/auth.ts +26 -0
  61. package/src/templates/shared/beta/web/app/api/beta-settings/route.ts +25 -0
  62. package/src/templates/shared/beta/web/app/api/validate-beta-code/route.ts +67 -0
  63. package/src/templates/shared/beta/web/lib/beta/settings.ts +31 -0
  64. package/src/templates/shared/cache/web/lib/cache.ts +44 -0
  65. package/src/templates/shared/config/web/lib/config.ts +112 -0
  66. package/src/templates/shared/config/web/next.config.mjs +62 -0
  67. package/src/templates/shared/contact/web/app/api/contact/route.ts +113 -0
  68. package/src/templates/shared/contact/web/app/contact/page.tsx +195 -0
  69. package/src/templates/shared/cookie-consent/web/components/cookie-consent.tsx +54 -0
  70. package/src/templates/shared/database/postgresql/web/drizzle.config.ts +16 -0
  71. package/src/templates/shared/database/postgresql/web/lib/db/drizzle.ts +39 -0
  72. package/src/templates/shared/database/postgresql/web/lib/db/schema.ts +33 -0
  73. package/src/templates/shared/database/supabase/web/lib/supabase/client.ts +12 -0
  74. package/src/templates/shared/database/supabase/web/lib/supabase/server.ts +31 -0
  75. package/src/templates/shared/database/supabase/web/lib/supabase/service.ts +15 -0
  76. package/src/templates/shared/email/web/lib/email/branding.ts +18 -0
  77. package/src/templates/shared/email/web/lib/email/client.ts +96 -0
  78. package/src/templates/shared/error-pages/web/app/error.tsx +70 -0
  79. package/src/templates/shared/error-pages/web/app/global-error.tsx +102 -0
  80. package/src/templates/shared/error-pages/web/app/not-found.tsx +39 -0
  81. package/src/templates/shared/health/web/app/api/health/route.ts +68 -0
  82. package/src/templates/shared/legal/web/app/(legal)/privacy/page.tsx +205 -0
  83. package/src/templates/shared/legal/web/app/(legal)/terms/page.tsx +154 -0
  84. package/src/templates/shared/legal/web/lib/legal-config.ts +50 -0
  85. package/src/templates/shared/loading/web/app/loading.tsx +5 -0
  86. package/src/templates/shared/loading/web/components/skeleton.tsx +95 -0
  87. package/src/templates/shared/middleware/web/middleware.ts +68 -0
  88. package/src/templates/shared/observability/web/lib/observability.ts +135 -0
  89. package/src/templates/shared/payments/web/app/api/webhooks/stripe/route.ts +109 -0
  90. package/src/templates/shared/platform/web/lib/platform.ts +37 -0
  91. package/src/templates/shared/redis/web/lib/rate-limit-store.ts +18 -0
  92. package/src/templates/shared/redis/web/lib/redis.ts +48 -0
  93. package/src/templates/shared/security/web/lib/api-security.ts +318 -0
  94. package/src/templates/shared/seo/web/app/api/og/route.tsx +97 -0
  95. package/src/templates/shared/seo/web/app/robots.ts +53 -0
  96. package/src/templates/shared/seo/web/app/sitemap.ts +53 -0
  97. package/src/templates/shared/utils/web/lib/api-response.ts +71 -0
  98. package/src/templates/shared/utils/web/lib/utils.ts +85 -0
  99. package/dist/cli/commands/add.d.ts +0 -6
  100. package/dist/cli/commands/add.d.ts.map +0 -1
  101. package/dist/cli/commands/add.js +0 -39
  102. package/dist/cli/commands/add.js.map +0 -1
  103. package/dist/cli/commands/create.d.ts +0 -45
  104. package/dist/cli/commands/create.d.ts.map +0 -1
  105. package/dist/cli/commands/create.js +0 -175
  106. package/dist/cli/commands/create.js.map +0 -1
  107. package/dist/cli/commands/index.d.ts +0 -4
  108. package/dist/cli/commands/index.d.ts.map +0 -1
  109. package/dist/cli/commands/index.js +0 -20
  110. package/dist/cli/commands/index.js.map +0 -1
  111. package/dist/cli/commands/update.d.ts +0 -6
  112. package/dist/cli/commands/update.d.ts.map +0 -1
  113. package/dist/cli/commands/update.js +0 -68
  114. package/dist/cli/commands/update.js.map +0 -1
  115. package/dist/cli/index.d.ts +0 -4
  116. package/dist/cli/index.d.ts.map +0 -1
  117. package/dist/cli/index.js +0 -61
  118. package/dist/cli/index.js.map +0 -1
  119. package/dist/cli/prompts/index.d.ts +0 -2
  120. package/dist/cli/prompts/index.d.ts.map +0 -1
  121. package/dist/cli/prompts/index.js +0 -18
  122. package/dist/cli/prompts/index.js.map +0 -1
  123. package/dist/cli/prompts/project-setup.d.ts +0 -5
  124. package/dist/cli/prompts/project-setup.d.ts.map +0 -1
  125. package/dist/cli/prompts/project-setup.js +0 -351
  126. package/dist/cli/prompts/project-setup.js.map +0 -1
  127. package/dist/cli/utils/git.d.ts +0 -9
  128. package/dist/cli/utils/git.d.ts.map +0 -1
  129. package/dist/cli/utils/git.js +0 -77
  130. package/dist/cli/utils/git.js.map +0 -1
  131. package/dist/cli/utils/index.d.ts +0 -5
  132. package/dist/cli/utils/index.d.ts.map +0 -1
  133. package/dist/cli/utils/index.js +0 -21
  134. package/dist/cli/utils/index.js.map +0 -1
  135. package/dist/cli/utils/logger.d.ts +0 -16
  136. package/dist/cli/utils/logger.d.ts.map +0 -1
  137. package/dist/cli/utils/logger.js +0 -55
  138. package/dist/cli/utils/logger.js.map +0 -1
  139. package/dist/cli/utils/package-manager.d.ts +0 -8
  140. package/dist/cli/utils/package-manager.d.ts.map +0 -1
  141. package/dist/cli/utils/package-manager.js +0 -92
  142. package/dist/cli/utils/package-manager.js.map +0 -1
  143. package/dist/cli/utils/spinner.d.ts +0 -7
  144. package/dist/cli/utils/spinner.d.ts.map +0 -1
  145. package/dist/cli/utils/spinner.js +0 -48
  146. package/dist/cli/utils/spinner.js.map +0 -1
  147. package/dist/cli/validators/dependencies.d.ts +0 -15
  148. package/dist/cli/validators/dependencies.d.ts.map +0 -1
  149. package/dist/cli/validators/dependencies.js +0 -108
  150. package/dist/cli/validators/dependencies.js.map +0 -1
  151. package/dist/cli/validators/index.d.ts +0 -3
  152. package/dist/cli/validators/index.d.ts.map +0 -1
  153. package/dist/cli/validators/index.js +0 -19
  154. package/dist/cli/validators/index.js.map +0 -1
  155. package/dist/cli/validators/project-name.d.ts +0 -5
  156. package/dist/cli/validators/project-name.d.ts.map +0 -1
  157. package/dist/cli/validators/project-name.js +0 -151
  158. package/dist/cli/validators/project-name.js.map +0 -1
  159. package/dist/generators/file-processor.d.ts +0 -28
  160. package/dist/generators/file-processor.d.ts.map +0 -1
  161. package/dist/generators/file-processor.js +0 -224
  162. package/dist/generators/file-processor.js.map +0 -1
  163. package/dist/generators/index.d.ts +0 -4
  164. package/dist/generators/index.d.ts.map +0 -1
  165. package/dist/generators/index.js +0 -20
  166. package/dist/generators/index.js.map +0 -1
  167. package/dist/generators/package-installer.d.ts +0 -29
  168. package/dist/generators/package-installer.d.ts.map +0 -1
  169. package/dist/generators/package-installer.js +0 -177
  170. package/dist/generators/package-installer.js.map +0 -1
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Shared API security utilities.
3
+ *
4
+ * Built on platform-core/auth building blocks — no local reimplementations.
5
+ * Uses constantTimeEqual, classifyError, rate limiting, and audit from the package.
6
+ *
7
+ * Two usage patterns:
8
+ * 1. Wrappers: withPublicApi / withAuthenticatedApi / withAdminApi (recommended)
9
+ * 2. Primitives: enforceRateLimit, isAdminRequest, errorResponse (manual composition)
10
+ */
11
+ import 'server-only';
12
+ import { NextRequest, NextResponse } from 'next/server';
13
+ import { randomUUID } from 'crypto';
14
+ import {
15
+ // Security primitives
16
+ constantTimeEqual,
17
+ // Rate limiting
18
+ CommonRateLimits,
19
+ type RateLimitRule,
20
+ type RateLimitOptions,
21
+ // Next.js API helpers
22
+ enforceRateLimit as _enforceRateLimit,
23
+ errorResponse,
24
+ zodErrorResponse,
25
+ classifyError,
26
+ } from '@digilogiclabs/platform-core/auth';
27
+
28
+ // Trigger env validation on first import (fail-fast in production)
29
+ import { config } from '@/lib/config';
30
+
31
+ // Re-export for convenience in routes
32
+ export { escapeHtml } from '@digilogiclabs/platform-core/auth';
33
+ export { errorResponse, zodErrorResponse };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Request ID / Correlation ID
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Generate or extract a request ID for correlation. */
40
+ export function getRequestId(request: NextRequest): string {
41
+ return request.headers.get('x-request-id') || randomUUID();
42
+ }
43
+
44
+ /**
45
+ * Rate-limit a request using Redis-backed store (if REDIS_URL is set)
46
+ * or in-memory fallback. Wraps platform-core's enforceRateLimit to
47
+ * automatically inject the store.
48
+ */
49
+ export async function enforceRateLimit(
50
+ request: { headers: { get(name: string): string | null } },
51
+ operation: string,
52
+ rule: RateLimitRule,
53
+ options?: {
54
+ identifier?: string;
55
+ userId?: string;
56
+ rateLimitOptions?: RateLimitOptions;
57
+ }
58
+ ): Promise<Response | null> {
59
+ const { getRateLimitStore } = await import('@/lib/rate-limit-store');
60
+ const store = getRateLimitStore();
61
+ return _enforceRateLimit(request, operation, rule, {
62
+ ...options,
63
+ rateLimitOptions: { ...options?.rateLimitOptions, ...(store ? { store } : {}) },
64
+ });
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Admin / Cron auth helpers
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Extract bearer token from Authorization header. */
72
+ function extractBearerToken(request: NextRequest): string | null {
73
+ const header = request.headers.get('authorization');
74
+ if (!header?.startsWith('Bearer ')) return null;
75
+ return header.slice(7);
76
+ }
77
+
78
+ /** Check if request has a valid admin bearer token (ADMIN_SECRET). Timing-safe. */
79
+ export function isAdminRequest(request: NextRequest): boolean {
80
+ const secret = config.adminSecret;
81
+ if (!secret) return false;
82
+ const token = extractBearerToken(request);
83
+ if (!token) return false;
84
+ return constantTimeEqual(token, secret);
85
+ }
86
+
87
+ /** Check if request has a valid cron bearer token (CRON_SECRET). Timing-safe. */
88
+ export function isCronRequest(request: NextRequest): boolean {
89
+ const secret = config.cronSecret;
90
+ if (!secret) return false;
91
+ const token = extractBearerToken(request);
92
+ if (!token) return false;
93
+ return constantTimeEqual(token, secret);
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Rate limiting presets — tune for your app's traffic patterns
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /** App-specific rate limit presets. Extend or override as needed. */
101
+ export const AppRateLimits = {
102
+ /** Public read endpoints */
103
+ publicRead: { limit: 60, windowSeconds: 60 } satisfies RateLimitRule,
104
+ /** Authenticated mutations */
105
+ authMutation: { limit: 30, windowSeconds: 60 } satisfies RateLimitRule,
106
+ /** Admin endpoints */
107
+ admin: CommonRateLimits.adminAction,
108
+ /** Beta code validation */
109
+ betaValidation: CommonRateLimits.betaValidation,
110
+ /** Webhook endpoints (generous — Stripe retries) */
111
+ webhook: { limit: 100, windowSeconds: 60 } satisfies RateLimitRule,
112
+ } as const;
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // API Wrapper Types
116
+ // ---------------------------------------------------------------------------
117
+
118
+ interface ApiWrapperConfig {
119
+ /** Rate limit rule (defaults to preset based on wrapper type) */
120
+ rateLimit?: RateLimitRule;
121
+ /** Operation name for rate limiting and logging */
122
+ operation?: string;
123
+ }
124
+
125
+ interface AuthenticatedApiContext {
126
+ /** The authenticated session */
127
+ session: { user: { id: string; email: string; roles?: string[] } };
128
+ /** Request correlation ID */
129
+ requestId: string;
130
+ }
131
+
132
+ interface PublicApiContext {
133
+ /** Request correlation ID */
134
+ requestId: string;
135
+ }
136
+
137
+ type PublicApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
138
+
139
+ type AuthenticatedApiHandler = (
140
+ request: NextRequest,
141
+ context: AuthenticatedApiContext
142
+ ) => Promise<NextResponse>;
143
+
144
+ type AdminApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
145
+
146
+ type CronApiHandler = (request: NextRequest, context: PublicApiContext) => Promise<NextResponse>;
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // API Wrappers — compose auth, rate limiting, error handling automatically
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Wrap a public API route with rate limiting and error handling.
154
+ * No authentication required.
155
+ *
156
+ * @example
157
+ * export const GET = withPublicApi({ operation: 'list-items' }, async (req, ctx) => {
158
+ * return NextResponse.json({ items: [] });
159
+ * });
160
+ */
161
+ export function withPublicApi(
162
+ handlerConfig: ApiWrapperConfig,
163
+ handler: PublicApiHandler
164
+ ): (request: NextRequest) => Promise<NextResponse> {
165
+ return async (request: NextRequest) => {
166
+ const requestId = getRequestId(request);
167
+ const operation = handlerConfig.operation || 'public';
168
+
169
+ try {
170
+ // Rate limiting
171
+ const rateLimited = await enforceRateLimit(
172
+ request,
173
+ operation,
174
+ handlerConfig.rateLimit || AppRateLimits.publicRead
175
+ );
176
+ if (rateLimited) return rateLimited as NextResponse;
177
+
178
+ const response = await handler(request, { requestId });
179
+ response.headers.set('x-request-id', requestId);
180
+ return response;
181
+ } catch (error) {
182
+ const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
183
+ return NextResponse.json({ ...body, requestId }, { status });
184
+ }
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Wrap an authenticated API route with session validation, rate limiting, and error handling.
190
+ * Requires a valid Auth.js session.
191
+ *
192
+ * @example
193
+ * export const POST = withAuthenticatedApi({ operation: 'create-item' }, async (req, ctx) => {
194
+ * const { session, requestId } = ctx;
195
+ * return NextResponse.json({ userId: session.user.id });
196
+ * });
197
+ */
198
+ export function withAuthenticatedApi(
199
+ handlerConfig: ApiWrapperConfig,
200
+ handler: AuthenticatedApiHandler
201
+ ): (request: NextRequest) => Promise<NextResponse> {
202
+ return async (request: NextRequest) => {
203
+ const requestId = getRequestId(request);
204
+ const operation = handlerConfig.operation || 'authenticated';
205
+
206
+ try {
207
+ // Dynamic import to avoid Edge runtime issues
208
+ const { auth } = await import('@/auth');
209
+ const session = await auth();
210
+
211
+ if (!session?.user?.id) {
212
+ return NextResponse.json({ error: 'Unauthorized', requestId }, { status: 401 });
213
+ }
214
+
215
+ // Rate limiting (per-user)
216
+ const rateLimited = await enforceRateLimit(
217
+ request,
218
+ operation,
219
+ handlerConfig.rateLimit || AppRateLimits.authMutation,
220
+ { userId: session.user.id }
221
+ );
222
+ if (rateLimited) return rateLimited as NextResponse;
223
+
224
+ const response = await handler(request, {
225
+ session: session as AuthenticatedApiContext['session'],
226
+ requestId,
227
+ });
228
+ response.headers.set('x-request-id', requestId);
229
+ return response;
230
+ } catch (error) {
231
+ const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
232
+ return NextResponse.json({ ...body, requestId }, { status });
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Wrap an admin API route with Bearer token auth, rate limiting, and error handling.
239
+ * Requires ADMIN_SECRET Bearer token.
240
+ *
241
+ * @example
242
+ * export const POST = withAdminApi({ operation: 'admin-action' }, async (req, ctx) => {
243
+ * return NextResponse.json({ success: true });
244
+ * });
245
+ */
246
+ export function withAdminApi(
247
+ handlerConfig: ApiWrapperConfig,
248
+ handler: AdminApiHandler
249
+ ): (request: NextRequest) => Promise<NextResponse> {
250
+ return async (request: NextRequest) => {
251
+ const requestId = getRequestId(request);
252
+ const operation = handlerConfig.operation || 'admin';
253
+
254
+ try {
255
+ if (!isAdminRequest(request)) {
256
+ return NextResponse.json({ error: 'Forbidden', requestId }, { status: 403 });
257
+ }
258
+
259
+ // Rate limiting
260
+ const rateLimited = await enforceRateLimit(
261
+ request,
262
+ operation,
263
+ handlerConfig.rateLimit || AppRateLimits.admin
264
+ );
265
+ if (rateLimited) return rateLimited as NextResponse;
266
+
267
+ const response = await handler(request, { requestId });
268
+ response.headers.set('x-request-id', requestId);
269
+ return response;
270
+ } catch (error) {
271
+ const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
272
+ return NextResponse.json({ ...body, requestId }, { status });
273
+ }
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Wrap a cron/scheduled API route with CRON_SECRET Bearer token auth,
279
+ * falling back to admin session check. Uses generous rate limits since
280
+ * cron jobs are server-to-server.
281
+ *
282
+ * @example
283
+ * export const POST = withCronApi({ operation: 'daily-digest' }, async (req, ctx) => {
284
+ * // Run scheduled task...
285
+ * return NextResponse.json({ processed: 42 });
286
+ * });
287
+ */
288
+ export function withCronApi(
289
+ handlerConfig: ApiWrapperConfig,
290
+ handler: CronApiHandler
291
+ ): (request: NextRequest) => Promise<NextResponse> {
292
+ return async (request: NextRequest) => {
293
+ const requestId = getRequestId(request);
294
+ const operation = handlerConfig.operation || 'cron';
295
+
296
+ try {
297
+ // Accept CRON_SECRET Bearer token or fall back to ADMIN_SECRET
298
+ if (!isCronRequest(request) && !isAdminRequest(request)) {
299
+ return NextResponse.json({ error: 'Forbidden', requestId }, { status: 403 });
300
+ }
301
+
302
+ // Rate limiting (generous — cron jobs are server-to-server)
303
+ const rateLimited = await enforceRateLimit(
304
+ request,
305
+ operation,
306
+ handlerConfig.rateLimit || AppRateLimits.webhook
307
+ );
308
+ if (rateLimited) return rateLimited as NextResponse;
309
+
310
+ const response = await handler(request, { requestId });
311
+ response.headers.set('x-request-id', requestId);
312
+ return response;
313
+ } catch (error) {
314
+ const { status, body } = classifyError(error, process.env.NODE_ENV === 'development');
315
+ return NextResponse.json({ ...body, requestId }, { status });
316
+ }
317
+ };
318
+ }
@@ -0,0 +1,97 @@
1
+ import { ImageResponse } from 'next/og';
2
+ import { NextRequest } from 'next/server';
3
+
4
+ export const runtime = 'edge';
5
+
6
+ /**
7
+ * Dynamic Open Graph image generator.
8
+ *
9
+ * Usage: /api/og?title=My+Page&subtitle=Description+here
10
+ *
11
+ * Customize the colors and branding below to match your app.
12
+ */
13
+
14
+ // ─── Customize these ────────────────────────────────────────
15
+ const APP_NAME = 'My App';
16
+ const BG_FROM = '#1e3a5f';
17
+ const BG_TO = '#0f172a';
18
+ const ACCENT = '#3b82f6';
19
+ // ─────────────────────────────────────────────────────────────
20
+
21
+ export async function GET(request: NextRequest) {
22
+ const { searchParams } = request.nextUrl;
23
+ const title = searchParams.get('title') || APP_NAME;
24
+ const subtitle = searchParams.get('subtitle') || '';
25
+
26
+ return new ImageResponse(
27
+ (
28
+ <div
29
+ style={{
30
+ width: '100%',
31
+ height: '100%',
32
+ display: 'flex',
33
+ flexDirection: 'column',
34
+ justifyContent: 'center',
35
+ padding: '60px 80px',
36
+ background: `linear-gradient(135deg, ${BG_FROM} 0%, ${BG_TO} 100%)`,
37
+ color: 'white',
38
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
39
+ }}
40
+ >
41
+ {/* App branding */}
42
+ <div
43
+ style={{
44
+ display: 'flex',
45
+ alignItems: 'center',
46
+ marginBottom: 40,
47
+ fontSize: 24,
48
+ opacity: 0.8,
49
+ letterSpacing: '0.05em',
50
+ }}
51
+ >
52
+ {APP_NAME}
53
+ </div>
54
+
55
+ {/* Title */}
56
+ <div
57
+ style={{
58
+ fontSize: title.length > 40 ? 48 : 64,
59
+ fontWeight: 700,
60
+ lineHeight: 1.1,
61
+ maxWidth: '90%',
62
+ marginBottom: subtitle ? 24 : 0,
63
+ }}
64
+ >
65
+ {title.length > 80 ? title.slice(0, 77) + '...' : title}
66
+ </div>
67
+
68
+ {/* Subtitle */}
69
+ {subtitle && (
70
+ <div
71
+ style={{
72
+ fontSize: 28,
73
+ opacity: 0.7,
74
+ maxWidth: '80%',
75
+ lineHeight: 1.3,
76
+ }}
77
+ >
78
+ {subtitle.length > 120 ? subtitle.slice(0, 117) + '...' : subtitle}
79
+ </div>
80
+ )}
81
+
82
+ {/* Accent bar */}
83
+ <div
84
+ style={{
85
+ position: 'absolute',
86
+ bottom: 0,
87
+ left: 0,
88
+ right: 0,
89
+ height: 6,
90
+ background: ACCENT,
91
+ }}
92
+ />
93
+ </div>
94
+ ),
95
+ { width: 1200, height: 630 },
96
+ );
97
+ }
@@ -0,0 +1,53 @@
1
+ import { MetadataRoute } from 'next';
2
+
3
+ const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
4
+
5
+ /**
6
+ * Robots.txt configuration.
7
+ *
8
+ * Controls which paths search engines and AI crawlers can access.
9
+ * Adjust the rules to match your app's public and private routes.
10
+ *
11
+ * @see https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots
12
+ */
13
+ export default function robots(): MetadataRoute.Robots {
14
+ return {
15
+ rules: [
16
+ // ─── Main search engines (Google, Bing, etc.) ──────────────
17
+ {
18
+ userAgent: ['Googlebot', 'Bingbot', 'Slurp', 'DuckDuckBot', 'Baiduspider', 'YandexBot'],
19
+ allow: '/',
20
+ disallow: ['/api/', '/dashboard/', '/admin/', '/auth/'],
21
+ },
22
+
23
+ // ─── AI crawlers (allow indexing, block private routes) ────
24
+ {
25
+ userAgent: [
26
+ 'GPTBot',
27
+ 'Google-Extended',
28
+ 'ChatGPT-User',
29
+ 'ClaudeBot',
30
+ 'PerplexityBot',
31
+ 'Amazonbot',
32
+ 'YouBot',
33
+ ],
34
+ allow: '/',
35
+ disallow: ['/api/', '/dashboard/', '/admin/', '/auth/'],
36
+ },
37
+
38
+ // ─── Training-only crawlers (block entirely) ───────────────
39
+ {
40
+ userAgent: ['CCBot', 'anthropic-ai'],
41
+ disallow: '/',
42
+ },
43
+
44
+ // ─── Default (allow public content) ────────────────────────
45
+ {
46
+ userAgent: '*',
47
+ allow: '/',
48
+ disallow: ['/api/', '/dashboard/', '/admin/', '/auth/'],
49
+ },
50
+ ],
51
+ sitemap: `${BASE_URL}/sitemap.xml`,
52
+ };
53
+ }
@@ -0,0 +1,53 @@
1
+ import { MetadataRoute } from 'next';
2
+
3
+ const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
4
+
5
+ /**
6
+ * Dynamic sitemap generator.
7
+ *
8
+ * Generates a sitemap for search engine crawlers.
9
+ * Add your app's dynamic routes (e.g., user profiles, blog posts)
10
+ * alongside the static pages.
11
+ *
12
+ * @see https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap
13
+ */
14
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
15
+ const now = new Date();
16
+
17
+ // ─── Static Pages ────────────────────────────────────────────
18
+ const staticPages: MetadataRoute.Sitemap = [
19
+ {
20
+ url: BASE_URL,
21
+ lastModified: now,
22
+ changeFrequency: 'daily',
23
+ priority: 1.0,
24
+ },
25
+ {
26
+ url: `${BASE_URL}/terms`,
27
+ lastModified: now,
28
+ changeFrequency: 'monthly',
29
+ priority: 0.3,
30
+ },
31
+ {
32
+ url: `${BASE_URL}/privacy`,
33
+ lastModified: now,
34
+ changeFrequency: 'monthly',
35
+ priority: 0.3,
36
+ },
37
+ ];
38
+
39
+ // ─── Dynamic Pages ───────────────────────────────────────────
40
+ // TODO: Add your dynamic routes here. Example:
41
+ //
42
+ // const { data: posts } = await db.from('posts').select('slug, updated_at');
43
+ // const postPages = (posts || []).map((post) => ({
44
+ // url: `${BASE_URL}/blog/${post.slug}`,
45
+ // lastModified: new Date(post.updated_at),
46
+ // changeFrequency: 'weekly' as const,
47
+ // priority: 0.7,
48
+ // }));
49
+ //
50
+ // return [...staticPages, ...postPages];
51
+
52
+ return staticPages;
53
+ }
@@ -0,0 +1,71 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { classifyError, buildPagination } from '@digilogiclabs/platform-core/auth';
3
+
4
+ /**
5
+ * Standardized API response helpers.
6
+ *
7
+ * Provides consistent response shapes across all API routes.
8
+ * Uses classifyError from platform-core for structured error handling.
9
+ */
10
+
11
+ /** Success response with optional status code */
12
+ export function successResponse<T>(data: T, status = 200) {
13
+ return NextResponse.json({ success: true, data }, { status });
14
+ }
15
+
16
+ /** Success response with a message */
17
+ export function successWithMessage<T>(data: T, message: string, status = 200) {
18
+ return NextResponse.json({ success: true, message, data }, { status });
19
+ }
20
+
21
+ /** 201 Created response */
22
+ export function createdResponse<T>(data: T, message = 'Created successfully') {
23
+ return NextResponse.json({ success: true, message, data }, { status: 201 });
24
+ }
25
+
26
+ /** 204 No Content response */
27
+ export function noContentResponse() {
28
+ return new NextResponse(null, { status: 204 });
29
+ }
30
+
31
+ /** 202 Accepted response (for async operations) */
32
+ export function acceptedResponse<T>(data: T, message = 'Request accepted') {
33
+ return NextResponse.json({ success: true, message, data }, { status: 202 });
34
+ }
35
+
36
+ /** Error response with status code */
37
+ export function errorResponse(message: string, status = 500, code?: string) {
38
+ return NextResponse.json(
39
+ { success: false, error: { message, ...(code && { code }) } },
40
+ { status }
41
+ );
42
+ }
43
+
44
+ /** Paginated response with metadata */
45
+ export function paginatedResponse<T>(data: T[], page: number, limit: number, total: number) {
46
+ return NextResponse.json({
47
+ success: true,
48
+ data,
49
+ pagination: buildPagination(page, limit, total),
50
+ });
51
+ }
52
+
53
+ /** Handle any error and return a structured response */
54
+ export function handleApiError(error: unknown) {
55
+ const isDev = process.env.NODE_ENV === 'development';
56
+ const { status, body } = classifyError(error, isDev);
57
+ return NextResponse.json({ success: false, error: body }, { status });
58
+ }
59
+
60
+ /** Wrap a handler with automatic error handling */
61
+ export function withErrorHandler(
62
+ handler: (request: Request) => Promise<NextResponse>
63
+ ): (request: Request) => Promise<NextResponse> {
64
+ return async (request: Request) => {
65
+ try {
66
+ return await handler(request);
67
+ } catch (error) {
68
+ return handleApiError(error);
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,85 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ /** Merge Tailwind classes with conflict resolution */
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ /** Format currency with locale support */
10
+ export function formatCurrency(amount: number, currency = 'USD', locale = 'en-US'): string {
11
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
12
+ }
13
+
14
+ /** Format date with locale support */
15
+ export function formatDate(date: Date | string, options?: Intl.DateTimeFormatOptions): string {
16
+ const d = typeof date === 'string' ? new Date(date) : date;
17
+ return d.toLocaleDateString(
18
+ 'en-US',
19
+ options || { year: 'numeric', month: 'long', day: 'numeric' }
20
+ );
21
+ }
22
+
23
+ /** Get relative time string (e.g., "2 hours ago", "in 3 days") */
24
+ export function getRelativeTime(date: Date | string): string {
25
+ const d = typeof date === 'string' ? new Date(date) : date;
26
+ const now = new Date();
27
+ const diffMs = now.getTime() - d.getTime();
28
+ const diffSec = Math.floor(diffMs / 1000);
29
+ const diffMin = Math.floor(diffSec / 60);
30
+ const diffHour = Math.floor(diffMin / 60);
31
+ const diffDay = Math.floor(diffHour / 24);
32
+
33
+ if (diffSec < 60) return 'just now';
34
+ if (diffMin < 60) return `${diffMin}m ago`;
35
+ if (diffHour < 24) return `${diffHour}h ago`;
36
+ if (diffDay < 7) return `${diffDay}d ago`;
37
+ if (diffDay < 30) return `${Math.floor(diffDay / 7)}w ago`;
38
+ return formatDate(d, {
39
+ month: 'short',
40
+ day: 'numeric',
41
+ year: diffDay > 365 ? 'numeric' : undefined,
42
+ });
43
+ }
44
+
45
+ /** Truncate text with ellipsis */
46
+ export function truncate(text: string, maxLength: number): string {
47
+ if (text.length <= maxLength) return text;
48
+ return text.slice(0, maxLength).trimEnd() + '...';
49
+ }
50
+
51
+ /** Get initials from a name (e.g., "John Doe" → "JD") */
52
+ export function getInitials(name: string, maxChars = 2): string {
53
+ return name
54
+ .split(' ')
55
+ .filter(Boolean)
56
+ .map((part) => part[0]?.toUpperCase() || '')
57
+ .slice(0, maxChars)
58
+ .join('');
59
+ }
60
+
61
+ /** Debounce a function */
62
+ export function debounce<T extends (...args: unknown[]) => unknown>(
63
+ fn: T,
64
+ delayMs: number
65
+ ): (...args: Parameters<T>) => void {
66
+ let timer: ReturnType<typeof setTimeout>;
67
+ return (...args: Parameters<T>) => {
68
+ clearTimeout(timer);
69
+ timer = setTimeout(() => fn(...args), delayMs);
70
+ };
71
+ }
72
+
73
+ /** Sleep for a given number of milliseconds */
74
+ export function sleep(ms: number): Promise<void> {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
77
+
78
+ /** Safely parse JSON, returning null on failure */
79
+ export function safeJsonParse<T = unknown>(json: string): T | null {
80
+ try {
81
+ return JSON.parse(json) as T;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }