@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,163 @@
1
+ import { extractPlainText } from "emdash";
2
+
3
+ //#region src/plugins/vectorize-search.ts
4
+ /** Safely extract a string from an unknown value */
5
+ function toString(value) {
6
+ return typeof value === "string" ? value : "";
7
+ }
8
+ /** Type guard: check if value is a record-like object */
9
+ function isRecord(value) {
10
+ return value != null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+ /**
13
+ * Get Cloudflare runtime environment from request
14
+ */
15
+ function getCloudflareEnv(request) {
16
+ const locals = request[Symbol.for("astro.locals")];
17
+ if (locals?.runtime?.env) return locals.runtime.env;
18
+ return null;
19
+ }
20
+ /**
21
+ * Extract searchable text from content entry
22
+ */
23
+ function extractSearchableText(content) {
24
+ const parts = [];
25
+ if (typeof content.title === "string") parts.push(content.title);
26
+ for (const [key, value] of Object.entries(content)) {
27
+ if (key === "title" || key === "id" || key === "slug") continue;
28
+ if (typeof value === "string") {
29
+ const text = extractPlainText(value);
30
+ if (text) parts.push(text);
31
+ } else if (Array.isArray(value)) {
32
+ const text = extractPlainText(value);
33
+ if (text) parts.push(text);
34
+ }
35
+ }
36
+ return parts.join("\n");
37
+ }
38
+ /**
39
+ * Create a Vectorize Search plugin definition
40
+ *
41
+ * Note: This returns a plain plugin definition object, not a resolved plugin.
42
+ * It should be passed to the emdash() integration's plugins array.
43
+ */
44
+ function vectorizeSearch(config = {}) {
45
+ const model = config.model ?? "@cf/bge-base-en-v1.5";
46
+ const targetCollections = config.collections;
47
+ let cachedEnv = null;
48
+ return {
49
+ id: "vectorize-search",
50
+ version: "1.0.0",
51
+ capabilities: ["read:content"],
52
+ hooks: {
53
+ "content:afterSave": { handler: async (event, _ctx) => {
54
+ const { content, collection } = event;
55
+ if (targetCollections && !targetCollections.includes(collection)) return;
56
+ if (!cachedEnv) {
57
+ console.warn("[vectorize-search] Environment not available in hook. Call the /query route first to initialize, or reindex manually.");
58
+ return;
59
+ }
60
+ const env = cachedEnv;
61
+ if (!env.AI || !env.VECTORIZE) {
62
+ console.warn("[vectorize-search] AI or VECTORIZE binding not available, skipping indexing");
63
+ return;
64
+ }
65
+ try {
66
+ const text = extractSearchableText(content);
67
+ if (!text.trim()) return;
68
+ const embedResult = await env.AI.run(model, { text: [text] });
69
+ if (!embedResult?.data?.[0]) {
70
+ console.error("[vectorize-search] Failed to generate embedding");
71
+ return;
72
+ }
73
+ const contentId = toString(content.id);
74
+ const contentSlug = toString(content.slug);
75
+ const contentTitle = toString(content.title);
76
+ await env.VECTORIZE.upsert([{
77
+ id: contentId,
78
+ values: embedResult.data[0],
79
+ metadata: {
80
+ collection,
81
+ slug: contentSlug ?? "",
82
+ title: contentTitle ?? ""
83
+ }
84
+ }]);
85
+ console.log(`[vectorize-search] Indexed ${collection}/${contentId}`);
86
+ } catch (error) {
87
+ console.error("[vectorize-search] Error indexing content:", error);
88
+ }
89
+ } },
90
+ "content:afterDelete": { handler: async (event, _ctx) => {
91
+ const { id, collection } = event;
92
+ if (targetCollections && !targetCollections.includes(collection)) return;
93
+ if (!cachedEnv?.VECTORIZE) return;
94
+ try {
95
+ await cachedEnv.VECTORIZE.deleteByIds([id]);
96
+ console.log(`[vectorize-search] Removed ${collection}/${id} from index`);
97
+ } catch (error) {
98
+ console.error("[vectorize-search] Error removing from index:", error);
99
+ }
100
+ } }
101
+ },
102
+ routes: {
103
+ query: { handler: async (ctx) => {
104
+ const { request } = ctx;
105
+ const input = isRecord(ctx.input) ? ctx.input : void 0;
106
+ const env = getCloudflareEnv(request);
107
+ if (env) cachedEnv = env;
108
+ if (!env?.AI || !env?.VECTORIZE) return {
109
+ error: "Vectorize or AI binding not available",
110
+ results: []
111
+ };
112
+ const query = typeof input?.q === "string" ? input.q : void 0;
113
+ if (!query) return {
114
+ error: "Query parameter 'q' is required",
115
+ results: []
116
+ };
117
+ try {
118
+ const embedResult = await env.AI.run(model, { text: [query] });
119
+ if (!embedResult?.data?.[0]) return {
120
+ error: "Failed to generate query embedding",
121
+ results: []
122
+ };
123
+ const queryOptions = {
124
+ topK: typeof input?.limit === "number" ? input.limit : 20,
125
+ returnMetadata: "all"
126
+ };
127
+ const collection = typeof input?.collection === "string" ? input.collection : void 0;
128
+ if (collection) queryOptions.filter = { collection };
129
+ return { results: (await env.VECTORIZE.query(embedResult.data[0], queryOptions)).matches.map((match) => ({
130
+ id: match.id,
131
+ score: match.score,
132
+ collection: toString(match.metadata?.collection),
133
+ slug: toString(match.metadata?.slug),
134
+ title: toString(match.metadata?.title)
135
+ })) };
136
+ } catch (error) {
137
+ console.error("[vectorize-search] Query error:", error);
138
+ return {
139
+ error: error instanceof Error ? error.message : "Query failed",
140
+ results: []
141
+ };
142
+ }
143
+ } },
144
+ reindex: { handler: async (ctx) => {
145
+ const { request } = ctx;
146
+ const env = getCloudflareEnv(request);
147
+ if (env) cachedEnv = env;
148
+ return {
149
+ success: false,
150
+ error: "REINDEX_NOT_SUPPORTED"
151
+ };
152
+ } }
153
+ },
154
+ admin: { pages: [{
155
+ path: "/settings",
156
+ label: "Vectorize Search",
157
+ icon: "search"
158
+ }] }
159
+ };
160
+ }
161
+
162
+ //#endregion
163
+ export { vectorizeSearch };
@@ -0,0 +1,255 @@
1
+ import { WorkerEntrypoint } from "cloudflare:workers";
2
+ import { PluginManifest, SandboxEmailSendCallback, SandboxOptions, SandboxRunner, SandboxRunnerFactory, SandboxedPlugin } from "emdash";
3
+
4
+ //#region src/sandbox/runner.d.ts
5
+ interface PluginBridgeProps {
6
+ pluginId: string;
7
+ pluginVersion: string;
8
+ capabilities: string[];
9
+ allowedHosts: string[];
10
+ storageCollections: string[];
11
+ }
12
+ /**
13
+ * Cloudflare sandbox runner using Worker Loader.
14
+ */
15
+ declare class CloudflareSandboxRunner implements SandboxRunner {
16
+ private plugins;
17
+ private options;
18
+ private resolvedLimits;
19
+ private siteInfo?;
20
+ constructor(options: SandboxOptions);
21
+ /**
22
+ * Set the email send callback for sandboxed plugins.
23
+ * Called after the EmailPipeline is created, since the pipeline
24
+ * doesn't exist when the sandbox runner is constructed.
25
+ */
26
+ setEmailSend(callback: SandboxEmailSendCallback | null): void;
27
+ /**
28
+ * Check if Worker Loader is available.
29
+ */
30
+ isAvailable(): boolean;
31
+ /**
32
+ * Load a sandboxed plugin.
33
+ *
34
+ * @param manifest - Plugin manifest with capabilities and storage declarations
35
+ * @param code - The bundled plugin JavaScript code
36
+ */
37
+ load(manifest: PluginManifest, code: string): Promise<SandboxedPlugin>;
38
+ /**
39
+ * Terminate all loaded plugins.
40
+ */
41
+ terminateAll(): Promise<void>;
42
+ }
43
+ /**
44
+ * Factory function for creating the Cloudflare sandbox runner.
45
+ *
46
+ * Matches the SandboxRunnerFactory signature. The LOADER and PluginBridge
47
+ * are obtained internally from cloudflare:workers imports.
48
+ */
49
+ declare const createSandboxRunner: SandboxRunnerFactory;
50
+ //#endregion
51
+ //#region src/sandbox/bridge.d.ts
52
+ /**
53
+ * Set the email send callback for all bridge instances.
54
+ * Called by the runner when the EmailPipeline is available.
55
+ */
56
+ declare function setEmailSendCallback(callback: SandboxEmailSendCallback | null): void;
57
+ /**
58
+ * Environment bindings required by PluginBridge
59
+ */
60
+ interface PluginBridgeEnv {
61
+ DB: D1Database;
62
+ MEDIA?: R2Bucket;
63
+ }
64
+ /**
65
+ * Props passed to the bridge via ctx.props when creating the loopback binding
66
+ */
67
+ interface PluginBridgeProps$1 {
68
+ pluginId: string;
69
+ pluginVersion: string;
70
+ capabilities: string[];
71
+ allowedHosts: string[];
72
+ storageCollections: string[];
73
+ }
74
+ /**
75
+ * PluginBridge WorkerEntrypoint
76
+ *
77
+ * Provides the context API to sandboxed plugins via RPC.
78
+ * All methods validate capabilities and scope operations to the plugin.
79
+ *
80
+ * Usage:
81
+ * 1. Export this class from your worker entrypoint
82
+ * 2. Sandboxed plugins get a binding to it via ctx.exports.PluginBridge({...})
83
+ * 3. Plugins call bridge methods which validate and proxy to the database
84
+ */
85
+ declare class PluginBridge extends WorkerEntrypoint<PluginBridgeEnv, PluginBridgeProps$1> {
86
+ /**
87
+ * KV operations use _plugin_storage with a special "__kv" collection.
88
+ * This provides consistent storage across sandboxed and non-sandboxed modes.
89
+ */
90
+ kvGet(key: string): Promise<unknown>;
91
+ kvSet(key: string, value: unknown): Promise<void>;
92
+ kvDelete(key: string): Promise<boolean>;
93
+ kvList(prefix?: string): Promise<Array<{
94
+ key: string;
95
+ value: unknown;
96
+ }>>;
97
+ storageGet(collection: string, id: string): Promise<unknown>;
98
+ storagePut(collection: string, id: string, data: unknown): Promise<void>;
99
+ storageDelete(collection: string, id: string): Promise<boolean>;
100
+ storageQuery(collection: string, opts?: {
101
+ limit?: number;
102
+ cursor?: string;
103
+ }): Promise<{
104
+ items: Array<{
105
+ id: string;
106
+ data: unknown;
107
+ }>;
108
+ hasMore: boolean;
109
+ cursor?: string;
110
+ }>;
111
+ storageCount(collection: string): Promise<number>;
112
+ storageGetMany(collection: string, ids: string[]): Promise<Map<string, unknown>>;
113
+ storagePutMany(collection: string, items: Array<{
114
+ id: string;
115
+ data: unknown;
116
+ }>): Promise<void>;
117
+ storageDeleteMany(collection: string, ids: string[]): Promise<number>;
118
+ contentGet(collection: string, id: string): Promise<{
119
+ id: string;
120
+ type: string;
121
+ data: Record<string, unknown>;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ } | null>;
125
+ contentList(collection: string, opts?: {
126
+ limit?: number;
127
+ cursor?: string;
128
+ }): Promise<{
129
+ items: Array<{
130
+ id: string;
131
+ type: string;
132
+ data: Record<string, unknown>;
133
+ createdAt: string;
134
+ updatedAt: string;
135
+ }>;
136
+ cursor?: string;
137
+ hasMore: boolean;
138
+ }>;
139
+ contentCreate(collection: string, data: Record<string, unknown>): Promise<{
140
+ id: string;
141
+ type: string;
142
+ data: Record<string, unknown>;
143
+ createdAt: string;
144
+ updatedAt: string;
145
+ }>;
146
+ contentUpdate(collection: string, id: string, data: Record<string, unknown>): Promise<{
147
+ id: string;
148
+ type: string;
149
+ data: Record<string, unknown>;
150
+ createdAt: string;
151
+ updatedAt: string;
152
+ }>;
153
+ contentDelete(collection: string, id: string): Promise<boolean>;
154
+ mediaGet(id: string): Promise<{
155
+ id: string;
156
+ filename: string;
157
+ mimeType: string;
158
+ size: number | null;
159
+ url: string;
160
+ createdAt: string;
161
+ } | null>;
162
+ mediaList(opts?: {
163
+ limit?: number;
164
+ cursor?: string;
165
+ mimeType?: string;
166
+ }): Promise<{
167
+ items: Array<{
168
+ id: string;
169
+ filename: string;
170
+ mimeType: string;
171
+ size: number | null;
172
+ url: string;
173
+ createdAt: string;
174
+ }>;
175
+ cursor?: string;
176
+ hasMore: boolean;
177
+ }>;
178
+ /**
179
+ * Create a pending media record and write bytes directly to R2.
180
+ *
181
+ * Unlike the admin UI flow (presigned URL → client PUT → confirm), sandboxed
182
+ * plugins are network-isolated and can't make external requests. The bridge
183
+ * accepts the file bytes directly and writes them to storage.
184
+ *
185
+ * Returns the media ID, storage key, and confirm URL. The plugin should
186
+ * call the confirm endpoint after this to finalize the record.
187
+ */
188
+ mediaUpload(filename: string, contentType: string, bytes: ArrayBuffer): Promise<{
189
+ mediaId: string;
190
+ storageKey: string;
191
+ url: string;
192
+ }>;
193
+ mediaDelete(id: string): Promise<boolean>;
194
+ httpFetch(url: string, init?: RequestInit): Promise<{
195
+ status: number;
196
+ headers: Record<string, string>;
197
+ text: string;
198
+ }>;
199
+ userGet(id: string): Promise<{
200
+ id: string;
201
+ email: string;
202
+ name: string | null;
203
+ role: number;
204
+ createdAt: string;
205
+ } | null>;
206
+ userGetByEmail(email: string): Promise<{
207
+ id: string;
208
+ email: string;
209
+ name: string | null;
210
+ role: number;
211
+ createdAt: string;
212
+ } | null>;
213
+ userList(opts?: {
214
+ role?: number;
215
+ limit?: number;
216
+ cursor?: string;
217
+ }): Promise<{
218
+ items: Array<{
219
+ id: string;
220
+ email: string;
221
+ name: string | null;
222
+ role: number;
223
+ createdAt: string;
224
+ }>;
225
+ nextCursor?: string;
226
+ }>;
227
+ emailSend(message: {
228
+ to: string;
229
+ subject: string;
230
+ text: string;
231
+ html?: string;
232
+ }): Promise<void>;
233
+ log(level: "debug" | "info" | "warn" | "error", msg: string, data?: unknown): void;
234
+ }
235
+ //#endregion
236
+ //#region src/sandbox/wrapper.d.ts
237
+ /**
238
+ * Options for wrapper generation
239
+ *
240
+ * **Known limitation:** `site` info is baked into the generated wrapper code
241
+ * at load time. If site settings change (e.g., admin updates site name/URL),
242
+ * sandboxed plugins will see stale values until the worker restarts.
243
+ * Trusted-mode plugins always read fresh values from the database.
244
+ */
245
+ interface WrapperOptions {
246
+ /** Site info to inject into the context (no RPC needed) */
247
+ site?: {
248
+ name: string;
249
+ url: string;
250
+ locale: string;
251
+ };
252
+ }
253
+ declare function generatePluginWrapper(manifest: PluginManifest, options?: WrapperOptions): string;
254
+ //#endregion
255
+ export { CloudflareSandboxRunner, PluginBridge, type PluginBridgeEnv, type PluginBridgeProps, createSandboxRunner, generatePluginWrapper, setEmailSendCallback };