anhedral 0.1.1 → 0.1.4
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/README.md +145 -35
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +67 -0
- package/dist/commands.d.ts +3 -8
- package/dist/commands.js +14 -64
- package/dist/dependencies.d.ts +35 -0
- package/dist/dependencies.js +175 -0
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +746 -419
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +117 -195
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +266 -68
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +86 -173
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +5 -16
- package/dist/util.d.ts +1 -0
- package/dist/util.js +65 -8
- package/package.json +12 -13
- package/dist/templates/default-web.d.ts +0 -2
- package/dist/templates/default-web.js +0 -1936
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ProjectOptions } from '../scaffold.js';
|
|
2
|
-
export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl }: ProjectOptions): Promise<void>;
|
|
2
|
+
export declare function scaffoldBackend(root: string, { projectName, displayName, frontendUrl, skipInstall }: ProjectOptions): Promise<void>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { writeFile,
|
|
2
|
+
import { writeFile, execCommand } from '../util.js';
|
|
3
|
+
import { anhedralPrint } from '../print.js';
|
|
3
4
|
import { getBackendInstallCommands } from '../commands.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { BACKEND_DEPENDENCIES } from '../dependencies.js';
|
|
6
|
+
export async function scaffoldBackend(root, { projectName, displayName, frontendUrl, skipInstall }) {
|
|
7
|
+
const dir = path.join(root, 'apps/api');
|
|
8
|
+
anhedralPrint.section('Backend (Fastify)');
|
|
9
|
+
anhedralPrint.step('Writing backend source files');
|
|
7
10
|
writePackageJson(dir, projectName);
|
|
8
11
|
writeTsConfig(dir);
|
|
9
12
|
writeEslintConfig(dir);
|
|
@@ -12,7 +15,6 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
|
|
|
12
15
|
writeEnvFile(dir, frontendUrl);
|
|
13
16
|
writeDrizzleConfig(dir);
|
|
14
17
|
writeVitestConfig(dir);
|
|
15
|
-
writeVercelJson(dir);
|
|
16
18
|
writeTypes(dir);
|
|
17
19
|
writeDbFiles(dir);
|
|
18
20
|
writeErrorFiles(dir);
|
|
@@ -23,10 +25,19 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
|
|
|
23
25
|
writeRoutes(dir, displayName);
|
|
24
26
|
writeAppAndIndex(dir, displayName);
|
|
25
27
|
writeVercelEntry(dir);
|
|
28
|
+
writeVercelConfig(dir);
|
|
26
29
|
writeTestFiles(dir);
|
|
30
|
+
anhedralPrint.done('Backend source files written');
|
|
31
|
+
if (skipInstall) {
|
|
32
|
+
anhedralPrint.info('Skipping backend dependency install (--skip-install)');
|
|
33
|
+
anhedralPrint.info('Run after init: pnpm install');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
anhedralPrint.step('Installing backend dependencies');
|
|
27
37
|
for (const command of getBackendInstallCommands()) {
|
|
28
|
-
|
|
38
|
+
execCommand(command.command, command.args, dir);
|
|
29
39
|
}
|
|
40
|
+
anhedralPrint.done('Backend dependencies installed');
|
|
30
41
|
}
|
|
31
42
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
43
|
// Config files
|
|
@@ -42,7 +53,7 @@ function writePackageJson(dir, projectName) {
|
|
|
42
53
|
build: 'pnpm typecheck',
|
|
43
54
|
typecheck: 'tsc --noEmit',
|
|
44
55
|
'db:generate': 'drizzle-kit generate',
|
|
45
|
-
'db:migrate': 'pnpm
|
|
56
|
+
'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
|
|
46
57
|
'db:studio': 'drizzle-kit studio',
|
|
47
58
|
'db:push': 'drizzle-kit push',
|
|
48
59
|
lint: 'eslint . --ext .js,.ts',
|
|
@@ -51,14 +62,16 @@ function writePackageJson(dir, projectName) {
|
|
|
51
62
|
},
|
|
52
63
|
keywords: [],
|
|
53
64
|
license: 'MIT',
|
|
65
|
+
dependencies: BACKEND_DEPENDENCIES.dependencies,
|
|
66
|
+
devDependencies: BACKEND_DEPENDENCIES.devDependencies,
|
|
54
67
|
}, null, 2) + '\n');
|
|
55
68
|
}
|
|
56
69
|
function writeTsConfig(dir) {
|
|
57
70
|
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
58
71
|
compilerOptions: {
|
|
59
72
|
target: 'ESNext',
|
|
60
|
-
module: '
|
|
61
|
-
moduleResolution: '
|
|
73
|
+
module: 'ESNext',
|
|
74
|
+
moduleResolution: 'Bundler',
|
|
62
75
|
skipLibCheck: true,
|
|
63
76
|
rootDir: '.',
|
|
64
77
|
outDir: 'dist',
|
|
@@ -104,6 +117,7 @@ function writeEnvExample(dir, frontendUrl = 'http://localhost:8081') {
|
|
|
104
117
|
PORT=8787
|
|
105
118
|
NODE_ENV=development
|
|
106
119
|
LOG_LEVEL=info
|
|
120
|
+
ANHEDRAL_DEMO=false
|
|
107
121
|
|
|
108
122
|
# Database (NeonDB)
|
|
109
123
|
DATABASE_URL="postgresql://neondb_owner:***@***.neon.tech/neondb?sslmode=require"
|
|
@@ -134,6 +148,7 @@ function writeEnvFile(dir, frontendUrl = 'http://localhost:8081') {
|
|
|
134
148
|
PORT=8787
|
|
135
149
|
NODE_ENV=development
|
|
136
150
|
LOG_LEVEL=info
|
|
151
|
+
ANHEDRAL_DEMO=true
|
|
137
152
|
|
|
138
153
|
# Database (NeonDB)
|
|
139
154
|
DATABASE_URL="postgresql://user:pass@localhost:5432/app?sslmode=disable"
|
|
@@ -162,8 +177,8 @@ RC_OFFERING_ID=default
|
|
|
162
177
|
function writeDrizzleConfig(dir) {
|
|
163
178
|
writeFile(path.join(dir, 'drizzle.config.ts'), `import type { Config } from 'drizzle-kit';
|
|
164
179
|
export default {
|
|
165
|
-
schema: '
|
|
166
|
-
out: '
|
|
180
|
+
schema: '../../packages/db/src/schema.ts',
|
|
181
|
+
out: '../../packages/db/migrations',
|
|
167
182
|
dialect: 'postgresql',
|
|
168
183
|
dbCredentials: { url: process.env.DATABASE_URL! }
|
|
169
184
|
} satisfies Config;
|
|
@@ -184,13 +199,6 @@ export default defineConfig({
|
|
|
184
199
|
});
|
|
185
200
|
`);
|
|
186
201
|
}
|
|
187
|
-
function writeVercelJson(dir) {
|
|
188
|
-
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
189
|
-
version: 2,
|
|
190
|
-
builds: [{ src: 'api/index.ts', use: '@vercel/node' }],
|
|
191
|
-
routes: [{ src: '/(.*)', dest: 'api/index.ts' }],
|
|
192
|
-
}, null, 2) + '\n');
|
|
193
|
-
}
|
|
194
202
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
203
|
// Types
|
|
196
204
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -207,6 +215,7 @@ export interface AppEnv {
|
|
|
207
215
|
PORT: number;
|
|
208
216
|
NODE_ENV: string;
|
|
209
217
|
LOG_LEVEL: string;
|
|
218
|
+
ANHEDRAL_DEMO?: string | null;
|
|
210
219
|
CLERK_PUBLISHABLE_KEY?: string | null;
|
|
211
220
|
CLERK_SECRET_KEY?: string | null;
|
|
212
221
|
FRONTEND_URL?: string | null;
|
|
@@ -265,183 +274,12 @@ export {};
|
|
|
265
274
|
// Database
|
|
266
275
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
267
276
|
function writeDbFiles(dir) {
|
|
268
|
-
writeFile(path.join(dir, 'src/db/index.ts'), `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!process.env.DATABASE_URL) {
|
|
272
|
-
throw new Error('DATABASE_URL is not defined');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const sql = neon(process.env.DATABASE_URL);
|
|
276
|
-
export const db = drizzle(sql);
|
|
277
|
-
export type Database = typeof db;
|
|
277
|
+
writeFile(path.join(dir, 'src/db/index.ts'), `export { db } from '@anhedral/db';
|
|
278
|
+
export type { Database } from '@anhedral/db';
|
|
278
279
|
`);
|
|
279
|
-
writeFile(path.join(dir, 'src/db/schema.ts'), `
|
|
280
|
-
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
|
281
|
-
|
|
282
|
-
export const SUBSCRIPTION_TIERS = ['free', 'pro'] as const;
|
|
283
|
-
export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number];
|
|
284
|
-
|
|
285
|
-
export const SUBSCRIPTION_STATUSES = ['active', 'expired', 'canceled'] as const;
|
|
286
|
-
export type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
|
|
287
|
-
|
|
288
|
-
export const SUBSCRIPTION_METHODS = ['trialing', 'redeemed', 'paid'] as const;
|
|
289
|
-
export type SubscriptionMethod = (typeof SUBSCRIPTION_METHODS)[number];
|
|
290
|
-
|
|
291
|
-
export const SUBSCRIPTION_ORIGINS = ['web', 'apple', 'google'] as const;
|
|
292
|
-
export type SubscriptionOrigin = (typeof SUBSCRIPTION_ORIGINS)[number];
|
|
293
|
-
|
|
294
|
-
export const SUBSCRIPTION_EVENT_TYPES = [
|
|
295
|
-
'trial_started', 'trial_converted', 'trial_expired',
|
|
296
|
-
'initial_purchase', 'renewal', 'product_change',
|
|
297
|
-
'cancellation_scheduled', 'cancellation_unscheduled', 'subscription_expired', 'subscription_canceled',
|
|
298
|
-
'promo_redeemed', 'billing_issue', 'billing_recovered',
|
|
299
|
-
] as const;
|
|
300
|
-
export type SubscriptionEventType = (typeof SUBSCRIPTION_EVENT_TYPES)[number];
|
|
301
|
-
|
|
302
|
-
export type SubscriptionMetadata = {
|
|
303
|
-
revenueCatProductId?: string; lastWebhookUpdate?: string; cancelReason?: string;
|
|
304
|
-
redeemCode?: string; redeemCodeRedeemedAt?: string;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
export type SubscriptionEventMetadata = {
|
|
308
|
-
revenueCatEventType?: string; revenueCatProductId?: string; promoCode?: string;
|
|
309
|
-
billingPeriod?: string; price?: { amount: number; currency: string };
|
|
310
|
-
store?: string; transactionId?: string; reason?: string;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
export const PROMO_CODE_DURATIONS = [1, 6, 12] as const;
|
|
314
|
-
export type PromoCodeDuration = (typeof PROMO_CODE_DURATIONS)[number];
|
|
315
|
-
|
|
316
|
-
export const users = pgTable('users', {
|
|
317
|
-
id: text('id').primaryKey(),
|
|
318
|
-
email: text('email').notNull().unique(),
|
|
319
|
-
displayName: text('display_name'),
|
|
320
|
-
lastLoginAt: timestamp('last_login_at'),
|
|
321
|
-
profileImageUrl: text('profile_image_url'),
|
|
322
|
-
avatarObjectKey: text('avatar_object_key'),
|
|
323
|
-
avatarMimeType: text('avatar_mime_type'),
|
|
324
|
-
creditsBalance: integer('credits_balance').notNull().default(250),
|
|
325
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
export const subscriptions = pgTable('subscriptions', {
|
|
329
|
-
id: text('id').primaryKey(),
|
|
330
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }).unique(),
|
|
331
|
-
tier: text('tier').$type<SubscriptionTier>().notNull().default('free'),
|
|
332
|
-
status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
|
|
333
|
-
method: text('method').$type<SubscriptionMethod>(),
|
|
334
|
-
origin: text('origin').$type<SubscriptionOrigin>(),
|
|
335
|
-
billingPeriod: text('billing_period'),
|
|
336
|
-
currentPeriodStart: timestamp('current_period_start'),
|
|
337
|
-
currentPeriodEnd: timestamp('current_period_end'),
|
|
338
|
-
canceledAt: timestamp('canceled_at'),
|
|
339
|
-
cancelAtPeriodEnd: boolean('cancel_at_period_end').notNull().default(false),
|
|
340
|
-
trialStart: timestamp('trial_start'),
|
|
341
|
-
trialEnd: timestamp('trial_end'),
|
|
342
|
-
dailyLimit: integer('daily_limit'),
|
|
343
|
-
metadata: jsonb('metadata').$type<SubscriptionMetadata>(),
|
|
344
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
345
|
-
updatedAt: timestamp('updated_at').$defaultFn(() => new Date()).notNull(),
|
|
346
|
-
}, (t) => [
|
|
347
|
-
index('subscriptions_user_idx').on(t.userId),
|
|
348
|
-
index('subscriptions_status_idx').on(t.status),
|
|
349
|
-
index('subscriptions_tier_idx').on(t.tier),
|
|
350
|
-
index('subscriptions_period_end_idx').on(t.currentPeriodEnd),
|
|
351
|
-
]);
|
|
352
|
-
|
|
353
|
-
export const subscriptionEvents = pgTable('subscription_events', {
|
|
354
|
-
id: text('id').primaryKey(),
|
|
355
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
356
|
-
subscriptionId: text('subscription_id').references(() => subscriptions.id, { onDelete: 'set null' }),
|
|
357
|
-
eventType: text('event_type').$type<SubscriptionEventType>().notNull(),
|
|
358
|
-
previousTier: text('previous_tier').$type<SubscriptionTier>(),
|
|
359
|
-
previousStatus: text('previous_status').$type<SubscriptionStatus>(),
|
|
360
|
-
previousMethod: text('previous_method').$type<SubscriptionMethod>(),
|
|
361
|
-
newTier: text('new_tier').$type<SubscriptionTier>(),
|
|
362
|
-
newStatus: text('new_status').$type<SubscriptionStatus>(),
|
|
363
|
-
newMethod: text('new_method').$type<SubscriptionMethod>(),
|
|
364
|
-
revenueCatEventType: text('revenuecat_event_type'),
|
|
365
|
-
revenueCatProductId: text('revenuecat_product_id'),
|
|
366
|
-
origin: text('origin').$type<SubscriptionOrigin>(),
|
|
367
|
-
periodStart: timestamp('period_start'),
|
|
368
|
-
periodEnd: timestamp('period_end'),
|
|
369
|
-
metadata: jsonb('metadata').$type<SubscriptionEventMetadata>(),
|
|
370
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
371
|
-
}, (t) => [
|
|
372
|
-
index('sub_events_user_idx').on(t.userId),
|
|
373
|
-
index('sub_events_user_created_idx').on(t.userId, t.createdAt),
|
|
374
|
-
index('sub_events_type_idx').on(t.eventType),
|
|
375
|
-
]);
|
|
376
|
-
|
|
377
|
-
export const trialClaims = pgTable('trial_claims', {
|
|
378
|
-
id: text('id').primaryKey(),
|
|
379
|
-
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
380
|
-
email: text('email').notNull().unique(),
|
|
381
|
-
claimedAt: timestamp('claimed_at').$defaultFn(() => new Date()).notNull(),
|
|
382
|
-
}, (t) => [index('trial_claims_email_idx').on(t.email)]);
|
|
383
|
-
|
|
384
|
-
export const promoCodes = pgTable('promo_codes', {
|
|
385
|
-
id: text('id').primaryKey(),
|
|
386
|
-
code: text('code').notNull().unique(),
|
|
387
|
-
months: integer('months').$type<PromoCodeDuration>().notNull(),
|
|
388
|
-
maxRedemptions: integer('max_redemptions').notNull().default(1),
|
|
389
|
-
redeemedCount: integer('redeemed_count').notNull().default(0),
|
|
390
|
-
expiresAt: timestamp('expires_at'),
|
|
391
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
392
|
-
}, (t) => [index('promo_codes_code_idx').on(t.code)]);
|
|
393
|
-
|
|
394
|
-
export const promoRedemptions = pgTable('promo_redemptions', {
|
|
395
|
-
id: text('id').primaryKey(),
|
|
396
|
-
promoCodeId: text('promo_code_id').notNull().references(() => promoCodes.id, { onDelete: 'cascade' }),
|
|
397
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
398
|
-
redeemedAt: timestamp('redeemed_at').$defaultFn(() => new Date()).notNull(),
|
|
399
|
-
entitlementExpiresAt: timestamp('entitlement_expires_at').notNull(),
|
|
400
|
-
}, (t) => [
|
|
401
|
-
index('promo_redemptions_user_idx').on(t.userId),
|
|
402
|
-
index('promo_redemptions_code_idx').on(t.promoCodeId),
|
|
403
|
-
]);
|
|
404
|
-
|
|
405
|
-
export type Users = InferSelectModel<typeof users>;
|
|
406
|
-
export type NewUsers = InferInsertModel<typeof users>;
|
|
407
|
-
export type Subscriptions = InferSelectModel<typeof subscriptions>;
|
|
408
|
-
export type NewSubscriptions = InferInsertModel<typeof subscriptions>;
|
|
409
|
-
export type SubscriptionEvents = InferSelectModel<typeof subscriptionEvents>;
|
|
410
|
-
export type NewSubscriptionEvents = InferInsertModel<typeof subscriptionEvents>;
|
|
411
|
-
export type TrialClaims = InferSelectModel<typeof trialClaims>;
|
|
412
|
-
export type NewTrialClaims = InferInsertModel<typeof trialClaims>;
|
|
413
|
-
export type PromoCodes = InferSelectModel<typeof promoCodes>;
|
|
414
|
-
export type NewPromoCodes = InferInsertModel<typeof promoCodes>;
|
|
415
|
-
export type PromoRedemptions = InferSelectModel<typeof promoRedemptions>;
|
|
416
|
-
export type NewPromoRedemptions = InferInsertModel<typeof promoRedemptions>;
|
|
280
|
+
writeFile(path.join(dir, 'src/db/schema.ts'), `export * from '@anhedral/db/schema';
|
|
417
281
|
`);
|
|
418
|
-
writeFile(path.join(dir, 'src/db/migrate.ts'), `import
|
|
419
|
-
import { migrate } from 'drizzle-orm/neon-http/migrator';
|
|
420
|
-
import { neon } from '@neondatabase/serverless';
|
|
421
|
-
import dotenv from 'dotenv';
|
|
422
|
-
|
|
423
|
-
dotenv.config({ quiet: true });
|
|
424
|
-
|
|
425
|
-
const runMigrate = async () => {
|
|
426
|
-
if (!process.env.DATABASE_URL) {
|
|
427
|
-
throw new Error('DATABASE_URL is not defined');
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const client = neon(process.env.DATABASE_URL);
|
|
431
|
-
const db = drizzle(client);
|
|
432
|
-
|
|
433
|
-
console.log('⏳ Running migrations...');
|
|
434
|
-
const start = Date.now();
|
|
435
|
-
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
|
436
|
-
console.log('✅ Migrations completed in', Date.now() - start, 'ms');
|
|
437
|
-
process.exit(0);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
runMigrate().catch((err) => {
|
|
441
|
-
console.error('❌ Migration failed');
|
|
442
|
-
console.error(err);
|
|
443
|
-
process.exit(1);
|
|
444
|
-
});
|
|
282
|
+
writeFile(path.join(dir, 'src/db/migrate.ts'), `import '@anhedral/db/migrate';
|
|
445
283
|
`);
|
|
446
284
|
}
|
|
447
285
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1297,6 +1135,7 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1297
1135
|
PORT: { type: 'number' },
|
|
1298
1136
|
NODE_ENV: { type: 'string', default: 'development' },
|
|
1299
1137
|
LOG_LEVEL: { type: 'string', default: 'info' },
|
|
1138
|
+
ANHEDRAL_DEMO: { type: 'string', default: 'false' },
|
|
1300
1139
|
CLERK_PUBLISHABLE_KEY: { type: 'string', nullable: true },
|
|
1301
1140
|
CLERK_SECRET_KEY: { type: 'string', nullable: true },
|
|
1302
1141
|
FRONTEND_URL: { type: 'string', nullable: true },
|
|
@@ -1306,8 +1145,8 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1306
1145
|
R2_ACCESS_KEY_ID: { type: 'string', nullable: true },
|
|
1307
1146
|
R2_SECRET_ACCESS_KEY: { type: 'string', nullable: true },
|
|
1308
1147
|
R2_BUCKET: { type: 'string', nullable: true },
|
|
1309
|
-
RC_SECRET_API_KEY: { type: 'string', default: '' },
|
|
1310
|
-
RC_WEBHOOK_SECRET: { type: 'string', default: '' },
|
|
1148
|
+
RC_SECRET_API_KEY: { type: 'string', nullable: true, default: '' },
|
|
1149
|
+
RC_WEBHOOK_SECRET: { type: 'string', nullable: true, default: '' },
|
|
1311
1150
|
RC_ENTITLEMENT_ID: { type: 'string', default: 'pro' },
|
|
1312
1151
|
RC_OFFERING_ID: { type: 'string', default: 'default' },
|
|
1313
1152
|
},
|
|
@@ -1318,6 +1157,23 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1318
1157
|
dotenv: !process.env.VERCEL,
|
|
1319
1158
|
confKey: 'env',
|
|
1320
1159
|
} as unknown as Record<string, unknown>);
|
|
1160
|
+
|
|
1161
|
+
if (fastify.env.NODE_ENV === 'production') {
|
|
1162
|
+
if (fastify.env.ANHEDRAL_DEMO === 'true') {
|
|
1163
|
+
throw new Error('ANHEDRAL_DEMO must be false in production');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const required = [
|
|
1167
|
+
'CLERK_PUBLISHABLE_KEY',
|
|
1168
|
+
'CLERK_SECRET_KEY',
|
|
1169
|
+
'RC_SECRET_API_KEY',
|
|
1170
|
+
'RC_WEBHOOK_SECRET',
|
|
1171
|
+
] as const;
|
|
1172
|
+
const missing = required.filter((key) => !fastify.env[key]);
|
|
1173
|
+
if (missing.length > 0) {
|
|
1174
|
+
throw new Error(\`Missing production environment variables: \${missing.join(', ')}\`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1321
1177
|
};
|
|
1322
1178
|
|
|
1323
1179
|
export default fp(configPlugin, { name: 'env-config', fastify: '5.x' });
|
|
@@ -1366,6 +1222,15 @@ export const clerkAuthPlugin: FastifyPluginAsync = async (fastify: FastifyInstan
|
|
|
1366
1222
|
const authenticate = async (req: FastifyRequest, _reply: FastifyReply) => {
|
|
1367
1223
|
if (req.method === 'OPTIONS' || req.url.startsWith('/health')) return;
|
|
1368
1224
|
|
|
1225
|
+
if (fastify.env?.ANHEDRAL_DEMO === 'true') {
|
|
1226
|
+
req.user = {
|
|
1227
|
+
id: 'user_demo',
|
|
1228
|
+
subscriptionTier: 'pro',
|
|
1229
|
+
subscriptionStatus: 'active',
|
|
1230
|
+
};
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1369
1234
|
const authHeader = req.headers.authorization;
|
|
1370
1235
|
if (!authHeader?.startsWith('Bearer ')) throw AuthError.unauthorized();
|
|
1371
1236
|
|
|
@@ -1929,6 +1794,23 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1929
1794
|
}, async (req: FastifyRequest, reply: FastifyReply) => {
|
|
1930
1795
|
if (!req.user?.id) throw AuthError.unauthorized();
|
|
1931
1796
|
|
|
1797
|
+
if (fastify.env.ANHEDRAL_DEMO === 'true') {
|
|
1798
|
+
return reply.send({
|
|
1799
|
+
user: {
|
|
1800
|
+
id: 'user_demo',
|
|
1801
|
+
email: 'demo@anhedral.dev',
|
|
1802
|
+
firstName: 'Demo',
|
|
1803
|
+
lastName: 'Builder',
|
|
1804
|
+
displayName: '${displayName} Demo',
|
|
1805
|
+
imageUrl: null,
|
|
1806
|
+
avatarUrl: null,
|
|
1807
|
+
creditsBalance: 250,
|
|
1808
|
+
subscriptionTier: 'pro',
|
|
1809
|
+
subscriptionStatus: 'active',
|
|
1810
|
+
},
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1932
1814
|
const clerkUser = await clerkClient.users.getUser(req.user.id);
|
|
1933
1815
|
const userData = await fastify.repos.users.getDashboardProfile(req.user.id);
|
|
1934
1816
|
const primaryEmail = clerkUser.emailAddresses.find(
|
|
@@ -2048,6 +1930,19 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
|
|
|
2048
1930
|
}, async (req, reply) => {
|
|
2049
1931
|
const userId = req.user?.id;
|
|
2050
1932
|
if (!userId) throw AuthError.unauthorized();
|
|
1933
|
+
if (app.env.ANHEDRAL_DEMO === 'true') {
|
|
1934
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
1935
|
+
return reply.send({
|
|
1936
|
+
pro: true,
|
|
1937
|
+
inTrial: false,
|
|
1938
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1939
|
+
periodStart: new Date().toISOString(),
|
|
1940
|
+
periodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1941
|
+
method: 'paid',
|
|
1942
|
+
managementUrl: 'https://app.revenuecat.com/',
|
|
1943
|
+
cancelAtPeriodEnd: false,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
2051
1946
|
const refreshRaw = (req.query as unknown as { refresh?: unknown }).refresh;
|
|
2052
1947
|
const requestedRefresh = refreshRaw === true || refreshRaw === 'true' || refreshRaw === 1 || refreshRaw === '1';
|
|
2053
1948
|
const forceRefresh = Boolean(app.env.RC_SECRET_API_KEY) && requestedRefresh;
|
|
@@ -2061,6 +1956,9 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
|
|
|
2061
1956
|
}, async (req, reply) => {
|
|
2062
1957
|
const userId = req.user?.id;
|
|
2063
1958
|
if (!userId) throw AuthError.unauthorized();
|
|
1959
|
+
if (app.env.ANHEDRAL_DEMO === 'true') {
|
|
1960
|
+
return { ok: true, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() };
|
|
1961
|
+
}
|
|
2064
1962
|
const { code } = req.body;
|
|
2065
1963
|
const validation = await app.repos.promoCodes.validateCode(code, userId);
|
|
2066
1964
|
if (!validation.valid || !validation.promoCode) {
|
|
@@ -2299,10 +2197,34 @@ async function getApp(): Promise<FastifyInstance> {
|
|
|
2299
2197
|
|
|
2300
2198
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
2301
2199
|
const app = await getApp();
|
|
2200
|
+
if (req.url === '/api' || req.url === '/backend') {
|
|
2201
|
+
req.url = '/';
|
|
2202
|
+
} else if (req.url?.startsWith('/api/')) {
|
|
2203
|
+
req.url = req.url.slice('/api'.length);
|
|
2204
|
+
} else if (req.url?.startsWith('/backend/')) {
|
|
2205
|
+
req.url = req.url.slice('/backend'.length);
|
|
2206
|
+
}
|
|
2302
2207
|
app.server.emit('request', req, res);
|
|
2303
2208
|
}
|
|
2304
2209
|
`);
|
|
2305
2210
|
}
|
|
2211
|
+
function writeVercelConfig(dir) {
|
|
2212
|
+
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
2213
|
+
version: 2,
|
|
2214
|
+
builds: [
|
|
2215
|
+
{
|
|
2216
|
+
src: 'api/index.ts',
|
|
2217
|
+
use: '@vercel/node',
|
|
2218
|
+
},
|
|
2219
|
+
],
|
|
2220
|
+
routes: [
|
|
2221
|
+
{
|
|
2222
|
+
src: '/(.*)',
|
|
2223
|
+
dest: 'api/index.ts',
|
|
2224
|
+
},
|
|
2225
|
+
],
|
|
2226
|
+
}, null, 2) + '\n');
|
|
2227
|
+
}
|
|
2306
2228
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2307
2229
|
// Tests
|
|
2308
2230
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ProjectOptions } from '../scaffold.js';
|
|
2
|
-
export declare function scaffoldExtension(root: string, { projectName, displayName }: ProjectOptions): Promise<void>;
|
|
2
|
+
export declare function scaffoldExtension(root: string, { projectName, displayName, skipInstall }: ProjectOptions): Promise<void>;
|