create-z3 0.0.47 → 0.0.49

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 (27) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +39 -38
  3. package/package.json +11 -11
  4. package/templates/nextjs/convex/auth/adapter/index.ts +7 -22
  5. package/templates/nextjs/convex/auth/index.ts +1 -1
  6. package/templates/nextjs/convex/auth/plugins/index.ts +6 -2
  7. package/templates/nextjs/convex/auth/sessions.ts +1 -1
  8. package/templates/nextjs/convex/auth.config.ts +7 -0
  9. package/templates/nextjs/convex/schema.ts +30 -2
  10. package/templates/nextjs/package.json +3 -2
  11. package/templates/nextjs/src/app/(frontend)/layout.tsx +21 -27
  12. package/templates/nextjs/src/auth/client.tsx +2 -4
  13. package/templates/nextjs/src/auth/permissions.ts +2 -2
  14. package/templates/nextjs/src/components/component-example.tsx +44 -492
  15. package/templates/nextjs/src/db/constants/index.ts +1 -0
  16. package/templates/tanstack-start/convex/auth/adapter/index.ts +7 -22
  17. package/templates/tanstack-start/convex/auth/index.ts +1 -1
  18. package/templates/tanstack-start/convex/auth/plugins/index.ts +5 -1
  19. package/templates/tanstack-start/convex/auth/sessions.ts +1 -1
  20. package/templates/tanstack-start/convex/auth.config.ts +7 -0
  21. package/templates/tanstack-start/convex/schema.ts +37 -2
  22. package/templates/tanstack-start/package.json +3 -2
  23. package/templates/tanstack-start/src/components/component-example.tsx +41 -460
  24. package/templates/tanstack-start/src/db/constants/index.ts +1 -0
  25. package/templates/tanstack-start/src/lib/auth/client.ts +2 -1
  26. package/templates/tanstack-start/src/lib/auth/permissions.ts +2 -2
  27. package/templates/tanstack-start/src/routes/auth/$authView.tsx +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isaiah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ async function isDirectoryEmpty(dirPath) {
37
37
  return true;
38
38
  }
39
39
  }
40
- function resolveProjectName(input2, cwd) {
40
+ function resolveProjectName(input2, _cwd) {
41
41
  return input2;
42
42
  }
43
43
 
@@ -1730,26 +1730,28 @@ function getAdditionalProviders() {
1730
1730
 
1731
1731
  // src/installers/string-utils.ts
1732
1732
  import fs3 from "fs-extra";
1733
- var DEFAULT_THEME = `--background: 100% 0.000 0;
1734
- --foreground: 3.9% 0.006 240;
1735
- --card: 100% 0.000 0;
1736
- --card-foreground: 3.9% 0.006 240;
1737
- --popover: 100% 0.000 0;
1738
- --popover-foreground: 3.9% 0.006 240;
1739
- --primary: 10% 0.003 240;
1740
- --primary-foreground: 98% 0.000 0;
1741
- --secondary: 95.9% 0.002 240;
1742
- --secondary-foreground: 10% 0.003 240;
1743
- --muted: 95.9% 0.002 240;
1744
- --muted-foreground: 46.1% 0.002 240;
1745
- --accent: 95.9% 0.002 240;
1746
- --accent-foreground: 10% 0.003 240;
1747
- --destructive: 60.2% 0.168 0;
1748
- --destructive-foreground: 98% 0.000 0;
1749
- --border: 90% 0.003 240;
1750
- --input: 90% 0.003 240;
1751
- --ring: 10% 0.003 240;
1752
- --radius: 0.5rem;`;
1733
+ var DEFAULT_THEME = `:root {
1734
+ --background: 100% 0.000 0;
1735
+ --foreground: 3.9% 0.006 240;
1736
+ --card: 100% 0.000 0;
1737
+ --card-foreground: 3.9% 0.006 240;
1738
+ --popover: 100% 0.000 0;
1739
+ --popover-foreground: 3.9% 0.006 240;
1740
+ --primary: 10% 0.003 240;
1741
+ --primary-foreground: 98% 0.000 0;
1742
+ --secondary: 95.9% 0.002 240;
1743
+ --secondary-foreground: 10% 0.003 240;
1744
+ --muted: 95.9% 0.002 240;
1745
+ --muted-foreground: 46.1% 0.002 240;
1746
+ --accent: 95.9% 0.002 240;
1747
+ --accent-foreground: 10% 0.003 240;
1748
+ --destructive: 60.2% 0.168 0;
1749
+ --destructive-foreground: 98% 0.000 0;
1750
+ --border: 90% 0.003 240;
1751
+ --input: 90% 0.003 240;
1752
+ --ring: 10% 0.003 240;
1753
+ --radius: 0.5rem;
1754
+ }`;
1753
1755
  function detectIndentation(line) {
1754
1756
  const match = line.match(/^(\s*)/);
1755
1757
  return match ? match[1] : "";
@@ -1798,9 +1800,11 @@ function generateCredentialsValue(enabled) {
1798
1800
  }
1799
1801
  function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
1800
1802
  const parts = [];
1801
- parts.push(`emailAndPassword: {
1802
- enabled: ${emailPasswordEnabled}
1803
+ if (emailPasswordEnabled) {
1804
+ parts.push(`emailAndPassword: {
1805
+ enabled: true
1803
1806
  },`);
1807
+ }
1804
1808
  if (oauthProviders.length > 0) {
1805
1809
  const providersObject = oauthProviders.map((providerId) => {
1806
1810
  const provider = getProvider(providerId);
@@ -2300,13 +2304,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
2300
2304
  async updateEnvExample(selectedProviders) {
2301
2305
  const envFilePath = join2(this.targetPath, ".env.example");
2302
2306
  const envVarsBlock = generateEnvVarsBlock(selectedProviders, "tanstack");
2303
- if (envVarsBlock) {
2304
- await replacePlaceholder(
2305
- envFilePath,
2306
- "# {{ENV_OAUTH_VARS}}",
2307
- envVarsBlock
2308
- );
2309
- }
2307
+ await replacePlaceholder(
2308
+ envFilePath,
2309
+ "# {{ENV_OAUTH_VARS}}",
2310
+ envVarsBlock,
2311
+ { graceful: true }
2312
+ );
2310
2313
  }
2311
2314
  /**
2312
2315
  * Update README with OAuth provider setup guides
@@ -2319,14 +2322,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
2319
2322
  async updateReadme(selectedProviders) {
2320
2323
  const readmeFilePath = join2(this.targetPath, "README.md");
2321
2324
  const readmeSection = generateReadmeSection(selectedProviders);
2322
- if (readmeSection) {
2323
- await replacePlaceholder(
2324
- readmeFilePath,
2325
- "<!-- {{OAUTH_SETUP_GUIDE}} -->",
2326
- readmeSection,
2327
- { graceful: true }
2328
- );
2329
- }
2325
+ await replacePlaceholder(
2326
+ readmeFilePath,
2327
+ "<!-- {{OAUTH_SETUP_GUIDE}} -->",
2328
+ readmeSection,
2329
+ { graceful: true }
2330
+ );
2330
2331
  }
2331
2332
  /**
2332
2333
  * Apply TweakCN theme to global CSS file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-z3",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding Z3 Stack applications (TanStack/Next.js + Convex + Better Auth)",
6
6
  "bin": {
@@ -10,13 +10,6 @@
10
10
  "dist",
11
11
  "templates"
12
12
  ],
13
- "scripts": {
14
- "build": "tsup src/index.ts --format esm --dts",
15
- "dev": "tsup src/index.ts --format esm --watch",
16
- "typecheck": "tsc --noEmit",
17
- "test": "vitest run",
18
- "test:watch": "vitest"
19
- },
20
13
  "dependencies": {
21
14
  "commander": "^12.0.0",
22
15
  "@inquirer/prompts": "^7.2.0",
@@ -55,7 +48,14 @@
55
48
  ],
56
49
  "repository": {
57
50
  "type": "git",
58
- "url": "https://github.com/zayecq/create-z3-app"
51
+ "url": "https://github.com/ianyimi/create-z3-app"
59
52
  },
60
- "license": "MIT"
61
- }
53
+ "license": "MIT",
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format esm --dts",
56
+ "dev": "tsup src/index.ts --format esm --watch",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
60
+ }
61
+ }
@@ -50,6 +50,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
50
50
 
51
51
  return {
52
52
  id: "convex",
53
+ // Tell the convex() plugin this context supports mutations. It checks
54
+ // ctx.context.adapter.options?.isRunMutationCtx — if falsy it replaces all
55
+ // adapter writes with silent no-ops. HTTP actions always have runMutation.
56
+ options: {
57
+ isRunMutationCtx: "runMutation" in ctx,
58
+ },
53
59
 
54
60
  create: async ({ data, model, select }): Promise<any> => {
55
61
  return await ctx.runMutation(internal.auth.db.dbCreate, {
@@ -189,34 +195,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
189
195
  if (data && fieldAttributes.type === "date") {
190
196
  return new Date(data).getTime();
191
197
  }
192
- // Handle array fields - Better Auth may send single values or arrays
193
- if ((fieldAttributes.type as string)?.endsWith("[]")) {
194
- // If already an array, return as-is
195
- if (Array.isArray(data)) {
196
- return data
197
- }
198
- // If it's a string that looks like JSON array, parse it
199
- if (typeof data === "string") {
200
- try {
201
- const parsed = JSON.parse(data)
202
- return Array.isArray(parsed) ? parsed : [data]
203
- } catch {
204
- // If parsing fails, wrap the string in an array
205
- return [data]
206
- }
207
- }
208
- // For any other value type, wrap in array
209
- if (data !== null && data !== undefined) {
210
- return [data]
211
- }
212
- }
213
198
  return data;
214
199
  },
215
200
  customTransformOutput: ({ data, fieldAttributes }) => {
216
201
  if (data && fieldAttributes.type === "date") {
217
202
  return new Date(data).getTime();
218
203
  }
219
- // Arrays are stored natively in Convex, no transformation needed for output
220
204
  return data;
221
205
  },
222
206
  debugLogs: config.debugLogs ?? false,
@@ -230,6 +214,7 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
230
214
  supportsDates: false,
231
215
  supportsJSON: true,
232
216
  supportsNumericIds: false,
217
+ supportsArrays: true,
233
218
  transaction: false,
234
219
  usePlural: false,
235
220
  },
@@ -39,7 +39,7 @@ export const createAuth = (
39
39
  trustedOrigins: [process.env.SITE_URL!],
40
40
  user: {
41
41
  additionalFields: {
42
- role: {
42
+ roles: {
43
43
  type: "string[]",
44
44
  defaultValue: [USER_ROLES.user],
45
45
  required: true
@@ -1,14 +1,18 @@
1
1
  import { nextCookies } from "better-auth/next-js"
2
- import { admin, apiKey } from "better-auth/plugins"
2
+ import { admin } from "better-auth/plugins"
3
+ import { apiKey } from "@better-auth/api-key"
4
+ import { convex } from "@convex-dev/better-auth/plugins"
3
5
  import { USER_ROLES } from "~/db/constants"
6
+ import authConfig from "@convex/auth.config"
4
7
 
5
8
  const plugins = [
6
9
  admin({
7
10
  adminRoles: [USER_ROLES.admin],
8
- defaultRole: USER_ROLES.user
11
+ defaultRole: USER_ROLES.user,
9
12
  }),
10
13
  apiKey(),
11
14
  nextCookies(),
15
+ convex({ authConfig }),
12
16
  ]
13
17
 
14
18
  export default plugins
@@ -53,7 +53,7 @@ export const getSessionWithUser = query({
53
53
  email: user.email,
54
54
  emailVerified: user.emailVerified,
55
55
  image: user.image,
56
- role: user.role,
56
+ roles: user.roles,
57
57
  },
58
58
  };
59
59
  },
@@ -0,0 +1,7 @@
1
+ import type { AuthConfig } from "convex/server"
2
+
3
+ import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"
4
+
5
+ export default {
6
+ providers: [getAuthConfigProvider()],
7
+ } satisfies AuthConfig
@@ -1,7 +1,7 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values"
3
3
 
4
- import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
4
+ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_API_KEYS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
5
5
 
6
6
  export default defineSchema({
7
7
  // Better Auth component tables (type definitions only - actual tables are in component)
@@ -19,7 +19,8 @@ export default defineSchema({
19
19
  isAnonymous: v.optional(v.union(v.null(), v.boolean())),
20
20
  phoneNumber: v.optional(v.union(v.null(), v.string())),
21
21
  phoneNumberVerified: v.optional(v.union(v.null(), v.boolean())),
22
- role: v.array(v.string()), // admin plugin
22
+ role: v.optional(v.string()), // admin plugin — single string in BA 1.5
23
+ roles: v.array(v.string()), // our multi-role field via additionalFields
23
24
  twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())),
24
25
  updatedAt: v.number(),
25
26
  userId: v.optional(v.union(v.null(), v.string()))
@@ -70,4 +71,31 @@ export default defineSchema({
70
71
  privateKey: v.optional(v.string()),
71
72
  publicKey: v.string(),
72
73
  }),
74
+
75
+ // Better Auth 1.5 — apiKey plugin (@better-auth/api-key)
76
+ [TABLE_SLUG_API_KEYS]: defineTable({
77
+ configId: v.string(),
78
+ name: v.optional(v.string()),
79
+ start: v.optional(v.string()),
80
+ referenceId: v.string(),
81
+ prefix: v.optional(v.string()),
82
+ key: v.string(),
83
+ refillInterval: v.optional(v.number()),
84
+ refillAmount: v.optional(v.number()),
85
+ lastRefillAt: v.optional(v.number()),
86
+ enabled: v.optional(v.boolean()),
87
+ rateLimitEnabled: v.optional(v.boolean()),
88
+ rateLimitTimeWindow: v.optional(v.number()),
89
+ rateLimitMax: v.optional(v.number()),
90
+ requestCount: v.optional(v.number()),
91
+ remaining: v.optional(v.number()),
92
+ lastRequest: v.optional(v.number()),
93
+ expiresAt: v.optional(v.number()),
94
+ createdAt: v.number(),
95
+ updatedAt: v.number(),
96
+ permissions: v.optional(v.string()),
97
+ metadata: v.optional(v.string()),
98
+ })
99
+ .index("by_referenceId", ["referenceId"])
100
+ .index("by_key", ["key"]),
73
101
  })
@@ -13,13 +13,14 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@base-ui/react": "^1.1.0",
16
- "@convex-dev/better-auth": "^0.10.10",
16
+ "@better-auth/api-key": "^1.0.0",
17
+ "@convex-dev/better-auth": "^0.11.0",
17
18
  "@convex-dev/react-query": "^0.1.0",
18
19
  "@daveyplate/better-auth-ui": "^3.3.15",
19
20
  "@t3-oss/env-nextjs": "^0.13.10",
20
21
  "@tanstack/react-form": "^1.27.7",
21
22
  "@tanstack/react-query": "^5.90.17",
22
- "better-auth": "^1.4.9",
23
+ "better-auth": "^1.5.0",
23
24
  "class-variance-authority": "^0.7.1",
24
25
  "clsx": "^2.1.1",
25
26
  "convex": "^1.31.5",
@@ -1,46 +1,40 @@
1
- import type { Metadata } from "next";
2
- import { Geist, Geist_Mono, Inter } from "next/font/google";
3
- import "./globals.css";
4
- import ServerProviders from "~/components/providers/server";
5
- import ClientProviders from "~/components/providers/client";
6
- import Head from "next/head";
1
+ import type { Metadata } from "next"
7
2
 
8
- const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
3
+ import { Geist, Geist_Mono, Inter } from "next/font/google"
4
+
5
+ import "./globals.css"
6
+
7
+ import ClientProviders from "~/components/providers/client"
8
+ import ServerProviders from "~/components/providers/server"
9
+
10
+ const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
9
11
 
10
12
  const geistSans = Geist({
11
- variable: "--font-geist-sans",
12
13
  subsets: ["latin"],
13
- });
14
+ variable: "--font-geist-sans",
15
+ })
14
16
 
15
17
  const geistMono = Geist_Mono({
16
- variable: "--font-geist-mono",
17
18
  subsets: ["latin"],
18
- });
19
+ variable: "--font-geist-mono",
20
+ })
19
21
 
20
22
  export const metadata: Metadata = {
21
- title: "Create Next App",
22
23
  description: "Generated by create next app",
23
- };
24
+ icons: { icon: "/favicons/favicon.ico" },
25
+ title: "Create Next App",
26
+ }
24
27
 
25
28
  export default function RootLayout({
26
29
  auth,
27
30
  children,
28
31
  }: Readonly<{
29
- auth: React.ReactNode;
30
- children: React.ReactNode;
32
+ auth: React.ReactNode
33
+ children: React.ReactNode
31
34
  }>) {
32
35
  return (
33
- <html lang="en" className={inter.variable}>
34
- <Head>
35
- <link
36
- href="/favicons/favicon.ico"
37
- rel="shortcut icon"
38
- type="image/x-icon"
39
- />
40
- </Head>
41
- <body
42
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
43
- >
36
+ <html className={inter.variable} lang="en" suppressHydrationWarning>
37
+ <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
44
38
  <ServerProviders>
45
39
  <ClientProviders>
46
40
  {children}
@@ -49,5 +43,5 @@ export default function RootLayout({
49
43
  </ServerProviders>
50
44
  </body>
51
45
  </html>
52
- );
46
+ )
53
47
  }
@@ -2,10 +2,8 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { convexClient } from "@convex-dev/better-auth/client/plugins";
4
4
  import { AuthUIProvider } from "@daveyplate/better-auth-ui";
5
- import {
6
- adminClient,
7
- apiKeyClient,
8
- } from "better-auth/client/plugins";
5
+ import { apiKeyClient } from "@better-auth/api-key/client";
6
+ import { adminClient } from "better-auth/client/plugins";
9
7
  import { createAuthClient } from 'better-auth/react'
10
8
  import Link from "next/link";
11
9
  import { useRouter } from "next/navigation";
@@ -80,9 +80,9 @@ export function hasPermission<Resource extends keyof Permissions>({
80
80
  resource: Resource;
81
81
  user?: null | User;
82
82
  }): boolean {
83
- if (!user?.role) { return false; }
83
+ if (!user?.roles) { return false; }
84
84
 
85
- return user.role.some((role) => {
85
+ return user.roles.some((role) => {
86
86
  const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
87
87
 
88
88
  if (!permission) { return false }