1010 * - All crypto via `crypto.subtle` (Web Crypto).
1111 *
1212 * Emdash's astro integration calls `createPlugin(options)` at build time
13- * for native-format plugins. `options` comes from the descriptor's
14- * `options` field (see src/index.ts). We feed them into
15- * `adaptSandboxEntry` to produce a `ResolvedPlugin` — the same shape
16- * standard-format plugins get for free.
13+ * for native-format plugins (`format: "native"` + `entrypoint` in the
14+ * descriptor — see src/index.ts). `options` comes from the descriptor's
15+ * `options` field. We build a native `ResolvedPlugin` with `definePlugin`.
16+ *
17+ * emdash 0.28 note: route handlers are the NATIVE single-arg shape
18+ * `(ctx: RouteContext) => Promise<unknown>`, where `RouteContext` extends
19+ * `PluginContext` — so `ctx` carries both the request (`ctx.request`,
20+ * `ctx.input`) and the plugin surface (`ctx.storage`, `ctx.kv`, `ctx.http`…).
21+ * DashCommerce's route handlers are authored two-arg `(routeCtx, ctx)`; we
22+ * adapt them to the single-arg form below (passing the one context as both).
23+ * We deliberately do NOT use `adaptSandboxEntry` here: it flattens
24+ * `routeCtx.request` to `{ url, method, headers }` with no body, which would
25+ * break the Stripe webhook's raw-body signature verification.
1726 */
1827
19- import { adaptSandboxEntry , definePlugin , type PluginDescriptor } from "emdash" ;
28+ import {
29+ definePlugin ,
30+ type FieldWidgetConfig ,
31+ type PluginAdminConfig ,
32+ type PluginAdminPage ,
33+ type PluginCapability ,
34+ type PluginContext ,
35+ type PluginDescriptor ,
36+ type PluginRoute ,
37+ type PortableTextBlockConfig ,
38+ type RouteContext ,
39+ } from "emdash" ;
2040
2141import { productBeforeSave } from "./hooks/content" ;
2242import { cronHandler } from "./hooks/cron" ;
@@ -33,59 +53,96 @@ import { subscriptionsPublicRoutes } from "./routes/subscriptions-public";
3353import { webhookRoutes } from "./routes/webhook" ;
3454import { DASHCOMMERCE_STORAGE } from "./storage-collections" ;
3555
36- const DEFAULT_CAPABILITIES = [
37- "read: content" ,
38- "write: content" ,
39- "read: media" ,
40- "read: users" ,
41- "network:fetch " ,
56+ const DEFAULT_CAPABILITIES : PluginCapability [ ] = [
57+ "content:read " ,
58+ "content:write " ,
59+ "media:read " ,
60+ "users:read " ,
61+ "network:request " ,
4262 "email:send" ,
4363] ;
4464const DEFAULT_ALLOWED_HOSTS = [ "api.stripe.com" , "files.stripe.com" ] ;
4565
46- const definition = definePlugin ( {
47- hooks : {
48- "content:beforeSave" : {
49- handler : productBeforeSave ,
50- } ,
51- cron : {
52- handler : cronHandler ,
53- } ,
54- "plugin:install" : {
55- handler : onInstall ,
56- } ,
57- "plugin:activate" : {
58- handler : onActivate ,
59- } ,
60- } ,
61- routes : {
62- ...cartRoutes ,
63- ...checkoutRoutes ,
64- ...configCheckRoutes ,
65- ...customerPortalRoutes ,
66- ...downloadsRoutes ,
67- ...ordersPublicRoutes ,
68- ...reviewsPublicRoutes ,
69- ...subscriptionsPublicRoutes ,
70- ...webhookRoutes ,
71- ...adminApiRoutes ,
72- } ,
73- } ) ;
66+ /**
67+ * DashCommerce's route handlers are authored in the two-arg convention
68+ * `(routeCtx, ctx)` — `routeCtx` for request data (`.request`, `.input`) and
69+ * `ctx` for the plugin surface. In emdash's native format both are the same
70+ * `RouteContext` (which extends `PluginContext`), so a single context serves
71+ * as both. This is the loose entry shape those route maps satisfy.
72+ */
73+ type CommerceRouteEntry = {
74+ public ?: boolean ;
75+ input ?: PluginRoute [ "input" ] ;
76+ handler : ( routeCtx : RouteContext , ctx : PluginContext ) => Promise < unknown > ;
77+ } ;
78+
79+ const HOOKS = {
80+ "content:beforeSave" : { handler : productBeforeSave } ,
81+ cron : { handler : cronHandler } ,
82+ "plugin:install" : { handler : onInstall } ,
83+ "plugin:activate" : { handler : onActivate } ,
84+ } ;
85+
86+ const ROUTES = {
87+ ...cartRoutes ,
88+ ...checkoutRoutes ,
89+ ...configCheckRoutes ,
90+ ...customerPortalRoutes ,
91+ ...downloadsRoutes ,
92+ ...ordersPublicRoutes ,
93+ ...reviewsPublicRoutes ,
94+ ...subscriptionsPublicRoutes ,
95+ ...webhookRoutes ,
96+ ...adminApiRoutes ,
97+ } as unknown as Record < string , CommerceRouteEntry > ;
98+
99+ /**
100+ * Adapt the authored two-arg route handlers into emdash's native single-arg
101+ * `PluginRoute` form. `RouteContext` extends `PluginContext`, so the one
102+ * `ctx` is passed as both arguments. `public` and `input` pass through.
103+ */
104+ function toNativeRoutes (
105+ routes : Record < string , CommerceRouteEntry > ,
106+ ) : Record < string , PluginRoute > {
107+ const out : Record < string , PluginRoute > = { } ;
108+ for ( const [ name , route ] of Object . entries ( routes ) ) {
109+ out [ name ] = {
110+ public : route . public ,
111+ input : route . input ,
112+ handler : ( ctx ) => route . handler ( ctx , ctx ) ,
113+ } ;
114+ }
115+ return out ;
116+ }
117+
118+ /**
119+ * emdash's `PluginDefinition.storage` requires an `indexes` array on every
120+ * collection; the descriptor's `storage` declaration leaves it optional.
121+ * Normalize to the runtime shape.
122+ */
123+ function toStorageConfig (
124+ decl : NonNullable < PluginDescriptor [ "storage" ] > ,
125+ ) : Record < string , { indexes : string [ ] ; uniqueIndexes ?: string [ ] } > {
126+ const out : Record < string , { indexes : string [ ] ; uniqueIndexes ?: string [ ] } > = { } ;
127+ for ( const [ name , cfg ] of Object . entries ( decl ) ) {
128+ out [ name ] = { indexes : cfg . indexes ?? [ ] , uniqueIndexes : cfg . uniqueIndexes } ;
129+ }
130+ return out ;
131+ }
74132
75133export interface CreatePluginOptions {
76134 id ?: string ;
77135 version ?: string ;
78- capabilities ?: string [ ] ;
136+ capabilities ?: PluginCapability [ ] ;
79137 allowedHosts ?: string [ ] ;
80138}
81139
82140/**
83- * Static admin page list — must match src/index.ts adminPages.
84- * Defined here so `adaptSandboxEntry` can populate `admin.pages`,
85- * which the emdash plugin-manager API reads to set `hasAdminPages`
86- * and show the Settings gear link on the Plugins page.
141+ * Static admin page list — must match src/index.ts adminPages. Populates
142+ * `admin.pages`, which the emdash plugin-manager API reads to set
143+ * `hasAdminPages` and show the Settings gear link on the Plugins page.
87144 */
88- const ADMIN_PAGES : PluginDescriptor [ "adminPages" ] = [
145+ const ADMIN_PAGES : PluginAdminPage [ ] = [
89146 { path : "/orders" , label : "Orders" , icon : "shopping-bag" } ,
90147 { path : "/customers" , label : "Customers" , icon : "users" } ,
91148 { path : "/coupons" , label : "Coupons" , icon : "tag" } ,
@@ -99,7 +156,7 @@ const ADMIN_PAGES: PluginDescriptor["adminPages"] = [
99156 { path : "/settings" , label : "Settings" , icon : "settings" } ,
100157] ;
101158
102- const ADMIN_WIDGETS : PluginDescriptor [ "adminWidgets" ] = [
159+ const ADMIN_WIDGETS : NonNullable < PluginAdminConfig [ "widgets" ] > = [
103160 { id : "revenue-snapshot" , title : "Revenue" , size : "half" } ,
104161 { id : "low-stock-alerts" , title : "Low Stock" , size : "half" } ,
105162 { id : "recent-orders" , title : "Recent Orders" , size : "full" } ,
@@ -110,13 +167,7 @@ const ADMIN_WIDGETS: PluginDescriptor["adminWidgets"] = [
110167// Custom content-field widgets this plugin provides. The content editor
111168// resolves `widget: "dashcommerce:<name>"` on a field to the React
112169// component exported from `admin/entry.tsx` under `fields[name]`.
113- //
114- // `adaptSandboxEntry` currently only forwards `adminPages`/`adminWidgets`
115- // from the descriptor, so we merge these onto `resolved.admin` ourselves
116- // below. Without that merge the admin manifest wouldn't know the widgets
117- // exist and the editor would fall back to the default JSON textarea —
118- // exactly the "prices field is still a text input" symptom.
119- const FIELD_WIDGETS = [
170+ const FIELD_WIDGETS : FieldWidgetConfig [ ] = [
120171 {
121172 name : "vendor-select" ,
122173 label : "Vendor picker" ,
@@ -129,7 +180,7 @@ const FIELD_WIDGETS = [
129180 } ,
130181] ;
131182
132- const PORTABLE_TEXT_BLOCKS = [
183+ const PORTABLE_TEXT_BLOCKS : PortableTextBlockConfig [ ] = [
133184 {
134185 type : "product-embed" ,
135186 label : "Embed Product" ,
@@ -151,40 +202,30 @@ const PORTABLE_TEXT_BLOCKS = [
151202] ;
152203
153204/**
154- * Native-format entry called by emdash at build time with the options
155- * serialized from the descriptor. Returns a ResolvedPlugin ready for the
205+ * Native-format entry called by emdash with the options serialized from the
206+ * descriptor (see src/index.ts). Returns a ` ResolvedPlugin` ready for the
156207 * HookPipeline.
157208 */
158209export function createPlugin ( options : CreatePluginOptions = { } ) {
159- // adaptSandboxEntry reads id/version/capabilities/allowedHosts/
160- // storage/adminPages/adminWidgets from the descriptor to build the
161- // ResolvedPlugin. adminPages must be present so the emdash plugin-manager
162- // API returns hasAdminPages:true and the Settings link appears.
163- const descriptor : PluginDescriptor = {
210+ return definePlugin ( {
164211 id : options . id ?? "dashcommerce" ,
165212 version : options . version ?? "0.0.0" ,
166- entrypoint : "@dashcommerce/core/sandbox" ,
167213 capabilities : options . capabilities ?? DEFAULT_CAPABILITIES ,
168214 allowedHosts : options . allowedHosts ?? DEFAULT_ALLOWED_HOSTS ,
169- storage : DASHCOMMERCE_STORAGE ,
170- adminPages : ADMIN_PAGES ,
171- adminWidgets : ADMIN_WIDGETS ,
172- } ;
173- const resolved = adaptSandboxEntry ( definition , descriptor ) ;
174- // Merge fieldWidgets / portableTextBlocks onto the resolved admin
175- // config. adaptSandboxEntry drops these today; without the merge the
176- // admin manifest would never learn about `dashcommerce:price-map`
177- // and the content editor would render the raw JSON textarea.
178- // biome-ignore lint/suspicious/noExplicitAny: widening PluginAdminConfig to attach fields emdash's type doesn't yet declare
179- const admin : any = ( resolved as unknown as { admin ?: unknown } ) . admin ?? { } ;
180- admin . fieldWidgets = FIELD_WIDGETS ;
181- admin . portableTextBlocks = PORTABLE_TEXT_BLOCKS ;
182- ( resolved as unknown as { admin : unknown } ) . admin = admin ;
183- return resolved ;
215+ storage : toStorageConfig ( DASHCOMMERCE_STORAGE ) ,
216+ hooks : HOOKS ,
217+ routes : toNativeRoutes ( ROUTES ) ,
218+ admin : {
219+ pages : ADMIN_PAGES ,
220+ widgets : ADMIN_WIDGETS ,
221+ fieldWidgets : FIELD_WIDGETS ,
222+ portableTextBlocks : PORTABLE_TEXT_BLOCKS ,
223+ } ,
224+ } ) ;
184225}
185226
186227/**
187- * Default export kept for direct-import consumers + tests that read the
188- * raw { hooks, routes } definition without going through adaptSandboxEntry .
228+ * Default export kept for direct-import consumers + tests that read the raw
229+ * ` { hooks, routes }` definition without going through `createPlugin` .
189230 */
190- export default definition ;
231+ export default { hooks : HOOKS , routes : ROUTES } ;
0 commit comments