@sonicjs-cms/core 2.19.0 → 3.0.0-beta.2
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 +4 -3
- package/dist/admin-documents-form.template-KN7JF66Q.cjs +19 -0
- package/dist/{admin-layout-catalyst.template-UMTIN66R.js.map → admin-documents-form.template-KN7JF66Q.cjs.map} +1 -1
- package/dist/admin-documents-form.template-NLSI6Z42.js +6 -0
- package/dist/{admin-layout-catalyst.template-HFD37TY5.cjs.map → admin-documents-form.template-NLSI6Z42.js.map} +1 -1
- package/dist/admin-layout-catalyst.template-WHJGSWWD.js +7 -0
- package/dist/admin-layout-catalyst.template-WHJGSWWD.js.map +1 -0
- package/dist/admin-layout-catalyst.template-ZK5HD545.cjs +17 -0
- package/dist/admin-layout-catalyst.template-ZK5HD545.cjs.map +1 -0
- package/dist/app-Bo0X1OWX.d.ts +1268 -0
- package/dist/app-Do66yCcV.d.cts +1268 -0
- package/dist/cache-DDARE4QE.js +4 -0
- package/dist/cache-DDARE4QE.js.map +1 -0
- package/dist/cache-LVYS4BPL.cjs +33 -0
- package/dist/cache-LVYS4BPL.cjs.map +1 -0
- package/dist/chunk-2CB4KY7I.cjs +771 -0
- package/dist/chunk-2CB4KY7I.cjs.map +1 -0
- package/dist/{chunk-55RDMDOP.js → chunk-3TB6AT6X.js} +148 -55
- package/dist/chunk-3TB6AT6X.js.map +1 -0
- package/dist/{chunk-ON5ZMSU4.js → chunk-6JQOUUOB.js} +3 -3
- package/dist/chunk-6JQOUUOB.js.map +1 -0
- package/dist/chunk-6OUHGKFD.js +387 -0
- package/dist/chunk-6OUHGKFD.js.map +1 -0
- package/dist/{chunk-7A4CB7T3.cjs → chunk-AAWNRBRB.cjs} +509 -91
- package/dist/chunk-AAWNRBRB.cjs.map +1 -0
- package/dist/chunk-AI663NBO.js +821 -0
- package/dist/chunk-AI663NBO.js.map +1 -0
- package/dist/chunk-BDDABDAB.cjs +1149 -0
- package/dist/chunk-BDDABDAB.cjs.map +1 -0
- package/dist/chunk-BLMTL57B.js +767 -0
- package/dist/chunk-BLMTL57B.js.map +1 -0
- package/dist/chunk-DNQCEKUK.cjs +327 -0
- package/dist/chunk-DNQCEKUK.cjs.map +1 -0
- package/dist/chunk-DSA4UX5B.cjs +276 -0
- package/dist/chunk-DSA4UX5B.cjs.map +1 -0
- package/dist/chunk-EF2NQUIQ.js +323 -0
- package/dist/chunk-EF2NQUIQ.js.map +1 -0
- package/dist/chunk-GCDZZNIN.js +192 -0
- package/dist/chunk-GCDZZNIN.js.map +1 -0
- package/dist/{chunk-ABB34XUS.cjs → chunk-H2AXVCLS.cjs} +667 -19
- package/dist/chunk-H2AXVCLS.cjs.map +1 -0
- package/dist/{chunk-XWIA3HVX.js → chunk-HDWE5FRJ.js} +6 -1249
- package/dist/chunk-HDWE5FRJ.js.map +1 -0
- package/dist/chunk-HIKBY7MS.cjs +70 -0
- package/dist/chunk-HIKBY7MS.cjs.map +1 -0
- package/dist/chunk-IESEVHXL.js +66 -0
- package/dist/chunk-IESEVHXL.js.map +1 -0
- package/dist/chunk-IVPRUGTY.js +242 -0
- package/dist/chunk-IVPRUGTY.js.map +1 -0
- package/dist/{chunk-JZVHLLSI.cjs → chunk-IXUHXTHW.cjs} +2 -151
- package/dist/chunk-IXUHXTHW.cjs.map +1 -0
- package/dist/chunk-J6JTWD2A.cjs +100 -0
- package/dist/chunk-J6JTWD2A.cjs.map +1 -0
- package/dist/chunk-JEQ7FLOD.cjs +199 -0
- package/dist/chunk-JEQ7FLOD.cjs.map +1 -0
- package/dist/chunk-K25XHMM3.js +566 -0
- package/dist/chunk-K25XHMM3.js.map +1 -0
- package/dist/chunk-LRZIAW7U.cjs +158 -0
- package/dist/chunk-LRZIAW7U.cjs.map +1 -0
- package/dist/{chunk-OHYBNCVL.cjs → chunk-MVIZJOO5.cjs} +10 -1256
- package/dist/chunk-MVIZJOO5.cjs.map +1 -0
- package/dist/{chunk-UYJ6TJHX.cjs → chunk-NAVPFIG5.cjs} +148 -55
- package/dist/chunk-NAVPFIG5.cjs.map +1 -0
- package/dist/chunk-NLJVSER2.js +273 -0
- package/dist/chunk-NLJVSER2.js.map +1 -0
- package/dist/chunk-NMPEMSU4.js +154 -0
- package/dist/chunk-NMPEMSU4.js.map +1 -0
- package/dist/chunk-NUKJ54GA.cjs +245 -0
- package/dist/chunk-NUKJ54GA.cjs.map +1 -0
- package/dist/{chunk-E4YFJBM2.cjs → chunk-QAYFOER6.cjs} +621 -829
- package/dist/chunk-QAYFOER6.cjs.map +1 -0
- package/dist/{chunk-BU7SFHGP.js → chunk-QZGABF2M.js} +3 -149
- package/dist/chunk-QZGABF2M.js.map +1 -0
- package/dist/chunk-RNZFGN4R.js +88 -0
- package/dist/chunk-RNZFGN4R.js.map +1 -0
- package/dist/{chunk-4NPCDK6B.js → chunk-RZ6H7OZK.js} +505 -90
- package/dist/chunk-RZ6H7OZK.js.map +1 -0
- package/dist/{chunk-OCL3HMEG.js → chunk-VD2EA3WT.js} +7004 -9807
- package/dist/chunk-VD2EA3WT.js.map +1 -0
- package/dist/{chunk-R4FOLLFB.cjs → chunk-VXE42MYF.cjs} +8730 -11520
- package/dist/chunk-VXE42MYF.cjs.map +1 -0
- package/dist/{chunk-4ZSNJDLS.cjs → chunk-WULONYGB.cjs} +9 -9
- package/dist/chunk-WULONYGB.cjs.map +1 -0
- package/dist/chunk-XW56B23A.cjs +408 -0
- package/dist/chunk-XW56B23A.cjs.map +1 -0
- package/dist/chunk-YA3TJ65D.cjs +575 -0
- package/dist/chunk-YA3TJ65D.cjs.map +1 -0
- package/dist/{chunk-TFNTM3OA.js → chunk-YHSQVQXX.js} +645 -15
- package/dist/chunk-YHSQVQXX.js.map +1 -0
- package/dist/chunk-YP7GW2G5.cjs +866 -0
- package/dist/chunk-YP7GW2G5.cjs.map +1 -0
- package/dist/{chunk-QFWHAFEO.js → chunk-ZEZ245PW.js} +148 -858
- package/dist/chunk-ZEZ245PW.js.map +1 -0
- package/dist/{chunk-JZV22DEV.js → chunk-ZGGXCFR6.js} +611 -817
- package/dist/chunk-ZGGXCFR6.js.map +1 -0
- package/dist/{collection-config-B4PG-AaF.d.cts → collection-config-JgHOpFCG.d.cts} +30 -2
- package/dist/{collection-config-B4PG-AaF.d.ts → collection-config-JgHOpFCG.d.ts} +30 -2
- package/dist/config-HFXANXCC.js +6 -0
- package/dist/config-HFXANXCC.js.map +1 -0
- package/dist/config-ON6FNMYX.cjs +19 -0
- package/dist/config-ON6FNMYX.cjs.map +1 -0
- package/dist/define-plugin-BzNHc1ZI.d.ts +1321 -0
- package/dist/define-plugin-IWDKYaVm.d.cts +1321 -0
- package/dist/document-projection-TDWRJX3Z.cjs +13 -0
- package/dist/document-projection-TDWRJX3Z.cjs.map +1 -0
- package/dist/document-projection-YYMC6I4U.js +4 -0
- package/dist/document-projection-YYMC6I4U.js.map +1 -0
- package/dist/index.cjs +13734 -4328
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +329 -492
- package/dist/index.d.ts +329 -492
- package/dist/index.js +13385 -3998
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +36 -32
- package/dist/middleware.d.cts +50 -7
- package/dist/middleware.d.ts +50 -7
- package/dist/middleware.js +7 -3
- package/dist/migrations-NJJWQUKK.cjs +13 -0
- package/dist/{migrations-566IIPS2.cjs.map → migrations-NJJWQUKK.cjs.map} +1 -1
- package/dist/migrations-WCAVBD7C.js +4 -0
- package/dist/{migrations-H5IXZNCO.js.map → migrations-WCAVBD7C.js.map} +1 -1
- package/dist/{plugin-bootstrap-DfVerYV4.d.cts → plugin-bootstrap-B8ThJU21.d.cts} +4315 -1661
- package/dist/{plugin-bootstrap-P_ciLp_C.d.ts → plugin-bootstrap-qu8hJgUt.d.ts} +4315 -1661
- package/dist/plugins.cjs +171 -12
- package/dist/plugins.d.cts +36 -2
- package/dist/plugins.d.ts +36 -2
- package/dist/plugins.js +5 -2
- package/dist/rbac-O73MFKDA.js +5 -0
- package/dist/rbac-O73MFKDA.js.map +1 -0
- package/dist/rbac-VONLJJKB.cjs +14 -0
- package/dist/rbac-VONLJJKB.cjs.map +1 -0
- package/dist/routes.cjs +41 -45
- package/dist/routes.d.cts +56 -146
- package/dist/routes.d.ts +56 -146
- package/dist/routes.js +17 -9
- package/dist/services.cjs +39 -72
- package/dist/services.d.cts +79 -54
- package/dist/services.d.ts +79 -54
- package/dist/services.js +6 -3
- package/dist/templates.cjs +17 -29
- package/dist/templates.d.cts +1 -66
- package/dist/templates.d.ts +1 -66
- package/dist/templates.js +3 -3
- package/dist/types-Dea1eNxU.d.cts +286 -0
- package/dist/types-Dea1eNxU.d.ts +286 -0
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utils.cjs +18 -17
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -1
- package/migrations/0001_core.sql +184 -0
- package/migrations/0002_documents.sql +163 -0
- package/package.json +12 -7
- package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +0 -17
- package/dist/admin-layout-catalyst.template-UMTIN66R.js +0 -7
- package/dist/app-C9esKLmh.d.cts +0 -112
- package/dist/app-C9esKLmh.d.ts +0 -112
- package/dist/chunk-4NPCDK6B.js.map +0 -1
- package/dist/chunk-4ZSNJDLS.cjs.map +0 -1
- package/dist/chunk-55RDMDOP.js.map +0 -1
- package/dist/chunk-635JAMSE.cjs +0 -653
- package/dist/chunk-635JAMSE.cjs.map +0 -1
- package/dist/chunk-7A4CB7T3.cjs.map +0 -1
- package/dist/chunk-ABB34XUS.cjs.map +0 -1
- package/dist/chunk-BU7SFHGP.js.map +0 -1
- package/dist/chunk-E4YFJBM2.cjs.map +0 -1
- package/dist/chunk-EXNEW5US.js +0 -648
- package/dist/chunk-EXNEW5US.js.map +0 -1
- package/dist/chunk-JZV22DEV.js.map +0 -1
- package/dist/chunk-JZVHLLSI.cjs.map +0 -1
- package/dist/chunk-OCL3HMEG.js.map +0 -1
- package/dist/chunk-OHYBNCVL.cjs.map +0 -1
- package/dist/chunk-ON5ZMSU4.js.map +0 -1
- package/dist/chunk-QFWHAFEO.js.map +0 -1
- package/dist/chunk-R4FOLLFB.cjs.map +0 -1
- package/dist/chunk-RLMUFFUD.cjs +0 -2219
- package/dist/chunk-RLMUFFUD.cjs.map +0 -1
- package/dist/chunk-TFNTM3OA.js.map +0 -1
- package/dist/chunk-UYJ6TJHX.cjs.map +0 -1
- package/dist/chunk-WAEQXGCX.cjs +0 -1898
- package/dist/chunk-WAEQXGCX.cjs.map +0 -1
- package/dist/chunk-XWIA3HVX.js.map +0 -1
- package/dist/chunk-ZYAYUIZE.js +0 -2217
- package/dist/chunk-ZYAYUIZE.js.map +0 -1
- package/dist/migrations-566IIPS2.cjs +0 -13
- package/dist/migrations-H5IXZNCO.js +0 -4
- package/dist/plugin-manager-BoM3Q7o7.d.cts +0 -328
- package/dist/plugin-manager-Efx9RyDX.d.ts +0 -328
- package/migrations/001_initial_schema.sql +0 -170
- package/migrations/002_faq_plugin.sql +0 -86
- package/migrations/003_stage5_enhancements.sql +0 -121
- package/migrations/004_stage6_user_management.sql +0 -183
- package/migrations/005_stage7_workflow_automation.sql +0 -294
- package/migrations/006_plugin_system.sql +0 -155
- package/migrations/007_demo_login_plugin.sql +0 -23
- package/migrations/008_fix_slug_validation.sql +0 -22
- package/migrations/009_system_logging.sql +0 -57
- package/migrations/011_config_managed_collections.sql +0 -15
- package/migrations/012_testimonials_plugin.sql +0 -80
- package/migrations/013_code_examples_plugin.sql +0 -177
- package/migrations/014_fix_plugin_registry.sql +0 -88
- package/migrations/015_add_remaining_plugins.sql +0 -89
- package/migrations/016_remove_duplicate_cache_plugin.sql +0 -17
- package/migrations/017_auth_configurable_fields.sql +0 -49
- package/migrations/018_settings_table.sql +0 -23
- package/migrations/019_remove_blog_posts_collection.sql +0 -15
- package/migrations/020_add_email_plugin.sql +0 -22
- package/migrations/021_add_magic_link_auth_plugin.sql +0 -42
- package/migrations/022_add_tinymce_plugin.sql +0 -25
- package/migrations/023_add_easy_mdx_plugin.sql +0 -25
- package/migrations/024_add_quill_editor_plugin.sql +0 -25
- package/migrations/025_add_easymde_plugin.sql +0 -25
- package/migrations/026_add_otp_login.sql +0 -42
- package/migrations/027_fix_slug_field_type.sql +0 -18
- package/migrations/028_fix_slug_field_type_in_schemas.sql +0 -30
- package/migrations/029_add_forms_system.sql +0 -184
- package/migrations/030_add_turnstile_to_forms.sql +0 -14
- package/migrations/031_ai_search_plugin.sql +0 -45
- package/migrations/032_user_profiles.sql +0 -37
- package/migrations/033_form_content_integration.sql +0 -19
- package/migrations/034_security_audit_plugin.sql +0 -27
- package/migrations/035_user_profiles_data_column.sql +0 -16
- package/migrations/036_analytics_events.sql +0 -22
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
import { i as HookSystemLike, m as TypedHooks, E as EmailProvider, s as EmailLogRow, t as EmailMessage, q as SendResult, f as HookEventName, l as TypedHookHandler } from './types-Dea1eNxU.cjs';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { d as HookSystem, b as HookHandler, k as PluginHook, v as PluginValidator$1, P as Plugin, u as PluginValidationResult, q as PluginRegistry, i as PluginConfig, t as PluginStatus, m as PluginManager$1, j as PluginContext } from './plugin-DDYetMF-.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin cron surface
|
|
7
|
+
*
|
|
8
|
+
* A plugin declares scheduled work as data:
|
|
9
|
+
*
|
|
10
|
+
* crons: [{ schedule: '*\/15 * * * *', hookFamily: 'email-reconciliation' }]
|
|
11
|
+
* async onCronTick(event, ctx) {
|
|
12
|
+
* if (event.hookFamily !== 'email-reconciliation') return
|
|
13
|
+
* ...
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Declaring a schedule does NOT by itself run anything (same as Payload's jobs
|
|
17
|
+
* queue): on Cloudflare Workers the execution mechanism is a Cron Trigger, which
|
|
18
|
+
* the runtime delivers to the Worker's `scheduled()` handler. `createScheduledHandler`
|
|
19
|
+
* builds that handler; it fans a fired trigger out to the plugins whose declared
|
|
20
|
+
* schedule matches, tagging each call with the matching `hookFamily` so a plugin
|
|
21
|
+
* with several crons can branch on it.
|
|
22
|
+
*
|
|
23
|
+
* The consumer must still register the cron expressions in `wrangler.toml`
|
|
24
|
+
* (`[triggers] crons = [...]`); the schedules declared here are the source of
|
|
25
|
+
* truth for which plugin handles which expression.
|
|
26
|
+
*
|
|
27
|
+
* Cron runs OUTSIDE the HTTP request context (no per-request `c.env`), which is
|
|
28
|
+
* exactly why services are reached through env-independent singletons.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** A scheduled-work declaration on a plugin. */
|
|
32
|
+
interface CronDeclaration {
|
|
33
|
+
/** Cron expression, e.g. `'*\/15 * * * *'`. Must also be in wrangler.toml triggers. */
|
|
34
|
+
schedule: string;
|
|
35
|
+
/** Logical family the handler branches on (e.g. `'email-reconciliation'`). */
|
|
36
|
+
hookFamily: string;
|
|
37
|
+
}
|
|
38
|
+
/** The event passed to a plugin's `onCronTick`. */
|
|
39
|
+
interface CronTickEvent {
|
|
40
|
+
/** The cron expression that fired. */
|
|
41
|
+
cron: string;
|
|
42
|
+
/** Epoch ms the trigger was scheduled for. */
|
|
43
|
+
scheduledTime: number;
|
|
44
|
+
/** The matching declaration's family, so a multi-cron plugin can branch. */
|
|
45
|
+
hookFamily: string;
|
|
46
|
+
}
|
|
47
|
+
/** Context handed to `onCronTick`. Mirrors the boot context; carries no request env. */
|
|
48
|
+
interface CronContext {
|
|
49
|
+
/** The live hook system (so cron work can dispatch/observe hooks). */
|
|
50
|
+
hooks: HookSystemLike;
|
|
51
|
+
/** Runtime bindings supplied by the Worker's scheduled() invocation. */
|
|
52
|
+
env?: Record<string, unknown>;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Structural contract this module needs from a plugin. Deliberately minimal (not
|
|
57
|
+
* the full `Plugin`) so the `src`/`dist` duplicate identities and user plugins all
|
|
58
|
+
* satisfy it without casts.
|
|
59
|
+
*/
|
|
60
|
+
interface CronablePlugin {
|
|
61
|
+
name?: string;
|
|
62
|
+
crons?: CronDeclaration[];
|
|
63
|
+
onCronTick?: (event: CronTickEvent, context: CronContext) => void | Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** A flattened view of every declared cron across a set of plugins. */
|
|
66
|
+
interface CollectedCron {
|
|
67
|
+
plugin: string;
|
|
68
|
+
schedule: string;
|
|
69
|
+
hookFamily: string;
|
|
70
|
+
}
|
|
71
|
+
/** Flatten every plugin's `crons[]` into one list (for diagnostics / wrangler sync). */
|
|
72
|
+
declare function collectCrons(plugins: Array<CronablePlugin | undefined | null>): CollectedCron[];
|
|
73
|
+
/** The set of distinct cron expressions declared across plugins. */
|
|
74
|
+
declare function collectCronSchedules(plugins: Array<CronablePlugin | undefined | null>): string[];
|
|
75
|
+
/** Outcome of a cron dispatch. */
|
|
76
|
+
interface CronDispatchResult {
|
|
77
|
+
/** Plugins whose `onCronTick` ran successfully (one entry per matching declaration). */
|
|
78
|
+
invoked: Array<{
|
|
79
|
+
plugin: string;
|
|
80
|
+
hookFamily: string;
|
|
81
|
+
}>;
|
|
82
|
+
/** Per-plugin errors (dispatch never throws). */
|
|
83
|
+
errors: Array<{
|
|
84
|
+
plugin: string;
|
|
85
|
+
hookFamily: string;
|
|
86
|
+
error: unknown;
|
|
87
|
+
}>;
|
|
88
|
+
/** True if the fired cron matched no declared schedule. */
|
|
89
|
+
unmatched: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Dispatch one fired cron expression to the plugins that declared it.
|
|
93
|
+
*
|
|
94
|
+
* For each plugin whose `crons[]` contains a declaration with `schedule === cron`,
|
|
95
|
+
* its `onCronTick` is invoked once per matching declaration, with the event's
|
|
96
|
+
* `hookFamily` set to that declaration's family. Errors are isolated per plugin.
|
|
97
|
+
*/
|
|
98
|
+
declare function dispatchCronTick(plugins: Array<CronablePlugin | undefined | null>, cron: string, scheduledTime: number, context: CronContext): Promise<CronDispatchResult>;
|
|
99
|
+
/** Minimal shape of a Cloudflare `ScheduledController`. */
|
|
100
|
+
interface ScheduledControllerLike {
|
|
101
|
+
cron: string;
|
|
102
|
+
scheduledTime: number;
|
|
103
|
+
}
|
|
104
|
+
/** Minimal shape of a Cloudflare `ExecutionContext`. */
|
|
105
|
+
interface ExecutionContextLike {
|
|
106
|
+
waitUntil?(promise: Promise<unknown>): void;
|
|
107
|
+
}
|
|
108
|
+
interface CreateScheduledHandlerOptions {
|
|
109
|
+
/** Plugins to consider (evaluated lazily at fire time). */
|
|
110
|
+
plugins: Array<CronablePlugin | undefined | null> | (() => Array<CronablePlugin | undefined | null>);
|
|
111
|
+
/** Provides the hook system (e.g. the singleton getter). */
|
|
112
|
+
getHooks: () => HookSystemLike;
|
|
113
|
+
/** Optional: when true, skip dispatch entirely (mirrors plugins.disableAll). */
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Boot function from {@link SonicJSApp.boot}. Called before the first cron
|
|
117
|
+
* dispatch so a cron-first cold isolate (one that never handled an HTTP
|
|
118
|
+
* request) still has a wired hook bus and reachable email service.
|
|
119
|
+
* Without this, `getHookSystem()` may throw in cron handlers.
|
|
120
|
+
*/
|
|
121
|
+
boot?: (env: Record<string, unknown>) => Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a Cloudflare `scheduled(controller, env, ctx)` handler that fans cron
|
|
125
|
+
* triggers out to plugins.
|
|
126
|
+
*
|
|
127
|
+
* Usage in a Worker entry:
|
|
128
|
+
* export default {
|
|
129
|
+
* fetch: app.fetch,
|
|
130
|
+
* scheduled: createScheduledHandler({ plugins, getHooks: getHookSystem }),
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
declare function createScheduledHandler(options: CreateScheduledHandlerOptions): (controller: ScheduledControllerLike, env: Record<string, unknown>, ctx?: ExecutionContextLike) => Promise<CronDispatchResult>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Hook-system singleton
|
|
137
|
+
*
|
|
138
|
+
* Gives env-independent access to the app's hook system. Code that runs outside
|
|
139
|
+
* the HTTP request context — most importantly scheduled (cron) handlers, which
|
|
140
|
+
* have no per-request `c.env` — needs a way to reach the hook system without
|
|
141
|
+
* threading it through every call. The app sets the singleton eagerly at
|
|
142
|
+
* construction; everything else reads it.
|
|
143
|
+
*
|
|
144
|
+
* Contract: `getHookSystem()` throws if read before the app has set one
|
|
145
|
+
* (throw-before-get), which surfaces wiring-order bugs loudly instead of
|
|
146
|
+
* silently no-oping. `setHookSystem()` is idempotent (last write wins) so that
|
|
147
|
+
* constructing multiple apps in one process — e.g. across tests — does not
|
|
148
|
+
* throw; call `resetHookSystem()` in test teardown for isolation.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/** Set the process-wide hook system. Last write wins. */
|
|
152
|
+
declare function setHookSystem(hookSystem: HookSystemLike): void;
|
|
153
|
+
/**
|
|
154
|
+
* Get the process-wide hook system.
|
|
155
|
+
* @throws if no hook system has been set yet.
|
|
156
|
+
*/
|
|
157
|
+
declare function getHookSystem(): HookSystemLike;
|
|
158
|
+
/** True if a hook system has been set. */
|
|
159
|
+
declare function hasHookSystem(): boolean;
|
|
160
|
+
/** Clear the singleton. Intended for test isolation. */
|
|
161
|
+
declare function resetHookSystem(): void;
|
|
162
|
+
/** Convenience: a typed facade over the current singleton hook system. */
|
|
163
|
+
declare function getTypedHooks(): TypedHooks;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Plugin route mounting primitive
|
|
167
|
+
*
|
|
168
|
+
* This is the shared, position-aware primitive that mounts plugin routes into
|
|
169
|
+
* the Hono app. It replaces the hand-wired, copy-pasted
|
|
170
|
+
* `if (plugin.routes) { for (...) app.route(...) }` blocks that previously lived
|
|
171
|
+
* in `app.ts`.
|
|
172
|
+
*
|
|
173
|
+
* ## Why synchronous
|
|
174
|
+
*
|
|
175
|
+
* Hono's `SmartRouter` builds (and then locks) its match tree on the first
|
|
176
|
+
* request. Calling `app.route()` after that throws:
|
|
177
|
+
*
|
|
178
|
+
* `Error: Can not add a route since the matcher is already built`
|
|
179
|
+
*
|
|
180
|
+
* Therefore route mounting MUST happen synchronously at app-construction time,
|
|
181
|
+
* before any request is served. A plugin's imperative `register(app)` hook is
|
|
182
|
+
* held to the same contract: returning a Promise is a hard error
|
|
183
|
+
* (`PluginRegisterMustBeSyncError`) rather than a silent hang. Asynchronous,
|
|
184
|
+
* env-dependent work (hook subscriptions, services, crons) belongs in a later
|
|
185
|
+
* lazy "wire" phase — not here.
|
|
186
|
+
*
|
|
187
|
+
* ## Position-awareness
|
|
188
|
+
*
|
|
189
|
+
* Plugin routes must be mounted BEFORE the bare `/admin` catch-all so that a
|
|
190
|
+
* plugin's own `/admin/<x>` pages are not shadowed. Callers are responsible for
|
|
191
|
+
* invoking `registerPluginRoutes()` at the correct position in `app.ts`; this
|
|
192
|
+
* module just performs the mounting deterministically.
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Minimal structural contract this module needs to mount a plugin.
|
|
197
|
+
*
|
|
198
|
+
* Deliberately NOT the full `Plugin` interface: the core plugins are typed
|
|
199
|
+
* against the built `@sonicjs-cms/core` `dist` declarations, while this file
|
|
200
|
+
* lives in `src`, and TypeScript treats those two `Plugin` types as distinct
|
|
201
|
+
* nominal identities. Accepting a structural subset lets both the `src` and
|
|
202
|
+
* `dist` `Plugin` shapes (and user-supplied plugins) be mounted without casts,
|
|
203
|
+
* and keeps `mount.ts` honest about what it actually reads.
|
|
204
|
+
*/
|
|
205
|
+
interface MountableRoute {
|
|
206
|
+
path: string;
|
|
207
|
+
handler: unknown;
|
|
208
|
+
priority?: number;
|
|
209
|
+
}
|
|
210
|
+
interface MountablePlugin {
|
|
211
|
+
/** Stable unique id (used by topo-sort). Falls back to `name`. */
|
|
212
|
+
id?: string;
|
|
213
|
+
name?: string;
|
|
214
|
+
/** IDs of plugins that must be mounted before this one. */
|
|
215
|
+
dependencies?: string[];
|
|
216
|
+
routes?: MountableRoute[];
|
|
217
|
+
/** Synchronous imperative route registration. A Promise return is rejected. */
|
|
218
|
+
register?: (app: any) => unknown;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Thrown when a plugin's `register(app)` hook returns a Promise.
|
|
222
|
+
*
|
|
223
|
+
* `register()` runs synchronously at construction time (see module docs). Move
|
|
224
|
+
* any async work to a lifecycle hook (`install`/`activate`) or the async wiring
|
|
225
|
+
* phase instead.
|
|
226
|
+
*/
|
|
227
|
+
declare class PluginRegisterMustBeSyncError extends Error {
|
|
228
|
+
constructor(pluginName: string);
|
|
229
|
+
}
|
|
230
|
+
/** A single mounted route, for diagnostics/introspection. */
|
|
231
|
+
interface MountedRoute {
|
|
232
|
+
plugin: string;
|
|
233
|
+
path: string;
|
|
234
|
+
}
|
|
235
|
+
/** Outcome of a mount pass. */
|
|
236
|
+
interface MountResult {
|
|
237
|
+
/** Routes successfully mounted, in mount order. */
|
|
238
|
+
mounted: MountedRoute[];
|
|
239
|
+
/** Plugins (or routes) skipped, with a human-readable reason. */
|
|
240
|
+
skipped: Array<{
|
|
241
|
+
plugin: string;
|
|
242
|
+
reason: string;
|
|
243
|
+
}>;
|
|
244
|
+
}
|
|
245
|
+
type AnyHono = Hono<any, any, any>;
|
|
246
|
+
/**
|
|
247
|
+
* Mount a single plugin's routes into `app`.
|
|
248
|
+
*
|
|
249
|
+
* Applies, in order:
|
|
250
|
+
* 1. The plugin's declarative `routes[]` (sorted by priority), and
|
|
251
|
+
* 2. The plugin's imperative `register(app)` hook, if present (sync-guarded).
|
|
252
|
+
*
|
|
253
|
+
* @param app Host Hono app (mount BEFORE the `/admin` catch-all).
|
|
254
|
+
* @param plugin Plugin to mount.
|
|
255
|
+
* @param result Optional accumulator for diagnostics.
|
|
256
|
+
*/
|
|
257
|
+
declare function mountPlugin(app: AnyHono, plugin: MountablePlugin, result?: MountResult): void;
|
|
258
|
+
/** Options for {@link registerPluginRoutes}. */
|
|
259
|
+
interface RegisterPluginRoutesOptions {
|
|
260
|
+
/**
|
|
261
|
+
* Label used in dev warnings (e.g. `'core'` or `'user'`). Purely cosmetic.
|
|
262
|
+
*/
|
|
263
|
+
source?: string;
|
|
264
|
+
/**
|
|
265
|
+
* When true, log a warning if two plugins mount the exact same route path
|
|
266
|
+
* (later registration is shadowed by Hono's first-match semantics). Defaults
|
|
267
|
+
* to true in non-production.
|
|
268
|
+
*/
|
|
269
|
+
warnOnDuplicatePath?: boolean;
|
|
270
|
+
/**
|
|
271
|
+
* When true, sort plugins by their `dependencies` field before mounting
|
|
272
|
+
* (dependency-first order). Defaults to true.
|
|
273
|
+
*/
|
|
274
|
+
sortByDependencies?: boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Forwarded to `topoSort` — unknown dependency id throws when true, warns
|
|
277
|
+
* when false (default).
|
|
278
|
+
*/
|
|
279
|
+
strict?: boolean;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Mount a list of plugins into `app`, in array order.
|
|
283
|
+
*
|
|
284
|
+
* This is the generic replacement for the hand-wired plugin blocks. Plugins are
|
|
285
|
+
* mounted in the order given (the caller decides ordering — typically core
|
|
286
|
+
* plugins first, then user `plugins.register` plugins), each before the bare
|
|
287
|
+
* `/admin` catch-all.
|
|
288
|
+
*
|
|
289
|
+
* Invalid entries are skipped (recorded in the result) rather than throwing, so
|
|
290
|
+
* one malformed plugin can't take down the whole app. The one hard error is a
|
|
291
|
+
* plugin whose `register()` is asynchronous — that is a contract violation that
|
|
292
|
+
* must surface loudly.
|
|
293
|
+
*
|
|
294
|
+
* @returns A {@link MountResult} describing what was mounted/skipped.
|
|
295
|
+
*/
|
|
296
|
+
declare function registerPluginRoutes(app: AnyHono, plugins: Array<MountablePlugin | undefined | null>, options?: RegisterPluginRoutesOptions): MountResult;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Plugin wiring phase (the async half of two-phase boot)
|
|
300
|
+
*
|
|
301
|
+
* Route mounting is synchronous and happens at construction (see `mount.ts`).
|
|
302
|
+
* Everything that needs the runtime environment — hook subscriptions and the
|
|
303
|
+
* `onBoot` lifecycle — happens here, lazily, on the first request, after every
|
|
304
|
+
* plugin has registered. Splitting the two avoids Hono's "matcher already built"
|
|
305
|
+
* lock (routes must mount before the first request; env-dependent wiring can
|
|
306
|
+
* only run once a request supplies `c.env`).
|
|
307
|
+
*
|
|
308
|
+
* This module is the wiring mechanism. Where/when it is invoked (a once-guarded
|
|
309
|
+
* first-request step) is the caller's concern; `createPluginWirer()` provides
|
|
310
|
+
* the once-guard.
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
/** A hook subscription declared by a plugin (the legacy `hooks[]` shape). */
|
|
314
|
+
interface WirableHook {
|
|
315
|
+
name: string;
|
|
316
|
+
handler: (data: any, context: any) => any;
|
|
317
|
+
priority?: number;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Structural contract this module needs from a plugin. Deliberately minimal (and
|
|
321
|
+
* not the full `Plugin` interface) so the `src`/`dist` duplicate `Plugin`
|
|
322
|
+
* identities and user-supplied plugins all satisfy it without casts.
|
|
323
|
+
*/
|
|
324
|
+
interface WirablePlugin {
|
|
325
|
+
/** Stable unique id (used by topo-sort). Falls back to `name`. */
|
|
326
|
+
id?: string;
|
|
327
|
+
name?: string;
|
|
328
|
+
/** IDs of plugins that must be wired before this one. */
|
|
329
|
+
dependencies?: string[];
|
|
330
|
+
/**
|
|
331
|
+
* When true, the plugin is inserted as 'active' on first install.
|
|
332
|
+
* Subsequent boots leave the admin-managed status untouched (the ON CONFLICT
|
|
333
|
+
* clause does not update `status`), so admins can still deactivate it.
|
|
334
|
+
*/
|
|
335
|
+
defaultActive?: boolean;
|
|
336
|
+
/**
|
|
337
|
+
* Capabilities declared by the plugin. When present (even as an empty array),
|
|
338
|
+
* the wire phase enforces that each declarative hook subscription is covered by
|
|
339
|
+
* a matching capability in {@link HOOK_CAPABILITY_MAP}. Absent on old-style
|
|
340
|
+
* `PluginBuilder` plugins — the gate is skipped for backwards compatibility.
|
|
341
|
+
*/
|
|
342
|
+
capabilities?: readonly string[];
|
|
343
|
+
hooks?: WirableHook[];
|
|
344
|
+
/**
|
|
345
|
+
* Async lifecycle hook run once, after all plugins have registered and their
|
|
346
|
+
* hooks are subscribed. The place for env-dependent setup (services, seeding,
|
|
347
|
+
* cron registration). Errors are isolated per-plugin.
|
|
348
|
+
*/
|
|
349
|
+
onBoot?: (context: PluginBootContext) => void | Promise<void>;
|
|
350
|
+
}
|
|
351
|
+
/** Options forwarded to {@link wireRegisteredPlugins}. */
|
|
352
|
+
interface WireOptions {
|
|
353
|
+
/**
|
|
354
|
+
* Strict mode: capability violations are captured as errors in `WireResult`
|
|
355
|
+
* (and the hook is skipped) instead of only logging a warning.
|
|
356
|
+
* Also makes unknown dependency ids hard errors instead of warnings.
|
|
357
|
+
* Enable in CI or development; leave off in production for resilience.
|
|
358
|
+
*/
|
|
359
|
+
strict?: boolean;
|
|
360
|
+
/**
|
|
361
|
+
* When false, skip dependency-aware ordering. Defaults to true (plugins with
|
|
362
|
+
* `dependencies` are wired after their declared dependencies).
|
|
363
|
+
*/
|
|
364
|
+
sortByDependencies?: boolean;
|
|
365
|
+
}
|
|
366
|
+
/** Context handed to `onBoot`. Kept loose; concrete bindings are filled by the host. */
|
|
367
|
+
interface PluginBootContext {
|
|
368
|
+
/** The live hook system (already carrying every plugin's subscriptions). */
|
|
369
|
+
hooks: HookSystemLike;
|
|
370
|
+
/** Runtime bindings, when available. */
|
|
371
|
+
env?: Record<string, unknown>;
|
|
372
|
+
[key: string]: unknown;
|
|
373
|
+
}
|
|
374
|
+
/** Outcome of a wiring pass. */
|
|
375
|
+
interface WireResult {
|
|
376
|
+
/** Total hook subscriptions registered. */
|
|
377
|
+
subscribed: number;
|
|
378
|
+
/** Names of plugins whose `onBoot` completed successfully. */
|
|
379
|
+
booted: string[];
|
|
380
|
+
/** Per-plugin errors (wiring never throws; one bad plugin can't break boot). */
|
|
381
|
+
errors: Array<{
|
|
382
|
+
plugin: string;
|
|
383
|
+
phase: 'subscribe' | 'onBoot';
|
|
384
|
+
error: unknown;
|
|
385
|
+
}>;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Subscribe every plugin's declarative `hooks[]` to the live hook system, then
|
|
389
|
+
* run each plugin's `onBoot`, in array order.
|
|
390
|
+
*
|
|
391
|
+
* Subscriptions happen for ALL plugins first, so that one plugin's `onBoot` can
|
|
392
|
+
* rely on another plugin's hooks already being registered. Per-plugin errors are
|
|
393
|
+
* captured in the result rather than thrown.
|
|
394
|
+
*/
|
|
395
|
+
declare function wireRegisteredPlugins(plugins: Array<WirablePlugin | undefined | null>, context: PluginBootContext, options?: WireOptions): Promise<WireResult>;
|
|
396
|
+
/**
|
|
397
|
+
* Wrap {@link wireRegisteredPlugins} in a once-guard.
|
|
398
|
+
*
|
|
399
|
+
* The returned function runs the wiring at most once per process: the first call
|
|
400
|
+
* starts it and caches the promise; every later call returns that same promise.
|
|
401
|
+
* This is the first-request trigger — many concurrent first requests all await
|
|
402
|
+
* one wiring pass.
|
|
403
|
+
*
|
|
404
|
+
* @param plugins Plugins to wire (evaluated lazily at first call).
|
|
405
|
+
* @param ctxFactory Builds the boot context at first-call time (so it can read
|
|
406
|
+
* per-request env).
|
|
407
|
+
*/
|
|
408
|
+
declare function createPluginWirer(plugins: Array<WirablePlugin | undefined | null> | (() => Array<WirablePlugin | undefined | null>), ctxFactory: () => PluginBootContext): () => Promise<WireResult>;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Plugin admin-sidebar menu registry.
|
|
412
|
+
*
|
|
413
|
+
* Plugins declare `menu: PluginMenuEntry[]` on definePlugin. registerPlugins
|
|
414
|
+
* collects every entry into this module singleton; the admin layout reads via
|
|
415
|
+
* `resolvePluginMenuItems(user)` to render the sidebar.
|
|
416
|
+
*
|
|
417
|
+
* Phase 1 — strings + paths only. No custom React/HTMX components in menu
|
|
418
|
+
* entries (icons are name strings looked up by the sidebar's icon map).
|
|
419
|
+
*/
|
|
420
|
+
interface PluginMenuEntry {
|
|
421
|
+
/** Display label. */
|
|
422
|
+
label: string;
|
|
423
|
+
/** Admin URL the entry navigates to (e.g. `/admin/email`). */
|
|
424
|
+
path: string;
|
|
425
|
+
/** Icon name from the admin icon map. Default: `puzzle-piece`. */
|
|
426
|
+
icon?: string;
|
|
427
|
+
/** Sort key (ASC). Default 100. */
|
|
428
|
+
order?: number;
|
|
429
|
+
/** Required permission slugs; if any matches the user's permissions, the entry is shown. */
|
|
430
|
+
permissions?: readonly string[];
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Resolved entry — guaranteed `icon` + `order`, ready to render. Same shape
|
|
434
|
+
* the catalyst sidebar consumes via `dynamicMenuItems`. `icon` is the rendered
|
|
435
|
+
* SVG HTML string (resolved via {@link MENU_ICON_MAP} when the entry supplied
|
|
436
|
+
* a name; passed through verbatim when the entry supplied raw SVG/HTML).
|
|
437
|
+
*/
|
|
438
|
+
interface ResolvedPluginMenuEntry {
|
|
439
|
+
label: string;
|
|
440
|
+
path: string;
|
|
441
|
+
icon: string;
|
|
442
|
+
order: number;
|
|
443
|
+
}
|
|
444
|
+
declare function setPluginMenu(items: readonly PluginMenuEntry[]): void;
|
|
445
|
+
declare function getPluginMenu(): readonly PluginMenuEntry[];
|
|
446
|
+
declare function resetPluginMenu(): void;
|
|
447
|
+
/**
|
|
448
|
+
* Filter entries by user permissions, sort by `order` ASC, project to render-ready
|
|
449
|
+
* shape with default icon. Pure function — same inputs, same output.
|
|
450
|
+
*
|
|
451
|
+
* Permission semantics: an entry with no `permissions` is always visible. An entry
|
|
452
|
+
* with `permissions: ['x','y']` is visible if the user has ANY of `x` or `y`.
|
|
453
|
+
* Users with `role: 'admin'` bypass all permission checks (admins see everything).
|
|
454
|
+
*/
|
|
455
|
+
declare function resolvePluginMenuItems(user?: {
|
|
456
|
+
permissions?: readonly string[];
|
|
457
|
+
role?: string;
|
|
458
|
+
}): ResolvedPluginMenuEntry[];
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Schema-driven plugin settings.
|
|
462
|
+
*
|
|
463
|
+
* Plugins declare `configSchema: { key: ConfigSchemaField }` on definePlugin.
|
|
464
|
+
* The host renders the admin settings UI from the schema, parses FormData back
|
|
465
|
+
* into typed values, and exposes the resulting record via `ctx.settings.load()`.
|
|
466
|
+
*
|
|
467
|
+
* Field kinds: 'string' | 'number' | 'boolean' | 'select'. New kinds get added
|
|
468
|
+
* here once and every consumer picks them up.
|
|
469
|
+
*
|
|
470
|
+
* Phase 1 — settings UI only. The renderer emits plain HTML strings designed to
|
|
471
|
+
* compose into the existing admin layout (no client-side JS). FormData parsing
|
|
472
|
+
* coalesces unchecked-checkbox omission into `false` (the only browser quirk
|
|
473
|
+
* the renderer must account for).
|
|
474
|
+
*/
|
|
475
|
+
interface BaseField {
|
|
476
|
+
label: string;
|
|
477
|
+
description?: string;
|
|
478
|
+
required?: boolean;
|
|
479
|
+
}
|
|
480
|
+
interface StringField extends BaseField {
|
|
481
|
+
type: 'string';
|
|
482
|
+
default?: string;
|
|
483
|
+
format?: 'email' | 'url' | 'password';
|
|
484
|
+
/** Render as `<input type="password">`. Implied when `format === 'password'`. */
|
|
485
|
+
sensitive?: boolean;
|
|
486
|
+
placeholder?: string;
|
|
487
|
+
minLength?: number;
|
|
488
|
+
maxLength?: number;
|
|
489
|
+
}
|
|
490
|
+
interface NumberField extends BaseField {
|
|
491
|
+
type: 'number';
|
|
492
|
+
default?: number;
|
|
493
|
+
min?: number;
|
|
494
|
+
max?: number;
|
|
495
|
+
step?: number;
|
|
496
|
+
}
|
|
497
|
+
interface BooleanField extends BaseField {
|
|
498
|
+
type: 'boolean';
|
|
499
|
+
default?: boolean;
|
|
500
|
+
}
|
|
501
|
+
interface SelectField extends BaseField {
|
|
502
|
+
type: 'select';
|
|
503
|
+
default?: string;
|
|
504
|
+
/** Either `['us','eu']` shorthand or `[{ value, label }]` for distinct display strings. */
|
|
505
|
+
options: readonly string[] | readonly {
|
|
506
|
+
value: string;
|
|
507
|
+
label: string;
|
|
508
|
+
}[];
|
|
509
|
+
}
|
|
510
|
+
type ConfigSchemaField = StringField | NumberField | BooleanField | SelectField;
|
|
511
|
+
type ConfigSchema = Record<string, ConfigSchemaField>;
|
|
512
|
+
/** Parsed shape — typed record inferred from the schema. */
|
|
513
|
+
type SettingsFor<S extends ConfigSchema> = {
|
|
514
|
+
[K in keyof S]: S[K] extends StringField ? string : S[K] extends NumberField ? number : S[K] extends BooleanField ? boolean : S[K] extends SelectField ? string : never;
|
|
515
|
+
};
|
|
516
|
+
interface ParsedField {
|
|
517
|
+
key: string;
|
|
518
|
+
field: ConfigSchemaField;
|
|
519
|
+
}
|
|
520
|
+
/** Stable, ordered field list (preserves declaration order). */
|
|
521
|
+
declare function parseConfigSchema(schema: ConfigSchema): ParsedField[];
|
|
522
|
+
/**
|
|
523
|
+
* Render all fields as HTML controls. Output is a sequence of `<div class="field">…</div>`
|
|
524
|
+
* blocks designed to drop into the admin form template (no surrounding `<form>` or
|
|
525
|
+
* submit button — those belong to the page that calls this).
|
|
526
|
+
*/
|
|
527
|
+
declare function renderSchemaFields(schema: ConfigSchema, currentValues?: Record<string, unknown>): string;
|
|
528
|
+
/**
|
|
529
|
+
* Coerce FormData entries into typed settings per the schema. Notably:
|
|
530
|
+
* - Unchecked checkboxes are omitted by the browser; we coalesce to `false`.
|
|
531
|
+
* - Numbers parse via `Number()` and fall back to the field's default if invalid.
|
|
532
|
+
* - Strings preserve empty string (NOT default) so an admin can intentionally clear.
|
|
533
|
+
*/
|
|
534
|
+
declare function parseFormDataToSettings(schema: ConfigSchema, form: FormData): Record<string, unknown>;
|
|
535
|
+
/** Fill missing keys with field defaults. Existing keys (incl. `false`/`0`/'') win. */
|
|
536
|
+
declare function applySchemaDefaults<S extends ConfigSchema>(schema: S, stored: Record<string, unknown>): Record<string, unknown>;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* registerPlugins — single chokepoint for plugin registration.
|
|
540
|
+
*
|
|
541
|
+
* Replaces the historical pattern of calling `registerPluginRoutes` + scheduling
|
|
542
|
+
* `wireRegisteredPlugins` + manually wiring `addMenuItem` calls + per-plugin
|
|
543
|
+
* settings-route handlers.
|
|
544
|
+
*
|
|
545
|
+
* Authors write:
|
|
546
|
+
* const myPlugin = definePlugin({ id, version, capabilities, register, hooks, menu, configSchema, ... })
|
|
547
|
+
*
|
|
548
|
+
* Hosts call:
|
|
549
|
+
* const reg = registerPlugins(app, [pluginA, pluginB], { hookSystem, env, ... })
|
|
550
|
+
* // ...
|
|
551
|
+
* await reg.boot(env) // first-request lazy wire
|
|
552
|
+
*
|
|
553
|
+
* What this fn does, in order:
|
|
554
|
+
* 1. Validate each plugin (id, semver, dep references) — throws on hard errors
|
|
555
|
+
* 2. Topologically order by `dependencies`
|
|
556
|
+
* 3. MOUNT pass — invoke `registerPluginRoutes()` (sync, routes only)
|
|
557
|
+
* 4. Collect declarative `menu[]` entries → setPluginMenu (overwrites prior)
|
|
558
|
+
* 5. Collect declarative `crons[]` for the host scheduled() handler
|
|
559
|
+
* 6. Return a registry + a one-shot `boot(env)` that lazily runs the WIRE pass
|
|
560
|
+
* (subscribe hooks + run onBoot + DB-reflect) on first request
|
|
561
|
+
*
|
|
562
|
+
* The mount/wire split is preserved (Hono's router locks after first request, so
|
|
563
|
+
* routes MUST mount synchronously at construction; env-dependent setup is lazy).
|
|
564
|
+
* That split is now an implementation detail — authors see ONE function.
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
type RegisterPluginsErrorReason = 'invalid_id' | 'invalid_semver' | 'duplicate_id' | 'register_returned_promise';
|
|
568
|
+
declare class RegisterPluginsError extends Error {
|
|
569
|
+
readonly reason: RegisterPluginsErrorReason;
|
|
570
|
+
readonly details: Readonly<Record<string, unknown>>;
|
|
571
|
+
constructor(reason: RegisterPluginsErrorReason, details: Readonly<Record<string, unknown>>);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Structural contract: the union of MountablePlugin + WirablePlugin + the
|
|
575
|
+
* declarative admin surface. Plugins built via `definePlugin()` satisfy this
|
|
576
|
+
* automatically; hand-rolled objects need at minimum `id`, `version`.
|
|
577
|
+
*/
|
|
578
|
+
interface RegisterablePlugin extends MountablePlugin, WirablePlugin {
|
|
579
|
+
id: string;
|
|
580
|
+
version: string;
|
|
581
|
+
displayName?: string;
|
|
582
|
+
sonicjsVersionRange?: string;
|
|
583
|
+
menu?: PluginMenuEntry[];
|
|
584
|
+
crons?: CronDeclaration[];
|
|
585
|
+
/** Schema-driven settings — the admin route reads this via getPluginDefinition(id). */
|
|
586
|
+
configSchema?: ConfigSchema;
|
|
587
|
+
}
|
|
588
|
+
interface RegisterPluginsHostContext {
|
|
589
|
+
/** Hook system the wire phase subscribes to. */
|
|
590
|
+
hookSystem: PluginBootContext['hooks'];
|
|
591
|
+
/** Runtime bindings (env.DB, env.KV, env.R2, etc.). Used by the wire phase. */
|
|
592
|
+
env?: Record<string, unknown>;
|
|
593
|
+
/** Forwarded to the mount pass as the `source` label in dev warnings. */
|
|
594
|
+
source?: string;
|
|
595
|
+
/** Strict-mode: cycle / unknown-dep / capability errors throw instead of warn. */
|
|
596
|
+
strict?: boolean;
|
|
597
|
+
/** Forwarded to registerPluginRoutes. */
|
|
598
|
+
mountOptions?: Omit<RegisterPluginRoutesOptions, 'source' | 'strict'>;
|
|
599
|
+
}
|
|
600
|
+
interface RegistryEntry {
|
|
601
|
+
id: string;
|
|
602
|
+
displayName: string;
|
|
603
|
+
version: string;
|
|
604
|
+
capabilities: readonly string[];
|
|
605
|
+
}
|
|
606
|
+
interface PluginsRegistry {
|
|
607
|
+
/** Plugins indexed by id, in topo-sorted order. */
|
|
608
|
+
readonly byId: ReadonlyMap<string, RegistryEntry>;
|
|
609
|
+
/** Plugin ids in registration order (post topo-sort). */
|
|
610
|
+
readonly order: readonly string[];
|
|
611
|
+
/** Aggregated menu entries (also pushed to the menu singleton). */
|
|
612
|
+
readonly menu: readonly PluginMenuEntry[];
|
|
613
|
+
/** Aggregated cron declarations. */
|
|
614
|
+
readonly crons: readonly CronDeclaration[];
|
|
615
|
+
/** Aggregated cron schedule expressions (deduped) — useful for wrangler.toml codegen. */
|
|
616
|
+
readonly cronSchedules: readonly string[];
|
|
617
|
+
/** Mount-pass diagnostics. */
|
|
618
|
+
readonly mountResult: MountResult;
|
|
619
|
+
/**
|
|
620
|
+
* Lazy wire pass — call from the first-request middleware. Idempotent
|
|
621
|
+
* (subsequent calls return the cached result of the first run).
|
|
622
|
+
*/
|
|
623
|
+
boot(env?: Record<string, unknown>): Promise<WireResult>;
|
|
624
|
+
}
|
|
625
|
+
declare function registerPlugins(app: Hono<any, any, any>, plugins: ReadonlyArray<RegisterablePlugin | undefined | null>, host: RegisterPluginsHostContext): PluginsRegistry;
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Hook System Implementation
|
|
629
|
+
*
|
|
630
|
+
* Provides event-driven extensibility for plugins
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
declare class HookSystemImpl implements HookSystem {
|
|
634
|
+
private hooks;
|
|
635
|
+
private executing;
|
|
636
|
+
/**
|
|
637
|
+
* Register a hook handler
|
|
638
|
+
*/
|
|
639
|
+
register(hookName: string, handler: HookHandler, priority?: number): void;
|
|
640
|
+
/**
|
|
641
|
+
* Execute all handlers for a hook
|
|
642
|
+
*/
|
|
643
|
+
execute(hookName: string, data: any, context?: any): Promise<any>;
|
|
644
|
+
/**
|
|
645
|
+
* Remove a hook handler
|
|
646
|
+
*/
|
|
647
|
+
unregister(hookName: string, handler: HookHandler): void;
|
|
648
|
+
/**
|
|
649
|
+
* Get all registered hooks for a name
|
|
650
|
+
*/
|
|
651
|
+
getHooks(hookName: string): PluginHook[];
|
|
652
|
+
/**
|
|
653
|
+
* Get all registered hook names
|
|
654
|
+
*/
|
|
655
|
+
getHookNames(): string[];
|
|
656
|
+
/**
|
|
657
|
+
* Get hook statistics
|
|
658
|
+
*/
|
|
659
|
+
getStats(): {
|
|
660
|
+
hookName: string;
|
|
661
|
+
handlerCount: number;
|
|
662
|
+
}[];
|
|
663
|
+
/**
|
|
664
|
+
* Clear all hooks (useful for testing)
|
|
665
|
+
*/
|
|
666
|
+
clear(): void;
|
|
667
|
+
/**
|
|
668
|
+
* Create a scoped hook system for a plugin
|
|
669
|
+
*/
|
|
670
|
+
createScope(_pluginName: string): ScopedHookSystem;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Scoped hook system for individual plugins
|
|
674
|
+
*/
|
|
675
|
+
declare class ScopedHookSystem {
|
|
676
|
+
private parent;
|
|
677
|
+
private registeredHooks;
|
|
678
|
+
constructor(parent: HookSystemImpl);
|
|
679
|
+
/**
|
|
680
|
+
* Register a hook (scoped to this plugin)
|
|
681
|
+
*/
|
|
682
|
+
register(hookName: string, handler: HookHandler, priority?: number): void;
|
|
683
|
+
/**
|
|
684
|
+
* Execute a hook
|
|
685
|
+
*/
|
|
686
|
+
execute(hookName: string, data: any, context?: any): Promise<any>;
|
|
687
|
+
/**
|
|
688
|
+
* Unregister a specific hook
|
|
689
|
+
*/
|
|
690
|
+
unregister(hookName: string, handler: HookHandler): void;
|
|
691
|
+
/**
|
|
692
|
+
* Unregister all hooks for this plugin
|
|
693
|
+
*/
|
|
694
|
+
unregisterAll(): void;
|
|
695
|
+
/**
|
|
696
|
+
* Get hooks registered by this plugin
|
|
697
|
+
*/
|
|
698
|
+
getRegisteredHooks(): {
|
|
699
|
+
hookName: string;
|
|
700
|
+
handler: HookHandler;
|
|
701
|
+
}[];
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Hook utilities
|
|
705
|
+
*/
|
|
706
|
+
declare class HookUtils {
|
|
707
|
+
/**
|
|
708
|
+
* Create a hook name with namespace
|
|
709
|
+
*/
|
|
710
|
+
static createHookName(namespace: string, event: string): string;
|
|
711
|
+
/**
|
|
712
|
+
* Parse a hook name to extract namespace and event
|
|
713
|
+
*/
|
|
714
|
+
static parseHookName(hookName: string): {
|
|
715
|
+
namespace: string;
|
|
716
|
+
event: string;
|
|
717
|
+
};
|
|
718
|
+
/**
|
|
719
|
+
* Create a middleware that executes hooks
|
|
720
|
+
*/
|
|
721
|
+
static createHookMiddleware(hookSystem: HookSystem, beforeHook?: string, afterHook?: string): (c: any, next: any) => Promise<void>;
|
|
722
|
+
/**
|
|
723
|
+
* Create a debounced hook handler
|
|
724
|
+
*/
|
|
725
|
+
static debounce(handler: HookHandler, delay: number): HookHandler;
|
|
726
|
+
/**
|
|
727
|
+
* Create a throttled hook handler
|
|
728
|
+
*/
|
|
729
|
+
static throttle(handler: HookHandler, limit: number): HookHandler;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Plugin Validator
|
|
734
|
+
*
|
|
735
|
+
* Validates plugin definitions, dependencies, and compatibility
|
|
736
|
+
*/
|
|
737
|
+
|
|
738
|
+
declare class PluginValidator implements PluginValidator$1 {
|
|
739
|
+
private static readonly RESERVED_NAMES;
|
|
740
|
+
private static readonly RESERVED_PATHS;
|
|
741
|
+
/**
|
|
742
|
+
* Validate plugin definition
|
|
743
|
+
*/
|
|
744
|
+
validate(plugin: Plugin): PluginValidationResult;
|
|
745
|
+
/**
|
|
746
|
+
* Validate plugin dependencies
|
|
747
|
+
*/
|
|
748
|
+
validateDependencies(plugin: Plugin, registry: PluginRegistry): PluginValidationResult;
|
|
749
|
+
/**
|
|
750
|
+
* Validate plugin compatibility with SonicJS version
|
|
751
|
+
*/
|
|
752
|
+
validateCompatibility(plugin: Plugin, sonicVersion: string): PluginValidationResult;
|
|
753
|
+
/**
|
|
754
|
+
* Check if two version ranges are compatible
|
|
755
|
+
*/
|
|
756
|
+
private isCompatible;
|
|
757
|
+
/**
|
|
758
|
+
* Validate plugin security constraints
|
|
759
|
+
*/
|
|
760
|
+
validateSecurity(plugin: Plugin): PluginValidationResult;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Plugin Registry Implementation
|
|
765
|
+
*
|
|
766
|
+
* Manages plugin registration, activation, and lifecycle
|
|
767
|
+
*/
|
|
768
|
+
|
|
769
|
+
declare class PluginRegistryImpl implements PluginRegistry {
|
|
770
|
+
private plugins;
|
|
771
|
+
private configs;
|
|
772
|
+
private statuses;
|
|
773
|
+
private validator;
|
|
774
|
+
constructor(validator?: PluginValidator);
|
|
775
|
+
/**
|
|
776
|
+
* Get plugin by name
|
|
777
|
+
*/
|
|
778
|
+
get(name: string): Plugin | undefined;
|
|
779
|
+
/**
|
|
780
|
+
* Get all registered plugins
|
|
781
|
+
*/
|
|
782
|
+
getAll(): Plugin[];
|
|
783
|
+
/**
|
|
784
|
+
* Get active plugins
|
|
785
|
+
*/
|
|
786
|
+
getActive(): Plugin[];
|
|
787
|
+
/**
|
|
788
|
+
* Register a plugin
|
|
789
|
+
*/
|
|
790
|
+
register(plugin: Plugin): Promise<void>;
|
|
791
|
+
/**
|
|
792
|
+
* Unregister a plugin
|
|
793
|
+
*/
|
|
794
|
+
unregister(name: string): Promise<void>;
|
|
795
|
+
/**
|
|
796
|
+
* Check if plugin is registered
|
|
797
|
+
*/
|
|
798
|
+
has(name: string): boolean;
|
|
799
|
+
/**
|
|
800
|
+
* Activate a plugin
|
|
801
|
+
*/
|
|
802
|
+
activate(name: string): Promise<void>;
|
|
803
|
+
/**
|
|
804
|
+
* Deactivate a plugin
|
|
805
|
+
*/
|
|
806
|
+
deactivate(name: string): Promise<void>;
|
|
807
|
+
/**
|
|
808
|
+
* Get plugin configuration
|
|
809
|
+
*/
|
|
810
|
+
getConfig(name: string): PluginConfig | undefined;
|
|
811
|
+
/**
|
|
812
|
+
* Set plugin configuration
|
|
813
|
+
*/
|
|
814
|
+
setConfig(name: string, config: PluginConfig): void;
|
|
815
|
+
/**
|
|
816
|
+
* Get plugin status
|
|
817
|
+
*/
|
|
818
|
+
getStatus(name: string): PluginStatus | undefined;
|
|
819
|
+
/**
|
|
820
|
+
* Get all plugin statuses
|
|
821
|
+
*/
|
|
822
|
+
getAllStatuses(): Map<string, PluginStatus>;
|
|
823
|
+
/**
|
|
824
|
+
* Update plugin status
|
|
825
|
+
*/
|
|
826
|
+
private updateStatus;
|
|
827
|
+
/**
|
|
828
|
+
* Get plugins that depend on the specified plugin
|
|
829
|
+
*/
|
|
830
|
+
private getDependents;
|
|
831
|
+
/**
|
|
832
|
+
* Get dependency graph
|
|
833
|
+
*/
|
|
834
|
+
getDependencyGraph(): Map<string, string[]>;
|
|
835
|
+
/**
|
|
836
|
+
* Resolve plugin load order based on dependencies
|
|
837
|
+
*/
|
|
838
|
+
resolveLoadOrder(): string[];
|
|
839
|
+
/**
|
|
840
|
+
* Export plugin configuration
|
|
841
|
+
*/
|
|
842
|
+
exportConfig(): {
|
|
843
|
+
plugins: PluginConfig[];
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Import plugin configuration
|
|
847
|
+
*/
|
|
848
|
+
importConfig(config: {
|
|
849
|
+
plugins: PluginConfig[];
|
|
850
|
+
}): void;
|
|
851
|
+
/**
|
|
852
|
+
* Clear all plugins (useful for testing)
|
|
853
|
+
*/
|
|
854
|
+
clear(): void;
|
|
855
|
+
/**
|
|
856
|
+
* Get registry statistics
|
|
857
|
+
*/
|
|
858
|
+
getStats(): {
|
|
859
|
+
total: number;
|
|
860
|
+
active: number;
|
|
861
|
+
inactive: number;
|
|
862
|
+
withErrors: number;
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Plugin Manager
|
|
868
|
+
*
|
|
869
|
+
* Central orchestrator for the plugin system
|
|
870
|
+
*/
|
|
871
|
+
|
|
872
|
+
declare class PluginManager implements PluginManager$1 {
|
|
873
|
+
readonly registry: PluginRegistry;
|
|
874
|
+
readonly hooks: HookSystem;
|
|
875
|
+
private validator;
|
|
876
|
+
private context?;
|
|
877
|
+
private scopedHooks;
|
|
878
|
+
private pluginRoutes;
|
|
879
|
+
constructor();
|
|
880
|
+
/**
|
|
881
|
+
* Initialize plugin system
|
|
882
|
+
*/
|
|
883
|
+
initialize(context: PluginContext): Promise<void>;
|
|
884
|
+
/**
|
|
885
|
+
* Load plugins from configuration
|
|
886
|
+
*/
|
|
887
|
+
loadPlugins(configs: PluginConfig[]): Promise<void>;
|
|
888
|
+
/**
|
|
889
|
+
* Install a plugin
|
|
890
|
+
*/
|
|
891
|
+
install(plugin: Plugin, config?: PluginConfig): Promise<void>;
|
|
892
|
+
/**
|
|
893
|
+
* Uninstall a plugin
|
|
894
|
+
*/
|
|
895
|
+
uninstall(name: string): Promise<void>;
|
|
896
|
+
/**
|
|
897
|
+
* Get plugin status
|
|
898
|
+
*/
|
|
899
|
+
getStatus(name: string): PluginStatus;
|
|
900
|
+
/**
|
|
901
|
+
* Get all plugin statuses
|
|
902
|
+
*/
|
|
903
|
+
getAllStatuses(): PluginStatus[];
|
|
904
|
+
/**
|
|
905
|
+
* Register plugin extensions (routes, middleware, etc.)
|
|
906
|
+
*/
|
|
907
|
+
private registerPluginExtensions;
|
|
908
|
+
/**
|
|
909
|
+
* Unregister plugin extensions
|
|
910
|
+
*/
|
|
911
|
+
private unregisterPluginExtensions;
|
|
912
|
+
/**
|
|
913
|
+
* Update plugin status
|
|
914
|
+
*/
|
|
915
|
+
private updatePluginStatus;
|
|
916
|
+
/**
|
|
917
|
+
* Create a logger for a plugin
|
|
918
|
+
*/
|
|
919
|
+
private createLogger;
|
|
920
|
+
/**
|
|
921
|
+
* Get plugin routes for mounting in main app
|
|
922
|
+
*/
|
|
923
|
+
getPluginRoutes(): Map<string, Hono>;
|
|
924
|
+
/**
|
|
925
|
+
* Get plugin middleware for main app
|
|
926
|
+
*/
|
|
927
|
+
getPluginMiddleware(): Array<{
|
|
928
|
+
name: string;
|
|
929
|
+
handler: any;
|
|
930
|
+
priority: number;
|
|
931
|
+
global: boolean;
|
|
932
|
+
}>;
|
|
933
|
+
/**
|
|
934
|
+
* Execute shutdown procedures
|
|
935
|
+
*/
|
|
936
|
+
shutdown(): Promise<void>;
|
|
937
|
+
/**
|
|
938
|
+
* Get plugin system statistics
|
|
939
|
+
*/
|
|
940
|
+
getStats(): {
|
|
941
|
+
registry: ReturnType<PluginRegistryImpl['getStats']>;
|
|
942
|
+
hooks: Array<{
|
|
943
|
+
hookName: string;
|
|
944
|
+
handlerCount: number;
|
|
945
|
+
}>;
|
|
946
|
+
routes: number;
|
|
947
|
+
middleware: number;
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Minimal structural D1 surface needed for logging. Avoids importing the full
|
|
953
|
+
* `D1Database` type and keeps the service trivially fakeable in tests.
|
|
954
|
+
*/
|
|
955
|
+
interface EmailLogDb {
|
|
956
|
+
prepare(query: string): {
|
|
957
|
+
bind(...values: unknown[]): {
|
|
958
|
+
run(): Promise<unknown>;
|
|
959
|
+
};
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
interface EmailServiceOptions {
|
|
963
|
+
/** The transport. */
|
|
964
|
+
provider: EmailProvider;
|
|
965
|
+
/** Default from-address used when a message omits `from`. */
|
|
966
|
+
defaultFrom: string;
|
|
967
|
+
/** Default reply-to applied when a message omits `replyTo`. */
|
|
968
|
+
defaultReplyTo?: string;
|
|
969
|
+
/** When provided, every send writes an email_log document to the documents table. */
|
|
970
|
+
db?: EmailLogDb;
|
|
971
|
+
/** Injectable clock (tests). Defaults to `Date.now`. */
|
|
972
|
+
now?: () => number;
|
|
973
|
+
/** Injectable id factory (tests). Defaults to `crypto.randomUUID`. */
|
|
974
|
+
idFactory?: () => string;
|
|
975
|
+
}
|
|
976
|
+
declare class EmailService {
|
|
977
|
+
private readonly provider;
|
|
978
|
+
private readonly defaultFrom;
|
|
979
|
+
private readonly defaultReplyTo?;
|
|
980
|
+
private readonly db?;
|
|
981
|
+
private readonly now;
|
|
982
|
+
private readonly idFactory;
|
|
983
|
+
constructor(options: EmailServiceOptions);
|
|
984
|
+
/** Name of the active transport (e.g. `'resend'`). */
|
|
985
|
+
getProviderName(): string;
|
|
986
|
+
/** True if the active transport is ready to send. */
|
|
987
|
+
isConfigured(): boolean;
|
|
988
|
+
/**
|
|
989
|
+
* Ask the active provider to reconcile delivery state for a batch of
|
|
990
|
+
* recently-sent email_log document rows. Returns empty array when the provider
|
|
991
|
+
* does not implement `reconcile()` (Resend, SendGrid, Console).
|
|
992
|
+
*
|
|
993
|
+
* Called by the `email-reconciliation` cron plugin.
|
|
994
|
+
*/
|
|
995
|
+
reconcileDelivery(rows: EmailLogRow[]): Promise<Array<{
|
|
996
|
+
id: string;
|
|
997
|
+
delivery_state: string;
|
|
998
|
+
}>>;
|
|
999
|
+
/** Normalize, send, and log. Never throws for ordinary delivery failures. */
|
|
1000
|
+
send(message: EmailMessage): Promise<SendResult>;
|
|
1001
|
+
/**
|
|
1002
|
+
* Best-effort write of an email_log document into the `documents` table.
|
|
1003
|
+
* Documents use SECONDS for timestamps (not ms). Email log entries are
|
|
1004
|
+
* draft-only (is_current_draft=1, is_published=0, maxVersionsPerRoot=1).
|
|
1005
|
+
* Returns the new document's root_id, or undefined if logging is disabled / fails.
|
|
1006
|
+
*/
|
|
1007
|
+
private writeLog;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Plugin capabilities
|
|
1012
|
+
*
|
|
1013
|
+
* A plugin declares the capabilities it needs (`capabilities: ['email:send']`).
|
|
1014
|
+
* The host then hands it a context whose powerful accessors are *gated* by those
|
|
1015
|
+
* declarations: reaching `ctx.email` without having declared `email:send` throws
|
|
1016
|
+
* `SonicCapabilityError` immediately, instead of failing deep inside a send.
|
|
1017
|
+
*
|
|
1018
|
+
* This is the isolation boundary that Strapi (namespacing only) and Payload (full
|
|
1019
|
+
* config access) don't have: capabilities make a plugin's blast radius explicit
|
|
1020
|
+
* and enforceable, and double as documentation of what a plugin can touch.
|
|
1021
|
+
*
|
|
1022
|
+
* Phase 1 vocabulary. Payment/queue/scheduled-fetch capabilities are intentionally
|
|
1023
|
+
* deferred to Phase 2 (see the overhaul plan §8.4 / open question 2).
|
|
1024
|
+
*/
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Fixed capability names. `db:<table>` is parameterized (a plugin owns specific
|
|
1028
|
+
* tables), so it is matched by pattern rather than listed here.
|
|
1029
|
+
*/
|
|
1030
|
+
declare const FIXED_CAPABILITIES: readonly ["email:send", "cache:read", "cache:write", "media:read", "media:write", "http:fetch", "cron:register", "admin:menu", "hooks.auth:subscribe", "hooks.content:subscribe", "hooks.email:subscribe"];
|
|
1031
|
+
type FixedCapability = (typeof FIXED_CAPABILITIES)[number];
|
|
1032
|
+
/** A scoped database capability, e.g. `db:email_log`. */
|
|
1033
|
+
type DbCapability = `db:${string}`;
|
|
1034
|
+
/** Any declarable capability. */
|
|
1035
|
+
type Capability = FixedCapability | DbCapability;
|
|
1036
|
+
/** True if `name` is a recognized capability (fixed name or a valid `db:<table>`). */
|
|
1037
|
+
declare function isKnownCapability(name: string): name is Capability;
|
|
1038
|
+
/**
|
|
1039
|
+
* Thrown when a plugin uses a capability it did not declare.
|
|
1040
|
+
*/
|
|
1041
|
+
declare class SonicCapabilityError extends Error {
|
|
1042
|
+
readonly capability: string;
|
|
1043
|
+
readonly plugin?: string;
|
|
1044
|
+
/** The API surface the plugin tried to access (e.g. 'ctx.cap.email'). Optional. */
|
|
1045
|
+
readonly accessedApi?: string;
|
|
1046
|
+
constructor(capability: string, plugin?: string, accessedApi?: string);
|
|
1047
|
+
}
|
|
1048
|
+
/** True if `capability` is in the granted set. */
|
|
1049
|
+
declare function hasCapability(granted: readonly string[], capability: string): boolean;
|
|
1050
|
+
/**
|
|
1051
|
+
* Assert a capability is granted, throwing {@link SonicCapabilityError} otherwise.
|
|
1052
|
+
*/
|
|
1053
|
+
declare function assertCapability(granted: readonly string[], capability: string, plugin?: string): void;
|
|
1054
|
+
/**
|
|
1055
|
+
* Validate a plugin's declared capability list. Returns the unknown entries (empty
|
|
1056
|
+
* array = all valid). Callers decide whether to warn or hard-fail.
|
|
1057
|
+
*
|
|
1058
|
+
* Apply {@link normalizeCapabilities} first if the input may contain deprecated
|
|
1059
|
+
* spellings (e.g. from an older SDK or a sibling fork's manifest).
|
|
1060
|
+
*/
|
|
1061
|
+
declare function validateCapabilities(declared: readonly string[]): string[];
|
|
1062
|
+
declare const CAPABILITY_RENAMES: {
|
|
1063
|
+
readonly 'storage:read': "media:read";
|
|
1064
|
+
readonly 'storage:write': "media:write";
|
|
1065
|
+
readonly 'hooks.cron:register': "cron:register";
|
|
1066
|
+
readonly 'hooks.auth:register': "hooks.auth:subscribe";
|
|
1067
|
+
readonly 'hooks.content-read:register': "hooks.content:subscribe";
|
|
1068
|
+
readonly 'hooks.content-write:register': "hooks.content:subscribe";
|
|
1069
|
+
readonly 'hooks.email-events:register': "hooks.email:subscribe";
|
|
1070
|
+
};
|
|
1071
|
+
/**
|
|
1072
|
+
* Resolve a (possibly deprecated) capability string to its canonical form, or
|
|
1073
|
+
* `null` if it is unknown after rename resolution. Renames apply first, then the
|
|
1074
|
+
* result is checked against the known vocabulary.
|
|
1075
|
+
*/
|
|
1076
|
+
declare function normalizeCapability(input: string): Capability | null;
|
|
1077
|
+
/**
|
|
1078
|
+
* Normalize a list of capability strings. Returns the canonical capabilities plus
|
|
1079
|
+
* the inputs that remained unknown after rename resolution, so the caller can warn
|
|
1080
|
+
* (production) or reject (strict).
|
|
1081
|
+
*/
|
|
1082
|
+
declare function normalizeCapabilities(declared: readonly string[]): {
|
|
1083
|
+
capabilities: Capability[];
|
|
1084
|
+
unknown: string[];
|
|
1085
|
+
};
|
|
1086
|
+
/** Provider factories for capability-backed accessors. Each is called lazily. */
|
|
1087
|
+
interface CapabilityProviders {
|
|
1088
|
+
email?: () => EmailService;
|
|
1089
|
+
cache?: () => unknown;
|
|
1090
|
+
http?: () => typeof fetch;
|
|
1091
|
+
}
|
|
1092
|
+
declare const CAP_NOT_DECLARED: unique symbol;
|
|
1093
|
+
/**
|
|
1094
|
+
* The type of a capability accessor the plugin did NOT declare. Deliberately a
|
|
1095
|
+
* branded type (not `never`, which is assignable to everything): using it where a
|
|
1096
|
+
* service is expected is a compile error, so `ctx.cap.email` without `email:send`
|
|
1097
|
+
* fails to type-check instead of silently passing.
|
|
1098
|
+
*/
|
|
1099
|
+
type CapabilityNotDeclared<C extends string = string> = {
|
|
1100
|
+
readonly [CAP_NOT_DECLARED]: C;
|
|
1101
|
+
};
|
|
1102
|
+
type WhenGranted<Caps extends readonly Capability[], C extends string, T> = C extends Caps[number] ? T : CapabilityNotDeclared<C>;
|
|
1103
|
+
type WhenGrantedAny<Caps extends readonly Capability[], C extends string, T> = Extract<Caps[number], C> extends never ? CapabilityNotDeclared<C> : T;
|
|
1104
|
+
/**
|
|
1105
|
+
* The gated context handed to a plugin. Accessors throw at runtime unless the
|
|
1106
|
+
* backing capability was declared; with a const-narrowed `Caps` tuple they are
|
|
1107
|
+
* also *typed* to the service (or `never`) at compile time, so `ctx.cap.email`
|
|
1108
|
+
* is `EmailService` only when `'email:send'` was declared.
|
|
1109
|
+
*/
|
|
1110
|
+
interface CapabilityContext<Caps extends readonly Capability[] = readonly Capability[]> {
|
|
1111
|
+
/** The capabilities granted to this plugin. */
|
|
1112
|
+
readonly capabilities: readonly string[];
|
|
1113
|
+
/** True if the plugin declared `capability`. */
|
|
1114
|
+
has(capability: string): boolean;
|
|
1115
|
+
/** Throw {@link SonicCapabilityError} unless `capability` was declared. */
|
|
1116
|
+
require(capability: string): void;
|
|
1117
|
+
/** Email service. `EmailService` when `email:send` is declared, else `never`. */
|
|
1118
|
+
readonly email: WhenGranted<Caps, 'email:send', EmailService>;
|
|
1119
|
+
/** Cache service. Present when `cache:read` or `cache:write` is declared. */
|
|
1120
|
+
readonly cache: WhenGrantedAny<Caps, 'cache:read' | 'cache:write', unknown>;
|
|
1121
|
+
/** Outbound fetch. Present when `http:fetch` is declared. */
|
|
1122
|
+
readonly http: WhenGranted<Caps, 'http:fetch', typeof fetch>;
|
|
1123
|
+
}
|
|
1124
|
+
/** The un-narrowed gated context (every accessor typed as its service). */
|
|
1125
|
+
type PluginCapabilityContext = CapabilityContext</** all */ readonly Capability[]>;
|
|
1126
|
+
/**
|
|
1127
|
+
* Build a capability-gated context.
|
|
1128
|
+
*
|
|
1129
|
+
* Accessors are lazy getters: they check the grant (and that a provider was
|
|
1130
|
+
* supplied) at access time, so merely constructing the context is cheap and
|
|
1131
|
+
* holding a reference to a capability you never use costs nothing.
|
|
1132
|
+
*
|
|
1133
|
+
* @param granted Capabilities the plugin declared.
|
|
1134
|
+
* @param providers Backing service factories (only the granted ones get called).
|
|
1135
|
+
* @param plugin Plugin name, for clearer errors.
|
|
1136
|
+
*/
|
|
1137
|
+
declare function createCapabilityContext<const Caps extends readonly Capability[]>(granted: Caps, providers?: CapabilityProviders, plugin?: string): CapabilityContext<Caps>;
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* definePlugin() — the v3 plugin authoring entry point
|
|
1141
|
+
*
|
|
1142
|
+
* A plugin is authored as a single typed declaration and consumed, unchanged, by
|
|
1143
|
+
* every part of the runtime:
|
|
1144
|
+
*
|
|
1145
|
+
* export const emailPlugin = definePlugin({
|
|
1146
|
+
* id: 'email',
|
|
1147
|
+
* version: '3.0.0',
|
|
1148
|
+
* capabilities: ['email:send', 'hooks.auth:subscribe', 'cron:register'],
|
|
1149
|
+
* register(app) { app.route('/admin/plugins/email', emailRoutes) }, // SYNC
|
|
1150
|
+
* async onBoot(ctx) { // ASYNC
|
|
1151
|
+
* ctx.hooks.on('auth:registration:completed', (p) => { ... }) // typed
|
|
1152
|
+
* ctx.cap.email // gated
|
|
1153
|
+
* },
|
|
1154
|
+
* crons: [{ schedule: '*\/15 * * * *', hookFamily: 'email-reconciliation' }],
|
|
1155
|
+
* async onCronTick(event, ctx) { ... },
|
|
1156
|
+
* })
|
|
1157
|
+
*
|
|
1158
|
+
* The object it returns satisfies the structural contracts the runtime already
|
|
1159
|
+
* uses — `MountablePlugin` (mount.ts), `WirablePlugin` (wire.ts), `CronablePlugin`
|
|
1160
|
+
* (cron.ts) — plus the legacy `Plugin` metadata fields the admin/registry read.
|
|
1161
|
+
* No adapters: a defined plugin drops straight into `plugins.register` or the core
|
|
1162
|
+
* plugin list.
|
|
1163
|
+
*
|
|
1164
|
+
* The value definePlugin adds over a hand-written object is the enriched context:
|
|
1165
|
+
* inside `onBoot`/`onCronTick` the author gets a *typed* hook facade (`ctx.hooks`)
|
|
1166
|
+
* and the *capability-gated* service context (`ctx.cap`), instead of the raw
|
|
1167
|
+
* string-keyed hook system. See the two-phase boot contract: `register` is
|
|
1168
|
+
* synchronous (routes only); everything env-dependent lives in `onBoot`.
|
|
1169
|
+
*/
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Declarative typed hook subscriptions: a map of canonical event name → handler,
|
|
1173
|
+
* each narrowed to that event's payload. Flattened into the plugin's `hooks[]`
|
|
1174
|
+
* and subscribed during the wire phase. Use `onBoot`'s `ctx.hooks.on()` instead
|
|
1175
|
+
* for dynamic/conditional subscriptions.
|
|
1176
|
+
*/
|
|
1177
|
+
type DeclarativeHooks = {
|
|
1178
|
+
[E in HookEventName]?: TypedHookHandler<E>;
|
|
1179
|
+
};
|
|
1180
|
+
/**
|
|
1181
|
+
* Context handed to a defined plugin's `onBoot` / `onCronTick`.
|
|
1182
|
+
*
|
|
1183
|
+
* Enriches the raw boot/cron context with a typed hook facade and the gated
|
|
1184
|
+
* capability context, while keeping `raw` and `env` available as an escape hatch.
|
|
1185
|
+
*/
|
|
1186
|
+
interface DefinedPluginContext<Caps extends readonly Capability[] = readonly Capability[]> {
|
|
1187
|
+
/** Typed hook facade — `ctx.hooks.on('auth:registration:completed', …)`. */
|
|
1188
|
+
hooks: TypedHooks;
|
|
1189
|
+
/**
|
|
1190
|
+
* Capability-gated services. With a const-narrowed `Caps`, `ctx.cap.email` is
|
|
1191
|
+
* typed `EmailService` only when `'email:send'` was declared (else `never`),
|
|
1192
|
+
* and throws `SonicCapabilityError` at runtime if accessed undeclared.
|
|
1193
|
+
*/
|
|
1194
|
+
cap: CapabilityContext<Caps>;
|
|
1195
|
+
/** Runtime bindings, when available (absent during construction). */
|
|
1196
|
+
env?: Record<string, unknown>;
|
|
1197
|
+
/** The unwrapped context the runtime passed. */
|
|
1198
|
+
raw: PluginBootContext | CronContext;
|
|
1199
|
+
}
|
|
1200
|
+
/** Input to {@link definePlugin}. */
|
|
1201
|
+
interface DefinePluginInput<Caps extends readonly Capability[] = readonly []> {
|
|
1202
|
+
/** Unique, stable plugin id (kebab-case). Becomes the plugin `name`. */
|
|
1203
|
+
id: string;
|
|
1204
|
+
/** Human-readable display name. Defaults to `id`. */
|
|
1205
|
+
name?: string;
|
|
1206
|
+
/**
|
|
1207
|
+
* Semantic version of this plugin (e.g. `'1.2.3'`). Must be a valid semver
|
|
1208
|
+
* string — invalid values emit a console.warn at definition time.
|
|
1209
|
+
*/
|
|
1210
|
+
version: string;
|
|
1211
|
+
/**
|
|
1212
|
+
* A semver range expressing which SonicJS core versions this plugin supports
|
|
1213
|
+
* (e.g. `'^3.0.0'` or `'>=3.1.0 <4.0.0'`). Checked against the running
|
|
1214
|
+
* core version at registration; a mismatch logs a compatibility warning but
|
|
1215
|
+
* does not block activation (plugins remain resilient by default).
|
|
1216
|
+
*/
|
|
1217
|
+
sonicjsVersionRange?: string;
|
|
1218
|
+
description?: string;
|
|
1219
|
+
author?: {
|
|
1220
|
+
name: string;
|
|
1221
|
+
email?: string;
|
|
1222
|
+
url?: string;
|
|
1223
|
+
};
|
|
1224
|
+
/** Other plugin ids this one needs (load-order / activation). */
|
|
1225
|
+
dependencies?: string[];
|
|
1226
|
+
/**
|
|
1227
|
+
* Capabilities this plugin declares. The gated `ctx.cap` accessors throw for
|
|
1228
|
+
* anything not listed here. Pass as a literal (`['email:send'] as const` is not
|
|
1229
|
+
* needed — the `const` type param infers the tuple) to get `ctx.cap` narrowed.
|
|
1230
|
+
* Unknown/deprecated names are normalized then warned about at definition.
|
|
1231
|
+
*/
|
|
1232
|
+
capabilities?: Caps;
|
|
1233
|
+
/** Declarative routes (mounted before the /admin catch-all). */
|
|
1234
|
+
routes?: MountableRoute[];
|
|
1235
|
+
/**
|
|
1236
|
+
* Imperative route registration. MUST be synchronous (Hono's router locks after
|
|
1237
|
+
* the first request). Async work belongs in `onBoot`.
|
|
1238
|
+
*/
|
|
1239
|
+
register?: (app: Hono) => void;
|
|
1240
|
+
/**
|
|
1241
|
+
* Declarative typed hook subscriptions (`{ 'content:after:create': (p) => … }`).
|
|
1242
|
+
* Subscribed during the wire phase; each handler is narrowed to its event payload.
|
|
1243
|
+
*/
|
|
1244
|
+
hooks?: DeclarativeHooks;
|
|
1245
|
+
/**
|
|
1246
|
+
* Run once on first request, after every plugin has registered. The place for
|
|
1247
|
+
* dynamic hook subscriptions (`ctx.hooks.on(...)`) and env-dependent setup.
|
|
1248
|
+
*/
|
|
1249
|
+
onBoot?: (context: DefinedPluginContext<Caps>) => void | Promise<void>;
|
|
1250
|
+
/** Scheduled-work declarations (also list the expressions in wrangler.toml). */
|
|
1251
|
+
crons?: CronDeclaration[];
|
|
1252
|
+
/** Handler for a fired cron; branch on `event.hookFamily`. */
|
|
1253
|
+
onCronTick?: (event: CronTickEvent, context: DefinedPluginContext<Caps>) => void | Promise<void>;
|
|
1254
|
+
install?: (context: unknown) => void | Promise<void>;
|
|
1255
|
+
uninstall?: (context: unknown) => void | Promise<void>;
|
|
1256
|
+
activate?: (context: unknown) => void | Promise<void>;
|
|
1257
|
+
deactivate?: (context: unknown) => void | Promise<void>;
|
|
1258
|
+
/**
|
|
1259
|
+
* Declarative admin-sidebar entries collected by registerPlugins. The catalyst
|
|
1260
|
+
* sidebar renders them automatically; no per-plugin `addMenuItem(...)` call
|
|
1261
|
+
* required.
|
|
1262
|
+
*/
|
|
1263
|
+
menu?: PluginMenuEntry[];
|
|
1264
|
+
/**
|
|
1265
|
+
* Schema-driven settings. Declaring this auto-renders the admin form at
|
|
1266
|
+
* `/admin/settings/plugins/:id`, parses FormData back into typed values, and
|
|
1267
|
+
* persists them via the plugin-service. Authors no longer hand-roll settings
|
|
1268
|
+
* routes/templates.
|
|
1269
|
+
*/
|
|
1270
|
+
configSchema?: ConfigSchema;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* The runtime shape produced by {@link definePlugin}. Carries the legacy metadata
|
|
1274
|
+
* fields plus the v3 surfaces; structurally satisfies `MountablePlugin`,
|
|
1275
|
+
* `WirablePlugin`, and `CronablePlugin`.
|
|
1276
|
+
*/
|
|
1277
|
+
interface DefinedPlugin {
|
|
1278
|
+
id: string;
|
|
1279
|
+
name: string;
|
|
1280
|
+
version: string;
|
|
1281
|
+
/** The semver range for SonicJS core compatibility declared by the author. */
|
|
1282
|
+
sonicjsVersionRange?: string;
|
|
1283
|
+
description?: string;
|
|
1284
|
+
author?: {
|
|
1285
|
+
name: string;
|
|
1286
|
+
email?: string;
|
|
1287
|
+
url?: string;
|
|
1288
|
+
};
|
|
1289
|
+
dependencies?: string[];
|
|
1290
|
+
capabilities: Capability[];
|
|
1291
|
+
routes?: MountableRoute[];
|
|
1292
|
+
register?: (app: Hono) => void;
|
|
1293
|
+
/** Declarative hook subscriptions, flattened for the wire phase. */
|
|
1294
|
+
hooks?: WirableHook[];
|
|
1295
|
+
/** Wrapped onBoot accepting the runtime's raw boot context. */
|
|
1296
|
+
onBoot?: (context: PluginBootContext) => void | Promise<void>;
|
|
1297
|
+
crons?: CronDeclaration[];
|
|
1298
|
+
/** Wrapped onCronTick accepting the runtime's raw cron context. */
|
|
1299
|
+
onCronTick?: (event: CronTickEvent, context: CronContext) => void | Promise<void>;
|
|
1300
|
+
install?: (context: unknown) => void | Promise<void>;
|
|
1301
|
+
uninstall?: (context: unknown) => void | Promise<void>;
|
|
1302
|
+
activate?: (context: unknown) => void | Promise<void>;
|
|
1303
|
+
deactivate?: (context: unknown) => void | Promise<void>;
|
|
1304
|
+
/** Declarative admin sidebar entries. registerPlugins collects + sets the menu singleton. */
|
|
1305
|
+
menu?: PluginMenuEntry[];
|
|
1306
|
+
/** Schema-driven settings. Renders the admin settings form for this plugin. */
|
|
1307
|
+
configSchema?: ConfigSchema;
|
|
1308
|
+
/** Marker so tooling/tests can detect a v3-defined plugin. */
|
|
1309
|
+
readonly __sonicV3: true;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Define a v3 plugin. Validates declared capabilities (warns on unknown), then
|
|
1313
|
+
* returns a runtime-ready plugin whose `onBoot`/`onCronTick` receive the enriched,
|
|
1314
|
+
* typed, capability-gated context. The `const Caps` type parameter captures the
|
|
1315
|
+
* declared capability tuple so `ctx.cap` is narrowed at the author's call site.
|
|
1316
|
+
*/
|
|
1317
|
+
declare function definePlugin<const Caps extends readonly Capability[] = readonly []>(input: DefinePluginInput<Caps>): DefinedPlugin;
|
|
1318
|
+
/** True if `plugin` was produced by {@link definePlugin}. */
|
|
1319
|
+
declare function isDefinedPlugin(plugin: unknown): plugin is DefinedPlugin;
|
|
1320
|
+
|
|
1321
|
+
export { collectCrons as $, RegisterPluginsError as A, type BooleanField as B, CAPABILITY_RENAMES as C, type DbCapability as D, type ExecutionContextLike as E, FIXED_CAPABILITIES as F, type RegisterPluginsErrorReason as G, HookSystemImpl as H, type RegisterPluginsHostContext as I, type RegisterablePlugin as J, type RegistryEntry as K, ScopedHookSystem as L, type MountResult as M, type NumberField as N, type SelectField as O, type ParsedField as P, type SettingsFor as Q, type RegisterPluginRoutesOptions as R, type ScheduledControllerLike as S, SonicCapabilityError as T, type StringField as U, type WirablePlugin as V, type WirableHook as W, type WireResult as X, applySchemaDefaults as Y, assertCapability as Z, collectCronSchedules as _, type Capability as a, createCapabilityContext as a0, createPluginWirer as a1, createScheduledHandler as a2, definePlugin as a3, dispatchCronTick as a4, getHookSystem as a5, getTypedHooks as a6, hasCapability as a7, hasHookSystem as a8, isDefinedPlugin as a9, isKnownCapability as aa, mountPlugin as ab, normalizeCapabilities as ac, normalizeCapability as ad, parseConfigSchema as ae, parseFormDataToSettings as af, registerPluginRoutes as ag, registerPlugins as ah, renderSchemaFields as ai, resetHookSystem as aj, setHookSystem as ak, validateCapabilities as al, wireRegisteredPlugins as am, type PluginMenuEntry as an, type ResolvedPluginMenuEntry as ao, getPluginMenu as ap, resetPluginMenu as aq, resolvePluginMenuItems as ar, setPluginMenu as as, type CapabilityContext as b, type CapabilityProviders as c, type CollectedCron as d, type ConfigSchema as e, type ConfigSchemaField as f, type CreateScheduledHandlerOptions as g, type CronContext as h, type CronDeclaration as i, type CronDispatchResult as j, type CronTickEvent as k, type CronablePlugin as l, type DeclarativeHooks as m, type DefinePluginInput as n, type DefinedPlugin as o, type DefinedPluginContext as p, type FixedCapability as q, HookUtils as r, type MountedRoute as s, type PluginBootContext as t, type PluginCapabilityContext as u, PluginManager as v, PluginRegisterMustBeSyncError as w, PluginRegistryImpl as x, PluginValidator as y, type PluginsRegistry as z };
|