feat(forms): gate Form A production embedding
This commit is contained in:
17 files changed
+280
-43
No files matched your search
+5
-2
@@ -5,10 +5,13 @@ PUBLIC_COUNTERS_MOCK_RESULT=success
|
||||
PUBLIC_AIA_FORMS_MODE=off
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=false
|
||||
PUBLIC_AIA_FORMS_MOCK_RESULT=success
|
||||
# The real Form A adapter is test-only. All three gates below are required;
|
||||
# indexing-enabled builds always fail closed even when they are set.
|
||||
# Isolated-test Form A requires this environment role and approval; it remains
|
||||
# unavailable in indexing-enabled builds even when both are set.
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=local
|
||||
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=false
|
||||
# Form A production additionally requires mode=live, LIVE_APPROVED=true and
|
||||
# environment=production. PUBLIC_ALLOW_INDEXING never authorizes a form.
|
||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=false
|
||||
# Explicit, local-only Form A meeting preview; requires PUBLIC_AIA_FORMS_MODE=mock.
|
||||
PUBLIC_AIA_FORM_A_PREVIEW=false
|
||||
PUBLIC_ALLOW_INDEXING=false
|
||||
@@ -31,7 +31,22 @@ PUBLIC_AIA_FORMS_LIVE_APPROVED=false
|
||||
PUBLIC_ALLOW_INDEXING=false
|
||||
```
|
||||
|
||||
It uses fixed allowlisted hosted URLs only: the contact routes use `formContext=contact` with their fixed English or Spanish language value, and the five landing routes use `formContext=landing&language=en`. `formContext` is the exact case-sensitive Jotform Unique Name supplied by the owner; it is presentation-only and does not accept visitor query values. A missing gate, any other environment role, an indexing-enabled build, a route outside that seven-route allowlist, or `live` mode renders the normal fail-closed Astro fallback instead.
|
||||
It uses fixed allowlisted hosted URLs only: the contact routes use `formContext=contact` with their fixed English or Spanish language value, and the five landing routes use `formContext=landing&language=en`. `formContext` is the exact case-sensitive Jotform Unique Name supplied by the owner; it is presentation-only and does not accept visitor query values. In `isolated-test` mode, a missing gate, any other environment role, an indexing-enabled build or a route outside that seven-route allowlist renders the normal fail-closed Astro fallback. The separately documented `live` gate uses the same route allowlist and its own independent approvals.
|
||||
|
||||
## Production-gated Form A adapter
|
||||
|
||||
The adapter remains off in normal builds. A production artifact may render Form A only when all four independent settings are present at build time:
|
||||
|
||||
```sh
|
||||
PUBLIC_AIA_FORMS_MODE=live
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=true
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=production
|
||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true
|
||||
```
|
||||
|
||||
`PUBLIC_ALLOW_INDEXING=true` is required for an indexing-enabled release artifact, but it is **not** a Form A authorization setting and cannot activate a frame by itself. The production gate preserves the same fixed seven-route allowlist, URLs, `formContext`, locale parameters, fixed heights, sandbox, no-referrer policy and conditional fallback. It does not enable Forms B/C, service drawers, native POST forms, analytics, advertising, tracking, webhooks, API forwarding or the Jotform resize handler.
|
||||
|
||||
For a deployment rollback, set `PUBLIC_AIA_FORMS_MODE=off` or remove any one of the three approval/environment values, rebuild the static artifact, verify no Form A identifier is emitted, and switch to the reviewed artifact. This disables Form A in Astro without changing the static site or the Jotform account. Do not change Jotform notification recipients, fields or availability as part of that rollback.
|
||||
|
||||
The adapter has a localized iframe title, a fixed-height responsive frame, a `no-referrer` policy, a minimal sandbox, and a direct hosted-form fallback. The fallback is outside the iframe, initially hidden without layout space, revealed after a 15-second missing-load timeout, cleared on frame load or component disposal, and duplicated only as localized `<noscript>` content. A frame `load` cancels the timeout but is never treated as Jotform initialization, accepted submission, conversion, or proof that the cross-origin application is healthy. It loads no Jotform helper script, reads no iframe DOM, and installs no `postMessage` handler.
|
||||
|
||||
@@ -77,4 +92,4 @@ JF-01 through JF-09, privacy/security review, operations approval, fluent-langua
|
||||
|
||||
## 2026-10-28 reconciliation
|
||||
|
||||
The current records supersede the earlier Form B mapping only for the five named landing journeys: all five now use Form A under the seven-route isolated-test gate. Form B remains for the other service-consultation journeys. The official Jotform JavaScript embed handler is **conditionally acceptable pending targeted verification**; it is not included in this adapter. Any future adoption must first verify its current source, origin and source-window validation, parent-page privileges, URL mutation, network requests, Firefox behavior and all supported error states. Fixed heights remain the current approved architecture until that review is separately accepted.
|
||||
The current records supersede the earlier Form B mapping only for the five named landing journeys: all five now use Form A under the seven-route isolated-test or separately approved production gate. Form B remains for the other service-consultation journeys. The official Jotform JavaScript embed handler is **conditionally acceptable pending targeted verification**; it is not included in this adapter. Any future adoption must first verify its current source, origin and source-window validation, parent-page privileges, URL mutation, network requests, Firefox behavior and all supported error states. Fixed heights remain the current approved architecture until that review is separately accepted.
|
||||
@@ -172,6 +172,7 @@ trusted-source comparison. No insecure retrieval was repeated.
|
||||
- Form A (`262946078314059`) is implemented only in a non-indexing, seven-route `isolated-test` adapter: `/contact`, `/es/contact`, the four campaign landing routes and `/lp/early-intervention`. The five landing journeys use fixed `formContext=landing&language=en`, superseding only their earlier Form B mapping. Form B remains for the other service-consultation journeys; Form C remains professional referrals.
|
||||
- Owner-confirmed direct-Jotform evidence covers synthetic EN/ES submission, native thank-you, conditional email/language, generic no-answer/no-PDF staff notification, autoresponder delivery, corrected fixed Reply-To, no webhooks/integrations, privacy scope, recipient access and retention. The owner also reports successful Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record the exact third route before release. Functional submission acceptance is complete only for those owner-reported samples. All-seven-route accessibility, runtime-network/telemetry, deployment and site-release acceptance remain open.
|
||||
- The adapter retains fixed URLs, responsive fixed heights, `no-referrer`, `allow-forms allow-scripts allow-same-origin`, no helper script, and a hosted fallback that is hidden without space until a 15-second missing-load timeout. A frame load only clears that timeout; it does not prove cross-origin application health or accepted submission. The official Jotform resizing handler is conditionally acceptable pending separately authorized source, origin, privilege, Firefox and error-state verification; it is not implemented. Fixed heights remain current.
|
||||
- A separate local production gate is prepared for Form A only. It requires `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production` and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true`; the fixed seven-route allowlist remains enforced. `PUBLIC_ALLOW_INDEXING=true` permits an indexing-enabled release artifact but never authorizes a form. Normal/off/mock/isolated behavior remains fail-closed, and Forms B/C, service drawers, analytics and API forwarding remain inactive. Marketing-only notification routing is a pre-cutover operational check. Rolling back Form A means rebuilding with mode `off` or an absent approval value and switching only the static artifact; it does not alter Jotform.
|
||||
|
||||
The indexing-enabled build has 127 HTML routes/canonicals and 109 sitemap URLs.
|
||||
Normal staging has 128 `noindex,nofollow` pages because it also contains `/lp`;
|
||||
|
||||
@@ -49,7 +49,7 @@ The final normal staging build was restored after the indexing build. The explic
|
||||
1. Record a locked Node/npm/toolchain and reproducible artifact manifest.
|
||||
2. Obtain sanitized Contabo/NixOS/nginx/TLS/DNS/Cloudflare facts.
|
||||
3. Verify host-local serving, canonical host, redirects, 404s, cache headers and rollback on an approved staging host.
|
||||
4. Complete Jotform Builder verification, blank-form creation, off/mock testing and privacy/security review before live IDs are mapped.
|
||||
4. Form A has owner-reported functional acceptance and a local production gate, but still needs exact release-environment review, all-seven-route accessibility acceptance, runtime telemetry/privacy review, marketing-recipient operational review and no-deployment static-artifact verification. Forms B/C remain unimplemented. Use Form A only when all four explicit build settings are approved: `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true`. `PUBLIC_ALLOW_INDEXING=true` is publication behavior only.
|
||||
5. Obtain P9-04 counter origin/credential/consent approval before a live adapter.
|
||||
6. Resolve P9-05 analytics/advertising measurement and P9-07 privacy/legal approval.
|
||||
7. Obtain P9-06 clinical/organizational and fluent-language approvals, including Arabic diagrams and Spanish HSA/FSA claims.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Phase 9 — Form A production-gate preparation
|
||||
|
||||
**Recorded:** 2026-10-28 UTC
|
||||
**Scope:** local Astro configuration and static artifact verification for Form A (`262946078314059`)
|
||||
**Status:** prepared locally; no environment was enabled, deployed or contacted.
|
||||
|
||||
## Required release configuration
|
||||
|
||||
The Form A iframe resolver enables an iframe only when every value below is present at build time:
|
||||
|
||||
```sh
|
||||
PUBLIC_AIA_FORMS_MODE=live
|
||||
PUBLIC_AIA_FORMS_LIVE_APPROVED=true
|
||||
PUBLIC_AIA_FORMS_ENVIRONMENT=production
|
||||
PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true
|
||||
```
|
||||
|
||||
`PUBLIC_ALLOW_INDEXING=true` is required for an indexing-enabled release artifact but is not an authorization gate. It cannot enable Form A by itself. Missing, invalid or mismatched values render the existing off-mode presentation.
|
||||
|
||||
The live gate accepts exactly seven routes: `/contact`, `/es/contact`, `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`. It preserves fixed `formContext` and locale values, fixed heights, `no-referrer`, `allow-forms allow-scripts allow-same-origin`, the first-party conditional hosted fallback and the fixed direct hosted URL.
|
||||
|
||||
## Deliberate exclusions
|
||||
|
||||
The production gate does not activate Forms B or C, service drawers, client consultation, referral workflows, native form actions, Jotform helper scripts, browser storage, Contabo forwarding, webhooks, analytics, advertising or production counters. Submissions remain direct browser-to-Jotform; Astro does not inspect the cross-origin frame or infer a conversion from a load event.
|
||||
|
||||
## Artifact and network verification
|
||||
|
||||
The production artifact verifier requires the exact Form A URL on each approved route and rejects Form A identifiers elsewhere. It rejects live Form A output in normal staging and plain indexing validation. The guarded browser runner accepts a `production` test mode but intercepts every non-loopback request before transmission; it records the attempted Jotform GETs and rejects POST, DELETE, WebSocket, API, analytics and advertising activity.
|
||||
|
||||
### Local results — 2026-10-28
|
||||
|
||||
- `astro check`: 137 files, zero diagnostics.
|
||||
- Focused Form A, nonproduction and artifact tests: 20 passing tests, including production-gate and isolated-test artifact fixtures.
|
||||
- Production-gated indexing artifact: 127 HTML routes, 109 sitemap URLs, 180 hreflang links, no `/lp` review index and exactly seven fixed Form A frames. The artifact verifier passed.
|
||||
- Guarded Chromium production-gate run: 35 responsive/no-JavaScript checks across the seven routes at 320, 390, 768 and 1440px, plus four fallback-state checks. It recorded 38 attempted Jotform **GET** loads and blocked all 62 non-loopback requests before transmission; it made no form write, API, analytics or advertising request.
|
||||
- Guarded Chromium isolated-test run: the same 35 checks and four fallback-state checks passed. Its 38 attempted Jotform **GET** loads were likewise blocked before transmission.
|
||||
- Guarded Chromium off and mock runs: 28 route/viewport checks each passed. Neither emitted a Jotform request or iframe.
|
||||
|
||||
These are repository-side rendering and gate checks only. They do not reproduce the owner-reported direct or embedded synthetic submissions, inspect a cross-origin frame, or establish a deployed-environment acceptance.
|
||||
|
||||
## Operational checks before cutover
|
||||
|
||||
1. The release and hosting owners must compare the sanitized production environment to the four required settings above.
|
||||
2. Confirm marketing-only notification routing remains intentional. This repository does not change notification recipients.
|
||||
3. Record the exact ABA landing route used in the owner-reported embedded synthetic test.
|
||||
4. Complete all-seven-route responsive, keyboard, screen-reader, language, validation-error and fallback acceptance.
|
||||
5. Review Jotform runtime requests, telemetry, CSP, referrer and static-host logging with privacy/security owners.
|
||||
6. Build both normal staging and the gated indexing-enabled release artifact, then run the artifact verifier and guarded browser test before release authorization.
|
||||
|
||||
## Rollback
|
||||
|
||||
Set `PUBLIC_AIA_FORMS_MODE=off` or remove any required live approval/environment value. Rebuild, verify the artifact contains no Form A iframe or identifier, and atomically switch to that reviewed static artifact. This disables the Astro embedding without changing the live site content or the Jotform account. It is not a substitute for the separate site rollback and Phase 10 release procedure.
|
||||
|
||||
No Jotform settings, forms, submissions, recipient routes, production API, DNS, hosting configuration, analytics or advertising were modified by this preparation.
|
||||
@@ -1,11 +1,13 @@
|
||||
# P9-03 source-specific implementation plan
|
||||
|
||||
Date: **2026-10-18** · Plan version **1.5 revision 2026-10-28** · Governing contract: [P9-03-JF-2026-10-18 v1.5](phase-9-jotform-integration-contract-2026-10-18.md).
|
||||
Date: **2026-10-18** · Plan version **1.6 revision 2026-10-28** · Governing contract: [P9-03-JF-2026-10-18 v1.6](phase-9-jotform-integration-contract-2026-10-18.md).
|
||||
|
||||
**Planning only. Do not execute any batch from this document without separate authorization.** Phase 9 establishes decisions and bounded implementation authority. Phase 10 retains final production integration acceptance, rollback and cutover. The user stages, signs and pushes.
|
||||
|
||||
> **Form A reconciliation — 2026-10-28 (current implementation evidence, no production release approval):** Form A — **AIA | Website | General Enquiries** (`262946078314059`) has owner-confirmed direct EN/ES synthetic submission, native thank-you, generic no-answer/no-PDF staff notification, autoresponder delivery, fixed hidden Reply-To, no-webhook/integration, conditional-email and language evidence. The owner also reports successful isolated Astro-iframe synthetic submission and confirmation on English contact, Spanish contact and one ABA landing placement; record that third route explicitly before release rather than inferring it. This completes functional submission acceptance for the reported samples, not seven-route responsive, accessibility, telemetry/privacy, deployment or site-release acceptance. Its hidden presentation field is **Advanced → Field Details → Unique Name = `formContext`** (exact case). The current Astro adapter is implemented only for gated, non-indexing isolated tests: `/contact`, `/es/contact`, `/lp/aba-therapy`, `/lp/aba-insurance`, `/lp/aba-ahcccs`, `/lp/aba-private-pay`, and `/lp/early-intervention`; the five landing journeys now use fixed `formContext=landing`, superseding their earlier Form B mapping. Forms B and C remain planned; their field keys and IDs must not be guessed.
|
||||
|
||||
> **Production-gate addendum — 2026-10-28:** The local adapter now also supports the same seven Form A placements only when `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all set. This is a build-time configuration contract, not a deployed environment or site-release authorization. It leaves Forms B/C, drawers and every unrelated lead surface in their off/mock path. An indexing-enabled release artifact is compatible with the gate, but `PUBLIC_ALLOW_INDEXING` never enables a form.
|
||||
|
||||
## 1. Actual baseline and reconciliation
|
||||
|
||||
Verified `faithful-astro-migration`, HEAD `59911eb929579af7bcd6c42de6476128dab62289`, clean tracked tree and empty index. Three October 17 untracked inventory artifacts exist, alongside the five exclusions and `.phase8-stage-paths.nul`. No unexpected implementation change was found. The prior 27-row owner worksheet remains blank; the current supplied connector summary is not silently written into it.
|
||||
@@ -240,3 +242,5 @@ Version 1.3, 2026-10-27: records owner-confirmed Form A configuration evidence f
|
||||
Version 1.4, 2026-10-28: reconciles owner-confirmed direct Form A runtime evidence and the implemented seven-route isolated iframe adapter. The five landing routes now use Form A under the isolated-test gate, superseding their earlier Form B mapping; Form B remains for other service-consultation journeys. Direct tests do not close embedded-iframe, runtime/privacy, accessibility or production gates.
|
||||
|
||||
Version 1.5, 2026-10-28: records owner-reported embedded synthetic submission and confirmation on the English and Spanish contact routes plus one ABA landing placement. It completes Form A functional submission acceptance for the reported samples, while retaining route-specific confirmation, all-seven-route responsive/accessibility, runtime/privacy, deployment and release gates.
|
||||
|
||||
Version 1.6, 2026-10-28: adds the exact production-gate configuration for Form A and its seven fixed placements. It does not activate an environment, change the Jotform account, map Forms B/C, or authorize a production release.
|
||||
@@ -1,10 +1,10 @@
|
||||
# P9-03 Jotform integration contract
|
||||
|
||||
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.5 · **Decision revision:** 2026-10-28
|
||||
**Contract ID:** P9-03-JF-2026-10-18 · **Version:** 1.6 · **Decision revision:** 2026-10-28
|
||||
|
||||
**Status: documented planning architecture; implementation and activation NOT authorized.**
|
||||
**Status: Form A owner-approved; production-gated adapter prepared locally; deployment and site release NOT authorized.**
|
||||
|
||||
This is the authoritative P9-03 planning contract for the owner decisions supplied on October 18 and revised through October 28. It supersedes conflicting *proposals* in the [October 17 inventory](phase-9-form-entry-point-inventory-2026-10-17.md), without rewriting that historical record. It does not supersede project policy, approve clinical/legal content beyond the Form A configuration scope recorded below, close the Phase 9 register, or authorize production changes. Future changes require an explicit owner decision, a version/change record and evidence links. Version 1.0 records the original planning instruction; version 1.1 adds the owner-supplied complete roster; version 1.2 supersedes the copy-based Form B proposal with three new blank forms; version 1.3 records limited owner-confirmed Form A configuration evidence; version 1.4 reconciles completed direct-Jotform testing and the seven-route isolated iframe adapter; version 1.5 records owner-reported embedded synthetic acceptance for sampled routes without treating it as seven-route release acceptance. No future commit SHA is invented.
|
||||
This is the authoritative P9-03 planning contract for the owner decisions supplied on October 18 and revised through October 28. It supersedes conflicting *proposals* in the [October 17 inventory](phase-9-form-entry-point-inventory-2026-10-17.md), without rewriting that historical record. It does not supersede project policy, approve clinical/legal content beyond the Form A configuration scope recorded below, close the Phase 9 register, or authorize production changes. Future changes require an explicit owner decision, a version/change record and evidence links. Version 1.0 records the original planning instruction; version 1.1 adds the owner-supplied complete roster; version 1.2 supersedes the copy-based Form B proposal with three new blank forms; version 1.3 records limited owner-confirmed Form A configuration evidence; version 1.4 reconciles completed direct-Jotform testing and the seven-route isolated iframe adapter; version 1.5 records owner-reported embedded synthetic acceptance for sampled routes; version 1.6 adds the independent production-build gate without deploying it. No future commit SHA is invented.
|
||||
|
||||
Companions: [source-specific implementation plan](phase-9-jotform-implementation-plan-2026-10-18.md), [verification and approval ledger](phase-9-jotform-verification-checklist-2026-10-18.csv).
|
||||
|
||||
@@ -67,7 +67,7 @@ The owner additionally reports successful synthetic submission and native confir
|
||||
|
||||
The remaining four or five landing placements, depending on the exact sampled ABA route, have automated rendering and fixed-URL coverage only. The current adapter is restricted to the seven placements listed in section 5 and to a separately gated, non-indexing `isolated-test` build. Its frame `load` event is only a transport signal; it is never treated as Jotform initialization, an accepted submission, or a conversion.
|
||||
|
||||
Open Form A gates are therefore: final responsive, keyboard, screen-reader and fluent-language acceptance across all seven placements; Jotform runtime/network and telemetry privacy review; deployment/hosting acceptance; and final site-release authorization.
|
||||
The production-gated adapter is now prepared in the repository, but no deployment environment is configured by this record. Open Form A gates are therefore: final responsive, keyboard, screen-reader and fluent-language acceptance across all seven placements; Jotform runtime/network and telemetry privacy review; marketing-recipient operational review; deployment/hosting acceptance; and final site-release authorization.
|
||||
|
||||
## 3. Account assets and accounting for all 27
|
||||
|
||||
@@ -228,7 +228,7 @@ Suggested semantic setting: `PUBLIC_AIA_FORMS_MODE`, exact values `off`, `mock`,
|
||||
| off | No active lead form/empty POST action, live iframe, Jotform preconnect/helper/network request, or analytics. Render approved localized contact fallback; no native-submit bypass on inactive lead UI |
|
||||
| mock | Synthetic local-only fixture, no external endpoint or real data; safe guarded Playwright use; no Jotform ID required |
|
||||
| isolated-test | Form A only: requires `PUBLIC_AIA_FORMS_MODE=isolated-test`, `PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test`, `PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=true`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=false`, `PUBLIC_ALLOW_INDEXING=false`, and a fixed seven-route allowlist. It is not a production mode. |
|
||||
| live | Separate owner/deployment approval; exact approved IDs/HTTPS origins/URLs and build/deploy gate; no automatic original-form fallback. The current Form A adapter rejects it. |
|
||||
| live | Form A only: `PUBLIC_AIA_FORMS_MODE=live`, `PUBLIC_AIA_FORMS_LIVE_APPROVED=true`, `PUBLIC_AIA_FORMS_ENVIRONMENT=production`, and `PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=true` are all required. The fixed seven-route allowlist, fixed origin/URLs, locale and context still apply. Missing/invalid values fail closed. `PUBLIC_ALLOW_INDEXING` is independent: it must be `true` for an indexing-enabled release but cannot enable Form A alone. Forms B/C and all other lead surfaces remain off. |
|
||||
|
||||
Off/mock output must not contain **new lead embed IDs/URLs or the historical `250727122848156` reference ID** in HTML, scripts, preload hints or client bundles. Existing clinical/Sign links intentionally remain in the site; they already contain production destinations. Preserving them conflicts with a blanket “no production ID anywhere” assertion, so this contract's new-embed restriction is scoped to the new lead integration. Existing user-initiated clinical navigation is not permission for automatic loading or test submissions. Do not claim the whole site has zero vendor destinations.
|
||||
|
||||
@@ -303,3 +303,5 @@ Version 1.3, 2026-10-27: records owner-confirmed Form A (`262946078314059`) evid
|
||||
Version 1.4, 2026-10-28: records owner-confirmed direct Form A EN/ES synthetic submission, native thank-you, notification/autoresponder delivery, fixed hidden Reply-To, no-answer/no-PDF staff notification, no webhook/integration, conditional-email and language evidence. It reconciles the five landing journeys to gated Form A isolated-test placements, documents the conditional hosted fallback, and preserves the remaining embedded-iframe, runtime/privacy and production gates. No repository-side Jotform access, submission, activation or deployment occurred.
|
||||
|
||||
Version 1.5, 2026-10-28: records the owner's successful embedded synthetic submission and native-confirmation report for English contact, Spanish contact and an ABA landing placement. It marks Form A functional submission acceptance complete only for those owner-reported samples, explicitly keeps the third route name to be recorded, and retains seven-route accessibility, runtime/privacy, deployment and site-release gates. It changes the official Jotform embed-handler decision from rejected to conditionally acceptable pending a separately authorized source, origin, privilege and Firefox/error-state review; the handler is not implemented.
|
||||
|
||||
Version 1.6, 2026-10-28: adds the locally tested Form A production-build gate. It requires `live`, the existing live approval, `production` environment role and a new independent Form A production approval; it permits an indexing-enabled build but does not derive authorization from indexing. It leaves normal/off/mock/isolated behavior unchanged, maps no Forms B/C or service drawers, and does not deploy or change Jotform.
|
||||
@@ -16,4 +16,5 @@ OA-05,Privacy/legal/security,"Protected fields, generic email/headers/recipients
|
||||
OA-06,Marketing,Deferred attribution categories and redesigned successful-submission semantics,Marketing,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-07,Hosting owner,"Static/API separation, compile/deploy gates, CSP/referrer/log policy, preview isolation and fallback topology",Hosting owner,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
OA-08,Release owner,"Phase 10 integration activation, go/no-go, artifact/vendor rollback and deployment",Release owner,Dated owner approval with scope and sanitized supporting evidence,Pending/Unverified,,Separate batch authorization; no action authorized by this checklist
|
||||
FA-PRD-01,Form A production build gate,"Confirm live mode, existing live approval, production environment role and independent Form A production approval are all set only in the intended release environment; verify no Form B/C or unrelated surface activates",Release + hosting owner,"Repository configuration resolver and production-artifact test; no deployed environment configured",Implemented locally — deployment verification pending,2026-10-28,Phase 10 deployment authorization and sanitized environment review
|
||||
INV-01,Supplied roster provenance and remaining verification,Complete 27-ID roster received and uniqueness checked; verify uncertain generic titles and relevant operational classification with account owner without inferring usage or retrieving submissions,Jotform account owner,Owner-supplied read-only connector roster; contract v1.2 section 3 lists all 27 unique IDs; individual roster no longer outstanding; new-form configuration and usage verification pending,Pending/Unverified,,Read-only metadata review authorization; no account mutation
|
||||
|
@@ -20,8 +20,8 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
|
||||
role="group"
|
||||
aria-labelledby="jotform-iframe-heading"
|
||||
data-jotform-form-a
|
||||
data-form-mode="isolated-test"
|
||||
data-form-environment="isolated-test"
|
||||
data-form-mode={config.mode}
|
||||
data-form-environment={config.environmentRole}
|
||||
data-logical-form="general"
|
||||
data-form-context={config.context}
|
||||
data-jotform-source="approved-form-a"
|
||||
|
||||
@@ -26,6 +26,7 @@ const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route: campaignRoute,
|
||||
|
||||
@@ -6,6 +6,7 @@ const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route: '/lp/early-intervention',
|
||||
@@ -38,7 +39,7 @@ const formAIframe = resolveFormAIframe({
|
||||
</div>
|
||||
</section>
|
||||
<div class="qr-process-band">
|
||||
<section class="qr-section qr-contact-section" aria-label="Contact AIA about early intervention"><div id="consultation" class="lead-form" aria-labelledby="consultation-heading" role="group" data-forms-mode={formAIframe.enabled ? 'isolated-test' : 'off'}>
|
||||
<section class="qr-section qr-contact-section" aria-label="Contact AIA about early intervention"><div id="consultation" class="lead-form" aria-labelledby="consultation-heading" role="group" data-forms-mode={formAIframe.enabled ? formAIframe.mode : 'off'}>
|
||||
|
||||
<div class="form-heading">
|
||||
<h2 id="consultation-heading">Contact AIA about early intervention</h2>
|
||||
|
||||
@@ -19,6 +19,7 @@ const formAIframe = resolveFormAIframe({
|
||||
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
|
||||
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
|
||||
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED,
|
||||
productionApproved: import.meta.env.PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED,
|
||||
environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
|
||||
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
|
||||
route,
|
||||
|
||||
@@ -25,9 +25,10 @@ export function formModeForRuntime(config: FormModeConfig): 'off' | 'mock' {
|
||||
|
||||
export type FormAIframeReason =
|
||||
| 'enabled'
|
||||
| 'mode-not-isolated-test'
|
||||
| 'mode-not-authorized'
|
||||
| 'isolated-test-not-approved'
|
||||
| 'live-approval-not-disabled'
|
||||
| 'production-approval-not-granted'
|
||||
| 'invalid-environment-role'
|
||||
| 'indexing-enabled'
|
||||
| 'route-not-authorized'
|
||||
@@ -38,6 +39,8 @@ export interface FormAIframeConfig {
|
||||
reason: FormAIframeReason;
|
||||
route: FormAIframeRoute | string;
|
||||
locale: 'en' | 'es' | string;
|
||||
mode?: 'isolated-test' | 'live';
|
||||
environmentRole?: 'isolated-test' | 'production';
|
||||
context?: FormAIframeContext;
|
||||
url?: string;
|
||||
title?: string;
|
||||
@@ -49,6 +52,7 @@ export interface ResolveFormAIframeOptions {
|
||||
rawMode: string | undefined;
|
||||
liveApproved: string | undefined;
|
||||
isolatedTestApproved: string | undefined;
|
||||
productionApproved: string | undefined;
|
||||
environmentRole: string | undefined;
|
||||
allowIndexing: string | undefined;
|
||||
route: string;
|
||||
@@ -57,12 +61,13 @@ export interface ResolveFormAIframeOptions {
|
||||
|
||||
/**
|
||||
* The owner-approved Form A identifier is deliberately only made into an
|
||||
* iframe URL after every isolated-test gate has passed. It is not a
|
||||
* production setting and it is not enabled by an API URL or a live mode.
|
||||
* iframe URL after every explicit isolated-test or production gate has
|
||||
* passed. It is never enabled by an API URL, hostname, or publication flag.
|
||||
*/
|
||||
export const formAIframeOrigin = 'https://form.jotform.com' as const;
|
||||
export const formAIframeId = '262946078314059' as const;
|
||||
const isolatedTestEnvironmentRole = 'isolated-test';
|
||||
const productionEnvironmentRole = 'production';
|
||||
|
||||
export type FormAIframeContext = 'contact' | 'landing';
|
||||
export type FormAIframeRoute =
|
||||
@@ -81,7 +86,8 @@ interface FormAIframePlacement {
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the complete fixed allowlist for the isolated Form A adapter.
|
||||
* This is the complete fixed allowlist for the Form A adapter in both
|
||||
* explicitly approved modes.
|
||||
* Campaign placements are an owner-approved override of their earlier
|
||||
* logical Form B mapping. The shared compact landing presentation uses the
|
||||
* same fixed `landing` context; no route may supply its own context or URL.
|
||||
@@ -121,8 +127,9 @@ export function buildFormAIframeUrl(locale: 'en' | 'es', context: FormAIframeCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the real Form A iframe only in an explicitly approved isolated
|
||||
* test build. `live` is intentionally not a valid route to this adapter.
|
||||
* Enables the real Form A iframe only through one of two explicit gates:
|
||||
* a non-indexing isolated test, or a separately approved production build.
|
||||
* No API URL, hostname, indexing setting, or form ID can activate it alone.
|
||||
*/
|
||||
export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIframeConfig {
|
||||
const disabled = (reason: Exclude<FormAIframeReason, 'enabled'>): FormAIframeConfig => ({
|
||||
@@ -132,11 +139,25 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
||||
locale: options.locale,
|
||||
});
|
||||
const config = resolveFormMode(options.rawMode, options.liveApproved);
|
||||
if (config.mode !== 'isolated-test') return disabled('mode-not-isolated-test');
|
||||
const environmentRole = options.environmentRole?.trim().toLowerCase();
|
||||
let mode: 'isolated-test' | 'live';
|
||||
let approvedEnvironment: 'isolated-test' | 'production';
|
||||
|
||||
if (config.mode === 'isolated-test') {
|
||||
if (options.liveApproved?.trim().toLowerCase() !== 'false') return disabled('live-approval-not-disabled');
|
||||
if (!isTrue(options.isolatedTestApproved)) return disabled('isolated-test-not-approved');
|
||||
if (options.environmentRole?.trim().toLowerCase() !== isolatedTestEnvironmentRole) return disabled('invalid-environment-role');
|
||||
if (environmentRole !== isolatedTestEnvironmentRole) return disabled('invalid-environment-role');
|
||||
if (isTrue(options.allowIndexing)) return disabled('indexing-enabled');
|
||||
mode = 'isolated-test';
|
||||
approvedEnvironment = 'isolated-test';
|
||||
} else if (config.mode === 'live') {
|
||||
if (!isTrue(options.productionApproved)) return disabled('production-approval-not-granted');
|
||||
if (environmentRole !== productionEnvironmentRole) return disabled('invalid-environment-role');
|
||||
mode = 'live';
|
||||
approvedEnvironment = 'production';
|
||||
} else {
|
||||
return disabled('mode-not-authorized');
|
||||
}
|
||||
|
||||
const placement = formAIframePlacements.find((candidate) => candidate.route === options.route);
|
||||
if (!placement) return disabled('route-not-authorized');
|
||||
@@ -147,6 +168,8 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
|
||||
reason: 'enabled',
|
||||
route: options.route,
|
||||
locale: placement.locale,
|
||||
mode,
|
||||
environmentRole: approvedEnvironment,
|
||||
context: placement.context,
|
||||
url: buildFormAIframeUrl(placement.locale, placement.context),
|
||||
...formAIframeCopy(placement.locale),
|
||||
|
||||
@@ -19,9 +19,11 @@ import { setTimeout as delay } from 'node:timers/promises';
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const moduleName = process.env.PLAYWRIGHT_MODULE ? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright';
|
||||
const mode = process.env.FORM_A_IFRAME_BROWSER_MODE ?? 'off';
|
||||
if (!['off', 'mock', 'isolated-test'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`);
|
||||
if (!['off', 'mock', 'isolated-test', 'production'].includes(mode)) throw new Error(`Unsupported Form A browser mode: ${mode}`);
|
||||
|
||||
const expectedIframe = mode === 'isolated-test';
|
||||
const expectedIframe = mode === 'isolated-test' || mode === 'production';
|
||||
const expectedFormMode = mode === 'production' ? 'live' : 'isolated-test';
|
||||
const expectedEnvironment = mode === 'production' ? 'production' : 'isolated-test';
|
||||
const routes = [
|
||||
{ route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' },
|
||||
{ route: '/es/contact', locale: 'es', context: 'contact', screenshot: 'es-contact' },
|
||||
@@ -91,7 +93,8 @@ try {
|
||||
assert.equal(await iframe.getAttribute('title'), locale === 'es' ? 'Formulario de consultas generales de AIA' : 'AIA general enquiries form', route);
|
||||
assert.equal(await iframe.getAttribute('referrerpolicy'), 'no-referrer', route);
|
||||
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', route);
|
||||
assert.equal(await page.locator('[data-jotform-form-a][data-form-mode="isolated-test"]').count(), 1, route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-mode="${expectedFormMode}"]`).count(), 1, route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-environment="${expectedEnvironment}"]`).count(), 1, route);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-context="${formContext}"]`).count(), 1, route);
|
||||
const frameBox = await iframe.boundingBox();
|
||||
const expectedFrameHeight = formContext === 'contact'
|
||||
@@ -152,6 +155,7 @@ try {
|
||||
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
assert.equal(response?.status(), 200, `${route} without JavaScript`);
|
||||
assert.equal(await page.locator('[data-jotform-form-a] iframe').count(), 1, `${route} without JavaScript`);
|
||||
assert.equal(await page.locator(`[data-jotform-form-a][data-form-mode="${expectedFormMode}"]`).count(), 1, `${route} without JavaScript`);
|
||||
const iframe = page.locator('[data-jotform-form-a] iframe');
|
||||
assert.equal(await iframe.evaluate((element) => element.innerHTML.trim()), '', `${route} without JavaScript should not place fallback content inside the iframe`);
|
||||
const noscriptFallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript');
|
||||
|
||||
@@ -15,6 +15,7 @@ const isolated = (overrides = {}) => ({
|
||||
rawMode: 'isolated-test',
|
||||
liveApproved: 'false',
|
||||
isolatedTestApproved: 'true',
|
||||
productionApproved: 'false',
|
||||
environmentRole: 'isolated-test',
|
||||
allowIndexing: 'false',
|
||||
route: '/contact',
|
||||
@@ -22,6 +23,18 @@ const isolated = (overrides = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const production = (overrides = {}) => ({
|
||||
rawMode: 'live',
|
||||
liveApproved: 'true',
|
||||
isolatedTestApproved: 'false',
|
||||
productionApproved: 'true',
|
||||
environmentRole: 'production',
|
||||
allowIndexing: 'true',
|
||||
route: '/contact',
|
||||
locale: 'en',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
assert.equal(formAIframeOrigin, 'https://form.jotform.com');
|
||||
assert.equal(formAIframeId, '262946078314059');
|
||||
@@ -54,14 +67,15 @@ test('Form A iframe uses only fixed, approved route parameters', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('Form A iframe requires every isolated-test gate and refuses live mode', () => {
|
||||
test('Form A iframe requires every isolated-test or production approval gate', () => {
|
||||
const enabled = resolveFormAIframe(isolated());
|
||||
assert.equal(enabled.enabled, true);
|
||||
assert.equal(enabled.reason, 'enabled');
|
||||
assert.equal(enabled.title, 'AIA general enquiries form');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'live', liveApproved: 'true' })).reason, 'mode-not-isolated-test');
|
||||
assert.equal(enabled.mode, 'isolated-test');
|
||||
assert.equal(enabled.environmentRole, 'isolated-test');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: undefined })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ liveApproved: 'true' })).reason, 'live-approval-not-disabled');
|
||||
assert.equal(resolveFormAIframe(isolated({ isolatedTestApproved: 'false' })).reason, 'isolated-test-not-approved');
|
||||
@@ -73,6 +87,18 @@ test('Form A iframe requires every isolated-test gate and refuses live mode', ()
|
||||
const campaign = resolveFormAIframe(isolated({ route: '/lp/aba-therapy' }));
|
||||
assert.equal(campaign.url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
|
||||
assert.equal(campaign.context, 'landing');
|
||||
const live = resolveFormAIframe(production());
|
||||
assert.equal(live.enabled, true);
|
||||
assert.equal(live.mode, 'live');
|
||||
assert.equal(live.environmentRole, 'production');
|
||||
assert.equal(live.url, 'https://form.jotform.com/262946078314059?formContext=contact&language=en');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: undefined })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ productionApproved: 'false' })).reason, 'production-approval-not-granted');
|
||||
assert.equal(resolveFormAIframe(production({ environmentRole: 'isolated-test' })).reason, 'invalid-environment-role');
|
||||
assert.equal(resolveFormAIframe(production({ liveApproved: 'false' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ rawMode: 'off' })).reason, 'mode-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/referrals' })).reason, 'route-not-authorized');
|
||||
assert.equal(resolveFormAIframe(production({ route: '/es/contact', locale: 'es' })).url, 'https://form.jotform.com/262946078314059?formContext=contact&language=es');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('isolated-test', undefined)), 'off');
|
||||
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), 'off');
|
||||
});
|
||||
@@ -86,8 +112,10 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(contact, /resolveFormAIframe/);
|
||||
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
|
||||
assert.match(contact, /JotformIframe/);
|
||||
assert.match(contact, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(campaign, /resolveFormAIframe/);
|
||||
assert.match(campaign, /JotformIframe/);
|
||||
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(campaign, /variant="embedded"/);
|
||||
assert.match(campaign, /\/lp\/aba-therapy/);
|
||||
assert.match(campaign, /const iframeDescription = 'Use this form to request a conversation\. An AIA team member can explain the next steps for your family\.'/);
|
||||
@@ -96,8 +124,11 @@ test('Only the seven allowlisted placements can render the adapter, with no help
|
||||
assert.match(earlyIntervention, /resolveFormAIframe/);
|
||||
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
|
||||
assert.match(earlyIntervention, /JotformIframe/);
|
||||
assert.match(earlyIntervention, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
|
||||
assert.match(adapter, /referrerpolicy="no-referrer"/);
|
||||
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/);
|
||||
assert.match(adapter, /data-form-mode=\{config\.mode\}/);
|
||||
assert.match(adapter, /data-form-environment=\{config\.environmentRole\}/);
|
||||
assert.match(adapter, /data-jotform-iframe-frame/);
|
||||
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
|
||||
assert.match(adapter, /<noscript>/);
|
||||
|
||||
@@ -36,18 +36,41 @@ function hasTerminalSlash(url) {
|
||||
return pathname !== '/' && pathname.endsWith('/');
|
||||
}
|
||||
|
||||
function unsafeSignals(html) {
|
||||
const formAProductionUrls = new Map([
|
||||
['/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['/es/contact', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['/lp/aba-therapy', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['/lp/aba-insurance', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['/lp/aba-ahcccs', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['/lp/aba-private-pay', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['/lp/early-intervention', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
]);
|
||||
|
||||
function iframeSources(html) {
|
||||
// Astro correctly escapes query separators in serialized HTML attributes.
|
||||
// Normalize that representation before comparing the fixed allowlisted URL.
|
||||
return [...html.matchAll(/<iframe\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi)].map((match) => match[1].replaceAll('&', '&'));
|
||||
}
|
||||
|
||||
function formAAdapters(html) {
|
||||
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
|
||||
}
|
||||
|
||||
function unsafeSignals(html, allowApprovedFormA = false) {
|
||||
const signals = [];
|
||||
if (/<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe');
|
||||
if (/262946078314059/i.test(html)) signals.push('isolated-test Form A identifier');
|
||||
if (!allowApprovedFormA && /<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe');
|
||||
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('isolated-test Form A identifier');
|
||||
if (/<script\b[^>]*\bsrc=["'][^"']*(?:jotform|googletagmanager|google-analytics|doubleclick)/i.test(html)) signals.push('active vendor tracking/form script');
|
||||
if (/data-(?:form|forms)-mode=["']live["']/i.test(html)) signals.push('live form mode');
|
||||
if (!allowApprovedFormA && /data-(?:form|forms)-mode=["']live["']/i.test(html)) signals.push('live form mode');
|
||||
if (/api\.azinstitute4autism\.com/i.test(html)) signals.push('production API origin');
|
||||
if (/AIza[0-9A-Za-z_-]{20,}|-----BEGIN (?:RSA|OPENSSH|EC) PRIVATE KEY-----|(?:api[_-]?key|secret|password)\s*[:=]/i.test(html)) signals.push('credential-like content');
|
||||
return signals;
|
||||
}
|
||||
|
||||
export async function verifyArtifact(root, mode = 'staging') {
|
||||
const formAEnabled = mode === 'isolated-test' || mode === 'production';
|
||||
const production = mode === 'production';
|
||||
const stagingPolicy = mode === 'staging' || mode === 'isolated-test';
|
||||
const htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
|
||||
const routes = [];
|
||||
const canonicalMap = new Map();
|
||||
@@ -56,6 +79,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
let hreflangLinks = 0;
|
||||
let activeForms = 0;
|
||||
let secretSignals = [];
|
||||
let formAIframes = 0;
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const html = await readText(file);
|
||||
@@ -66,12 +90,22 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const actions = formActions(html);
|
||||
const nonSearchActions = actions.filter((action) => action && action !== '/search');
|
||||
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
|
||||
const signals = unsafeSignals(html);
|
||||
const expectedFormAUrl = formAEnabled ? formAProductionUrls.get(route) : undefined;
|
||||
const signals = unsafeSignals(html, formAEnabled);
|
||||
const jotformFrames = iframeSources(html).filter((src) => {
|
||||
try {
|
||||
return new URL(src).origin === 'https://form.jotform.com';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const adapters = formAAdapters(html);
|
||||
if (!title) errors.push(`${route}: missing title`);
|
||||
if (!canonical) errors.push(`${route}: missing canonical`);
|
||||
if (!robots) errors.push(`${route}: missing robots directive`);
|
||||
if (trailingSlashUrls.length) errors.push(`${route}: root-relative URL(s) end in a slash: ${trailingSlashUrls.join(', ')}`);
|
||||
if (mode === 'staging' && robots !== 'noindex,nofollow') errors.push(`${route}: staging robots is ${robots ?? 'missing'}`);
|
||||
if (stagingPolicy && robots !== 'noindex,nofollow') errors.push(`${route}: staging robots is ${robots ?? 'missing'}`);
|
||||
if (production && robots === 'noindex,nofollow') errors.push(`${route}: production artifact is noindex`);
|
||||
if (canonical) {
|
||||
const prior = canonicalMap.get(canonical);
|
||||
if (prior) errors.push(`${route}: duplicate canonical ${canonical} (also ${prior})`);
|
||||
@@ -79,6 +113,19 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
}
|
||||
hreflangLinks += hrefValues(html, 'alternate').filter((href) => href).length;
|
||||
if (signals.length) secretSignals.push({ route, signals });
|
||||
if (formAEnabled) {
|
||||
if (expectedFormAUrl) {
|
||||
if (jotformFrames.length !== 1 || jotformFrames[0] !== expectedFormAUrl) errors.push(`${route}: missing or invalid approved Form A iframe URL`);
|
||||
const expectedMode = production ? 'live' : 'isolated-test';
|
||||
const expectedEnvironment = production ? 'production' : 'isolated-test';
|
||||
if (adapters.length !== 1 || !adapters[0].includes(`data-form-mode="${expectedMode}"`) || !adapters[0].includes(`data-form-environment="${expectedEnvironment}"`)) {
|
||||
errors.push(`${route}: Form A adapter lacks the approved ${expectedMode} ${expectedEnvironment} gate`);
|
||||
}
|
||||
formAIframes += jotformFrames.length;
|
||||
} else if (jotformFrames.length || adapters.length || /262946078314059/i.test(html)) {
|
||||
errors.push(`${route}: unauthorized Form A iframe or identifier in production artifact`);
|
||||
}
|
||||
}
|
||||
if (nonSearchActions.length) activeForms += nonSearchActions.length;
|
||||
routes.push({ route, title, robots, canonical, nativeForms: actions.length, nonSearchActions: nonSearchActions.length });
|
||||
}
|
||||
@@ -87,7 +134,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
const sitemap = await fs.readFile(sitemapFile, 'utf8').catch(() => '');
|
||||
const sitemapRoutes = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/gi)].map((match) => match[1]);
|
||||
const reviewIndex = htmlFiles.some((file) => routeFromHtml(file, root) === '/lp');
|
||||
if (mode === 'staging') {
|
||||
if (stagingPolicy) {
|
||||
if (sitemapRoutes.length) errors.push(`staging sitemap contains ${sitemapRoutes.length} URLs`);
|
||||
if (hreflangLinks) errors.push(`staging emits ${hreflangLinks} hreflang links`);
|
||||
if (!reviewIndex) errors.push('staging /lp review index is missing');
|
||||
@@ -112,7 +159,14 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
digest.update(createHash('sha256').update(await fs.readFile(file)).digest('hex'));
|
||||
digest.update('\n');
|
||||
}
|
||||
if (formAResourceFiles.length) errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
if (formAEnabled) {
|
||||
const expectedFormAFiles = new Set([...formAProductionUrls.keys()].map((route) => route === '/' ? 'index.html' : `${route.slice(1)}/index.html`));
|
||||
const unexpectedFormAFiles = formAResourceFiles.filter((file) => !expectedFormAFiles.has(file));
|
||||
if (unexpectedFormAFiles.length) errors.push(`Form A identifier appears outside approved production routes: ${unexpectedFormAFiles.join(', ')}`);
|
||||
if (formAIframes !== formAProductionUrls.size) errors.push(`production artifact has ${formAIframes} approved Form A iframe(s), expected ${formAProductionUrls.size}`);
|
||||
} else if (formAResourceFiles.length) {
|
||||
errors.push(`isolated-test Form A identifier appears in artifact: ${formAResourceFiles.join(', ')}`);
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
htmlRoutes: htmlFiles.length,
|
||||
@@ -120,6 +174,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
hreflangLinks,
|
||||
reviewIndex,
|
||||
activeNativeFormActions: activeForms,
|
||||
formAIframes,
|
||||
artifactFiles: files.length,
|
||||
artifactSha256: digest.digest('hex'),
|
||||
routes,
|
||||
@@ -132,7 +187,7 @@ export async function verifyArtifact(root, mode = 'staging') {
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|
||||
const mode = process.argv.find((arg) => arg.startsWith('--mode='))?.slice('--mode='.length) ?? 'staging';
|
||||
const root = process.argv.find((arg) => arg.startsWith('--root='))?.slice('--root='.length) ?? 'dist';
|
||||
if (!['staging', 'indexing'].includes(mode)) throw new Error(`Unsupported mode: ${mode}`);
|
||||
if (!['staging', 'indexing', 'isolated-test', 'production'].includes(mode)) throw new Error(`Unsupported mode: ${mode}`);
|
||||
const result = await verifyArtifact(path.resolve(root), mode);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.passed) process.exitCode = 1;
|
||||
|
||||
@@ -7,15 +7,31 @@ import { verifyArtifact } from './phase10-artifact-verify.mjs';
|
||||
|
||||
async function fixture(mode = 'staging') {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-phase10-artifact-'));
|
||||
const productionFrames = [
|
||||
['contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=en'],
|
||||
['es/contact/index.html', 'https://form.jotform.com/262946078314059?formContext=contact&language=es'],
|
||||
['lp/aba-therapy/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['lp/aba-insurance/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['lp/aba-ahcccs/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['lp/aba-private-pay/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
['lp/early-intervention/index.html', 'https://form.jotform.com/262946078314059?formContext=landing&language=en'],
|
||||
];
|
||||
const pages = mode === 'staging'
|
||||
? [['index.html', 'noindex,nofollow'], ['lp/index.html', 'noindex,nofollow']]
|
||||
: [['index.html', 'index,follow']];
|
||||
for (const [file, robots] of pages) {
|
||||
? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
|
||||
: mode === 'isolated-test'
|
||||
? [
|
||||
...productionFrames.map(([file, frame]) => [file, 'noindex,nofollow', `<section data-jotform-form-a data-form-mode="isolated-test" data-form-environment="isolated-test"><iframe src="${frame}"></iframe></section>`]),
|
||||
['lp/index.html', 'noindex,nofollow', ''],
|
||||
]
|
||||
: mode === 'production'
|
||||
? productionFrames.map(([file, frame]) => [file, 'index,follow', `<section data-jotform-form-a data-form-mode="live" data-form-environment="production"><iframe src="${frame}"></iframe></section>`])
|
||||
: [['index.html', 'index,follow', '']];
|
||||
for (const [file, robots, body] of pages) {
|
||||
const target = path.join(root, file);
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(target, `<!doctype html><html><head><title>${file}</title><meta name="robots" content="${robots}"><link rel="canonical" href="https://www.azinstitute4autism.com/${file}"></head><body><main><h1>${file}</h1></main></body></html>`);
|
||||
await fs.writeFile(target, `<!doctype html><html><head><title>${file}</title><meta name="robots" content="${robots}"><link rel="canonical" href="https://www.azinstitute4autism.com/${file}"></head><body><main><h1>${file}</h1>${body}</main></body></html>`);
|
||||
}
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), mode === 'staging' ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
await fs.writeFile(path.join(root, 'sitemap.xml'), ['staging', 'isolated-test'].includes(mode) ? '<urlset></urlset>' : '<urlset><url><loc>https://www.azinstitute4autism.com/</loc></url></urlset>');
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -33,6 +49,31 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
|
||||
assert.equal(result.reviewIndex, false);
|
||||
});
|
||||
|
||||
test('isolated-test artifact permits exactly the seven approved Form A iframe placements under staging safeguards', async () => {
|
||||
const result = await verifyArtifact(await fixture('isolated-test'), 'isolated-test');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 7);
|
||||
assert.equal(result.sitemapUrls, 0);
|
||||
assert.equal(result.reviewIndex, true);
|
||||
});
|
||||
|
||||
test('production artifact permits exactly the seven approved Form A iframe placements', async () => {
|
||||
const result = await verifyArtifact(await fixture('production'), 'production');
|
||||
assert.equal(result.passed, true);
|
||||
assert.equal(result.formAIframes, 7);
|
||||
assert.equal(result.reviewIndex, false);
|
||||
});
|
||||
|
||||
test('production artifact rejects an invalid Form A URL', async () => {
|
||||
const root = await fixture('production');
|
||||
const contact = path.join(root, 'contact/index.html');
|
||||
const html = await fs.readFile(contact, 'utf8');
|
||||
await fs.writeFile(contact, html.replace('formContext=contact&language=en', 'formContext=landing&language=en'));
|
||||
const result = await verifyArtifact(root, 'production');
|
||||
assert.equal(result.passed, false);
|
||||
assert.ok(result.errors.some((error) => error.includes('invalid approved Form A iframe URL')));
|
||||
});
|
||||
|
||||
test('staging artifact rejects active native form actions', async () => {
|
||||
const root = await fixture('staging');
|
||||
await fs.appendFile(path.join(root, 'index.html'), '<form action="https://example.invalid/submit"></form>');
|
||||
|
||||
Reference in new issue
Block a user