@stacksjs/forms 0.75.164 → 0.75.166
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 +2 -0
- package/dist/index.js +1 -1
- package/dist/spam.d.ts +28 -0
- package/dist/spam.js +1 -0
- package/dist/submissions.js +1 -1
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { CreatedForm, CreateFormFieldInput, CreateFormInput } from './create';
|
|
2
|
+
export type { SpamPolicy, SpamSignals } from './spam';
|
|
2
3
|
export type { SubmissionListRow, SubmitOptions, SubmitResult } from './submissions';
|
|
3
4
|
export type {
|
|
4
5
|
FieldChoice,
|
|
@@ -18,6 +19,7 @@ export { evaluateConditions, visibleFields } from './conditions';
|
|
|
18
19
|
export { createForm, loadFormByHandle, toCreateFields } from './create';
|
|
19
20
|
export { loadFormByUuid, publicDefinition } from './definition';
|
|
20
21
|
export { dispatchSubmissionNotifications } from './notifications';
|
|
22
|
+
export { checkSubmissionSpam } from './spam';
|
|
21
23
|
export {
|
|
22
24
|
completeSubmissionPayment,
|
|
23
25
|
exportSubmissionsCsv,
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{evaluateConditions,visibleFields}from"./conditions";export{createForm,loadFormByHandle,toCreateFields}from"./create";export{loadFormByUuid,publicDefinition}from"./definition";export{dispatchSubmissionNotifications}from"./notifications";export{completeSubmissionPayment,exportSubmissionsCsv,fetchSubmissions,submissionIdentity,submitForm}from"./submissions";export{checkUpload,extensionOf,fileFieldNamed,formUploadPrefix,isOwnedUploadPath,resolveUploadLimits}from"./uploads";export{computeAmountCents,validateSubmission}from"./validate";
|
|
1
|
+
export{evaluateConditions,visibleFields}from"./conditions";export{createForm,loadFormByHandle,toCreateFields}from"./create";export{loadFormByUuid,publicDefinition}from"./definition";export{dispatchSubmissionNotifications}from"./notifications";export{checkSubmissionSpam}from"./spam";export{completeSubmissionPayment,exportSubmissionsCsv,fetchSubmissions,submissionIdentity,submitForm}from"./submissions";export{checkUpload,extensionOf,fileFieldNamed,formUploadPrefix,isOwnedUploadPath,resolveUploadLimits}from"./uploads";export{computeAmountCents,validateSubmission}from"./validate";
|
package/dist/spam.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a submission looks automated, or null when it does not:
|
|
3
|
+
* `honeypot` (answer as if it worked, and store nothing, so the bot learns
|
|
4
|
+
* nothing) or `too_fast` (ask them to try again).
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkSubmissionSpam(signals: SpamSignals, policy?: SpamPolicy): 'honeypot' | 'too_fast' | null;
|
|
7
|
+
/**
|
|
8
|
+
* The two spam signals a public form can check without a CAPTCHA.
|
|
9
|
+
*
|
|
10
|
+
* - The honeypot: a field people never see (hidden with CSS, `tabindex=-1`,
|
|
11
|
+
* `autocomplete=off`). Form-filling bots fill every input, so any value
|
|
12
|
+
* in it means a bot.
|
|
13
|
+
* - The time on the page: the client reports how many milliseconds passed
|
|
14
|
+
* between showing the form and sending it. Nobody reads a form and types
|
|
15
|
+
* their name and email in under a few seconds.
|
|
16
|
+
*
|
|
17
|
+
* Neither proves a human, and neither tracks anyone or loads a third-party
|
|
18
|
+
* script. They stop the common drive-by bots; a rate limit per address or
|
|
19
|
+
* IP is still the caller's job.
|
|
20
|
+
*/
|
|
21
|
+
export declare interface SpamSignals {
|
|
22
|
+
honeypot?: unknown
|
|
23
|
+
renderedAtMs?: unknown
|
|
24
|
+
}
|
|
25
|
+
export declare interface SpamPolicy {
|
|
26
|
+
honeypot?: boolean
|
|
27
|
+
minSubmitSeconds?: number
|
|
28
|
+
}
|
package/dist/spam.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function checkSubmissionSpam(signals,policy={}){if((policy.honeypot??!0)&&typeof signals.honeypot==="string"&&signals.honeypot.trim()!=="")return"honeypot";const minSeconds=policy.minSubmitSeconds??3,elapsed=typeof signals.renderedAtMs==="number"?signals.renderedAtMs:typeof signals.renderedAtMs==="string"&&signals.renderedAtMs.trim()!==""?Number(signals.renderedAtMs):void 0;if(minSeconds>0&&elapsed!==void 0&&Number.isFinite(elapsed)&&elapsed<minSeconds*1000)return"too_fast";return null}
|
package/dist/submissions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{randomUUID}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database/runtime";import{validateSubmission}from"./validate";export async function submitForm(form,payload,options={}){if(form.status!=="active")return{ok:!1,status:409,message:"This form is not accepting responses."};const spam=config.forms?.spam??{};if(
|
|
1
|
+
import{randomUUID}from"node:crypto";import{config}from"@stacksjs/config";import{db,sqlDateTime}from"@stacksjs/database/runtime";import{checkSubmissionSpam}from"./spam";import{validateSubmission}from"./validate";export async function submitForm(form,payload,options={}){if(form.status!=="active")return{ok:!1,status:409,message:"This form is not accepting responses."};const spam=config.forms?.spam??{},spamSignal=checkSubmissionSpam(options,spam);if(spamSignal==="honeypot")return{ok:!0,submissionId:0,submissionUuid:"",status:"complete",amountCents:null,confirmation:form.settings.confirmation?.type==="message"?form.settings.confirmation.value:null,redirect:null};if(spamSignal==="too_fast")return{ok:!1,status:429,message:"That was submitted too quickly. Please try again."};const validated=validateSubmission(form,payload);if(!validated.ok)return{ok:!1,status:422,errors:validated.errors};const status=form.settings.payment&&(validated.amountCents??0)>0?"pending_payment":"complete",uuid=randomUUID(),now=sqlDateTime(new Date);await db.insertInto("form_submissions").values({uuid,form_id:form.id,site_id:form.siteId,data:JSON.stringify(validated.values),email:validated.email,name:validated.name,status,amount_cents:validated.amountCents,ip:options.ip??null,submitted_at:now,created_at:now,updated_at:now}).execute();const row=await db.selectFrom("form_submissions").where("uuid","=",uuid).select(["id"]).executeTakeFirst(),confirmation=form.settings.confirmation;return{ok:!0,submissionId:Number(row?.id??0),submissionUuid:uuid,status,amountCents:validated.amountCents,confirmation:confirmation?.type==="message"?confirmation.value:null,redirect:confirmation?.type==="redirect"?confirmation.value:null}}export async function completeSubmissionPayment(submissionUuid,paymentIntentId){const now=sqlDateTime(new Date),updated=await db.updateTable("form_submissions").set({status:"complete",payment_intent_id:paymentIntentId,updated_at:now}).where("uuid","=",submissionUuid).where("status","=","pending_payment").execute();return(typeof updated==="number"?updated:Number(updated?.changes??0))>0}export async function fetchSubmissions(formId,options={}){return(await db.selectFrom("form_submissions").where("form_id","=",formId).where("status","!=","spam").select(["id","uuid","data","email","name","status","amount_cents","submitted_at"]).orderBy("id","desc").limit(options.limit??100).offset(options.offset??0).execute()).map((row)=>({id:Number(row.id),uuid:row.uuid,values:(()=>{try{return row.data?JSON.parse(row.data):{}}catch{return{}}})(),email:row.email,name:row.name,status:row.status,amountCents:row.amount_cents==null?null:Number(row.amount_cents),submittedAt:row.submitted_at}))}export async function submissionIdentity(submissionId){const row=await db.selectFrom("form_submissions").where("id","=",submissionId).select(["email","name","data"]).executeTakeFirst();let values={};try{values=row?.data?JSON.parse(row.data):{}}catch{}return{email:row?.email??null,name:row?.name??null,values}}function csvEscape(value){const text=value===void 0||value===null?"":String(value),defused=/^[=+\-@]/.test(text)?`'${text}`:text;return/[",\n\r]/.test(defused)?`"${defused.replace(/"/g,'""')}"`:defused}export async function exportSubmissionsCsv(form){const fields=form.fields.filter((field)=>field.type!=="section_break").sort((a,b)=>a.position-b.position),rows=await fetchSubmissions(form.id,{limit:1e5}),lines=[[...fields.map((field)=>field.label),"Email","Name","Status","Amount","Submitted at"].map(csvEscape).join(",")];for(const row of rows)lines.push([...fields.map((field)=>csvEscape(row.values[field.name])),csvEscape(row.email),csvEscape(row.name),csvEscape(row.status),csvEscape(row.amountCents==null?"":(row.amountCents/100).toFixed(2)),csvEscape(row.submittedAt)].join(","));return lines.join(`
|
|
2
2
|
`)}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/forms",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.75.
|
|
4
|
+
"version": "0.75.166",
|
|
5
5
|
"description": "The Stacks form-builder functionality.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"prepublishOnly": "bun run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@stacksjs/config": "0.75.
|
|
60
|
-
"@stacksjs/database": "0.75.
|
|
61
|
-
"@stacksjs/logging": "0.75.
|
|
62
|
-
"@stacksjs/notifications": "0.75.
|
|
59
|
+
"@stacksjs/config": "0.75.166",
|
|
60
|
+
"@stacksjs/database": "0.75.166",
|
|
61
|
+
"@stacksjs/logging": "0.75.166",
|
|
62
|
+
"@stacksjs/notifications": "0.75.166"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@stacksjs/storage": "0.75.
|
|
66
|
-
"@stacksjs/validation": "0.75.
|
|
65
|
+
"@stacksjs/storage": "0.75.166",
|
|
66
|
+
"@stacksjs/validation": "0.75.166",
|
|
67
67
|
"better-dx": "^0.2.24"
|
|
68
68
|
},
|
|
69
69
|
"sideEffects": false
|