appflare 0.0.24 → 0.0.26

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/cli/core/build.ts CHANGED
@@ -32,7 +32,7 @@ export async function buildFromConfig(params: {
32
32
 
33
33
  await assertDirExists(
34
34
  projectDirAbs,
35
- `Project dir not found: ${projectDirAbs}`
35
+ `Project dir not found: ${projectDirAbs}`,
36
36
  );
37
37
  await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
38
38
 
@@ -42,7 +42,7 @@ export async function buildFromConfig(params: {
42
42
  // Re-export the user schema inside the generated output so downstream code can import it from the build directory.
43
43
  const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
44
44
  outDirAbs,
45
- schemaPathAbs
45
+ schemaPathAbs,
46
46
  );
47
47
  const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
48
48
  export type AppflareGeneratedSchema = typeof schema;
@@ -57,7 +57,7 @@ export default schema;
57
57
  });
58
58
  await fs.writeFile(
59
59
  path.join(outDirAbs, "src", "schema-types.ts"),
60
- schemaTypesTs
60
+ schemaTypesTs,
61
61
  );
62
62
 
63
63
  // (Re)generate built-in DB handlers based on the schema tables.
@@ -78,6 +78,8 @@ export default schema;
78
78
  config.auth && config.auth.enabled === false
79
79
  ? undefined
80
80
  : (config.auth?.basePath ?? "/auth"),
81
+ authEnabled: config.auth?.enabled !== false,
82
+ configPathAbs,
81
83
  });
82
84
  await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
83
85
 
@@ -99,7 +101,7 @@ export default schema;
99
101
  });
100
102
  await fs.writeFile(
101
103
  path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
102
- websocketDoTs
104
+ websocketDoTs,
103
105
  );
104
106
 
105
107
  const schedulerTs = generateSchedulerHandlers({
@@ -110,7 +112,7 @@ export default schema;
110
112
  });
111
113
  await fs.writeFile(
112
114
  path.join(outDirAbs, "server", "scheduler.ts"),
113
- schedulerTs
115
+ schedulerTs,
114
116
  );
115
117
 
116
118
  const { code: cronTs } = generateCronHandlers({
@@ -144,7 +146,7 @@ async function writeEmitTsconfig(params: {
144
146
  "./_generated";
145
147
  const tsconfigPathAbs = path.join(
146
148
  params.configDirAbs,
147
- ".appflare.tsconfig.emit.json"
149
+ ".appflare.tsconfig.emit.json",
148
150
  );
149
151
  const content = {
150
152
  compilerOptions: {
@@ -4,7 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import { AppflareConfig, assertFileExists } from "../utils/utils";
5
5
 
6
6
  export async function loadConfig(
7
- configPathAbs: string
7
+ configPathAbs: string,
8
8
  ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
9
9
  await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
10
10
  const configDirAbs = path.dirname(configPathAbs);
@@ -13,7 +13,7 @@ export async function loadConfig(
13
13
  const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
14
14
  if (!config || typeof config !== "object") {
15
15
  throw new Error(
16
- `Invalid config export in ${configPathAbs} (expected default export object)`
16
+ `Invalid config export in ${configPathAbs} (expected default export object)`,
17
17
  );
18
18
  }
19
19
  if (typeof config.dir !== "string" || !config.dir) {
@@ -40,6 +40,12 @@ export async function loadConfig(
40
40
  if (auth.options !== undefined && typeof auth.options !== "object") {
41
41
  throw new Error(`Invalid config.auth.options in ${configPathAbs}`);
42
42
  }
43
+ if (
44
+ auth.clientOptions !== undefined &&
45
+ typeof auth.clientOptions !== "object"
46
+ ) {
47
+ throw new Error(`Invalid config.auth.clientOptions in ${configPathAbs}`);
48
+ }
43
49
  }
44
50
 
45
51
  const storage = (config as AppflareConfig).storage;
@@ -61,7 +67,7 @@ export async function loadConfig(
61
67
  typeof storage.bucketBinding !== "string"
62
68
  ) {
63
69
  throw new Error(
64
- `Invalid config.storage.bucketBinding in ${configPathAbs}`
70
+ `Invalid config.storage.bucketBinding in ${configPathAbs}`,
65
71
  );
66
72
  }
67
73
  if (
@@ -69,9 +75,18 @@ export async function loadConfig(
69
75
  typeof storage.defaultCacheControl !== "string"
70
76
  ) {
71
77
  throw new Error(
72
- `Invalid config.storage.defaultCacheControl in ${configPathAbs}`
78
+ `Invalid config.storage.defaultCacheControl in ${configPathAbs}`,
73
79
  );
74
80
  }
81
+ if (
82
+ storage.kvBinding !== undefined &&
83
+ typeof storage.kvBinding !== "string"
84
+ ) {
85
+ throw new Error(`Invalid config.storage.kvBinding in ${configPathAbs}`);
86
+ }
87
+ if (storage.kvId !== undefined && typeof storage.kvId !== "string") {
88
+ throw new Error(`Invalid config.storage.kvId in ${configPathAbs}`);
89
+ }
75
90
  }
76
91
  return { config: config as AppflareConfig, configDirAbs };
77
92
  }
package/cli/core/index.ts CHANGED
@@ -27,6 +27,7 @@ type AppflareConfig = {
27
27
  auth?: {
28
28
  enabled?: boolean;
29
29
  basePath?: string;
30
+ clientOptions?: Record<string, unknown>;
30
31
  };
31
32
  };
32
33
 
@@ -37,12 +38,12 @@ program.name("appflare").description("Appflare CLI").version("0.0.0");
37
38
  program
38
39
  .command("build")
39
40
  .description(
40
- "Generate typed schema + query/mutation client/server into outDir"
41
+ "Generate typed schema + query/mutation client/server into outDir",
41
42
  )
42
43
  .option(
43
44
  "-c, --config <path>",
44
45
  "Path to appflare.config.ts",
45
- "appflare.config.ts"
46
+ "appflare.config.ts",
46
47
  )
47
48
  .option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
48
49
  .action(async (options: { config: string; emit?: boolean }) => {
@@ -69,7 +70,7 @@ async function main(): Promise<void> {
69
70
  }
70
71
 
71
72
  async function loadConfig(
72
- configPathAbs: string
73
+ configPathAbs: string,
73
74
  ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
74
75
  await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
75
76
  const configDirAbs = path.dirname(configPathAbs);
@@ -78,7 +79,7 @@ async function loadConfig(
78
79
  const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
79
80
  if (!config || typeof config !== "object") {
80
81
  throw new Error(
81
- `Invalid config export in ${configPathAbs} (expected default export object)`
82
+ `Invalid config export in ${configPathAbs} (expected default export object)`,
82
83
  );
83
84
  }
84
85
  if (typeof config.dir !== "string" || !config.dir) {
@@ -107,7 +108,7 @@ async function buildFromConfig(params: {
107
108
 
108
109
  await assertDirExists(
109
110
  projectDirAbs,
110
- `Project dir not found: ${projectDirAbs}`
111
+ `Project dir not found: ${projectDirAbs}`,
111
112
  );
112
113
  await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
113
114
 
@@ -116,7 +117,7 @@ async function buildFromConfig(params: {
116
117
 
117
118
  const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
118
119
  outDirAbs,
119
- schemaPathAbs
120
+ schemaPathAbs,
120
121
  );
121
122
  const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
122
123
  export type AppflareGeneratedSchema = typeof schema;
@@ -131,7 +132,7 @@ export default schema;
131
132
  });
132
133
  await fs.writeFile(
133
134
  path.join(outDirAbs, "src", "schema-types.ts"),
134
- schemaTypesTs
135
+ schemaTypesTs,
135
136
  );
136
137
 
137
138
  // (Re)generate built-in DB handlers based on the schema tables.
@@ -152,6 +153,8 @@ export default schema;
152
153
  config.auth && config.auth.enabled === false
153
154
  ? undefined
154
155
  : (config.auth?.basePath ?? "/auth"),
156
+ authEnabled: config.auth?.enabled !== false,
157
+ configPathAbs,
155
158
  });
156
159
  await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
157
160
 
@@ -173,7 +176,7 @@ export default schema;
173
176
  });
174
177
  await fs.writeFile(
175
178
  path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
176
- websocketDoTs
179
+ websocketDoTs,
177
180
  );
178
181
 
179
182
  const schedulerTs = generateSchedulerHandlers({
@@ -184,7 +187,7 @@ export default schema;
184
187
  });
185
188
  await fs.writeFile(
186
189
  path.join(outDirAbs, "server", "scheduler.ts"),
187
- schedulerTs
190
+ schedulerTs,
188
191
  );
189
192
 
190
193
  if (emit) {
@@ -38,7 +38,7 @@ import type {
38
38
  InternalQueryContext,
39
39
  InternalQueryDefinition,
40
40
  } from "./schema-types";
41
-
41
+ {{configImport}}
42
42
  `;
43
43
 
44
44
  const TYPE_DEFINITIONS_TEMPLATE = `
@@ -286,11 +286,13 @@ export type QueriesClient = {{queriesTypeDef}};
286
286
 
287
287
  export type MutationsClient = {{mutationsTypeDef}};
288
288
 
289
+ {{authClientTypeDefinitions}}
290
+
289
291
  export type AppflareApiClient = {
290
292
  queries: QueriesClient;
291
293
  mutations: MutationsClient;
292
294
  storage: StorageManagerClient;
293
- auth?: ReturnType<typeof createAuthClient>;
295
+ auth?: AppflareAuthClient;
294
296
  };
295
297
 
296
298
  export type AppflareApiOptions = {
@@ -298,10 +300,10 @@ export type AppflareApiOptions = {
298
300
  fetcher?: RequestExecutor;
299
301
  realtime?: RealtimeConfig;
300
302
  storage?: StorageManagerOptions;
301
- auth?: false | (BetterAuthClientOptions & { baseURL?: string });
303
+ auth?: (BetterAuthClientOptions & { baseURL?: string });
302
304
  };
303
305
 
304
- export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApiClient {
306
+ export function createAppflareApi(options: AppflareApiOptions = {}) {
305
307
  const baseUrl = normalizeBaseUrl(options.baseUrl);
306
308
  const request = options.fetcher ?? defaultFetcher;
307
309
  const realtime = resolveRealtimeConfig(baseUrl, options.realtime);
@@ -309,14 +311,7 @@ export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApi
309
311
  const mutations: MutationsClient = {{mutationsInit}};
310
312
  const storage = createStorageManagerClient(baseUrl, request, options.storage);
311
313
  const authBasePath = normalizeAuthBasePath({{authBasePath}}) ?? "/auth";
312
- const auth = options.auth === false
313
- ? undefined
314
- : createAuthClient({
315
- ...(options.auth ?? {}),
316
- baseURL:
317
- (options.auth as any)?.baseURL ??
318
- buildUrl(baseUrl, authBasePath),
319
- });
314
+ {{authClientInit}}
320
315
  return { queries, mutations, storage, auth };
321
316
  }
322
317
 
@@ -675,7 +670,7 @@ function generateImports(params: {
675
670
  }): { importLines: string[]; importAliasBySource: Map<string, string> } {
676
671
  const handlerImportsGrouped = groupBy(
677
672
  params.handlers,
678
- (h) => h.sourceFileAbs
673
+ (h) => h.sourceFileAbs,
679
674
  );
680
675
 
681
676
  const importLines: string[] = [];
@@ -685,7 +680,7 @@ function generateImports(params: {
685
680
  importAliasBySource.set(fileAbs, alias);
686
681
  const importPath = toImportPathFromGeneratedSrc(params.outDirAbs, fileAbs);
687
682
  importLines.push(
688
- `import * as ${alias} from ${JSON.stringify(importPath)};`
683
+ `import * as ${alias} from ${JSON.stringify(importPath)};`,
689
684
  );
690
685
  }
691
686
  return { importLines, importAliasBySource };
@@ -701,7 +696,7 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
701
696
  const mutations = handlers.filter((h) => h.kind === "mutation");
702
697
  const internalQueries = handlers.filter((h) => h.kind === "internalQuery");
703
698
  const internalMutations = handlers.filter(
704
- (h) => h.kind === "internalMutation"
699
+ (h) => h.kind === "internalMutation",
705
700
  );
706
701
 
707
702
  const queriesByFile = groupBy(queries, (h) => h.routePath);
@@ -709,7 +704,7 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
709
704
  const internalQueriesByFile = groupBy(internalQueries, (h) => h.routePath);
710
705
  const internalMutationsByFile = groupBy(
711
706
  internalMutations,
712
- (h) => h.routePath
707
+ (h) => h.routePath,
713
708
  );
714
709
 
715
710
  return {
@@ -725,7 +720,7 @@ function generateTypeDefs(
725
720
  mutationsByFile: Map<string, DiscoveredHandler[]>,
726
721
  internalQueriesByFile: Map<string, DiscoveredHandler[]>,
727
722
  internalMutationsByFile: Map<string, DiscoveredHandler[]>,
728
- importAliasBySource: Map<string, string>
723
+ importAliasBySource: Map<string, string>,
729
724
  ): {
730
725
  queriesTypeDef: string;
731
726
  mutationsTypeDef: string;
@@ -736,11 +731,11 @@ function generateTypeDefs(
736
731
  const mutationsTypeLines = generateMutationsTypeLines(mutationsByFile);
737
732
  const internalQueriesTypeLines = generateInternalTypeLines(
738
733
  internalQueriesByFile,
739
- importAliasBySource
734
+ importAliasBySource,
740
735
  );
741
736
  const internalMutationsTypeLines = generateInternalTypeLines(
742
737
  internalMutationsByFile,
743
- importAliasBySource
738
+ importAliasBySource,
744
739
  );
745
740
 
746
741
  const queriesTypeDef =
@@ -767,15 +762,15 @@ function generateTypeDefs(
767
762
  function generateClientInits(
768
763
  queriesByFile: Map<string, DiscoveredHandler[]>,
769
764
  mutationsByFile: Map<string, DiscoveredHandler[]>,
770
- importAliasBySource: Map<string, string>
765
+ importAliasBySource: Map<string, string>,
771
766
  ): { queriesInit: string; mutationsInit: string } {
772
767
  const queriesClientLines = generateQueriesClientLines(
773
768
  queriesByFile,
774
- importAliasBySource
769
+ importAliasBySource,
775
770
  );
776
771
  const mutationsClientLines = generateMutationsClientLines(
777
772
  mutationsByFile,
778
- importAliasBySource
773
+ importAliasBySource,
779
774
  );
780
775
 
781
776
  const queriesInit =
@@ -788,12 +783,12 @@ function generateClientInits(
788
783
 
789
784
  function generateInternalInit(
790
785
  internalByFile: Map<string, DiscoveredHandler[]>,
791
- importAliasBySource: Map<string, string>
786
+ importAliasBySource: Map<string, string>,
792
787
  ): string {
793
788
  if (internalByFile.size === 0) return "{}";
794
789
  const lines: string[] = [];
795
790
  for (const [fileName, list] of Array.from(internalByFile.entries()).sort(
796
- (a, b) => a[0].localeCompare(b[0])
791
+ (a, b) => a[0].localeCompare(b[0]),
797
792
  )) {
798
793
  const fileKey = renderObjectKey(fileName);
799
794
  const inner = list
@@ -813,19 +808,19 @@ ${lines.join("\n")}
813
808
 
814
809
  function generateInternalMeta(
815
810
  internalByFile: Map<string, DiscoveredHandler[]>,
816
- importAliasBySource: Map<string, string>
811
+ importAliasBySource: Map<string, string>,
817
812
  ): string {
818
813
  if (internalByFile.size === 0) return "";
819
814
  const lines: string[] = [];
820
815
  for (const [fileName, list] of Array.from(internalByFile.entries()).sort(
821
- (a, b) => a[0].localeCompare(b[0])
816
+ (a, b) => a[0].localeCompare(b[0]),
822
817
  )) {
823
818
  for (const h of list.slice().sort((a, b) => a.name.localeCompare(b.name))) {
824
819
  const alias = importAliasBySource.get(h.sourceFileAbs)!;
825
820
  lines.push(
826
821
  `{ file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(
827
- h.name
828
- )}, handler: ${alias}.${h.name} },`
822
+ h.name,
823
+ )}, handler: ${alias}.${h.name} },`,
829
824
  );
830
825
  }
831
826
  }
@@ -836,6 +831,8 @@ export function generateApiClient(params: {
836
831
  handlers: DiscoveredHandler[];
837
832
  outDirAbs: string;
838
833
  authBasePath?: string;
834
+ authEnabled?: boolean;
835
+ configPathAbs?: string;
839
836
  }): string {
840
837
  const { importLines, importAliasBySource } = generateImports(params);
841
838
  const {
@@ -854,12 +851,12 @@ export function generateApiClient(params: {
854
851
  mutationsByFile,
855
852
  internalQueriesByFile,
856
853
  internalMutationsByFile,
857
- importAliasBySource
854
+ importAliasBySource,
858
855
  );
859
856
  const { queriesInit, mutationsInit } = generateClientInits(
860
857
  queriesByFile,
861
858
  mutationsByFile,
862
- importAliasBySource
859
+ importAliasBySource,
863
860
  );
864
861
  const internalHandlersCombined = new Map<string, DiscoveredHandler[]>();
865
862
  for (const [file, list] of Array.from(internalQueriesByFile.entries())) {
@@ -871,29 +868,65 @@ export function generateApiClient(params: {
871
868
  }
872
869
  const internalInit = generateInternalInit(
873
870
  internalHandlersCombined,
874
- importAliasBySource
871
+ importAliasBySource,
875
872
  );
876
873
  const internalQueriesMeta = generateInternalMeta(
877
874
  internalQueriesByFile,
878
- importAliasBySource
875
+ importAliasBySource,
879
876
  );
880
877
  const internalMutationsMeta = generateInternalMeta(
881
878
  internalMutationsByFile,
882
- importAliasBySource
879
+ importAliasBySource,
883
880
  );
884
881
 
885
882
  const authBasePathLiteral = JSON.stringify(params.authBasePath ?? "/auth");
886
883
 
884
+ // Generate config import and auth client options handling
885
+ let configImport = "";
886
+ let authClientTypeDefinitions =
887
+ "type AppflareAuthClient = ReturnType<typeof createAuthClient>;";
888
+ let authClientInit = ` const auth = createAuthClient({
889
+ ...(options.auth ?? {}),
890
+ baseURL:
891
+ (options.auth as any)?.baseURL ??
892
+ buildUrl(baseUrl, authBasePath),
893
+ });`;
894
+
895
+ if (params.authEnabled && params.configPathAbs) {
896
+ const configImportPath = toImportPathFromGeneratedSrc(
897
+ params.outDirAbs,
898
+ params.configPathAbs,
899
+ );
900
+ configImport = `\nimport __appflareConfig from ${JSON.stringify(configImportPath)};`;
901
+
902
+ // Use a factory function pattern to properly infer the client type from clientOptions
903
+ authClientTypeDefinitions = `const __getAppflareAuthClientOptions = () => (__appflareConfig.auth?.clientOptions ?? {}) as const;
904
+ type AppflareAuthClientOptions = ReturnType<typeof __getAppflareAuthClientOptions>;
905
+ const __createTypedAuthClient = (baseURL: string) => createAuthClient({
906
+ ...__getAppflareAuthClientOptions(),
907
+ baseURL,
908
+ });
909
+ type AppflareAuthClient = ReturnType<typeof __createTypedAuthClient>;`;
910
+
911
+ authClientInit = ` const auth = createAuthClient({
912
+ ...__getAppflareAuthClientOptions(),
913
+ ...(options.auth ?? {}),
914
+ baseURL:
915
+ (options.auth as any)?.baseURL ??
916
+ buildUrl(baseUrl, authBasePath),
917
+ });`;
918
+ }
919
+
887
920
  const typeBlocks = generateTypeBlocks(params.handlers, importAliasBySource);
888
921
 
889
922
  return (
890
- HEADER_TEMPLATE +
923
+ HEADER_TEMPLATE.replace("{{configImport}}", configImport) +
891
924
  importLines.join("\n") +
892
925
  TYPE_DEFINITIONS_TEMPLATE +
893
926
  typeBlocks.join("\n\n") +
894
927
  INTERNAL_TEMPLATE.replace(
895
928
  "{{internalQueriesTypeDef}}",
896
- internalQueriesTypeDef
929
+ internalQueriesTypeDef,
897
930
  )
898
931
  .replace("{{internalMutationsTypeDef}}", internalMutationsTypeDef)
899
932
  .replace("{{internalInit}}", internalInit)
@@ -903,7 +936,9 @@ export function generateApiClient(params: {
903
936
  .replace("{{mutationsTypeDef}}", mutationsTypeDef)
904
937
  .replace("{{queriesInit}}", queriesInit)
905
938
  .replace("{{mutationsInit}}", mutationsInit)
906
- .replace("{{authBasePath}}", authBasePathLiteral) +
939
+ .replace("{{authBasePath}}", authBasePathLiteral)
940
+ .replace("{{authClientTypeDefinitions}}", authClientTypeDefinitions)
941
+ .replace("{{authClientInit}}", authClientInit) +
907
942
  UTILITY_FUNCTIONS_TEMPLATE
908
943
  );
909
944
  }
@@ -89,6 +89,15 @@ export function generateWranglerJson(params: {
89
89
  };
90
90
  }
91
91
 
92
+ if (params.config.storage?.kvBinding) {
93
+ wrangler.kv_namespaces = [
94
+ {
95
+ binding: params.config.storage.kvBinding,
96
+ id: params.config.storage.kvId ?? "",
97
+ },
98
+ ];
99
+ }
100
+
92
101
  if (params.cronTriggers && params.cronTriggers.length > 0) {
93
102
  wrangler.triggers = {
94
103
  crons: Array.from(new Set(params.cronTriggers)),
@@ -24,11 +24,13 @@ export function buildAuthSection(config: AppflareConfig): AuthSection {
24
24
  ? [
25
25
  `const __appflareAuthConfig = (appflareConfig as any).auth;`,
26
26
  `const __appflareAuthBasePath = __appflareAuthConfig?.basePath ?? "/auth";`,
27
+ `const __appflareStorageConfig = (appflareConfig as any).storage;`,
28
+ `const __appflareKvBinding = __appflareStorageConfig?.kvBinding;`,
27
29
  `const __appflareAuth =`,
28
30
  `\t__appflareAuthConfig &&`,
29
31
  `\t__appflareAuthConfig.enabled !== false &&`,
30
32
  `\t__appflareAuthConfig.options`,
31
- `\t\t? initBetterAuth(__appflareAuthConfig.options as any)`,
33
+ `\t\t? initBetterAuth(__appflareAuthConfig.options as any, __appflareKvBinding)`,
32
34
  `\t\t: undefined;`,
33
35
  `const __appflareAuthRouter = __appflareAuth`,
34
36
  `\t? createBetterAuthRouter({`,
package/cli/index.ts CHANGED
@@ -39,12 +39,12 @@ program.name("appflare").description("Appflare CLI").version("0.0.0");
39
39
  program
40
40
  .command("build")
41
41
  .description(
42
- "Generate typed schema + query/mutation client/server into outDir"
42
+ "Generate typed schema + query/mutation client/server into outDir",
43
43
  )
44
44
  .option(
45
45
  "-c, --config <path>",
46
46
  "Path to appflare.config.ts",
47
- "appflare.config.ts"
47
+ "appflare.config.ts",
48
48
  )
49
49
  .option("--emit", "Also run tsc to emit JS + .d.ts into outDir/dist")
50
50
  .option("-w, --watch", "Watch for changes and rebuild")
@@ -73,7 +73,7 @@ program
73
73
  console.error(message);
74
74
  process.exitCode = 1;
75
75
  }
76
- }
76
+ },
77
77
  );
78
78
 
79
79
  void main();
@@ -83,7 +83,7 @@ async function main(): Promise<void> {
83
83
  }
84
84
 
85
85
  async function loadConfig(
86
- configPathAbs: string
86
+ configPathAbs: string,
87
87
  ): Promise<{ config: AppflareConfig; configDirAbs: string }> {
88
88
  await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
89
89
  const configDirAbs = path.dirname(configPathAbs);
@@ -92,7 +92,7 @@ async function loadConfig(
92
92
  const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
93
93
  if (!config || typeof config !== "object") {
94
94
  throw new Error(
95
- `Invalid config export in ${configPathAbs} (expected default export object)`
95
+ `Invalid config export in ${configPathAbs} (expected default export object)`,
96
96
  );
97
97
  }
98
98
  if (typeof config.dir !== "string" || !config.dir) {
@@ -133,7 +133,7 @@ async function buildFromConfig(params: {
133
133
 
134
134
  await assertDirExists(
135
135
  projectDirAbs,
136
- `Project dir not found: ${projectDirAbs}`
136
+ `Project dir not found: ${projectDirAbs}`,
137
137
  );
138
138
  await assertFileExists(schemaPathAbs, `Schema not found: ${schemaPathAbs}`);
139
139
 
@@ -143,7 +143,7 @@ async function buildFromConfig(params: {
143
143
  // Re-export the user schema inside the generated output so downstream code can import it from the build directory.
144
144
  const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
145
145
  outDirAbs,
146
- schemaPathAbs
146
+ schemaPathAbs,
147
147
  );
148
148
  const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
149
149
  export type AppflareGeneratedSchema = typeof schema;
@@ -158,7 +158,7 @@ export default schema;
158
158
  });
159
159
  await fs.writeFile(
160
160
  path.join(outDirAbs, "src", "schema-types.ts"),
161
- schemaTypesTs
161
+ schemaTypesTs,
162
162
  );
163
163
 
164
164
  // (Re)generate built-in DB handlers based on the schema tables.
@@ -179,6 +179,8 @@ export default schema;
179
179
  config.auth && config.auth.enabled === false
180
180
  ? undefined
181
181
  : (config.auth?.basePath ?? "/auth"),
182
+ authEnabled: config.auth?.enabled !== false,
183
+ configPathAbs,
182
184
  });
183
185
  await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
184
186
 
@@ -200,7 +202,7 @@ export default schema;
200
202
  });
201
203
  await fs.writeFile(
202
204
  path.join(outDirAbs, "server", "websocket-hibernation-server.ts"),
203
- websocketDoTs
205
+ websocketDoTs,
204
206
  );
205
207
 
206
208
  const schedulerTs = generateSchedulerHandlers({
@@ -211,11 +213,11 @@ export default schema;
211
213
  });
212
214
  await fs.writeFile(
213
215
  path.join(outDirAbs, "server", "scheduler.ts"),
214
- schedulerTs
216
+ schedulerTs,
215
217
  );
216
218
 
217
219
  const cronHandlersPresent = handlers.some(
218
- (handler) => handler.kind === "cron"
220
+ (handler) => handler.kind === "cron",
219
221
  );
220
222
  const { code: cronTs, cronTriggers } = generateCronHandlers({
221
223
  handlers,
@@ -228,7 +230,7 @@ export default schema;
228
230
  const allowedOrigins = normalizeAllowedOrigins(
229
231
  process.env.APPFLARE_ALLOWED_ORIGINS ??
230
232
  config.corsOrigin ??
231
- "http://localhost:3000"
233
+ "http://localhost:3000",
232
234
  );
233
235
  const workerIndexTs = generateCloudflareWorkerIndex({
234
236
  allowedOrigins,
@@ -297,7 +299,7 @@ async function watchAndBuild(params: {
297
299
 
298
300
  lastWatchConfig = normalized;
299
301
  console.log(
300
- `[appflare] watching ${normalized.targets.length} path(s) (ignoring ${normalized.ignored.length})`
302
+ `[appflare] watching ${normalized.targets.length} path(s) (ignoring ${normalized.ignored.length})`,
301
303
  );
302
304
  };
303
305
 
@@ -319,7 +321,7 @@ async function watchAndBuild(params: {
319
321
  config,
320
322
  configDirAbs,
321
323
  configPathAbs: params.configPathAbs,
322
- })
324
+ }),
323
325
  );
324
326
  console.log("[appflare] build started");
325
327
  await buildFromConfig({
@@ -384,7 +386,7 @@ function normalizeWatchConfig(config: WatchConfig): WatchConfig {
384
386
  const normalizeList = (list: string[]): string[] =>
385
387
  [
386
388
  ...new Set(
387
- list.map((item) => (hasGlob(item) ? item : path.resolve(item)))
389
+ list.map((item) => (hasGlob(item) ? item : path.resolve(item))),
388
390
  ),
389
391
  ].sort();
390
392
 
@@ -1,20 +1,29 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { BetterAuthOptions } from "better-auth";
4
+ import type { BetterAuthClientOptions } from "better-auth/client";
4
5
  import type { StorageManagerOptions } from "../../server/storage/types";
5
6
 
6
7
  export type AppflareAuthConfig<
7
- Options extends BetterAuthOptions = BetterAuthOptions,
8
+ Options extends BetterAuthOptions,
9
+ ClientOptions extends BetterAuthClientOptions,
8
10
  > = {
9
11
  enabled?: boolean;
10
12
  basePath?: string;
13
+ /** Server-side Better Auth options. */
11
14
  options?: Options;
15
+ /** Client-side Better Auth options used in the generated API client. */
16
+ clientOptions?: ClientOptions;
12
17
  };
13
-
14
18
  export type AppflareStorageConfig<
15
19
  Env = unknown,
16
20
  Principal = unknown,
17
- > = StorageManagerOptions<Env, Principal>;
21
+ > = StorageManagerOptions<Env, Principal> & {
22
+ /** Optional KV binding name created in the generated worker. */
23
+ kvBinding?: string;
24
+ /** Optional KV namespace ID used in wrangler.json. */
25
+ kvId?: string;
26
+ };
18
27
 
19
28
  export type AppflareSchedulerConfig = {
20
29
  /** Set to false to disable scheduler generation. */
@@ -27,6 +36,7 @@ export type AppflareSchedulerConfig = {
27
36
 
28
37
  export type AppflareConfig<
29
38
  Options extends BetterAuthOptions = BetterAuthOptions,
39
+ ClientOptions extends BetterAuthClientOptions = BetterAuthClientOptions,
30
40
  > = {
31
41
  dir: string;
32
42
  schema: string;
@@ -46,7 +56,7 @@ export type AppflareConfig<
46
56
  compatibilityDate?: string;
47
57
  [key: string]: unknown;
48
58
  };
49
- auth?: AppflareAuthConfig<Options>;
59
+ auth?: AppflareAuthConfig<Options, ClientOptions>;
50
60
  storage?: AppflareStorageConfig;
51
61
  scheduler?: AppflareSchedulerConfig;
52
62
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },
@@ -25,12 +25,12 @@
25
25
  "react": "^18.3.1"
26
26
  },
27
27
  "dependencies": {
28
- "better-auth": "^1.4.9",
28
+ "@hono/standard-validator": "^0.2.1",
29
+ "better-auth": "^1.4.18",
29
30
  "better-fetch": "^1.1.2",
30
31
  "chokidar": "^5.0.0",
31
- "commander": "^14.0.1",
32
- "@hono/standard-validator": "^0.2.1",
33
32
  "cloudflare-do-mongo": "^0.1.2",
33
+ "commander": "^14.0.1",
34
34
  "hono": "^4.6.8",
35
35
  "mongodb": "^7.0.0",
36
36
  "zod": "^4.1.13"
package/server/auth.ts CHANGED
@@ -41,11 +41,32 @@ export function createBetterAuthRouter<
41
41
 
42
42
  export function initBetterAuth<Options extends BetterAuthOptions>(
43
43
  options: Options,
44
+ kvBinding?: string,
44
45
  ): Auth<Options> {
45
- return betterAuth({
46
+ const authConfig: BetterAuthOptions = {
46
47
  ...options,
47
48
  database: mongodbAdapter(getDatabase((env as any).MONGO_DB) as any),
48
- });
49
+ };
50
+
51
+ // Add secondary storage if KV binding is provided
52
+ if (kvBinding && (env as any)[kvBinding]) {
53
+ (authConfig as any).secondaryStorage = {
54
+ get: async (key: string) => {
55
+ const kv = (env as any)[kvBinding];
56
+ return await kv.get(key);
57
+ },
58
+ set: async (key: string, value: string, ttl?: number) => {
59
+ const kv = (env as any)[kvBinding];
60
+ await kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
61
+ },
62
+ delete: async (key: string) => {
63
+ const kv = (env as any)[kvBinding];
64
+ await kv.delete(key);
65
+ },
66
+ };
67
+ }
68
+
69
+ return betterAuth(authConfig as Options);
49
70
  }
50
71
  export const getHeaders = (headers: Headers) => {
51
72
  const newHeaders = Object.fromEntries(headers as any);