appflare 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -17,10 +17,16 @@ export default {
17
17
  dir: "./app", // Root folder containing your handlers
18
18
  schema: "./schema.ts", // Path to the Zod schema file
19
19
  outDir: "./_generated", // Where generated files are written
20
+ auth: {
21
+ // Optional: Better Auth config forwarded to the generated server
22
+ enabled: false,
23
+ basePath: "/auth",
24
+ options: {},
25
+ },
20
26
  };
21
27
  ```
22
28
 
23
- The loader in [packages/appflare/cli/core/config.ts](packages/appflare/cli/core/config.ts) validates presence and types of `dir`, `schema`, and `outDir` and resolves paths relative to the config file location.
29
+ The loader in [packages/appflare/cli/core/config.ts](packages/appflare/cli/core/config.ts) validates presence and types of `dir`, `schema`, and `outDir`, lightly checks optional `auth` (base path, enabled flag, options object), and resolves paths relative to the config file location.
24
30
 
25
31
  ## Build pipeline
26
32
 
package/cli/core/build.ts CHANGED
@@ -36,7 +36,11 @@ export async function buildFromConfig(params: {
36
36
  await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
37
37
  await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
38
38
 
39
- const schemaTypesTs = await generateSchemaTypes({ schemaPathAbs });
39
+ const schemaTypesTs = await generateSchemaTypes({
40
+ schemaPathAbs,
41
+ configPathAbs,
42
+ outDirAbs,
43
+ });
40
44
  await fs.writeFile(
41
45
  path.join(outDirAbs, "src", "schema-types.ts"),
42
46
  schemaTypesTs
@@ -53,13 +57,22 @@ export async function buildFromConfig(params: {
53
57
  configPathAbs,
54
58
  });
55
59
 
56
- const apiTs = generateApiClient({ handlers, outDirAbs });
60
+ const apiTs = generateApiClient({
61
+ handlers,
62
+ outDirAbs,
63
+ authBasePath:
64
+ config.auth && config.auth.enabled === false
65
+ ? undefined
66
+ : (config.auth?.basePath ?? "/auth"),
67
+ });
57
68
  await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
58
69
 
59
70
  const serverTs = generateHonoServer({
60
71
  handlers,
61
72
  outDirAbs,
62
73
  schemaPathAbs,
74
+ configPathAbs,
75
+ config,
63
76
  });
64
77
  await fs.writeFile(path.join(outDirAbs, "server", "server.ts"), serverTs);
65
78
 
@@ -25,5 +25,53 @@ export async function loadConfig(
25
25
  if (typeof config.outDir !== "string" || !config.outDir) {
26
26
  throw new Error(`Invalid config.outDir in ${configPathAbs}`);
27
27
  }
28
+
29
+ const auth = (config as AppflareConfig).auth;
30
+ if (auth !== undefined) {
31
+ if (!auth || typeof auth !== "object") {
32
+ throw new Error(`Invalid config.auth in ${configPathAbs}`);
33
+ }
34
+ if (auth.basePath !== undefined && typeof auth.basePath !== "string") {
35
+ throw new Error(`Invalid config.auth.basePath in ${configPathAbs}`);
36
+ }
37
+ if (auth.enabled !== undefined && typeof auth.enabled !== "boolean") {
38
+ throw new Error(`Invalid config.auth.enabled in ${configPathAbs}`);
39
+ }
40
+ if (auth.options !== undefined && typeof auth.options !== "object") {
41
+ throw new Error(`Invalid config.auth.options in ${configPathAbs}`);
42
+ }
43
+ }
44
+
45
+ const storage = (config as AppflareConfig).storage;
46
+ if (storage !== undefined) {
47
+ if (!storage || typeof storage !== "object") {
48
+ throw new Error(`Invalid config.storage in ${configPathAbs}`);
49
+ }
50
+ if (!Array.isArray(storage.rules)) {
51
+ throw new Error(`Invalid config.storage.rules in ${configPathAbs}`);
52
+ }
53
+ if (
54
+ storage.basePath !== undefined &&
55
+ typeof storage.basePath !== "string"
56
+ ) {
57
+ throw new Error(`Invalid config.storage.basePath in ${configPathAbs}`);
58
+ }
59
+ if (
60
+ storage.bucketBinding !== undefined &&
61
+ typeof storage.bucketBinding !== "string"
62
+ ) {
63
+ throw new Error(
64
+ `Invalid config.storage.bucketBinding in ${configPathAbs}`
65
+ );
66
+ }
67
+ if (
68
+ storage.defaultCacheControl !== undefined &&
69
+ typeof storage.defaultCacheControl !== "string"
70
+ ) {
71
+ throw new Error(
72
+ `Invalid config.storage.defaultCacheControl in ${configPathAbs}`
73
+ );
74
+ }
75
+ }
28
76
  return { config: config as AppflareConfig, configDirAbs };
29
77
  }
package/cli/core/index.ts CHANGED
@@ -105,7 +105,11 @@ async function buildFromConfig(params: {
105
105
  await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
106
106
  await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
107
107
 
108
- const schemaTypesTs = await generateSchemaTypes({ schemaPathAbs });
108
+ const schemaTypesTs = await generateSchemaTypes({
109
+ schemaPathAbs,
110
+ configPathAbs,
111
+ outDirAbs,
112
+ });
109
113
  await fs.writeFile(
110
114
  path.join(outDirAbs, "src", "schema-types.ts"),
111
115
  schemaTypesTs
@@ -122,13 +126,22 @@ async function buildFromConfig(params: {
122
126
  configPathAbs,
123
127
  });
124
128
 
125
- const apiTs = generateApiClient({ handlers, outDirAbs });
129
+ const apiTs = generateApiClient({
130
+ handlers,
131
+ outDirAbs,
132
+ authBasePath:
133
+ config.auth && config.auth.enabled === false
134
+ ? undefined
135
+ : (config.auth?.basePath ?? "/auth"),
136
+ });
126
137
  await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
127
138
 
128
139
  const serverTs = generateHonoServer({
129
140
  handlers,
130
141
  outDirAbs,
131
142
  schemaPathAbs,
143
+ configPathAbs,
144
+ config,
132
145
  });
133
146
  await fs.writeFile(path.join(outDirAbs, "server", "server.ts"), serverTs);
134
147
 
@@ -22,6 +22,8 @@ const HEADER_TEMPLATE = `/* eslint-disable */
22
22
 
23
23
  import fetch from "better-fetch";
24
24
  import { z } from "zod";
25
+ import type { BetterAuthClientOptions } from "better-auth/client";
26
+ import { createAuthClient } from "better-auth/client";
25
27
 
26
28
  import type {
27
29
  AnyValidator,
@@ -128,6 +130,39 @@ export type AppflareHandler<THandler extends AnyHandlerDefinition> = HandlerInvo
128
130
  > &
129
131
  HandlerMetadata<THandler>;
130
132
 
133
+ export type StoragePutResult = {
134
+ key: string;
135
+ size: number;
136
+ contentType: string;
137
+ cacheControl: string;
138
+ };
139
+
140
+ export type StorageDeleteResult = {
141
+ key: string;
142
+ deleted: boolean;
143
+ };
144
+
145
+ export type StorageManagerClient = {
146
+ url: (path: string) => string;
147
+ get: (path: string, init?: RequestInit) => Promise<Response>;
148
+ head: (path: string, init?: RequestInit) => Promise<Response>;
149
+ put: (
150
+ path: string,
151
+ body: BodyInit,
152
+ init?: RequestInit
153
+ ) => Promise<StoragePutResult>;
154
+ post: (
155
+ path: string,
156
+ body: BodyInit,
157
+ init?: RequestInit
158
+ ) => Promise<StoragePutResult>;
159
+ delete: (path: string, init?: RequestInit) => Promise<StorageDeleteResult>;
160
+ };
161
+
162
+ export type StorageManagerOptions = {
163
+ basePath?: string;
164
+ };
165
+
131
166
  type RequestExecutor = (
132
167
  input: RequestInfo | URL,
133
168
  init?: RequestInit
@@ -166,12 +201,16 @@ export type MutationsClient = {{mutationsTypeDef}};
166
201
  export type AppflareApiClient = {
167
202
  queries: QueriesClient;
168
203
  mutations: MutationsClient;
204
+ storage: StorageManagerClient;
205
+ auth?: ReturnType<typeof createAuthClient>;
169
206
  };
170
207
 
171
208
  export type AppflareApiOptions = {
172
209
  baseUrl?: string;
173
210
  fetcher?: RequestExecutor;
174
211
  realtime?: RealtimeConfig;
212
+ storage?: StorageManagerOptions;
213
+ auth?: false | (BetterAuthClientOptions & { baseURL?: string });
175
214
  };
176
215
 
177
216
  export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApiClient {
@@ -180,7 +219,17 @@ export function createAppflareApi(options: AppflareApiOptions = {}): AppflareApi
180
219
  const realtime = resolveRealtimeConfig(baseUrl, options.realtime);
181
220
  const queries: QueriesClient = {{queriesInit}};
182
221
  const mutations: MutationsClient = {{mutationsInit}};
183
- return { queries, mutations };
222
+ const storage = createStorageManagerClient(baseUrl, request, options.storage);
223
+ const authBasePath = normalizeAuthBasePath({{authBasePath}}) ?? "/auth";
224
+ const auth = options.auth === false
225
+ ? undefined
226
+ : createAuthClient({
227
+ ...(options.auth ?? {}),
228
+ baseURL:
229
+ (options.auth as any)?.baseURL ??
230
+ buildUrl(baseUrl, authBasePath),
231
+ });
232
+ return { queries, mutations, storage, auth };
184
233
  }
185
234
 
186
235
  `;
@@ -281,6 +330,12 @@ function createHandlerWebsocket<TArgs, TResult>(
281
330
  `;
282
331
 
283
332
  const UTILITY_FUNCTIONS_TEMPLATE_PART3 = `
333
+ function normalizeAuthBasePath(basePath?: string | null): string | undefined {
334
+ if (!basePath) return undefined;
335
+ const prefixed = basePath.startsWith("/") ? basePath : \`/\${basePath}\`;
336
+ return prefixed.replace(/\\/+$/, "") || "/auth";
337
+ }
338
+
284
339
  function normalizeBaseUrl(baseUrl?: string): string {
285
340
  if (!baseUrl) {
286
341
  return "";
@@ -359,6 +414,55 @@ function serializeQueryValue(value: unknown): string {
359
414
  return String(value);
360
415
  }
361
416
 
417
+ function normalizeStorageBasePath(basePath?: string): string {
418
+ if (!basePath) return "/storage";
419
+ const prefixed = basePath.startsWith("/") ? basePath : \`/\${basePath}\`;
420
+ const trimmed = prefixed.replace(/\\/+$/, "");
421
+ return trimmed || "/storage";
422
+ }
423
+
424
+ function buildStoragePath(basePath: string, path: string): string {
425
+ const trimmedBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
426
+ const normalizedPath = path.startsWith("/") ? path : \`/\${path}\`;
427
+ if (
428
+ normalizedPath === trimmedBase ||
429
+ normalizedPath.startsWith(\`\${trimmedBase}/\`)
430
+ ) {
431
+ return normalizedPath;
432
+ }
433
+ return \`\${trimmedBase}\${normalizedPath}\`;
434
+ }
435
+
436
+ function createStorageManagerClient(
437
+ baseUrl: string,
438
+ request: RequestExecutor,
439
+ options?: StorageManagerOptions
440
+ ): StorageManagerClient {
441
+ const basePath = normalizeStorageBasePath(options?.basePath);
442
+ const toUrl = (path: string) => buildUrl(baseUrl, buildStoragePath(basePath, path));
443
+
444
+ const sendJson = async <T>(
445
+ method: string,
446
+ path: string,
447
+ init?: RequestInit
448
+ ): Promise<T> => {
449
+ const response = await request(toUrl(path), { ...(init ?? {}), method });
450
+ return parseJson<T>(response);
451
+ };
452
+
453
+ return {
454
+ url: toUrl,
455
+ get: (path, init) => request(toUrl(path), { ...(init ?? {}), method: "GET" }),
456
+ head: (path, init) => request(toUrl(path), { ...(init ?? {}), method: "HEAD" }),
457
+ put: (path, body, init) =>
458
+ sendJson<StoragePutResult>("PUT", path, { ...(init ?? {}), body }),
459
+ post: (path, body, init) =>
460
+ sendJson<StoragePutResult>("POST", path, { ...(init ?? {}), body }),
461
+ delete: (path, init) =>
462
+ sendJson<StorageDeleteResult>("DELETE", path, { ...(init ?? {}) }),
463
+ };
464
+ }
465
+
362
466
  function isPlainObject(value: unknown): value is Record<string, unknown> {
363
467
  return !!value && typeof value === "object" && !Array.isArray(value);
364
468
  }
@@ -498,6 +602,7 @@ function generateClientInits(
498
602
  export function generateApiClient(params: {
499
603
  handlers: DiscoveredHandler[];
500
604
  outDirAbs: string;
605
+ authBasePath?: string;
501
606
  }): string {
502
607
  const { importLines, importAliasBySource } = generateImports(params);
503
608
  const { queriesByFile, mutationsByFile } = generateGroupedHandlers(
@@ -513,6 +618,8 @@ export function generateApiClient(params: {
513
618
  importAliasBySource
514
619
  );
515
620
 
621
+ const authBasePathLiteral = JSON.stringify(params.authBasePath ?? "/auth");
622
+
516
623
  const typeBlocks = generateTypeBlocks(params.handlers, importAliasBySource);
517
624
 
518
625
  return (
@@ -523,7 +630,8 @@ export function generateApiClient(params: {
523
630
  CLIENT_TYPES_TEMPLATE.replace("{{queriesTypeDef}}", queriesTypeDef)
524
631
  .replace("{{mutationsTypeDef}}", mutationsTypeDef)
525
632
  .replace("{{queriesInit}}", queriesInit)
526
- .replace("{{mutationsInit}}", mutationsInit) +
633
+ .replace("{{mutationsInit}}", mutationsInit)
634
+ .replace("{{authBasePath}}", authBasePathLiteral) +
527
635
  UTILITY_FUNCTIONS_TEMPLATE
528
636
  );
529
637
  }
@@ -0,0 +1,195 @@
1
+ import path from "node:path";
2
+ import type { AppflareConfig } from "../utils/utils";
3
+
4
+ const DEFAULT_ALLOWED_ORIGINS = ["http://localhost:3000"];
5
+
6
+ const resolveAllowedOrigins = (origins?: string[]): string[] =>
7
+ origins && origins.length > 0 ? origins : DEFAULT_ALLOWED_ORIGINS;
8
+
9
+ const sanitizeWorkerName = (configDirAbs: string): string => {
10
+ const base = path.basename(configDirAbs);
11
+ const slug = base.replace(/[^A-Za-z0-9_-]/g, "-").toLowerCase();
12
+ return slug || "appflare-worker";
13
+ };
14
+
15
+ const toBucketName = (binding: string): string =>
16
+ binding.toLowerCase().replace(/_/g, "-") || "appflare-storage";
17
+
18
+ export function generateCloudflareWorkerIndex(params: {
19
+ allowedOrigins?: string[];
20
+ }): string {
21
+ const allowedOrigins = resolveAllowedOrigins(params.allowedOrigins);
22
+ const allowedOriginsCsv = allowedOrigins.join(",");
23
+
24
+ return `/* eslint-disable */
25
+ /**
26
+ * This file is auto-generated by the Appflare CLI.
27
+ * Do not edit directly.
28
+ */
29
+
30
+ import { createAppflareHonoServer } from "./server";
31
+ import { WebSocketHibernationServer } from "./websocket-hibernation-server";
32
+ import { getDatabase } from "cloudflare-do-mongo";
33
+ import { MONGO_DURABLE_OBJECT } from "cloudflare-do-mongo/do";
34
+ import type { Hono } from "hono";
35
+ import { cors } from "hono/cors";
36
+ import { Db } from "mongodb";
37
+
38
+ type DurableObjectNamespaceLike = {
39
+ idFromName(name: string): any;
40
+ get(id: any): { fetch(input: any, init?: RequestInit): Promise<Response> };
41
+ };
42
+
43
+ type Env = {
44
+ MONGO_DB: unknown;
45
+ MONGO_URI?: string;
46
+ WEBSOCKET_HIBERNATION_SERVER: DurableObjectNamespaceLike;
47
+ MONGO_DURABLE_OBJECT: DurableObjectNamespaceLike;
48
+ APPFLARE_STORAGE?: unknown;
49
+ ALLOWED_ORIGINS?: string;
50
+ };
51
+
52
+ type WorkerEnv = { Bindings: Env };
53
+
54
+ const parseAllowedOrigins = (value?: string | null): string[] =>
55
+ (value ?? ${JSON.stringify(allowedOriginsCsv)})
56
+ .split(",")
57
+ .map((origin) => origin.trim())
58
+ .filter(Boolean);
59
+
60
+ const resolveCorsOrigin = (
61
+ origin: string | null,
62
+ allowed: string[]
63
+ ): string | undefined => {
64
+ if (!origin) return undefined;
65
+ if (allowed.includes("*")) return origin;
66
+ return allowed.includes(origin) ? origin : undefined;
67
+ };
68
+
69
+ export default {
70
+ async fetch(request, env, ctx): Promise<Response> {
71
+ const allowedOrigins = parseAllowedOrigins(env.ALLOWED_ORIGINS);
72
+ const resolveOrigin = (origin: string | null) =>
73
+ resolveCorsOrigin(origin, allowedOrigins);
74
+
75
+ const app = createAppflareHonoServer({
76
+ db: getDatabase(env.MONGO_DB) as unknown as Db,
77
+ corsOrigin: allowedOrigins,
78
+ realtime: {
79
+ durableObject: env.WEBSOCKET_HIBERNATION_SERVER,
80
+ durableObjectName: "primary",
81
+ notify: async (payload) => {
82
+ const id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("primary");
83
+ const stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
84
+ await stub.fetch("http://appflare-realtime/notify", {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body: JSON.stringify(payload),
88
+ });
89
+ },
90
+ },
91
+ }) as unknown as Hono<WorkerEnv>;
92
+
93
+ app.use(
94
+ "*",
95
+ cors({
96
+ origin: resolveOrigin,
97
+ credentials: true,
98
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
99
+ allowHeaders: ["Content-Type", "Authorization", "Cookie"],
100
+ exposeHeaders: ["set-cookie"],
101
+ })
102
+ );
103
+
104
+ const origin = request.headers.get("Origin");
105
+ const allowedOrigin = resolveOrigin(origin);
106
+ if (request.method === "OPTIONS") {
107
+ return new Response(null, {
108
+ status: 204,
109
+ headers: {
110
+ "Access-Control-Allow-Origin": allowedOrigin ?? "",
111
+ "Access-Control-Allow-Credentials": "true",
112
+ "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
113
+ "Access-Control-Allow-Headers":
114
+ request.headers.get("Access-Control-Request-Headers") ??
115
+ "Content-Type, Authorization, Cookie",
116
+ Vary: "Origin",
117
+ },
118
+ });
119
+ }
120
+
121
+ const upgradeHeader = request.headers.get("Upgrade");
122
+ if (upgradeHeader === "websocket") {
123
+ const url = new URL(request.url);
124
+ if (url.pathname === "/ws") {
125
+ const id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName("primary");
126
+ const stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
127
+ return stub.fetch(request);
128
+ }
129
+ }
130
+
131
+ const response = await app.fetch(request, env, ctx);
132
+ if (allowedOrigin) {
133
+ response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
134
+ response.headers.set("Access-Control-Allow-Credentials", "true");
135
+ response.headers.append("Vary", "Origin");
136
+ }
137
+ return response;
138
+ },
139
+ } satisfies ExportedHandler<Env>;
140
+
141
+ export { MONGO_DURABLE_OBJECT, WebSocketHibernationServer };
142
+ `;
143
+ }
144
+
145
+ export function generateWranglerJson(params: {
146
+ config: AppflareConfig;
147
+ configDirAbs: string;
148
+ allowedOrigins?: string[];
149
+ }): string {
150
+ const allowedOrigins = resolveAllowedOrigins(params.allowedOrigins);
151
+ const bucketBinding =
152
+ params.config.storage?.bucketBinding ?? "APPFLARE_STORAGE";
153
+ const r2Buckets = params.config.storage
154
+ ? [
155
+ {
156
+ binding: bucketBinding,
157
+ bucket_name: toBucketName(bucketBinding),
158
+ },
159
+ ]
160
+ : undefined;
161
+
162
+ const wrangler: Record<string, unknown> = {
163
+ $schema: "node_modules/wrangler/config-schema.json",
164
+ name: sanitizeWorkerName(params.configDirAbs),
165
+ main: "./server/index.ts",
166
+ compatibility_date: new Date().toISOString().slice(0, 10),
167
+ compatibility_flags: [
168
+ "nodejs_compat",
169
+ "nodejs_compat_populate_process_env",
170
+ ],
171
+ migrations: [
172
+ { new_sqlite_classes: ["WebSocketHibernationServer"], tag: "v1" },
173
+ { new_sqlite_classes: ["MONGO_DURABLE_OBJECT"], tag: "v2" },
174
+ ],
175
+ durable_objects: {
176
+ bindings: [
177
+ {
178
+ class_name: "WebSocketHibernationServer",
179
+ name: "WEBSOCKET_HIBERNATION_SERVER",
180
+ },
181
+ { class_name: "MONGO_DURABLE_OBJECT", name: "MONGO_DURABLE_OBJECT" },
182
+ ],
183
+ },
184
+ observability: { enabled: true },
185
+ vars: {
186
+ ALLOWED_ORIGINS: allowedOrigins.join(","),
187
+ },
188
+ };
189
+
190
+ if (r2Buckets && r2Buckets.length > 0) {
191
+ wrangler.r2_buckets = r2Buckets;
192
+ }
193
+
194
+ return `${JSON.stringify(wrangler, null, 2)}\n`;
195
+ }