@kywi-software/core 0.2.1 → 0.3.0
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 +57 -2
- package/dist/admin/app.d.ts +10 -1
- package/dist/admin/app.d.ts.map +1 -1
- package/dist/admin/app.js +2 -2
- package/dist/admin/app.js.map +1 -1
- package/dist/admin/approval-queue.d.ts +3 -1
- package/dist/admin/approval-queue.d.ts.map +1 -1
- package/dist/admin/approval-queue.js +2 -2
- package/dist/admin/approval-queue.js.map +1 -1
- package/dist/admin/audience-list.d.ts.map +1 -1
- package/dist/admin/audience-list.js +9 -1
- package/dist/admin/audience-list.js.map +1 -1
- package/dist/admin/chrome.d.ts +7 -1
- package/dist/admin/chrome.d.ts.map +1 -1
- package/dist/admin/chrome.js +2 -1
- package/dist/admin/chrome.js.map +1 -1
- package/dist/admin/content-editor-api.d.ts +27 -1
- package/dist/admin/content-editor-api.d.ts.map +1 -1
- package/dist/admin/content-editor-api.js +60 -2
- package/dist/admin/content-editor-api.js.map +1 -1
- package/dist/admin/content-form.d.ts +62 -6
- package/dist/admin/content-form.d.ts.map +1 -1
- package/dist/admin/content-form.js +118 -13
- package/dist/admin/content-form.js.map +1 -1
- package/dist/admin/content-permissions-tab.d.ts +13 -0
- package/dist/admin/content-permissions-tab.d.ts.map +1 -0
- package/dist/admin/content-permissions-tab.js +90 -0
- package/dist/admin/content-permissions-tab.js.map +1 -0
- package/dist/admin/context.d.ts +8 -0
- package/dist/admin/context.d.ts.map +1 -1
- package/dist/admin/context.js.map +1 -1
- package/dist/admin/feed-builder.d.ts.map +1 -1
- package/dist/admin/feed-builder.js +20 -1
- package/dist/admin/feed-builder.js.map +1 -1
- package/dist/admin/field-components.d.ts.map +1 -1
- package/dist/admin/field-components.js +26 -1
- package/dist/admin/field-components.js.map +1 -1
- package/dist/admin/form-builder.js +6 -1
- package/dist/admin/form-builder.js.map +1 -1
- package/dist/admin/form-helpers.d.ts.map +1 -1
- package/dist/admin/form-helpers.js +2 -0
- package/dist/admin/form-helpers.js.map +1 -1
- package/dist/admin/layout-editor/component-actions.d.ts +18 -0
- package/dist/admin/layout-editor/component-actions.d.ts.map +1 -1
- package/dist/admin/layout-editor/component-actions.js +19 -0
- package/dist/admin/layout-editor/component-actions.js.map +1 -1
- package/dist/admin/layout-editor/component-picker-field.d.ts +20 -0
- package/dist/admin/layout-editor/component-picker-field.d.ts.map +1 -0
- package/dist/admin/layout-editor/component-picker-field.js +16 -0
- package/dist/admin/layout-editor/component-picker-field.js.map +1 -0
- package/dist/admin/layout-editor/drag-utils.d.ts +48 -0
- package/dist/admin/layout-editor/drag-utils.d.ts.map +1 -1
- package/dist/admin/layout-editor/drag-utils.js +43 -0
- package/dist/admin/layout-editor/drag-utils.js.map +1 -1
- package/dist/admin/layout-editor/editor-canvas.d.ts +9 -1
- package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-canvas.js +41 -15
- package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
- package/dist/admin/layout-editor/editor-state.d.ts +9 -1
- package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-state.js +34 -0
- package/dist/admin/layout-editor/editor-state.js.map +1 -1
- package/dist/admin/layout-editor/editor.css +83 -0
- package/dist/admin/layout-editor/form-picker-field.d.ts +17 -0
- package/dist/admin/layout-editor/form-picker-field.d.ts.map +1 -0
- package/dist/admin/layout-editor/form-picker-field.js +35 -0
- package/dist/admin/layout-editor/form-picker-field.js.map +1 -0
- package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
- package/dist/admin/layout-editor/layout-editor.js +95 -28
- package/dist/admin/layout-editor/layout-editor.js.map +1 -1
- package/dist/admin/layout-editor/mode-toggle.d.ts.map +1 -1
- package/dist/admin/layout-editor/mode-toggle.js +1 -1
- package/dist/admin/layout-editor/mode-toggle.js.map +1 -1
- package/dist/admin/layout-editor/module-overlay.d.ts +10 -1
- package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/module-overlay.js +3 -3
- package/dist/admin/layout-editor/module-overlay.js.map +1 -1
- package/dist/admin/layout-editor/props-panel.d.ts +10 -1
- package/dist/admin/layout-editor/props-panel.d.ts.map +1 -1
- package/dist/admin/layout-editor/props-panel.js +25 -2
- package/dist/admin/layout-editor/props-panel.js.map +1 -1
- package/dist/admin/layout-editor/section-insert-line.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-insert-line.js +1 -1
- package/dist/admin/layout-editor/section-insert-line.js.map +1 -1
- package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-overlay.js +1 -1
- package/dist/admin/layout-editor/section-overlay.js.map +1 -1
- package/dist/admin/layout-editor/section-template-picker.d.ts +9 -1
- package/dist/admin/layout-editor/section-template-picker.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-template-picker.js +45 -2
- package/dist/admin/layout-editor/section-template-picker.js.map +1 -1
- package/dist/admin/registry.d.ts.map +1 -1
- package/dist/admin/registry.js +11 -0
- package/dist/admin/registry.js.map +1 -1
- package/dist/admin/server.d.ts +15 -2
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +37 -1
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/styles/admin.css +16 -4
- package/dist/admin/surface-matcher.d.ts +27 -0
- package/dist/admin/surface-matcher.d.ts.map +1 -1
- package/dist/admin/surface-matcher.js +11 -0
- package/dist/admin/surface-matcher.js.map +1 -1
- package/dist/admin/surfaces/approvals/approvals-data.d.ts +29 -9
- package/dist/admin/surfaces/approvals/approvals-data.d.ts.map +1 -1
- package/dist/admin/surfaces/approvals/approvals-data.js +65 -22
- package/dist/admin/surfaces/approvals/approvals-data.js.map +1 -1
- package/dist/admin/surfaces/approvals/approvals-index.d.ts.map +1 -1
- package/dist/admin/surfaces/approvals/approvals-index.js +47 -14
- package/dist/admin/surfaces/approvals/approvals-index.js.map +1 -1
- package/dist/admin/surfaces/content/content-edit.d.ts.map +1 -1
- package/dist/admin/surfaces/content/content-edit.js +34 -8
- package/dist/admin/surfaces/content/content-edit.js.map +1 -1
- package/dist/admin/surfaces/content/content-index.d.ts.map +1 -1
- package/dist/admin/surfaces/content/content-index.js +29 -2
- package/dist/admin/surfaces/content/content-index.js.map +1 -1
- package/dist/admin/surfaces/content/content-new.d.ts.map +1 -1
- package/dist/admin/surfaces/content/content-new.js +6 -4
- package/dist/admin/surfaces/content/content-new.js.map +1 -1
- package/dist/admin/surfaces/contentTree/content-tree.d.ts.map +1 -1
- package/dist/admin/surfaces/contentTree/content-tree.js +25 -2
- package/dist/admin/surfaces/contentTree/content-tree.js.map +1 -1
- package/dist/admin/surfaces/contentTypes/content-types-designer.d.ts.map +1 -1
- package/dist/admin/surfaces/contentTypes/content-types-designer.js +78 -47
- package/dist/admin/surfaces/contentTypes/content-types-designer.js.map +1 -1
- package/dist/admin/surfaces/feeds/feed-editor.d.ts.map +1 -1
- package/dist/admin/surfaces/feeds/feed-editor.js +21 -4
- package/dist/admin/surfaces/feeds/feed-editor.js.map +1 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.js +7 -4
- package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
- package/dist/admin/surfaces/media/media-client.d.ts.map +1 -1
- package/dist/admin/surfaces/media/media-client.js +10 -2
- package/dist/admin/surfaces/media/media-client.js.map +1 -1
- package/dist/admin/surfaces/settings/settings-site.d.ts.map +1 -1
- package/dist/admin/surfaces/settings/settings-site.js +39 -2
- package/dist/admin/surfaces/settings/settings-site.js.map +1 -1
- package/dist/admin/surfaces/settings/settings-sites.js +18 -0
- package/dist/admin/surfaces/settings/settings-sites.js.map +1 -1
- package/dist/admin/surfaces/trash/trash-index.d.ts.map +1 -1
- package/dist/admin/surfaces/trash/trash-index.js +45 -8
- package/dist/admin/surfaces/trash/trash-index.js.map +1 -1
- package/dist/admin/surfaces/users/users-data.d.ts +11 -0
- package/dist/admin/surfaces/users/users-data.d.ts.map +1 -1
- package/dist/admin/surfaces/users/users-data.js +20 -1
- package/dist/admin/surfaces/users/users-data.js.map +1 -1
- package/dist/admin/surfaces/webServices/web-services-data.d.ts +11 -1
- package/dist/admin/surfaces/webServices/web-services-data.d.ts.map +1 -1
- package/dist/admin/surfaces/webServices/web-services-data.js +13 -0
- package/dist/admin/surfaces/webServices/web-services-data.js.map +1 -1
- package/dist/admin/surfaces/webServices/webhooks.d.ts +3 -0
- package/dist/admin/surfaces/webServices/webhooks.d.ts.map +1 -0
- package/dist/admin/surfaces/webServices/webhooks.js +69 -0
- package/dist/admin/surfaces/webServices/webhooks.js.map +1 -0
- package/dist/admin/webhook-manager.d.ts +32 -0
- package/dist/admin/webhook-manager.d.ts.map +1 -0
- package/dist/admin/webhook-manager.js +62 -0
- package/dist/admin/webhook-manager.js.map +1 -0
- package/dist/api/admin-features-routes.d.ts.map +1 -1
- package/dist/api/admin-features-routes.js +30 -0
- package/dist/api/admin-features-routes.js.map +1 -1
- package/dist/api/api-key-routes.d.ts.map +1 -1
- package/dist/api/api-key-routes.js +6 -1
- package/dist/api/api-key-routes.js.map +1 -1
- package/dist/api/auth.d.ts +8 -0
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.js.map +1 -1
- package/dist/api/comment-routes.d.ts.map +1 -1
- package/dist/api/comment-routes.js +102 -47
- package/dist/api/comment-routes.js.map +1 -1
- package/dist/api/config-routes.d.ts +10 -2
- package/dist/api/config-routes.d.ts.map +1 -1
- package/dist/api/config-routes.js +46 -25
- package/dist/api/config-routes.js.map +1 -1
- package/dist/api/content-routes.d.ts.map +1 -1
- package/dist/api/content-routes.js +41 -0
- package/dist/api/content-routes.js.map +1 -1
- package/dist/api/content-schemas.d.ts.map +1 -1
- package/dist/api/content-schemas.js +34 -1
- package/dist/api/content-schemas.js.map +1 -1
- package/dist/api/content-type-routes.d.ts.map +1 -1
- package/dist/api/content-type-routes.js +58 -3
- package/dist/api/content-type-routes.js.map +1 -1
- package/dist/api/feed-routes.d.ts +180 -0
- package/dist/api/feed-routes.d.ts.map +1 -1
- package/dist/api/feed-routes.js +10 -2
- package/dist/api/feed-routes.js.map +1 -1
- package/dist/api/form-routes.d.ts +6 -0
- package/dist/api/form-routes.d.ts.map +1 -1
- package/dist/api/form-routes.js +93 -14
- package/dist/api/form-routes.js.map +1 -1
- package/dist/api/graphql/resolvers.d.ts +4 -4
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +12 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/media-routes.d.ts.map +1 -1
- package/dist/api/media-routes.js +18 -1
- package/dist/api/media-routes.js.map +1 -1
- package/dist/api/webhook-routes.d.ts.map +1 -1
- package/dist/api/webhook-routes.js +19 -2
- package/dist/api/webhook-routes.js.map +1 -1
- package/dist/api/workflow-routes.d.ts.map +1 -1
- package/dist/api/workflow-routes.js +163 -4
- package/dist/api/workflow-routes.js.map +1 -1
- package/dist/auth/roles.d.ts +28 -0
- package/dist/auth/roles.d.ts.map +1 -1
- package/dist/auth/roles.js +49 -0
- package/dist/auth/roles.js.map +1 -1
- package/dist/config/reserved-field-names.d.ts +29 -0
- package/dist/config/reserved-field-names.d.ts.map +1 -0
- package/dist/config/reserved-field-names.js +59 -0
- package/dist/config/reserved-field-names.js.map +1 -0
- package/dist/config/types.d.ts +31 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/db/analytics-queries.d.ts +1 -1
- package/dist/db/api-key-queries.d.ts +7 -2
- package/dist/db/api-key-queries.d.ts.map +1 -1
- package/dist/db/api-key-queries.js +8 -2
- package/dist/db/api-key-queries.js.map +1 -1
- package/dist/db/apply-snapshot.d.ts +58 -0
- package/dist/db/apply-snapshot.d.ts.map +1 -0
- package/dist/db/apply-snapshot.js +142 -0
- package/dist/db/apply-snapshot.js.map +1 -0
- package/dist/db/audit-queries.d.ts +3 -3
- package/dist/db/category-queries.d.ts +3 -3
- package/dist/db/changeset-audit-queries.d.ts +2 -2
- package/dist/db/changeset-queries.d.ts +5 -5
- package/dist/db/changeset-queries.d.ts.map +1 -1
- package/dist/db/changeset-queries.js +24 -7
- package/dist/db/changeset-queries.js.map +1 -1
- package/dist/db/content-queries.d.ts +6 -0
- package/dist/db/content-queries.d.ts.map +1 -1
- package/dist/db/content-queries.js +8 -0
- package/dist/db/content-queries.js.map +1 -1
- package/dist/db/content-type-queries.d.ts +20 -0
- package/dist/db/content-type-queries.d.ts.map +1 -1
- package/dist/db/content-type-queries.js +32 -0
- package/dist/db/content-type-queries.js.map +1 -1
- package/dist/db/dashboard-queries.d.ts.map +1 -1
- package/dist/db/dashboard-queries.js +5 -1
- package/dist/db/dashboard-queries.js.map +1 -1
- package/dist/db/dynamic-schema.d.ts.map +1 -1
- package/dist/db/dynamic-schema.js +2 -0
- package/dist/db/dynamic-schema.js.map +1 -1
- package/dist/db/extension-queries.d.ts +8 -8
- package/dist/db/feed-queries.d.ts +3 -3
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +7 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/media-queries.d.ts +31 -7
- package/dist/db/media-queries.d.ts.map +1 -1
- package/dist/db/media-queries.js +13 -1
- package/dist/db/media-queries.js.map +1 -1
- package/dist/db/membership-dedupe.d.ts +26 -0
- package/dist/db/membership-dedupe.d.ts.map +1 -0
- package/dist/db/membership-dedupe.js +68 -0
- package/dist/db/membership-dedupe.js.map +1 -0
- package/dist/db/revision-queries.d.ts +122 -0
- package/dist/db/revision-queries.d.ts.map +1 -0
- package/dist/db/revision-queries.js +213 -0
- package/dist/db/revision-queries.js.map +1 -0
- package/dist/db/schema-apply.d.ts +8 -0
- package/dist/db/schema-apply.d.ts.map +1 -1
- package/dist/db/schema-apply.js +17 -0
- package/dist/db/schema-apply.js.map +1 -1
- package/dist/db/schema-field-queries.d.ts +2 -0
- package/dist/db/schema-field-queries.d.ts.map +1 -1
- package/dist/db/schema-field-queries.js +4 -0
- package/dist/db/schema-field-queries.js.map +1 -1
- package/dist/db/site-plugin-queries.d.ts +2 -2
- package/dist/db/user-queries.d.ts +12 -0
- package/dist/db/user-queries.d.ts.map +1 -1
- package/dist/db/user-queries.js +18 -4
- package/dist/db/user-queries.js.map +1 -1
- package/dist/db/version-queries.d.ts +37 -0
- package/dist/db/version-queries.d.ts.map +1 -1
- package/dist/db/version-queries.js +36 -0
- package/dist/db/version-queries.js.map +1 -1
- package/dist/db/webhook-queries.d.ts +27 -5
- package/dist/db/webhook-queries.d.ts.map +1 -1
- package/dist/db/webhook-queries.js +17 -0
- package/dist/db/webhook-queries.js.map +1 -1
- package/dist/db/workflow-queries.d.ts +2 -2
- package/dist/experiments/schema.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/webhook-retry.d.ts +22 -1
- package/dist/hooks/webhook-retry.d.ts.map +1 -1
- package/dist/hooks/webhook-retry.js +38 -1
- package/dist/hooks/webhook-retry.js.map +1 -1
- package/dist/host/index.d.ts +28 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +56 -1
- package/dist/host/index.js.map +1 -1
- package/dist/layout/comments-module.d.ts +18 -0
- package/dist/layout/comments-module.d.ts.map +1 -0
- package/dist/layout/comments-module.js +130 -0
- package/dist/layout/comments-module.js.map +1 -0
- package/dist/layout/feed-hydration.d.ts +75 -0
- package/dist/layout/feed-hydration.d.ts.map +1 -0
- package/dist/layout/feed-hydration.js +158 -0
- package/dist/layout/feed-hydration.js.map +1 -0
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +2 -1
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/module-render-context.d.ts +13 -0
- package/dist/layout/module-render-context.d.ts.map +1 -0
- package/dist/layout/module-render-context.js +15 -0
- package/dist/layout/module-render-context.js.map +1 -0
- package/dist/layout/modules-extended.d.ts.map +1 -1
- package/dist/layout/modules-extended.js +92 -13
- package/dist/layout/modules-extended.js.map +1 -1
- package/dist/layout/registry.d.ts.map +1 -1
- package/dist/layout/registry.js +22 -6
- package/dist/layout/registry.js.map +1 -1
- package/dist/layout/renderer.d.ts +17 -1
- package/dist/layout/renderer.d.ts.map +1 -1
- package/dist/layout/renderer.js +11 -4
- package/dist/layout/renderer.js.map +1 -1
- package/dist/nav/index.d.ts +1 -0
- package/dist/nav/index.d.ts.map +1 -1
- package/dist/nav/index.js +4 -0
- package/dist/nav/index.js.map +1 -1
- package/dist/nav/url-resolver.d.ts +10 -2
- package/dist/nav/url-resolver.d.ts.map +1 -1
- package/dist/nav/url-resolver.js +17 -3
- package/dist/nav/url-resolver.js.map +1 -1
- package/dist/schema/base.d.ts +37 -1
- package/dist/schema/base.d.ts.map +1 -1
- package/dist/schema/base.js +20 -2
- package/dist/schema/base.js.map +1 -1
- package/dist/schema/generate.d.ts +8 -0
- package/dist/schema/generate.d.ts.map +1 -1
- package/dist/schema/generate.js +7 -4
- package/dist/schema/generate.js.map +1 -1
- package/dist/schema/mapper.d.ts.map +1 -1
- package/dist/schema/mapper.js +2 -0
- package/dist/schema/mapper.js.map +1 -1
- package/dist/schema/mysql-tables.d.ts +34 -0
- package/dist/schema/mysql-tables.d.ts.map +1 -1
- package/dist/schema/mysql-tables.js +14 -2
- package/dist/schema/mysql-tables.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/site/styles.css +384 -0
- package/package.json +2 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `webServices.webhooks` surface — outgoing webhook endpoint management, the
|
|
5
|
+
* third Web Services surface alongside OAuth Clients + API Keys (#57). Follows
|
|
6
|
+
* the API Keys / OAuth surface pattern exactly: `useAdminContext().fetch` for
|
|
7
|
+
* all requests, core's `WebhookManager` component, and the shared
|
|
8
|
+
* `SecretRevealModal` for the one-time signing-secret reveal on create.
|
|
9
|
+
*
|
|
10
|
+
* Signing secrets are shown once at creation and redacted from every list/read
|
|
11
|
+
* afterwards (the server retains them to sign HMAC payloads — they are NOT
|
|
12
|
+
* hashed, unlike verify-only OAuth client secrets).
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
15
|
+
import { useAdminContext } from '../../context.js';
|
|
16
|
+
import { LoadingState, ErrorState } from '../../primitives.js';
|
|
17
|
+
import { SecretRevealModal } from './secret-reveal-modal.js';
|
|
18
|
+
import { WebhookManager } from '../../webhook-manager.js';
|
|
19
|
+
import { loadWebhooks, createWebhook, deleteWebhook, loadWebhookDeliveries, } from './web-services-data.js';
|
|
20
|
+
export default function WebServicesWebhooksSurface(_props) {
|
|
21
|
+
const { fetch: adminFetch } = useAdminContext();
|
|
22
|
+
const [webhooks, setWebhooks] = useState([]);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const [revealed, setRevealed] = useState(null);
|
|
26
|
+
const load = useCallback(async () => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
const list = await loadWebhooks(adminFetch);
|
|
31
|
+
list.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''));
|
|
32
|
+
setWebhooks(list);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
setError(e instanceof Error ? e.message : 'Failed to load webhooks');
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}, [adminFetch]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
load();
|
|
43
|
+
}, [load]);
|
|
44
|
+
const handleCreate = useCallback(async (data) => {
|
|
45
|
+
setError(null);
|
|
46
|
+
try {
|
|
47
|
+
const created = await createWebhook(adminFetch, data);
|
|
48
|
+
// Reveal the signing secret exactly once — it is redacted on every read.
|
|
49
|
+
setRevealed({ url: created.url, secret: created.secret });
|
|
50
|
+
await load();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
setError(e instanceof Error ? e.message : 'Failed to create webhook');
|
|
54
|
+
}
|
|
55
|
+
}, [load, adminFetch]);
|
|
56
|
+
const handleDelete = useCallback(async (id) => {
|
|
57
|
+
setError(null);
|
|
58
|
+
try {
|
|
59
|
+
await deleteWebhook(adminFetch, id);
|
|
60
|
+
await load();
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
setError(e instanceof Error ? e.message : 'Failed to delete webhook');
|
|
64
|
+
}
|
|
65
|
+
}, [load, adminFetch]);
|
|
66
|
+
const handleLoadDeliveries = useCallback((id) => loadWebhookDeliveries(adminFetch, id), [adminFetch]);
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "kywi-admin-page-header", children: _jsxs("div", { children: [_jsx("h1", { children: "Webhooks" }), _jsxs("p", { className: "kywi-admin-section-hint", children: ["Register endpoints that Kywi calls when content, comments, or categories change. Each delivery is signed (HMAC-SHA256) with the endpoint\u2019s secret, sent as", ' ', _jsx("code", { children: "X-Kywi-Signature" }), ". Failed deliveries are retried automatically with exponential backoff. The signing secret is shown once at creation and never again."] })] }) }), error && _jsx(ErrorState, { message: error, onRetry: load }), loading ? (_jsx(LoadingState, { label: "Loading webhooks\u2026" })) : (_jsx(WebhookManager, { webhooks: webhooks, onCreateWebhook: data => void handleCreate(data), onDeleteWebhook: id => void handleDelete(id), onLoadDeliveries: handleLoadDeliveries })), revealed && (_jsx(SecretRevealModal, { title: "Webhook signing secret", intro: `Copy the signing secret for “${revealed.url}” now. It is redacted from every later view and will not be shown again.`, label: "Signing secret", value: revealed.secret, onClose: () => setRevealed(null) }))] }));
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../../../src/admin/surfaces/webServices/webhooks.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface WebhookSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
events: string[];
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
/** True when a signing secret is configured (the secret itself is never listed). */
|
|
7
|
+
secretSet?: boolean;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WebhookCreateData {
|
|
11
|
+
url: string;
|
|
12
|
+
secret: string;
|
|
13
|
+
events: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface WebhookDeliverySummary {
|
|
16
|
+
id: string;
|
|
17
|
+
event: string;
|
|
18
|
+
status: string;
|
|
19
|
+
attempts: number;
|
|
20
|
+
httpStatus: number | null;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
nextRetryAt: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface WebhookManagerProps {
|
|
25
|
+
webhooks: WebhookSummary[];
|
|
26
|
+
onCreateWebhook?: (data: WebhookCreateData) => void;
|
|
27
|
+
onDeleteWebhook?: (id: string) => void;
|
|
28
|
+
/** Load recent deliveries for an endpoint (status / attempts log). */
|
|
29
|
+
onLoadDeliveries?: (id: string) => Promise<WebhookDeliverySummary[]>;
|
|
30
|
+
}
|
|
31
|
+
export declare function WebhookManager({ webhooks, onCreateWebhook, onDeleteWebhook, onLoadDeliveries, }: WebhookManagerProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
//# sourceMappingURL=webhook-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-manager.d.ts","sourceRoot":"","sources":["../../src/admin/webhook-manager.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAID,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAA;CACrE;AAqBD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,EAAE,mBAAmB,2CA4JrB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
// ── Available events ────────────────────────────────────────────────────────────
|
|
5
|
+
// Mirrors WEBHOOK_EVENTS in hooks/webhook-dispatch.ts — only events that are
|
|
6
|
+
// actually dispatched are advertised here.
|
|
7
|
+
const AVAILABLE_EVENTS = [
|
|
8
|
+
'content:afterCreate',
|
|
9
|
+
'content:afterUpdate',
|
|
10
|
+
'content:afterPublish',
|
|
11
|
+
'content:afterDelete',
|
|
12
|
+
'comment:afterCreate',
|
|
13
|
+
'comment:afterApprove',
|
|
14
|
+
'comment:afterDelete',
|
|
15
|
+
'category:afterCreate',
|
|
16
|
+
'category:afterUpdate',
|
|
17
|
+
'category:afterDelete',
|
|
18
|
+
];
|
|
19
|
+
// ── WebhookManager ──────────────────────────────────────────────────────────────
|
|
20
|
+
export function WebhookManager({ webhooks, onCreateWebhook, onDeleteWebhook, onLoadDeliveries, }) {
|
|
21
|
+
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
22
|
+
const [deleteConfirmId, setDeleteConfirmId] = useState(null);
|
|
23
|
+
const [deliveriesFor, setDeliveriesFor] = useState(null);
|
|
24
|
+
const [deliveries, setDeliveries] = useState([]);
|
|
25
|
+
const [deliveriesLoading, setDeliveriesLoading] = useState(false);
|
|
26
|
+
async function toggleDeliveries(id) {
|
|
27
|
+
if (deliveriesFor === id) {
|
|
28
|
+
setDeliveriesFor(null);
|
|
29
|
+
setDeliveries([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setDeliveriesFor(id);
|
|
33
|
+
setDeliveries([]);
|
|
34
|
+
if (!onLoadDeliveries)
|
|
35
|
+
return;
|
|
36
|
+
setDeliveriesLoading(true);
|
|
37
|
+
try {
|
|
38
|
+
setDeliveries(await onLoadDeliveries(id));
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setDeliveriesLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs("div", { className: "kywi-admin-webhook-manager", children: [_jsxs("div", { className: "kywi-admin-webhook-header", children: [_jsxs("h2", { className: "kywi-admin-webhook-title", children: ["Webhook Endpoints ", _jsxs("span", { className: "kywi-admin-webhook-count", children: ["(", webhooks.length, ")"] })] }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: () => setShowCreateForm(true), children: "Add Endpoint" })] }), showCreateForm && (_jsx(WebhookCreateForm, { onSubmit: (data) => {
|
|
45
|
+
onCreateWebhook?.(data);
|
|
46
|
+
setShowCreateForm(false);
|
|
47
|
+
}, onCancel: () => setShowCreateForm(false) })), webhooks.length === 0 ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "No webhook endpoints configured." })) : (_jsxs("table", { className: "kywi-admin-webhook-table", role: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "URL" }), _jsx("th", { children: "Events" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Created" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: webhooks.map(hook => (_jsxs(React.Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { children: _jsx("code", { children: hook.url }) }), _jsx("td", { children: (hook.events ?? []).map(ev => (_jsx("span", { className: "kywi-admin-webhook-event-label", children: ev }, ev))) }), _jsx("td", { children: hook.isActive ? 'Active' : 'Inactive' }), _jsx("td", { children: hook.createdAt ? new Date(hook.createdAt).toLocaleDateString() : '—' }), _jsxs("td", { children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm", onClick: () => void toggleDeliveries(hook.id), "aria-expanded": deliveriesFor === hook.id, "aria-label": `${deliveriesFor === hook.id ? 'Hide' : 'View'} deliveries for ${hook.url}`, children: deliveriesFor === hook.id ? 'Hide Deliveries' : 'View Deliveries' }), deleteConfirmId === hook.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "kywi-admin-confirm-text", children: "Delete this endpoint?" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm kywi-btn-danger", onClick: () => { onDeleteWebhook?.(hook.id); setDeleteConfirmId(null); }, "aria-label": `Confirm delete ${hook.url}`, children: "Yes, Delete" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm", onClick: () => setDeleteConfirmId(null), children: "Cancel" })] })) : (_jsx("button", { type: "button", className: "kywi-btn kywi-btn-sm kywi-btn-danger", onClick: () => setDeleteConfirmId(hook.id), "aria-label": `Delete ${hook.url}`, children: "Delete" }))] })] }), deliveriesFor === hook.id && (_jsx("tr", { children: _jsx("td", { colSpan: 5, children: deliveriesLoading ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "Loading deliveries\u2026" })) : deliveries.length === 0 ? (_jsx("p", { className: "kywi-admin-webhook-empty", children: "No deliveries recorded yet." })) : (_jsxs("table", { className: "kywi-admin-webhook-deliveries-table", role: "table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Event" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Attempts" }), _jsx("th", { children: "HTTP" }), _jsx("th", { children: "When" })] }) }), _jsx("tbody", { children: deliveries.map(d => (_jsxs("tr", { children: [_jsx("td", { children: d.event }), _jsx("td", { children: d.status }), _jsx("td", { children: d.attempts }), _jsx("td", { children: d.httpStatus ?? '—' }), _jsx("td", { children: d.createdAt ? new Date(d.createdAt).toLocaleString() : '—' })] }, d.id))) })] })) }) }))] }, hook.id))) })] }))] }));
|
|
48
|
+
}
|
|
49
|
+
function WebhookCreateForm({ onSubmit, onCancel }) {
|
|
50
|
+
const [url, setUrl] = useState('');
|
|
51
|
+
const [secret, setSecret] = useState('');
|
|
52
|
+
const [events, setEvents] = useState([]);
|
|
53
|
+
function handleEventToggle(event, checked) {
|
|
54
|
+
setEvents(prev => checked ? [...prev, event] : prev.filter(e => e !== event));
|
|
55
|
+
}
|
|
56
|
+
function handleSubmit(e) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
onSubmit({ url, secret, events });
|
|
59
|
+
}
|
|
60
|
+
return (_jsxs("form", { className: "kywi-admin-webhook-form", onSubmit: handleSubmit, children: [_jsxs("div", { className: "kywi-form-field", children: [_jsx("label", { htmlFor: "webhook-url", children: "Endpoint URL" }), _jsx("input", { id: "webhook-url", type: "url", className: "kywi-input", value: url, onChange: (e) => setUrl(e.target.value), placeholder: "https://example.com/hooks/kywi", required: true })] }), _jsxs("div", { className: "kywi-form-field", children: [_jsx("label", { htmlFor: "webhook-secret", children: "Signing Secret" }), _jsx("input", { id: "webhook-secret", type: "text", className: "kywi-input", value: secret, onChange: (e) => setSecret(e.target.value), minLength: 16, placeholder: "At least 16 characters", required: true }), _jsxs("p", { className: "kywi-admin-section-hint", children: ["Used to sign each payload (HMAC-SHA256, sent as ", _jsx("code", { children: "X-Kywi-Signature" }), "). Shown once here \u2014 store it now; it is redacted from every later view."] })] }), _jsxs("fieldset", { className: "kywi-form-field", children: [_jsx("legend", { children: "Events" }), AVAILABLE_EVENTS.map(event => (_jsxs("label", { className: "kywi-admin-webhook-event-label", children: [_jsx("input", { type: "checkbox", checked: events.includes(event), onChange: (e) => handleEventToggle(event, e.target.checked) }), event] }, event)))] }), _jsxs("div", { className: "kywi-admin-webhook-form-actions", children: [_jsx("button", { type: "submit", className: "kywi-btn kywi-btn-primary", disabled: events.length === 0, children: "Add Endpoint" }), _jsx("button", { type: "button", className: "kywi-btn", onClick: onCancel, children: "Cancel" })] })] }));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=webhook-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-manager.js","sourceRoot":"","sources":["../../src/admin/webhook-manager.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-features-routes.d.ts","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"admin-features-routes.d.ts","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,oBAAoB,CAAA;AA2CzE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,yBAAyB,GAAG,IAAI,CAqDhG"}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { resolveAdminConfig } from '../config/define.js';
|
|
2
3
|
import { jsonOk, jsonError, unauthorized, forbidden, validationError } from './response.js';
|
|
3
4
|
import { resolveAuth } from './auth.js';
|
|
4
5
|
import { hasMinRole } from '../auth/roles.js';
|
|
5
6
|
import { toSiteUuid } from './site-context.js';
|
|
6
7
|
import { getAdminFeatureFlags, setAdminFeatureFlags } from '../db/admin-features-queries.js';
|
|
8
|
+
/**
|
|
9
|
+
* True when a runtime flag key (an admin area or surface id) is held OFF by the
|
|
10
|
+
* config ceiling and therefore cannot be re-enabled at runtime. Mirrors the
|
|
11
|
+
* config-ceiling checks in `isSurfaceEnabled` (surface-matcher.ts): master off,
|
|
12
|
+
* area denylist, or surface-id denylist. The Admin Features surface is never
|
|
13
|
+
* config-locked by an area/surface denylist (the escape hatch), so a superAdmin
|
|
14
|
+
* can always runtime-enable it; only the master switch removes it.
|
|
15
|
+
*/
|
|
16
|
+
function isConfigLocked(ceiling, key) {
|
|
17
|
+
if (ceiling.enabled === false)
|
|
18
|
+
return true;
|
|
19
|
+
if (ceiling.features[key] === false)
|
|
20
|
+
return true;
|
|
21
|
+
if (ceiling.disableSurfaces.includes(key))
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* T16 runtime admin feature-flag routes (plan §4). Thin wrappers over the
|
|
9
27
|
* per-site `admin.features` settings-key queries.
|
|
@@ -52,6 +70,18 @@ export function registerAdminFeaturesRoutes(router, ctx) {
|
|
|
52
70
|
if (!parsed.success) {
|
|
53
71
|
return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
|
|
54
72
|
}
|
|
73
|
+
// Honest config ceiling (#58): the config hard-ceiling holds at resolution
|
|
74
|
+
// time regardless, so silently storing `{comments:true}` for a config-disabled
|
|
75
|
+
// feature was a lie to anyone scripting the API. Reject any attempt to ENABLE a
|
|
76
|
+
// config-locked feature (nothing is persisted) instead of returning a hollow
|
|
77
|
+
// 200. Setting a locked feature to `false`, or toggling unlocked features, is
|
|
78
|
+
// unaffected — so the admin UI's normal area toggles keep working.
|
|
79
|
+
const ceiling = resolveAdminConfig(config.admin);
|
|
80
|
+
const lockedKeys = Object.keys(parsed.data).filter(key => parsed.data[key] === true && isConfigLocked(ceiling, key));
|
|
81
|
+
if (lockedKeys.length > 0) {
|
|
82
|
+
return jsonError('CONFIG_LOCKED', `Cannot enable feature(s) disabled in kywi.config.ts: ${lockedKeys.join(', ')}. ` +
|
|
83
|
+
'Config is a hard ceiling over runtime flags — change it in kywi.config.ts.', 409, lockedKeys.map(key => ({ path: key, message: 'Disabled by kywi.config.ts (config-locked)' })));
|
|
84
|
+
}
|
|
55
85
|
const url = new URL(req.url);
|
|
56
86
|
const siteId = await toSiteUuid(db, url.searchParams.get('siteId'), config);
|
|
57
87
|
const flags = await setAdminFeatureFlags(db, siteId, parsed.data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-features-routes.js","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"admin-features-routes.js","sourceRoot":"","sources":["../../src/api/admin-features-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-key-routes.d.ts","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAY7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"api-key-routes.d.ts","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAY7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAqDzF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { jsonOk, jsonError, unauthorized, notFound, validationError } from './response.js';
|
|
2
|
+
import { jsonOk, jsonError, unauthorized, forbidden, notFound, validationError } from './response.js';
|
|
3
3
|
import { createApiKey, listApiKeys, revokeApiKey } from '../db/api-key-queries.js';
|
|
4
4
|
import { resolveAuth } from './auth.js';
|
|
5
5
|
const createApiKeySchema = z.object({
|
|
@@ -15,6 +15,11 @@ export function registerApiKeyRoutes(router, db, authSecret) {
|
|
|
15
15
|
const auth = await authenticate(req);
|
|
16
16
|
if (!auth.user)
|
|
17
17
|
return unauthorized();
|
|
18
|
+
// #54: an API key must not be able to mint another API key. Otherwise a
|
|
19
|
+
// read-scoped key could bootstrap a higher-scoped one and escalate past its
|
|
20
|
+
// own ceiling. Key creation is a session-only (human, admin UI) operation.
|
|
21
|
+
if (auth.viaApiKey)
|
|
22
|
+
return forbidden('API keys cannot create API keys');
|
|
18
23
|
let body;
|
|
19
24
|
try {
|
|
20
25
|
body = await req.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-key-routes.js","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"api-key-routes.js","sourceRoot":"","sources":["../../src/api/api-key-routes.ts"],"names":[],"mappings":"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"}
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import type { KywiDb } from '../db/client.js';
|
|
|
3
3
|
export interface AuthResult {
|
|
4
4
|
user: UserContext | null;
|
|
5
5
|
error?: string;
|
|
6
|
+
/**
|
|
7
|
+
* True when the request was authenticated by a bearer API key (`kywi_…`)
|
|
8
|
+
* rather than a session/JWT. Credential-minting routes use this to refuse
|
|
9
|
+
* bootstrapping — an API key must never be able to create another API key
|
|
10
|
+
* (a read-scoped key could otherwise mint a higher-scoped one, defeating the
|
|
11
|
+
* scope model, #54). Session auth leaves this falsy, so it is unaffected.
|
|
12
|
+
*/
|
|
13
|
+
viaApiKey?: boolean;
|
|
6
14
|
}
|
|
7
15
|
export declare function verifyAuthHeader(authHeader: string | null, secret: string): Promise<AuthResult>;
|
|
8
16
|
export declare function resolveAuth(authHeader: string | null, secret: string, db: KywiDb): Promise<AuthResult>;
|
package/dist/api/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAK7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,UAAU,CAAC,CAwBrB"}
|
package/dist/api/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jwtVerify } from 'jose';
|
|
2
2
|
import { resolveApiKey } from '../db/api-key-queries.js';
|
|
3
3
|
import { getUserById, getUserGroups, buildUserContext } from '../db/user-queries.js';
|
|
4
|
+
import { effectiveApiKeyRole } from '../auth/roles.js';
|
|
4
5
|
export async function verifyAuthHeader(authHeader, secret) {
|
|
5
6
|
if (!authHeader) {
|
|
6
7
|
return { user: null };
|
|
@@ -43,7 +44,16 @@ export async function resolveAuth(authHeader, secret, db) {
|
|
|
43
44
|
if (!user || !user.isActive)
|
|
44
45
|
return { user: null, error: 'API key owner not found or inactive' };
|
|
45
46
|
const groups = await getUserGroups(db, user.id);
|
|
46
|
-
|
|
47
|
+
const ctx = buildUserContext(user, groups);
|
|
48
|
+
// #54: enforce the key's declared scopes as a ceiling on the owner's role.
|
|
49
|
+
// The effective permission is intersection(owner role, key scopes) — NOT a
|
|
50
|
+
// fallback to the owner's role. Otherwise a read-scoped key (the kind handed
|
|
51
|
+
// to a static frontend or CI job) inherits the owner's full role and can
|
|
52
|
+
// POST/PUT/DELETE/publish content. Every route authorizes off `user.role`
|
|
53
|
+
// (canAccessContent / hasMinRole), so capping the role here uniformly gates
|
|
54
|
+
// all API-key traffic. The session/cookie path never reaches this branch, so
|
|
55
|
+
// it is unaffected.
|
|
56
|
+
return { user: { ...ctx, role: effectiveApiKeyRole(ctx.role, result.scopes) }, viaApiKey: true };
|
|
47
57
|
}
|
|
48
58
|
return verifyAuthHeader(authHeader, secret);
|
|
49
59
|
}
|
package/dist/api/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-routes.d.ts","sourceRoot":"","sources":["../../src/api/comment-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"comment-routes.d.ts","sourceRoot":"","sources":["../../src/api/comment-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAsBxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,UAAU,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,YAAY,CAAA;IAC1B,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IACzC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAwBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,mBAAmB,GAAG,IAAI,CA0TpF"}
|
|
@@ -6,6 +6,7 @@ import { hasMinRole } from '../auth/roles.js';
|
|
|
6
6
|
import { listComments, listSiteComments, getCommentById, createComment, updateComment, updateCommentStatus, deleteComment, flagComment, unflagComment, } from '../db/comment-queries.js';
|
|
7
7
|
import { isSpam } from '../comments/spam-filter.js';
|
|
8
8
|
import { getContentById } from '../db/content-queries.js';
|
|
9
|
+
import { checkHoneypot } from './form-routes.js';
|
|
9
10
|
import { jsonOk, jsonError, notFound, unauthorized, forbidden, validationError } from './response.js';
|
|
10
11
|
import { listSubscribers } from '../db/comment-subscription-queries.js';
|
|
11
12
|
import { sendTemplatedEmail, COMMENT_NOTIFICATION_TEMPLATE } from '../email/index.js';
|
|
@@ -15,6 +16,12 @@ const createCommentSchema = z.object({
|
|
|
15
16
|
authorName: z.string().optional(),
|
|
16
17
|
authorEmail: z.string().optional(),
|
|
17
18
|
});
|
|
19
|
+
// Collection-endpoint variant (POST /comments): the content is named in the body
|
|
20
|
+
// rather than the URL path, so the public comment form / Comments layout module
|
|
21
|
+
// can submit without knowing the content's base-type segment.
|
|
22
|
+
const createCommentCollectionSchema = createCommentSchema.extend({
|
|
23
|
+
contentId: z.string().min(1),
|
|
24
|
+
});
|
|
18
25
|
const updateCommentSchema = z.object({
|
|
19
26
|
body: z.string().min(1).optional(),
|
|
20
27
|
status: z.string().optional(),
|
|
@@ -22,37 +29,17 @@ const updateCommentSchema = z.object({
|
|
|
22
29
|
export function registerCommentRoutes(router, ctx) {
|
|
23
30
|
const { db, authSecret } = ctx;
|
|
24
31
|
const commentHooks = createCommentHookDispatcher(ctx.hookRegistry);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
status = 'approved';
|
|
37
|
-
}
|
|
38
|
-
const comments = await listComments(db, params.id, status);
|
|
39
|
-
return jsonOk(comments);
|
|
40
|
-
});
|
|
41
|
-
// POST /content/:type/:id/comments — create comment
|
|
42
|
-
router.post('/content/:type/:id/comments', async (req, params) => {
|
|
43
|
-
const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
|
|
44
|
-
let body;
|
|
45
|
-
try {
|
|
46
|
-
body = await req.json();
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
|
|
50
|
-
}
|
|
51
|
-
const parsed = createCommentSchema.safeParse(body);
|
|
52
|
-
if (!parsed.success) {
|
|
53
|
-
return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
|
|
54
|
-
}
|
|
55
|
-
const content = await getContentById(db, null, params.id);
|
|
32
|
+
/**
|
|
33
|
+
* Shared create path for both comment-submission endpoints
|
|
34
|
+
* (`POST /content/:type/:id/comments` and the collection `POST /comments`).
|
|
35
|
+
* Validates the target content, honours the content type's `commentable`
|
|
36
|
+
* flag, screens for spam, persists the comment as `pending` (so every new
|
|
37
|
+
* comment lands in the moderation queue), fires the afterCreate hook, and
|
|
38
|
+
* best-effort notifies subscribers. Honeypot + JSON parsing are handled by the
|
|
39
|
+
* callers, which hold the raw request body.
|
|
40
|
+
*/
|
|
41
|
+
async function submitComment(auth, contentId, data) {
|
|
42
|
+
const content = await getContentById(db, null, contentId);
|
|
56
43
|
if (!content)
|
|
57
44
|
return notFound('Content not found');
|
|
58
45
|
// Enforce commentable flag on content type
|
|
@@ -61,38 +48,38 @@ export function registerCommentRoutes(router, ctx) {
|
|
|
61
48
|
if (contentTypeConfig && contentTypeConfig.commentable === false) {
|
|
62
49
|
return jsonError('FORBIDDEN', 'Comments are not allowed on this content type', 403);
|
|
63
50
|
}
|
|
64
|
-
if (isSpam(
|
|
51
|
+
if (isSpam(data.body)) {
|
|
65
52
|
return jsonError('SPAM_REJECTED', 'Comment rejected as spam', 400);
|
|
66
53
|
}
|
|
67
54
|
const commentData = {
|
|
68
55
|
siteId: content.siteId,
|
|
69
|
-
contentId
|
|
70
|
-
body:
|
|
56
|
+
contentId,
|
|
57
|
+
body: data.body,
|
|
71
58
|
status: 'pending',
|
|
72
59
|
};
|
|
73
|
-
if (
|
|
74
|
-
commentData.parentId =
|
|
60
|
+
if (data.parentId !== undefined) {
|
|
61
|
+
commentData.parentId = data.parentId;
|
|
75
62
|
}
|
|
76
63
|
if (auth.user !== null) {
|
|
77
64
|
commentData.authorId = auth.user.id;
|
|
78
65
|
}
|
|
79
|
-
if (
|
|
80
|
-
commentData.authorName =
|
|
66
|
+
if (data.authorName !== undefined) {
|
|
67
|
+
commentData.authorName = data.authorName;
|
|
81
68
|
}
|
|
82
|
-
if (
|
|
83
|
-
commentData.authorEmail =
|
|
69
|
+
if (data.authorEmail !== undefined) {
|
|
70
|
+
commentData.authorEmail = data.authorEmail;
|
|
84
71
|
}
|
|
85
72
|
const comment = await createComment(db, commentData);
|
|
86
73
|
await commentHooks.afterCreate(comment);
|
|
87
74
|
// W6: send email notifications to subscribers
|
|
88
75
|
if (ctx.emailProvider) {
|
|
89
76
|
try {
|
|
90
|
-
const subscribers = await listSubscribers(db,
|
|
91
|
-
const authorEmail =
|
|
77
|
+
const subscribers = await listSubscribers(db, contentId);
|
|
78
|
+
const authorEmail = data.authorEmail ?? (auth.user ? auth.user.id : undefined);
|
|
92
79
|
const contentTitle = content.title ?? 'Untitled';
|
|
93
80
|
const siteName = ctx.config.sites[0]?.name ?? 'Kywi';
|
|
94
81
|
const baseUrl = ctx.baseUrl ?? 'http://localhost:3000';
|
|
95
|
-
const contentUrl = `${baseUrl}/content/${
|
|
82
|
+
const contentUrl = `${baseUrl}/content/${contentId}`;
|
|
96
83
|
for (const sub of subscribers) {
|
|
97
84
|
// Skip the comment author
|
|
98
85
|
if (authorEmail && sub.email === authorEmail)
|
|
@@ -103,8 +90,8 @@ export function registerCommentRoutes(router, ctx) {
|
|
|
103
90
|
template: COMMENT_NOTIFICATION_TEMPLATE,
|
|
104
91
|
vars: {
|
|
105
92
|
contentTitle,
|
|
106
|
-
commentAuthor:
|
|
107
|
-
commentBody:
|
|
93
|
+
commentAuthor: data.authorName ?? 'Someone',
|
|
94
|
+
commentBody: data.body,
|
|
108
95
|
siteName,
|
|
109
96
|
contentUrl,
|
|
110
97
|
},
|
|
@@ -116,15 +103,83 @@ export function registerCommentRoutes(router, ctx) {
|
|
|
116
103
|
}
|
|
117
104
|
}
|
|
118
105
|
return jsonOk(comment, 201);
|
|
106
|
+
}
|
|
107
|
+
// GET /content/:type/:id/comments — list comments on content
|
|
108
|
+
router.get('/content/:type/:id/comments', async (req, params) => {
|
|
109
|
+
const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
|
|
110
|
+
const isEditor = auth.user !== null && hasMinRole(auth.user.role, 'editor');
|
|
111
|
+
const url = new URL(req.url);
|
|
112
|
+
const statusParam = url.searchParams.get('status') ?? undefined;
|
|
113
|
+
let status;
|
|
114
|
+
if (isEditor) {
|
|
115
|
+
status = statusParam;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
status = 'approved';
|
|
119
|
+
}
|
|
120
|
+
const comments = await listComments(db, params.id, status);
|
|
121
|
+
return jsonOk(comments);
|
|
122
|
+
});
|
|
123
|
+
// POST /content/:type/:id/comments — create comment (content named in the path)
|
|
124
|
+
router.post('/content/:type/:id/comments', async (req, params) => {
|
|
125
|
+
const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
|
|
126
|
+
let body;
|
|
127
|
+
try {
|
|
128
|
+
body = await req.json();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
|
|
132
|
+
}
|
|
133
|
+
// Honeypot — a bot that fills the hidden field is silently accepted (so it
|
|
134
|
+
// can't tell it was blocked) but nothing is persisted (shared with forms).
|
|
135
|
+
if (checkHoneypot(body)) {
|
|
136
|
+
return jsonOk({ status: 'received' }, 200);
|
|
137
|
+
}
|
|
138
|
+
const parsed = createCommentSchema.safeParse(body);
|
|
139
|
+
if (!parsed.success) {
|
|
140
|
+
return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
|
|
141
|
+
}
|
|
142
|
+
return submitComment(auth, params.id, parsed.data);
|
|
119
143
|
});
|
|
120
|
-
//
|
|
144
|
+
// POST /comments — public comment submission (content named in the body). This
|
|
145
|
+
// is the collection endpoint the public comment form / Comments layout module
|
|
146
|
+
// posts to; without it POST /comments matched the GET-only /comments route and
|
|
147
|
+
// returned 405, leaving the whole feature inert end-to-end (#29).
|
|
148
|
+
router.post('/comments', async (req) => {
|
|
149
|
+
const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
|
|
150
|
+
let body;
|
|
151
|
+
try {
|
|
152
|
+
body = await req.json();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
|
|
156
|
+
}
|
|
157
|
+
if (checkHoneypot(body)) {
|
|
158
|
+
return jsonOk({ status: 'received' }, 200);
|
|
159
|
+
}
|
|
160
|
+
const parsed = createCommentCollectionSchema.safeParse(body);
|
|
161
|
+
if (!parsed.success) {
|
|
162
|
+
return validationError(parsed.error.errors.map(e => ({ path: e.path.join('.'), message: e.message })));
|
|
163
|
+
}
|
|
164
|
+
const { contentId, ...data } = parsed.data;
|
|
165
|
+
return submitComment(auth, contentId, data);
|
|
166
|
+
});
|
|
167
|
+
// GET /comments — dual-mode collection read.
|
|
168
|
+
// • Public: `?contentId=…` returns that content's APPROVED comments only, no
|
|
169
|
+
// auth required — this is what the public Comments module renders.
|
|
170
|
+
// • Admin: no `contentId` → the editor-only site moderation listing.
|
|
121
171
|
router.get('/comments', async (req) => {
|
|
172
|
+
const url = new URL(req.url);
|
|
173
|
+
const contentIdParam = url.searchParams.get('contentId');
|
|
174
|
+
if (contentIdParam) {
|
|
175
|
+
const comments = await listComments(db, contentIdParam, 'approved');
|
|
176
|
+
return jsonOk(comments);
|
|
177
|
+
}
|
|
122
178
|
const auth = await resolveAuth(req.headers.get('authorization'), authSecret, db);
|
|
123
179
|
if (!auth.user)
|
|
124
180
|
return unauthorized();
|
|
125
181
|
if (!hasMinRole(auth.user.role, 'editor'))
|
|
126
182
|
return forbidden();
|
|
127
|
-
const url = new URL(req.url);
|
|
128
183
|
const statusParam = url.searchParams.get('status') ?? undefined;
|
|
129
184
|
const siteId = await toSiteUuid(db, url.searchParams.get('siteId'), ctx.config);
|
|
130
185
|
if (!siteId)
|