@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,13 @@
1
+ /**
2
+ * Cloudflare Sandbox Runner - RUNTIME ENTRY
3
+ *
4
+ * This module is loaded at runtime when plugins need to be sandboxed.
5
+ * It imports cloudflare:workers and should NOT be imported at config time.
6
+ *
7
+ * For config-time usage, import { sandbox } from "@emdash-cms/cloudflare" instead.
8
+ *
9
+ */
10
+
11
+ export { CloudflareSandboxRunner, createSandboxRunner, type PluginBridgeProps } from "./runner.js";
12
+ export { PluginBridge, setEmailSendCallback, type PluginBridgeEnv } from "./bridge.js";
13
+ export { generatePluginWrapper } from "./wrapper.js";
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Cloudflare Sandbox Runner
3
+ *
4
+ * Uses Worker Loader to run plugins in isolated V8 isolates.
5
+ * Plugins communicate with the host via a BRIDGE service binding
6
+ * that enforces capabilities and scopes operations.
7
+ *
8
+ * This module imports directly from cloudflare:workers to access
9
+ * the LOADER binding and PluginBridge export. It's only loaded
10
+ * when the user configures `sandboxRunner: "@emdash-cms/cloudflare/sandbox"`.
11
+ *
12
+ */
13
+
14
+ import { env, exports } from "cloudflare:workers";
15
+ import type {
16
+ SandboxRunner,
17
+ SandboxedPlugin,
18
+ SandboxEmailSendCallback,
19
+ SandboxOptions,
20
+ SandboxRunnerFactory,
21
+ SerializedRequest,
22
+ PluginManifest,
23
+ } from "emdash";
24
+
25
+ import { setEmailSendCallback } from "./bridge.js";
26
+ import type { WorkerLoader, WorkerStub, PluginBridgeBinding, WorkerLoaderLimits } from "./types.js";
27
+ import { generatePluginWrapper } from "./wrapper.js";
28
+
29
+ /**
30
+ * Default resource limits for sandboxed plugins.
31
+ *
32
+ * cpuMs and subrequests are enforced by Worker Loader at the V8 isolate level.
33
+ * wallTimeMs is enforced by the runner via Promise.race.
34
+ * memoryMb is declared for API compatibility but NOT currently enforced —
35
+ * Worker Loader doesn't expose a memory limit option. V8 isolates have a
36
+ * platform-level memory ceiling (~128MB) but it's not configurable per-worker.
37
+ */
38
+ const DEFAULT_LIMITS = {
39
+ cpuMs: 50,
40
+ memoryMb: 128,
41
+ subrequests: 10,
42
+ wallTimeMs: 30_000,
43
+ } as const;
44
+
45
+ export interface PluginBridgeProps {
46
+ pluginId: string;
47
+ pluginVersion: string;
48
+ capabilities: string[];
49
+ allowedHosts: string[];
50
+ storageCollections: string[];
51
+ }
52
+
53
+ /**
54
+ * Get the Worker Loader binding from env
55
+ */
56
+ function getLoader(): WorkerLoader | null {
57
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Worker Loader binding accessed from untyped env object
58
+ return (env as Record<string, unknown>).LOADER as WorkerLoader | null;
59
+ }
60
+
61
+ /**
62
+ * Get the PluginBridge from exports (loopback binding)
63
+ */
64
+ function getPluginBridge(): ((opts: { props: PluginBridgeProps }) => PluginBridgeBinding) | null {
65
+ // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- PluginBridge accessed from untyped cloudflare:workers exports
66
+ return (exports as Record<string, unknown>).PluginBridge as
67
+ | ((opts: { props: PluginBridgeProps }) => PluginBridgeBinding)
68
+ | null;
69
+ }
70
+
71
+ /**
72
+ * Resolved resource limits with defaults applied.
73
+ */
74
+ interface ResolvedLimits {
75
+ cpuMs: number;
76
+ memoryMb: number;
77
+ subrequests: number;
78
+ wallTimeMs: number;
79
+ }
80
+
81
+ /**
82
+ * Resolve resource limits by merging user-provided overrides with defaults.
83
+ */
84
+ function resolveLimits(limits?: SandboxOptions["limits"]): ResolvedLimits {
85
+ return {
86
+ cpuMs: limits?.cpuMs ?? DEFAULT_LIMITS.cpuMs,
87
+ memoryMb: limits?.memoryMb ?? DEFAULT_LIMITS.memoryMb,
88
+ subrequests: limits?.subrequests ?? DEFAULT_LIMITS.subrequests,
89
+ wallTimeMs: limits?.wallTimeMs ?? DEFAULT_LIMITS.wallTimeMs,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Cloudflare sandbox runner using Worker Loader.
95
+ */
96
+ export class CloudflareSandboxRunner implements SandboxRunner {
97
+ private plugins = new Map<string, CloudflareSandboxedPlugin>();
98
+ private options: SandboxOptions;
99
+ private resolvedLimits: ResolvedLimits;
100
+ private siteInfo?: { name: string; url: string; locale: string };
101
+
102
+ constructor(options: SandboxOptions) {
103
+ this.options = options;
104
+ this.resolvedLimits = resolveLimits(options.limits);
105
+ this.siteInfo = options.siteInfo;
106
+
107
+ // Wire email send callback if provided at construction time
108
+ setEmailSendCallback(options.emailSend ?? null);
109
+ }
110
+
111
+ /**
112
+ * Set the email send callback for sandboxed plugins.
113
+ * Called after the EmailPipeline is created, since the pipeline
114
+ * doesn't exist when the sandbox runner is constructed.
115
+ */
116
+ setEmailSend(callback: SandboxEmailSendCallback | null): void {
117
+ setEmailSendCallback(callback);
118
+ }
119
+
120
+ /**
121
+ * Check if Worker Loader is available.
122
+ */
123
+ isAvailable(): boolean {
124
+ return !!getLoader() && !!getPluginBridge();
125
+ }
126
+
127
+ /**
128
+ * Load a sandboxed plugin.
129
+ *
130
+ * @param manifest - Plugin manifest with capabilities and storage declarations
131
+ * @param code - The bundled plugin JavaScript code
132
+ */
133
+ async load(manifest: PluginManifest, code: string): Promise<SandboxedPlugin> {
134
+ const pluginId = `${manifest.id}:${manifest.version}`;
135
+
136
+ // Return cached plugin if available
137
+ const existing = this.plugins.get(pluginId);
138
+ if (existing) return existing;
139
+
140
+ const loader = getLoader();
141
+ const pluginBridge = getPluginBridge();
142
+
143
+ if (!loader) {
144
+ throw new Error(
145
+ "Worker Loader not available. Add worker_loaders binding to wrangler config.",
146
+ );
147
+ }
148
+
149
+ if (!pluginBridge) {
150
+ throw new Error(
151
+ "PluginBridge not available. Export PluginBridge from your worker entrypoint.",
152
+ );
153
+ }
154
+
155
+ const plugin = new CloudflareSandboxedPlugin(
156
+ manifest,
157
+ code,
158
+ loader,
159
+ pluginBridge,
160
+ this.resolvedLimits,
161
+ this.siteInfo,
162
+ );
163
+
164
+ this.plugins.set(pluginId, plugin);
165
+ return plugin;
166
+ }
167
+
168
+ /**
169
+ * Terminate all loaded plugins.
170
+ */
171
+ async terminateAll(): Promise<void> {
172
+ for (const plugin of this.plugins.values()) {
173
+ await plugin.terminate();
174
+ }
175
+ this.plugins.clear();
176
+ }
177
+ }
178
+
179
+ /**
180
+ * A plugin running in a Worker Loader isolate.
181
+ *
182
+ * IMPORTANT: Worker stubs and bridge bindings are tied to request context.
183
+ * We must create fresh stubs for each invocation to avoid I/O isolation errors:
184
+ * "Cannot perform I/O on behalf of a different request"
185
+ */
186
+ class CloudflareSandboxedPlugin implements SandboxedPlugin {
187
+ readonly id: string;
188
+ readonly manifest: PluginManifest;
189
+ private loader: WorkerLoader;
190
+ private createBridge: (opts: { props: PluginBridgeProps }) => PluginBridgeBinding;
191
+ private code: string;
192
+ private wrapperCode: string | null = null;
193
+ private limits: ResolvedLimits;
194
+ private siteInfo?: { name: string; url: string; locale: string };
195
+
196
+ constructor(
197
+ manifest: PluginManifest,
198
+ code: string,
199
+ loader: WorkerLoader,
200
+ createBridge: (opts: { props: PluginBridgeProps }) => PluginBridgeBinding,
201
+ limits: ResolvedLimits,
202
+ siteInfo?: { name: string; url: string; locale: string },
203
+ ) {
204
+ this.id = `${manifest.id}:${manifest.version}`;
205
+ this.manifest = manifest;
206
+ this.code = code;
207
+ this.loader = loader;
208
+ this.createBridge = createBridge;
209
+ this.limits = limits;
210
+ this.siteInfo = siteInfo;
211
+ }
212
+
213
+ /**
214
+ * Create a fresh worker stub for the current request.
215
+ *
216
+ * Worker Loader stubs contain bindings (like BRIDGE) that are tied to the
217
+ * request context in which they were created. Reusing stubs across requests
218
+ * causes "Cannot perform I/O on behalf of a different request" errors.
219
+ *
220
+ * The Worker Loader internally caches the V8 isolate, so we only pay the
221
+ * cost of creating the bridge binding and stub wrapper per request.
222
+ */
223
+ private createWorker(): WorkerStub {
224
+ // Cache the wrapper code (CPU-bound, no I/O context issues)
225
+ if (!this.wrapperCode) {
226
+ this.wrapperCode = generatePluginWrapper(this.manifest, {
227
+ site: this.siteInfo,
228
+ });
229
+ }
230
+
231
+ // Create fresh bridge binding for THIS request
232
+ const bridgeBinding = this.createBridge({
233
+ props: {
234
+ pluginId: this.manifest.id,
235
+ pluginVersion: this.manifest.version || "0.0.0",
236
+ capabilities: this.manifest.capabilities || [],
237
+ allowedHosts: this.manifest.allowedHosts || [],
238
+ storageCollections: Object.keys(this.manifest.storage || {}),
239
+ },
240
+ });
241
+
242
+ // Build Worker Loader limits from resolved resource limits
243
+ const loaderLimits: WorkerLoaderLimits = {
244
+ cpuMs: this.limits.cpuMs,
245
+ subRequests: this.limits.subrequests,
246
+ };
247
+
248
+ // Get a fresh stub with the new bridge binding.
249
+ // Worker Loader caches the isolate but the stub/bindings are per-call.
250
+ return this.loader.get(this.id, () => ({
251
+ compatibilityDate: "2025-01-01",
252
+ mainModule: "plugin.js",
253
+ modules: {
254
+ "plugin.js": { js: this.wrapperCode! },
255
+ "sandbox-plugin.js": { js: this.code },
256
+ },
257
+ // Block direct network access - plugins must use ctx.http via bridge
258
+ globalOutbound: null,
259
+ // Enforce resource limits at the V8 isolate level
260
+ limits: loaderLimits,
261
+ env: {
262
+ // Plugin metadata
263
+ PLUGIN_ID: this.manifest.id,
264
+ PLUGIN_VERSION: this.manifest.version || "0.0.0",
265
+ // Bridge binding for all host operations
266
+ BRIDGE: bridgeBinding,
267
+ },
268
+ }));
269
+ }
270
+
271
+ /**
272
+ * Run a function with wall-time enforcement.
273
+ *
274
+ * CPU limits and subrequest limits are enforced by the Worker Loader
275
+ * at the V8 isolate level. Wall-time is enforced here because Worker
276
+ * Loader doesn't expose a wall-time limit — a plugin could stall
277
+ * indefinitely waiting on network I/O.
278
+ */
279
+ private async withWallTimeLimit<T>(operation: string, fn: () => Promise<T>): Promise<T> {
280
+ const wallTimeMs = this.limits.wallTimeMs;
281
+ let timer: ReturnType<typeof setTimeout> | undefined;
282
+
283
+ const timeout = new Promise<never>((_, reject) => {
284
+ timer = setTimeout(() => {
285
+ reject(
286
+ new Error(
287
+ `Plugin ${this.manifest.id} exceeded wall-time limit of ${wallTimeMs}ms during ${operation}`,
288
+ ),
289
+ );
290
+ }, wallTimeMs);
291
+ });
292
+
293
+ try {
294
+ return await Promise.race([fn(), timeout]);
295
+ } finally {
296
+ if (timer !== undefined) clearTimeout(timer);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Invoke a hook in the sandboxed plugin.
302
+ *
303
+ * CPU and subrequest limits are enforced by Worker Loader.
304
+ * Wall-time is enforced here.
305
+ */
306
+ async invokeHook(hookName: string, event: unknown): Promise<unknown> {
307
+ return this.withWallTimeLimit(`hook:${hookName}`, () => {
308
+ const worker = this.createWorker();
309
+ const entrypoint = worker.getEntrypoint<PluginEntrypoint>("default");
310
+ return entrypoint.invokeHook(hookName, event);
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Invoke an API route in the sandboxed plugin.
316
+ *
317
+ * CPU and subrequest limits are enforced by Worker Loader.
318
+ * Wall-time is enforced here.
319
+ */
320
+ async invokeRoute(
321
+ routeName: string,
322
+ input: unknown,
323
+ request: SerializedRequest,
324
+ ): Promise<unknown> {
325
+ return this.withWallTimeLimit(`route:${routeName}`, () => {
326
+ const worker = this.createWorker();
327
+ const entrypoint = worker.getEntrypoint<PluginEntrypoint>("default");
328
+ return entrypoint.invokeRoute(routeName, input, request);
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Terminate the sandboxed plugin.
334
+ */
335
+ async terminate(): Promise<void> {
336
+ // Worker Loader manages isolate lifecycle - nothing to do here
337
+ this.wrapperCode = null;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * The RPC interface exposed by the plugin wrapper.
343
+ */
344
+ interface PluginEntrypoint {
345
+ invokeHook(hookName: string, event: unknown): Promise<unknown>;
346
+ invokeRoute(routeName: string, input: unknown, request: SerializedRequest): Promise<unknown>;
347
+ }
348
+
349
+ /**
350
+ * Factory function for creating the Cloudflare sandbox runner.
351
+ *
352
+ * Matches the SandboxRunnerFactory signature. The LOADER and PluginBridge
353
+ * are obtained internally from cloudflare:workers imports.
354
+ */
355
+ export const createSandboxRunner: SandboxRunnerFactory = (options) => {
356
+ return new CloudflareSandboxRunner(options);
357
+ };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Cloudflare-specific types for sandbox runner
3
+ */
4
+
5
+ import type { D1Database, R2Bucket } from "@cloudflare/workers-types";
6
+
7
+ /**
8
+ * Environment bindings required for sandbox runner.
9
+ * These must be configured in wrangler.jsonc.
10
+ */
11
+ export interface CloudflareSandboxEnv {
12
+ /** Worker Loader binding for spawning plugin isolates */
13
+ LOADER?: WorkerLoader;
14
+ /** D1 database for plugin storage and bridge operations */
15
+ DB: D1Database;
16
+ /** R2 bucket for plugin code storage (optional if loading from config) */
17
+ PLUGINS?: R2Bucket;
18
+ }
19
+
20
+ /**
21
+ * Worker Loader binding type.
22
+ * This is the API provided by Cloudflare's Worker Loader feature.
23
+ */
24
+ export interface WorkerLoader {
25
+ /**
26
+ * Get or create a dynamic worker instance.
27
+ *
28
+ * @param name - Unique identifier for this worker instance
29
+ * @param config - Configuration function returning worker setup
30
+ * @returns A stub to interact with the dynamic worker
31
+ */
32
+ get(name: string, config: () => WorkerLoaderConfig | Promise<WorkerLoaderConfig>): WorkerStub;
33
+ }
34
+
35
+ /**
36
+ * Configuration for a dynamically loaded worker.
37
+ */
38
+ export interface WorkerLoaderConfig {
39
+ /** Compatibility date for the worker */
40
+ compatibilityDate?: string;
41
+ /** Name of the main module (must be in modules) */
42
+ mainModule: string;
43
+ /** Map of module names to their code */
44
+ modules: Record<string, string | { js: string }>;
45
+ /** Environment bindings to pass to the worker */
46
+ env?: Record<string, unknown>;
47
+ /**
48
+ * Outbound fetch handler.
49
+ * Set to null to block all network access.
50
+ * Set to a service binding to intercept/proxy requests.
51
+ */
52
+ globalOutbound?: null | object;
53
+ /**
54
+ * Resource limits enforced at the V8 isolate level.
55
+ * Analogous to Workers for Platforms custom limits.
56
+ */
57
+ limits?: WorkerLoaderLimits;
58
+ }
59
+
60
+ /**
61
+ * Resource limits for a dynamically loaded worker.
62
+ * Enforced by the Worker Loader runtime at the V8 isolate level.
63
+ */
64
+ export interface WorkerLoaderLimits {
65
+ /** Maximum CPU time in milliseconds per invocation */
66
+ cpuMs?: number;
67
+ /** Maximum number of subrequests (fetch/service-binding calls) per invocation */
68
+ subRequests?: number;
69
+ }
70
+
71
+ /**
72
+ * Stub returned by Worker Loader for interacting with dynamic workers.
73
+ */
74
+ export interface WorkerStub {
75
+ /**
76
+ * Get the default entrypoint (fetch handler).
77
+ */
78
+ fetch(request: Request): Promise<Response>;
79
+
80
+ /**
81
+ * Get a named entrypoint class instance for RPC.
82
+ */
83
+ getEntrypoint<T = unknown>(name?: string): T;
84
+ }
85
+
86
+ /**
87
+ * Plugin manifest - loaded from manifest.json in plugin bundle.
88
+ */
89
+ export interface LoadedPluginManifest {
90
+ id: string;
91
+ version: string;
92
+ capabilities: string[];
93
+ allowedHosts: string[];
94
+ storage: Record<string, { indexes: Array<string | string[]> }>;
95
+ hooks: string[];
96
+ routes: string[];
97
+ }
98
+
99
+ /**
100
+ * Content item shape returned by bridge content operations.
101
+ * Matches core's ContentItem from plugins/types.ts.
102
+ */
103
+ interface BridgeContentItem {
104
+ id: string;
105
+ type: string;
106
+ data: Record<string, unknown>;
107
+ createdAt: string;
108
+ updatedAt: string;
109
+ }
110
+
111
+ /**
112
+ * Media item shape returned by bridge media operations.
113
+ * Matches core's MediaItem from plugins/types.ts.
114
+ */
115
+ interface BridgeMediaItem {
116
+ id: string;
117
+ filename: string;
118
+ mimeType: string;
119
+ size: number | null;
120
+ url: string;
121
+ createdAt: string;
122
+ }
123
+
124
+ /**
125
+ * Type for the PluginBridge binding passed to sandboxed workers.
126
+ * This is the RPC interface exposed by PluginBridge WorkerEntrypoint.
127
+ */
128
+ export interface PluginBridgeBinding {
129
+ // KV
130
+ kvGet(key: string): Promise<unknown>;
131
+ kvSet(key: string, value: unknown): Promise<void>;
132
+ kvDelete(key: string): Promise<boolean>;
133
+ kvList(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
134
+ // Storage
135
+ storageGet(collection: string, id: string): Promise<unknown>;
136
+ storagePut(collection: string, id: string, data: unknown): Promise<void>;
137
+ storageDelete(collection: string, id: string): Promise<boolean>;
138
+ storageQuery(
139
+ collection: string,
140
+ opts?: { limit?: number; cursor?: string },
141
+ ): Promise<{ items: Array<{ id: string; data: unknown }>; hasMore: boolean; cursor?: string }>;
142
+ storageCount(collection: string): Promise<number>;
143
+ storageGetMany(collection: string, ids: string[]): Promise<Map<string, unknown>>;
144
+ storagePutMany(collection: string, items: Array<{ id: string; data: unknown }>): Promise<void>;
145
+ storageDeleteMany(collection: string, ids: string[]): Promise<number>;
146
+ // Content
147
+ contentGet(collection: string, id: string): Promise<BridgeContentItem | null>;
148
+ contentList(
149
+ collection: string,
150
+ opts?: { limit?: number; cursor?: string },
151
+ ): Promise<{ items: BridgeContentItem[]; cursor?: string; hasMore: boolean }>;
152
+ contentCreate(collection: string, data: Record<string, unknown>): Promise<BridgeContentItem>;
153
+ contentUpdate(
154
+ collection: string,
155
+ id: string,
156
+ data: Record<string, unknown>,
157
+ ): Promise<BridgeContentItem>;
158
+ contentDelete(collection: string, id: string): Promise<boolean>;
159
+ // Media
160
+ mediaGet(id: string): Promise<BridgeMediaItem | null>;
161
+ mediaList(opts?: {
162
+ limit?: number;
163
+ cursor?: string;
164
+ mimeType?: string;
165
+ }): Promise<{ items: BridgeMediaItem[]; cursor?: string; hasMore: boolean }>;
166
+ mediaUpload(
167
+ filename: string,
168
+ contentType: string,
169
+ bytes: ArrayBuffer,
170
+ ): Promise<{ mediaId: string; storageKey: string; url: string }>;
171
+ mediaDelete(id: string): Promise<boolean>;
172
+ // Network
173
+ httpFetch(
174
+ url: string,
175
+ init?: RequestInit,
176
+ ): Promise<{ status: number; headers: Record<string, string>; text: string }>;
177
+ // Email
178
+ emailSend(message: { to: string; subject: string; text: string; html?: string }): Promise<void>;
179
+ // Logging
180
+ log(level: "debug" | "info" | "warn" | "error", msg: string, data?: unknown): void;
181
+ }