kempo 4.2.19 → 4.2.21
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/app-public/CATCH.page.html +15 -0
- package/app-public/kempo-global.global.html +0 -6
- package/bin/cli.js +4 -21
- package/dist/admin/accounts/groups/[name]/index.page.html +114 -4
- package/dist/admin/accounts/groups/index.page.html +89 -4
- package/dist/admin/accounts/permissions/index.page.html +86 -4
- package/dist/admin/accounts/users/[id]/index.page.html +117 -3
- package/dist/admin/accounts/users/index.page.html +118 -4
- package/dist/admin/components/AddGroupControl.js +1 -5
- package/dist/admin/components/AddGroupControl.js.map +1 -1
- package/dist/admin/components/AdminTableControl.js +2 -0
- package/dist/admin/components/AdminTableControl.js.map +1 -0
- package/dist/admin/components/ClearExpiredSessionsControl.js +1 -5
- package/dist/admin/components/ClearExpiredSessionsControl.js.map +1 -1
- package/dist/admin/components/GroupDeleteSelected.js +1 -5
- package/dist/admin/components/GroupDeleteSelected.js.map +1 -1
- package/dist/admin/components/GroupRemoveRecord.js +1 -5
- package/dist/admin/components/GroupRemoveRecord.js.map +1 -1
- package/dist/admin/components/TableRowDelete.js +1 -3
- package/dist/admin/components/TableRowDelete.js.map +1 -1
- package/dist/admin/components/TableRowLink.js +1 -7
- package/dist/admin/components/TableRowLink.js.map +1 -1
- package/dist/admin/components/tableControls/AddMember.js +1 -5
- package/dist/admin/components/tableControls/AddMember.js.map +1 -1
- package/dist/admin/components/tableControls/AddPermission.js +1 -5
- package/dist/admin/components/tableControls/AddPermission.js.map +1 -1
- package/dist/admin/components/tableControls/InstallExtension.js +1 -5
- package/dist/admin/components/tableControls/InstallExtension.js.map +1 -1
- package/dist/admin/components/tableControls/UninstallExtension.js +1 -5
- package/dist/admin/components/tableControls/UninstallExtension.js.map +1 -1
- package/dist/admin/content/fragments/edit/index.page.html +8 -1
- package/dist/admin/content/fragments/index.page.html +88 -3
- package/dist/admin/content/globals/edit/index.page.html +8 -1
- package/dist/admin/content/globals/index.page.html +103 -8
- package/dist/admin/content/pages/edit/index.page.html +8 -1
- package/dist/admin/content/pages/index.page.html +225 -28
- package/dist/admin/content/templates/edit/index.page.html +8 -1
- package/dist/admin/content/templates/index.page.html +117 -3
- package/dist/admin/default.template.html +20 -1
- package/dist/admin/extensions/index.page.html +59 -2
- package/dist/admin/icons/extension_add.svg +1 -0
- package/dist/admin/icons/extension_remove.svg +1 -0
- package/dist/admin/index.page.html +5 -1
- package/dist/admin/init.js.map +1 -1
- package/dist/admin/nav.fragment.html +84 -2
- package/dist/admin/settings/[owner]/[name]/index.page.html +110 -4
- package/dist/admin/settings/index.page.html +89 -4
- package/dist/kempo/api/admin-globals/DELETE.js +1 -1
- package/dist/kempo/api/admin-globals/DELETE.js.map +1 -1
- package/dist/kempo/api/admin-globals/GET.js +1 -1
- package/dist/kempo/api/admin-globals/GET.js.map +1 -1
- package/dist/kempo/api/admin-globals/POST.js +1 -1
- package/dist/kempo/api/admin-globals/POST.js.map +1 -1
- package/dist/kempo/api/admin-globals/[id]/GET.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/GET.js.map +1 -1
- package/dist/kempo/api/admin-globals/[id]/PUT.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/PUT.js.map +1 -1
- package/dist/kempo/api/admin-globals/[id]/disable/PUT.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/disable/PUT.js.map +1 -1
- package/dist/kempo/api/admin-globals/[id]/enable/PUT.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/enable/PUT.js.map +1 -1
- package/dist/kempo/api/extensions/DELETE.js +1 -1
- package/dist/kempo/api/extensions/DELETE.js.map +1 -1
- package/dist/kempo/api/extensions/POST.js +1 -1
- package/dist/kempo/api/extensions/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/disable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/disable/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/enable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/enable/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/update/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/update/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/update-check/GET.js +1 -1
- package/dist/kempo/api/extensions/[name]/update-check/GET.js.map +1 -1
- package/dist/kempo/api/fragments/DELETE.js +1 -1
- package/dist/kempo/api/fragments/DELETE.js.map +1 -1
- package/dist/kempo/api/fragments/GET.js +1 -1
- package/dist/kempo/api/fragments/GET.js.map +1 -1
- package/dist/kempo/api/fragments/POST.js +1 -1
- package/dist/kempo/api/fragments/POST.js.map +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/file/GET.js +1 -1
- package/dist/kempo/api/fragments/file/GET.js.map +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js.map +1 -1
- package/dist/kempo/api/fragments/file/PUT.js +1 -1
- package/dist/kempo/api/fragments/file/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js.map +1 -1
- package/dist/kempo/api/globals/DELETE.js +1 -1
- package/dist/kempo/api/globals/DELETE.js.map +1 -1
- package/dist/kempo/api/globals/GET.js +1 -1
- package/dist/kempo/api/globals/GET.js.map +1 -1
- package/dist/kempo/api/globals/POST.js +1 -1
- package/dist/kempo/api/globals/POST.js.map +1 -1
- package/dist/kempo/api/globals/disable/PUT.js +1 -1
- package/dist/kempo/api/globals/disable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/enable/PUT.js +1 -1
- package/dist/kempo/api/globals/enable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/entry/GET.js +1 -1
- package/dist/kempo/api/globals/entry/GET.js.map +1 -1
- package/dist/kempo/api/globals/entry/PUT.js +1 -1
- package/dist/kempo/api/globals/entry/PUT.js.map +1 -1
- package/dist/kempo/api/globals/lock/PUT.js +1 -1
- package/dist/kempo/api/globals/lock/PUT.js.map +1 -1
- package/dist/kempo/api/pages/DELETE.js +1 -1
- package/dist/kempo/api/pages/DELETE.js.map +1 -1
- package/dist/kempo/api/pages/GET.js +1 -1
- package/dist/kempo/api/pages/GET.js.map +1 -1
- package/dist/kempo/api/pages/POST.js +1 -1
- package/dist/kempo/api/pages/POST.js.map +1 -1
- package/dist/kempo/api/pages/directories/GET.js +1 -1
- package/dist/kempo/api/pages/directories/GET.js.map +1 -1
- package/dist/kempo/api/pages/disable/PUT.js +1 -1
- package/dist/kempo/api/pages/disable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/enable/PUT.js +1 -1
- package/dist/kempo/api/pages/enable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/file/GET.js +1 -1
- package/dist/kempo/api/pages/file/GET.js.map +1 -1
- package/dist/kempo/api/pages/file/PATCH.js +1 -1
- package/dist/kempo/api/pages/file/PATCH.js.map +1 -1
- package/dist/kempo/api/pages/file/PUT.js +1 -1
- package/dist/kempo/api/pages/file/PUT.js.map +1 -1
- package/dist/kempo/api/pages/lock/PUT.js +1 -1
- package/dist/kempo/api/pages/lock/PUT.js.map +1 -1
- package/dist/kempo/api/pages/rescan/POST.js +2 -0
- package/dist/kempo/api/pages/rescan/POST.js.map +1 -0
- package/dist/kempo/api/pages/search/GET.js +1 -1
- package/dist/kempo/api/pages/search/GET.js.map +1 -1
- package/dist/kempo/api/pages/templates/GET.js +1 -1
- package/dist/kempo/api/pages/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/DELETE.js +1 -1
- package/dist/kempo/api/templates/DELETE.js.map +1 -1
- package/dist/kempo/api/templates/GET.js +1 -1
- package/dist/kempo/api/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/POST.js +1 -1
- package/dist/kempo/api/templates/POST.js.map +1 -1
- package/dist/kempo/api/templates/disable/PUT.js +1 -1
- package/dist/kempo/api/templates/disable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/enable/PUT.js +1 -1
- package/dist/kempo/api/templates/enable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/file/GET.js +1 -1
- package/dist/kempo/api/templates/file/GET.js.map +1 -1
- package/dist/kempo/api/templates/file/PATCH.js +1 -1
- package/dist/kempo/api/templates/file/PATCH.js.map +1 -1
- package/dist/kempo/api/templates/file/PUT.js +1 -1
- package/dist/kempo/api/templates/file/PUT.js.map +1 -1
- package/dist/kempo/api/templates/lock/PUT.js +1 -1
- package/dist/kempo/api/templates/lock/PUT.js.map +1 -1
- package/dist/kempo/api/user/[userid]/name/GET.js +2 -0
- package/dist/kempo/api/user/[userid]/name/GET.js.map +1 -0
- package/dist/kempo/components/UserDisplayName.js +2 -0
- package/dist/kempo/components/UserDisplayName.js.map +1 -0
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/middleware/kempo.js +99 -25
- package/package.json +16 -9
- package/server/utils/admin-global-content/createAdminGlobalContent.js +4 -3
- package/server/utils/admin-global-content/deleteAdminGlobalContent.js +2 -2
- package/server/utils/admin-global-content/deleteAdminGlobalContentByOwner.js +2 -2
- package/server/utils/admin-global-content/disableAdminGlobalContent.js +2 -2
- package/server/utils/admin-global-content/enableAdminGlobalContent.js +2 -2
- package/server/utils/admin-global-content/getAdminGlobalContent.js +2 -2
- package/server/utils/admin-global-content/helpers.js +12 -1
- package/server/utils/admin-global-content/listAdminGlobalContent.js +2 -2
- package/server/utils/admin-global-content/updateAdminGlobalContent.js +2 -2
- package/server/utils/email/sendEmail.js +9 -3
- package/server/utils/extensions/uninstallExtension.js +54 -21
- package/server/utils/fragments/listFragments.js +10 -1
- package/server/utils/hooks/triggerHook.js +15 -13
- package/server/utils/pages/listPages.js +57 -7
- package/app-public/kempo-global.global-disabled.html +0 -0
- package/app-public/new-page.page.html +0 -17
- package/app-public/test.page.html +0 -21
package/middleware/kempo.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { dirname, join, extname } from 'path';
|
|
1
|
+
import { dirname, join, extname, isAbsolute, resolve } from 'path';
|
|
2
2
|
import { readFile, stat, readdir } from 'fs/promises';
|
|
3
3
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
4
4
|
import { renderExternalPage } from 'kempo-server/templating';
|
|
5
5
|
import getSession from '../server/utils/auth/getSession.js';
|
|
6
6
|
import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
|
|
7
7
|
import { getEnabledExtensions } from '../server/utils/extensions/scopeCache.js';
|
|
8
|
+
import triggerHook from '../server/utils/hooks/triggerHook.js';
|
|
9
|
+
import { ADMIN_GLOBALS_DIR } from '../server/utils/admin-global-content/helpers.js';
|
|
8
10
|
|
|
9
11
|
const MIME_TYPES = {
|
|
10
12
|
'.js': 'application/javascript',
|
|
@@ -25,8 +27,17 @@ const ROUTE_FILES = ['GET.js','POST.js','PUT.js','DELETE.js','PATCH.js','HEAD.js
|
|
|
25
27
|
|
|
26
28
|
const NODE_MODULES = join(process.cwd(), 'node_modules');
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
/*
|
|
31
|
+
Admin portal source. Defaults to dist/admin relative to this file, which works both in this repo
|
|
32
|
+
and as a consumer dependency. Set `middleware.adminRoot` in the server config (resolved from cwd)
|
|
33
|
+
to point at src/admin instead, so admin pages can be edited without rebuilding.
|
|
34
|
+
*/
|
|
35
|
+
const DEFAULT_ADMIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'admin');
|
|
36
|
+
|
|
37
|
+
const resolveAdminRoot = adminRoot => {
|
|
38
|
+
if(!adminRoot) return DEFAULT_ADMIN_ROOT;
|
|
39
|
+
return isAbsolute(adminRoot) ? adminRoot : resolve(process.cwd(), adminRoot);
|
|
40
|
+
};
|
|
30
41
|
|
|
31
42
|
const buildResolveDir = (base, url) => {
|
|
32
43
|
const parts = url.replace(/\/+$/, '').split('/').filter(Boolean);
|
|
@@ -51,6 +62,13 @@ const walkDynamic = async (base, segments) => {
|
|
|
51
62
|
return { filePath: join(base, head), params: {} };
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
if(rest.length === 0){
|
|
66
|
+
for(const entry of entries){
|
|
67
|
+
if(entry.isFile() && entry.name === `${head}.page.html`){
|
|
68
|
+
return { filePath: join(base, entry.name), params: {} };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
54
72
|
for(const entry of entries){
|
|
55
73
|
if(!entry.isDirectory() || !entry.name.startsWith('[') || !entry.name.endsWith(']')) continue;
|
|
56
74
|
const paramName = entry.name.slice(1, -1);
|
|
@@ -69,15 +87,55 @@ const executeRouteFile = async (filePath, request, response, params = {}) => {
|
|
|
69
87
|
return true;
|
|
70
88
|
};
|
|
71
89
|
|
|
72
|
-
|
|
90
|
+
/*
|
|
91
|
+
Every .page.html render must go through here. The `middleware:before_page` hook lets extensions
|
|
92
|
+
veto a render (throw `{code}` to send that status, or `{redirect}` to redirect), which is how
|
|
93
|
+
extensions guard their own private pages. Rendering a page anywhere without this helper silently
|
|
94
|
+
bypasses those guards.
|
|
95
|
+
*/
|
|
96
|
+
const renderGuardedPage = async (pageFilePath, request, response, rootDir, resolveDir, params = {}, extraGlobalDirs = []) => {
|
|
97
|
+
try {
|
|
98
|
+
await triggerHook('middleware:before_page', {
|
|
99
|
+
url: request.url.split('?')[0],
|
|
100
|
+
query: request.query || {},
|
|
101
|
+
params,
|
|
102
|
+
cookies: request.cookies || {},
|
|
103
|
+
}, { bail: true });
|
|
104
|
+
} catch(e) {
|
|
105
|
+
if(e?.redirect) return response.redirect(e.redirect);
|
|
106
|
+
response.writeHead(e?.code || 404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
107
|
+
response.end('');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const html = await renderExternalPage(pageFilePath, rootDir, resolveDir, {}, {}, 10, extraGlobalDirs);
|
|
112
|
+
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
113
|
+
response.end(html);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
Directories that contribute *.global.html to an admin render, on top of ADMIN_ROOT itself.
|
|
118
|
+
|
|
119
|
+
Each enabled extension's admin/ directory is read straight from its package at render time — the
|
|
120
|
+
same way its admin pages and public pages are resolved — so nothing is copied into kempo's
|
|
121
|
+
dist/admin on install. Enabling, disabling, upgrading or removing an extension takes effect
|
|
122
|
+
immediately, and a rebuild of kempo cannot discard it.
|
|
123
|
+
|
|
124
|
+
Admin globals authored through the admin UI live in the consumer's project instead, since those
|
|
125
|
+
are genuinely mutable and cannot live inside a package.
|
|
126
|
+
*/
|
|
127
|
+
export const adminGlobalDirs = async () => [
|
|
128
|
+
ADMIN_GLOBALS_DIR,
|
|
129
|
+
...(await getEnabledExtensions()).map(ext => join(NODE_MODULES, ext.name, 'admin')),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const serveDir = async (dirPath, method, request, response, resolveDir, rootDir, params = {}, extraGlobalDirs = []) => {
|
|
73
133
|
const candidates = [`${method}.js`, 'index.page.html', 'index.js', 'index.html', 'CATCH.js'];
|
|
74
134
|
for(const candidate of candidates){
|
|
75
135
|
const candidatePath = join(dirPath, candidate);
|
|
76
136
|
try { await stat(candidatePath); } catch { continue; }
|
|
77
137
|
if(candidate.endsWith('.page.html')){
|
|
78
|
-
|
|
79
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
80
|
-
response.end(html);
|
|
138
|
+
await renderGuardedPage(candidatePath, request, response, rootDir, resolveDir, params, extraGlobalDirs);
|
|
81
139
|
return true;
|
|
82
140
|
}
|
|
83
141
|
if(ROUTE_FILES.includes(candidate)){
|
|
@@ -93,6 +151,7 @@ const serveDir = async (dirPath, method, request, response, resolveDir, rootDir,
|
|
|
93
151
|
|
|
94
152
|
export default config => {
|
|
95
153
|
const PROJECT_PUBLIC = config.rootPath || join(process.cwd(), 'public');
|
|
154
|
+
const ADMIN_ROOT = resolveAdminRoot(config.adminRoot);
|
|
96
155
|
|
|
97
156
|
return async (request, response, next) => {
|
|
98
157
|
const { path } = request;
|
|
@@ -127,9 +186,10 @@ export default config => {
|
|
|
127
186
|
if(!url.startsWith('/admin/extension/')){
|
|
128
187
|
const segments = url.slice('/admin'.length).replace(/^\//, '').split('/').filter(Boolean);
|
|
129
188
|
const resolveDir = buildResolveDir(ADMIN_ROOT, url);
|
|
189
|
+
const globalDirs = await adminGlobalDirs();
|
|
130
190
|
|
|
131
191
|
if(segments.length === 0){
|
|
132
|
-
const served = await serveDir(ADMIN_ROOT, method, request, response, resolveDir, ADMIN_ROOT);
|
|
192
|
+
const served = await serveDir(ADMIN_ROOT, method, request, response, resolveDir, ADMIN_ROOT, {}, globalDirs);
|
|
133
193
|
if(served) return;
|
|
134
194
|
} else {
|
|
135
195
|
const walked = await walkDynamic(ADMIN_ROOT, segments);
|
|
@@ -139,14 +199,12 @@ export default config => {
|
|
|
139
199
|
try { fileStat = await stat(filePath); } catch { /* not found */ }
|
|
140
200
|
|
|
141
201
|
if(fileStat?.isDirectory()){
|
|
142
|
-
const served = await serveDir(filePath, method, request, response, resolveDir, ADMIN_ROOT, params);
|
|
202
|
+
const served = await serveDir(filePath, method, request, response, resolveDir, ADMIN_ROOT, params, globalDirs);
|
|
143
203
|
if(served) return;
|
|
144
204
|
} else if(fileStat?.isFile()){
|
|
145
205
|
const name = filePath.split(/[/\\]/).pop();
|
|
146
206
|
if(name.endsWith('.page.html')){
|
|
147
|
-
|
|
148
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
149
|
-
response.end(html);
|
|
207
|
+
await renderGuardedPage(filePath, request, response, ADMIN_ROOT, resolveDir, params, globalDirs);
|
|
150
208
|
return;
|
|
151
209
|
}
|
|
152
210
|
if(ROUTE_FILES.includes(name)){
|
|
@@ -174,6 +232,7 @@ export default config => {
|
|
|
174
232
|
const adminDir = join(NODE_MODULES, extName, 'admin');
|
|
175
233
|
const resolveDir = buildResolveDir(ADMIN_ROOT, url);
|
|
176
234
|
const method = request.method?.toUpperCase() || 'GET';
|
|
235
|
+
const globalDirs = await adminGlobalDirs();
|
|
177
236
|
|
|
178
237
|
const pageCandidates = subPath.endsWith('/')
|
|
179
238
|
? [join(adminDir, subPath, 'index.page.html')]
|
|
@@ -185,11 +244,9 @@ export default config => {
|
|
|
185
244
|
for(const pagePath of pageCandidates){
|
|
186
245
|
try {
|
|
187
246
|
await stat(pagePath);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return;
|
|
192
|
-
} catch { /* try next */ }
|
|
247
|
+
} catch { continue; }
|
|
248
|
+
await renderGuardedPage(pagePath, request, response, ADMIN_ROOT, resolveDir, {}, globalDirs);
|
|
249
|
+
return;
|
|
193
250
|
}
|
|
194
251
|
|
|
195
252
|
const staticPath = join(adminDir, subPath === '/' ? 'index.html' : subPath);
|
|
@@ -210,14 +267,12 @@ export default config => {
|
|
|
210
267
|
let walkedStat;
|
|
211
268
|
try { walkedStat = await stat(walkedPath); } catch { /* not found */ }
|
|
212
269
|
if(walkedStat?.isDirectory()){
|
|
213
|
-
const served = await serveDir(walkedPath, method, request, response, resolveDir, ADMIN_ROOT, params);
|
|
270
|
+
const served = await serveDir(walkedPath, method, request, response, resolveDir, ADMIN_ROOT, params, globalDirs);
|
|
214
271
|
if(served) return;
|
|
215
272
|
} else if(walkedStat?.isFile()){
|
|
216
273
|
const name = walkedPath.split(/[/\\]/).pop();
|
|
217
274
|
if(name.endsWith('.page.html')){
|
|
218
|
-
|
|
219
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
220
|
-
response.end(html);
|
|
275
|
+
await renderGuardedPage(walkedPath, request, response, ADMIN_ROOT, resolveDir, params, globalDirs);
|
|
221
276
|
return;
|
|
222
277
|
}
|
|
223
278
|
if(ROUTE_FILES.includes(name)){
|
|
@@ -276,9 +331,7 @@ export default config => {
|
|
|
276
331
|
} else if(fileStat?.isFile()){
|
|
277
332
|
const name = filePath.split(/[/\\]/).pop();
|
|
278
333
|
if(name.endsWith('.page.html')){
|
|
279
|
-
|
|
280
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
281
|
-
response.end(html);
|
|
334
|
+
await renderGuardedPage(filePath, request, response, PROJECT_PUBLIC, resolveDir, params);
|
|
282
335
|
return;
|
|
283
336
|
}
|
|
284
337
|
if(ROUTE_FILES.includes(name)){
|
|
@@ -292,7 +345,28 @@ export default config => {
|
|
|
292
345
|
}
|
|
293
346
|
}
|
|
294
347
|
|
|
295
|
-
|
|
348
|
+
// File not found in extension's public dir — fall through to consumer page routing
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/*
|
|
353
|
+
Consumer public page routing: intercept .page.html before kempo-server SSR handles them
|
|
354
|
+
*/
|
|
355
|
+
{
|
|
356
|
+
const resolveDir = buildResolveDir(PROJECT_PUBLIC, url);
|
|
357
|
+
const urlPath = url.replace(/\/+$/, '');
|
|
358
|
+
|
|
359
|
+
// Mirror kempo-server's SSR probe: url.page.html and url/index.page.html
|
|
360
|
+
const candidates = [
|
|
361
|
+
join(PROJECT_PUBLIC, urlPath + '.page.html'),
|
|
362
|
+
join(PROJECT_PUBLIC, urlPath, 'index.page.html'),
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
for(const pageFilePath of candidates){
|
|
366
|
+
try { await stat(pageFilePath); } catch { continue; }
|
|
367
|
+
await renderGuardedPage(pageFilePath, request, response, PROJECT_PUBLIC, resolveDir);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
296
370
|
}
|
|
297
371
|
|
|
298
372
|
next();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fullstack CMS framework for creating websites with user authentication",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "node dev.
|
|
7
|
+
"dev": "node scripts/dev.js",
|
|
8
8
|
"build": "node scripts/build.js",
|
|
9
9
|
"db:generate": "drizzle-kit generate",
|
|
10
10
|
"db:migrate": "drizzle-kit migrate",
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"db:studio": "drizzle-kit studio",
|
|
13
13
|
"makeAdmin": "node ./scripts/make-admin.js",
|
|
14
14
|
"removeAdmin": "node ./scripts/removeAdmin.js",
|
|
15
|
-
"test:email": "node ./scripts/test-email.js"
|
|
15
|
+
"test:email": "node ./scripts/test-email.js",
|
|
16
|
+
"test": "kempo-test -n",
|
|
17
|
+
"test:node": "kempo-test -n",
|
|
18
|
+
"link:local": "npm install ../kempo-server ../kempo-ui --no-save"
|
|
16
19
|
},
|
|
17
20
|
"author": "Dustin Poissant",
|
|
18
21
|
"license": "MIT",
|
|
@@ -44,16 +47,20 @@
|
|
|
44
47
|
"dotenv": "^17.2.3",
|
|
45
48
|
"drizzle-kit": "^0.31.7",
|
|
46
49
|
"drizzle-orm": "^0.44.7",
|
|
47
|
-
"html-minifier-terser": "^7.2.0",
|
|
48
50
|
"inquirer": "^12.6.1",
|
|
49
|
-
"kempo": "^4.2.16",
|
|
50
|
-
"kempo-server": "^3.1.0",
|
|
51
|
-
"kempo-testing-framework": "^1.4.6",
|
|
52
|
-
"kempo-ui": "^0.4.6",
|
|
53
51
|
"lexical": "^0.42.0",
|
|
54
52
|
"monaco-editor": "^0.55.1",
|
|
55
53
|
"postgres": "^3.4.7",
|
|
56
|
-
"resend": "^6.5.2"
|
|
54
|
+
"resend": "^6.5.2"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"kempo-css": ">=2.2.5",
|
|
58
|
+
"kempo-server": ">=3.1.4",
|
|
59
|
+
"kempo-ui": ">=0.4.39"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"html-minifier-terser": "^7.2.0",
|
|
63
|
+
"kempo-testing-framework": "^1.4.6",
|
|
57
64
|
"terser": "^5.46.2"
|
|
58
65
|
},
|
|
59
66
|
"repository": {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'fs/promises';
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
5
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
6
6
|
|
|
7
|
-
export default async ({ adminDir, name, location, priority, owner, markup }) => {
|
|
7
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, name, location, priority, owner, markup }) => {
|
|
8
8
|
if(!adminDir){
|
|
9
9
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
10
10
|
}
|
|
@@ -37,6 +37,7 @@ export default async ({ adminDir, name, location, priority, owner, markup }) =>
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
entries.push(entry);
|
|
40
|
+
await mkdir(adminDir, { recursive: true });
|
|
40
41
|
await writeFile(filePath, serializeEntries(entries) + '\n', 'utf-8');
|
|
41
42
|
|
|
42
43
|
return [null, entry];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, ids }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, ids }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, owner }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, owner }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, id }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, id }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, id }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, id }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, id }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, id }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -4,6 +4,17 @@ import { existsSync } from 'fs';
|
|
|
4
4
|
|
|
5
5
|
const ADMIN_GLOBAL_FILE = 'kempo-global.global.html';
|
|
6
6
|
|
|
7
|
+
/*
|
|
8
|
+
Admin global content created through the admin UI is per-installation state, so it lives in the
|
|
9
|
+
consumer's project rather than in kempo's dist/admin — `npm run build` deletes dist/ wholesale and
|
|
10
|
+
reinstalling or upgrading kempo replaces it, either of which would silently discard it.
|
|
11
|
+
|
|
12
|
+
Extension-owned admin globals are NOT stored here. Each extension ships its own *.global.html
|
|
13
|
+
inside its package and the middleware scans it at render time, the same way extension pages are
|
|
14
|
+
resolved, so there is nothing to write on install or clean up on uninstall.
|
|
15
|
+
*/
|
|
16
|
+
const ADMIN_GLOBALS_DIR = join(process.cwd(), '.kempo', 'admin-globals');
|
|
17
|
+
|
|
7
18
|
const parseEntries = raw => {
|
|
8
19
|
const entries = [];
|
|
9
20
|
const regex = /<content\s([^>]*)>([\s\S]*?)<\/content>/g;
|
|
@@ -44,4 +55,4 @@ const serializeEntries = entries => entries.map(e => {
|
|
|
44
55
|
return `<content ${attrs}>\n${e.markup}\n</content>`;
|
|
45
56
|
}).join('\n');
|
|
46
57
|
|
|
47
|
-
export { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries };
|
|
58
|
+
export { ADMIN_GLOBAL_FILE, ADMIN_GLOBALS_DIR, parseEntries, serializeEntries };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR } = {}) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries } from './helpers.js';
|
|
4
|
+
import { ADMIN_GLOBAL_FILE, parseEntries, serializeEntries, ADMIN_GLOBALS_DIR } from './helpers.js';
|
|
5
5
|
|
|
6
|
-
export default async ({ adminDir, id, name, location, priority, markup }) => {
|
|
6
|
+
export default async ({ adminDir = ADMIN_GLOBALS_DIR, id, name, location, priority, markup }) => {
|
|
7
7
|
if(!adminDir){
|
|
8
8
|
return [{ code: 400, msg: 'Admin directory is required' }, null];
|
|
9
9
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Resend } from 'resend';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/*
|
|
4
|
+
Constructed on first send, not at import. The Resend constructor throws when the key is missing,
|
|
5
|
+
and this module is reachable from the server SDK, so building it at module scope made an unset
|
|
6
|
+
RESEND_API_KEY fail every route that transitively imports it rather than just disabling email.
|
|
7
|
+
*/
|
|
8
|
+
let resend = null;
|
|
9
|
+
const client = () => (resend ??= new Resend(process.env.RESEND_API_KEY));
|
|
4
10
|
|
|
5
11
|
export default async ({ to, subject, html }) => {
|
|
6
12
|
if(!process.env.RESEND_API_KEY){
|
|
7
13
|
return [{ code: 500, msg: 'RESEND_API_KEY not configured' }, null];
|
|
8
14
|
}
|
|
9
|
-
|
|
15
|
+
|
|
10
16
|
if(!to){
|
|
11
17
|
return [{ code: 400, msg: 'Recipient email is required' }, null];
|
|
12
18
|
}
|
|
@@ -22,7 +28,7 @@ export default async ({ to, subject, html }) => {
|
|
|
22
28
|
const from = process.env.SMTP_FROM || 'onboarding@resend.dev';
|
|
23
29
|
|
|
24
30
|
try {
|
|
25
|
-
const { data, error } = await
|
|
31
|
+
const { data, error } = await client().emails.send({
|
|
26
32
|
from,
|
|
27
33
|
to,
|
|
28
34
|
subject,
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
|
-
import { extension, hook, permission, setting, group } from '../../db/schema.js';
|
|
3
|
-
import { eq, inArray } from 'drizzle-orm';
|
|
2
|
+
import { extension, hook, permission, setting, group, groupPermission, userGroup } from '../../db/schema.js';
|
|
3
|
+
import { eq, inArray, like } from 'drizzle-orm';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
6
|
import { sql } from 'drizzle-orm';
|
|
7
7
|
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
8
8
|
import { invalidateScopeCache } from './scopeCache.js';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/*
|
|
11
|
+
Uninstall an extension.
|
|
12
|
+
|
|
13
|
+
`purgeData` false (the default) deregisters the extension but leaves everything it created in
|
|
14
|
+
place — its tables and their rows, group memberships, permissions, groups and settings — so
|
|
15
|
+
reinstalling restores the site exactly as it was. This is the right mode for reinstalling to pick
|
|
16
|
+
up a fix.
|
|
17
|
+
|
|
18
|
+
`purgeData` true additionally destroys all of that. For an extension like kempo-blog that means
|
|
19
|
+
every post and comment, so it must always be a deliberate choice rather than a default.
|
|
20
|
+
|
|
21
|
+
Hooks and the extension row are removed either way. A registered hook whose module is no longer
|
|
22
|
+
installed cannot be tolerated: `middleware:before_page` is triggered with `bail: true`, so a
|
|
23
|
+
handler that fails to import now aborts the render and every page 404s.
|
|
24
|
+
*/
|
|
25
|
+
export default async ({ name, purgeData = false }) => {
|
|
11
26
|
if(!name){
|
|
12
27
|
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
13
28
|
}
|
|
@@ -32,9 +47,9 @@ export default async ({ name }) => {
|
|
|
32
47
|
} catch {}
|
|
33
48
|
|
|
34
49
|
/*
|
|
35
|
-
Drop declarative schema tables
|
|
50
|
+
Drop declarative schema tables. Only when purging — these hold the extension's actual content.
|
|
36
51
|
*/
|
|
37
|
-
if(kempoConfig.schema){
|
|
52
|
+
if(purgeData && kempoConfig.schema){
|
|
38
53
|
try {
|
|
39
54
|
const extRoot = join(process.cwd(), 'node_modules', name);
|
|
40
55
|
const schemaModule = await import(pathToFileURL(join(extRoot, kempoConfig.schema)).href);
|
|
@@ -48,25 +63,43 @@ export default async ({ name }) => {
|
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
/*
|
|
51
|
-
Remove declarative groups
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if(groupNames.length){
|
|
55
|
-
await db.delete(group).where(inArray(group.name, groupNames)).catch(() => {});
|
|
56
|
-
}
|
|
66
|
+
Remove declarative groups, permissions and settings. Only when purging: these are what a
|
|
67
|
+
reinstall would otherwise restore, and group memberships in particular represent real user
|
|
68
|
+
state — every registered user of kempo-blog belongs to kempo-blog:member.
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
userGroup and groupPermission both reference group.name, and groupPermission also references
|
|
71
|
+
permission.name, none of them cascading. The membership and grant rows therefore have to go
|
|
72
|
+
first or these deletes fail the foreign key — which they used to do silently, leaving every
|
|
73
|
+
uninstalled extension's groups and permissions behind forever.
|
|
60
74
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
75
|
+
if(purgeData){
|
|
76
|
+
const groupNames = (kempoConfig.groups || []).map(g => g.name);
|
|
77
|
+
const permNames = (kempoConfig.permissions || []).map(p => p.name);
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
try {
|
|
80
|
+
if(groupNames.length){
|
|
81
|
+
await db.delete(userGroup).where(inArray(userGroup.groupName, groupNames));
|
|
82
|
+
await db.delete(groupPermission).where(inArray(groupPermission.groupName, groupNames));
|
|
83
|
+
}
|
|
84
|
+
if(permNames.length){
|
|
85
|
+
await db.delete(groupPermission).where(inArray(groupPermission.permissionName, permNames));
|
|
86
|
+
}
|
|
87
|
+
if(groupNames.length){
|
|
88
|
+
await db.delete(group).where(inArray(group.name, groupNames));
|
|
89
|
+
}
|
|
90
|
+
if(permNames.length){
|
|
91
|
+
await db.delete(permission).where(inArray(permission.name, permNames));
|
|
92
|
+
}
|
|
93
|
+
} catch(error) {
|
|
94
|
+
return [{ code: 500, msg: `Failed to remove extension groups and permissions: ${error.message}` }, null];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
Ownership is encoded in the setting name as "extname:settingname" — the setting table has no
|
|
99
|
+
owner column, so these have to be matched by prefix.
|
|
100
|
+
*/
|
|
101
|
+
await db.delete(setting).where(like(setting.name, `${name}:%`));
|
|
102
|
+
}
|
|
70
103
|
|
|
71
104
|
try {
|
|
72
105
|
await db.delete(hook).where(eq(hook.owner, name));
|
|
@@ -3,6 +3,14 @@ import { relative } from 'path';
|
|
|
3
3
|
import scanDir from '../fs/scanDir.js';
|
|
4
4
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
5
|
|
|
6
|
+
const parseLocations = content => {
|
|
7
|
+
const locations = [];
|
|
8
|
+
const regex = /<location\s[^/]*?name="([^"]+)"[^/]*?\/>/g;
|
|
9
|
+
let match;
|
|
10
|
+
while((match = regex.exec(content)) !== null) locations.push(match[1]);
|
|
11
|
+
return locations;
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
export default async ({ rootDir }) => {
|
|
7
15
|
if(!rootDir){
|
|
8
16
|
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
@@ -24,7 +32,8 @@ export default async ({ rootDir }) => {
|
|
|
24
32
|
disabled: isDisabled,
|
|
25
33
|
author: meta.author || '',
|
|
26
34
|
createdAt: meta.created || '',
|
|
27
|
-
updatedAt: meta.updated || ''
|
|
35
|
+
updatedAt: meta.updated || '',
|
|
36
|
+
locations: parseLocations(content)
|
|
28
37
|
};
|
|
29
38
|
});
|
|
30
39
|
|
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
2
|
import { hook } from '../../db/schema.js';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
6
|
|
|
7
7
|
const handlerCache = new Map();
|
|
8
8
|
|
|
9
9
|
export const clearHandlerCache = () => handlerCache.clear();
|
|
10
10
|
|
|
11
|
-
const requireFromProject = createRequire(join(process.cwd(), 'package.json'));
|
|
12
|
-
|
|
13
11
|
const resolveHandler = (owner, callback) => {
|
|
14
12
|
if(callback.startsWith('./') || callback.startsWith('../')){
|
|
15
|
-
|
|
16
|
-
const pkgPath = requireFromProject.resolve(`${owner}/package.json`);
|
|
17
|
-
return join(dirname(pkgPath), callback);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
13
|
+
return join(process.cwd(), 'node_modules', owner, callback);
|
|
21
14
|
}
|
|
22
15
|
return callback;
|
|
23
16
|
};
|
|
24
17
|
|
|
25
|
-
export default async (event, data = {}) => {
|
|
18
|
+
export default async (event, data = {}, { bail = false } = {}) => {
|
|
26
19
|
if(!event){
|
|
27
20
|
return [{ code: 400, msg: 'Event is required' }, null];
|
|
28
21
|
}
|
|
@@ -38,10 +31,17 @@ export default async (event, data = {}) => {
|
|
|
38
31
|
let handler = handlerCache.get(handlerPath);
|
|
39
32
|
if(!handler){
|
|
40
33
|
try {
|
|
41
|
-
const mod = await import(handlerPath);
|
|
34
|
+
const mod = await import(pathToFileURL(handlerPath).href);
|
|
42
35
|
handler = mod.default || mod;
|
|
43
36
|
handlerCache.set(handlerPath, handler);
|
|
44
|
-
} catch {
|
|
37
|
+
} catch(err) {
|
|
38
|
+
/*
|
|
39
|
+
Never fail silently: a hook that cannot load is indistinguishable from one that
|
|
40
|
+
allowed the request. For bailing events (guards) that would fail open, so rethrow.
|
|
41
|
+
*/
|
|
42
|
+
console.error(`[kempo] Hook handler failed to load for "${event}" (${h.owner}): ${err.message}`);
|
|
43
|
+
if(bail) throw err;
|
|
44
|
+
results.push({ hookId: h.id, owner: h.owner, error: err.message });
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -50,12 +50,14 @@ export default async (event, data = {}) => {
|
|
|
50
50
|
const result = await handler(data);
|
|
51
51
|
results.push({ hookId: h.id, owner: h.owner, result });
|
|
52
52
|
} catch(err) {
|
|
53
|
+
if(bail) throw err;
|
|
53
54
|
results.push({ hookId: h.id, owner: h.owner, error: err.message });
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
return [null, { results }];
|
|
58
59
|
} catch(error) {
|
|
60
|
+
if(bail) throw error;
|
|
59
61
|
return [{ code: 500, msg: 'Failed to trigger hooks' }, null];
|
|
60
62
|
}
|
|
61
63
|
};
|