@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.
- package/CHANGELOG.md +28 -0
- package/README.md +55 -6
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +201 -1
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +238 -235
- package/dist/inspect.js.map +1 -1
- package/dist/make.js +16 -15
- package/dist/make.js.map +1 -1
- package/dist/provider-audit.d.ts +131 -0
- package/dist/provider-audit.d.ts.map +1 -0
- package/dist/provider-audit.js +1013 -0
- package/dist/provider-audit.js.map +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +3 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/mysql.js +2 -2
- package/dist/templates/db/postgres.js +2 -2
- package/dist/templates/db/sqlite.js +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +1 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -0
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +90 -20
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/todos.js +5 -5
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +22 -4
- package/src/config.ts +99 -0
- package/src/db.ts +316 -1
- package/src/index.ts +180 -1
- package/src/inspect.ts +412 -369
- package/src/make.ts +16 -15
- package/src/provider-audit.ts +1681 -0
- package/src/templates/base.ts +3 -2
- package/src/templates/db/mysql.ts +2 -2
- package/src/templates/db/postgres.ts +2 -2
- package/src/templates/db/sqlite.ts +2 -2
- package/src/templates/index.ts +1 -0
- package/src/templates/server.ts +90 -20
- package/src/templates/todos.ts +5 -5
package/src/templates/base.ts
CHANGED
|
@@ -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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
245
|
+
dbProvider: `import "@beignet/core/server-only";
|
|
246
246
|
import { createProvider } from "@beignet/core/providers";
|
|
247
247
|
import {
|
|
248
248
|
createDrizzleSqliteIdempotencyPort,
|
package/src/templates/index.ts
CHANGED
|
@@ -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 },
|
package/src/templates/server.ts
CHANGED
|
@@ -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
|
|
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:
|
|
167
|
-
|
|
168
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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";
|
package/src/templates/todos.ts
CHANGED
|
@@ -174,7 +174,7 @@ export const todoPolicy = definePolicy({
|
|
|
174
174
|
authorizeOwner(ctx, todo, "delete"),
|
|
175
175
|
});
|
|
176
176
|
`,
|
|
177
|
-
routes: `import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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";
|