@edge-base/plugin-core 0.1.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.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ <h1 align="center">@edge-base/plugin-core</h1>
2
+
3
+ <p align="center">
4
+ <b>Public plugin authoring API for EdgeBase</b><br>
5
+ Define plugin factories, typed plugin contexts, and plugin testing helpers for the EdgeBase plugin ecosystem
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/@edge-base/plugin-core"><img src="https://img.shields.io/npm/v/%40edge-base%2Fplugin-core?color=brightgreen" alt="npm"></a>&nbsp;
10
+ <a href="https://edgebase.fun/docs/plugins/creating-plugins"><img src="https://img.shields.io/badge/docs-plugins-blue" alt="Docs"></a>&nbsp;
11
+ <a href="https://github.com/edge-base/edgebase/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://edgebase.fun/docs/plugins"><b>Plugins Overview</b></a> ·
16
+ <a href="https://edgebase.fun/docs/plugins/creating-plugins"><b>Creating Plugins</b></a> ·
17
+ <a href="https://edgebase.fun/docs/plugins/api-reference"><b>API Reference</b></a> ·
18
+ <a href="https://edgebase.fun/docs/plugins/using-plugins"><b>Using Plugins</b></a>
19
+ </p>
20
+
21
+ ---
22
+
23
+ `@edge-base/plugin-core` is the package plugin authors use to build installable EdgeBase plugins.
24
+
25
+ It gives you:
26
+
27
+ - `definePlugin()` for typed plugin factories
28
+ - typed contexts for functions, hooks, and migrations
29
+ - public plugin contracts like `PluginDefinition`
30
+ - `createMockContext()` for tests and local validation
31
+
32
+ This package is for **plugin packages**, not for normal application code.
33
+
34
+ > Beta: the public plugin contract is usable, but the ecosystem and tooling are still evolving.
35
+
36
+ ## Documentation Map
37
+
38
+ - [Plugins Overview](https://edgebase.fun/docs/plugins)
39
+ Understand how plugins fit into an EdgeBase project
40
+ - [Creating Plugins](https://edgebase.fun/docs/plugins/creating-plugins)
41
+ End-to-end tutorial for building a plugin package
42
+ - [Plugin API Reference](https://edgebase.fun/docs/plugins/api-reference)
43
+ Public types, contexts, and helper contracts
44
+ - [Using Plugins](https://edgebase.fun/docs/plugins/using-plugins)
45
+ What host apps do after a plugin exists
46
+
47
+ ## For AI Coding Assistants
48
+
49
+ This package ships with an `llms.txt` file for AI-assisted plugin development.
50
+
51
+ You can find it:
52
+
53
+ - after install: `node_modules/@edge-base/plugin-core/llms.txt`
54
+ - in the repository: [llms.txt](https://github.com/edge-base/edgebase/blob/main/packages/plugins/core/llms.txt)
55
+
56
+ Use it when you want an agent to:
57
+
58
+ - generate plugin packages with the correct factory shape
59
+ - avoid confusing app code with plugin code
60
+ - use typed `ctx.pluginConfig` and `ctx.admin` correctly
61
+ - model migrations and hooks without guessing unsupported runtime behavior
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ npm install @edge-base/plugin-core
67
+ ```
68
+
69
+ If you want a starter layout, you can scaffold one with the CLI:
70
+
71
+ ```bash
72
+ npx --package @edge-base/cli edgebase create-plugin my-plugin
73
+ ```
74
+
75
+ ## Quick Start
76
+
77
+ ```ts
78
+ import { definePlugin } from '@edge-base/plugin-core';
79
+
80
+ interface StripeConfig {
81
+ secretKey: string;
82
+ }
83
+
84
+ export const stripePlugin = definePlugin<StripeConfig>({
85
+ name: '@edge-base/plugin-stripe',
86
+ version: '0.1.0',
87
+ tables: {
88
+ customers: {
89
+ schema: {
90
+ email: { type: 'string', required: true },
91
+ },
92
+ },
93
+ },
94
+ functions: {
95
+ 'create-checkout': {
96
+ trigger: { type: 'http', method: 'POST' },
97
+ handler: async (ctx) => {
98
+ const key = ctx.pluginConfig.secretKey;
99
+ return Response.json({ ok: true, hasKey: !!key });
100
+ },
101
+ },
102
+ },
103
+ });
104
+ ```
105
+
106
+ `definePlugin()` returns a factory. The host app installs your plugin and calls that factory with user config from its EdgeBase project.
107
+
108
+ ## What This Package Covers
109
+
110
+ | Area | Included |
111
+ | --- | --- |
112
+ | Plugin factory API | `definePlugin<TConfig>()` |
113
+ | Public plugin contracts | `PluginDefinition`, `PluginFunctionContext`, `PluginHooks`, `PluginMigrationContext` |
114
+ | Plugin admin surface | `ctx.admin` contracts for DB, auth, SQL, KV, D1, Vectorize, push, functions |
115
+ | Testing helpers | `createMockContext()` |
116
+ | Contract version export | `EDGEBASE_PLUGIN_API_VERSION` |
117
+
118
+ ## Typical Plugin Capabilities
119
+
120
+ With `@edge-base/plugin-core`, a plugin can contribute:
121
+
122
+ - tables
123
+ - functions
124
+ - auth hooks
125
+ - storage hooks
126
+ - `onInstall` setup
127
+ - semver-keyed migrations
128
+
129
+ ## Testing Plugin Logic
130
+
131
+ Use `createMockContext()` when you want to test handlers without running a full EdgeBase project:
132
+
133
+ ```ts
134
+ import { createMockContext } from '@edge-base/plugin-core';
135
+
136
+ const ctx = createMockContext({
137
+ pluginConfig: { secretKey: 'test-key' },
138
+ });
139
+ ```
140
+
141
+ From there you can call your plugin handlers with a predictable mock context.
142
+
143
+ ## Package Boundary
144
+
145
+ Reach for this package when you are:
146
+
147
+ - publishing an EdgeBase plugin to npm
148
+ - sharing reusable EdgeBase functionality across projects
149
+ - building plugin factories that host apps will configure
150
+
151
+ Do **not** use this package for:
152
+
153
+ - normal browser app code
154
+ - SSR app code
155
+ - server-side app admin logic
156
+
157
+ For those, use:
158
+
159
+ - [`@edge-base/web`](https://www.npmjs.com/package/@edge-base/web)
160
+ - [`@edge-base/ssr`](https://www.npmjs.com/package/@edge-base/ssr)
161
+ - [`@edge-base/admin`](https://www.npmjs.com/package/@edge-base/admin)
162
+
163
+ ## License
164
+
165
+ MIT
@@ -0,0 +1,472 @@
1
+ /**
2
+ * @edge-base/plugin-core — Plugin definition API for EdgeBase.
3
+ *
4
+ * Explicit import pattern — plugins are factory functions that return PluginInstance.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Plugin author (e.g. @edge-base/plugin-stripe/server/src/index.ts)
9
+ * import { definePlugin } from '@edge-base/plugin-core';
10
+ *
11
+ * interface StripeConfig { secretKey: string; webhookSecret: string; currency?: string }
12
+ *
13
+ * export const stripePlugin = definePlugin<StripeConfig>({
14
+ * name: '@edge-base/plugin-stripe',
15
+ * tables: { customers: { schema: { ... } } },
16
+ * functions: {
17
+ * 'create-checkout': {
18
+ * trigger: { type: 'http', method: 'POST' },
19
+ * handler: async (ctx) => {
20
+ * const key = ctx.pluginConfig.secretKey; // ← typed
21
+ * return Response.json({ ok: true });
22
+ * },
23
+ * },
24
+ * },
25
+ * hooks: {
26
+ * afterSignUp: async (ctx) => { /* ... *​/ },
27
+ * },
28
+ * });
29
+ *
30
+ * // App developer (edgebase.config.ts)
31
+ * import { stripePlugin } from '@edge-base/plugin-stripe';
32
+ * export default defineConfig({
33
+ * plugins: [ stripePlugin({ secretKey: process.env.STRIPE_SECRET_KEY! }) ],
34
+ * });
35
+ * ```
36
+ */
37
+ import type { PluginInstance, PluginManifest, TableConfig, FunctionTrigger, DbProvider } from '@edge-base/shared';
38
+ export { CURRENT_PLUGIN_API_VERSION as EDGEBASE_PLUGIN_API_VERSION } from '@edge-base/shared';
39
+ export interface PluginTableProxy {
40
+ insert(data: Record<string, unknown>): Promise<Record<string, unknown>>;
41
+ upsert(data: Record<string, unknown>, options?: {
42
+ conflictTarget?: string;
43
+ }): Promise<Record<string, unknown>>;
44
+ update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
45
+ delete(id: string): Promise<{
46
+ deleted: boolean;
47
+ }>;
48
+ get(id: string): Promise<Record<string, unknown>>;
49
+ list(options?: {
50
+ limit?: number;
51
+ filter?: unknown;
52
+ }): Promise<{
53
+ items: Record<string, unknown>[];
54
+ }>;
55
+ }
56
+ export interface PluginAdminAuthContext {
57
+ getUser(userId: string): Promise<Record<string, unknown>>;
58
+ listUsers(options?: {
59
+ limit?: number;
60
+ cursor?: string;
61
+ }): Promise<{
62
+ users: Record<string, unknown>[];
63
+ cursor?: string;
64
+ }>;
65
+ createUser(data: {
66
+ email: string;
67
+ password: string;
68
+ displayName?: string;
69
+ role?: string;
70
+ }): Promise<Record<string, unknown>>;
71
+ updateUser(userId: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
72
+ deleteUser(userId: string): Promise<void>;
73
+ setCustomClaims(userId: string, claims: Record<string, unknown>): Promise<void>;
74
+ revokeAllSessions(userId: string): Promise<void>;
75
+ }
76
+ /**
77
+ * Full admin surface available in plugin contexts.
78
+ * Matches server FunctionAdminContext — all operations bypass access rules.
79
+ */
80
+ export interface PluginAdminContext {
81
+ /** Table access on default namespace (shortcut for `db('shared').table(name)`). */
82
+ table(name: string): PluginTableProxy;
83
+ /** Access a specific DB namespace instance. */
84
+ db(namespace: string, id?: string): {
85
+ table(name: string): PluginTableProxy;
86
+ };
87
+ /** Admin user management. */
88
+ auth: PluginAdminAuthContext;
89
+ /** Raw SQL on a DB namespace DO. */
90
+ sql(namespace: string, id: string | undefined, query: string, params?: unknown[]): Promise<unknown[]>;
91
+ /** Server-side broadcast to a realtime channel. */
92
+ broadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<void>;
93
+ /** Inter-function calls. */
94
+ functions: {
95
+ call(name: string, data?: unknown): Promise<unknown>;
96
+ };
97
+ /** KV namespace access. */
98
+ kv(namespace: string): PluginKvProxy;
99
+ /** D1 database access. */
100
+ d1(database: string): PluginD1Proxy;
101
+ /** Vectorize index access. */
102
+ vector(index: string): PluginVectorProxy;
103
+ /** Push notification management. */
104
+ push: PluginPushProxy;
105
+ }
106
+ export interface PluginKvProxy {
107
+ get(key: string): Promise<string | null>;
108
+ set(key: string, value: string, options?: {
109
+ ttl?: number;
110
+ }): Promise<void>;
111
+ delete(key: string): Promise<void>;
112
+ list(options?: {
113
+ prefix?: string;
114
+ limit?: number;
115
+ cursor?: string;
116
+ }): Promise<{
117
+ keys: string[];
118
+ cursor?: string;
119
+ }>;
120
+ }
121
+ export interface PluginD1Proxy {
122
+ exec<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T[]>;
123
+ }
124
+ export interface PluginVectorProxy {
125
+ /** Insert or update vectors. */
126
+ upsert(vectors: Array<{
127
+ id: string;
128
+ values: number[];
129
+ metadata?: Record<string, unknown>;
130
+ namespace?: string;
131
+ }>): Promise<{
132
+ ok: true;
133
+ count?: number;
134
+ mutationId?: string;
135
+ }>;
136
+ /** Insert new vectors (fails if ID exists). */
137
+ insert(vectors: Array<{
138
+ id: string;
139
+ values: number[];
140
+ metadata?: Record<string, unknown>;
141
+ namespace?: string;
142
+ }>): Promise<{
143
+ ok: true;
144
+ count?: number;
145
+ mutationId?: string;
146
+ }>;
147
+ /** Semantic search by vector. */
148
+ search(vector: number[], options?: {
149
+ topK?: number;
150
+ filter?: Record<string, unknown>;
151
+ namespace?: string;
152
+ returnValues?: boolean;
153
+ returnMetadata?: boolean | 'all' | 'indexed' | 'none';
154
+ }): Promise<Array<{
155
+ id: string;
156
+ score: number;
157
+ values?: number[];
158
+ metadata?: Record<string, unknown>;
159
+ namespace?: string;
160
+ }>>;
161
+ /** Find similar vectors by an existing vector ID. */
162
+ queryById(vectorId: string, options?: {
163
+ topK?: number;
164
+ filter?: Record<string, unknown>;
165
+ namespace?: string;
166
+ returnValues?: boolean;
167
+ returnMetadata?: boolean | 'all' | 'indexed' | 'none';
168
+ }): Promise<Array<{
169
+ id: string;
170
+ score: number;
171
+ values?: number[];
172
+ metadata?: Record<string, unknown>;
173
+ namespace?: string;
174
+ }>>;
175
+ /** Retrieve vectors by IDs. */
176
+ getByIds(ids: string[]): Promise<Array<{
177
+ id: string;
178
+ values?: number[];
179
+ metadata?: Record<string, unknown>;
180
+ namespace?: string;
181
+ }>>;
182
+ /** Delete vectors by IDs. */
183
+ delete(ids: string[]): Promise<{
184
+ ok: true;
185
+ count?: number;
186
+ mutationId?: string;
187
+ }>;
188
+ /** Get index metadata (dimensions, vector count, metric). */
189
+ describe(): Promise<{
190
+ vectorCount: number;
191
+ dimensions: number;
192
+ metric: string;
193
+ id?: string;
194
+ name?: string;
195
+ processedUpToDatetime?: string;
196
+ processedUpToMutation?: string;
197
+ }>;
198
+ }
199
+ export interface PluginPushProxy {
200
+ send(userId: string, payload: Record<string, unknown>): Promise<{
201
+ sent: number;
202
+ failed: number;
203
+ removed: number;
204
+ }>;
205
+ sendMany(userIds: string[], payload: Record<string, unknown>): Promise<{
206
+ sent: number;
207
+ failed: number;
208
+ removed: number;
209
+ }>;
210
+ getTokens(userId: string): Promise<Array<{
211
+ deviceId: string;
212
+ platform: string;
213
+ updatedAt: string;
214
+ deviceInfo?: Record<string, string>;
215
+ metadata?: Record<string, unknown>;
216
+ }>>;
217
+ getLogs(userId: string, limit?: number): Promise<Array<{
218
+ sentAt: string;
219
+ userId: string;
220
+ platform: string;
221
+ status: string;
222
+ collapseId?: string;
223
+ error?: string;
224
+ }>>;
225
+ sendToToken(token: string, payload: Record<string, unknown>, platform?: string): Promise<{
226
+ sent: number;
227
+ failed: number;
228
+ error?: string;
229
+ }>;
230
+ sendToTopic(topic: string, payload: Record<string, unknown>): Promise<{
231
+ success: boolean;
232
+ error?: string;
233
+ }>;
234
+ broadcast(payload: Record<string, unknown>): Promise<{
235
+ success: boolean;
236
+ error?: string;
237
+ }>;
238
+ }
239
+ /**
240
+ * Context passed to plugin function handlers.
241
+ * Mirrors the server FunctionContext — admin surface matches FunctionAdminContext exactly.
242
+ *
243
+ * @typeParam TConfig - Plugin config shape from definePlugin<TConfig>()
244
+ */
245
+ export interface PluginFunctionContext<TConfig = Record<string, unknown>> {
246
+ /** The incoming HTTP request. */
247
+ request: Request;
248
+ /** Authenticated user (null if unauthenticated). */
249
+ auth: {
250
+ id: string;
251
+ email?: string;
252
+ isAnonymous?: boolean;
253
+ custom?: Record<string, unknown>;
254
+ } | null;
255
+ /** Plugin-specific config — typed via definePlugin<TConfig>(). Injected by factory closure. */
256
+ pluginConfig: TConfig;
257
+ /** Route parameters extracted from trigger path (e.g. `/stripe/[id]` → `{ id: '...' }`). */
258
+ params: Record<string, string>;
259
+ /** Server-side EdgeBase admin client (admin-level access — bypasses access rules). */
260
+ admin: PluginAdminContext;
261
+ /** Trigger data (before/after for DB triggers). */
262
+ data?: {
263
+ before?: Record<string, unknown>;
264
+ after?: Record<string, unknown>;
265
+ };
266
+ }
267
+ /**
268
+ * Context passed to plugin storage hook handlers.
269
+ * Storage hooks receive file metadata only — NO file content (Worker 128MB memory limit).
270
+ * Blocking hooks (`before*`) can throw to reject. Non-blocking hooks (`after*`) run via waitUntil.
271
+ */
272
+ export interface PluginStorageHookContext<TConfig = Record<string, unknown>> {
273
+ file: {
274
+ key: string;
275
+ bucket: string;
276
+ size: number;
277
+ contentType: string;
278
+ etag?: string;
279
+ uploadedAt?: string;
280
+ uploadedBy?: string | null;
281
+ customMetadata?: Record<string, string>;
282
+ };
283
+ /** Authenticated user who performed the action (null for service key or unauthenticated). */
284
+ auth: {
285
+ id: string;
286
+ email?: string;
287
+ } | null;
288
+ /** Plugin-specific config injected by factory closure. */
289
+ pluginConfig: TConfig;
290
+ /** Server-side admin client — access to DB, KV, D1, Vectorize, push, etc. */
291
+ admin: PluginAdminContext;
292
+ }
293
+ /**
294
+ * Context passed to plugin onInstall and migration handlers.
295
+ * Provides admin-level access for schema alterations, data migrations, and service setup.
296
+ *
297
+ * NOTE: Some admin methods (kv, d1, vector, broadcast, functions, push) require workerUrl
298
+ * which is derived from the first incoming request. These will throw if workerUrl is unavailable.
299
+ */
300
+ export interface PluginMigrationContext<TConfig = Record<string, unknown>> {
301
+ /** Plugin-specific config injected by factory closure. */
302
+ pluginConfig: TConfig;
303
+ /** Admin context for DB operations and service access. */
304
+ admin: PluginAdminContext;
305
+ /** Previous plugin version (null for onInstall / first deploy). */
306
+ previousVersion: string | null;
307
+ }
308
+ /**
309
+ * Context passed to plugin auth hook handlers.
310
+ * Mirrors the server's executeAuthHook context with full admin access.
311
+ */
312
+ export interface PluginHookContext<TConfig = Record<string, unknown>> {
313
+ request: Request;
314
+ auth: null;
315
+ /** Server-side admin client with full resource access. */
316
+ admin: PluginAdminContext;
317
+ data: {
318
+ after: Record<string, unknown>;
319
+ };
320
+ pluginConfig: TConfig;
321
+ }
322
+ export interface PluginHooks<TConfig = Record<string, unknown>> {
323
+ /** Before user creation. Throw to reject signup. 5s timeout. */
324
+ beforeSignUp?: (ctx: PluginHookContext<TConfig>) => Promise<Record<string, unknown> | void>;
325
+ /** After user creation. Non-blocking (waitUntil). */
326
+ afterSignUp?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
327
+ /** Before session creation. Throw to reject signin. 5s timeout. */
328
+ beforeSignIn?: (ctx: PluginHookContext<TConfig>) => Promise<Record<string, unknown> | void>;
329
+ /** After session creation. Non-blocking (waitUntil). */
330
+ afterSignIn?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
331
+ /** On JWT refresh. Return a plain object of claim overrides. 5s timeout. */
332
+ onTokenRefresh?: (ctx: PluginHookContext<TConfig>) => Promise<Record<string, unknown> | void>;
333
+ /** Before password reset. Throw to reject. 5s timeout. */
334
+ beforePasswordReset?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
335
+ /** After password reset. Non-blocking (waitUntil). */
336
+ afterPasswordReset?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
337
+ /** Before sign out. Throw to reject. 5s timeout. */
338
+ beforeSignOut?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
339
+ /** After sign out. Non-blocking (waitUntil). */
340
+ afterSignOut?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
341
+ /** On account deletion. Non-blocking (waitUntil). */
342
+ onDeleteAccount?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
343
+ /** On email verification. Non-blocking (waitUntil). */
344
+ onEmailVerified?: (ctx: PluginHookContext<TConfig>) => Promise<void>;
345
+ /** Before upload. Return Record<string,string> to merge custom metadata. Throw to reject. 5s timeout. */
346
+ beforeUpload?: (ctx: PluginStorageHookContext<TConfig>) => Promise<Record<string, string> | void>;
347
+ /** Before file deletion. Throw to reject. 5s timeout. */
348
+ beforeDelete?: (ctx: PluginStorageHookContext<TConfig>) => Promise<void>;
349
+ /** Before file download. Throw to reject. 5s timeout. */
350
+ beforeDownload?: (ctx: PluginStorageHookContext<TConfig>) => Promise<void>;
351
+ /** After upload. Non-blocking (waitUntil). Receives final R2 metadata. */
352
+ afterUpload?: (ctx: PluginStorageHookContext<TConfig>) => Promise<void>;
353
+ /** After deletion. Non-blocking (waitUntil). */
354
+ afterDelete?: (ctx: PluginStorageHookContext<TConfig>) => Promise<void>;
355
+ /** On metadata update. Non-blocking (waitUntil). */
356
+ onMetadataUpdate?: (ctx: PluginStorageHookContext<TConfig>) => Promise<void>;
357
+ }
358
+ export interface PluginDefinition<TConfig = Record<string, unknown>> {
359
+ /** Plugin unique name (e.g. '@edge-base/plugin-stripe'). */
360
+ name: string;
361
+ /**
362
+ * Public plugin contract version.
363
+ * Defaults to the current runtime contract when omitted.
364
+ */
365
+ pluginApiVersion?: number;
366
+ /** Semantic version string (e.g. '1.0.0'). Required for migration support. */
367
+ version?: string;
368
+ /** Serializable metadata used by CLI/docs tooling. */
369
+ manifest?: PluginManifest;
370
+ /** Plugin tables. Keys = table names (plugin.name/ prefix added automatically). */
371
+ tables?: Record<string, TableConfig>;
372
+ /** DB block for plugin tables. Default: 'shared'. */
373
+ dbBlock?: string;
374
+ /**
375
+ * Database provider required by this plugin.
376
+ * - `'do'` (default): Durable Object + SQLite
377
+ * - `'neon'`: Requires Neon PostgreSQL and a configured connection string
378
+ * - `'postgres'`: Requires custom PostgreSQL
379
+ */
380
+ provider?: DbProvider;
381
+ /** Plugin functions. Keys = function names (plugin.name/ prefix added automatically). */
382
+ functions?: Record<string, {
383
+ trigger: FunctionTrigger;
384
+ handler: (ctx: PluginFunctionContext<TConfig>) => Promise<Response | unknown>;
385
+ }>;
386
+ /** Auth + storage hooks. */
387
+ hooks?: PluginHooks<TConfig>;
388
+ /** Runs once on first deploy with this plugin. Use for initial seed data, external webhook registration, etc. */
389
+ onInstall?: (ctx: PluginMigrationContext<TConfig>) => Promise<void>;
390
+ /**
391
+ * Version-keyed migration functions. Run in semver order on deploy when plugin version changes.
392
+ * Migrations MUST be idempotent — concurrent Worker instances may execute the same migration.
393
+ * Use either onInstall OR migrations['1.0.0'], not both (onInstall runs first, then migrations).
394
+ */
395
+ migrations?: Record<string, (ctx: PluginMigrationContext<TConfig>) => Promise<void>>;
396
+ }
397
+ /**
398
+ * Define an EdgeBase plugin. Returns a factory function that takes user config
399
+ * and returns a PluginInstance for use in edgebase.config.ts.
400
+ *
401
+ * The factory closure captures userConfig so every handler receives pluginConfig
402
+ * without the server needing to know about plugins.
403
+ *
404
+ * @typeParam TConfig - Shape of the plugin config
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * export const stripePlugin = definePlugin<{ secretKey: string }>({
409
+ * name: '@edge-base/plugin-stripe',
410
+ * tables: { customers: { schema: { ... } } },
411
+ * functions: {
412
+ * 'create-checkout': {
413
+ * trigger: { type: 'http', method: 'POST' },
414
+ * handler: async (ctx) => {
415
+ * const key = ctx.pluginConfig.secretKey; // typed, injected by closure
416
+ * return Response.json({ ok: true });
417
+ * },
418
+ * },
419
+ * },
420
+ * });
421
+ * ```
422
+ */
423
+ export declare function definePlugin<TConfig>(definition: PluginDefinition<TConfig>): (userConfig: TConfig) => PluginInstance;
424
+ /**
425
+ * Create a mock PluginFunctionContext for unit testing plugin handlers.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * import { createMockContext } from '@edge-base/plugin-core';
430
+ *
431
+ * const ctx = createMockContext({
432
+ * auth: { id: 'user-1' },
433
+ * pluginConfig: { secretKey: 'sk_test_xxx' },
434
+ * body: { priceId: 'price_xxx' },
435
+ * });
436
+ * const response = await myHandler(ctx);
437
+ * expect(response.status).toBe(200);
438
+ * ```
439
+ */
440
+ export declare function createMockContext<TConfig = Record<string, unknown>>(options?: {
441
+ auth?: PluginFunctionContext['auth'];
442
+ pluginConfig?: TConfig;
443
+ params?: Record<string, string>;
444
+ body?: unknown;
445
+ method?: string;
446
+ url?: string;
447
+ headers?: Record<string, string>;
448
+ }): PluginFunctionContext<TConfig>;
449
+ /**
450
+ * Base interface for plugin client SDKs.
451
+ * Use this as a guide when creating typed client wrappers.
452
+ *
453
+ * @example
454
+ * ```typescript
455
+ * import type { PluginClientFactory } from '@edge-base/plugin-core';
456
+ *
457
+ * interface StripeClient { createCheckout(params: CheckoutParams): Promise<CheckoutResult>; }
458
+ *
459
+ * export const createStripePlugin: PluginClientFactory<StripeClient> = (client) => ({
460
+ * async createCheckout(params) {
461
+ * return client.functions.call('@edge-base/plugin-stripe/create-checkout', params) as Promise<CheckoutResult>;
462
+ * },
463
+ * });
464
+ * ```
465
+ */
466
+ export interface PluginClientHost {
467
+ table(name: string): unknown;
468
+ functions: {
469
+ call(name: string, data?: unknown): Promise<unknown>;
470
+ };
471
+ }
472
+ export type PluginClientFactory<T> = (client: PluginClientHost) => T;
package/dist/index.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * @edge-base/plugin-core — Plugin definition API for EdgeBase.
3
+ *
4
+ * Explicit import pattern — plugins are factory functions that return PluginInstance.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Plugin author (e.g. @edge-base/plugin-stripe/server/src/index.ts)
9
+ * import { definePlugin } from '@edge-base/plugin-core';
10
+ *
11
+ * interface StripeConfig { secretKey: string; webhookSecret: string; currency?: string }
12
+ *
13
+ * export const stripePlugin = definePlugin<StripeConfig>({
14
+ * name: '@edge-base/plugin-stripe',
15
+ * tables: { customers: { schema: { ... } } },
16
+ * functions: {
17
+ * 'create-checkout': {
18
+ * trigger: { type: 'http', method: 'POST' },
19
+ * handler: async (ctx) => {
20
+ * const key = ctx.pluginConfig.secretKey; // ← typed
21
+ * return Response.json({ ok: true });
22
+ * },
23
+ * },
24
+ * },
25
+ * hooks: {
26
+ * afterSignUp: async (ctx) => { /* ... *​/ },
27
+ * },
28
+ * });
29
+ *
30
+ * // App developer (edgebase.config.ts)
31
+ * import { stripePlugin } from '@edge-base/plugin-stripe';
32
+ * export default defineConfig({
33
+ * plugins: [ stripePlugin({ secretKey: process.env.STRIPE_SECRET_KEY! }) ],
34
+ * });
35
+ * ```
36
+ */
37
+ import { CURRENT_PLUGIN_API_VERSION } from '@edge-base/shared';
38
+ export { CURRENT_PLUGIN_API_VERSION as EDGEBASE_PLUGIN_API_VERSION } from '@edge-base/shared';
39
+ // ─── definePlugin() — Factory Pattern ───
40
+ /**
41
+ * Define an EdgeBase plugin. Returns a factory function that takes user config
42
+ * and returns a PluginInstance for use in edgebase.config.ts.
43
+ *
44
+ * The factory closure captures userConfig so every handler receives pluginConfig
45
+ * without the server needing to know about plugins.
46
+ *
47
+ * @typeParam TConfig - Shape of the plugin config
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * export const stripePlugin = definePlugin<{ secretKey: string }>({
52
+ * name: '@edge-base/plugin-stripe',
53
+ * tables: { customers: { schema: { ... } } },
54
+ * functions: {
55
+ * 'create-checkout': {
56
+ * trigger: { type: 'http', method: 'POST' },
57
+ * handler: async (ctx) => {
58
+ * const key = ctx.pluginConfig.secretKey; // typed, injected by closure
59
+ * return Response.json({ ok: true });
60
+ * },
61
+ * },
62
+ * },
63
+ * });
64
+ * ```
65
+ */
66
+ export function definePlugin(definition) {
67
+ return (userConfig) => {
68
+ // Wrap function handlers with pluginConfig closure
69
+ const functions = {};
70
+ if (definition.functions) {
71
+ for (const [name, def] of Object.entries(definition.functions)) {
72
+ functions[name] = {
73
+ trigger: def.trigger,
74
+ handler: async (ctx) => {
75
+ ctx.pluginConfig = userConfig;
76
+ return def.handler(ctx);
77
+ },
78
+ };
79
+ }
80
+ }
81
+ // Wrap auth + storage hooks with pluginConfig closure
82
+ const hooks = {};
83
+ if (definition.hooks) {
84
+ for (const [event, hookFn] of Object.entries(definition.hooks)) {
85
+ if (hookFn) {
86
+ hooks[event] = async (ctx) => {
87
+ ctx.pluginConfig = userConfig;
88
+ return hookFn(ctx);
89
+ };
90
+ }
91
+ }
92
+ }
93
+ // Wrap onInstall with pluginConfig closure
94
+ let onInstall;
95
+ if (definition.onInstall) {
96
+ const installFn = definition.onInstall;
97
+ onInstall = async (ctx) => {
98
+ ctx.pluginConfig = userConfig;
99
+ return installFn(ctx);
100
+ };
101
+ }
102
+ // Wrap migrations with pluginConfig closure
103
+ let migrations;
104
+ if (definition.migrations) {
105
+ migrations = {};
106
+ for (const [version, migrateFn] of Object.entries(definition.migrations)) {
107
+ migrations[version] = async (ctx) => {
108
+ ctx.pluginConfig = userConfig;
109
+ return migrateFn(ctx);
110
+ };
111
+ }
112
+ }
113
+ return {
114
+ name: definition.name,
115
+ pluginApiVersion: definition.pluginApiVersion ?? CURRENT_PLUGIN_API_VERSION,
116
+ version: definition.version,
117
+ manifest: definition.manifest,
118
+ config: userConfig,
119
+ tables: definition.tables,
120
+ dbBlock: definition.dbBlock,
121
+ provider: definition.provider,
122
+ functions: Object.keys(functions).length > 0 ? functions : undefined,
123
+ hooks: Object.keys(hooks).length > 0 ? hooks : undefined,
124
+ onInstall,
125
+ migrations,
126
+ };
127
+ };
128
+ }
129
+ // ─── Testing Utilities ───
130
+ /**
131
+ * Create a mock PluginFunctionContext for unit testing plugin handlers.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * import { createMockContext } from '@edge-base/plugin-core';
136
+ *
137
+ * const ctx = createMockContext({
138
+ * auth: { id: 'user-1' },
139
+ * pluginConfig: { secretKey: 'sk_test_xxx' },
140
+ * body: { priceId: 'price_xxx' },
141
+ * });
142
+ * const response = await myHandler(ctx);
143
+ * expect(response.status).toBe(200);
144
+ * ```
145
+ */
146
+ export function createMockContext(options) {
147
+ const method = options?.method ?? 'POST';
148
+ const url = options?.url ?? 'http://localhost:8787/api/functions/test';
149
+ const headers = new Headers(options?.headers);
150
+ if (options?.body)
151
+ headers.set('Content-Type', 'application/json');
152
+ const request = new Request(url, {
153
+ method,
154
+ headers,
155
+ body: options?.body ? JSON.stringify(options.body) : undefined,
156
+ });
157
+ // In-memory table store for testing
158
+ const stores = {};
159
+ function getStore(name) {
160
+ if (!stores[name])
161
+ stores[name] = new Map();
162
+ return stores[name];
163
+ }
164
+ function createTableProxy(name) {
165
+ const store = getStore(name);
166
+ return {
167
+ async insert(data) {
168
+ const id = data.id ?? crypto.randomUUID();
169
+ const doc = { id, ...data, createdAt: new Date().toISOString() };
170
+ store.set(id, doc);
171
+ return doc;
172
+ },
173
+ async upsert(data, options) {
174
+ const conflictTarget = options?.conflictTarget ?? 'id';
175
+ const existing = Array.from(store.values()).find((doc) => doc[conflictTarget] !== undefined && doc[conflictTarget] === data[conflictTarget]);
176
+ if (existing) {
177
+ const id = String(existing.id ?? data.id ?? crypto.randomUUID());
178
+ const updated = {
179
+ ...existing,
180
+ ...data,
181
+ id,
182
+ updatedAt: new Date().toISOString(),
183
+ };
184
+ store.set(id, updated);
185
+ return updated;
186
+ }
187
+ const id = data.id ?? crypto.randomUUID();
188
+ const doc = { id, ...data, createdAt: new Date().toISOString() };
189
+ store.set(id, doc);
190
+ return doc;
191
+ },
192
+ async update(id, data) {
193
+ const existing = store.get(id) ?? {};
194
+ const updated = { ...existing, ...data, updatedAt: new Date().toISOString() };
195
+ store.set(id, updated);
196
+ return updated;
197
+ },
198
+ async delete(id) {
199
+ const existed = store.has(id);
200
+ store.delete(id);
201
+ return { deleted: existed };
202
+ },
203
+ async get(id) {
204
+ return store.get(id) ?? null;
205
+ },
206
+ async list() {
207
+ return { items: Array.from(store.values()) };
208
+ },
209
+ };
210
+ }
211
+ const mockAuth = {
212
+ async getUser() {
213
+ return {};
214
+ },
215
+ async listUsers() {
216
+ return { users: [] };
217
+ },
218
+ async createUser() {
219
+ return {};
220
+ },
221
+ async updateUser() {
222
+ return {};
223
+ },
224
+ async deleteUser() { },
225
+ async setCustomClaims() { },
226
+ async revokeAllSessions() { },
227
+ };
228
+ const mockPush = {
229
+ async send() {
230
+ return { sent: 0, failed: 0, removed: 0 };
231
+ },
232
+ async sendMany() {
233
+ return { sent: 0, failed: 0, removed: 0 };
234
+ },
235
+ async getTokens() {
236
+ return [];
237
+ },
238
+ async getLogs() {
239
+ return [];
240
+ },
241
+ async sendToToken() {
242
+ return { sent: 0, failed: 0 };
243
+ },
244
+ async sendToTopic() {
245
+ return { success: true };
246
+ },
247
+ async broadcast() {
248
+ return { success: true };
249
+ },
250
+ };
251
+ const mockAdmin = {
252
+ table: createTableProxy,
253
+ db(_namespace, _id) {
254
+ return { table: createTableProxy };
255
+ },
256
+ auth: mockAuth,
257
+ async sql() {
258
+ return [];
259
+ },
260
+ async broadcast() { },
261
+ functions: {
262
+ async call() {
263
+ return {};
264
+ },
265
+ },
266
+ kv() {
267
+ return {
268
+ async get() {
269
+ return null;
270
+ },
271
+ async set() { },
272
+ async delete() { },
273
+ async list() {
274
+ return { keys: [] };
275
+ },
276
+ };
277
+ },
278
+ d1() {
279
+ return {
280
+ async exec() {
281
+ return [];
282
+ },
283
+ };
284
+ },
285
+ vector() {
286
+ return {
287
+ async upsert() {
288
+ return { ok: true, count: 0 };
289
+ },
290
+ async insert() {
291
+ return { ok: true, count: 0 };
292
+ },
293
+ async search() {
294
+ return [];
295
+ },
296
+ async queryById() {
297
+ return [];
298
+ },
299
+ async getByIds() {
300
+ return [];
301
+ },
302
+ async delete() {
303
+ return { ok: true, count: 0 };
304
+ },
305
+ async describe() {
306
+ return { vectorCount: 0, dimensions: 0, metric: 'cosine' };
307
+ },
308
+ };
309
+ },
310
+ push: mockPush,
311
+ };
312
+ return {
313
+ request,
314
+ auth: options?.auth ?? null,
315
+ pluginConfig: (options?.pluginConfig ?? {}),
316
+ params: options?.params ?? {},
317
+ admin: mockAdmin,
318
+ };
319
+ }
package/llms.txt ADDED
@@ -0,0 +1,105 @@
1
+ # EdgeBase Plugin Core
2
+
3
+ Use this file as a quick-reference contract for AI coding assistants working with `@edge-base/plugin-core`.
4
+
5
+ ## Package Boundary
6
+
7
+ Use `@edge-base/plugin-core` for authoring installable EdgeBase plugins.
8
+
9
+ Do not use it for regular application code. For browser apps use `@edge-base/web`. For trusted server code use `@edge-base/admin`. For SSR cookie helpers use `@edge-base/ssr`.
10
+
11
+ ## Source Of Truth
12
+
13
+ - Package README: https://github.com/edge-base/edgebase/blob/main/packages/plugins/core/README.md
14
+ - Plugins overview: https://edgebase.fun/docs/plugins
15
+ - Creating plugins: https://edgebase.fun/docs/plugins/creating-plugins
16
+ - Plugin API reference: https://edgebase.fun/docs/plugins/api-reference
17
+ - Using plugins: https://edgebase.fun/docs/plugins/using-plugins
18
+
19
+ ## Canonical Examples
20
+
21
+ ### Define a plugin
22
+
23
+ ```ts
24
+ import { definePlugin } from '@edge-base/plugin-core';
25
+
26
+ interface MyPluginConfig {
27
+ apiKey: string;
28
+ }
29
+
30
+ export const myPlugin = definePlugin<MyPluginConfig>({
31
+ name: 'my-plugin',
32
+ version: '0.1.0',
33
+ tables: {
34
+ items: {
35
+ schema: {
36
+ title: { type: 'string', required: true },
37
+ },
38
+ },
39
+ },
40
+ functions: {
41
+ ping: {
42
+ trigger: { type: 'http', method: 'GET' },
43
+ handler: async (ctx) => {
44
+ return Response.json({
45
+ ok: true,
46
+ configured: !!ctx.pluginConfig.apiKey,
47
+ });
48
+ },
49
+ },
50
+ },
51
+ });
52
+ ```
53
+
54
+ ### Use plugin config inside a hook
55
+
56
+ ```ts
57
+ hooks: {
58
+ afterSignUp: async (ctx) => {
59
+ const apiKey = ctx.pluginConfig.apiKey;
60
+ await ctx.admin.table('my-plugin/customers').insert({
61
+ userId: ctx.data.after.id,
62
+ apiKeyPresent: !!apiKey,
63
+ });
64
+ },
65
+ }
66
+ ```
67
+
68
+ ### Use mock context in tests
69
+
70
+ ```ts
71
+ import { createMockContext } from '@edge-base/plugin-core';
72
+
73
+ const ctx = createMockContext({
74
+ pluginConfig: { apiKey: 'test-key' },
75
+ });
76
+ ```
77
+
78
+ ## Hard Rules
79
+
80
+ - `definePlugin<TConfig>()` returns a factory: `(userConfig: TConfig) => PluginInstance`
81
+ - `ctx.pluginConfig` is injected automatically into plugin functions, hooks, `onInstall`, and migrations
82
+ - plugin table and function keys are local names; EdgeBase applies plugin namespacing at integration time
83
+ - `ctx.admin` is an admin-level surface and bypasses access rules
84
+ - `dbBlock` defaults to `'shared'` when omitted
85
+ - `provider` defaults to `'do'` when omitted
86
+ - semver-keyed migrations must be idempotent
87
+ - `EDGEBASE_PLUGIN_API_VERSION` is only needed when manually constructing a `PluginInstance`; normal plugin authors should rely on `definePlugin()`
88
+
89
+ ## Common Mistakes
90
+
91
+ - do not export a raw `PluginInstance` unless you have a strong reason; use `definePlugin()`
92
+ - do not assume plugin code runs in a separate server process; it is bundled into the same EdgeBase runtime
93
+ - do not put app-specific secrets directly in module scope when they should come from `userConfig`
94
+ - do not treat plugin tables as globally named; plugin namespacing matters
95
+ - do not use this package for normal app code imports
96
+
97
+ ## Quick Reference
98
+
99
+ ```text
100
+ definePlugin<TConfig>(definition) -> returns plugin factory
101
+ createMockContext(options?) -> mock typed context for tests
102
+ EDGEBASE_PLUGIN_API_VERSION -> public plugin contract version
103
+ ctx.pluginConfig -> plugin instance config captured by closure
104
+ ctx.admin -> admin-level server access inside plugin handlers
105
+ ```
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@edge-base/plugin-core",
3
+ "version": "0.1.1",
4
+ "description": "EdgeBase plugin authoring helpers and public plugin contracts",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/edge-base/edgebase.git",
9
+ "directory": "packages/plugins/core"
10
+ },
11
+ "homepage": "https://edgebase.fun",
12
+ "bugs": "https://github.com/edge-base/edgebase/issues",
13
+ "keywords": [
14
+ "edgebase",
15
+ "plugins",
16
+ "plugin-api"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "sideEffects": false,
25
+ "files": [
26
+ "dist",
27
+ "llms.txt"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "prepack": "pnpm run build"
38
+ },
39
+ "dependencies": {
40
+ "@edge-base/shared": "workspace:*"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^5.7.0"
44
+ }
45
+ }