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 +7 -1
- package/cli/core/build.ts +15 -2
- package/cli/core/config.ts +48 -0
- package/cli/core/index.ts +15 -2
- package/cli/generators/generate-api-client/index.ts +110 -2
- package/cli/generators/generate-cloudflare-worker.ts +195 -0
- package/cli/generators/generate-hono-server.ts +189 -25
- package/cli/generators/generate-websocket-durable-object.ts +5 -1
- package/cli/index.ts +44 -2
- package/cli/schema/schema-static-types.ts +2 -2
- package/cli/schema/schema.ts +54 -1
- package/cli/utils/utils.ts +22 -1
- package/package.json +8 -3
- package/react/hooks/useQuery.ts +1 -1
- package/server/README.md +65 -0
- package/server/auth.ts +75 -0
- package/server/database/context.ts +1 -27
- package/server/storage/auth.ts +14 -0
- package/server/storage/bucket.ts +22 -0
- package/server/storage/context.ts +34 -0
- package/server/storage/index.ts +38 -0
- package/server/storage/operations.ts +149 -0
- package/server/storage/route-handler.ts +60 -0
- package/server/storage/types.ts +55 -0
- package/server/storage/utils.ts +47 -0
- package/server/storage.ts +6 -0
- package/server/utils/id-utils.ts +11 -3
- package/tsconfig.json +2 -1
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({
|
|
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({
|
|
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
|
|
package/cli/core/config.ts
CHANGED
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
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
|
+
}
|