@nextlyhq/plugin-form-builder 0.0.2-alpha.66 → 0.0.2-alpha.67

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/dist/index.js CHANGED
@@ -30,8 +30,9 @@ import {
30
30
  parseRedirectReference,
31
31
  pickedDocumentField,
32
32
  pluralize,
33
- toTitleCase
34
- } from "./chunk-VIGKSRSH.js";
33
+ toTitleCase,
34
+ warnIfWebhooksConfigured
35
+ } from "./chunk-X5WLJLAR.js";
35
36
 
36
37
  // src/plugin.ts
37
38
  import { createRequire } from "module";
@@ -74,16 +75,13 @@ function applyRedirectPattern(pattern, document) {
74
75
  }
75
76
 
76
77
  // src/collections/access-defaults.ts
77
- var authenticated = ({ user }) => !!user;
78
- var administrative = ({ roles }) => roles.includes("admin") || roles.includes("super-admin");
78
+ var permitted = ({ permissions, collection, operation }) => permissions.includes(`${collection}:${operation}`);
79
79
  function accessWithDefaults(overrides, defaults = {}) {
80
80
  return {
81
- read: overrides?.read ?? defaults.read ?? authenticated,
82
- create: overrides?.create ?? defaults.create ?? authenticated,
83
- update: overrides?.update ?? authenticated,
84
- // Deleting is administrative in both collections; the DB permission layer
85
- // remains the backstop underneath it.
86
- delete: overrides?.delete ?? administrative
81
+ read: overrides?.read ?? defaults.read ?? permitted,
82
+ create: overrides?.create ?? defaults.create ?? permitted,
83
+ update: overrides?.update ?? permitted,
84
+ delete: overrides?.delete ?? permitted
87
85
  };
88
86
  }
89
87
 
@@ -787,7 +785,8 @@ function submissionsCollection(pluginConfig) {
787
785
  ...overrides.admin
788
786
  },
789
787
  // Access control with sensible defaults
790
- // Public can create (submit forms), but only authenticated users can read/update/delete
788
+ // Public can create (submit forms); reading, updating and deleting need the
789
+ // matching permission on the caller's role (`read-form-submissions`, ...).
791
790
  // `create: true` — a visitor submits without an account. Reading, updating
792
791
  // and deleting are the shared defaults.
793
792
  access: accessWithDefaults(accessOverrides, { create: true }),
@@ -2979,6 +2978,7 @@ function validateAccess(access, errors) {
2979
2978
  }
2980
2979
  function validateFormConfig(config) {
2981
2980
  const errors = [];
2981
+ warnIfWebhooksConfigured(config);
2982
2982
  validateSlug(config.slug, errors);
2983
2983
  validateFields(config.fields, errors);
2984
2984
  validateSettings(config.settings, errors);
@@ -3057,184 +3057,6 @@ var hidden = (config) => ({
3057
3057
  type: "hidden"
3058
3058
  });
3059
3059
 
3060
- // src/handlers/webhooks.ts
3061
- import { safeFetch, ExternalUrlError } from "nextly";
3062
- var WEBHOOK_TIMEOUT_MS = 1e4;
3063
- var SIGNATURE_HEADER = "X-Webhook-Signature";
3064
- var EVENT_HEADER = "X-Webhook-Event";
3065
- var DELIVERY_ID_HEADER = "X-Webhook-Delivery-ID";
3066
- async function generateSignature(payload, secret) {
3067
- const encoder = new TextEncoder();
3068
- const keyData = encoder.encode(secret);
3069
- const messageData = encoder.encode(payload);
3070
- const cryptoKey = await crypto.subtle.importKey(
3071
- "raw",
3072
- keyData,
3073
- { name: "HMAC", hash: "SHA-256" },
3074
- false,
3075
- ["sign"]
3076
- );
3077
- const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
3078
- const hashArray = Array.from(new Uint8Array(signature));
3079
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
3080
- return `sha256=${hashHex}`;
3081
- }
3082
- async function sendWebhook(webhook, payload) {
3083
- const startTime = Date.now();
3084
- const payloadString = JSON.stringify(payload);
3085
- try {
3086
- const headers = {
3087
- "Content-Type": "application/json",
3088
- [EVENT_HEADER]: payload.event,
3089
- [DELIVERY_ID_HEADER]: payload.submission.id,
3090
- ...webhook.headers
3091
- };
3092
- if (webhook.secret) {
3093
- headers[SIGNATURE_HEADER] = await generateSignature(
3094
- payloadString,
3095
- webhook.secret
3096
- );
3097
- }
3098
- const controller = new AbortController();
3099
- const timeoutId = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);
3100
- try {
3101
- const response = await safeFetch(webhook.url, {
3102
- method: webhook.method || "POST",
3103
- headers,
3104
- body: payloadString,
3105
- signal: controller.signal,
3106
- allowLocalhost: process.env.NODE_ENV !== "production"
3107
- });
3108
- clearTimeout(timeoutId);
3109
- const durationMs = Date.now() - startTime;
3110
- if (response.ok) {
3111
- return {
3112
- url: webhook.url,
3113
- success: true,
3114
- statusCode: response.status,
3115
- durationMs
3116
- };
3117
- }
3118
- return {
3119
- url: webhook.url,
3120
- success: false,
3121
- statusCode: response.status,
3122
- error: `HTTP ${response.status}: ${response.statusText}`,
3123
- durationMs
3124
- };
3125
- } finally {
3126
- clearTimeout(timeoutId);
3127
- }
3128
- } catch (error) {
3129
- const durationMs = Date.now() - startTime;
3130
- if (error instanceof Error && error.name === "AbortError") {
3131
- return {
3132
- url: webhook.url,
3133
- success: false,
3134
- error: `Timeout after ${WEBHOOK_TIMEOUT_MS}ms`,
3135
- durationMs
3136
- };
3137
- }
3138
- if (error instanceof ExternalUrlError) {
3139
- return {
3140
- url: webhook.url,
3141
- success: false,
3142
- error: `Webhook URL rejected for SSRF safety: ${error.message}`,
3143
- durationMs
3144
- };
3145
- }
3146
- return {
3147
- url: webhook.url,
3148
- success: false,
3149
- error: error instanceof Error ? error.message : String(error),
3150
- durationMs
3151
- };
3152
- }
3153
- }
3154
- async function triggerWebhooks(options) {
3155
- const { event, form, submission } = options;
3156
- const webhooks = form.webhooks || [];
3157
- const relevantWebhooks = webhooks.filter((w) => w.events.includes(event));
3158
- if (relevantWebhooks.length === 0) {
3159
- return {
3160
- triggered: 0,
3161
- successful: 0,
3162
- failed: 0,
3163
- results: []
3164
- };
3165
- }
3166
- const deliveryPromises = relevantWebhooks.map(async (webhook) => {
3167
- const payload = {
3168
- event,
3169
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3170
- form: {
3171
- id: form.id,
3172
- slug: form.slug,
3173
- name: form.name
3174
- },
3175
- submission: webhook.includeData !== false ? submission : {
3176
- id: submission.id,
3177
- submittedAt: submission.submittedAt
3178
- }
3179
- };
3180
- return sendWebhook(webhook, payload);
3181
- });
3182
- const results = await Promise.all(deliveryPromises);
3183
- const successful = results.filter((r) => r.success).length;
3184
- const failed = results.filter((r) => !r.success).length;
3185
- return {
3186
- triggered: relevantWebhooks.length,
3187
- successful,
3188
- failed,
3189
- results
3190
- };
3191
- }
3192
- function fireWebhooks(options, logger) {
3193
- triggerWebhooks(options).then((result) => {
3194
- if (result.triggered > 0) {
3195
- if (result.failed > 0) {
3196
- logger?.warn?.("Some webhooks failed", {
3197
- event: options.event,
3198
- formSlug: options.form.slug,
3199
- submissionId: options.submission.id,
3200
- triggered: result.triggered,
3201
- successful: result.successful,
3202
- failed: result.failed,
3203
- errors: result.results.filter((r) => !r.success).map((r) => ({ url: r.url, error: r.error }))
3204
- });
3205
- } else {
3206
- logger?.info?.("Webhooks triggered successfully", {
3207
- event: options.event,
3208
- formSlug: options.form.slug,
3209
- submissionId: options.submission.id,
3210
- count: result.triggered
3211
- });
3212
- }
3213
- }
3214
- }).catch((error) => {
3215
- logger?.error?.("Unexpected error triggering webhooks", {
3216
- event: options.event,
3217
- formSlug: options.form.slug,
3218
- submissionId: options.submission.id,
3219
- error: error instanceof Error ? error.message : String(error)
3220
- });
3221
- });
3222
- }
3223
- function isValidWebhookUrl(url2) {
3224
- try {
3225
- const parsed = new URL(url2);
3226
- if (parsed.protocol !== "https:" && !parsed.hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
3227
- return false;
3228
- }
3229
- return true;
3230
- } catch {
3231
- return false;
3232
- }
3233
- }
3234
- function getSupportedWebhookEvents() {
3235
- return ["submission.created", "submission.updated", "submission.deleted"];
3236
- }
3237
-
3238
3060
  // src/index.ts
3239
3061
  var formBuilderPlugin = formBuilder().plugin;
3240
3062
  export {
@@ -3256,7 +3078,6 @@ export {
3256
3078
  exportToCSV,
3257
3079
  exportToJSON,
3258
3080
  file,
3259
- fireWebhooks,
3260
3081
  formBuilder,
3261
3082
  formBuilderPlugin,
3262
3083
  formatExportValue,
@@ -3266,7 +3087,6 @@ export {
3266
3087
  getFormBuilderConfig,
3267
3088
  getFormSubmissionStats,
3268
3089
  getSupportedComparisonOperators,
3269
- getSupportedWebhookEvents,
3270
3090
  getValidationErrors,
3271
3091
  hidden,
3272
3092
  isCheckboxFormField,
@@ -3279,7 +3099,6 @@ export {
3279
3099
  isSelectFormField,
3280
3100
  isTextFormField,
3281
3101
  isValidComparisonOperator,
3282
- isValidWebhookUrl,
3283
3102
  number,
3284
3103
  option,
3285
3104
  phone,
@@ -3293,7 +3112,6 @@ export {
3293
3112
  time,
3294
3113
  toTitleCase,
3295
3114
  transformFormData,
3296
- triggerWebhooks,
3297
3115
  url,
3298
3116
  validateFormConfig,
3299
3117
  validateFormData,