kempo 4.2.20 → 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.
Files changed (83) hide show
  1. package/bin/cli.js +4 -21
  2. package/dist/admin/accounts/groups/[name]/index.page.html +24 -24
  3. package/dist/admin/accounts/groups/index.page.html +30 -30
  4. package/dist/admin/accounts/permissions/index.page.html +30 -30
  5. package/dist/admin/accounts/users/[id]/index.page.html +17 -17
  6. package/dist/admin/accounts/users/index.page.html +18 -18
  7. package/dist/admin/components/AddGroupControl.js +1 -5
  8. package/dist/admin/components/AddGroupControl.js.map +1 -1
  9. package/dist/admin/components/AdminTableControl.js +2 -0
  10. package/dist/admin/components/AdminTableControl.js.map +1 -0
  11. package/dist/admin/components/ClearExpiredSessionsControl.js +1 -5
  12. package/dist/admin/components/ClearExpiredSessionsControl.js.map +1 -1
  13. package/dist/admin/components/GroupDeleteSelected.js +1 -5
  14. package/dist/admin/components/GroupDeleteSelected.js.map +1 -1
  15. package/dist/admin/components/GroupRemoveRecord.js +1 -5
  16. package/dist/admin/components/GroupRemoveRecord.js.map +1 -1
  17. package/dist/admin/components/TableRowDelete.js +1 -3
  18. package/dist/admin/components/TableRowDelete.js.map +1 -1
  19. package/dist/admin/components/TableRowLink.js +1 -7
  20. package/dist/admin/components/TableRowLink.js.map +1 -1
  21. package/dist/admin/components/tableControls/AddMember.js +1 -5
  22. package/dist/admin/components/tableControls/AddMember.js.map +1 -1
  23. package/dist/admin/components/tableControls/AddPermission.js +1 -5
  24. package/dist/admin/components/tableControls/AddPermission.js.map +1 -1
  25. package/dist/admin/components/tableControls/InstallExtension.js +1 -5
  26. package/dist/admin/components/tableControls/InstallExtension.js.map +1 -1
  27. package/dist/admin/components/tableControls/UninstallExtension.js +1 -5
  28. package/dist/admin/components/tableControls/UninstallExtension.js.map +1 -1
  29. package/dist/admin/content/fragments/index.page.html +28 -28
  30. package/dist/admin/content/globals/index.page.html +28 -28
  31. package/dist/admin/content/pages/index.page.html +28 -28
  32. package/dist/admin/content/templates/index.page.html +28 -28
  33. package/dist/admin/extensions/index.page.html +18 -18
  34. package/dist/admin/init.js.map +1 -1
  35. package/dist/admin/settings/index.page.html +30 -30
  36. package/dist/kempo/api/admin-globals/DELETE.js +1 -1
  37. package/dist/kempo/api/admin-globals/DELETE.js.map +1 -1
  38. package/dist/kempo/api/admin-globals/GET.js +1 -1
  39. package/dist/kempo/api/admin-globals/GET.js.map +1 -1
  40. package/dist/kempo/api/admin-globals/POST.js +1 -1
  41. package/dist/kempo/api/admin-globals/POST.js.map +1 -1
  42. package/dist/kempo/api/admin-globals/[id]/GET.js +1 -1
  43. package/dist/kempo/api/admin-globals/[id]/GET.js.map +1 -1
  44. package/dist/kempo/api/admin-globals/[id]/PUT.js +1 -1
  45. package/dist/kempo/api/admin-globals/[id]/PUT.js.map +1 -1
  46. package/dist/kempo/api/admin-globals/[id]/disable/PUT.js +1 -1
  47. package/dist/kempo/api/admin-globals/[id]/disable/PUT.js.map +1 -1
  48. package/dist/kempo/api/admin-globals/[id]/enable/PUT.js +1 -1
  49. package/dist/kempo/api/admin-globals/[id]/enable/PUT.js.map +1 -1
  50. package/dist/kempo/api/extensions/DELETE.js +1 -1
  51. package/dist/kempo/api/extensions/DELETE.js.map +1 -1
  52. package/dist/kempo/api/extensions/POST.js +1 -1
  53. package/dist/kempo/api/extensions/POST.js.map +1 -1
  54. package/dist/kempo/api/extensions/[name]/disable/POST.js +1 -1
  55. package/dist/kempo/api/extensions/[name]/disable/POST.js.map +1 -1
  56. package/dist/kempo/api/extensions/[name]/enable/POST.js +1 -1
  57. package/dist/kempo/api/extensions/[name]/enable/POST.js.map +1 -1
  58. package/dist/kempo/api/extensions/[name]/update/POST.js +1 -1
  59. package/dist/kempo/api/extensions/[name]/update/POST.js.map +1 -1
  60. package/dist/kempo/api/extensions/[name]/update-check/GET.js +1 -1
  61. package/dist/kempo/api/extensions/[name]/update-check/GET.js.map +1 -1
  62. package/dist/kempo/api/pages/search/GET.js +1 -1
  63. package/dist/kempo/api/pages/search/GET.js.map +1 -1
  64. package/dist/kempo/api/user/[userid]/name/GET.js +2 -0
  65. package/dist/kempo/api/user/[userid]/name/GET.js.map +1 -0
  66. package/dist/kempo/components/UserDisplayName.js +2 -0
  67. package/dist/kempo/components/UserDisplayName.js.map +1 -0
  68. package/dist/kempo/sdk.js +1 -1
  69. package/dist/kempo/sdk.js.map +1 -1
  70. package/middleware/kempo.js +70 -45
  71. package/package.json +15 -8
  72. package/server/utils/admin-global-content/createAdminGlobalContent.js +4 -3
  73. package/server/utils/admin-global-content/deleteAdminGlobalContent.js +2 -2
  74. package/server/utils/admin-global-content/deleteAdminGlobalContentByOwner.js +2 -2
  75. package/server/utils/admin-global-content/disableAdminGlobalContent.js +2 -2
  76. package/server/utils/admin-global-content/enableAdminGlobalContent.js +2 -2
  77. package/server/utils/admin-global-content/getAdminGlobalContent.js +2 -2
  78. package/server/utils/admin-global-content/helpers.js +12 -1
  79. package/server/utils/admin-global-content/listAdminGlobalContent.js +2 -2
  80. package/server/utils/admin-global-content/updateAdminGlobalContent.js +2 -2
  81. package/server/utils/email/sendEmail.js +9 -3
  82. package/server/utils/extensions/uninstallExtension.js +54 -21
  83. package/server/utils/hooks/triggerHook.js +8 -1
@@ -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
- const resend = new Resend(process.env.RESEND_API_KEY);
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 resend.emails.send({
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
- export default async ({ name }) => {
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
- const groupNames = (kempoConfig.groups || []).map(g => g.name);
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
- Remove declarative permissions
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
- const permNames = (kempoConfig.permissions || []).map(p => p.name);
62
- if(permNames.length){
63
- await db.delete(permission).where(inArray(permission.name, permNames)).catch(() => {});
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
- Remove declarative settings (owner-namespaced: "extname:settingname")
68
- */
69
- await db.delete(setting).where(eq(setting.owner, name)).catch(() => {});
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));
@@ -34,7 +34,14 @@ export default async (event, data = {}, { bail = false } = {}) => {
34
34
  const mod = await import(pathToFileURL(handlerPath).href);
35
35
  handler = mod.default || mod;
36
36
  handlerCache.set(handlerPath, handler);
37
- } 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 });
38
45
  continue;
39
46
  }
40
47
  }