{"_id":"@adpena/action-pages","_rev":"2-088745a48bea3f298e00f5a2a9de8694","name":"@adpena/action-pages","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.0":{"name":"@adpena/action-pages","version":"0.2.0","keywords":["emdash","campaign","action-pages","petition","fundraise","gotv","actblue","progressive","cloudflare-workers","fec-compliance","ab-testing","embed","i18n"],"license":"MIT","_id":"@adpena/action-pages@0.2.0","maintainers":[{"name":"adpena","email":"adpena@gmail.com"}],"homepage":"https://github.com/adpena/action-pages#readme","bugs":{"url":"https://github.com/adpena/action-pages/issues"},"dist":{"shasum":"1f6a27765fcca2d75889fc48e5cd951267a622b8","tarball":"https://registry.npmjs.org/@adpena/action-pages/-/action-pages-0.2.0.tgz","fileCount":77,"integrity":"sha512-9WgpDjaGN9xZdAQX1y6S/iMuZjBHDGQ4S+aCzbk+dYOcO9CwkY4HByVctcr6XbCHveNVLO943ejaaD0ZuDvxgQ==","signatures":[{"sig":"MEYCIQCm2vqvb2o1yGy66G1dQAEzmF31aAEqcgT6U7BCSRusegIhAPKIjbg4Z1xaGSnON+DO/S3bPjuI+Zy48cQdOtqAKjhe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":306506},"type":"module","engines":{"node":">=20"},"exports":{".":"./src/index.ts","./admin":"./src/admin/index.tsx","./lib/*":"./src/lib/*.ts","./sandbox":"./src/sandbox-entry.ts","./modules/*":"./src/modules/*.ts","./components/*":"./src/components/*.tsx"},"gitHead":"ead13e358b2c3379c30928ca024021426f2bbdc7","scripts":{"test":"vitest run","typecheck":"tsc --noEmit"},"_npmUser":{"name":"adpena","email":"adpena@gmail.com"},"repository":{"url":"git+https://github.com/adpena/action-pages.git","type":"git"},"_npmVersion":"11.12.1","description":"Composable campaign action pages for emdash CMS — petitions, fundraising, GOTV, signups with hero templates, i18n, compliance, and ad platform click tracking","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@adpena/notifications":"^0.1.0"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.0.0","emdash":"^0.1.0","vitest":"^4.1.0","react-dom":"^19.0.0","typescript":"^5.8.0"},"peerDependencies":{"react":">=18.0.0","emdash":">=0.1.0","react-dom":">=18.0.0"},"_npmOperationalInternal":{"tmp":"tmp/action-pages_0.2.0_1775599872287_0.07731777770427639","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@adpena/action-pages","version":"0.3.0","description":"Open-source Action Network alternative — 8 action types, 9 platform integrations, AI page generation, brand extraction, full CRM. Edge-native on Cloudflare Workers.","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/adpena/action-pages.git"},"homepage":"https://github.com/adpena/action-pages#readme","bugs":{"url":"https://github.com/adpena/action-pages/issues"},"exports":{".":"./src/index.ts","./sandbox":"./src/sandbox-entry.ts"},"keywords":["emdash","cms","plugin","action-pages","fundraising","campaign","fec-compliance","disclaimer","ab-testing","political","embed","widget"],"peerDependencies":{"emdash":">=0.1.0"},"devDependencies":{"vitest":"^3.0.0"},"scripts":{"test":"vitest run","test:watch":"vitest"},"dependencies":{"@adpena/notifications":"^0.1.0"},"gitHead":"b8efc50b0122534160b1ac28004e951e32978e61","_id":"@adpena/action-pages@0.3.0","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-NqPZY5XgJp7cT1AzoHqC/pM2avh3vqMI7lCs9snuw49Pcwpi0wFHgrsx2noKdeeIhZIoJryFWHHQfh9u3n1sYg==","shasum":"9405596e2c4a437164c8863c116c33a293bd9c2d","tarball":"https://registry.npmjs.org/@adpena/action-pages/-/action-pages-0.3.0.tgz","fileCount":96,"unpackedSize":493462,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCbwZ1SfrSXP2nRPEgD242JeFwGujfGzPZL5/OgmOsSOgIhALzVNlTM34eNoavhr3F49bFPHqJrkbrEN46M1If7B/A3"}]},"_npmUser":{"name":"adpena","email":"adpena@gmail.com"},"directories":{},"maintainers":[{"name":"adpena","email":"adpena@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/action-pages_0.3.0_1775640571038_0.2559477254642719"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-07T22:11:12.202Z","modified":"2026-04-08T09:29:31.299Z","0.2.0":"2026-04-07T22:11:12.443Z","0.3.0":"2026-04-08T09:29:31.186Z"},"bugs":{"url":"https://github.com/adpena/action-pages/issues"},"license":"MIT","homepage":"https://github.com/adpena/action-pages#readme","keywords":["emdash","cms","plugin","action-pages","fundraising","campaign","fec-compliance","disclaimer","ab-testing","political","embed","widget"],"repository":{"type":"git","url":"git+https://github.com/adpena/action-pages.git"},"description":"Open-source Action Network alternative — 8 action types, 9 platform integrations, AI page generation, brand extraction, full CRM. Edge-native on Cloudflare Workers.","maintainers":[{"name":"adpena","email":"adpena@gmail.com"}],"readme":"# @crafted/action-pages\n\nCampaign action page engine for [emdash CMS](https://emdashcms.com). Build donation, petition, and GOTV pledge pages with FEC/state disclaimer compliance, geo-personalization, deterministic A/B testing, and embeddable widgets -- all running inside emdash's plugin sandbox.\n\n## Features\n\n- **Campaign-scoped pages** -- group action pages under campaigns with independent slugs\n- **FEC + state disclaimer compliance** -- built-in dataset covering federal rules (11 CFR 110.11) and 10 state jurisdictions (CA, CO, DC, FL, GA, MD, NY, PA, TX, VA), including AI disclosure requirements\n- **Geo-personalization** -- resolve visitor jurisdiction from request geo headers; adjust donation amounts by region\n- **Deterministic A/B testing** -- hash-based variant assignment (no cookies, no state) with conversion tracking\n- **Embeddable widgets** -- generate a `<script>` tag that renders an action page in a Shadow DOM iframe on any site\n- **Rate limiting + Turnstile** -- per-IP rate limiting with optional Cloudflare Turnstile bot protection\n- **Submission validation** -- type-safe input validation and sanitization for donation clicks, petition signatures, and GOTV pledges\n- **OG metadata injection** -- automatic Open Graph tags for action page URLs via the `page:metadata` hook\n\n## Installation\n\n### As a trusted plugin (config-based)\n\nInstall the package:\n\n```bash\nnpm install @crafted/action-pages\n```\n\nRegister the plugin in your emdash site's Astro config:\n\n```typescript\n// astro.config.mjs\nimport { defineConfig } from \"astro/config\";\nimport { emdash } from \"emdash/astro\";\nimport { actionPages } from \"@crafted/action-pages\";\n\nexport default defineConfig({\n  integrations: [\n    emdash({\n      plugins: [actionPages()],\n    }),\n  ],\n});\n```\n\n### From the emdash Marketplace\n\nOnce the plugin is published to the marketplace, install it from the admin dashboard under **Plugins > Marketplace**. Marketplace plugins run in the sandbox automatically.\n\n## Configuration\n\nThe plugin registers itself with ID `crafted-action-pages` and requests these capabilities:\n\n| Capability | Purpose |\n|---|---|\n| `read:content` | Read action page and campaign data |\n| `write:content` | Create demo campaign on install |\n| `email:send` | Future: submission confirmation emails |\n| `network:fetch` | Turnstile verification |\n| `page:inject` | OG metadata for action page URLs |\n\n### Plugin storage collections\n\n| Collection | Indexes | Purpose |\n|---|---|---|\n| `campaigns` | `slug` | Campaign groupings |\n| `action_pages` | `slug`, `status`, `campaign_id` | Individual action pages |\n| `submissions` | `page_id`, `campaign_id`, `created_at` | Form submissions |\n| `ab_variants` | `page_id` | A/B test variant stats |\n\n## Usage\n\n### Creating campaigns and pages\n\nOn first activation, the plugin seeds a demo campaign (`demo`) with a sample donation page (`demo-donate`). Create additional campaigns and pages through the admin UI or by writing directly to plugin storage.\n\n### Rendering action pages\n\nThe plugin provides a JSON API via its routes. Your site needs a page template to render the data. A minimal Astro example:\n\n```astro\n---\n// src/pages/action/[slug].astro\n// This file lives in YOUR site, not in the plugin.\nconst pageData = await fetch(`${Astro.url.origin}/_emdash/api/plugins/crafted-action-pages/page?slug=${Astro.params.slug}`);\nconst { data } = await pageData.json();\n---\n<html>\n  <body>\n    <h1>{data.title}</h1>\n    <p>{data.body}</p>\n    <p class=\"disclaimer\">{data.disclaimer.combined}</p>\n  </body>\n</html>\n```\n\nThis separation is intentional -- the plugin handles data, compliance, and A/B logic; the site controls presentation.\n\n### Embedding on external sites\n\nGenerate an embed script for any action page:\n\n```\nGET /_emdash/api/plugins/crafted-action-pages/embed?slug=demo-donate\n```\n\nInclude the returned `<script>` tag on any HTML page. It creates a Shadow DOM container with an iframe pointing to your action page.\n\n## API Reference\n\nAll routes are under `/_emdash/api/plugins/crafted-action-pages/`.\n\n### `GET /page`\n\nReturns action page data with resolved disclaimer, A/B variant, and geo context.\n\n**Query parameters:**\n- `slug` (required) -- action page slug\n- `campaign` (optional) -- campaign slug to scope the lookup\n\n**Response (200):**\n```json\n{\n  \"data\": {\n    \"title\": \"Support the Cause\",\n    \"type\": \"fundraise\",\n    \"body\": \"Your contribution makes a difference.\",\n    \"actblue_url\": \"https://secure.actblue.com/donate/example\",\n    \"refcode\": \"crafted-demo\",\n    \"amounts\": [10, 25, 50, 100, 250],\n    \"variant\": \"control\",\n    \"disclaimer\": {\n      \"federal\": { \"text\": \"Paid for by Friends of Progress\", \"statute_citation\": \"11 CFR 110.11\", \"ai_disclosure_required\": false, \"ai_disclosure_text\": null },\n      \"state\": null,\n      \"combined\": \"Paid for by Friends of Progress\"\n    },\n    \"jurisdiction\": \"US-CA\",\n    \"campaign\": \"demo\"\n  }\n}\n```\n\n### `POST /submit`\n\nRecords a submission (donation click, petition signature, or GOTV pledge).\n\n**Request body:**\n```json\n{\n  \"page_id\": \"uuid\",\n  \"campaign_id\": \"uuid\",\n  \"type\": \"donation_click\",\n  \"data\": {},\n  \"visitor_id\": \"uuid\",\n  \"variant\": \"control\",\n  \"turnstile_token\": \"...\"\n}\n```\n\n**Submission types and required fields:**\n- `donation_click` -- no required fields\n- `petition_sign` -- `first_name`, `last_name`, `email`, `zip`\n- `gotv_pledge` -- `first_name`, `zip`\n\n**Response (200):** `{ \"data\": { \"ok\": true } }`\n\n### `GET /embed`\n\nReturns a JavaScript snippet that embeds an action page via Shadow DOM iframe.\n\n**Query parameters:**\n- `slug` (required) -- action page slug\n- `campaign` (optional) -- campaign slug\n\n**Response:** `application/javascript` content\n\n### `GET /stats` (authenticated)\n\nReturns A/B variant statistics for a page.\n\n**Query parameters:**\n- `page_id` (required) -- action page ID\n- `campaign` (optional) -- campaign slug\n\n**Response (200):**\n```json\n{\n  \"data\": {\n    \"page_id\": \"...\",\n    \"campaign\": null,\n    \"variants\": [\n      { \"variant\": \"control\", \"impressions\": 100, \"conversions\": 12, \"conversion_rate\": 0.12 },\n      { \"variant\": \"urgency\", \"impressions\": 98, \"conversions\": 18, \"conversion_rate\": 0.1837 }\n    ]\n  }\n}\n```\n\n## Compliance Dataset\n\nThe `data/disclaimers/` directory contains structured JSON files with political advertisement disclaimer requirements:\n\n- `federal.json` -- FEC rules under 11 CFR 110.11 (digital, print, broadcast)\n- `states/*.json` -- 10 state jurisdictions (CA, CO, DC, FL, GA, MD, NY, PA, TX, VA)\n- `schema.json` -- JSON schema for disclaimer records\n- `LEGAL_NOTES.md` -- scope, limitations, and legal disclaimers about the dataset\n- `VERIFICATION.md` -- verification methodology and source citations\n\nEach record includes:\n- `jurisdiction`, `type` (digital/print/broadcast/sms/email), `context` (general/candidate_authorized/independent_expenditure/pac)\n- `required_text` with `{variable}` interpolation\n- `ai_disclosure_required`, `ai_disclosure_text`, `ai_disclosure_statute`\n- `statute_citation`, `effective_date`, `last_verified`, `source_url`\n\n**This is not legal advice.** See `data/disclaimers/LEGAL_NOTES.md` for full caveats. Always consult qualified election law counsel.\n\n### Contributing to the dataset\n\nAdd a new state by creating `data/disclaimers/states/{state_code}.json` following the schema in `schema.json`. Include `source_url` and `last_verified` for every record.\n\n## Architecture\n\nThe plugin follows emdash's two-entrypoint pattern:\n\n- **`src/index.ts`** -- `PluginDescriptor` factory (runs at build time in Vite). Declares ID, version, capabilities, storage collections, and admin pages.\n- **`src/sandbox-entry.ts`** -- `definePlugin()` with runtime hooks and routes (runs at request time in the sandbox).\n\n### Pure modules (`src/modules/`)\n\nAll business logic lives in pure, framework-free modules with no emdash imports:\n\n| Module | Purpose |\n|---|---|\n| `ab-assign.ts` | Deterministic hash-based A/B variant assignment |\n| `disclaimers.ts` | Disclaimer resolution with jurisdiction layering and variable interpolation |\n| `geo-ask.ts` | Jurisdiction resolution from geo context |\n| `validate.ts` | Submission input validation and sanitization |\n\nThese modules have 100% unit test coverage and can be tested without emdash.\n\n### Site-level rendering\n\nThe Astro page at `src/pages/action/[slug].astro` is **not part of this plugin** -- it is a site-level template that calls the plugin's API routes and renders the response. Each site controls its own presentation. The plugin only provides data and compliance logic.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}