@frigear-nu/forms 0.0.5 → 0.0.7

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 (185) hide show
  1. package/README.md +16 -1
  2. package/dist/module.d.mts +148 -8
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +64 -35
  5. package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
  6. package/dist/runtime/app/components/FormBuilder.vue +220 -0
  7. package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
  8. package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
  9. package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
  10. package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
  11. package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
  12. package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
  13. package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
  14. package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
  15. package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
  16. package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
  17. package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
  18. package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
  19. package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
  20. package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
  21. package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
  22. package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
  23. package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
  24. package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
  25. package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
  26. package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
  27. package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
  28. package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
  29. package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
  30. package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
  31. package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
  32. package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
  33. package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
  34. package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
  35. package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
  36. package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
  37. package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
  38. package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
  39. package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
  40. package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
  41. package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
  42. package/dist/runtime/app/components/FormBuilderText.vue +127 -0
  43. package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
  44. package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
  45. package/dist/runtime/app/components/FormCaptcha.vue +95 -0
  46. package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
  47. package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
  48. package/dist/runtime/app/components/FormEmbed.vue +113 -0
  49. package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
  50. package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
  51. package/dist/runtime/app/components/FormField.vue +35 -3
  52. package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
  53. package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
  54. package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
  55. package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
  56. package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
  57. package/dist/runtime/app/components/FormPreview.vue +244 -0
  58. package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
  59. package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
  60. package/dist/runtime/app/components/FormRenderer.vue +16 -7
  61. package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
  62. package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
  63. package/dist/runtime/app/composables/index.d.ts +4 -1
  64. package/dist/runtime/app/composables/index.js +4 -1
  65. package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
  66. package/dist/runtime/app/composables/useCaptcha.js +39 -0
  67. package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
  68. package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
  69. package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
  70. package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
  71. package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
  72. package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
  73. package/dist/runtime/app/composables/useForms.d.ts +18 -3
  74. package/dist/runtime/app/composables/useForms.js +38 -5
  75. package/dist/runtime/app/pages/admin-form.vue +112 -69
  76. package/dist/runtime/app/pages/admin-submission.vue +7 -16
  77. package/dist/runtime/app/pages/admin-submissions.vue +41 -46
  78. package/dist/runtime/app/pages/admin.vue +128 -44
  79. package/dist/runtime/app/pages/form.vue +71 -5
  80. package/dist/runtime/i18n/lang/da.json +237 -3
  81. package/dist/runtime/i18n/lang/en.json +237 -3
  82. package/dist/runtime/menus.js +12 -0
  83. package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
  84. package/dist/runtime/server/db/schema.sqlite.js +33 -1
  85. package/dist/runtime/server/mcp/schemas.d.ts +9 -0
  86. package/dist/runtime/server/mcp/schemas.js +8 -0
  87. package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
  88. package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
  89. package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
  90. package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
  91. package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
  92. package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
  93. package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
  94. package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
  95. package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
  96. package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
  97. package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
  98. package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
  99. package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
  100. package/dist/runtime/server/mcp/tools/get-form.js +12 -0
  101. package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
  102. package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
  103. package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
  104. package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
  105. package/dist/runtime/server/plugins/email-delivery.js +16 -11
  106. package/dist/runtime/server/plugins/encryption.d.ts +2 -0
  107. package/dist/runtime/server/plugins/encryption.js +4 -0
  108. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  109. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  110. package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
  111. package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
  112. package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
  113. package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
  114. package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
  115. package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
  116. package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
  117. package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
  118. package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
  119. package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
  120. package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
  121. package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
  122. package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
  123. package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
  124. package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
  125. package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
  126. package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
  127. package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
  128. package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
  129. package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
  130. package/dist/runtime/server/routes/admin/form.get.js +5 -1
  131. package/dist/runtime/server/routes/admin/form.patch.js +3 -12
  132. package/dist/runtime/server/routes/admin/forms.post.js +1 -6
  133. package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
  134. package/dist/runtime/server/routes/public/access.get.js +3 -0
  135. package/dist/runtime/server/routes/public/form.get.js +3 -1
  136. package/dist/runtime/server/routes/public/submit.post.js +2 -2
  137. package/dist/runtime/server/utils/access.d.ts +27 -0
  138. package/dist/runtime/server/utils/access.js +63 -0
  139. package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
  140. package/dist/runtime/server/utils/auth.d.ts +7 -7
  141. package/dist/runtime/server/utils/auth.js +4 -4
  142. package/dist/runtime/server/utils/captcha.d.ts +10 -0
  143. package/dist/runtime/server/utils/captcha.js +25 -0
  144. package/dist/runtime/server/utils/crypto.d.ts +18 -0
  145. package/dist/runtime/server/utils/crypto.js +48 -0
  146. package/dist/runtime/server/utils/forms.d.ts +6 -1
  147. package/dist/runtime/server/utils/forms.js +28 -0
  148. package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
  149. package/dist/runtime/server/utils/mcp-access.js +8 -0
  150. package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
  151. package/dist/runtime/server/utils/rate-limit.js +19 -0
  152. package/dist/runtime/server/utils/render.d.ts +2 -11
  153. package/dist/runtime/server/utils/render.js +1 -26
  154. package/dist/runtime/server/utils/secrets.d.ts +28 -0
  155. package/dist/runtime/server/utils/secrets.js +60 -0
  156. package/dist/runtime/server/utils/submit.d.ts +12 -1
  157. package/dist/runtime/server/utils/submit.js +18 -4
  158. package/dist/runtime/utils/builder.d.ts +73 -0
  159. package/dist/runtime/utils/builder.js +220 -0
  160. package/dist/runtime/utils/captcha.d.ts +9 -0
  161. package/dist/runtime/utils/captcha.js +7 -0
  162. package/dist/runtime/utils/conditions.d.ts +3 -1
  163. package/dist/runtime/utils/conditions.js +4 -0
  164. package/dist/runtime/utils/definition.d.ts +116 -6
  165. package/dist/runtime/utils/definition.js +53 -4
  166. package/dist/runtime/utils/embed.d.ts +8 -0
  167. package/dist/runtime/utils/embed.js +7 -0
  168. package/dist/runtime/utils/index.d.ts +16 -3
  169. package/dist/runtime/utils/index.js +66 -3
  170. package/dist/runtime/utils/markdown.d.ts +1 -5
  171. package/dist/runtime/utils/markdown.js +1 -5
  172. package/dist/runtime/utils/phone.d.ts +29 -0
  173. package/dist/runtime/utils/phone.js +294 -0
  174. package/dist/runtime/utils/preview.d.ts +12 -0
  175. package/dist/runtime/utils/preview.js +17 -0
  176. package/dist/runtime/utils/schedule.d.ts +1 -0
  177. package/dist/runtime/utils/schedule.js +1 -0
  178. package/dist/runtime/utils/schema.d.ts +2 -0
  179. package/dist/runtime/utils/schema.js +12 -4
  180. package/dist/runtime/utils/template.d.ts +57 -0
  181. package/dist/runtime/utils/template.js +147 -0
  182. package/dist/runtime/utils/text.d.ts +0 -3
  183. package/dist/runtime/utils/text.js +0 -5
  184. package/dist/types.d.mts +5 -1
  185. package/package.json +14 -5
package/README.md CHANGED
@@ -4,7 +4,22 @@ Dynamic, database-stored forms for the frigear ecosystem. A form is a JSON defin
4
4
  with conditional fields), submissions are stored with a snapshot of the definition they were answered against, and
5
5
  delivery (email, webhooks, ...) hangs off the `forms:submitted` Nitro hook.
6
6
 
7
- Full documentation: `apps/docs/content/4.modules/26.forms.md`.
7
+ Captcha fields (Cloudflare Turnstile, reCAPTCHA, hCaptcha) need a site key (`NUXT_PUBLIC_FORMS_CAPTCHA_<PROVIDER>_SITE_KEY`)
8
+ and a secret key (`NUXT_FORMS_CAPTCHA_<PROVIDER>_SECRET_KEY`) with `<PROVIDER>` one of `TURNSTILE`, `RECAPTCHA`, `HCAPTCHA`;
9
+ `NUXT_PUBLIC_FORMS_CAPTCHA_DEFAULT_PROVIDER` picks the provider of a field that names none. A secret key can also be set
10
+ per form in the admin builder: it is stored encrypted (AES-256-GCM, key from `NUXT_FORMS_ENCRYPTION_KEY`, else
11
+ `NUXT_ENCRYPTION_KEY`, else the session password; changing the key makes stored secrets unreadable) and takes precedence
12
+ over the environment one. Without any secret key submissions fail closed.
13
+
14
+ Embed a published form in Markdown (Nuxt Content / MDC) with the globally registered `FormEmbed` component
15
+ (`slug`, `title`, `locale`, `class`; honors the form's access settings):
16
+
17
+ ```md
18
+ ::form-embed{slug="contact"}
19
+ ::
20
+ ```
21
+
22
+ Full documentation: `apps/docs/content/4.modules/07.forms.md`.
8
23
 
9
24
  ```ts
10
25
  export default defineNuxtConfig({
package/dist/module.d.mts CHANGED
@@ -1,20 +1,23 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { H3Event } from 'h3';
3
+ import { Schedule } from '@frigear-nu/module-kit/schedule';
4
+ export { Schedule as FormSchedule, ScheduleMoment as FormScheduleMoment } from '@frigear-nu/module-kit/schedule';
5
+ import { Translated } from '@frigear-nu/ui/translated';
6
+ export { Translated } from '@frigear-nu/ui/translated';
3
7
 
4
- /** A text that is either one string or a map of locale -> string. */
5
- type Translated = string | Record<string, string>;
6
8
  type FormStatus = 'draft' | 'published' | 'archived';
7
9
  type FormMode = 'single' | 'stepped';
8
10
  type FormStorage = 'db' | 'none';
9
11
  type DeliveryStatus = 'none' | 'pending' | 'delivered' | 'failed';
10
- type FieldType = 'text' | 'email' | 'tel' | 'number' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'date' | 'file' | 'markdown';
11
- type ConditionOp = 'eq' | 'neq' | 'in' | 'truthy';
12
+ type FieldType = 'text' | 'email' | 'tel' | 'number' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'date' | 'file' | 'markdown' | 'captcha';
13
+ type CaptchaProvider = 'turnstile' | 'recaptcha' | 'hcaptcha';
14
+ type ConditionOp = 'eq' | 'neq' | 'in' | 'empty' | 'notEmpty' | 'truthy';
12
15
  /** Shows a field (or step) only while the condition on another field's answer holds. */
13
16
  type Condition = {
14
17
  /** Key of the field whose answer is inspected. */
15
18
  field: string;
16
19
  op: ConditionOp;
17
- /** Compared value for `eq`/`neq`; a list of accepted values for `in`. Unused for `truthy`. */
20
+ /** Compared value for `eq`/`neq`; a list of accepted values for `in`. Unused for `empty`, `notEmpty` and `truthy` (an alias of `notEmpty`). */
18
21
  value?: unknown;
19
22
  };
20
23
  type FieldOption = {
@@ -37,6 +40,24 @@ type FieldValidation = {
37
40
  /** Replaces the default error message of every rule on this field. */
38
41
  message?: Translated;
39
42
  };
43
+ /** Settings of a `captcha` field. The visitor's response token lives in the form state only and is never stored. */
44
+ type CaptchaConfig = {
45
+ /** Defaults to `turnstile`. */
46
+ provider?: CaptchaProvider;
47
+ /** Public site key; overrides `runtimeConfig.public.forms.captcha.<provider>.siteKey` for this field. */
48
+ siteKey?: string;
49
+ theme?: 'auto' | 'light' | 'dark';
50
+ size?: 'normal' | 'compact';
51
+ };
52
+ /** Settings of a `tel` field. The answer is always one string; with a country selector it is `+<dial code><number>`, e.g. `+4512345678`. */
53
+ type PhoneConfig = {
54
+ /** `simple`: one free-form input (default). `country`: a country selector next to the national number, merged into one value. */
55
+ mode?: 'simple' | 'country';
56
+ /** ISO 3166-1 alpha-2 codes the visitor may choose from (`country` mode). Default: all countries. */
57
+ countries?: string[];
58
+ /** Preselected country (`country` mode). Defaults to `DK` when offered, else the first offered country. */
59
+ defaultCountry?: string;
60
+ };
40
61
  type FormField = {
41
62
  /** Unique across the whole form; the key of the answer. */
42
63
  key: string;
@@ -50,9 +71,13 @@ type FormField = {
50
71
  options?: FieldOption[];
51
72
  /** `file` fields only: allow more than one file. */
52
73
  multiple?: boolean;
53
- /** Browser `autocomplete` token, e.g. `email`. */
74
+ /** Browser `autocomplete` token, e.g. `email`. Defaults by type: `email` for email, `tel` for phone (`tel-national` next to a country selector). */
54
75
  autocomplete?: string;
55
76
  validation?: FieldValidation;
77
+ /** `tel` fields only. */
78
+ phone?: PhoneConfig;
79
+ /** `captcha` fields only. */
80
+ captcha?: CaptchaConfig;
56
81
  showIf?: Condition;
57
82
  };
58
83
  type FormStep = {
@@ -65,9 +90,22 @@ type FormStep = {
65
90
  };
66
91
  type FormDelivery = {
67
92
  email?: {
68
- /** Recipient addresses. */
93
+ /** Recipient addresses; used when no `routes` entry matches. */
69
94
  to: string[];
95
+ /** May use `{{variables}}` (see the forms docs). */
70
96
  subject?: Translated;
97
+ /**
98
+ * Custom plain-text body with `{{variables}}` (`{{answers}}`, `{{answer.<key>}}`, `{{form.name}}`, ...).
99
+ * Without it the email lists the answers.
100
+ */
101
+ body?: Translated;
102
+ /** Sends to different recipients depending on the answer of one field (e.g. a subject select). */
103
+ routes?: {
104
+ field: string;
105
+ to: Record<string, string[]>;
106
+ };
107
+ /** Key of an email field whose answer becomes the `Reply-To` of the delivered email. */
108
+ replyToField?: string;
71
109
  };
72
110
  };
73
111
  type FormSubmitSettings = {
@@ -80,6 +118,18 @@ type FormSubmitSettings = {
80
118
  keep: string[];
81
119
  };
82
120
  };
121
+ /** `web`: public page and API. `api`: API only, the public page does not exist. */
122
+ type FormAvailability = 'web' | 'api';
123
+ /** How a submitter proves access: nothing, a signed-in user, a bearer token or a shared password. */
124
+ type FormAuthMode = 'none' | 'user' | 'token' | 'password';
125
+
126
+ /** Who may read and submit the form, and when. Secrets (tokens, password hash) are never part of the definition. */
127
+ type FormAccess = {
128
+ availability: FormAvailability;
129
+ auth: FormAuthMode;
130
+ /** Defaults to `{ type: 'always' }`. */
131
+ schedule?: Schedule;
132
+ };
83
133
  type FormDefinition = {
84
134
  mode: FormMode;
85
135
  /** `none` never stores answers (or files): they are only emitted through `forms:submitted` and emailed. */
@@ -87,9 +137,12 @@ type FormDefinition = {
87
137
  steps: FormStep[];
88
138
  submit?: FormSubmitSettings;
89
139
  delivery?: FormDelivery;
140
+ /** Defaults to `{ availability: 'web', auth: 'none' }` (open to everyone, as forms always were). */
141
+ access?: FormAccess;
90
142
  };
91
143
  type ModuleOptions = {
92
144
  settings?: {
145
+ mcp?: boolean;
93
146
  pages?: {
94
147
  /** Public form page; must contain a `:slug` param. `true` = `/forms/:slug`. */
95
148
  form?: string | boolean;
@@ -115,6 +168,22 @@ type ModuleOptions = {
115
168
  enabled?: boolean;
116
169
  forms?: SeedForm[];
117
170
  };
171
+ /**
172
+ * Keys for `captcha` fields. Site keys are public, secret keys stay on the server. Both can be set per
173
+ * provider here or through `NUXT_PUBLIC_FORMS_CAPTCHA_<PROVIDER>_SITE_KEY` / `NUXT_FORMS_CAPTCHA_<PROVIDER>_SECRET_KEY`.
174
+ */
175
+ captcha?: Partial<Record<CaptchaProvider, {
176
+ siteKey?: string;
177
+ secretKey?: string;
178
+ }>> & {
179
+ /** Provider of a captcha field that does not name one. Defaults to `turnstile`; env `NUXT_PUBLIC_FORMS_CAPTCHA_DEFAULT_PROVIDER`. */
180
+ defaultProvider?: CaptchaProvider;
181
+ };
182
+ /**
183
+ * Key that encrypts per-form secrets in the database (env `NUXT_FORMS_ENCRYPTION_KEY`). Falls back to
184
+ * `NUXT_ENCRYPTION_KEY`, then to the session password. Changing it makes stored secrets unreadable.
185
+ */
186
+ encryptionKey?: string;
118
187
  };
119
188
  type SeedForm = {
120
189
  slug: string;
@@ -129,6 +198,10 @@ type FormsRuntimeConfig = {
129
198
  enabled: boolean;
130
199
  forms: SeedForm[];
131
200
  };
201
+ captcha: Record<CaptchaProvider, {
202
+ secretKey: string;
203
+ }>;
204
+ encryptionKey: string;
132
205
  };
133
206
  type PublicModuleOptions = {
134
207
  pages: {
@@ -138,6 +211,11 @@ type PublicModuleOptions = {
138
211
  adminSubmissions?: string;
139
212
  adminSubmission?: string;
140
213
  };
214
+ captcha: Record<CaptchaProvider, {
215
+ siteKey: string;
216
+ }> & {
217
+ defaultProvider: CaptchaProvider;
218
+ };
141
219
  };
142
220
  type AuthorizeContext = {
143
221
  event: H3Event;
@@ -172,6 +250,22 @@ type SubmittedContext = {
172
250
  /** Set by a handler to report that it delivered the submission (email delivery does). */
173
251
  delivered: boolean;
174
252
  };
253
+ type CaptchaVerifyContext = {
254
+ event: H3Event;
255
+ provider: CaptchaProvider;
256
+ token: string;
257
+ ip: string | undefined;
258
+ /** The form being submitted, when known. */
259
+ formId?: string;
260
+ /**
261
+ * The secret key the siteverify call would use: the form's own encrypted secret, else the environment/config
262
+ * one, else `undefined` (which fails closed). Server-side only, never sent anywhere but the provider.
263
+ */
264
+ secretKey?: string;
265
+ secretSource?: SecretSource;
266
+ /** Set by a handler to decide the outcome; left `undefined` the provider's siteverify endpoint is asked. */
267
+ valid: boolean | undefined;
268
+ };
175
269
  type DeliveredContext = {
176
270
  submissionId: string | null;
177
271
  formSlug: string;
@@ -186,6 +280,45 @@ type PublicFormDto = {
186
280
  /** The definition without `delivery` (recipients are never public). */
187
281
  definition: Omit<FormDefinition, 'delivery'>;
188
282
  };
283
+ type FormAccessTokenDto = {
284
+ id: string;
285
+ name: string;
286
+ /** First characters of the token, to recognise it; the token itself is shown once on creation. */
287
+ prefix: string;
288
+ createdAt: string;
289
+ lastUsedAt: string | null;
290
+ revokedAt: string | null;
291
+ };
292
+ type CreatedFormAccessTokenDto = FormAccessTokenDto & {
293
+ token: string;
294
+ };
295
+ type FormPasswordStatusDto = {
296
+ set: boolean;
297
+ };
298
+ /** Where a captcha secret key comes from: the form's encrypted secret, the environment/config, or nowhere. */
299
+ type SecretSource = 'form' | 'env' | 'unset';
300
+ /** Which per-form secrets are set. Names only: values are write-only and never leave the server. */
301
+ type FormSecretsDto = {
302
+ /** Names of the secrets stored for this form. */
303
+ keys: string[];
304
+ /** Per captcha provider, which secret key a submission would use. */
305
+ captcha: Record<CaptchaProvider, {
306
+ source: SecretSource;
307
+ }>;
308
+ /** The stored secrets cannot be decrypted (the encryption key changed); set them again. */
309
+ unreadable: boolean;
310
+ };
311
+ /** What the public page needs to decide between the form, a gate and "not found". Never contains secrets. */
312
+ type FormAccessInfoDto = {
313
+ slug: string;
314
+ name: string;
315
+ access: FormAccess;
316
+ signedIn: boolean;
317
+ /** False outside the schedule: the page shows a "closed" notice instead of the form. */
318
+ open: boolean;
319
+ /** Next moment the form opens (ISO), when it is closed and will open again. */
320
+ opensAt?: string | null;
321
+ };
189
322
  type FormSummaryDto = {
190
323
  id: string;
191
324
  slug: string;
@@ -204,6 +337,8 @@ type FormAdminDto = {
204
337
  status: FormStatus;
205
338
  version: number;
206
339
  definition: FormDefinition;
340
+ /** Stored submissions; only set when the form is fetched on its own. Can be above 0 even when storage is now `none`. */
341
+ submissionCount?: number;
207
342
  createdAt: string;
208
343
  updatedAt: string;
209
344
  };
@@ -271,6 +406,11 @@ declare module 'nitropack' {
271
406
  * here (webhooks, CRM, ...); set `ctx.delivered = true` once done. Throwing marks delivery failed.
272
407
  */
273
408
  'forms:submitted': (ctx: SubmittedContext) => void | Promise<void>;
409
+ /**
410
+ * A captcha token must be verified. Set `ctx.valid` to decide; when left `undefined` the provider's
411
+ * siteverify endpoint is called with the configured secret key. Lets hosts and tests replace the network call.
412
+ */
413
+ 'forms:captcha:verify': (ctx: CaptchaVerifyContext) => void | Promise<void>;
274
414
  /** Delivery of a submission finished (successfully or not). */
275
415
  'forms:delivered': (ctx: DeliveredContext) => void | Promise<void>;
276
416
  }
@@ -278,4 +418,4 @@ declare module 'nitropack' {
278
418
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
279
419
 
280
420
  export { _default as default };
281
- export type { AuthorizeContext, Condition, ConditionOp, DeliveredContext, DeliveryStatus, FieldOption, FieldType, FieldValidation, FileDescriptor, FormAdminDto, FormDefinition, FormDelivery, FormField, FormMode, FormStatus, FormStep, FormStorage, FormSubmitSettings, FormSummaryDto, FormsRuntimeConfig, ModuleOptions, PublicFormDto, PublicModuleOptions, SeedForm, SubmissionDto, SubmissionFileDto, SubmissionSummaryDto, SubmitResponse, SubmittedContext, SubmittedFile, Translated };
421
+ export type { AuthorizeContext, CaptchaConfig, CaptchaProvider, CaptchaVerifyContext, Condition, ConditionOp, CreatedFormAccessTokenDto, DeliveredContext, DeliveryStatus, FieldOption, FieldType, FieldValidation, FileDescriptor, FormAccess, FormAccessInfoDto, FormAccessTokenDto, FormAdminDto, FormAuthMode, FormAvailability, FormDefinition, FormDelivery, FormField, FormMode, FormPasswordStatusDto, FormSecretsDto, FormStatus, FormStep, FormStorage, FormSubmitSettings, FormSummaryDto, FormsRuntimeConfig, ModuleOptions, PhoneConfig, PublicFormDto, PublicModuleOptions, SecretSource, SeedForm, SubmissionDto, SubmissionFileDto, SubmissionSummaryDto, SubmitResponse, SubmittedContext, SubmittedFile };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/forms",
3
3
  "configKey": "forms",
4
- "version": "0.0.5",
4
+ "version": "0.0.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,12 +1,10 @@
1
- import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, extendPages, addServerHandler } from '@nuxt/kit';
2
- import { registerModule, registerLocales, registerDatabaseTable, defineModulePage } from '@frigear-nu/module-kit/utils';
1
+ import { defineNuxtModule, createResolver, hasNuxtModule, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, addComponent, addServerHandler } from '@nuxt/kit';
2
+ import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, registerDatabaseTable, addModulePages, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
3
+ import { adminRoute } from '@frigear-nu/admin/utils';
4
+ import { addAuthScope } from '@frigear-nu/auth/utils';
5
+ import { addMcpDefinitions } from '@frigear-nu/mcp/utils';
3
6
 
4
7
  const MODULE_NAME = "@frigear-nu/forms";
5
- function resolvePagePath(setting, defaultPath) {
6
- if (typeof setting === "string") return setting;
7
- if (setting === true) return defaultPath;
8
- return void 0;
9
- }
10
8
  const module$1 = defineNuxtModule({
11
9
  meta: { name: MODULE_NAME, configKey: "forms" },
12
10
  defaults: {
@@ -19,25 +17,44 @@ const module$1 = defineNuxtModule({
19
17
  defaults: { kv: true, blob: true }
20
18
  },
21
19
  "@frigear-nu/module-kit": { version: ">=0.0.1" },
20
+ "@frigear-nu/auth": { version: ">=0.0.1" },
21
+ "@frigear-nu/mcp": { version: ">=0.0.1" },
22
22
  "@frigear-nu/users": { version: ">=0.0.1" },
23
23
  "@frigear-nu/notify": { version: ">=0.0.1" },
24
- "@frigear-nu/menu": { version: ">=0.0.1", optional: true }
24
+ "@frigear-nu/ui": { version: ">=0.0.1" },
25
+ "@frigear-nu/menu": { version: ">=0.0.1", optional: true },
26
+ // Provides the shared page components of the admin pages (`AdminPage`, `AdminTable`, ...).
27
+ "@frigear-nu/admin": { version: ">=0.0.0", optional: true }
25
28
  },
26
29
  async setup(options, nuxt) {
27
30
  registerModule(MODULE_NAME, nuxt);
31
+ addAuthScope(
32
+ nuxt,
33
+ { name: "forms:manage", description: "Manage forms and submissions (staff)" },
34
+ { name: "forms:submit", description: "Submit forms as you" },
35
+ { name: "forms:pii", description: "See submission answers and file names through MCP tools (also needs forms:manage)" }
36
+ );
28
37
  const resolver = createResolver(import.meta.url);
38
+ registerTailwindSource(nuxt, resolver.resolve("./runtime/app"));
29
39
  registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
30
40
  const settings = options.settings ?? {};
31
- const withMenus = hasNuxtModule("@frigear-nu/menu");
32
41
  const formPath = resolvePagePath(settings.pages?.form, "/forms/:slug");
33
- const adminPath = resolvePagePath(settings.pages?.admin, "/admin/forms");
34
- const adminFormPath = resolvePagePath(settings.pages?.adminForm, "/admin/forms/:id");
35
- const adminSubmissionsPath = resolvePagePath(settings.pages?.adminSubmissions, "/admin/forms/:id/submissions");
36
- const adminSubmissionPath = resolvePagePath(settings.pages?.adminSubmission, "/admin/forms/:id/submissions/:submissionId");
42
+ const withAdmin = hasNuxtModule("@frigear-nu/admin");
43
+ const adminPage = (setting, ...segments) => withAdmin ? resolvePagePath(setting, adminRoute(nuxt, "forms", ...segments)) : void 0;
44
+ const adminPath = adminPage(settings.pages?.admin);
45
+ const adminFormPath = adminPage(settings.pages?.adminForm, ":id");
46
+ const adminSubmissionsPath = adminPage(settings.pages?.adminSubmissions, ":id", "submissions");
47
+ const adminSubmissionPath = adminPage(settings.pages?.adminSubmission, ":id", "submissions", ":submissionId");
37
48
  nuxt.options.runtimeConfig.forms = {
38
49
  rateLimit: settings.rateLimit ?? 10,
39
50
  rateLimitWindowSeconds: settings.rateLimitWindowSeconds ?? 600,
40
- seed: { enabled: options.seed?.enabled ?? false, forms: options.seed?.forms ?? [] }
51
+ seed: { enabled: options.seed?.enabled ?? false, forms: options.seed?.forms ?? [] },
52
+ captcha: {
53
+ turnstile: { secretKey: options.captcha?.turnstile?.secretKey ?? "" },
54
+ recaptcha: { secretKey: options.captcha?.recaptcha?.secretKey ?? "" },
55
+ hcaptcha: { secretKey: options.captcha?.hcaptcha?.secretKey ?? "" }
56
+ },
57
+ encryptionKey: options.encryptionKey ?? ""
41
58
  };
42
59
  nuxt.options.runtimeConfig.public.forms = {
43
60
  pages: {
@@ -46,6 +63,12 @@ const module$1 = defineNuxtModule({
46
63
  adminForm: adminFormPath,
47
64
  adminSubmissions: adminSubmissionsPath,
48
65
  adminSubmission: adminSubmissionPath
66
+ },
67
+ captcha: {
68
+ turnstile: { siteKey: options.captcha?.turnstile?.siteKey ?? "" },
69
+ recaptcha: { siteKey: options.captcha?.recaptcha?.siteKey ?? "" },
70
+ hcaptcha: { siteKey: options.captcha?.hcaptcha?.siteKey ?? "" },
71
+ defaultProvider: options.captcha?.defaultProvider ?? "turnstile"
49
72
  }
50
73
  };
51
74
  await registerDatabaseTable(nuxt, {
@@ -58,44 +81,50 @@ const module$1 = defineNuxtModule({
58
81
  route("get", "/admin/forms/:id", "admin/form.get");
59
82
  route("patch", "/admin/forms/:id", "admin/form.patch");
60
83
  route("delete", "/admin/forms/:id", "admin/form.delete");
84
+ route("post", "/admin/forms/:id/clone", "admin/form-clone.post");
61
85
  route("get", "/admin/forms/:id/submissions", "admin/submissions.get");
86
+ route("get", "/admin/forms/:id/tokens", "admin/form-tokens.get");
87
+ route("post", "/admin/forms/:id/tokens", "admin/form-tokens.post");
88
+ route("delete", "/admin/forms/:id/tokens/:tokenId", "admin/form-token.delete");
89
+ route("get", "/admin/forms/:id/password", "admin/form-password.get");
90
+ route("put", "/admin/forms/:id/password", "admin/form-password.put");
91
+ route("delete", "/admin/forms/:id/password", "admin/form-password.delete");
92
+ route("get", "/admin/forms/:id/secrets", "admin/form-secrets.get");
93
+ route("put", "/admin/forms/:id/secrets", "admin/form-secrets.put");
94
+ route("delete", "/admin/forms/:id/secrets/:key", "admin/form-secret.delete");
62
95
  route("post", "/admin/import", "admin/import.post");
63
96
  route("get", "/admin/submissions/:id", "admin/submission.get");
64
97
  route("get", "/admin/submissions/:id/files/:fileId", "admin/submission-file.get");
65
98
  route("post", "/admin/submissions/:id/redeliver", "admin/submission-redeliver.post");
66
99
  route("get", "/:slug", "public/form.get");
100
+ route("get", "/:slug/access", "public/access.get");
67
101
  route("post", "/:slug/submit", "public/submit.post");
68
102
  addServerImportsDir(resolver.resolve("./runtime/server/utils"));
103
+ if (settings.mcp === true) addMcpDefinitions(nuxt, resolver.resolve("./runtime/server/mcp"));
104
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/encryption"));
105
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/staff-scopes"));
69
106
  addServerPlugin(resolver.resolve("./runtime/server/plugins/email-delivery"));
70
107
  addServerPlugin(resolver.resolve("./runtime/server/plugins/seed"));
71
108
  addImports([
72
109
  { name: "useForm", from: resolver.resolve("./runtime/app/composables/useForms") },
110
+ { name: "useFormAccess", from: resolver.resolve("./runtime/app/composables/useForms") },
111
+ { name: "FORM_PASSWORD_HEADER", from: resolver.resolve("./runtime/app/composables/useForms") },
73
112
  { name: "useFormsLocale", from: resolver.resolve("./runtime/app/composables/useForms") },
113
+ { name: "useFormsLocales", from: resolver.resolve("./runtime/app/composables/useForms") },
74
114
  { name: "useFormsPaths", from: resolver.resolve("./runtime/app/composables/useForms") },
115
+ { name: "useFormLink", from: resolver.resolve("./runtime/app/composables/useForms") },
75
116
  { name: "useFormStepper", from: resolver.resolve("./runtime/app/composables/useFormStepper") }
76
117
  ]);
77
118
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
78
- const pages = [
79
- { name: "forms-form", path: formPath, file: "form.vue" },
80
- { name: "forms-admin", path: adminPath, file: "admin.vue" },
81
- { name: "forms-admin-form", path: adminFormPath, file: "admin-form.vue" },
82
- { name: "forms-admin-submissions", path: adminSubmissionsPath, file: "admin-submissions.vue" },
83
- { name: "forms-admin-submission", path: adminSubmissionPath, file: "admin-submission.vue" }
84
- ];
85
- if (pages.some((page) => page.path)) {
86
- extendPages((existing) => {
87
- const taken = new Set(existing.map((page) => page.path));
88
- for (const page of pages) {
89
- if (page.path && !taken.has(page.path)) {
90
- existing.push(defineModulePage(MODULE_NAME, { name: page.name, path: page.path, file: resolver.resolve(`./runtime/app/pages/${page.file}`) }));
91
- }
92
- }
93
- });
94
- }
95
- if (withMenus) {
96
- const { registerMenuSource } = await import('@frigear-nu/menu/utils');
97
- registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
98
- }
119
+ addComponent({ name: "FormEmbed", filePath: resolver.resolve("./runtime/app/components/FormEmbed.vue"), global: true });
120
+ addModulePages(MODULE_NAME, resolver, [
121
+ { name: "forms-form", path: formPath, file: "./runtime/app/pages/form.vue" },
122
+ { name: "forms-admin", path: adminPath, file: "./runtime/app/pages/admin.vue" },
123
+ { name: "forms-admin-form", path: adminFormPath, file: "./runtime/app/pages/admin-form.vue" },
124
+ { name: "forms-admin-submissions", path: adminSubmissionsPath, file: "./runtime/app/pages/admin-submissions.vue" },
125
+ { name: "forms-admin-submission", path: adminSubmissionPath, file: "./runtime/app/pages/admin-submission.vue" }
126
+ ]);
127
+ addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
99
128
  }
100
129
  });
101
130
 
@@ -0,0 +1,26 @@
1
+ import type { FormDefinition } from '../../../types.js';
2
+ /** The saved form; enables per-form secrets (captcha secret key), which are stored right away, not with the definition. */
3
+ type __VLS_Props = {
4
+ formId?: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue: FormDefinition;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare var __VLS_82: {};
11
+ type __VLS_Slots = {} & {
12
+ preview?: (props: typeof __VLS_82) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ "update:modelValue": (value: FormDefinition) => any;
16
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
+ "onUpdate:modelValue"?: ((value: FormDefinition) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
22
+ type __VLS_WithSlots<T, S> = T & {
23
+ new (): {
24
+ $slots: S;
25
+ };
26
+ };