@actuate-media/cms-core 0.32.0 → 0.33.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.
Files changed (41) hide show
  1. package/dist/__tests__/api/form-webhooks.test.d.ts +2 -0
  2. package/dist/__tests__/api/form-webhooks.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/form-webhooks.test.js +162 -0
  4. package/dist/__tests__/api/form-webhooks.test.js.map +1 -0
  5. package/dist/__tests__/forms/notify.test.d.ts +2 -0
  6. package/dist/__tests__/forms/notify.test.d.ts.map +1 -0
  7. package/dist/__tests__/forms/notify.test.js +114 -0
  8. package/dist/__tests__/forms/notify.test.js.map +1 -0
  9. package/dist/__tests__/forms/webhooks.test.d.ts +2 -0
  10. package/dist/__tests__/forms/webhooks.test.d.ts.map +1 -0
  11. package/dist/__tests__/forms/webhooks.test.js +203 -0
  12. package/dist/__tests__/forms/webhooks.test.js.map +1 -0
  13. package/dist/api/handlers.d.ts.map +1 -1
  14. package/dist/api/handlers.js +145 -0
  15. package/dist/api/handlers.js.map +1 -1
  16. package/dist/config/types.d.ts +11 -6
  17. package/dist/config/types.d.ts.map +1 -1
  18. package/dist/forms/entries.d.ts.map +1 -1
  19. package/dist/forms/entries.js +16 -1
  20. package/dist/forms/entries.js.map +1 -1
  21. package/dist/forms/index.d.ts +3 -1
  22. package/dist/forms/index.d.ts.map +1 -1
  23. package/dist/forms/index.js +3 -1
  24. package/dist/forms/index.js.map +1 -1
  25. package/dist/forms/notify.d.ts +37 -0
  26. package/dist/forms/notify.d.ts.map +1 -0
  27. package/dist/forms/notify.js +198 -0
  28. package/dist/forms/notify.js.map +1 -0
  29. package/dist/forms/submission.d.ts +21 -0
  30. package/dist/forms/submission.d.ts.map +1 -1
  31. package/dist/forms/submission.js +59 -5
  32. package/dist/forms/submission.js.map +1 -1
  33. package/dist/forms/webhooks.d.ts +54 -0
  34. package/dist/forms/webhooks.d.ts.map +1 -0
  35. package/dist/forms/webhooks.js +292 -0
  36. package/dist/forms/webhooks.js.map +1 -0
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/package.json +1 -1
@@ -28,6 +28,8 @@ import { listEntries as listEntriesService, getEntryWithSchema as getEntryWithSc
28
28
  import { processSubmission, buildPublicForm, SubmissionValidationError, } from '../forms/submission.js';
29
29
  import { validateSubmissionFiles, FORM_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_MAX_FILE_BYTES, } from '../forms/files.js';
30
30
  import { resolveSubmissionFields } from '../forms/submission.js';
31
+ import { createFormWebhook, listFormWebhooks, updateFormWebhook, deleteFormWebhook, testFormWebhook, FormWebhookError, } from '../forms/webhooks.js';
32
+ import { sendFormNotifications } from '../forms/notify.js';
31
33
  import { checkForUpdates } from '../upgrade/version-check.js';
32
34
  import { createUpgradePR } from '../upgrade/upgrade-pr.js';
33
35
  import { encryptField, decryptField } from '../security/encrypted-fields.js';
@@ -290,6 +292,8 @@ function mapFormError(err) {
290
292
  if (err instanceof FormHasSubmissionsError) {
291
293
  return json({ error: err.message, code: 'FORM_HAS_SUBMISSIONS', submissionCount: err.submissionCount }, 409);
292
294
  }
295
+ if (err instanceof FormWebhookError)
296
+ return errorResponse(err.message, err.status);
293
297
  return null;
294
298
  }
295
299
  function parseFormsQuery(url) {
@@ -4720,6 +4724,147 @@ export function registerCMSRoutes(router) {
4720
4724
  return mapFormError(err) ?? internalError(err);
4721
4725
  }
4722
4726
  });
4727
+ // ── Per-form webhooks (CRUD + test) ──────────────────────────────────────
4728
+ router.get('/forms/:id/webhooks', async (request, params) => {
4729
+ try {
4730
+ const auth = await requireAuth(request);
4731
+ if (auth.error)
4732
+ return auth.error;
4733
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4734
+ if (roleErr)
4735
+ return roleErr;
4736
+ const form = await getFormByIdService(params.id);
4737
+ if (!form)
4738
+ return errorResponse('Form not found', 404);
4739
+ const webhooks = await listFormWebhooks(params.id);
4740
+ return json({ data: webhooks });
4741
+ }
4742
+ catch (err) {
4743
+ return mapFormError(err) ?? internalError(err);
4744
+ }
4745
+ });
4746
+ router.post('/forms/:id/webhooks', async (request, params) => {
4747
+ try {
4748
+ const auth = await requireAuth(request);
4749
+ if (auth.error)
4750
+ return auth.error;
4751
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4752
+ if (roleErr)
4753
+ return roleErr;
4754
+ const form = await getFormByIdService(params.id);
4755
+ if (!form)
4756
+ return errorResponse('Form not found', 404);
4757
+ const body = (await request.json().catch(() => ({})));
4758
+ if (!body.url || typeof body.url !== 'string') {
4759
+ return errorResponse('A webhook url is required', 400);
4760
+ }
4761
+ const ctx = buildActionContext(auth.session, db());
4762
+ const webhook = await createFormWebhook(ctx, params.id, body);
4763
+ // The plaintext secret is returned exactly once here, on creation.
4764
+ return json({ data: webhook }, 201);
4765
+ }
4766
+ catch (err) {
4767
+ return mapFormError(err) ?? internalError(err);
4768
+ }
4769
+ });
4770
+ router.patch('/forms/:id/webhooks/:webhookId', async (request, params) => {
4771
+ try {
4772
+ const auth = await requireAuth(request);
4773
+ if (auth.error)
4774
+ return auth.error;
4775
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4776
+ if (roleErr)
4777
+ return roleErr;
4778
+ const body = (await request.json().catch(() => ({})));
4779
+ const ctx = buildActionContext(auth.session, db());
4780
+ const webhook = await updateFormWebhook(ctx, params.webhookId, body);
4781
+ return json({ data: webhook });
4782
+ }
4783
+ catch (err) {
4784
+ return mapFormError(err) ?? internalError(err);
4785
+ }
4786
+ });
4787
+ router.delete('/forms/:id/webhooks/:webhookId', async (request, params) => {
4788
+ try {
4789
+ const auth = await requireAuth(request);
4790
+ if (auth.error)
4791
+ return auth.error;
4792
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4793
+ if (roleErr)
4794
+ return roleErr;
4795
+ const ctx = buildActionContext(auth.session, db());
4796
+ await deleteFormWebhook(ctx, params.webhookId);
4797
+ return json({ data: { success: true } });
4798
+ }
4799
+ catch (err) {
4800
+ return mapFormError(err) ?? internalError(err);
4801
+ }
4802
+ });
4803
+ router.post('/forms/:id/webhooks/:webhookId/test', async (request, params) => {
4804
+ try {
4805
+ const auth = await requireAuth(request);
4806
+ if (auth.error)
4807
+ return auth.error;
4808
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4809
+ if (roleErr)
4810
+ return roleErr;
4811
+ const ctx = buildActionContext(auth.session, db());
4812
+ const result = await testFormWebhook(ctx, params.webhookId);
4813
+ return json({ data: result });
4814
+ }
4815
+ catch (err) {
4816
+ return mapFormError(err) ?? internalError(err);
4817
+ }
4818
+ });
4819
+ // ── Test notification email ──────────────────────────────────────────────
4820
+ router.post('/forms/:id/notifications/test', async (request, params) => {
4821
+ try {
4822
+ const auth = await requireAuth(request);
4823
+ if (auth.error)
4824
+ return auth.error;
4825
+ const roleErr = requireRole(auth.session.role, WRITE_ROLES);
4826
+ if (roleErr)
4827
+ return roleErr;
4828
+ const form = await getFormByIdService(params.id);
4829
+ if (!form)
4830
+ return errorResponse('Form not found', 404);
4831
+ const settings = await getFormNotificationSettingsService(params.id);
4832
+ const primaryRecipient = (settings?.recipients ?? [])[0];
4833
+ if (!settings?.enabled || !primaryRecipient) {
4834
+ return errorResponse('Configure at least one recipient before sending a test', 400);
4835
+ }
4836
+ // Sample values so operators see a realistic email. The autoresponder is
4837
+ // suppressed for tests so we never email an arbitrary sample address.
4838
+ const fields = form.fields ?? [];
4839
+ const sampleData = {};
4840
+ for (const field of fields) {
4841
+ if (field.type === 'honeypot' || field.type === 'divider' || field.type === 'richtext') {
4842
+ continue;
4843
+ }
4844
+ if (field.type === 'email')
4845
+ sampleData[field.key] = primaryRecipient;
4846
+ else if (field.type === 'checkbox' || field.type === 'consent')
4847
+ sampleData[field.key] = true;
4848
+ else
4849
+ sampleData[field.key] = `Sample ${field.label || field.key}`;
4850
+ }
4851
+ const ctx = {
4852
+ formId: form.id,
4853
+ formName: form.name,
4854
+ entryNumber: null,
4855
+ submittedAt: new Date().toISOString(),
4856
+ fields,
4857
+ data: sampleData,
4858
+ senderName: 'Test Submission',
4859
+ senderEmail: primaryRecipient,
4860
+ };
4861
+ const result = await sendFormNotifications({ ...settings, autoresponderEnabled: false }, ctx);
4862
+ return json({ data: result });
4863
+ }
4864
+ catch (err) {
4865
+ return mapFormError(err) ?? internalError(err);
4866
+ }
4867
+ });
4723
4868
  // ---------------------------------------------------------------------------
4724
4869
  // Redirects routes
4725
4870
  // ---------------------------------------------------------------------------