Compare commits

..
8 Commits
Author SHA1 Message Date
zbatte e075874186 create 7 files 2026-07-03 00:48:03 -06:00
zbatte 25e39beef8 update .ht.sqlite 2026-07-02 23:59:35 -06:00
zbatte d561326b84 update .ht.sqlite 2026-07-02 23:15:38 -06:00
zbatteandClaude Fable 5 d607badac1 fix(instanda): register the add-on on gform_loaded at the intended priority 5
The priority argument was passed to Guard::wrap() (which takes a single callable
and silently ignored it) instead of to add_action(), so the add-on registered at
the default priority 10 rather than 5. Move the 5 to add_action's third argument
so registration runs at the same early priority as Gravity Forms' own bundled
add-on bootstraps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:54:01 -06:00
zbatteandClaude Fable 5 5d2e9cb7e5 fix(instanda): hygiene pass + v1.1.0 (uninstall, notices, cron, resubmit, theme)
- F-15: uninstall now removes ALL espinstanda_* options and transients (incl. the
  encrypted password and its self-provisioned key).
- F-16: register the "Gravity Forms required" admin notice unconditionally so it is
  reachable when GF is inactive (gform_loaded never fires then).
- F-22: self-heal the daily retention cron on admin_init (matches the schema self-heal).
- F-19: re-validate the stored payload via Field_Mapper::validate_payload() before a
  resubmit (DB-tamper trust boundary).
- F-20: increment the retry count on resubmit and dispatch the previously-dead
  espinstanda_resubmit_success notification event.
- F-23: emit the theme's Form-5 date-range JS only on pages that embed a Gravity Form
  and disconnect the MutationObserver (was observing the DOM subtree forever).
- F-24: bump plugin to 1.1.0; add readme.txt changelog documenting the audit fixes and
  the required ops actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:20:10 -06:00
zbatteandClaude Fable 5 0732dd662d fix(instanda): rate limiting, UTC timestamps, and close silent-failure paths
- F-06: throttle the public create per IP (20/h) and per email (5/h) before the
  paid API call and the PII write.
- F-11: wrap the create path in try/catch - on an unexpected error fail closed for
  that submission (no quote-less entry), alert admins, keep the form working.
- F-14: abort the StartQuote when the write-ahead insert fails (no unaudited call);
  fire the failure alert even when storage is disabled (build an in-memory tx).
- F-10/F-17: treat a 2xx with a missing/empty quoteRef as ambiguous
  (possible_duplicate=1), not a hard failure - a quote may exist.
- F-07: write created_at/delivered_at/event timestamps in UTC to match the gmdate()
  retention/dedupe cutoffs; display in site-local via get_date_from_gmt.
- F-09: log (no silent return) when async record_feed_result cannot resolve a
  delivered transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:13:06 -06:00
zbatteandClaude Fable 5 2b1c226d46 fix(instanda): content-based dedupe, resubmit guards, PII retention
- F-01: derive the dedupe key from submission content (form id + mapped values)
  instead of a per-request token, so identical re-POSTs / retry-after-error
  collapse to the existing quote. Keeps the per-request lock; adds a content lock
  race guard and a 30-min recent_delivered_by_dedupe short-circuit.
- F-12: refuse resubmit of an already-delivered transaction (AJAX + worker).
- F-18: scope the resubmit self-recovery guard to the content dedupe key so a
  customer's different event is no longer skipped.
- F-05: anonymize PII on stale failed/pending rows via the cron; add delete_by_email
  and wire WP's Erase Personal Data eraser.
- F-13: exempt stranded delivered rows (no entry_id) from the retention purge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:06:13 -06:00
zbatteandClaude Fable 5 4d7abdbb3b fix(instanda): set env on go-live, correct secrets docblock, add feed advisory
- F-03: switch_to_live() now sets espinstanda_env=live so environment() actually
  resolves to Live after go-live (was leaving real quotes on the Test endpoint).
- F-04: correct the class-config docblock overstating the at-rest guarantee; it
  only holds when SPG_INSTANDA_KEY is a constant (else the key is self-provisioned
  into the same DB as the ciphertext).
- F-02: add advisory A5 "exactly one active INSTANDA feed" to catch a stray
  test-form feed that also creates quotes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:06:10 -06:00
23 changed files with 2483 additions and 68 deletions
@@ -0,0 +1,5 @@
[ 53923ms] [INFO] Autofocus processing was blocked because a document's URL has a fragment '#gf_5'. @ https://hubinsurance.mystagingwebsite.com/:0
[ 111627ms] [LOG] JQMIGRATE: Migrate is installed, version 3.4.0 @ https://consumeruat.instanda.us/Scripts/jquery-migrate-3.4.0.min.js:1
[ 112217ms] [LOG] initial resources loaded. @ https://consumeruat.instanda.us/Public/PublicUatUserLogin:208
[ 112218ms] [WARNING] Site Environment is not present in Instanda variables. Using URL as backup. @ https://blue-sand-09e2e8710.3.azurestaticapps.net/src/instanda.scripts.js:37
[ 112355ms] [LOG] HCC Engine found. Initializing... @ https://blue-sand-09e2e8710.3.azurestaticapps.net/src/UAT/SPG/instanda.spg.general.js:41
@@ -0,0 +1,366 @@
- generic [active] [ref=e1]:
- link "Skip to content" [ref=e2] [cursor=pointer]:
- /url: "#content"
- banner "Site" [ref=e3]:
- generic [ref=e4]:
- link "HUB Insurance" [ref=e6] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- img "HUB Insurance" [ref=e7]
- navigation "Primary" [ref=e8]:
- list [ref=e11]:
- listitem [ref=e12]:
- link "About" [ref=e13] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/about
- listitem [ref=e14]:
- link "Contact" [ref=e15] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- listitem [ref=e16]:
- link "Get a Quote" [ref=e17] [cursor=pointer]:
- /url: "#get-a-quote"
- generic [ref=e19]:
- heading "Special Event Insurance" [level=1] [ref=e20]:
- text: Special
- emphasis [ref=e21]: Event
- text: Insurance
- paragraph
- paragraph [ref=e22]: "{Hero Content} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
- list [ref=e23]:
- listitem [ref=e24]: Lorem ipsum dolor
- listitem [ref=e25]: Lorem ipsum dolor
- listitem [ref=e26]: Lorem ipsum dolor
- listitem [ref=e27]: Lorem ipsum dolor
- paragraph
- link "Hero CTA Lorem" [ref=e28] [cursor=pointer]:
- /url: https://www.getfused.com
- main [ref=e32]:
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]:
- paragraph [ref=e36]: How it Works
- heading "Three Simple Steps" [level=2] [ref=e37]
- generic [ref=e38]:
- paragraph
- generic [ref=e39]:
- generic [ref=e40]:
- heading "Start a Quote" [level=2] [ref=e41]
- paragraph
- paragraph [ref=e42]: An asterisk (*) indicates a required field.
- generic [ref=e43]:
- generic [ref=e45]:
- generic [ref=e46]:
- generic [ref=e47]:
- text: Event Role
- generic [ref=e49]: "*"
- combobox "Event Role *" [ref=e51]:
- option [selected]
- option "Event Host or Organizer"
- option "Vendor, Exhibitor or Subcontractor"
- option "Performer, Entertainer or DJ"
- generic [ref=e52]:
- generic [ref=e53]:
- text: Activity / Event Type
- generic [ref=e55]: "*"
- combobox "Activity / Event Type *" [ref=e57]:
- option [selected]
- option "Animal Adoption Event"
- option "Animal Shows"
- option "Antique Show"
- option "Art Festival"
- option "Art Show"
- option "Auction"
- option "Award Presentation"
- option "Baby Shower"
- option "Bachelor Party"
- option "Bachelorette Party"
- option "Badminton Tournament / Event"
- option "Bake Sale"
- option "Ballet"
- option "Banquet"
- option "Baptism"
- option "Bar/Bat Mitzvah"
- option "Bar/Pub Crawl"
- option "Baseball Tournament / Event"
- option "Basketball Tournament / Event"
- option "Battle of the Bands"
- option "Bazaar"
- option "Beauty Pageant"
- option "Beer Garden/Tent"
- option "Beer Mile"
- option "Billiards Tournament / Event"
- option "Bingo Games"
- option "Birthday Party"
- option "Block Parties/Street Closures/Street Fairs"
- option "BMX Tournament / Event"
- option "Boat Show (On Land)"
- option "Boat Show"
- option "Bocce Ball"
- option "Body Building Tournament / Event"
- option "Book Fair"
- option "Book Signing"
- option "Bowling Tournament / Event"
- option "Boxing Tournament / Event"
- option "Breakfast"
- option "Bridal Shower"
- option "Brunch - No Admission Charge - Invite Only"
- option "Business Meeting"
- option "Business Party"
- option "Cage Fight Tournament / Event"
- option "Cannabis Event"
- option "Car Show"
- option "Car Wash"
- option "Card Games"
- option "Casino Night"
- option "Cheerleading Tournament / Event"
- option "Chess Tournament"
- option "Church Service or Meeting"
- option "Cocktail Party - No Admission Charge - Invite Only"
- option "College Reunion"
- option "Comedy Show"
- option "Company Retreats"
- option "Concert - Choir"
- option "Concert - Classic Rock"
- option "Concert - Classical"
- option "Concert - Country"
- option "Concert - Doo-Wop"
- option "Concert - Electronic/EDM"
- option "Concert - Folk Guitar"
- option "Concert - Foreign"
- option "Concert - Heavy Metal"
- option "Concert - High Profile Performer"
- option "Concert - Hip Hop/Rap"
- option "Concert - Jazz"
- option "Concert - Music Festivals"
- option "Concert - Opera"
- option "Concert - Orchestra"
- option "Concert - Pop"
- option "Concert - R&B"
- option "Concert - Reggae"
- option "Concert - Rock"
- option "Concert - SKA"
- option "Concert - Soul"
- option "Concert - Symphony"
- option "Concert - Tribute Bands"
- option "Consumer Show"
- option "Cooking Competition"
- option "Corn Mazes"
- option "Cornhole Tournament / Event"
- option "Costume Contest"
- option "Craft Show"
- option "Cycling"
- option "Dance Competition"
- option "Dance Recital"
- option "Dance Team Tournament / Event"
- option "Debutante Ball"
- option "Demolition Derby"
- option "Dinner - No Admission Charge - Invite Only"
- option "Dirt Bike Tournament / Event"
- option "Dog Show"
- option "Drive-In Movie/Concert"
- option "Egg Hunt"
- option "Engagement Party"
- option "Exotic Dancer Event"
- option "Extreme Sporting Tournament / Event"
- option "Family Reunion"
- option "Farm Show"
- option "Farmer's Market"
- option "Fashion Show"
- option "Fencing Tournament / Event"
- option "Field Hockey Tournament / Event"
- option "Film Screenings"
- option "Film Shoot (In Studio)"
- option "First Communion"
- option "Fishing Tournament/Event"
- option "Flag Football Tournament / Event"
- option "Flea Market"
- option "Flower Show"
- option "Food Festival"
- option "Football Tournament / Event"
- option "Fraternity/Sorority Party"
- option "Frisbee Golf Tournament / Event"
- option "Fundraiser Dinner"
- option "Funeral Service"
- option "Gala with Seated Dinner"
- option "Garden Show"
- option "Gift Wrapping Party"
- option "Golf Tournament / Event"
- option "Gymnastics Tournament / Event"
- option "Haunted Houses"
- option "Health Fair"
- option "High School Graduation Party"
- option "High School Reunion"
- option "High School Graduation Ceremony"
- option "Holiday Character (Santa, Easter Bunny, etc.)"
- option "Holiday Party No Admission Charge Invite Only"
- option "Home and Garden Tour"
- option "Horse Show/Competition"
- option "Ice Hockey Tournament / Event"
- option "Ice Skating Show / Event"
- option "Indoor Convention"
- option "Indoor Tradeshow"
- option "Jewelry Show"
- option "Job Fair"
- option "Lacrosse Tournament / Event"
- option "Lecture"
- option "Livestock Auction"
- option "Luncheon"
- option "Magic Show"
- option "Marching Band Tournament / Event"
- option "Mardis Gras Party"
- option "Martial Arts Tournament / Event"
- option "Math Tournament"
- option "Medical Conference"
- option "Meeting"
- option "Memorial Service"
- option "Mixed Martial Arts Tournament / Event"
- option "Motocross"
- option "Motorcycle or Race Car On Road Competition"
- option "Motorcycle Run"
- option "Mud Bogs"
- option "New Year's Eve Party"
- option "Parade with Floats"
- option "Parade without Floats"
- option "Piano Recital"
- option "Pickleball Tournament / Event"
- option "Picnics"
- option "Pig Roast"
- option "Play Readings"
- option "Poetry Reading"
- option "Poker Runs"
- option "Political Rally"
- option "Prom"
- option "Quinceanera"
- option "Renaissance Fairs"
- option "Retirement Party"
- option "Road Race (13.2M or longer)"
- option "Road Race (Running only; 13.1M or less)"
- option "Rummage Sale"
- option "Seminar"
- option "Skateboarding Tournament / Event"
- option "Soccer Tournament / Event"
- option "Softball Tournament / Event"
- option "Spikeball Tournament / Event"
- option "Surfing Tournament / Event"
- option "Swimming Tournament / Event"
- option "Tap Dancing Show"
- option "Tennis Tournament / Event"
- option "Theatrical Production"
- option "Toy Drive"
- option "Track and Field Tournament / Event"
- option "Triathlon Tournament / Event"
- option "Trivia Night"
- option "Ultimate Frisbee Tournament / Event"
- option "Valentine's Day Party"
- option "Video Games Contests"
- option "Volleyball Tournament / Event"
- option "Voter Registration"
- option "Walk-a-thon Tournament / Event"
- option "Wedding Anniversary Party"
- option "Wedding Ceremony and/or Reception"
- option "Weightlifting Tournament / Event"
- option "Wrestling Tournament / Event"
- option "Yard Sale"
- option "Yoga Tournament / Event"
- option "Zumba Tournament / Event"
- generic [ref=e58]:
- generic [ref=e59]:
- text: Event Location
- generic [ref=e61]: "*"
- combobox "Event Location *" [ref=e63]:
- option [selected]
- option "Alabama"
- option "Alaska"
- option "Arizona"
- option "Arkansas"
- option "California"
- option "Colorado"
- option "Connecticut"
- option "Delaware"
- option "District of Columbia"
- option "Florida"
- option "Georgia"
- option "Hawaii"
- option "Idaho"
- option "Illinois"
- option "Indiana"
- option "Iowa"
- option "Kansas"
- option "Kentucky"
- option "Louisiana"
- option "Maine"
- option "Maryland"
- option "Massachusetts"
- option "Michigan"
- option "Minnesota"
- option "Mississippi"
- option "Missouri"
- option "Montana"
- option "Nebraska"
- option "Nevada"
- option "New Hampshire"
- option "New Jersey"
- option "New Mexico"
- option "New York"
- option "North Carolina"
- option "North Dakota"
- option "Ohio"
- option "Oklahoma"
- option "Oregon"
- option "Pennsylvania"
- option "Rhode Island"
- option "South Carolina"
- option "South Dakota"
- option "Tennessee"
- option "Texas"
- option "Utah"
- option "Vermont"
- option "Virginia"
- option "Washington"
- option "West Virginia"
- option "Wisconsin"
- option "Wyoming"
- generic [ref=e64]:
- generic [ref=e65]:
- text: Maximum Daily Attendance
- generic [ref=e67]: "*"
- spinbutton "Maximum Daily Attendance *" [ref=e69]
- generic [ref=e70]:
- generic [ref=e71]:
- text: Event Start Date
- generic [ref=e73]: "*"
- generic [ref=e74]:
- textbox "Event Start Date *" [ref=e75]:
- /placeholder: yyyy-mm-dd
- generic [ref=e76]: YYYY dash MM dash DD
- generic [ref=e77]:
- generic [ref=e78]:
- text: Event End Date
- generic [ref=e80]: "*"
- generic [ref=e81]:
- textbox "Event End Date *" [ref=e82]:
- /placeholder: yyyy-mm-dd
- generic [ref=e83]: YYYY dash MM dash DD
- generic [ref=e84]:
- generic [ref=e85]:
- text: Customer Email Address
- generic [ref=e87]: "*"
- textbox "Customer Email Address *" [ref=e89]
- button "Submit" [ref=e91] [cursor=pointer]
- paragraph
- paragraph
- generic [ref=e92]:
- complementary [ref=e97]:
- list [ref=e99]:
- listitem [ref=e100]:
- link "Home" [ref=e101] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- listitem [ref=e102]:
- link "Blog" [ref=e103] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/blog
- listitem [ref=e104]:
- link "Contact" [ref=e105] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- contentinfo "Site" [ref=e108]:
- generic [ref=e110]:
- text: © 2026 HUB Insurance • Built with
- link "GeneratePress" [ref=e111] [cursor=pointer]:
- /url: https://generatepress.com
@@ -0,0 +1,369 @@
- generic [ref=e1]:
- link "Skip to content" [ref=e2] [cursor=pointer]:
- /url: "#content"
- banner "Site" [ref=e3]:
- generic [ref=e4]:
- link "HUB Insurance" [ref=e6] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- img "HUB Insurance" [ref=e7]
- navigation "Primary" [ref=e8]:
- list [ref=e11]:
- listitem [ref=e12]:
- link "About" [ref=e13] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/about
- listitem [ref=e14]:
- link "Contact" [ref=e15] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- listitem [ref=e16]:
- link "Get a Quote" [ref=e17] [cursor=pointer]:
- /url: "#get-a-quote"
- generic [ref=e19]:
- heading "Special Event Insurance" [level=1] [ref=e20]:
- text: Special
- emphasis [ref=e21]: Event
- text: Insurance
- paragraph
- paragraph [ref=e22]: "{Hero Content} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
- list [ref=e23]:
- listitem [ref=e24]: Lorem ipsum dolor
- listitem [ref=e25]: Lorem ipsum dolor
- listitem [ref=e26]: Lorem ipsum dolor
- listitem [ref=e27]: Lorem ipsum dolor
- paragraph
- link "Hero CTA Lorem" [ref=e28] [cursor=pointer]:
- /url: https://www.getfused.com
- main [ref=e32]:
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]:
- paragraph [ref=e36]: How it Works
- heading "Three Simple Steps" [level=2] [ref=e37]
- generic [ref=e38]:
- paragraph
- generic [ref=e39]:
- generic [ref=e40]:
- heading "Start a Quote" [level=2] [ref=e41]
- paragraph
- paragraph [ref=e42]: An asterisk (*) indicates a required field.
- generic [ref=e43]:
- generic [ref=e45]:
- generic [ref=e46]:
- generic [ref=e47]:
- text: Event Role
- generic [ref=e49]: "*"
- combobox "Event Role *" [ref=e51]:
- option
- option "Event Host or Organizer" [selected]
- option "Vendor, Exhibitor or Subcontractor"
- option "Performer, Entertainer or DJ"
- generic [ref=e52]:
- generic [ref=e53]:
- text: Activity / Event Type
- generic [ref=e55]: "*"
- combobox "Activity / Event Type *" [ref=e57]:
- option
- option "Animal Adoption Event"
- option "Animal Shows"
- option "Antique Show"
- option "Art Festival"
- option "Art Show"
- option "Auction"
- option "Award Presentation"
- option "Baby Shower"
- option "Bachelor Party"
- option "Bachelorette Party"
- option "Badminton Tournament / Event"
- option "Bake Sale"
- option "Ballet"
- option "Banquet"
- option "Baptism"
- option "Bar/Bat Mitzvah"
- option "Bar/Pub Crawl"
- option "Baseball Tournament / Event"
- option "Basketball Tournament / Event"
- option "Battle of the Bands"
- option "Bazaar"
- option "Beauty Pageant"
- option "Beer Garden/Tent"
- option "Beer Mile"
- option "Billiards Tournament / Event"
- option "Bingo Games"
- option "Birthday Party" [selected]
- option "Block Parties/Street Closures/Street Fairs"
- option "BMX Tournament / Event"
- option "Boat Show (On Land)"
- option "Boat Show"
- option "Bocce Ball"
- option "Body Building Tournament / Event"
- option "Book Fair"
- option "Book Signing"
- option "Bowling Tournament / Event"
- option "Boxing Tournament / Event"
- option "Breakfast"
- option "Bridal Shower"
- option "Brunch - No Admission Charge - Invite Only"
- option "Business Meeting"
- option "Business Party"
- option "Cage Fight Tournament / Event"
- option "Cannabis Event"
- option "Car Show"
- option "Car Wash"
- option "Card Games"
- option "Casino Night"
- option "Cheerleading Tournament / Event"
- option "Chess Tournament"
- option "Church Service or Meeting"
- option "Cocktail Party - No Admission Charge - Invite Only"
- option "College Reunion"
- option "Comedy Show"
- option "Company Retreats"
- option "Concert - Choir"
- option "Concert - Classic Rock"
- option "Concert - Classical"
- option "Concert - Country"
- option "Concert - Doo-Wop"
- option "Concert - Electronic/EDM"
- option "Concert - Folk Guitar"
- option "Concert - Foreign"
- option "Concert - Heavy Metal"
- option "Concert - High Profile Performer"
- option "Concert - Hip Hop/Rap"
- option "Concert - Jazz"
- option "Concert - Music Festivals"
- option "Concert - Opera"
- option "Concert - Orchestra"
- option "Concert - Pop"
- option "Concert - R&B"
- option "Concert - Reggae"
- option "Concert - Rock"
- option "Concert - SKA"
- option "Concert - Soul"
- option "Concert - Symphony"
- option "Concert - Tribute Bands"
- option "Consumer Show"
- option "Cooking Competition"
- option "Corn Mazes"
- option "Cornhole Tournament / Event"
- option "Costume Contest"
- option "Craft Show"
- option "Cycling"
- option "Dance Competition"
- option "Dance Recital"
- option "Dance Team Tournament / Event"
- option "Debutante Ball"
- option "Demolition Derby"
- option "Dinner - No Admission Charge - Invite Only"
- option "Dirt Bike Tournament / Event"
- option "Dog Show"
- option "Drive-In Movie/Concert"
- option "Egg Hunt"
- option "Engagement Party"
- option "Exotic Dancer Event"
- option "Extreme Sporting Tournament / Event"
- option "Family Reunion"
- option "Farm Show"
- option "Farmer's Market"
- option "Fashion Show"
- option "Fencing Tournament / Event"
- option "Field Hockey Tournament / Event"
- option "Film Screenings"
- option "Film Shoot (In Studio)"
- option "First Communion"
- option "Fishing Tournament/Event"
- option "Flag Football Tournament / Event"
- option "Flea Market"
- option "Flower Show"
- option "Food Festival"
- option "Football Tournament / Event"
- option "Fraternity/Sorority Party"
- option "Frisbee Golf Tournament / Event"
- option "Fundraiser Dinner"
- option "Funeral Service"
- option "Gala with Seated Dinner"
- option "Garden Show"
- option "Gift Wrapping Party"
- option "Golf Tournament / Event"
- option "Gymnastics Tournament / Event"
- option "Haunted Houses"
- option "Health Fair"
- option "High School Graduation Party"
- option "High School Reunion"
- option "High School Graduation Ceremony"
- option "Holiday Character (Santa, Easter Bunny, etc.)"
- option "Holiday Party No Admission Charge Invite Only"
- option "Home and Garden Tour"
- option "Horse Show/Competition"
- option "Ice Hockey Tournament / Event"
- option "Ice Skating Show / Event"
- option "Indoor Convention"
- option "Indoor Tradeshow"
- option "Jewelry Show"
- option "Job Fair"
- option "Lacrosse Tournament / Event"
- option "Lecture"
- option "Livestock Auction"
- option "Luncheon"
- option "Magic Show"
- option "Marching Band Tournament / Event"
- option "Mardis Gras Party"
- option "Martial Arts Tournament / Event"
- option "Math Tournament"
- option "Medical Conference"
- option "Meeting"
- option "Memorial Service"
- option "Mixed Martial Arts Tournament / Event"
- option "Motocross"
- option "Motorcycle or Race Car On Road Competition"
- option "Motorcycle Run"
- option "Mud Bogs"
- option "New Year's Eve Party"
- option "Parade with Floats"
- option "Parade without Floats"
- option "Piano Recital"
- option "Pickleball Tournament / Event"
- option "Picnics"
- option "Pig Roast"
- option "Play Readings"
- option "Poetry Reading"
- option "Poker Runs"
- option "Political Rally"
- option "Prom"
- option "Quinceanera"
- option "Renaissance Fairs"
- option "Retirement Party"
- option "Road Race (13.2M or longer)"
- option "Road Race (Running only; 13.1M or less)"
- option "Rummage Sale"
- option "Seminar"
- option "Skateboarding Tournament / Event"
- option "Soccer Tournament / Event"
- option "Softball Tournament / Event"
- option "Spikeball Tournament / Event"
- option "Surfing Tournament / Event"
- option "Swimming Tournament / Event"
- option "Tap Dancing Show"
- option "Tennis Tournament / Event"
- option "Theatrical Production"
- option "Toy Drive"
- option "Track and Field Tournament / Event"
- option "Triathlon Tournament / Event"
- option "Trivia Night"
- option "Ultimate Frisbee Tournament / Event"
- option "Valentine's Day Party"
- option "Video Games Contests"
- option "Volleyball Tournament / Event"
- option "Voter Registration"
- option "Walk-a-thon Tournament / Event"
- option "Wedding Anniversary Party"
- option "Wedding Ceremony and/or Reception"
- option "Weightlifting Tournament / Event"
- option "Wrestling Tournament / Event"
- option "Yard Sale"
- option "Yoga Tournament / Event"
- option "Zumba Tournament / Event"
- generic [ref=e58]:
- generic [ref=e59]:
- text: Event Location
- generic [ref=e61]: "*"
- combobox "Event Location *" [ref=e63]:
- option
- option "Alabama"
- option "Alaska"
- option "Arizona"
- option "Arkansas"
- option "California"
- option "Colorado"
- option "Connecticut"
- option "Delaware"
- option "District of Columbia"
- option "Florida"
- option "Georgia"
- option "Hawaii"
- option "Idaho"
- option "Illinois"
- option "Indiana"
- option "Iowa"
- option "Kansas"
- option "Kentucky"
- option "Louisiana"
- option "Maine"
- option "Maryland"
- option "Massachusetts"
- option "Michigan"
- option "Minnesota"
- option "Mississippi"
- option "Missouri"
- option "Montana"
- option "Nebraska"
- option "Nevada"
- option "New Hampshire"
- option "New Jersey"
- option "New Mexico"
- option "New York" [selected]
- option "North Carolina"
- option "North Dakota"
- option "Ohio"
- option "Oklahoma"
- option "Oregon"
- option "Pennsylvania"
- option "Rhode Island"
- option "South Carolina"
- option "South Dakota"
- option "Tennessee"
- option "Texas"
- option "Utah"
- option "Vermont"
- option "Virginia"
- option "Washington"
- option "West Virginia"
- option "Wisconsin"
- option "Wyoming"
- generic [ref=e64]:
- generic [ref=e65]:
- text: Maximum Daily Attendance
- generic [ref=e67]: "*"
- spinbutton "Maximum Daily Attendance *" [ref=e69]: "100"
- generic [ref=e70]:
- generic [ref=e71]:
- text: Event Start Date
- generic [ref=e73]: "*"
- generic [ref=e74]:
- textbox "Event Start Date *" [ref=e75]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-01
- generic [ref=e76]: YYYY dash MM dash DD
- generic [ref=e77]:
- generic [ref=e78]:
- text: Event End Date
- generic [ref=e80]: "*"
- generic [ref=e81]:
- textbox "Event End Date *" [ref=e82]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-03
- generic [ref=e83]: YYYY dash MM dash DD
- generic [ref=e84]:
- generic [ref=e85]:
- text: Customer Email Address
- generic [ref=e87]: "*"
- textbox "Customer Email Address *" [ref=e89]: instanda-smoke-20260703@example.com
- button "Submit" [active] [ref=e91] [cursor=pointer]
- paragraph
- paragraph
- generic [ref=e92]:
- complementary [ref=e97]:
- list [ref=e99]:
- listitem [ref=e100]:
- link "Home" [ref=e101] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- listitem [ref=e102]:
- link "Blog" [ref=e103] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/blog
- listitem [ref=e104]:
- link "Contact" [ref=e105] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- contentinfo "Site" [ref=e108]:
- generic [ref=e110]:
- text: © 2026 HUB Insurance • Built with
- link "GeneratePress" [ref=e111] [cursor=pointer]:
- /url: https://generatepress.com
- text:  
@@ -0,0 +1,377 @@
- generic [ref=e1]:
- link "Skip to content" [ref=e2] [cursor=pointer]:
- /url: "#content"
- banner "Site" [ref=e3]:
- generic [ref=e4]:
- link "HUB Insurance" [ref=e6] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- img "HUB Insurance" [ref=e7]
- navigation "Primary" [ref=e8]:
- list [ref=e11]:
- listitem [ref=e12]:
- link "About" [ref=e13] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/about
- listitem [ref=e14]:
- link "Contact" [ref=e15] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- listitem [ref=e16]:
- link "Get a Quote" [ref=e17] [cursor=pointer]:
- /url: "#get-a-quote"
- generic [ref=e19]:
- heading "Special Event Insurance" [level=1] [ref=e20]:
- text: Special
- emphasis [ref=e21]: Event
- text: Insurance
- paragraph
- paragraph [ref=e22]: "{Hero Content} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
- list [ref=e23]:
- listitem [ref=e24]: Lorem ipsum dolor
- listitem [ref=e25]: Lorem ipsum dolor
- listitem [ref=e26]: Lorem ipsum dolor
- listitem [ref=e27]: Lorem ipsum dolor
- paragraph
- link "Hero CTA Lorem" [ref=e28] [cursor=pointer]:
- /url: https://www.getfused.com
- main [ref=e32]:
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]:
- paragraph [ref=e36]: How it Works
- heading "Three Simple Steps" [level=2] [ref=e37]
- generic [ref=e38]:
- paragraph
- generic [ref=e39]:
- generic [active] [ref=e115]:
- text: There was a problem with your submission. Please review the fields below.
- list [ref=e116]:
- listitem [ref=e117]:
- 'link "Customer Email Address: The email address entered is invalid." [ref=e118] [cursor=pointer]':
- /url: "#field_5_10"
- generic [ref=e119]:
- heading "Start a Quote" [level=2] [ref=e120]
- paragraph
- paragraph [ref=e121]: An asterisk (*) indicates a required field.
- generic [ref=e122]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- text: Event Role
- generic [ref=e128]: "*"
- combobox "Event Role *" [ref=e130]:
- option
- option "Event Host or Organizer" [selected]
- option "Vendor, Exhibitor or Subcontractor"
- option "Performer, Entertainer or DJ"
- generic [ref=e131]:
- generic [ref=e132]:
- text: Activity / Event Type
- generic [ref=e134]: "*"
- combobox "Activity / Event Type *" [ref=e136]:
- option
- option "Animal Adoption Event"
- option "Animal Shows"
- option "Antique Show"
- option "Art Festival"
- option "Art Show"
- option "Auction"
- option "Award Presentation"
- option "Baby Shower"
- option "Bachelor Party"
- option "Bachelorette Party"
- option "Badminton Tournament / Event"
- option "Bake Sale"
- option "Ballet"
- option "Banquet"
- option "Baptism"
- option "Bar/Bat Mitzvah"
- option "Bar/Pub Crawl"
- option "Baseball Tournament / Event"
- option "Basketball Tournament / Event"
- option "Battle of the Bands"
- option "Bazaar"
- option "Beauty Pageant"
- option "Beer Garden/Tent"
- option "Beer Mile"
- option "Billiards Tournament / Event"
- option "Bingo Games"
- option "Birthday Party" [selected]
- option "Block Parties/Street Closures/Street Fairs"
- option "BMX Tournament / Event"
- option "Boat Show (On Land)"
- option "Boat Show"
- option "Bocce Ball"
- option "Body Building Tournament / Event"
- option "Book Fair"
- option "Book Signing"
- option "Bowling Tournament / Event"
- option "Boxing Tournament / Event"
- option "Breakfast"
- option "Bridal Shower"
- option "Brunch - No Admission Charge - Invite Only"
- option "Business Meeting"
- option "Business Party"
- option "Cage Fight Tournament / Event"
- option "Cannabis Event"
- option "Car Show"
- option "Car Wash"
- option "Card Games"
- option "Casino Night"
- option "Cheerleading Tournament / Event"
- option "Chess Tournament"
- option "Church Service or Meeting"
- option "Cocktail Party - No Admission Charge - Invite Only"
- option "College Reunion"
- option "Comedy Show"
- option "Company Retreats"
- option "Concert - Choir"
- option "Concert - Classic Rock"
- option "Concert - Classical"
- option "Concert - Country"
- option "Concert - Doo-Wop"
- option "Concert - Electronic/EDM"
- option "Concert - Folk Guitar"
- option "Concert - Foreign"
- option "Concert - Heavy Metal"
- option "Concert - High Profile Performer"
- option "Concert - Hip Hop/Rap"
- option "Concert - Jazz"
- option "Concert - Music Festivals"
- option "Concert - Opera"
- option "Concert - Orchestra"
- option "Concert - Pop"
- option "Concert - R&B"
- option "Concert - Reggae"
- option "Concert - Rock"
- option "Concert - SKA"
- option "Concert - Soul"
- option "Concert - Symphony"
- option "Concert - Tribute Bands"
- option "Consumer Show"
- option "Cooking Competition"
- option "Corn Mazes"
- option "Cornhole Tournament / Event"
- option "Costume Contest"
- option "Craft Show"
- option "Cycling"
- option "Dance Competition"
- option "Dance Recital"
- option "Dance Team Tournament / Event"
- option "Debutante Ball"
- option "Demolition Derby"
- option "Dinner - No Admission Charge - Invite Only"
- option "Dirt Bike Tournament / Event"
- option "Dog Show"
- option "Drive-In Movie/Concert"
- option "Egg Hunt"
- option "Engagement Party"
- option "Exotic Dancer Event"
- option "Extreme Sporting Tournament / Event"
- option "Family Reunion"
- option "Farm Show"
- option "Farmer's Market"
- option "Fashion Show"
- option "Fencing Tournament / Event"
- option "Field Hockey Tournament / Event"
- option "Film Screenings"
- option "Film Shoot (In Studio)"
- option "First Communion"
- option "Fishing Tournament/Event"
- option "Flag Football Tournament / Event"
- option "Flea Market"
- option "Flower Show"
- option "Food Festival"
- option "Football Tournament / Event"
- option "Fraternity/Sorority Party"
- option "Frisbee Golf Tournament / Event"
- option "Fundraiser Dinner"
- option "Funeral Service"
- option "Gala with Seated Dinner"
- option "Garden Show"
- option "Gift Wrapping Party"
- option "Golf Tournament / Event"
- option "Gymnastics Tournament / Event"
- option "Haunted Houses"
- option "Health Fair"
- option "High School Graduation Party"
- option "High School Reunion"
- option "High School Graduation Ceremony"
- option "Holiday Character (Santa, Easter Bunny, etc.)"
- option "Holiday Party No Admission Charge Invite Only"
- option "Home and Garden Tour"
- option "Horse Show/Competition"
- option "Ice Hockey Tournament / Event"
- option "Ice Skating Show / Event"
- option "Indoor Convention"
- option "Indoor Tradeshow"
- option "Jewelry Show"
- option "Job Fair"
- option "Lacrosse Tournament / Event"
- option "Lecture"
- option "Livestock Auction"
- option "Luncheon"
- option "Magic Show"
- option "Marching Band Tournament / Event"
- option "Mardis Gras Party"
- option "Martial Arts Tournament / Event"
- option "Math Tournament"
- option "Medical Conference"
- option "Meeting"
- option "Memorial Service"
- option "Mixed Martial Arts Tournament / Event"
- option "Motocross"
- option "Motorcycle or Race Car On Road Competition"
- option "Motorcycle Run"
- option "Mud Bogs"
- option "New Year's Eve Party"
- option "Parade with Floats"
- option "Parade without Floats"
- option "Piano Recital"
- option "Pickleball Tournament / Event"
- option "Picnics"
- option "Pig Roast"
- option "Play Readings"
- option "Poetry Reading"
- option "Poker Runs"
- option "Political Rally"
- option "Prom"
- option "Quinceanera"
- option "Renaissance Fairs"
- option "Retirement Party"
- option "Road Race (13.2M or longer)"
- option "Road Race (Running only; 13.1M or less)"
- option "Rummage Sale"
- option "Seminar"
- option "Skateboarding Tournament / Event"
- option "Soccer Tournament / Event"
- option "Softball Tournament / Event"
- option "Spikeball Tournament / Event"
- option "Surfing Tournament / Event"
- option "Swimming Tournament / Event"
- option "Tap Dancing Show"
- option "Tennis Tournament / Event"
- option "Theatrical Production"
- option "Toy Drive"
- option "Track and Field Tournament / Event"
- option "Triathlon Tournament / Event"
- option "Trivia Night"
- option "Ultimate Frisbee Tournament / Event"
- option "Valentine's Day Party"
- option "Video Games Contests"
- option "Volleyball Tournament / Event"
- option "Voter Registration"
- option "Walk-a-thon Tournament / Event"
- option "Wedding Anniversary Party"
- option "Wedding Ceremony and/or Reception"
- option "Weightlifting Tournament / Event"
- option "Wrestling Tournament / Event"
- option "Yard Sale"
- option "Yoga Tournament / Event"
- option "Zumba Tournament / Event"
- generic [ref=e137]:
- generic [ref=e138]:
- text: Event Location
- generic [ref=e140]: "*"
- combobox "Event Location *" [ref=e142]:
- option
- option "Alabama"
- option "Alaska"
- option "Arizona"
- option "Arkansas"
- option "California"
- option "Colorado"
- option "Connecticut"
- option "Delaware"
- option "District of Columbia"
- option "Florida"
- option "Georgia"
- option "Hawaii"
- option "Idaho"
- option "Illinois"
- option "Indiana"
- option "Iowa"
- option "Kansas"
- option "Kentucky"
- option "Louisiana"
- option "Maine"
- option "Maryland"
- option "Massachusetts"
- option "Michigan"
- option "Minnesota"
- option "Mississippi"
- option "Missouri"
- option "Montana"
- option "Nebraska"
- option "Nevada"
- option "New Hampshire"
- option "New Jersey"
- option "New Mexico"
- option "New York" [selected]
- option "North Carolina"
- option "North Dakota"
- option "Ohio"
- option "Oklahoma"
- option "Oregon"
- option "Pennsylvania"
- option "Rhode Island"
- option "South Carolina"
- option "South Dakota"
- option "Tennessee"
- option "Texas"
- option "Utah"
- option "Vermont"
- option "Virginia"
- option "Washington"
- option "West Virginia"
- option "Wisconsin"
- option "Wyoming"
- generic [ref=e143]:
- generic [ref=e144]:
- text: Maximum Daily Attendance
- generic [ref=e146]: "*"
- spinbutton "Maximum Daily Attendance *" [ref=e148]: "100"
- generic [ref=e149]:
- generic [ref=e150]:
- text: Event Start Date
- generic [ref=e152]: "*"
- generic [ref=e153]:
- textbox "Event Start Date *" [ref=e154]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-01
- generic [ref=e155]: YYYY dash MM dash DD
- generic [ref=e156]:
- generic [ref=e157]:
- text: Event End Date
- generic [ref=e159]: "*"
- generic [ref=e160]:
- textbox "Event End Date *" [ref=e161]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-03
- generic [ref=e162]: YYYY dash MM dash DD
- generic [ref=e163]:
- generic [ref=e164]:
- text: Customer Email Address
- generic [ref=e166]: "*"
- textbox "Customer Email Address *" [invalid] [ref=e168]: instanda-smoke-20260703@example.com
- generic [ref=e169]: The email address entered is invalid.
- button "Submit" [ref=e171] [cursor=pointer]
- paragraph
- paragraph
- generic [ref=e92]:
- complementary [ref=e97]:
- list [ref=e99]:
- listitem [ref=e100]:
- link "Home" [ref=e101] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- listitem [ref=e102]:
- link "Blog" [ref=e103] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/blog
- listitem [ref=e104]:
- link "Contact" [ref=e105] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- contentinfo "Site" [ref=e108]:
- generic [ref=e110]:
- text: © 2026 HUB Insurance • Built with
- link "GeneratePress" [ref=e111] [cursor=pointer]:
- /url: https://generatepress.com
- text:  
- generic [ref=e172]: "There was a problem with your submission, Please review the fields below, Customer Email Address: The email address entered is invalid,"
@@ -0,0 +1,377 @@
- generic [ref=e1]:
- link "Skip to content" [ref=e2] [cursor=pointer]:
- /url: "#content"
- banner "Site" [ref=e3]:
- generic [ref=e4]:
- link "HUB Insurance" [ref=e6] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- img "HUB Insurance" [ref=e7]
- navigation "Primary" [ref=e8]:
- list [ref=e11]:
- listitem [ref=e12]:
- link "About" [ref=e13] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/about
- listitem [ref=e14]:
- link "Contact" [ref=e15] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- listitem [ref=e16]:
- link "Get a Quote" [ref=e17] [cursor=pointer]:
- /url: "#get-a-quote"
- generic [ref=e19]:
- heading "Special Event Insurance" [level=1] [ref=e20]:
- text: Special
- emphasis [ref=e21]: Event
- text: Insurance
- paragraph
- paragraph [ref=e22]: "{Hero Content} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
- list [ref=e23]:
- listitem [ref=e24]: Lorem ipsum dolor
- listitem [ref=e25]: Lorem ipsum dolor
- listitem [ref=e26]: Lorem ipsum dolor
- listitem [ref=e27]: Lorem ipsum dolor
- paragraph
- link "Hero CTA Lorem" [ref=e28] [cursor=pointer]:
- /url: https://www.getfused.com
- main [ref=e32]:
- generic [ref=e33]:
- generic [ref=e34]:
- generic [ref=e35]:
- paragraph [ref=e36]: How it Works
- heading "Three Simple Steps" [level=2] [ref=e37]
- generic [ref=e38]:
- paragraph
- generic [ref=e39]:
- generic [ref=e115]:
- text: There was a problem with your submission. Please review the fields below.
- list [ref=e116]:
- listitem [ref=e117]:
- 'link "Customer Email Address: The email address entered is invalid." [ref=e118] [cursor=pointer]':
- /url: "#field_5_10"
- generic [ref=e119]:
- heading "Start a Quote" [level=2] [ref=e120]
- paragraph
- paragraph [ref=e121]: An asterisk (*) indicates a required field.
- generic [ref=e122]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- text: Event Role
- generic [ref=e128]: "*"
- combobox "Event Role *" [ref=e130]:
- option
- option "Event Host or Organizer" [selected]
- option "Vendor, Exhibitor or Subcontractor"
- option "Performer, Entertainer or DJ"
- generic [ref=e131]:
- generic [ref=e132]:
- text: Activity / Event Type
- generic [ref=e134]: "*"
- combobox "Activity / Event Type *" [ref=e136]:
- option
- option "Animal Adoption Event"
- option "Animal Shows"
- option "Antique Show"
- option "Art Festival"
- option "Art Show"
- option "Auction"
- option "Award Presentation"
- option "Baby Shower"
- option "Bachelor Party"
- option "Bachelorette Party"
- option "Badminton Tournament / Event"
- option "Bake Sale"
- option "Ballet"
- option "Banquet"
- option "Baptism"
- option "Bar/Bat Mitzvah"
- option "Bar/Pub Crawl"
- option "Baseball Tournament / Event"
- option "Basketball Tournament / Event"
- option "Battle of the Bands"
- option "Bazaar"
- option "Beauty Pageant"
- option "Beer Garden/Tent"
- option "Beer Mile"
- option "Billiards Tournament / Event"
- option "Bingo Games"
- option "Birthday Party" [selected]
- option "Block Parties/Street Closures/Street Fairs"
- option "BMX Tournament / Event"
- option "Boat Show (On Land)"
- option "Boat Show"
- option "Bocce Ball"
- option "Body Building Tournament / Event"
- option "Book Fair"
- option "Book Signing"
- option "Bowling Tournament / Event"
- option "Boxing Tournament / Event"
- option "Breakfast"
- option "Bridal Shower"
- option "Brunch - No Admission Charge - Invite Only"
- option "Business Meeting"
- option "Business Party"
- option "Cage Fight Tournament / Event"
- option "Cannabis Event"
- option "Car Show"
- option "Car Wash"
- option "Card Games"
- option "Casino Night"
- option "Cheerleading Tournament / Event"
- option "Chess Tournament"
- option "Church Service or Meeting"
- option "Cocktail Party - No Admission Charge - Invite Only"
- option "College Reunion"
- option "Comedy Show"
- option "Company Retreats"
- option "Concert - Choir"
- option "Concert - Classic Rock"
- option "Concert - Classical"
- option "Concert - Country"
- option "Concert - Doo-Wop"
- option "Concert - Electronic/EDM"
- option "Concert - Folk Guitar"
- option "Concert - Foreign"
- option "Concert - Heavy Metal"
- option "Concert - High Profile Performer"
- option "Concert - Hip Hop/Rap"
- option "Concert - Jazz"
- option "Concert - Music Festivals"
- option "Concert - Opera"
- option "Concert - Orchestra"
- option "Concert - Pop"
- option "Concert - R&B"
- option "Concert - Reggae"
- option "Concert - Rock"
- option "Concert - SKA"
- option "Concert - Soul"
- option "Concert - Symphony"
- option "Concert - Tribute Bands"
- option "Consumer Show"
- option "Cooking Competition"
- option "Corn Mazes"
- option "Cornhole Tournament / Event"
- option "Costume Contest"
- option "Craft Show"
- option "Cycling"
- option "Dance Competition"
- option "Dance Recital"
- option "Dance Team Tournament / Event"
- option "Debutante Ball"
- option "Demolition Derby"
- option "Dinner - No Admission Charge - Invite Only"
- option "Dirt Bike Tournament / Event"
- option "Dog Show"
- option "Drive-In Movie/Concert"
- option "Egg Hunt"
- option "Engagement Party"
- option "Exotic Dancer Event"
- option "Extreme Sporting Tournament / Event"
- option "Family Reunion"
- option "Farm Show"
- option "Farmer's Market"
- option "Fashion Show"
- option "Fencing Tournament / Event"
- option "Field Hockey Tournament / Event"
- option "Film Screenings"
- option "Film Shoot (In Studio)"
- option "First Communion"
- option "Fishing Tournament/Event"
- option "Flag Football Tournament / Event"
- option "Flea Market"
- option "Flower Show"
- option "Food Festival"
- option "Football Tournament / Event"
- option "Fraternity/Sorority Party"
- option "Frisbee Golf Tournament / Event"
- option "Fundraiser Dinner"
- option "Funeral Service"
- option "Gala with Seated Dinner"
- option "Garden Show"
- option "Gift Wrapping Party"
- option "Golf Tournament / Event"
- option "Gymnastics Tournament / Event"
- option "Haunted Houses"
- option "Health Fair"
- option "High School Graduation Party"
- option "High School Reunion"
- option "High School Graduation Ceremony"
- option "Holiday Character (Santa, Easter Bunny, etc.)"
- option "Holiday Party No Admission Charge Invite Only"
- option "Home and Garden Tour"
- option "Horse Show/Competition"
- option "Ice Hockey Tournament / Event"
- option "Ice Skating Show / Event"
- option "Indoor Convention"
- option "Indoor Tradeshow"
- option "Jewelry Show"
- option "Job Fair"
- option "Lacrosse Tournament / Event"
- option "Lecture"
- option "Livestock Auction"
- option "Luncheon"
- option "Magic Show"
- option "Marching Band Tournament / Event"
- option "Mardis Gras Party"
- option "Martial Arts Tournament / Event"
- option "Math Tournament"
- option "Medical Conference"
- option "Meeting"
- option "Memorial Service"
- option "Mixed Martial Arts Tournament / Event"
- option "Motocross"
- option "Motorcycle or Race Car On Road Competition"
- option "Motorcycle Run"
- option "Mud Bogs"
- option "New Year's Eve Party"
- option "Parade with Floats"
- option "Parade without Floats"
- option "Piano Recital"
- option "Pickleball Tournament / Event"
- option "Picnics"
- option "Pig Roast"
- option "Play Readings"
- option "Poetry Reading"
- option "Poker Runs"
- option "Political Rally"
- option "Prom"
- option "Quinceanera"
- option "Renaissance Fairs"
- option "Retirement Party"
- option "Road Race (13.2M or longer)"
- option "Road Race (Running only; 13.1M or less)"
- option "Rummage Sale"
- option "Seminar"
- option "Skateboarding Tournament / Event"
- option "Soccer Tournament / Event"
- option "Softball Tournament / Event"
- option "Spikeball Tournament / Event"
- option "Surfing Tournament / Event"
- option "Swimming Tournament / Event"
- option "Tap Dancing Show"
- option "Tennis Tournament / Event"
- option "Theatrical Production"
- option "Toy Drive"
- option "Track and Field Tournament / Event"
- option "Triathlon Tournament / Event"
- option "Trivia Night"
- option "Ultimate Frisbee Tournament / Event"
- option "Valentine's Day Party"
- option "Video Games Contests"
- option "Volleyball Tournament / Event"
- option "Voter Registration"
- option "Walk-a-thon Tournament / Event"
- option "Wedding Anniversary Party"
- option "Wedding Ceremony and/or Reception"
- option "Weightlifting Tournament / Event"
- option "Wrestling Tournament / Event"
- option "Yard Sale"
- option "Yoga Tournament / Event"
- option "Zumba Tournament / Event"
- generic [ref=e137]:
- generic [ref=e138]:
- text: Event Location
- generic [ref=e140]: "*"
- combobox "Event Location *" [ref=e142]:
- option
- option "Alabama"
- option "Alaska"
- option "Arizona"
- option "Arkansas"
- option "California"
- option "Colorado"
- option "Connecticut"
- option "Delaware"
- option "District of Columbia"
- option "Florida"
- option "Georgia"
- option "Hawaii"
- option "Idaho"
- option "Illinois"
- option "Indiana"
- option "Iowa"
- option "Kansas"
- option "Kentucky"
- option "Louisiana"
- option "Maine"
- option "Maryland"
- option "Massachusetts"
- option "Michigan"
- option "Minnesota"
- option "Mississippi"
- option "Missouri"
- option "Montana"
- option "Nebraska"
- option "Nevada"
- option "New Hampshire"
- option "New Jersey"
- option "New Mexico"
- option "New York" [selected]
- option "North Carolina"
- option "North Dakota"
- option "Ohio"
- option "Oklahoma"
- option "Oregon"
- option "Pennsylvania"
- option "Rhode Island"
- option "South Carolina"
- option "South Dakota"
- option "Tennessee"
- option "Texas"
- option "Utah"
- option "Vermont"
- option "Virginia"
- option "Washington"
- option "West Virginia"
- option "Wisconsin"
- option "Wyoming"
- generic [ref=e143]:
- generic [ref=e144]:
- text: Maximum Daily Attendance
- generic [ref=e146]: "*"
- spinbutton "Maximum Daily Attendance *" [ref=e148]: "100"
- generic [ref=e149]:
- generic [ref=e150]:
- text: Event Start Date
- generic [ref=e152]: "*"
- generic [ref=e153]:
- textbox "Event Start Date *" [ref=e154]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-01
- generic [ref=e155]: YYYY dash MM dash DD
- generic [ref=e156]:
- generic [ref=e157]:
- text: Event End Date
- generic [ref=e159]: "*"
- generic [ref=e160]:
- textbox "Event End Date *" [ref=e161]:
- /placeholder: yyyy-mm-dd
- text: 2026-09-03
- generic [ref=e162]: YYYY dash MM dash DD
- generic [ref=e163]:
- generic [ref=e164]:
- text: Customer Email Address
- generic [ref=e166]: "*"
- textbox "Customer Email Address *" [invalid] [ref=e168]: instanda-smoke@getfused.com
- generic [ref=e169]: The email address entered is invalid.
- button "Submit" [active] [ref=e171] [cursor=pointer]
- paragraph
- paragraph
- generic [ref=e92]:
- complementary [ref=e97]:
- list [ref=e99]:
- listitem [ref=e100]:
- link "Home" [ref=e101] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- listitem [ref=e102]:
- link "Blog" [ref=e103] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/blog
- listitem [ref=e104]:
- link "Contact" [ref=e105] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- contentinfo "Site" [ref=e108]:
- generic [ref=e110]:
- text: © 2026 HUB Insurance • Built with
- link "GeneratePress" [ref=e111] [cursor=pointer]:
- /url: https://generatepress.com
- text:  
- generic [ref=e172]: "There was a problem with your submission, Please review the fields below, Customer Email Address: The email address entered is invalid,"
@@ -0,0 +1,29 @@
- generic [active] [ref=f2e1]:
- generic [ref=f2e2]:
- banner [ref=f2e4]:
- navigation [ref=f2e5]:
- generic [ref=f2e6]:
- link "consumer.instanda.us" [ref=f2e8] [cursor=pointer]:
- /url: /Public/Index
- img "consumer.instanda.us" [ref=f2e9]
- list [ref=f2e11]:
- listitem [ref=f2e12]:
- link "Home" [ref=f2e13] [cursor=pointer]:
- /url: /Public/Index
- listitem [ref=f2e14]:
- link "Get a quote for HUB Special Event Insurance" [ref=f2e15] [cursor=pointer]:
- /url: /Public/PreQuoteQuestions?PackageId=25536&pageNumber=1
- listitem [ref=f2e16]:
- link "Retrieve quote" [ref=f2e17] [cursor=pointer]:
- /url: /Public/RetrieveQuote
- form [ref=f2e23]:
- generic [ref=f2e24]:
- generic [ref=f2e25]: Password
- textbox [ref=f2e26]
- button "Sign In" [ref=f2e28] [cursor=pointer]
- contentinfo [ref=f2e29]:
- generic [ref=f2e30]:
- text: Site built and hosted using
- link "Instanda" [ref=f2e31] [cursor=pointer]:
- /url: http://www.instanda.com/
- text: v1.107.0.6 insurance software.
@@ -0,0 +1,366 @@
- generic [active] [ref=f3e1]:
- link "Skip to content" [ref=f3e2] [cursor=pointer]:
- /url: "#content"
- banner "Site" [ref=f3e3]:
- generic [ref=f3e4]:
- link "HUB Insurance" [ref=f3e6] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- img "HUB Insurance" [ref=f3e7]
- navigation "Primary" [ref=f3e8]:
- list [ref=f3e11]:
- listitem [ref=f3e12]:
- link "About" [ref=f3e13] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/about
- listitem [ref=f3e14]:
- link "Contact" [ref=f3e15] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- listitem [ref=f3e16]:
- link "Get a Quote" [ref=f3e17] [cursor=pointer]:
- /url: "#get-a-quote"
- generic [ref=f3e19]:
- heading "Special Event Insurance" [level=1] [ref=f3e20]:
- text: Special
- emphasis [ref=f3e21]: Event
- text: Insurance
- paragraph
- paragraph [ref=f3e22]: "{Hero Content} Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
- list [ref=f3e23]:
- listitem [ref=f3e24]: Lorem ipsum dolor
- listitem [ref=f3e25]: Lorem ipsum dolor
- listitem [ref=f3e26]: Lorem ipsum dolor
- listitem [ref=f3e27]: Lorem ipsum dolor
- paragraph
- link "Hero CTA Lorem" [ref=f3e28] [cursor=pointer]:
- /url: https://www.getfused.com
- main [ref=f3e32]:
- generic [ref=f3e33]:
- generic [ref=f3e34]:
- generic [ref=f3e35]:
- paragraph [ref=f3e36]: How it Works
- heading "Three Simple Steps" [level=2] [ref=f3e37]
- generic [ref=f3e38]:
- paragraph
- generic [ref=f3e39]:
- generic [ref=f3e40]:
- heading "Start a Quote" [level=2] [ref=f3e41]
- paragraph
- paragraph [ref=f3e42]: An asterisk (*) indicates a required field.
- generic [ref=f3e43]:
- generic [ref=f3e45]:
- generic [ref=f3e46]:
- generic [ref=f3e47]:
- text: Event Role
- generic [ref=f3e49]: "*"
- combobox "Event Role *" [ref=f3e51]:
- option [selected]
- option "Event Host or Organizer"
- option "Vendor, Exhibitor or Subcontractor"
- option "Performer, Entertainer or DJ"
- generic [ref=f3e52]:
- generic [ref=f3e53]:
- text: Activity / Event Type
- generic [ref=f3e55]: "*"
- combobox "Activity / Event Type *" [ref=f3e57]:
- option [selected]
- option "Animal Adoption Event"
- option "Animal Shows"
- option "Antique Show"
- option "Art Festival"
- option "Art Show"
- option "Auction"
- option "Award Presentation"
- option "Baby Shower"
- option "Bachelor Party"
- option "Bachelorette Party"
- option "Badminton Tournament / Event"
- option "Bake Sale"
- option "Ballet"
- option "Banquet"
- option "Baptism"
- option "Bar/Bat Mitzvah"
- option "Bar/Pub Crawl"
- option "Baseball Tournament / Event"
- option "Basketball Tournament / Event"
- option "Battle of the Bands"
- option "Bazaar"
- option "Beauty Pageant"
- option "Beer Garden/Tent"
- option "Beer Mile"
- option "Billiards Tournament / Event"
- option "Bingo Games"
- option "Birthday Party"
- option "Block Parties/Street Closures/Street Fairs"
- option "BMX Tournament / Event"
- option "Boat Show (On Land)"
- option "Boat Show"
- option "Bocce Ball"
- option "Body Building Tournament / Event"
- option "Book Fair"
- option "Book Signing"
- option "Bowling Tournament / Event"
- option "Boxing Tournament / Event"
- option "Breakfast"
- option "Bridal Shower"
- option "Brunch - No Admission Charge - Invite Only"
- option "Business Meeting"
- option "Business Party"
- option "Cage Fight Tournament / Event"
- option "Cannabis Event"
- option "Car Show"
- option "Car Wash"
- option "Card Games"
- option "Casino Night"
- option "Cheerleading Tournament / Event"
- option "Chess Tournament"
- option "Church Service or Meeting"
- option "Cocktail Party - No Admission Charge - Invite Only"
- option "College Reunion"
- option "Comedy Show"
- option "Company Retreats"
- option "Concert - Choir"
- option "Concert - Classic Rock"
- option "Concert - Classical"
- option "Concert - Country"
- option "Concert - Doo-Wop"
- option "Concert - Electronic/EDM"
- option "Concert - Folk Guitar"
- option "Concert - Foreign"
- option "Concert - Heavy Metal"
- option "Concert - High Profile Performer"
- option "Concert - Hip Hop/Rap"
- option "Concert - Jazz"
- option "Concert - Music Festivals"
- option "Concert - Opera"
- option "Concert - Orchestra"
- option "Concert - Pop"
- option "Concert - R&B"
- option "Concert - Reggae"
- option "Concert - Rock"
- option "Concert - SKA"
- option "Concert - Soul"
- option "Concert - Symphony"
- option "Concert - Tribute Bands"
- option "Consumer Show"
- option "Cooking Competition"
- option "Corn Mazes"
- option "Cornhole Tournament / Event"
- option "Costume Contest"
- option "Craft Show"
- option "Cycling"
- option "Dance Competition"
- option "Dance Recital"
- option "Dance Team Tournament / Event"
- option "Debutante Ball"
- option "Demolition Derby"
- option "Dinner - No Admission Charge - Invite Only"
- option "Dirt Bike Tournament / Event"
- option "Dog Show"
- option "Drive-In Movie/Concert"
- option "Egg Hunt"
- option "Engagement Party"
- option "Exotic Dancer Event"
- option "Extreme Sporting Tournament / Event"
- option "Family Reunion"
- option "Farm Show"
- option "Farmer's Market"
- option "Fashion Show"
- option "Fencing Tournament / Event"
- option "Field Hockey Tournament / Event"
- option "Film Screenings"
- option "Film Shoot (In Studio)"
- option "First Communion"
- option "Fishing Tournament/Event"
- option "Flag Football Tournament / Event"
- option "Flea Market"
- option "Flower Show"
- option "Food Festival"
- option "Football Tournament / Event"
- option "Fraternity/Sorority Party"
- option "Frisbee Golf Tournament / Event"
- option "Fundraiser Dinner"
- option "Funeral Service"
- option "Gala with Seated Dinner"
- option "Garden Show"
- option "Gift Wrapping Party"
- option "Golf Tournament / Event"
- option "Gymnastics Tournament / Event"
- option "Haunted Houses"
- option "Health Fair"
- option "High School Graduation Party"
- option "High School Reunion"
- option "High School Graduation Ceremony"
- option "Holiday Character (Santa, Easter Bunny, etc.)"
- option "Holiday Party No Admission Charge Invite Only"
- option "Home and Garden Tour"
- option "Horse Show/Competition"
- option "Ice Hockey Tournament / Event"
- option "Ice Skating Show / Event"
- option "Indoor Convention"
- option "Indoor Tradeshow"
- option "Jewelry Show"
- option "Job Fair"
- option "Lacrosse Tournament / Event"
- option "Lecture"
- option "Livestock Auction"
- option "Luncheon"
- option "Magic Show"
- option "Marching Band Tournament / Event"
- option "Mardis Gras Party"
- option "Martial Arts Tournament / Event"
- option "Math Tournament"
- option "Medical Conference"
- option "Meeting"
- option "Memorial Service"
- option "Mixed Martial Arts Tournament / Event"
- option "Motocross"
- option "Motorcycle or Race Car On Road Competition"
- option "Motorcycle Run"
- option "Mud Bogs"
- option "New Year's Eve Party"
- option "Parade with Floats"
- option "Parade without Floats"
- option "Piano Recital"
- option "Pickleball Tournament / Event"
- option "Picnics"
- option "Pig Roast"
- option "Play Readings"
- option "Poetry Reading"
- option "Poker Runs"
- option "Political Rally"
- option "Prom"
- option "Quinceanera"
- option "Renaissance Fairs"
- option "Retirement Party"
- option "Road Race (13.2M or longer)"
- option "Road Race (Running only; 13.1M or less)"
- option "Rummage Sale"
- option "Seminar"
- option "Skateboarding Tournament / Event"
- option "Soccer Tournament / Event"
- option "Softball Tournament / Event"
- option "Spikeball Tournament / Event"
- option "Surfing Tournament / Event"
- option "Swimming Tournament / Event"
- option "Tap Dancing Show"
- option "Tennis Tournament / Event"
- option "Theatrical Production"
- option "Toy Drive"
- option "Track and Field Tournament / Event"
- option "Triathlon Tournament / Event"
- option "Trivia Night"
- option "Ultimate Frisbee Tournament / Event"
- option "Valentine's Day Party"
- option "Video Games Contests"
- option "Volleyball Tournament / Event"
- option "Voter Registration"
- option "Walk-a-thon Tournament / Event"
- option "Wedding Anniversary Party"
- option "Wedding Ceremony and/or Reception"
- option "Weightlifting Tournament / Event"
- option "Wrestling Tournament / Event"
- option "Yard Sale"
- option "Yoga Tournament / Event"
- option "Zumba Tournament / Event"
- generic [ref=f3e58]:
- generic [ref=f3e59]:
- text: Event Location
- generic [ref=f3e61]: "*"
- combobox "Event Location *" [ref=f3e63]:
- option [selected]
- option "Alabama"
- option "Alaska"
- option "Arizona"
- option "Arkansas"
- option "California"
- option "Colorado"
- option "Connecticut"
- option "Delaware"
- option "District of Columbia"
- option "Florida"
- option "Georgia"
- option "Hawaii"
- option "Idaho"
- option "Illinois"
- option "Indiana"
- option "Iowa"
- option "Kansas"
- option "Kentucky"
- option "Louisiana"
- option "Maine"
- option "Maryland"
- option "Massachusetts"
- option "Michigan"
- option "Minnesota"
- option "Mississippi"
- option "Missouri"
- option "Montana"
- option "Nebraska"
- option "Nevada"
- option "New Hampshire"
- option "New Jersey"
- option "New Mexico"
- option "New York"
- option "North Carolina"
- option "North Dakota"
- option "Ohio"
- option "Oklahoma"
- option "Oregon"
- option "Pennsylvania"
- option "Rhode Island"
- option "South Carolina"
- option "South Dakota"
- option "Tennessee"
- option "Texas"
- option "Utah"
- option "Vermont"
- option "Virginia"
- option "Washington"
- option "West Virginia"
- option "Wisconsin"
- option "Wyoming"
- generic [ref=f3e64]:
- generic [ref=f3e65]:
- text: Maximum Daily Attendance
- generic [ref=f3e67]: "*"
- spinbutton "Maximum Daily Attendance *" [ref=f3e69]
- generic [ref=f3e70]:
- generic [ref=f3e71]:
- text: Event Start Date
- generic [ref=f3e73]: "*"
- generic [ref=f3e74]:
- textbox "Event Start Date *" [ref=f3e75]:
- /placeholder: yyyy-mm-dd
- generic [ref=f3e76]: YYYY dash MM dash DD
- generic [ref=f3e77]:
- generic [ref=f3e78]:
- text: Event End Date
- generic [ref=f3e80]: "*"
- generic [ref=f3e81]:
- textbox "Event End Date *" [ref=f3e82]:
- /placeholder: yyyy-mm-dd
- generic [ref=f3e83]: YYYY dash MM dash DD
- generic [ref=f3e84]:
- generic [ref=f3e85]:
- text: Customer Email Address
- generic [ref=f3e87]: "*"
- textbox "Customer Email Address *" [ref=f3e89]
- button "Submit" [ref=f3e91] [cursor=pointer]
- paragraph
- paragraph
- generic [ref=f3e92]:
- complementary [ref=f3e97]:
- list [ref=f3e99]:
- listitem [ref=f3e100]:
- link "Home" [ref=f3e101] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/
- listitem [ref=f3e102]:
- link "Blog" [ref=f3e103] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/blog
- listitem [ref=f3e104]:
- link "Contact" [ref=f3e105] [cursor=pointer]:
- /url: https://hubinsurance.mystagingwebsite.com/contact
- contentinfo "Site" [ref=f3e108]:
- generic [ref=f3e110]:
- text: © 2026 HUB Insurance • Built with
- link "GeneratePress" [ref=f3e111] [cursor=pointer]:
- /url: https://generatepress.com
+2
View File
@@ -0,0 +1,2 @@
/cache
/project.local.yml
+133
View File
@@ -0,0 +1,133 @@
# the name by which the project can be referenced within Serena
project_name: "hub-insurance"
# list of languages for which language servers are started; choose from:
# al angular ansible bash clojure
# cpp cpp_ccls crystal csharp csharp_omnisharp
# dart elixir elm erlang fortran
# fsharp go groovy haskell haxe
# hlsl html java json julia
# kotlin lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor powershell python
# python_jedi python_ty r rego ruby
# ruby_solargraph rust scala scss solidity
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- php
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
ls_specific_settings: {}
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
fixed_tools: []
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
# for this project.
# This setting can, in turn, be overridden by CLI parameters (--mode).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
default_modes:
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
added_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
# list of regex patterns for memories to completely ignore.
# Matching memories will not appear in list_memories or activate_project output
# and cannot be accessed via read_memory or write_memory.
# To access ignored memory files, use the read_file tool on the raw file path.
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
Binary file not shown.
@@ -3,7 +3,7 @@
* Plugin Name: ESP INSTANDA Start Quote Integration * Plugin Name: ESP INSTANDA Start Quote Integration
* Plugin URI: https://getfused.com * Plugin URI: https://getfused.com
* Description: Pushes Gravity Forms "Start a Quote" submissions into INSTANDA's Package API with write-ahead transaction logging, native entry recording, admin resubmit, and a test-mode lock. Form-agnostic - attach the feed to any form. * Description: Pushes Gravity Forms "Start a Quote" submissions into INSTANDA's Package API with write-ahead transaction logging, native entry recording, admin resubmit, and a test-mode lock. Form-agnostic - attach the feed to any form.
* Version: 1.0.7 * Version: 1.1.0
* Requires at least: 7.0 * Requires at least: 7.0
* Requires PHP: 8.5 * Requires PHP: 8.5
* Author: Getfused * Author: Getfused
@@ -75,25 +75,36 @@ function platform_blockers(): array {
} }
\add_action('admin_init', static function (): void { \add_action('admin_init', static function (): void {
if (!empty(platform_blockers())) {
return;
}
// Keep the schema current if the plugin was updated without re-activation. // Keep the schema current if the plugin was updated without re-activation.
if (empty(platform_blockers()) && Transaction_Store::needs_migration()) { if (Transaction_Store::needs_migration()) {
Transaction_Store::install_table(); Transaction_Store::install_table();
} }
// Self-heal the retention cron if its scheduled event was ever lost (the schema
// self-heals above; the cron must too, or retention silently stops forever).
if (!\wp_next_scheduled(CLEANUP_HOOK)) {
\wp_schedule_event(time(), 'daily', CLEANUP_HOOK);
}
});
// Requirements notice registered UNCONDITIONALLY: gform_loaded never fires when Gravity
// Forms is absent, so a notice registered only inside it would be unreachable in exactly
// the case (GF missing) an admin most needs to see it.
\add_action('admin_notices', static function (): void {
$errors = platform_blockers();
if ($errors) {
echo '<div class="notice notice-error"><p><strong>ESP INSTANDA Integration is inactive:</strong> '
. esc_html(implode(' ', $errors)) . '</p></div>';
}
}); });
/* ------------------------------------------------ Gravity Forms Add-On registration */ /* ------------------------------------------------ Gravity Forms Add-On registration */
\add_action('gform_loaded', Guard::wrap(static function (): void { \add_action('gform_loaded', Guard::wrap(static function (): void {
if (!empty(platform_blockers()) || !method_exists('GFForms', 'include_feed_addon_framework')) { if (!empty(platform_blockers()) || !method_exists('GFForms', 'include_feed_addon_framework')) {
\add_action('admin_notices', static function (): void { return; // the requirements notice is registered unconditionally above
$errors = platform_blockers();
if (!$errors) {
return;
}
echo '<div class="notice notice-error"><p><strong>ESP INSTANDA Integration is inactive:</strong> '
. esc_html(implode(' ', $errors)) . '</p></div>';
});
return;
} }
\GFForms::include_feed_addon_framework(); \GFForms::include_feed_addon_framework();
@@ -111,7 +122,7 @@ function platform_blockers(): array {
require_once __DIR__ . '/includes/class-instanda-addon.php'; require_once __DIR__ . '/includes/class-instanda-addon.php';
\GFAddOn::register(Instanda_AddOn::class); \GFAddOn::register(Instanda_AddOn::class);
}, 5)); }), 5);
/* ----------------------------------------------------------------- cron cleanup */ /* ----------------------------------------------------------------- cron cleanup */
@@ -158,7 +158,9 @@ final class Api_Client {
$data = is_array($data) ? $data : []; $data = is_array($data) ? $data : [];
if ($code >= 200 && $code < 300) { if ($code >= 200 && $code < 300) {
$ref = isset($data['quoteRef']) ? (string) $data['quoteRef'] : null; // An empty-string quoteRef is not usable - treat it as missing so the caller
// handles the 2xx-without-quoteRef case as ambiguous, not as a delivered quote.
$ref = isset($data['quoteRef']) && $data['quoteRef'] !== '' ? (string) $data['quoteRef'] : null;
$url = isset($data['urlSingleUse']) ? (string) $data['urlSingleUse'] : null; $url = isset($data['urlSingleUse']) ? (string) $data['urlSingleUse'] : null;
return Api_Result::success($code, $ref, $url, $excerpt); return Api_Result::success($code, $ref, $url, $excerpt);
} }
@@ -9,8 +9,11 @@
* an explicit, gated switch to Live - see the Go-Live checklist. * an explicit, gated switch to Live - see the Go-Live checklist.
* *
* Secrets entered in the admin UI are encrypted at rest with libsodium * Secrets entered in the admin UI are encrypted at rest with libsodium
* (sodium_crypto_secretbox, XSalsa20-Poly1305). The key comes from the * (sodium_crypto_secretbox, XSalsa20-Poly1305). The "a database dump alone never
* SPG_INSTANDA_KEY constant, so a database dump alone never exposes a usable secret. * exposes a usable secret" guarantee holds ONLY when the SPG_INSTANDA_KEY constant is
* set: without it the key is self-provisioned into the DB (see db_key()), so the key
* and ciphertext live in the same database and a dump recovers the password. Set
* SPG_INSTANDA_KEY (and the credential constants) in production for the hardened posture.
* Live credentials are expected as constants and are never written to the database. * Live credentials are expected as constants and are never written to the database.
* *
* @package ESP\Instanda * @package ESP\Instanda
@@ -28,5 +28,12 @@ final class Cron_Cleanup {
if ($deleted > 0) { if ($deleted > 0) {
Logger::debug("Retention purge removed {$deleted} delivered transaction(s) older than {$days} day(s)."); Logger::debug("Retention purge removed {$deleted} delivered transaction(s) older than {$days} day(s).");
} }
// Scrub PII from unresolved (failed/pending) rows past the window; they are never
// timer-deleted, so this bounds how long customer PII is retained on them.
$scrubbed = $store->anonymize_stale_unresolved($days);
if ($scrubbed > 0) {
Logger::debug("Retention scrub anonymized PII on {$scrubbed} unresolved transaction(s) older than {$days} day(s).");
}
} }
} }
@@ -121,6 +121,37 @@ final class Field_Mapper {
return $errors; return $errors;
} }
/**
* Sanity-check a stored StartQuote wire body before re-sending it (resubmit trust
* boundary): the DB is treated as untrusted input. Returns error codes (empty = ok).
* Pure - uses a fixed UTC zone since only Y-m-d well-formedness is being checked.
*
* @param array<string,mixed> $body A body previously built by to_payload().
* @return list<string>
*/
public function validate_payload(array $body): array {
$errors = [];
foreach (['Role', 'ActivityType', 'PQ_LocationState_CHOICE'] as $key) {
if (trim((string) ($body[$key] ?? '')) === '') {
$errors[] = $key . '_missing';
}
}
if (!$this->is_positive_integer($body['PQ_MaximumDailyAttendance_NUM'] ?? null)) {
$errors[] = 'attendance_invalid';
}
$email = trim((string) ($body['PQ_CustomerEmailAddress_TXT'] ?? ''));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'email_invalid';
}
$tz = new \DateTimeZone('UTC');
foreach (['PQ_EventStart_DATE', 'PQ_EventEnd_DATE'] as $key) {
if ($this->parse_date((string) ($body[$key] ?? ''), $tz) === null) {
$errors[] = $key . '_invalid';
}
}
return $errors;
}
/** D8 - inclusive consecutive-day count: (end - start) + 1; a single-day event is 1. */ /** D8 - inclusive consecutive-day count: (end - start) + 1; a single-day event is 1. */
private function consecutive_days(\DateTimeImmutable $start, \DateTimeImmutable $end): int { private function consecutive_days(\DateTimeImmutable $start, \DateTimeImmutable $end): int {
return (int) $start->diff($end)->days + 1; return (int) $start->diff($end)->days + 1;
@@ -106,9 +106,29 @@ final class Go_Live {
['key' => 'A2', 'label' => 'Alert email recipient(s)', 'detail' => $emails !== '' ? $emails : 'None set', 'pass' => $emails !== ''], ['key' => 'A2', 'label' => 'Alert email recipient(s)', 'detail' => $emails !== '' ? $emails : 'None set', 'pass' => $emails !== ''],
['key' => 'A3', 'label' => 'Front-end form submit + redirect verified in a browser', 'detail' => $this->ack_detail('espinstanda_frontend_ack'), 'pass' => (bool) get_option('espinstanda_frontend_ack')], ['key' => 'A3', 'label' => 'Front-end form submit + redirect verified in a browser', 'detail' => $this->ack_detail('espinstanda_frontend_ack'), 'pass' => (bool) get_option('espinstanda_frontend_ack')],
['key' => 'A4', 'label' => 'No unresolved failures', 'detail' => $unresolved === 0 ? 'Clean' : "{$unresolved} unresolved", 'pass' => $unresolved === 0], ['key' => 'A4', 'label' => 'No unresolved failures', 'detail' => $unresolved === 0 ? 'Clean' : "{$unresolved} unresolved", 'pass' => $unresolved === 0],
['key' => 'A5', 'label' => 'Exactly one active INSTANDA feed', 'detail' => $this->feed_summary(), 'pass' => $this->active_feed_count() === 1],
]; ];
} }
/** Count active INSTANDA feeds across all forms (should be exactly one at go-live). */
private function active_feed_count(): int {
if (!class_exists(__NAMESPACE__ . '\\Instanda_AddOn')) {
return 0;
}
$feeds = Instanda_AddOn::get_instance()->get_feeds();
return count(array_filter((array) $feeds, static fn ($f) => !empty($f['is_active'])));
}
private function feed_summary(): string {
$n = $this->active_feed_count();
if ($n === 1) {
return '1 active feed';
}
return $n === 0
? 'No active feed - the quote form will not push to INSTANDA'
: "{$n} active feeds - deactivate all but the canonical quote form (a stray test-form feed also creates quotes)";
}
/** @return array{complete:int,total:int,ready:bool} */ /** @return array{complete:int,total:int,ready:bool} */
public function readiness(): array { public function readiness(): array {
$blocking = $this->blocking_items(); $blocking = $this->blocking_items();
@@ -129,6 +149,10 @@ final class Go_Live {
return false; return false;
} }
update_option('espinstanda_operating_mode', 'live'); update_option('espinstanda_operating_mode', 'live');
// Switching the mode must also select the Live environment, mirroring
// return_to_test(); otherwise environment() stays clamped to Test and real
// customer quotes would keep hitting the Test endpoint after go-live.
update_option('espinstanda_env', 'live');
update_option('espinstanda_operating_mode_audit', [ update_option('espinstanda_operating_mode_audit', [
'actor' => get_current_user_id(), 'actor' => get_current_user_id(),
'ts' => current_time('mysql'), 'ts' => current_time('mysql'),
@@ -18,7 +18,7 @@ namespace ESP\Instanda;
final class Instanda_AddOn extends \GFFeedAddOn { final class Instanda_AddOn extends \GFFeedAddOn {
protected $_version = '1.0.7'; protected $_version = '1.1.0';
protected $_min_gravityforms_version = '2.10.3'; protected $_min_gravityforms_version = '2.10.3';
protected $_slug = 'espinstanda'; protected $_slug = 'espinstanda';
protected $_path = 'esp-instanda-integration/esp-instanda-integration.php'; protected $_path = 'esp-instanda-integration/esp-instanda-integration.php';
@@ -71,6 +71,9 @@ final class Instanda_AddOn extends \GFFeedAddOn {
add_action('esp_instanda_resubmit', Guard::wrap([Resubmit_Handler::class, 'run'])); add_action('esp_instanda_resubmit', Guard::wrap([Resubmit_Handler::class, 'run']));
// Data-subject erasure: wire customer transactions into WP's Erase Personal Data tool.
add_filter('wp_privacy_personal_data_erasers', Guard::wrap([$this, 'register_privacy_erasers']));
// Keep Gravity Forms' scripts out of Jetpack Boost's JS concat/defer, which // Keep Gravity Forms' scripts out of Jetpack Boost's JS concat/defer, which
// otherwise break GF's AJAX init and the post-submit INSTANDA redirect. // otherwise break GF's AJAX init and the post-submit INSTANDA redirect.
Boost_Compat::register(); Boost_Compat::register();
@@ -389,7 +392,14 @@ JS;
} }
} }
if ($tx === null || $tx->quote_ref === null) { if ($tx === null || $tx->quote_ref === null) {
return; // nothing delivered to record (defensive) // No-silent-failure: an async feed with no resolvable delivered transaction means
// entry meta / note / notification are being skipped - make that visible.
Logger::error(sprintf(
'record_feed_result: no delivered transaction for entry #%d (dedupe %s); entry meta/notification not recorded.',
$entry_id,
(string) (Submission::current_dedupe() ?? 'n/a')
));
return;
} }
Entry_Integration::record($entry_id, $tx->quote_ref, (string) $tx->quote_url, 'delivered', $tx->environment); Entry_Integration::record($entry_id, $tx->quote_ref, (string) $tx->quote_url, 'delivered', $tx->environment);
@@ -402,6 +412,27 @@ JS;
} }
} }
/* ------------------------------------------------------------ privacy */
public function register_privacy_erasers($erasers) {
$erasers['esp-instanda'] = [
'eraser_friendly_name' => 'INSTANDA quote transactions',
'callback' => [$this, 'privacy_erase'],
];
return $erasers;
}
/** WP Erase Personal Data callback - deletes all INSTANDA transactions for an email. */
public function privacy_erase($email, $page = 1) {
$removed = (new Transaction_Store())->delete_by_email((string) $email);
return [
'items_removed' => $removed > 0,
'items_retained' => false,
'messages' => [],
'done' => true,
];
}
/* ------------------------------------------------------------ admin pages */ /* ------------------------------------------------------------ admin pages */
public function register_admin_pages() { public function register_admin_pages() {
@@ -463,7 +494,8 @@ JS;
echo '<h2>Event log</h2><ul class="espinstanda-timeline">'; echo '<h2>Event log</h2><ul class="espinstanda-timeline">';
foreach ($tx->event_log as $e) { foreach ($tx->event_log as $e) {
echo '<li><code>' . esc_html($e['ts'] ?? '') . '</code> ' . esc_html($e['message'] ?? '') . '</li>'; $ts = isset($e['ts']) && $e['ts'] !== '' ? get_date_from_gmt((string) $e['ts'], 'Y-m-d H:i') : '';
echo '<li><code>' . esc_html($ts) . '</code> ' . esc_html($e['message'] ?? '') . '</li>';
} }
echo '</ul>'; echo '</ul>';
@@ -475,7 +507,8 @@ JS;
'Status' => ucfirst($tx->status), 'Environment' => strtoupper($tx->environment), 'Status' => ucfirst($tx->status), 'Environment' => strtoupper($tx->environment),
'quoteRef' => $tx->quote_ref ?? '—', 'HTTP' => $tx->http_status ?? '—', 'quoteRef' => $tx->quote_ref ?? '—', 'HTTP' => $tx->http_status ?? '—',
'Retries' => $tx->retry_count, 'Dedupe key' => $tx->dedupe_key, 'Retries' => $tx->retry_count, 'Dedupe key' => $tx->dedupe_key,
'Customer' => $tx->customer_email, 'Created' => $tx->created_at, 'Customer' => $tx->customer_email,
'Created' => $tx->created_at !== '' ? get_date_from_gmt($tx->created_at, 'Y-m-d H:i') : '—',
]; ];
foreach ($rows as $k => $v) { foreach ($rows as $k => $v) {
echo '<tr><th>' . esc_html($k) . '</th><td>' . esc_html((string) $v) . '</td></tr>'; echo '<tr><th>' . esc_html($k) . '</th><td>' . esc_html((string) $v) . '</td></tr>';
@@ -592,6 +625,9 @@ JS;
if ($tx === null) { if ($tx === null) {
wp_send_json_error(['message' => 'Not found']); wp_send_json_error(['message' => 'Not found']);
} }
if ($tx->status === Tx_Status::Delivered->value) {
wp_send_json_error(['message' => 'Already delivered - resubmitting would create a duplicate quote']);
}
if ($tx->environment !== Config::environment()->value) { if ($tx->environment !== Config::environment()->value) {
wp_send_json_error(['message' => 'Cross-environment resubmit is blocked']); wp_send_json_error(['message' => 'Cross-environment resubmit is blocked']);
} }
@@ -22,6 +22,10 @@ final class Submission {
private const LOCK_TTL = 60; // seconds private const LOCK_TTL = 60; // seconds
private const RESULT_TTL = 120; private const RESULT_TTL = 120;
private const STASH_TTL = 300; private const STASH_TTL = 300;
public const DEDUPE_WINDOW = 1800; // 30 min - collapse identical resubmits to the existing quote
private const RL_IP_MAX = 20; // valid submissions per IP per hour
private const RL_EMAIL_MAX = 5; // valid submissions per email per hour
private const RL_WINDOW = 3600; // rate-limit window (seconds)
/** Wire var key => feed meta key (the mapped GF field id is stored under each). */ /** Wire var key => feed meta key (the mapped GF field id is stored under each). */
private const FIELD_KEYS = [ private const FIELD_KEYS = [
@@ -55,22 +59,44 @@ final class Submission {
return $validation_result; // GATE 3 - only act on the final page return $validation_result; // GATE 3 - only act on the final page
} }
$token = $this->submission_token($form); // Fail CLOSED for this one submission if the create path throws (no quote-less
if (!$this->acquire_lock($token)) { // entry saved), keep the form working for everyone else, and surface the failure.
return $this->replay($token, $validation_result); // GATE 4 - duplicate fire: replay cached outcome try {
return $this->process($validation_result, $form, $feed);
} catch (\Throwable $e) {
Guard::report($e);
$this->record_hard_failure($e);
$this->pending_message = 'We could not start your quote just now. Please try again.';
return $this->mark_form_invalid($validation_result);
}
} }
$dedupe = $this->dedupe_key($token); /** The gated INSTANDA create - runs only after GATE 1-3 pass, inside validate()'s guard. */
$store = new Transaction_Store(); private function process(array $validation_result, array $form, array $feed): array {
$token = $this->submission_token($form);
if (!$this->acquire_lock($token)) {
return $this->replay($token, $validation_result); // GATE 4 - within-request double-fire
}
// Own double-fire guard: a prior attempt for this submission already delivered. $store = new Transaction_Store();
$existing = $store->find_by_dedupe($dedupe); $values = $this->extract_values($form, $feed);
if ($existing !== null && $existing->status === Tx_Status::Delivered->value) { $dedupe = $this->content_dedupe_key($form, $values); // content-addressed, stable across requests
// Cross-request duplicate guard: an identical submission already delivered
// recently -> reuse its quote instead of creating a second one at INSTANDA.
$existing = $store->recent_delivered_by_dedupe($dedupe, self::DEDUPE_WINDOW);
if ($existing !== null) {
$this->stash_success($dedupe, (string) $existing->quote_ref, $existing->quote_url); $this->stash_success($dedupe, (string) $existing->quote_ref, $existing->quote_url);
return $this->remember($token, $validation_result, true); return $this->remember($token, $validation_result, true);
} }
$values = $this->extract_values($form, $feed); // Race guard: serialize concurrent identical submits (atomic where a persistent
// object cache is present, e.g. production; degrades to a transient lock locally).
if (!$this->acquire_content_lock($dedupe)) {
$this->pending_message = 'We are finalizing your quote - please wait a moment and try again.';
return $this->remember($token, $this->mark_form_invalid($validation_result), false);
}
$mapper = new Field_Mapper(); $mapper = new Field_Mapper();
$tz = wp_timezone_string(); $tz = wp_timezone_string();
@@ -80,6 +106,13 @@ final class Submission {
return $this->remember($token, $validation_result, false); return $this->remember($token, $validation_result, false);
} }
// Abuse control: throttle the paid API + PII table per IP and per email.
$throttle = $this->rate_limit_message((string) ($values['customer_email'] ?? ''));
if ($throttle !== null) {
$this->pending_message = $throttle;
return $this->remember($token, $this->mark_form_invalid($validation_result), false);
}
$body = $mapper->to_payload($values, $tz); $body = $mapper->to_payload($values, $tz);
// Write-ahead: persist BEFORE the HTTP call (redacted body carries no secrets). // Write-ahead: persist BEFORE the HTTP call (redacted body carries no secrets).
@@ -94,35 +127,47 @@ final class Submission {
$tx->role_label = (string) ($values['role'] ?? ''); $tx->role_label = (string) ($values['role'] ?? '');
$tx->redacted_payload = (string) wp_json_encode($body); $tx->redacted_payload = (string) wp_json_encode($body);
$tx_id = $store->create_pending($tx); $tx_id = $store->create_pending($tx);
// A failed write-ahead insert means we cannot audit the call - do not send it.
if ($tx_id === 0) {
Logger::error('Write-ahead insert failed; aborting StartQuote to avoid an unaudited call.');
$this->alert_failure($this->synth_tx($form, $values, $dedupe, 'Write-ahead insert failed - submission not sent.'), null);
Failure_Surface::bump();
$this->pending_message = $this->friendly_failure_message(true);
return $this->remember($token, $this->mark_form_invalid($validation_result), false);
}
} }
$result = (new Api_Client())->start_quote($body, $dedupe); $result = (new Api_Client())->start_quote($body, $dedupe);
$has_ref = $result->quote_ref !== null && $result->quote_ref !== '';
if ($result->ok && $result->quote_ref !== null) { if ($result->ok && $has_ref) {
if ($tx_id) { if ($tx_id) {
$store->mark_success($tx_id, $result->quote_ref, $result->url_single_use, $result->http_status); $store->mark_success($tx_id, (string) $result->quote_ref, $result->url_single_use, $result->http_status);
} }
$this->stash_success($dedupe, $result->quote_ref, $result->url_single_use); $this->stash_success($dedupe, (string) $result->quote_ref, $result->url_single_use);
return $this->remember($token, $validation_result, true); return $this->remember($token, $validation_result, true);
} }
// Failure / ambiguous timeout - input preserved, no entry saved. // Failure / ambiguous - input preserved, no entry saved. A 2xx without a usable
// quoteRef is ambiguous too: INSTANDA may have created a quote we cannot see.
$ambiguous = $result->is_ambiguous || ($result->ok && !$has_ref);
if ($tx_id) { if ($tx_id) {
if ($result->is_ambiguous) { if ($ambiguous) {
$store->mark_ambiguous($tx_id, $result->http_status, $result->error_string()); $store->mark_ambiguous($tx_id, $result->http_status, $result->error_string());
} else { } else {
$store->mark_failed($tx_id, $result->http_status, $result->response_excerpt, $result->error_string()); $store->mark_failed($tx_id, $result->http_status, $result->response_excerpt, $result->error_string());
} }
$stored = $store->find($tx_id); $stored = $store->find($tx_id) ?? $this->synth_tx($form, $values, $dedupe, $result->error_string());
if ($stored !== null) { } else {
(new Notifier())->validation_failure_alert($stored, $result); // Storage disabled - still surface the failure (no silent drop).
} $stored = $this->synth_tx($form, $values, $dedupe, $result->error_string());
} }
$this->alert_failure($stored, $result);
Failure_Surface::bump(); Failure_Surface::bump();
$this->pending_message = $this->friendly_failure_message($result); $this->pending_message = $this->friendly_failure_message($ambiguous);
$invalid = $this->mark_form_invalid($validation_result); return $this->remember($token, $this->mark_form_invalid($validation_result), false);
return $this->remember($token, $invalid, false);
} }
/** gform_validation_message filter - surface our form-level failure message. */ /** gform_validation_message filter - surface our form-level failure message. */
@@ -164,10 +209,10 @@ final class Submission {
} }
private function read_stash_for_entry(array $form): array { private function read_stash_for_entry(array $form): array {
// Best-effort: the confirmation runs in the same request as a fresh success. // Best-effort: confirmation runs in the same request as a fresh success, so the
$token = $this->submission_token($form); // content dedupe key was recorded in the request-scoped static during validate().
$dedupe = $this->dedupe_key($token); $dedupe = self::$current_dedupe;
return $this->read_stash($dedupe) ?? []; return $dedupe !== null ? ($this->read_stash($dedupe) ?? []) : [];
} }
/* ----------------------------------------------------------- feed + values */ /* ----------------------------------------------------------- feed + values */
@@ -228,12 +273,31 @@ final class Submission {
return 'espinstanda_' . $unique; return 'espinstanda_' . $unique;
} }
private function dedupe_key(string $token): string { /**
return substr('dk_' . md5($token), 0, 64); * Content-addressed dedupe key: two submissions of the same form with the same
* mapped field values produce the same key, so an identical re-POST or a
* retry-after-error collapses to the existing quote instead of creating a new one.
*/
private function content_dedupe_key(array $form, array $values): string {
$parts = [(string) ($form['id'] ?? '')];
foreach (self::FIELD_KEYS as $key) {
$v = trim((string) ($values[$key] ?? ''));
$parts[] = $key === 'customer_email' ? strtolower($v) : $v;
}
return 'dk_' . substr(hash('sha256', implode('|', $parts)), 0, 60);
} }
/** Per-request lock: stops a single submission from double-firing within one request. */
private function acquire_lock(string $token): bool { private function acquire_lock(string $token): bool {
$key = 'lock_' . md5($token); return $this->try_lock('lock_' . md5($token));
}
/** Brief atomic lock on the content key: serializes concurrent identical submits. */
private function acquire_content_lock(string $dedupe): bool {
return $this->try_lock('clock_' . md5($dedupe));
}
private function try_lock(string $key): bool {
if (function_exists('wp_cache_add') && wp_cache_add($key, 1, 'espinstanda', self::LOCK_TTL)) { if (function_exists('wp_cache_add') && wp_cache_add($key, 1, 'espinstanda', self::LOCK_TTL)) {
set_transient('espinstanda_' . $key, 1, self::LOCK_TTL); set_transient('espinstanda_' . $key, 1, self::LOCK_TTL);
return true; return true;
@@ -307,10 +371,62 @@ final class Submission {
unset($field); unset($field);
} }
private function friendly_failure_message(Api_Result $result): string { private function friendly_failure_message(bool $ambiguous): string {
if ($result->is_ambiguous) { if ($ambiguous) {
return 'We could not confirm your quote. Please try again in a moment - we have saved your details.'; return 'We could not confirm your quote. Please try again in a moment - we have saved your details.';
} }
return 'We could not start your quote just now. Please review your entries and try again.'; return 'We could not start your quote just now. Please review your entries and try again.';
} }
/* ----------------------------------------------------------- abuse control */
/** A friendly message when over the per-IP or per-email hourly limit, else null. */
private function rate_limit_message(string $email): ?string {
$ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
if ($ip !== '' && $this->bump_counter('rl_ip_' . md5($ip)) > self::RL_IP_MAX) {
return 'Too many quote attempts from your connection. Please try again later.';
}
$email = strtolower(trim($email));
if ($email !== '' && $this->bump_counter('rl_email_' . md5($email)) > self::RL_EMAIL_MAX) {
return 'You have started several quotes recently. Please try again later or contact us for help.';
}
return null;
}
private function bump_counter(string $key): int {
$tkey = 'espinstanda_' . $key;
$n = (int) get_transient($tkey) + 1;
set_transient($tkey, $n, self::RL_WINDOW);
return $n;
}
/* ----------------------------------------------------------- failure surfacing */
private function alert_failure(Transaction $tx, ?Api_Result $result): void {
(new Notifier())->validation_failure_alert($tx, $result);
}
/** In-memory transaction used to alert when no row was stored (storage off / insert failed). */
private function synth_tx(array $form, array $values, string $dedupe, string $error): Transaction {
$tx = new Transaction();
$tx->form_id = (int) ($form['id'] ?? 0);
$tx->environment = Config::environment()->value;
$tx->dedupe_key = $dedupe;
$tx->customer_email = (string) ($values['customer_email'] ?? '');
$tx->role_label = (string) ($values['role'] ?? '');
$tx->error_detail = $error;
return $tx;
}
/** Best-effort admin alert when process() itself throws (F-11). Never throws. */
private function record_hard_failure(\Throwable $e): void {
try {
$tx = new Transaction();
$tx->environment = Config::environment()->value;
$tx->error_detail = 'Unexpected error during quote submission: ' . $e->getMessage();
(new Notifier())->validation_failure_alert($tx);
} catch (\Throwable $ignored) {
// never let the failure path itself throw
}
}
} }
@@ -137,7 +137,7 @@ final class Transaction_Store {
/** Write-ahead insert. Returns the new row id (0 on failure). */ /** Write-ahead insert. Returns the new row id (0 on failure). */
public function create_pending(Transaction $t): int { public function create_pending(Transaction $t): int {
global $wpdb; global $wpdb;
$now = current_time('mysql'); $now = current_time('mysql', true); // UTC - matches the gmdate() retention/dedupe cutoffs
$t->event_log[] = self::event('Write-ahead record created (status: pending)'); $t->event_log[] = self::event('Write-ahead record created (status: pending)');
$ok = $wpdb->insert(self::table_name(), [ $ok = $wpdb->insert(self::table_name(), [
@@ -159,14 +159,14 @@ final class Transaction_Store {
} }
public function mark_success(int $id, string $quote_ref, ?string $quote_url, ?int $http_status): void { public function mark_success(int $id, string $quote_ref, ?string $quote_url, ?int $http_status): void {
$delivered = current_time('mysql'); $delivered = current_time('mysql', true); // UTC
$this->update($id, [ $this->update($id, [
'status' => Tx_Status::Delivered->value, 'status' => Tx_Status::Delivered->value,
'quote_ref' => $quote_ref, 'quote_ref' => $quote_ref,
'quote_url' => $quote_url, 'quote_url' => $quote_url,
'http_status' => $http_status, 'http_status' => $http_status,
'delivered_at' => $delivered, 'delivered_at' => $delivered,
'expiry' => $this->compute_expiry($delivered), 'expiry' => $this->compute_expiry(),
], 'Delivered - quoteRef ' . $quote_ref); ], 'Delivered - quoteRef ' . $quote_ref);
} }
@@ -251,6 +251,22 @@ final class Transaction_Store {
return $row ? Transaction::from_row($row) : null; return $row ? Transaction::from_row($row) : null;
} }
/**
* Most recent delivered transaction for a content-addressed dedupe key within a window.
* Powers the cross-request duplicate guard: an identical submission that already
* delivered recently is reused instead of creating a second quote.
*/
public function recent_delivered_by_dedupe(string $dedupe_key, int $within_seconds): ?Transaction {
global $wpdb;
$table = self::table_name();
$cutoff = gmdate('Y-m-d H:i:s', time() - $within_seconds);
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE dedupe_key = %s AND status = %s AND delivered_at >= %s ORDER BY id DESC LIMIT 1",
$dedupe_key, Tx_Status::Delivered->value, $cutoff
), ARRAY_A);
return $row ? Transaction::from_row($row) : null;
}
/** /**
* Paged list for the admin browser, scoped to the active environment. * Paged list for the admin browser, scoped to the active environment.
* *
@@ -308,19 +324,43 @@ final class Transaction_Store {
/* ----------------------------------------------------------------- retention */ /* ----------------------------------------------------------------- retention */
/** /**
* Purge ONLY delivered records past the retention window for the given env. * Purge delivered records past the retention window for the given env, EXCEPT
* Pending/failed are never deleted on a timer. Returns the number removed. * stranded leads (delivered but never attached to an entry) which must survive so a
* quote whose customer was never notified is not silently lost. Returns rows removed.
*/ */
public function purge_delivered_older_than(int $days, Environment $env): int { public function purge_delivered_older_than(int $days, Environment $env): int {
global $wpdb; global $wpdb;
$table = self::table_name(); $table = self::table_name();
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS)); $cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
return (int) $wpdb->query($wpdb->prepare( return (int) $wpdb->query($wpdb->prepare(
"DELETE FROM {$table} WHERE status = %s AND environment = %s AND delivered_at IS NOT NULL AND delivered_at < %s", "DELETE FROM {$table} WHERE status = %s AND environment = %s AND entry_id IS NOT NULL AND delivered_at IS NOT NULL AND delivered_at < %s",
Tx_Status::Delivered->value, $env->value, $cutoff Tx_Status::Delivered->value, $env->value, $cutoff
)); ));
} }
/**
* Scrub PII from unresolved (failed/pending) records older than the window while
* keeping the row for audit. Failed/pending rows are never timer-deleted, so without
* this their customer email/payload would be retained indefinitely. Returns rows scrubbed.
*/
public function anonymize_stale_unresolved(int $days): int {
global $wpdb;
$table = self::table_name();
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
return (int) $wpdb->query($wpdb->prepare(
"UPDATE {$table} SET customer_email = '', redacted_payload = '', response_excerpt = NULL
WHERE status IN ('failed', 'pending') AND customer_email <> '' AND created_at < %s",
$cutoff
));
}
/** Data-subject erasure: remove every transaction for a customer email. Returns rows deleted. */
public function delete_by_email(string $email): int {
global $wpdb;
$table = self::table_name();
return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE customer_email = %s", $email));
}
/* ----------------------------------------------------------------- internals */ /* ----------------------------------------------------------------- internals */
private function update(int $id, array $data, ?string $event): void { private function update(int $id, array $data, ?string $event): void {
@@ -335,14 +375,14 @@ final class Transaction_Store {
$wpdb->update(self::table_name(), $data, ['id' => $id]); $wpdb->update(self::table_name(), $data, ['id' => $id]);
} }
private function compute_expiry(string $delivered_at): string { /** Expiry = now + retention window, in UTC (delivered_at is written at ~the same instant). */
private function compute_expiry(): string {
$days = (int) get_option('espinstanda_retention_days', 7); $days = (int) get_option('espinstanda_retention_days', 7);
$ts = strtotime($delivered_at) ?: time(); return gmdate('Y-m-d H:i:s', time() + ($days * DAY_IN_SECONDS));
return gmdate('Y-m-d H:i:s', $ts + ($days * DAY_IN_SECONDS));
} }
/** @return array{ts:string,level:string,message:string} */ /** @return array{ts:string,level:string,message:string} */
private static function event(string $message, string $level = 'info'): array { private static function event(string $message, string $level = 'info'): array {
return ['ts' => current_time('mysql'), 'level' => $level, 'message' => Logger::redact($message)]; return ['ts' => current_time('mysql', true), 'level' => $level, 'message' => Logger::redact($message)];
} }
} }
@@ -83,7 +83,7 @@ final class Transactions_List_Table extends \WP_List_Table {
protected function column_default($item, $column_name): string { protected function column_default($item, $column_name): string {
return match ($column_name) { return match ($column_name) {
'submitted' => esc_html($item->created_at) . '<br><span class="mono">#' . $item->id . '</span>', 'submitted' => esc_html(get_date_from_gmt($item->created_at, 'Y-m-d H:i')) . '<br><span class="mono">#' . $item->id . '</span>',
'email' => esc_html($item->customer_email), 'email' => esc_html($item->customer_email),
'role' => esc_html($item->role_label), 'role' => esc_html($item->role_label),
'status' => $this->status_badge($item), 'status' => $this->status_badge($item),
@@ -136,15 +136,21 @@ final class Resubmit_Handler {
if ($tx === null) { if ($tx === null) {
return; return;
} }
if ($tx->status === Tx_Status::Delivered->value) {
$store->append_event($tx_id, 'Resubmit blocked - already delivered (would duplicate the quote).', 'error');
return;
}
if ($tx->environment !== Config::environment()->value) { if ($tx->environment !== Config::environment()->value) {
$store->append_event($tx_id, 'Resubmit blocked - cross-environment.', 'error'); $store->append_event($tx_id, 'Resubmit blocked - cross-environment.', 'error');
return; return;
} }
// Self-recovery guard: skip if the customer already has a recent successful quote. // Self-recovery guard: skip only when an IDENTICAL submission already delivered
$recent = $store->recent_success_for_email($tx->customer_email, DAY_IN_SECONDS); // recently. Scoped to the content dedupe key so a customer's different event
// (different key) is never blocked.
$recent = $store->recent_delivered_by_dedupe($tx->dedupe_key, Submission::DEDUPE_WINDOW);
if ($recent !== null && $recent->id !== $tx->id) { if ($recent !== null && $recent->id !== $tx->id) {
$store->append_event($tx_id, 'Resubmit skipped - customer already has a recent successful quote.'); $store->append_event($tx_id, 'Resubmit skipped - an identical submission already has a recent quote.');
return; return;
} }
@@ -153,14 +159,24 @@ final class Resubmit_Handler {
$store->append_event($tx_id, 'Resubmit aborted - stored payload unreadable.', 'error'); $store->append_event($tx_id, 'Resubmit aborted - stored payload unreadable.', 'error');
return; return;
} }
// Trust boundary: the stored body is untrusted input on the way back out to the
// insurer - re-validate it before re-sending (guards against DB tampering).
$payload_errors = (new Field_Mapper())->validate_payload($body);
if ($payload_errors) {
$store->append_event($tx_id, 'Resubmit aborted - stored payload failed re-validation: ' . implode(', ', $payload_errors), 'error');
return;
}
$store->increment_retry($tx_id); // this attempt is a retry - keep the count truthful
$result = (new Api_Client())->start_quote($body, $tx->dedupe_key); $result = (new Api_Client())->start_quote($body, $tx->dedupe_key);
if ($result->ok && $result->quote_ref !== null) { if ($result->ok && $result->quote_ref !== null && $result->quote_ref !== '') {
$store->mark_success($tx_id, $result->quote_ref, $result->url_single_use, $result->http_status); $store->mark_success($tx_id, $result->quote_ref, $result->url_single_use, $result->http_status);
Failure_Surface::bump(); Failure_Surface::bump();
if ($tx->entry_id) { if ($tx->entry_id) {
Entry_Integration::record($tx->entry_id, $result->quote_ref, (string) $result->url_single_use, 'delivered', $tx->environment); Entry_Integration::record($tx->entry_id, $result->quote_ref, (string) $result->url_single_use, 'delivered', $tx->environment);
self::notify_resubmit_success((int) $tx->entry_id);
} else { } else {
// Stranded lead (no entry) - mint a fresh link and email the customer (option B). // Stranded lead (no entry) - mint a fresh link and email the customer (option B).
$fresh = (string) $result->url_single_use; $fresh = (string) $result->url_single_use;
@@ -176,4 +192,19 @@ final class Resubmit_Handler {
Failure_Surface::bump(); Failure_Surface::bump();
} }
} }
/** Dispatch the registered espinstanda_resubmit_success notification event for an entry. */
private static function notify_resubmit_success(int $entry_id): void {
if (!class_exists('\GFAPI')) {
return;
}
$entry = \GFAPI::get_entry($entry_id);
if (is_wp_error($entry)) {
return;
}
$form = \GFAPI::get_form((int) $entry['form_id']);
if ($form) {
\GFAPI::send_notifications($form, $entry, 'espinstanda_resubmit_success');
}
}
} }
@@ -0,0 +1,67 @@
=== ESP INSTANDA Start Quote Integration ===
Contributors: getfused
Requires at least: 7.0
Requires PHP: 8.5
Stable tag: 1.1.0
Pushes Gravity Forms "Start a Quote" submissions into INSTANDA's Package API with
write-ahead transaction logging, native entry recording, admin resubmit, and a test-mode lock.
== Changelog ==
= 1.1.0 =
Remediation of the 2026-07 security/reliability/PII audit (see docs/audit-esp-instanda-2026-07.md).
No duplicate policies / no lost quotes:
* Dedupe key is now content-addressed (form id + mapped field values) instead of a
per-request token, so an identical re-POST or a retry-after-error reuses the existing
quote instead of creating a second one at INSTANDA (F-01). Adds a 30-minute
short-circuit window and a content-key race lock.
* Refuse to resubmit an already-delivered transaction (F-12); scope the resubmit
self-recovery guard to the content dedupe key so a customer's *different* event is not
skipped (F-18).
* A 2xx response with a missing/empty quoteRef is now recorded as ambiguous
(possible_duplicate = 1), not a hard failure (F-10, F-17).
* Aborts the StartQuote when the write-ahead insert fails, so no unaudited call is made (F-14).
Go-live safety:
* switch_to_live() now also sets the environment to Live; previously the go-live button
left real quotes hitting the Test endpoint (F-03).
* Go-live advisory flags when more than one active INSTANDA feed exists (F-02).
Security & PII:
* Corrected the at-rest-secret docblock: the "DB dump never exposes a secret" guarantee
only holds when SPG_INSTANDA_KEY is set as a constant (F-04).
* Retention now scrubs PII from stale failed/pending rows (previously kept forever) and
exempts stranded delivered leads from the purge (F-05, F-13); wired WordPress's Erase
Personal Data tool to delete a customer's transactions (F-05).
* Uninstall now removes ALL plugin options and transients, including the encrypted
password and its key (F-15).
* Added per-IP (20/h) and per-email (5/h) rate limiting on the public create (F-06).
Reliability & correctness:
* Timestamps are written in UTC to match the retention/dedupe cutoffs and displayed in
site-local time (F-07). NOTE: rows created before 1.1.0 were stored in site-local time;
clear the transaction table (test data) at deploy to avoid a mixed-timezone history.
* Fail closed on an unexpected error in the create path (no quote-less entry) and alert
admins, instead of silently passing (F-11); log when async recording cannot resolve a
transaction (F-09).
* The "Gravity Forms required" admin notice is now shown even when Gravity Forms is
inactive (F-16); the daily retention cron self-heals on admin_init (F-22).
* Resubmit re-validates the stored payload before re-sending (F-19); the Retries count is
now incremented and the resubmit-success notification event is dispatched (F-20).
Theme:
* The Form-5 date-range inline JS is emitted only on pages that embed a Gravity Form and
its MutationObserver now disconnects instead of watching the DOM forever (F-23).
Operations required alongside this release (see the audit report, Ops checklist):
* Set SPG_INSTANDA_KEY + SPG_INSTANDA_USERNAME_LIVE/_PASSWORD_LIVE + SPG_INSTANDA_ENV in
production wp-config.php (blocking) - OPS-1 / F-04.
* Deactivate the INSTANDA feed on the test Form 6 in production (blocking) - OPS-3 / F-02.
* Configure espinstanda_alert_emails and the quote-success notification (blocking) - OPS-4.
* Rotate the SendGrid API key out of committed wp-config.php - OPS-2 / F-08.
* Add reCAPTCHA/Akismet to the canonical quote form - OPS-5 / F-06.
= 1.0.7 =
* Initial audited release.
@@ -20,10 +20,19 @@ try {
// Table name is a trusted internal constant, not user input. // Table name is a trusted internal constant, not user input.
$wpdb->query("DROP TABLE IF EXISTS {$table}"); $wpdb->query("DROP TABLE IF EXISTS {$table}");
delete_option('espinstanda_db_version'); // Remove EVERY plugin option and transient - including the encrypted Test password
delete_option('espinstanda_operating_mode_audit'); // and its self-provisioned key - so no recoverable secret survives uninstall.
$opt = $wpdb->esc_like('espinstanda_') . '%';
$tr = '_transient_' . $wpdb->esc_like('espinstanda_') . '%';
$trt = '_transient_timeout_' . $wpdb->esc_like('espinstanda_') . '%';
$wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s",
$opt,
$tr,
$trt
));
// Clear any scheduled cleanup that may still be queued. // Clear any scheduled events that may still be queued.
wp_clear_scheduled_hook('esp_instanda_cleanup'); wp_clear_scheduled_hook('esp_instanda_cleanup');
} catch (\Throwable $e) { } catch (\Throwable $e) {
if (function_exists('error_log')) { if (function_exists('error_log')) {
+16 -2
View File
@@ -189,7 +189,17 @@ function gf_validate_date_range( $validation_result ) {
add_action( 'wp_footer', 'gf_date_range_scripts' ); add_action( 'wp_footer', 'gf_date_range_scripts' );
function gf_date_range_scripts() { function gf_date_range_scripts() {
if ( ! class_exists( 'GFForms' ) ) { if ( ! class_exists( 'GFForms' ) || is_admin() ) {
return;
}
// Only emit this inline script on requests whose content embeds a Gravity Form -
// keeps ~220 lines of JS off every other page. Server-side validation is unaffected.
// If the post cannot be inspected, fall back to emitting (safe default).
$post = get_post();
if ( $post instanceof WP_Post
&& ! has_shortcode( (string) $post->post_content, 'gravityform' )
&& ! has_block( 'gravityforms/form', $post )
&& false === strpos( (string) $post->post_content, 'gform' ) ) {
return; return;
} }
?> ?>
@@ -410,10 +420,14 @@ function gf_date_range_scripts() {
} }
if ( ! attachListeners() ) { if ( ! attachListeners() ) {
var tries = 0;
var observer = new MutationObserver( function () { var observer = new MutationObserver( function () {
if ( attachListeners() ) observer.disconnect(); // Stop once the fields are wired, or after a bounded number of tries, so we
// never observe the whole document subtree indefinitely on non-form pages.
if ( attachListeners() || ++tries > 20 ) observer.disconnect();
} ); } );
observer.observe( document.body, { childList: true, subtree: true } ); observer.observe( document.body, { childList: true, subtree: true } );
setTimeout( function () { observer.disconnect(); }, 10000 );
} }
} )(); } )();