@nextlyhq/plugin-form-builder 0.0.2-alpha.65 → 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.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as nextly from 'nextly';
2
2
  import { CollectionConfig, PluginContext } from 'nextly';
3
3
  import { PluginDefinition } from '@nextlyhq/plugin-sdk';
4
- import { R as ResolvedFormBuilderConfig, F as FormBuilderPluginOptions, a as FormConfig, b as FormSettings, c as FormNotifications, C as CheckboxFormField, D as DateFormField, E as EmailFormField, d as FileFormField, H as HiddenFormField, N as NumberFormField, P as PhoneFormField, e as RadioFormField, S as SelectFormField, T as TextFormField, f as TextareaFormField, g as TimeFormField, U as UrlFormField, h as ConditionalLogic, A as AnyFormField, i as SubmissionDocument, j as FormDocument, W as WebhookEvent } from './types-hLfLe81K.js';
5
- export { B as BUILT_IN_FORM_FIELD_TYPES, k as BaseFormField, l as ConditionalLogicCondition, m as CustomFormField, n as EmailConfig, o as FieldBlockConfig, p as FormAccess, q as FormField, r as FormFieldType, s as FormFieldTypeId, t as FormHookContext, u as FormHooks, v as FormNotification, w as FormNotificationItem, x as FormSubmission, V as ValidationRules, y as WebhookConfig, z as isCheckboxFormField, G as isEmailFormField, I as isFileFormField, J as isFormField, K as isFormFieldType, L as isKnownFormField, M as isSelectFormField, O as isTextFormField } from './types-hLfLe81K.js';
4
+ import { R as ResolvedFormBuilderConfig, F as FormBuilderPluginOptions, a as FormConfig, b as FormSettings, c as FormNotifications, C as CheckboxFormField, D as DateFormField, E as EmailFormField, d as FileFormField, H as HiddenFormField, N as NumberFormField, P as PhoneFormField, e as RadioFormField, S as SelectFormField, T as TextFormField, f as TextareaFormField, g as TimeFormField, U as UrlFormField, h as ConditionalLogic, A as AnyFormField, i as SubmissionDocument, j as FormDocument } from './types-Bo4lk9BD.js';
5
+ export { B as BUILT_IN_FORM_FIELD_TYPES, k as BaseFormField, l as ConditionalLogicCondition, m as CustomFormField, n as EmailConfig, o as FieldBlockConfig, p as FormAccess, q as FormField, r as FormFieldType, s as FormFieldTypeId, t as FormHookContext, u as FormHooks, v as FormNotification, w as FormNotificationItem, x as FormSubmission, V as ValidationRules, y as isCheckboxFormField, z as isEmailFormField, G as isFileFormField, I as isFormField, J as isFormFieldType, K as isKnownFormField, L as isSelectFormField, M as isTextFormField } from './types-Bo4lk9BD.js';
6
6
  import { z } from 'zod';
7
7
  import 'react';
8
8
 
@@ -936,6 +936,20 @@ declare function validateFormData(data: Record<string, unknown>, fields: AnyForm
936
936
  * @param result - Zod safe parse result
937
937
  * @returns Object mapping field names to error messages
938
938
  */
939
+ /** A field that failed, with the machine code a wire error reports. */
940
+ interface ValidationIssue {
941
+ path: string;
942
+ /** `REQUIRED` for an answer that is missing or empty, else `INVALID`. */
943
+ code: "REQUIRED" | "INVALID";
944
+ message: string;
945
+ }
946
+ /**
947
+ * The same failures as a field-to-message map.
948
+ *
949
+ * Derived from {@link getValidationIssues} rather than walking the issues a
950
+ * second time, so the two cannot disagree about which message belongs to a
951
+ * field or which failure won when one field had several.
952
+ */
939
953
  declare function getValidationErrors(result: z.ZodSafeParseResult<unknown>): Record<string, string>;
940
954
 
941
955
  /**
@@ -1166,21 +1180,126 @@ interface SubmitFormOptions {
1166
1180
  /** Submitter's user agent string */
1167
1181
  userAgent?: string;
1168
1182
  };
1183
+ /**
1184
+ * The HTTP request this submission arrived on.
1185
+ *
1186
+ * A route calling this helper is its own route, outside everything Nextly
1187
+ * pins a request around, so nothing else can supply it. Without it the write
1188
+ * reaches the submissions hooks looking like a seed or a job, and every rule
1189
+ * scoped to a visitor stands down: the rate limit above all.
1190
+ *
1191
+ * The request rather than an address, because the core resolves the client
1192
+ * from it under this deployment's proxy-trust settings. `metadata.ipAddress`
1193
+ * is a string the caller chose, which is the right shape for the audit column
1194
+ * it fills and the wrong shape for deciding who to throttle.
1195
+ */
1196
+ request?: Request;
1197
+ /**
1198
+ * Whose permissions the reads and the write are judged against.
1199
+ *
1200
+ * Defaults to `system`, so every existing caller behaves as before.
1201
+ */
1202
+ access?: SubmissionAccess;
1203
+ /**
1204
+ * The language the visitor submitted in.
1205
+ *
1206
+ * Decides which translation a picked redirect page is read in, and so which
1207
+ * URL the visitor is sent to: a page whose slug is `thanks` in English and
1208
+ * `merci` in French answered `/thanks` to everyone, because the target was
1209
+ * read with no locale and the default answered. The built-in route takes it
1210
+ * from `?locale=` on the submit URL, the same way core's own routes take a
1211
+ * locale on the wire; a host route passes whatever it resolved for the page
1212
+ * the form was on.
1213
+ *
1214
+ * A code that is not a configured locale reads as the default rather than
1215
+ * failing the submission, which the collection services decide. Absent means
1216
+ * the default, as it always has. The read wildcards (`all`, `*`) are not a
1217
+ * language a visitor can have submitted in and are treated as absent: a read
1218
+ * in "every language" answers a localized URL field with one value per
1219
+ * language, which no URL pattern can be filled from.
1220
+ */
1221
+ locale?: string;
1169
1222
  }
1223
+ /**
1224
+ * How this submission reaches the collections it touches.
1225
+ *
1226
+ * `system` is the default and is right for host code: the caller already
1227
+ * decided, and there is no visitor whose permissions could be consulted.
1228
+ *
1229
+ * `public` is for a route serving an anonymous visitor. It keeps the
1230
+ * collection's configured `access` rules in force, so a host that closed
1231
+ * submissions or restricted reading forms is obeyed rather than bypassed. The
1232
+ * former core endpoint ran this way, and elevating a public route to `system`
1233
+ * silently overrode that configuration.
1234
+ */
1235
+ type SubmissionAccess = {
1236
+ as: "system";
1237
+ } | {
1238
+ as: "public";
1239
+ };
1240
+ /**
1241
+ * Why a submission ended the way it did.
1242
+ *
1243
+ * `success` alone cannot carry this. An HTTP route has to answer 404 for a slug
1244
+ * nobody used, 409 for a form its author closed and 400 for data that did not
1245
+ * validate, and a boolean plus a human sentence cannot tell those apart without
1246
+ * matching on the sentence.
1247
+ *
1248
+ * `accepted` covers every ending a visitor is told succeeded, INCLUDING the two
1249
+ * that are deliberately indistinguishable from success: a submission the
1250
+ * honeypot flagged, and one the rate limiter refused. A caller must not try to
1251
+ * separate them, which is why they do not appear here as their own names.
1252
+ */
1253
+ type SubmitFormOutcome = "accepted" | "no-such-form" | "closed" | "invalid" | "duplicate" | "failed";
1170
1254
  /**
1171
1255
  * Result of form submission.
1172
1256
  */
1173
1257
  interface SubmitFormResult {
1174
1258
  /** Whether the submission was successful */
1175
1259
  success: boolean;
1260
+ /**
1261
+ * Which ending this was, for a caller that has to map it to a status code.
1262
+ *
1263
+ * Required rather than optional, so a return path added later cannot omit it
1264
+ * and leave the HTTP route guessing from the message text.
1265
+ */
1266
+ outcome: SubmitFormOutcome;
1176
1267
  /** The created submission document (on success) */
1177
1268
  submission?: SubmissionDocument;
1178
1269
  /** Error message (on failure) */
1179
1270
  error?: string;
1180
1271
  /** Field-level validation errors */
1181
1272
  validationErrors?: Record<string, string>;
1273
+ /**
1274
+ * The same failures with their machine codes, for a caller building an HTTP
1275
+ * error. `validationErrors` above cannot carry one, so a route reading only
1276
+ * that has to invent a code and reports every blank answer as `INVALID`.
1277
+ */
1278
+ validationIssues?: ValidationIssue[];
1182
1279
  /** Redirect URL (if form configured for redirect on success) */
1183
1280
  redirect?: string;
1281
+ /**
1282
+ * The error that ended a `failed` submission, kept rather than flattened.
1283
+ *
1284
+ * A collection can refuse a write for reasons its host chose: a
1285
+ * `beforeValidate` hook throwing `NextlyError.validation`, a uniqueness
1286
+ * conflict. Those carry their own status and field detail, and reporting them
1287
+ * all as one internal error tells a visitor the server broke when in fact
1288
+ * their submission was answered deliberately.
1289
+ */
1290
+ cause?: unknown;
1291
+ /**
1292
+ * The toast the form's author wrote for a successful submission.
1293
+ *
1294
+ * Carried on the result rather than read from the form again by whoever
1295
+ * presents it: the HTTP route would otherwise have to fetch the form a second
1296
+ * time purely to learn a string this function already had in hand, and a
1297
+ * second read is a second chance to disagree about which form was submitted.
1298
+ *
1299
+ * Present on every `accepted` ending, including the ones a bot caused, so the
1300
+ * message cannot be used to tell an accepted submission from a flagged one.
1301
+ */
1302
+ successMessage?: string;
1184
1303
  }
1185
1304
  /**
1186
1305
  * Context for the submission handler.
@@ -1289,8 +1408,16 @@ declare function getFormSubmissionStats(formSlug: string, context: SubmitFormCon
1289
1408
  /**
1290
1409
  * Spam Detection
1291
1410
  *
1292
- * Implements honeypot and rate limiting spam protection for form submissions.
1293
- * Provides a pluggable approach to spam detection that can be extended.
1411
+ * Honeypot and rate limiting for form submissions.
1412
+ *
1413
+ * The honeypot is a pure read of the submitted payload. The rate limit is not:
1414
+ * it counts, and where the count lives decides whether the limit means
1415
+ * anything. It lives in the deployment's own {@link RateLimitStore}, the one
1416
+ * the REST and auth limiters already share, so `rateLimit.store` is configured
1417
+ * once and all three agree about what they are counting. A store private to
1418
+ * this module would count per process, and on a deployment that spans several
1419
+ * the effective limit becomes `configured x instances` -- a number the operator
1420
+ * never chose, that fails open, and only under load.
1294
1421
  *
1295
1422
  * @module handlers/spam-detection
1296
1423
  * @since 0.1.0
@@ -1376,184 +1503,6 @@ interface SpamCheckResult {
1376
1503
  * ```
1377
1504
  */
1378
1505
  declare function checkSpam(options: SpamCheckOptions): Promise<SpamCheckResult>;
1379
- /**
1380
- * Clean up expired rate limit entries.
1381
- *
1382
- * Call this periodically (e.g., every 5 minutes) to prevent
1383
- * memory leaks from accumulated rate limit entries.
1384
- *
1385
- * @param maxAgeMs - Maximum age of entries to keep (default: 5 minutes)
1386
- * @returns Number of entries removed
1387
- *
1388
- * @example
1389
- * ```typescript
1390
- * // Set up periodic cleanup
1391
- * setInterval(() => {
1392
- * const removed = cleanupRateLimitStore();
1393
- * console.log(`Cleaned up ${removed} expired rate limit entries`);
1394
- * }, 5 * 60 * 1000); // Every 5 minutes
1395
- * ```
1396
- */
1397
- declare function cleanupRateLimitStore(maxAgeMs?: number): number;
1398
- /**
1399
- * Get the current size of the rate limit store.
1400
- *
1401
- * Useful for monitoring and debugging.
1402
- *
1403
- * @returns Number of entries in the store
1404
- */
1405
- declare function getRateLimitStoreSize(): number;
1406
- /**
1407
- * Clear all rate limit entries.
1408
- *
1409
- * Useful for testing or resetting state.
1410
- */
1411
- declare function clearRateLimitStore(): void;
1412
- /**
1413
- * Check if a specific IP/form is currently rate limited.
1414
- *
1415
- * @param ipAddress - IP address to check
1416
- * @param formSlug - Form slug
1417
- * @param config - Rate limit configuration
1418
- * @returns Whether the IP is currently rate limited
1419
- */
1420
- declare function isRateLimited(ipAddress: string, formSlug: string, config: {
1421
- maxSubmissions: number;
1422
- windowMs: number;
1423
- }): boolean;
1424
-
1425
- /**
1426
- * Webhook Handler
1427
- *
1428
- * Triggers webhook notifications for form events.
1429
- * Webhooks are fired asynchronously and do not block form submission.
1430
- *
1431
- * @module handlers/webhooks
1432
- * @since 0.1.0
1433
- */
1434
-
1435
- /**
1436
- * Webhook payload sent to configured endpoints.
1437
- */
1438
- interface WebhookPayload {
1439
- /** Event type that triggered the webhook */
1440
- event: WebhookEvent;
1441
- /** ISO 8601 timestamp when the event occurred */
1442
- timestamp: string;
1443
- /** Form information */
1444
- form: {
1445
- id: string;
1446
- slug: string;
1447
- name: string;
1448
- };
1449
- /** Submission information (full data or minimal based on config) */
1450
- submission: SubmissionDocument | {
1451
- id: string;
1452
- submittedAt: Date;
1453
- };
1454
- }
1455
- /**
1456
- * Options for triggering webhooks.
1457
- */
1458
- interface TriggerWebhooksOptions {
1459
- /** Event type */
1460
- event: WebhookEvent;
1461
- /** Form document */
1462
- form: FormDocument;
1463
- /** Submission document */
1464
- submission: SubmissionDocument;
1465
- }
1466
- /**
1467
- * Result of a single webhook delivery attempt.
1468
- */
1469
- interface WebhookDeliveryResult {
1470
- /** Webhook URL */
1471
- url: string;
1472
- /** Whether the delivery was successful */
1473
- success: boolean;
1474
- /** HTTP status code (if request completed) */
1475
- statusCode?: number;
1476
- /** Error message (if delivery failed) */
1477
- error?: string;
1478
- /** Time taken in milliseconds */
1479
- durationMs: number;
1480
- }
1481
- /**
1482
- * Result of triggering webhooks for an event.
1483
- */
1484
- interface TriggerWebhooksResult {
1485
- /** Number of webhooks triggered */
1486
- triggered: number;
1487
- /** Number of successful deliveries */
1488
- successful: number;
1489
- /** Number of failed deliveries */
1490
- failed: number;
1491
- /** Individual delivery results */
1492
- results: WebhookDeliveryResult[];
1493
- }
1494
- /**
1495
- * Trigger webhooks for a form event.
1496
- *
1497
- * This function fires all matching webhooks asynchronously and does not
1498
- * block form submission. Failed webhooks are logged but do not cause
1499
- * submission failures.
1500
- *
1501
- * @param options - Trigger options
1502
- * @returns Result of webhook triggering
1503
- *
1504
- * @example
1505
- * ```typescript
1506
- * // After creating a submission
1507
- * const result = await triggerWebhooks({
1508
- * event: 'submission.created',
1509
- * form,
1510
- * submission,
1511
- * });
1512
- *
1513
- * console.log(`Triggered ${result.triggered} webhooks`);
1514
- * console.log(`Successful: ${result.successful}, Failed: ${result.failed}`);
1515
- * ```
1516
- */
1517
- declare function triggerWebhooks(options: TriggerWebhooksOptions): Promise<TriggerWebhooksResult>;
1518
- /**
1519
- * Trigger webhooks without waiting for completion.
1520
- *
1521
- * This is the recommended way to call webhooks from submission handlers
1522
- * as it doesn't block the response. Errors are logged but not thrown.
1523
- *
1524
- * @param options - Trigger options
1525
- * @param logger - Optional logger for error reporting
1526
- *
1527
- * @example
1528
- * ```typescript
1529
- * // In submission handler - fire and forget
1530
- * fireWebhooks(
1531
- * { event: 'submission.created', form, submission },
1532
- * pluginContext.logger
1533
- * );
1534
- *
1535
- * // Continue with response immediately
1536
- * return { success: true, submission };
1537
- * ```
1538
- */
1539
- declare function fireWebhooks(options: TriggerWebhooksOptions, logger?: {
1540
- info?: (message: string, data?: Record<string, unknown>) => void;
1541
- warn?: (message: string, data?: Record<string, unknown>) => void;
1542
- error?: (message: string, data?: Record<string, unknown>) => void;
1543
- }): void;
1544
- /**
1545
- * Validate a webhook URL.
1546
- *
1547
- * @param url - URL to validate
1548
- * @returns Whether the URL is valid for webhooks
1549
- */
1550
- declare function isValidWebhookUrl(url: string): boolean;
1551
- /**
1552
- * Get the list of supported webhook events.
1553
- *
1554
- * @returns Array of supported event types
1555
- */
1556
- declare function getSupportedWebhookEvents(): WebhookEvent[];
1557
1506
 
1558
1507
  /**
1559
1508
  * Forms Collection
@@ -1646,4 +1595,4 @@ declare function submissionsCollection(pluginConfig: ResolvedFormBuilderConfig):
1646
1595
 
1647
1596
  declare const formBuilderPlugin: nextly.PluginDefinition;
1648
1597
 
1649
- export { AnyFormField, type CSVExportOptions, CheckboxFormField, type ComparisonOperator, ConditionalLogic, DEFAULT_FORM_SETTINGS, DEFAULT_NOTIFICATION_SETTINGS, DateFormField, EmailFormField, type ExportedJSON, FileFormField, FormBuilderPluginOptions, type FormBuilderPluginResult, FormConfig, FormDocument, type FormFieldOption, FormNotifications, FormSettings, type FormValidationError, type FormValidationErrorCode, type FormValidationResult, HiddenFormField, type JSONExportOptions, type NextlyPlugin, NumberFormField, PhoneFormField, RESERVED_FORM_SLUGS, RadioFormField, ResolvedFormBuilderConfig, SelectFormField, type SpamCheckConfig, type SpamCheckOptions, type SpamCheckResult, SubmissionDocument, type SubmitFormContext, type SubmitFormOptions, type SubmitFormResult, TextFormField, TextareaFormField, TimeFormField, type TriggerWebhooksOptions, type TriggerWebhooksResult, UrlFormField, type WebhookDeliveryResult, WebhookEvent, type WebhookPayload, applyFieldDefaults, applyFormDefaults, assertValidFormConfig, checkSpam, checkbox, cleanupRateLimitStore, clearRateLimitStore, createFormConfig, date, downloadFile, email, evaluateConditions, exportAndDownload, exportToCSV, exportToJSON, file, fireWebhooks, formBuilder, formBuilderPlugin, formatExportValue, formsCollection, generateExportFilename, generateZodSchema, getFormBuilderConfig, getFormSubmissionStats, getRateLimitStoreSize, getSupportedComparisonOperators, getSupportedWebhookEvents, getValidationErrors, hidden, isFormAcceptingSubmissions, isRateLimited, isValidComparisonOperator, isValidWebhookUrl, number, option, phone, pluralize, radio, select, submissionsCollection, submitForm, text, textarea, time, toTitleCase, transformFormData, triggerWebhooks, url, validateFormConfig, validateFormData, validateSubmission };
1598
+ export { AnyFormField, type CSVExportOptions, CheckboxFormField, type ComparisonOperator, ConditionalLogic, DEFAULT_FORM_SETTINGS, DEFAULT_NOTIFICATION_SETTINGS, DateFormField, EmailFormField, type ExportedJSON, FileFormField, FormBuilderPluginOptions, type FormBuilderPluginResult, FormConfig, FormDocument, type FormFieldOption, FormNotifications, FormSettings, type FormValidationError, type FormValidationErrorCode, type FormValidationResult, HiddenFormField, type JSONExportOptions, type NextlyPlugin, NumberFormField, PhoneFormField, RESERVED_FORM_SLUGS, RadioFormField, ResolvedFormBuilderConfig, SelectFormField, type SpamCheckConfig, type SpamCheckOptions, type SpamCheckResult, SubmissionDocument, type SubmitFormContext, type SubmitFormOptions, type SubmitFormResult, TextFormField, TextareaFormField, TimeFormField, UrlFormField, applyFieldDefaults, applyFormDefaults, assertValidFormConfig, checkSpam, checkbox, createFormConfig, date, downloadFile, email, evaluateConditions, exportAndDownload, exportToCSV, exportToJSON, file, formBuilder, formBuilderPlugin, formatExportValue, formsCollection, generateExportFilename, generateZodSchema, getFormBuilderConfig, getFormSubmissionStats, getSupportedComparisonOperators, getValidationErrors, hidden, isFormAcceptingSubmissions, isValidComparisonOperator, number, option, phone, pluralize, radio, select, submissionsCollection, submitForm, text, textarea, time, toTitleCase, transformFormData, url, validateFormConfig, validateFormData, validateSubmission };