@emdash-cms/cloudflare 0.0.1

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 (67) hide show
  1. package/dist/auth/index.d.mts +81 -0
  2. package/dist/auth/index.mjs +147 -0
  3. package/dist/cache/config.d.mts +52 -0
  4. package/dist/cache/config.mjs +55 -0
  5. package/dist/cache/runtime.d.mts +40 -0
  6. package/dist/cache/runtime.mjs +191 -0
  7. package/dist/d1-introspector-bZf0_ylK.mjs +57 -0
  8. package/dist/db/d1.d.mts +43 -0
  9. package/dist/db/d1.mjs +74 -0
  10. package/dist/db/do.d.mts +96 -0
  11. package/dist/db/do.mjs +489 -0
  12. package/dist/db/playground-middleware.d.mts +20 -0
  13. package/dist/db/playground-middleware.mjs +533 -0
  14. package/dist/db/playground.d.mts +39 -0
  15. package/dist/db/playground.mjs +26 -0
  16. package/dist/do-class-DY2Ba2RJ.mjs +174 -0
  17. package/dist/do-class-x5Xh_G62.d.mts +73 -0
  18. package/dist/do-dialect-BhFcRSFQ.mjs +58 -0
  19. package/dist/do-playground-routes-CmwFeGwJ.mjs +49 -0
  20. package/dist/do-types-CY0G0oyh.d.mts +14 -0
  21. package/dist/images-4RT9Ag8_.d.mts +76 -0
  22. package/dist/index.d.mts +200 -0
  23. package/dist/index.mjs +214 -0
  24. package/dist/media/images-runtime.d.mts +10 -0
  25. package/dist/media/images-runtime.mjs +215 -0
  26. package/dist/media/stream-runtime.d.mts +10 -0
  27. package/dist/media/stream-runtime.mjs +218 -0
  28. package/dist/plugins/index.d.mts +32 -0
  29. package/dist/plugins/index.mjs +163 -0
  30. package/dist/sandbox/index.d.mts +255 -0
  31. package/dist/sandbox/index.mjs +945 -0
  32. package/dist/storage/r2.d.mts +31 -0
  33. package/dist/storage/r2.mjs +116 -0
  34. package/dist/stream-DdbcvKi0.d.mts +78 -0
  35. package/package.json +109 -0
  36. package/src/auth/cloudflare-access.ts +303 -0
  37. package/src/auth/index.ts +16 -0
  38. package/src/cache/config.ts +81 -0
  39. package/src/cache/runtime.ts +328 -0
  40. package/src/cloudflare.d.ts +31 -0
  41. package/src/db/d1-introspector.ts +120 -0
  42. package/src/db/d1.ts +112 -0
  43. package/src/db/do-class.ts +275 -0
  44. package/src/db/do-dialect.ts +125 -0
  45. package/src/db/do-playground-routes.ts +65 -0
  46. package/src/db/do-preview-routes.ts +48 -0
  47. package/src/db/do-preview-sign.ts +100 -0
  48. package/src/db/do-preview.ts +268 -0
  49. package/src/db/do-types.ts +12 -0
  50. package/src/db/do.ts +62 -0
  51. package/src/db/playground-middleware.ts +340 -0
  52. package/src/db/playground-toolbar.ts +341 -0
  53. package/src/db/playground.ts +49 -0
  54. package/src/db/preview-toolbar.ts +220 -0
  55. package/src/index.ts +285 -0
  56. package/src/media/images-runtime.ts +353 -0
  57. package/src/media/images.ts +114 -0
  58. package/src/media/stream-runtime.ts +392 -0
  59. package/src/media/stream.ts +118 -0
  60. package/src/plugins/index.ts +7 -0
  61. package/src/plugins/vectorize-search.ts +393 -0
  62. package/src/sandbox/bridge.ts +1008 -0
  63. package/src/sandbox/index.ts +13 -0
  64. package/src/sandbox/runner.ts +357 -0
  65. package/src/sandbox/types.ts +181 -0
  66. package/src/sandbox/wrapper.ts +238 -0
  67. package/src/storage/r2.ts +200 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Plugin Wrapper Generator
3
+ *
4
+ * Generates the code that wraps a plugin to run in a Worker Loader isolate.
5
+ * The wrapper:
6
+ * - Imports plugin hooks and routes from a separate module ("sandbox-plugin.js")
7
+ * - Creates plugin context that proxies to BRIDGE service binding
8
+ * - Exposes hooks and routes via RPC through WorkerEntrypoint
9
+ *
10
+ * Plugin code runs in its own module scope, isolated from the wrapper template.
11
+ *
12
+ */
13
+
14
+ import type { PluginManifest } from "emdash";
15
+
16
+ const TRAILING_SLASH_RE = /\/$/;
17
+ const NEWLINE_RE = /[\n\r]/g;
18
+ const COMMENT_CLOSE_RE = /\*\//g;
19
+
20
+ /**
21
+ * Options for wrapper generation
22
+ *
23
+ * **Known limitation:** `site` info is baked into the generated wrapper code
24
+ * at load time. If site settings change (e.g., admin updates site name/URL),
25
+ * sandboxed plugins will see stale values until the worker restarts.
26
+ * Trusted-mode plugins always read fresh values from the database.
27
+ */
28
+ export interface WrapperOptions {
29
+ /** Site info to inject into the context (no RPC needed) */
30
+ site?: { name: string; url: string; locale: string };
31
+ }
32
+
33
+ export function generatePluginWrapper(manifest: PluginManifest, options?: WrapperOptions): string {
34
+ const storageCollections = Object.keys(manifest.storage || {});
35
+ const site = options?.site ?? { name: "", url: "", locale: "en" };
36
+ const hasReadUsers = manifest.capabilities.includes("read:users");
37
+ const hasEmailSend = manifest.capabilities.includes("email:send");
38
+
39
+ return `
40
+ // =============================================================================
41
+ // Sandboxed Plugin Wrapper
42
+ // Generated by @emdash-cms/cloudflare
43
+ // Plugin: ${sanitizeComment(manifest.id)}@${sanitizeComment(manifest.version)}
44
+ // =============================================================================
45
+
46
+ import { WorkerEntrypoint } from "cloudflare:workers";
47
+
48
+ // Plugin code lives in a separate module for scope isolation
49
+ import pluginModule from "sandbox-plugin.js";
50
+
51
+ // Extract hooks and routes from the plugin module
52
+ const hooks = pluginModule?.hooks || pluginModule?.default?.hooks || {};
53
+ const routes = pluginModule?.routes || pluginModule?.default?.routes || {};
54
+
55
+ // -----------------------------------------------------------------------------
56
+ // Context Factory - creates ctx that proxies to BRIDGE
57
+ // -----------------------------------------------------------------------------
58
+
59
+ function createContext(env) {
60
+ const bridge = env.BRIDGE;
61
+ const storageCollections = ${JSON.stringify(storageCollections)};
62
+
63
+ // KV - proxies to bridge.kvGet/Set/Delete/List
64
+ const kv = {
65
+ get: (key) => bridge.kvGet(key),
66
+ set: (key, value) => bridge.kvSet(key, value),
67
+ delete: (key) => bridge.kvDelete(key),
68
+ list: (prefix) => bridge.kvList(prefix)
69
+ };
70
+
71
+ // Storage collection factory
72
+ function createStorageCollection(collectionName) {
73
+ return {
74
+ get: (id) => bridge.storageGet(collectionName, id),
75
+ put: (id, data) => bridge.storagePut(collectionName, id, data),
76
+ delete: (id) => bridge.storageDelete(collectionName, id),
77
+ exists: async (id) => (await bridge.storageGet(collectionName, id)) !== null,
78
+ query: (opts) => bridge.storageQuery(collectionName, opts),
79
+ count: (where) => bridge.storageCount(collectionName, where),
80
+ getMany: (ids) => bridge.storageGetMany(collectionName, ids),
81
+ putMany: (items) => bridge.storagePutMany(collectionName, items),
82
+ deleteMany: (ids) => bridge.storageDeleteMany(collectionName, ids)
83
+ };
84
+ }
85
+
86
+ // Storage proxy that creates collections on access
87
+ const storage = new Proxy({}, {
88
+ get(_, collectionName) {
89
+ if (typeof collectionName !== "string") return undefined;
90
+ return createStorageCollection(collectionName);
91
+ }
92
+ });
93
+
94
+ // Content access - proxies to bridge (capability enforced by bridge)
95
+ const content = {
96
+ get: (collection, id) => bridge.contentGet(collection, id),
97
+ list: (collection, opts) => bridge.contentList(collection, opts),
98
+ create: (collection, data) => bridge.contentCreate(collection, data),
99
+ update: (collection, id, data) => bridge.contentUpdate(collection, id, data),
100
+ delete: (collection, id) => bridge.contentDelete(collection, id)
101
+ };
102
+
103
+ // Media access - proxies to bridge (capability enforced by bridge)
104
+ const media = {
105
+ get: (id) => bridge.mediaGet(id),
106
+ list: (opts) => bridge.mediaList(opts),
107
+ upload: (filename, contentType, bytes) => bridge.mediaUpload(filename, contentType, bytes),
108
+ getUploadUrl: () => { throw new Error("getUploadUrl is not available in sandbox mode. Use media.upload(filename, contentType, bytes) instead."); },
109
+ delete: (id) => bridge.mediaDelete(id)
110
+ };
111
+
112
+ // HTTP access - proxies to bridge (capability + host enforced by bridge)
113
+ const http = {
114
+ fetch: async (url, init) => {
115
+ const result = await bridge.httpFetch(url, init);
116
+ // Bridge returns serialized response, reconstruct Response-like object
117
+ return {
118
+ status: result.status,
119
+ ok: result.status >= 200 && result.status < 300,
120
+ headers: new Headers(result.headers),
121
+ text: async () => result.text,
122
+ json: async () => JSON.parse(result.text)
123
+ };
124
+ }
125
+ };
126
+
127
+ // Logger - proxies to bridge
128
+ const log = {
129
+ debug: (msg, data) => bridge.log("debug", msg, data),
130
+ info: (msg, data) => bridge.log("info", msg, data),
131
+ warn: (msg, data) => bridge.log("warn", msg, data),
132
+ error: (msg, data) => bridge.log("error", msg, data)
133
+ };
134
+
135
+ // Site info - injected at wrapper generation time, no RPC needed
136
+ const site = ${JSON.stringify(site)};
137
+
138
+ // URL helper - generates absolute URLs from paths
139
+ const siteBaseUrl = ${JSON.stringify(site.url.replace(TRAILING_SLASH_RE, ""))};
140
+ function url(path) {
141
+ if (!path.startsWith("/")) {
142
+ throw new Error('URL path must start with "/", got: "' + path + '"');
143
+ }
144
+ if (path.startsWith("//")) {
145
+ throw new Error('URL path must not be protocol-relative, got: "' + path + '"');
146
+ }
147
+ return siteBaseUrl + path;
148
+ }
149
+
150
+ // User access - proxies to bridge (capability enforced by bridge)
151
+ const users = ${hasReadUsers} ? {
152
+ get: (id) => bridge.userGet(id),
153
+ getByEmail: (email) => bridge.userGetByEmail(email),
154
+ list: (opts) => bridge.userList(opts)
155
+ } : undefined;
156
+
157
+ // Email access - proxies to bridge (capability enforced by bridge)
158
+ const email = ${hasEmailSend} ? {
159
+ send: (message) => bridge.emailSend(message)
160
+ } : undefined;
161
+
162
+ return {
163
+ plugin: {
164
+ id: env.PLUGIN_ID,
165
+ version: env.PLUGIN_VERSION
166
+ },
167
+ storage,
168
+ kv,
169
+ content,
170
+ media,
171
+ http,
172
+ log,
173
+ site,
174
+ url,
175
+ users,
176
+ email
177
+ };
178
+ }
179
+
180
+ // -----------------------------------------------------------------------------
181
+ // Worker Entrypoint (RPC interface)
182
+ // -----------------------------------------------------------------------------
183
+
184
+ export default class PluginEntrypoint extends WorkerEntrypoint {
185
+ async invokeHook(hookName, event) {
186
+ const ctx = createContext(this.env);
187
+
188
+ // Find the hook handler
189
+ const hookDef = hooks[hookName];
190
+
191
+ if (!hookDef) {
192
+ // No handler for this hook - that's ok, return undefined
193
+ return undefined;
194
+ }
195
+
196
+ // Get the handler (might be wrapped in config object)
197
+ const handler = typeof hookDef === "function" ? hookDef : hookDef.handler;
198
+
199
+ if (typeof handler !== "function") {
200
+ throw new Error(\`Hook \${hookName} handler is not a function\`);
201
+ }
202
+
203
+ // Execute the hook
204
+ return handler(event, ctx);
205
+ }
206
+
207
+ async invokeRoute(routeName, input, serializedRequest) {
208
+ const ctx = createContext(this.env);
209
+
210
+ // Find the route handler
211
+ const route = routes[routeName];
212
+
213
+ if (!route) {
214
+ throw new Error(\`Route not found: \${routeName}\`);
215
+ }
216
+
217
+ // Get handler (might be direct function or object with handler)
218
+ const handler = typeof route === "function" ? route : route.handler;
219
+
220
+ if (typeof handler !== "function") {
221
+ throw new Error(\`Route \${routeName} handler is not a function\`);
222
+ }
223
+
224
+ // Execute the route handler with input, request metadata, and context
225
+ return handler({ input, request: serializedRequest, requestMeta: serializedRequest.meta }, ctx);
226
+ }
227
+ }
228
+ `;
229
+ }
230
+
231
+ /**
232
+ * Sanitize a string for inclusion in a JavaScript comment.
233
+ * Prevents comment injection via manifest.id or manifest.version containing
234
+ * newlines or comment-closing sequences.
235
+ */
236
+ function sanitizeComment(s: string): string {
237
+ return s.replace(NEWLINE_RE, " ").replace(COMMENT_CLOSE_RE, "* /");
238
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Cloudflare R2 Storage Implementation - RUNTIME ENTRY
3
+ *
4
+ * Uses R2 bindings directly when running on Cloudflare Workers.
5
+ * This avoids the AWS SDK overhead and works with the native R2 API.
6
+ *
7
+ * This module imports directly from cloudflare:workers to access R2 bindings.
8
+ * Do NOT import this at config time - use { r2 } from "@emdash-cms/cloudflare" instead.
9
+ *
10
+ * For Astro 6 / Cloudflare adapter v13+:
11
+ * - Bindings are accessed via `import { env } from 'cloudflare:workers'`
12
+ */
13
+
14
+ import { env } from "cloudflare:workers";
15
+ import type {
16
+ Storage,
17
+ UploadResult,
18
+ DownloadResult,
19
+ ListResult,
20
+ ListOptions,
21
+ SignedUploadUrl,
22
+ SignedUploadOptions,
23
+ } from "emdash";
24
+ import { EmDashStorageError } from "emdash";
25
+
26
+ /** Regex to remove trailing slashes from URLs */
27
+ const TRAILING_SLASH_REGEX = /\/$/;
28
+
29
+ /**
30
+ * R2 Storage implementation using native bindings
31
+ */
32
+ export class R2Storage implements Storage {
33
+ private bucket: R2Bucket;
34
+ private publicUrl?: string;
35
+
36
+ constructor(bucket: R2Bucket, publicUrl?: string) {
37
+ this.bucket = bucket;
38
+ this.publicUrl = publicUrl;
39
+ }
40
+
41
+ async upload(options: {
42
+ key: string;
43
+ body: Buffer | Uint8Array | ReadableStream<Uint8Array>;
44
+ contentType: string;
45
+ }): Promise<UploadResult> {
46
+ try {
47
+ const result = await this.bucket.put(options.key, options.body, {
48
+ httpMetadata: {
49
+ contentType: options.contentType,
50
+ },
51
+ });
52
+
53
+ if (!result) {
54
+ throw new EmDashStorageError(`Failed to upload file: ${options.key}`, "UPLOAD_FAILED");
55
+ }
56
+
57
+ return {
58
+ key: options.key,
59
+ url: this.getPublicUrl(options.key),
60
+ size: result.size,
61
+ };
62
+ } catch (error) {
63
+ if (error instanceof EmDashStorageError) throw error;
64
+ throw new EmDashStorageError(
65
+ `Failed to upload file: ${options.key}`,
66
+ "UPLOAD_FAILED",
67
+ error,
68
+ );
69
+ }
70
+ }
71
+
72
+ async download(key: string): Promise<DownloadResult> {
73
+ try {
74
+ const object = await this.bucket.get(key);
75
+
76
+ if (!object) {
77
+ throw new EmDashStorageError(`File not found: ${key}`, "NOT_FOUND");
78
+ }
79
+
80
+ // R2ObjectBody has the body property — use it as a type guard
81
+ if (!("body" in object) || !object.body) {
82
+ throw new EmDashStorageError(`File not found: ${key}`, "NOT_FOUND");
83
+ }
84
+
85
+ return {
86
+ body: object.body,
87
+ contentType: object.httpMetadata?.contentType || "application/octet-stream",
88
+ size: object.size,
89
+ };
90
+ } catch (error) {
91
+ if (error instanceof EmDashStorageError) throw error;
92
+ throw new EmDashStorageError(`Failed to download file: ${key}`, "DOWNLOAD_FAILED", error);
93
+ }
94
+ }
95
+
96
+ async delete(key: string): Promise<void> {
97
+ try {
98
+ await this.bucket.delete(key);
99
+ } catch (error) {
100
+ // R2 delete is idempotent
101
+ throw new EmDashStorageError(`Failed to delete file: ${key}`, "DELETE_FAILED", error);
102
+ }
103
+ }
104
+
105
+ async exists(key: string): Promise<boolean> {
106
+ try {
107
+ const object = await this.bucket.head(key);
108
+ return object !== null;
109
+ } catch (error) {
110
+ throw new EmDashStorageError(
111
+ `Failed to check file existence: ${key}`,
112
+ "HEAD_FAILED",
113
+ error,
114
+ );
115
+ }
116
+ }
117
+
118
+ async list(options: ListOptions = {}): Promise<ListResult> {
119
+ try {
120
+ const response = await this.bucket.list({
121
+ prefix: options.prefix,
122
+ limit: options.limit,
123
+ cursor: options.cursor,
124
+ });
125
+
126
+ return {
127
+ files: response.objects.map((item) => ({
128
+ key: item.key,
129
+ size: item.size,
130
+ lastModified: item.uploaded,
131
+ etag: item.etag,
132
+ })),
133
+ nextCursor: response.truncated ? response.cursor : undefined,
134
+ };
135
+ } catch (error) {
136
+ throw new EmDashStorageError("Failed to list files", "LIST_FAILED", error);
137
+ }
138
+ }
139
+
140
+ async getSignedUploadUrl(_options: SignedUploadOptions): Promise<SignedUploadUrl> {
141
+ // R2 doesn't support pre-signed URLs in the same way as S3
142
+ // For R2, uploads go through the Worker
143
+ // This method is here for interface compatibility but throws an error
144
+ throw new EmDashStorageError(
145
+ "R2 bindings do not support pre-signed upload URLs. " +
146
+ "Use the S3 API with R2 credentials for signed URL support, " +
147
+ "or upload through the Worker.",
148
+ "NOT_SUPPORTED",
149
+ );
150
+ }
151
+
152
+ getPublicUrl(key: string): string {
153
+ if (this.publicUrl) {
154
+ return `${this.publicUrl.replace(TRAILING_SLASH_REGEX, "")}/${key}`;
155
+ }
156
+ // Without a public URL, we can't generate one for R2 bindings
157
+ // Return a relative path that should be served through the API
158
+ return `/_emdash/api/media/file/${key}`;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Create R2 storage adapter
164
+ * This is the factory function called at runtime
165
+ *
166
+ * Uses cloudflare:workers to access bindings directly.
167
+ */
168
+ export function createStorage(config: Record<string, unknown>): Storage {
169
+ const binding = typeof config.binding === "string" ? config.binding : "";
170
+ const publicUrl = typeof config.publicUrl === "string" ? config.publicUrl : undefined;
171
+
172
+ if (!binding) {
173
+ throw new EmDashStorageError(
174
+ `R2 binding name is required in storage config.`,
175
+ "BINDING_NOT_FOUND",
176
+ );
177
+ }
178
+
179
+ // env from cloudflare:workers doesn't have an index signature, so cast is needed
180
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- R2Bucket binding accessed from untyped env object
181
+ const bucket = (env as Record<string, unknown>)[binding] as R2Bucket | undefined;
182
+
183
+ if (!bucket) {
184
+ throw new EmDashStorageError(
185
+ `R2 binding "${binding}" not found. ` +
186
+ `Make sure the binding is defined in wrangler.jsonc and ` +
187
+ `you're running on Cloudflare Workers.\n\n` +
188
+ `Example wrangler.jsonc:\n` +
189
+ `{\n` +
190
+ ` "r2_buckets": [{\n` +
191
+ ` "binding": "${binding}",\n` +
192
+ ` "bucket_name": "my-bucket"\n` +
193
+ ` }]\n` +
194
+ `}`,
195
+ "BINDING_NOT_FOUND",
196
+ );
197
+ }
198
+
199
+ return new R2Storage(bucket, publicUrl);
200
+ }