@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 +165 -0
- package/dist/index.d.ts +472 -0
- package/dist/index.js +319 -0
- package/llms.txt +105 -0
- package/package.json +45 -0
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>
|
|
10
|
+
<a href="https://edgebase.fun/docs/plugins/creating-plugins"><img src="https://img.shields.io/badge/docs-plugins-blue" alt="Docs"></a>
|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|