{"_id":"@alufie/form","_rev":"7-e2689e3bf814b3be5b38f1406bd03ae3","name":"@alufie/form","dist-tags":{"latest":"1.0.2"},"versions":{"0.1.0":{"name":"@alufie/form","version":"0.1.0","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@0.1.0","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"bf1e31310e0d3da50d43f8f103b54bcceb8f0fca","tarball":"https://registry.npmjs.org/@alufie/form/-/form-0.1.0.tgz","fileCount":46,"integrity":"sha512-0Om6cdg0I9anaBsDU/pIfSSJ5z06qwXnVpzA01PQByV8ytrtTG0m6w1LJs8TNWUMRxTTpsUI1/ubpEwwWxVvKg==","signatures":[{"sig":"MEUCIC1nHVeZAivi2z9x1J1RP1CDEf1IyhIhI7gFblxX93HxAiEAuuVd91gBKcW+MHDwFGsVDvNnshYxeAJEY4JdG77NCU0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":104697},"main":"./packages/core/dist/index.js","type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"ddc7c7f6c919ac56e01f00f5bc1ca0c73ef27812","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"vitest":"^3.2.4","typescript":"^5.9.3","@types/node":"^24.6.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.1.0_1776653314736_0.12449448811879216","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@alufie/form","version":"0.1.1","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@0.1.1","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"c14a790ae810aac8771cf0b6bc58ae025be4c124","tarball":"https://registry.npmjs.org/@alufie/form/-/form-0.1.1.tgz","fileCount":46,"integrity":"sha512-kNZz64SS2s9inpTOugDX4ietMme6hb7G7H7/SK/hKG5Ed2QAf5Pa2wTeFSRSRkBc5gxjoPGwYJ5cyzCVSNJZrg==","signatures":[{"sig":"MEUCIQCWjpo7IfSy0dgg3OfqOkVckQUfNLJFOhqNsA2h7b9zSgIgBJRcJ8Y6K6vAnjYLVrRgriUNUlmofhIeBQk/mcTzo1I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":104764},"main":"./packages/core/dist/index.js","pnpm":{"overrides":{"cookie":"^1.0.2"}},"type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"ddc7c7f6c919ac56e01f00f5bc1ca0c73ef27812","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"vitest":"^4.1.5","typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.1.1_1776831392598_0.16093516825359977","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@alufie/form","version":"0.1.2","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@0.1.2","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"baa23700db382d9694a94526b57e37a6455becf9","tarball":"https://registry.npmjs.org/@alufie/form/-/form-0.1.2.tgz","fileCount":46,"integrity":"sha512-XEzuek1YjtIILAck0Ph6fu/XqmCZ/2g29mOjjQd8dwLtKB3MtxPd9eJH+e12SiNbpBna1tOr68ZuImrJ4U+xSQ==","signatures":[{"sig":"MEQCIG4/AW0txu3DkTzMy4Rvrd5xE4FAUcQto+MkRWr999/SAiAdvH94MHa2CafRuX/vrutHyuSQN+kOLrwuyEzn0v/jyg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":127592},"main":"./packages/core/dist/index.js","pnpm":{"overrides":{"cookie":"^1.0.2"}},"type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"87f148398b32834b9a00d3b6ffbc909180ca3bbc","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"vitest":"^4.1.5","typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.1.2_1778465541503_0.23258664688510544","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@alufie/form","version":"0.1.3","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@0.1.3","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"f5009307af9fe918a9d2745a1242e70e96399e98","tarball":"https://registry.npmjs.org/@alufie/form/-/form-0.1.3.tgz","fileCount":46,"integrity":"sha512-s/0RENaRLeEDKaa0xYmEpWxE4n9L+ETvIIlDDMaOFN5vl5IFoN0Ca1JPLNlzk92kM9VPXBJK9mOW1vu0YkykRQ==","signatures":[{"sig":"MEUCIQCtTM9luDTBm1EUU3GaRrKqndhQIouPVXINMmm7rnpeWQIgbbQHLVdfZgoRt+mZoQMhYGNI6nmeK2F1mXeoFWGXSFg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":132720},"main":"./packages/core/dist/index.js","pnpm":{"overrides":{"cookie":"^1.0.2"}},"type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"9e24eb25a362b9b450ea06f3f557ade2d366675a","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.5","typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.1.3_1778828791304_0.9924984066984601","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@alufie/form","version":"1.0.0","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@1.0.0","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"d92d2f0d38f4852af24379cfe0443d7be8ccc5c3","tarball":"https://registry.npmjs.org/@alufie/form/-/form-1.0.0.tgz","fileCount":46,"integrity":"sha512-TxNQoyZq6TqN5/1/FfQEEa6nzYnqUdX29uvnrDo95OWRoMvcmjTGfcZSl3c8j8/heOhCF0Uw/UYGc0lVm3Ml2Q==","signatures":[{"sig":"MEQCIBdZ2QlectR4gLxTWVqwwnIYPBjBqwRRo6wzGonncWG+AiAa2/L102EdrR+FZljvRfIeIgZvIprLqJNRU34iEQwfHQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":134229},"main":"./packages/core/dist/index.js","pnpm":{"overrides":{"cookie":"^1.0.2"}},"type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"fe19f288aedc90365251f77386af802dfb6b2d86","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.5","typescript":"^6.0.3","@types/node":"^25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/form_1.0.0_1778848889539_0.44880468424485565","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@alufie/form","version":"1.0.1","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"license":"MIT","_id":"@alufie/form@1.0.1","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"dist":{"shasum":"2a1abcaa90609058769276689bca8e97884c8c13","tarball":"https://registry.npmjs.org/@alufie/form/-/form-1.0.1.tgz","fileCount":46,"integrity":"sha512-wuT28rJbNR4J3MgWUsxVnw6lotpsAIYjlquY3TkNd/I4qLa6xvT3HvZYrkGBmBqtSJDX3DDjVJT8+821hnkT1Q==","signatures":[{"sig":"MEQCIALBvuUcydlF1t7/S6yIO+jrXY769U6cybfwH1OPmZkxAiAPbvXxXp2Z4sZOKSZH5M+VJfzIaeuSRGXM12wFcQm0fA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":134713},"main":"./packages/core/dist/index.js","pnpm":{"overrides":{"cookie":"^1.0.2"},"onlyBuiltDependencies":["better-sqlite3"]},"type":"module","types":"./packages/core/dist/index.d.ts","exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"gitHead":"fe19f288aedc90365251f77386af802dfb6b2d86","scripts":{"test":"pnpm -r --if-present test","build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check"},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"_npmVersion":"11.8.0","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.5","typescript":"^6.0.3","@types/node":"^25.6.0","better-sqlite3":"^12.10.0","@types/better-sqlite3":"^7.6.13"},"_npmOperationalInternal":{"tmp":"tmp/form_1.0.1_1779721337288_0.4572730109151486","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@alufie/form","version":"1.0.2","description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","type":"module","license":"MIT","sideEffects":false,"main":"./packages/core/dist/index.js","types":"./packages/core/dist/index.d.ts","publishConfig":{"access":"public"},"exports":{".":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./core":{"types":"./packages/core/dist/index.d.ts","default":"./packages/core/dist/index.js"},"./drizzle":{"types":"./packages/drizzle/dist/index.d.ts","default":"./packages/drizzle/dist/index.js"},"./resend":{"types":"./packages/resend/dist/index.d.ts","default":"./packages/resend/dist/index.js"},"./turnstile":{"types":"./packages/turnstile/dist/index.d.ts","default":"./packages/turnstile/dist/index.js"},"./sveltekit":{"types":"./packages/sveltekit/dist/index.d.ts","default":"./packages/sveltekit/dist/index.js"},"./ui-svelte":"./packages/ui-svelte/src/index.ts","./package.json":"./package.json"},"scripts":{"build":"pnpm -r --if-present build && node ./scripts/rewrite-public-imports.mjs","check":"pnpm -r --if-present check","lint":"eslint .","test":"pnpm -r --if-present test"},"devDependencies":{"@eslint/js":"^10.0.1","@types/better-sqlite3":"^9.6.0","@types/node":"^26.3.0","better-sqlite3":"^13.0.3","eslint":"^10.9.1","eslint-plugin-svelte":"^3.23.0","globals":"^17.11.0","typescript":"^6.0.3","typescript-eslint":"^8.68.0","vitest":"^4.1.11"},"keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"gitHead":"2f0ca1abb29e5aa624a87c55c5afd667b1f08a2b","_id":"@alufie/form@1.0.2","_nodeVersion":"24.18.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-izFrG7IGPsiKx7pUsSgq5274HG7Se3ZnCqCZUSPrlWsRb4ous+QWQgqA+qF4fXUIljV3GG0HPfzW/f4UvQx67g==","shasum":"52ce4bcd6fcbebdab83a791e4e9a4426dfef5d72","tarball":"https://registry.npmjs.org/@alufie/form/-/form-1.0.2.tgz","fileCount":46,"unpackedSize":133433,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCg2jqvtzgYTStfjYYLaUo/q/sKVvciKW12iHdYXV+AiAIgebpvRMzpAfBljd4yvZB2cWkMwpDijULEI56IR7TOgjA="}]},"_npmUser":{"name":"alufie","email":"dev@alufie.com"},"directories":{},"maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/form_1.0.2_1787983854697_0.17056373216478593"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-20T02:48:34.607Z","modified":"2026-08-29T06:10:54.960Z","0.1.0":"2026-04-20T02:48:34.873Z","0.1.1":"2026-04-22T04:16:32.768Z","0.1.2":"2026-05-11T02:12:21.648Z","0.1.3":"2026-05-15T07:06:31.448Z","1.0.0":"2026-05-15T12:41:29.711Z","1.0.1":"2026-05-25T15:02:17.468Z","1.0.2":"2026-08-29T06:10:54.808Z"},"license":"MIT","keywords":["forms","confirmation","verification","sveltekit","comments","reviews","contact-form"],"description":"Verified submission workflows for contact forms, comments, reviews, and feedback with tree-shakeable subpath exports.","maintainers":[{"name":"alufie","email":"dev@alufie.com"}],"readme":"# @alufie/form\n\n`@alufie/form` is a reusable verified submission system for contact forms, comments, reviews, feedback, and similar user-generated workflows.\n\nAs of `1.0.0`, the package is fully headless:\n\n- core returns machine-readable result codes and structured data only\n- built-in result messages were removed\n- SvelteKit helpers no longer choose HTTP statuses, redirects, or payload shapes\n- apps are expected to own localization and transport policy\n\nIt extracts an existing production contact confirmation pattern into a configurable, packageable lifecycle:\n\n1. accept a submission\n2. run anti-spam checks\n3. hold it in a pending state\n4. send a confirmation email\n5. confirm once with a signed token\n6. notify, queue for approval, or publish\n\nThis workspace currently provides a working headless core plus first-party adapters for Drizzle, Resend, Turnstile, and SvelteKit.\n\n## Install\n\n```sh\nnpm install @alufie/form\n```\n\nImport only the parts you use:\n\n```ts\nimport { createFormSystem } from '@alufie/form/core';\nimport { createSubmitAction } from '@alufie/form/sveltekit';\nimport { createDrizzleSubmissionStorage } from '@alufie/form/drizzle';\n```\n\nThis package is exposed through subpath exports so consumers can avoid pulling in adapters they do not use.\n\n## Table of contents\n\n- [What this is for](#what-this-is-for)\n- [What is included](#what-is-included)\n- [Architecture](#architecture)\n- [Submission lifecycle](#submission-lifecycle)\n- [Package guide](#package-guide)\n- [Core API](#core-api)\n- [Configuration reference](#configuration-reference)\n- [Storage model](#storage-model)\n- [SvelteKit integration](#sveltekit-integration)\n- [Migration notes](#migration-notes)\n- [Example workflows](#example-workflows)\n- [Security model](#security-model)\n- [Workspace structure](#workspace-structure)\n- [Development](#development)\n- [Current limitations](#current-limitations)\n- [Roadmap ideas](#roadmap-ideas)\n\n## What this is for\n\nUse `@alufie/form` when you want:\n\n- email-confirmed contact forms\n- verified comments before moderation\n- verified reviews before publication\n- feedback or lead forms with anti-spam protection\n- a reusable backend lifecycle that can be plugged into multiple apps\n\nThis is not yet a full public comment platform with:\n\n- threaded discussion\n- moderation dashboard UI\n- end-user account management\n- permanent public rendering layer\n\nInstead, this package family focuses on the verified submission workflow that sits before those higher-level product features.\n\n## What is included\n\nThe public package currently includes these subpath exports:\n\n- `@alufie/form/core`\n  Headless workflow engine, status transitions, contracts, hooks, token lifecycle, and orchestration.\n- `@alufie/form/drizzle`\n  Drizzle schema and storage adapter for persisted submissions.\n- `@alufie/form/resend`\n  Resend email transport adapter.\n- `@alufie/form/turnstile`\n  Cloudflare Turnstile captcha verifier.\n- `@alufie/form/sveltekit`\n  Thin SvelteKit request helpers that require app-owned result mapping.\n- `@alufie/form/ui-svelte`\n  Minimal Svelte UI helpers for status display and honeypot fields.\n\n## Architecture\n\nThe project is intentionally split into a headless core and optional integrations.\n\n### Core responsibilities\n\n`@alufie/form/core` owns:\n\n- submission state machine\n- confirmation token generation and hashing\n- transition rules\n- orchestration of validation, anti-spam, confirmation, and notification\n- moderation actions\n- lifecycle hooks\n\n### Adapter responsibilities\n\nAdapters plug infrastructure into the core:\n\n- storage\n- email transport\n- captcha verification\n- framework-specific request/response helpers\n- optional UI primitives\n\n### Design goal\n\nThe core package must not depend on:\n\n- SvelteKit\n- Drizzle\n- Resend\n- Turnstile\n- framework env modules\n\nThat keeps the lifecycle portable while still allowing opinionated first-party integrations.\n\n## Submission lifecycle\n\nThe current implementation supports these statuses:\n\n- `submitted`\n- `pending_confirmation`\n- `confirmed`\n- `pending_approval`\n- `approved`\n- `rejected`\n- `published`\n- `expired`\n\n### Typical flow\n\nDefault contact-style flow:\n\n1. user submits the form\n2. validator checks the payload\n3. honeypot, rate limiting, and captcha checks run if configured\n4. payload is sanitized if a sanitizer is configured\n5. system stores the submission\n6. system sends a confirmation email if confirmation is required\n7. user clicks the verification link\n8. token is consumed exactly once\n9. submission transitions and post-confirmation behavior runs\n\n### Policy options\n\nThe current policy knobs are:\n\n- confirmation mode: `required` or `disabled`\n- post-confirmation action: `notify`, `await_approval`, or `publish`\n- moderation mode: `disabled` or `required`\n- token expiry hours\n\n### Default behavior\n\nIf no policies are overridden:\n\n- confirmation is enabled\n- moderation is disabled\n- post-confirmation action is `notify`\n- token expiry is 24 hours\n\n## Package guide\n\n### `@alufie/form/core`\n\nUse this when you want the lifecycle engine without any framework lock-in.\n\nMain exports:\n\n- `createFormSystem`\n- `generateToken`\n- `hashToken`\n- types for storage, validation, transport, policies, results, and result codes\n\nSee:\n\n- `packages/core/src/index.ts`\n- `packages/core/src/system.ts`\n- `packages/core/src/types.ts`\n\n### `@alufie/form/drizzle`\n\nUse this when you want a Drizzle-backed submission store.\n\nIt currently exports:\n\n- `formSubmissionTable`\n- `sqliteFormSubmissionTable`\n- `pgFormSubmissionTable`\n- `createDrizzleSubmissionStorage`\n- `DrizzleTransactionMode`\n\nSee:\n\n- `packages/drizzle/src/index.ts`\n\n### `@alufie/form/resend`\n\nUse this when Resend is your email provider.\n\nIt exports:\n\n- `createResendTransport`\n\nSee:\n\n- `packages/resend/src/index.ts`\n\n### `@alufie/form/turnstile`\n\nUse this when Cloudflare Turnstile is your captcha provider.\n\nIt exports:\n\n- `createTurnstileVerifier`\n\nSee:\n\n- `packages/turnstile/src/index.ts`\n\n### `@alufie/form/sveltekit`\n\nUse this when your app is built with SvelteKit.\n\nIt currently exports:\n\n- `createSubmitAction`\n- `createConfirmationHandler`\n- `createModerationHandler`\n\nSee:\n\n- `packages/sveltekit/src/index.ts`\n\n### `@alufie/form/ui-svelte`\n\nOptional small UI helpers for Svelte apps:\n\n- `FormStatus`\n- `HiddenHoneypot`\n\nSee:\n\n- `packages/ui-svelte/src/index.ts`\n\n## Core API\n\nThe main entrypoint is:\n\n```ts\nimport { createFormSystem } from '@alufie/form/core';\n```\n\n### `createFormSystem(config)`\n\nCreates a configured submission system instance.\n\nThe returned object currently exposes:\n\n- `submit({ data }, context?)`\n- `confirm(token, context?)`\n- `approve(submissionId, context?)`\n- `reject(submissionId, context?)`\n- `publish(submissionId, context?)`\n- `resendConfirmation(submissionId, context?)`\n- `cleanupExpired(context?)`\n\nResult contract:\n\n- `code` is the stable machine-readable contract\n- result objects do not include built-in display messages\n- consumers should branch on `result.code` and map that into app-owned messages, statuses, redirects, and payloads\n\n### Example\n\n```ts\nimport { createFormSystem } from '@alufie/form/core';\n\nconst system = createFormSystem({\n  type: 'contact',\n  storage,\n  validator,\n  templates,\n  emailTransport,\n  generateConfirmationUrl: (token, submission) =>\n    `https://example.com/contact/verify/${token}`,\n});\n```\n\n## Configuration reference\n\nThe most important config fields are below.\n\n### Required config\n\n- `type`\n  Logical submission type like `contact`, `comment`, or `review`.\n- `storage`\n  A storage adapter implementing the `SubmissionStorage` contract.\n- `validator`\n  Validates raw input and returns typed payload data.\n- `templates`\n  Renders confirmation and internal notification emails.\n- `emailTransport`\n  Sends email messages generated by templates.\n- `generateConfirmationUrl`\n  Converts a raw confirmation token into a user-facing URL.\n\n### Optional config\n\n- `sanitizer`\n  Sanitizes validated payload data before persistence.\n- `captchaVerifier`\n  Verifies captcha tokens.\n- `rateLimiter`\n  Applies request rate limits.\n- `duplicateChecker`\n  Rejects near-duplicate submissions if desired.\n- `hooks`\n  Lifecycle hooks for submit/confirm/approve/reject/publish.\n- `logger`\n  Structured logging hooks.\n- `policies`\n  Workflow policy overrides.\n- `generateId`\n  Custom ID generation.\n- `getCaptchaToken`\n  Extracts the captcha token from raw input.\n- `getHoneypotValue`\n  Extracts the honeypot field value from raw input.\n- `getRateLimitKey`\n  Chooses a key for rate limiting.\n- `getInternalRecipients`\n  Overrides internal email recipients.\n\n### Validator contract\n\nYour validator should return:\n\n```ts\n{\n  success: true,\n  data: typedPayload\n}\n```\n\nor:\n\n```ts\n{\n  success: false,\n  message: 'Validation failed',\n  fieldErrors: {\n    email: ['Valid email is required']\n  }\n}\n```\n\n### Template contract\n\nTemplates must implement:\n\n- `renderConfirmationEmail`\n- `renderInternalNotification`\n\nOptional:\n\n- `renderApprovalRequest`\n- `renderDecisionEmail`\n\n### Example config\n\n```ts\nconst system = createFormSystem({\n  type: 'contact',\n  storage,\n  validator: {\n    validate(input) {\n      return {\n        success: true,\n        data: {\n          name: String(input.name ?? ''),\n          email: String(input.email ?? ''),\n          message: String(input.message ?? ''),\n        },\n      };\n    },\n  },\n  sanitizer: {\n    sanitize(payload) {\n      return {\n        ...payload,\n        email: payload.email.trim().toLowerCase(),\n        message: payload.message.trim(),\n      };\n    },\n  },\n  templates: {\n    renderConfirmationEmail({ submission, confirmationUrl }) {\n      return {\n        to: submission.submitterEmail,\n        subject: 'Confirm your submission',\n        text: `Confirm here: ${confirmationUrl}`,\n      };\n    },\n    renderInternalNotification({ submission }) {\n      return {\n        to: 'admin@example.com',\n        replyTo: submission.submitterEmail,\n        subject: `New verified ${submission.type}`,\n        text: JSON.stringify(submission.payload, null, 2),\n      };\n    },\n  },\n  emailTransport,\n  generateConfirmationUrl: (token) =>\n    `https://example.com/contact/verify/${token}`,\n});\n```\n\n## Storage model\n\nThe generalized data model is a reusable `form_submission` table.\n\nThe Drizzle adapter includes SQLite and PostgreSQL table definitions for:\n\n- `id`\n- `type`\n- `status`\n- `payload`\n- `submitter_email`\n- `submitter_name`\n- `confirmation_token_hash`\n- `confirmation_token_expires_at`\n- `confirmed_at`\n- `approved_at`\n- `published_at`\n- `rejected_at`\n- `meta`\n- `created_at`\n- `updated_at`\n\n### Important storage notes\n\n- raw confirmation tokens are never stored\n- only the token hash is persisted\n- confirmation is one-time by design\n- the Drizzle adapter consumes confirmation tokens atomically\n- `createDrizzleSubmissionStorage({ db })` uses the SQLite table by default\n- pass `table: pgFormSubmissionTable` when using the PostgreSQL schema\n- transaction mode defaults to `sync` for `sqliteFormSubmissionTable` and `async` for `pgFormSubmissionTable`\n- pass `transactionMode` explicitly if your Drizzle driver does not match the table default\n\n## SvelteKit integration\n\n### Submit action\n\nThe SvelteKit helpers are intentionally thin. They only:\n\n- read request/form data\n- build submission context\n- call `system.*`\n- pass the raw result to your app-owned mapper\n\nRecommended pattern:\n\n- keep localization, HTTP status policy, redirect policy, and UI payload mapping in one app-owned module\n- import that module from routes/actions instead of scattering switch statements per route\n\n### Recommended policy module\n\n```ts\n// src/lib/server/form-policy.ts\nexport const submitMessages = {\n  requires_confirmation: '確認信已寄出，請前往信箱完成驗證。',\n  validation_failed: '請檢查表單欄位。',\n  captcha_failed: '驗證失敗，請再試一次。',\n  rate_limited: '送出過於頻繁，請稍後再試。',\n  duplicate: '您已提交過相似內容。',\n  error: '系統暫時無法處理，請稍後再試。'\n} as const;\n\nexport const submitStatuses = {\n  validation_failed: 400,\n  captcha_failed: 400,\n  duplicate: 409,\n  rate_limited: 429,\n  error: 500\n} as const;\n\nexport const confirmStatuses = {\n  invalid_token: 404,\n  already_consumed: 409,\n  expired: 410,\n  error: 500\n} as const;\n```\n\n### Submit action\n\n```ts\nimport { createSubmitAction } from '@alufie/form/sveltekit';\nimport { system } from '$lib/server/system';\nimport { fail } from '@sveltejs/kit';\nimport { submitMessages, submitStatuses } from '$lib/server/form-policy';\n\nexport const actions = {\n  default: createSubmitAction({\n    system,\n    mapResult(result) {\n      if (!result.ok) {\n        return fail(submitStatuses[result.code] ?? 400, {\n          success: false,\n          code: result.code,\n          message: submitMessages[result.code] ?? '送出失敗。',\n          fieldErrors: result.fieldErrors ?? {},\n        });\n      }\n\n      return {\n        success: true,\n        code: result.code,\n        email: result.submission?.submitterEmail,\n        message: submitMessages[result.code] ?? '已送出。',\n      };\n    },\n  }),\n};\n```\n\n### Confirmation route\n\nUse `createConfirmationHandler` for token verification:\n\n```ts\nimport { createConfirmationHandler } from '@alufie/form/sveltekit';\nimport { system } from '$lib/server/system';\nimport { redirect } from '@sveltejs/kit';\nimport { confirmStatuses } from '$lib/server/form-policy';\n\nexport const GET = createConfirmationHandler({\n  system,\n  getToken: ({ params }) => params.token ?? '',\n  mapResult(result) {\n    if (result.ok) {\n      throw redirect(303, '/contact?verified=true');\n    }\n\n    return new Response(result.code, {\n      status: confirmStatuses[result.code] ?? 400,\n    });\n  },\n});\n```\n\n### Moderation routes\n\nUse `createModerationHandler` for admin transitions:\n\n```ts\nimport { createModerationHandler } from '@alufie/form/sveltekit';\nimport { system } from '$lib/server/system';\n\nexport const POST = createModerationHandler({\n  system,\n  action: 'approve',\n  getSubmissionId: ({ params }) => params.id ?? '',\n  mapResult(result) {\n    return new Response(JSON.stringify(result), {\n      headers: { 'content-type': 'application/json' },\n      status: result.ok ? 200 : result.code === 'not_found' ? 404 : 400,\n    });\n  },\n});\n```\n\n## Migration notes\n\n`1.0.0` is a breaking release.\n\nConsumers must update if they relied on any of the following:\n\n- `result.message`\n- helper-default `fail(...)` responses\n- helper-default redirect behavior\n- helper-default HTTP status mapping\n- helper-default JSON or form payload shapes\n\n### Before\n\n```ts\nexport const actions = {\n  default: createSubmitAction({ system }),\n};\n```\n\n### After\n\n```ts\nimport { fail } from '@sveltejs/kit';\nimport { submitMessages, submitStatuses } from '$lib/server/form-policy';\n\nexport const actions = {\n  default: createSubmitAction({\n    system,\n    mapResult(result) {\n      if (!result.ok) {\n        return fail(submitStatuses[result.code] ?? 400, {\n          code: result.code,\n          message: submitMessages[result.code] ?? '送出失敗。',\n          fieldErrors: result.fieldErrors ?? {},\n        });\n      }\n\n      return {\n        success: true,\n        code: result.code,\n      };\n    },\n  }),\n};\n```\n\nRecommended migration approach:\n\n- branch on `result.code`, not display text\n- centralize message/status/redirect policy in one app-owned module such as `src/lib/server/form-policy.ts`\n- keep routes/actions thin and import policy from that shared module\n\n## Example workflows\n\n### 1. Contact form\n\nRecommended policy:\n\n- confirmation: `required`\n- post-confirmation action: `notify`\n- moderation: `disabled`\n\nMeaning:\n\n- user must confirm email\n- after confirmation, internal admin notification is sent\n\n### 2. Comment submission\n\nRecommended policy:\n\n- confirmation: `required`\n- post-confirmation action: `await_approval`\n- moderation: `disabled` or `required`\n\nMeaning:\n\n- user confirms ownership of email\n- comment enters approval queue\n- admin can approve, reject, or publish\n\n### 3. Review submission\n\nRecommended policy:\n\n- confirmation: `required`\n- post-confirmation action: `publish`\n\nMeaning:\n\n- user confirms email\n- review is published immediately after confirmation\n\n### 4. Internal feedback form\n\nRecommended policy:\n\n- confirmation: `disabled`\n- post-confirmation action: `notify`\n\nMeaning:\n\n- skip email confirmation\n- use the same validation, storage, and anti-spam pipeline\n\n## Security model\n\nThe current implementation includes these safeguards:\n\n- honeypot support\n- pluggable rate limiting\n- pluggable captcha verification\n- validator boundary before persistence\n- optional sanitizer boundary before persistence\n- hashed confirmation tokens\n- one-time confirmation token consumption\n- expiry-based cleanup\n\n### Security defaults\n\n- token hashing uses SHA-256\n- tokens are URL-safe\n- raw confirmation tokens are only sent to the user\n- expired pending confirmations can be moved to `expired`\n\n### Recommended production hardening\n\nFor production apps, also add:\n\n- request logging\n- duplicate submission detection\n- stronger audit trail persistence\n- provider retry strategy\n- admin authentication around moderation routes\n- rate limiting keyed by IP and email when appropriate\n\n## Workspace structure\n\n```text\nalufie-form/\n  packages/\n    core/\n    drizzle/\n    resend/\n    sveltekit/\n    turnstile/\n    ui-svelte/\n```\n\nUseful files:\n\n- `packages/core/src/system.ts`\n- [packages/core/test/system.test.ts]\n\n## Development\n\n### Install\n\n```sh\npnpm install\n```\n\n### Run checks\n\n```sh\npnpm test\npnpm check\npnpm build\n```\n\n## Current limitations\n\nThis workspace is functional, but still intentionally lightweight.\n\nCurrent limitations include:\n\n- no packaged moderation dashboard UI\n- no built-in database migrations folder yet beyond exported schema/adapter\n- no webhook adapter yet\n- no job queue adapter yet\n- no first-party React/Next.js adapter yet\n- no built-in duplicate detection adapter yet\n- no localization framework for templates yet\n- no publish script or release automation yet\n\n## Roadmap ideas\n\nNatural next steps:\n\n- add a more complete production integration example in docs\n- add webhook and queue adapters\n- add moderation dashboard primitives\n- add richer template composition helpers\n- add first-party Next.js and generic Express/Fetch adapters\n- add audit log persistence and metrics hooks\n- add retry and dead-letter strategies for email delivery\n\n## Reference files\n\n- Workspace root: [alufie-form]\n- Core API: `packages/core/src/index.ts`\n- SvelteKit adapter: `packages/sveltekit/src/index.ts`\n","readmeFilename":"README.md"}