@beignet/cli 0.0.23 → 0.0.25

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 (51) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +55 -6
  3. package/dist/config.d.ts +21 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +51 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/db.d.ts +30 -0
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +201 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +109 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspect.d.ts.map +1 -1
  15. package/dist/inspect.js +238 -235
  16. package/dist/inspect.js.map +1 -1
  17. package/dist/make.js +16 -15
  18. package/dist/make.js.map +1 -1
  19. package/dist/provider-audit.d.ts +131 -0
  20. package/dist/provider-audit.d.ts.map +1 -0
  21. package/dist/provider-audit.js +1013 -0
  22. package/dist/provider-audit.js.map +1 -0
  23. package/dist/templates/base.d.ts.map +1 -1
  24. package/dist/templates/base.js +3 -2
  25. package/dist/templates/base.js.map +1 -1
  26. package/dist/templates/db/mysql.js +2 -2
  27. package/dist/templates/db/postgres.js +2 -2
  28. package/dist/templates/db/sqlite.js +2 -2
  29. package/dist/templates/index.d.ts.map +1 -1
  30. package/dist/templates/index.js +1 -0
  31. package/dist/templates/index.js.map +1 -1
  32. package/dist/templates/server.d.ts +1 -0
  33. package/dist/templates/server.d.ts.map +1 -1
  34. package/dist/templates/server.js +90 -20
  35. package/dist/templates/server.js.map +1 -1
  36. package/dist/templates/todos.js +5 -5
  37. package/package.json +2 -2
  38. package/skills/app-structure/SKILL.md +22 -4
  39. package/src/config.ts +99 -0
  40. package/src/db.ts +316 -1
  41. package/src/index.ts +180 -1
  42. package/src/inspect.ts +412 -369
  43. package/src/make.ts +16 -15
  44. package/src/provider-audit.ts +1681 -0
  45. package/src/templates/base.ts +3 -2
  46. package/src/templates/db/mysql.ts +2 -2
  47. package/src/templates/db/postgres.ts +2 -2
  48. package/src/templates/db/sqlite.ts +2 -2
  49. package/src/templates/index.ts +1 -0
  50. package/src/templates/server.ts +90 -20
  51. package/src/templates/todos.ts +5 -5
@@ -419,7 +419,7 @@ export function envExample(ctx: TemplateContext): string {
419
419
  "# Copy to .env.local and fill in values for your app.",
420
420
  "APP_URL=http://localhost:3000",
421
421
  "",
422
- "# Dev-only default; remove in production.",
422
+ "# Dev-only default; remove or protect in production.",
423
423
  "DEVTOOLS_ENABLED=true",
424
424
  "",
425
425
  "LOG_LEVEL=info",
@@ -428,7 +428,8 @@ export function envExample(ctx: TemplateContext): string {
428
428
  "",
429
429
  ...databaseLines,
430
430
  "",
431
- "BETTER_AUTH_SECRET=local-dev-better-auth-secret-change-me",
431
+ "# Optional for local development; required in production.",
432
+ "# BETTER_AUTH_SECRET=replace-with-a-random-32-character-secret",
432
433
  "BETTER_AUTH_URL=http://localhost:3000",
433
434
  "# BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000",
434
435
  "",
@@ -115,7 +115,7 @@ export const todos = mysqlTable(
115
115
  }),
116
116
  );
117
117
  `,
118
- drizzleTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
118
+ drizzleTodoRepository: `import "@beignet/core/server-only";
119
119
  import { offsetPageResult } from "@beignet/core/pagination";
120
120
  import type { DrizzleMysqlDatabase } from "@beignet/provider-db-drizzle/mysql";
121
121
  import { count, desc, eq } from "drizzle-orm";
@@ -255,7 +255,7 @@ async function prepare(pool: Pool): Promise<void> {
255
255
  await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
256
256
  }
257
257
  `,
258
- dbProvider: `import type { beignetServerOnly } from "@beignet/core/server-only";
258
+ dbProvider: `import "@beignet/core/server-only";
259
259
  import { createProvider } from "@beignet/core/providers";
260
260
  import {
261
261
  createDrizzleMysqlIdempotencyPort,
@@ -105,7 +105,7 @@ export const todos = pgTable(
105
105
  }),
106
106
  );
107
107
  `,
108
- drizzleTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
108
+ drizzleTodoRepository: `import "@beignet/core/server-only";
109
109
  import { offsetPageResult } from "@beignet/core/pagination";
110
110
  import type { DrizzlePostgresDatabase } from "@beignet/provider-db-drizzle/postgres";
111
111
  import { count, desc, eq } from "drizzle-orm";
@@ -243,7 +243,7 @@ async function prepare(pool: Pool): Promise<void> {
243
243
  await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
244
244
  }
245
245
  `,
246
- dbProvider: `import type { beignetServerOnly } from "@beignet/core/server-only";
246
+ dbProvider: `import "@beignet/core/server-only";
247
247
  import { createProvider } from "@beignet/core/providers";
248
248
  import {
249
249
  createDrizzlePostgresIdempotencyPort,
@@ -101,7 +101,7 @@ export const todos = sqliteTable(
101
101
  }),
102
102
  );
103
103
  `,
104
- drizzleTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
104
+ drizzleTodoRepository: `import "@beignet/core/server-only";
105
105
  import { offsetPageResult } from "@beignet/core/pagination";
106
106
  import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
107
107
  import { count, desc, eq } from "drizzle-orm";
@@ -242,7 +242,7 @@ async function prepare(client: Client): Promise<void> {
242
242
  await migrate(drizzle(client), { migrationsFolder: "drizzle" });
243
243
  }
244
244
  `,
245
- dbProvider: `import type { beignetServerOnly } from "@beignet/core/server-only";
245
+ dbProvider: `import "@beignet/core/server-only";
246
246
  import { createProvider } from "@beignet/core/providers";
247
247
  import {
248
248
  createDrizzleSqliteIdempotencyPort,
@@ -132,6 +132,7 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
132
132
  { path: "lib/env.ts", content: env(ctx) },
133
133
  { path: "lib/auth.ts", content: serverFiles.authHelpers },
134
134
  { path: "lib/better-auth.ts", content: betterAuth(ctx) },
135
+ { path: "lib/tenant.ts", content: serverFiles.tenantHelper },
135
136
  { path: "lib/use-case.ts", content: serverFiles.useCaseBuilder },
136
137
  { path: "server/index.ts", content: server(ctx) },
137
138
  { path: "server/context.ts", content: serverFiles.serverContext },
@@ -8,7 +8,7 @@ import {
8
8
  export function serverProviders(ctx: TemplateContext): string {
9
9
  const db = databaseDescriptor(ctx);
10
10
  const imports = [
11
- 'import type { beignetServerOnly } from "@beignet/core/server-only";',
11
+ 'import "@beignet/core/server-only";',
12
12
  'import { createDevtoolsProvider } from "@beignet/devtools";',
13
13
  'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";',
14
14
  `import { ${db.providerFactory} } from "@beignet/provider-db-drizzle/${db.subpath}";`,
@@ -155,6 +155,19 @@ export function env(ctx: TemplateContext): string {
155
155
  import { z } from "zod";
156
156
 
157
157
  const BooleanEnv = z.enum(["true", "false"]).transform((value) => value === "true");
158
+ const LOCAL_BETTER_AUTH_SECRET = "local-dev-better-auth-secret-change-me";
159
+ const isProductionRuntime =
160
+ process.env.NODE_ENV === "production" &&
161
+ process.env.NEXT_PHASE !== "phase-production-build";
162
+ const BetterAuthSecret = z.string().min(32).superRefine((value, ctx) => {
163
+ if (isProductionRuntime && value === LOCAL_BETTER_AUTH_SECRET) {
164
+ ctx.addIssue({
165
+ code: "custom",
166
+ message:
167
+ "BETTER_AUTH_SECRET must be unique in production. Generate a strong secret instead of using the local development placeholder.",
168
+ });
169
+ }
170
+ });
158
171
 
159
172
  export const env = createEnv({
160
173
  server: {
@@ -163,10 +176,9 @@ export const env = createEnv({
163
176
  .default("development"),
164
177
  APP_URL: z.string().url().default("http://localhost:3000"),
165
178
  DEVTOOLS_ENABLED: BooleanEnv.optional(),
166
- BETTER_AUTH_SECRET: z
167
- .string()
168
- .min(32)
169
- .default("local-dev-better-auth-secret-change-me"),
179
+ BETTER_AUTH_SECRET: isProductionRuntime
180
+ ? BetterAuthSecret
181
+ : BetterAuthSecret.default(LOCAL_BETTER_AUTH_SECRET),
170
182
  BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
171
183
  BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
172
184
  ${databaseEnvLines}
@@ -283,7 +295,7 @@ export function server(ctx: TemplateContext): string {
283
295
  \t\t\tDB_URL: env.${db.urlEnvVar},
284
296
  \t\t},`;
285
297
 
286
- return `import type { beignetServerOnly } from "@beignet/core/server-only";
298
+ return `import "@beignet/core/server-only";
287
299
  import { createNextServer } from "@beignet/next";
288
300
  import { createErrorReportingHooks, createIdempotencyHooks } from "@beignet/core/server";
289
301
  import type { AppContext } from "@/app-context";
@@ -369,7 +381,7 @@ export const errors = defineErrors({
369
381
 
370
382
  export const appError = createAppError(errors);
371
383
  `,
372
- useCaseBuilder: `import type { beignetServerOnly } from "@beignet/core/server-only";
384
+ useCaseBuilder: `import "@beignet/core/server-only";
373
385
  import { createUseCase } from "@beignet/core/application";
374
386
  import type { AppContext } from "@/app-context";
375
387
 
@@ -403,10 +415,14 @@ export type AuthUser = {
403
415
  id: string;
404
416
  email?: string;
405
417
  name?: string;
418
+ tenantId?: string;
419
+ organizationId?: string;
406
420
  };
407
421
 
408
422
  export type AuthSessionMetadata = {
409
423
  id?: string;
424
+ tenantId?: string;
425
+ organizationId?: string;
410
426
  };
411
427
 
412
428
  export type AuthSession = BeignetAuthSession<
@@ -420,7 +436,7 @@ export type AuthPort = BeignetAuthPort<
420
436
  AuthRequest
421
437
  >;
422
438
  `,
423
- serverRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
439
+ serverRoutes: `import "@beignet/core/server-only";
424
440
  import { contractsFromRoutes, defineRoutes } from "@beignet/next";
425
441
  import type { AppContext } from "@/app-context";
426
442
  import { todoRoutes } from "@/features/todos/routes";
@@ -430,17 +446,17 @@ export const routes = defineRoutes<AppContext>([
430
446
  ]);
431
447
  export const contracts = contractsFromRoutes(routes);
432
448
  `,
433
- serverContext: `import type { beignetServerOnly } from "@beignet/core/server-only";
449
+ serverContext: `import "@beignet/core/server-only";
434
450
  import {
435
451
  type ActivityActor,
436
452
  createAnonymousActor,
437
453
  createServiceActor,
438
- createTenant,
439
454
  createUserActor,
440
455
  } from "@beignet/core/ports";
441
456
  import { defineServerContext } from "@beignet/core/server";
442
457
  import type { TraceContext } from "@beignet/core/tracing";
443
458
  import type { AppContext, AppRuntimePorts } from "@/app-context";
459
+ import { resolveRequestTenant, resolveServiceTenant } from "@/lib/tenant";
444
460
 
445
461
  export type AppServiceContextInput =
446
462
  | {
@@ -456,7 +472,7 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
456
472
  gate: (ports) => ports.gate,
457
473
  request: async ({ req, ports, requestId, trace }) => {
458
474
  const auth = await ports.auth.getSession(req);
459
- const tenantId = req.headers.get("x-tenant-id") || undefined;
475
+ const tenant = resolveRequestTenant({ auth });
460
476
 
461
477
  return {
462
478
  requestId,
@@ -466,7 +482,7 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
466
482
  auth,
467
483
  ...trace,
468
484
  ports,
469
- tenant: tenantId ? createTenant(tenantId) : undefined,
485
+ ...(tenant ? { tenant } : {}),
470
486
  };
471
487
  },
472
488
  service: ({
@@ -479,15 +495,67 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
479
495
  input: AppServiceContextInput;
480
496
  requestId: string;
481
497
  trace: TraceContext;
482
- }) => ({
483
- requestId,
484
- actor: input?.actor ?? createServiceActor("beignet-service"),
485
- auth: null,
486
- ...trace,
487
- ports,
488
- tenant: createTenant(input?.tenantId ?? "tenant_default"),
489
- }),
498
+ }) => {
499
+ const tenant = resolveServiceTenant(input?.tenantId);
500
+
501
+ return {
502
+ requestId,
503
+ actor: input?.actor ?? createServiceActor("beignet-service"),
504
+ auth: null,
505
+ ...trace,
506
+ ports,
507
+ ...(tenant ? { tenant } : {}),
508
+ };
509
+ },
490
510
  });
511
+ `,
512
+ tenantHelper: `import { type ActivityTenant, createTenant } from "@beignet/core/ports";
513
+ import type { AuthSession } from "@/ports/auth";
514
+
515
+ export type TenantResolutionInput = {
516
+ auth: AuthSession | null;
517
+ };
518
+
519
+ export function resolveRequestTenant({
520
+ auth,
521
+ }: TenantResolutionInput): ActivityTenant | undefined {
522
+ const tenantId = auth ? tenantIdFromAuth(auth) : undefined;
523
+
524
+ return tenantId ? createTenant(tenantId) : undefined;
525
+ }
526
+
527
+ export function resolveServiceTenant(
528
+ tenantId: string | undefined,
529
+ ): ActivityTenant | undefined {
530
+ const normalizedTenantId = normalizeTenantId(tenantId);
531
+
532
+ return normalizedTenantId ? createTenant(normalizedTenantId) : undefined;
533
+ }
534
+
535
+ function tenantIdFromAuth(auth: AuthSession) {
536
+ return (
537
+ stringProperty(auth.session, "tenantId") ??
538
+ stringProperty(auth.session, "organizationId") ??
539
+ stringProperty(auth.user, "tenantId") ??
540
+ stringProperty(auth.user, "organizationId")
541
+ );
542
+ }
543
+
544
+ function stringProperty(value: unknown, key: string) {
545
+ if (!isRecord(value)) return undefined;
546
+
547
+ return normalizeTenantId(value[key]);
548
+ }
549
+
550
+ function normalizeTenantId(value: unknown) {
551
+ return typeof value === "string" && value.trim().length > 0
552
+ ? value.trim()
553
+ : undefined;
554
+ }
555
+
556
+ function isRecord(value: unknown): value is Record<string, unknown> {
557
+ return typeof value === "object" && value !== null;
558
+ }
491
559
  `,
492
560
  apiCatchAllRoute: `import { server } from "@/server";
493
561
 
@@ -553,11 +621,13 @@ export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
553
621
  });
554
622
  `,
555
623
  openApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
624
+ import { env } from "@/lib/env";
556
625
  import { server } from "@/server";
557
626
 
558
627
  export const GET = createOpenAPIHandler(server.contracts, {
559
628
  title: "Beignet starter API",
560
629
  version: "0.1.0",
630
+ servers: [{ url: env.APP_URL }],
561
631
  });
562
632
  `,
563
633
  authRoute: `import { toNextJsHandler } from "better-auth/next-js";
@@ -174,7 +174,7 @@ export const todoPolicy = definePolicy({
174
174
  authorizeOwner(ctx, todo, "delete"),
175
175
  });
176
176
  `,
177
- routes: `import type { beignetServerOnly } from "@beignet/core/server-only";
177
+ routes: `import "@beignet/core/server-only";
178
178
  import { defineRouteGroup } from "@beignet/next";
179
179
  import type { AppContext } from "@/app-context";
180
180
  import {
@@ -200,7 +200,7 @@ export const todoRoutes = defineRouteGroup<AppContext>()({
200
200
  ],
201
201
  });
202
202
  `,
203
- listTodosUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
203
+ listTodosUseCase: `import "@beignet/core/server-only";
204
204
  import { normalizeOffsetPage } from "@beignet/core/pagination";
205
205
  import { requireUser } from "@/lib/auth";
206
206
  import { useCase } from "@/lib/use-case";
@@ -220,7 +220,7 @@ export const listTodosUseCase = useCase
220
220
  return ctx.ports.todos.list(user.id, page);
221
221
  });
222
222
  `,
223
- createTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
223
+ createTodoUseCase: `import "@beignet/core/server-only";
224
224
  import { requireUser } from "@/lib/auth";
225
225
  import { useCase } from "@/lib/use-case";
226
226
  import { CreateTodoInputSchema, TodoSchema } from "../schemas";
@@ -238,7 +238,7 @@ export const createTodoUseCase = useCase
238
238
  );
239
239
  });
240
240
  `,
241
- updateTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
241
+ updateTodoUseCase: `import "@beignet/core/server-only";
242
242
  import { appError } from "@/features/shared/errors";
243
243
  import { requireUser } from "@/lib/auth";
244
244
  import { useCase } from "@/lib/use-case";
@@ -263,7 +263,7 @@ export const updateTodoUseCase = useCase
263
263
  });
264
264
  });
265
265
  `,
266
- deleteTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
266
+ deleteTodoUseCase: `import "@beignet/core/server-only";
267
267
  import { appError } from "@/features/shared/errors";
268
268
  import { requireUser } from "@/lib/auth";
269
269
  import { useCase } from "@/lib/use-case";