feat(forms): gate Form A production embedding

This commit is contained in:
DeCentN2Madness committed 2026-10-28 18:29:32 -07:00
1 parent 1a56cd7603
commit 0eb3fcc3f6
17 files changed
+280 -43

No files matched your search

+5 -2
View File
@@ -5,10 +5,13 @@ PUBLIC_COUNTERS_MOCK_RESULT=success
PUBLIC_AIA_FORMS_MODE=off PUBLIC_AIA_FORMS_MODE=off
PUBLIC_AIA_FORMS_LIVE_APPROVED=false PUBLIC_AIA_FORMS_LIVE_APPROVED=false
PUBLIC_AIA_FORMS_MOCK_RESULT=success PUBLIC_AIA_FORMS_MOCK_RESULT=success
# The real Form A adapter is test-only. All three gates below are required; # Isolated-test Form A requires this environment role and approval; it remains
# indexing-enabled builds always fail closed even when they are set. # unavailable in indexing-enabled builds even when both are set.
PUBLIC_AIA_FORMS_ENVIRONMENT=local PUBLIC_AIA_FORMS_ENVIRONMENT=local
PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=false 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. # Explicit, local-only Form A meeting preview; requires PUBLIC_AIA_FORMS_MODE=mock.
PUBLIC_AIA_FORM_A_PREVIEW=false PUBLIC_AIA_FORM_A_PREVIEW=false
PUBLIC_ALLOW_INDEXING=false PUBLIC_ALLOW_INDEXING=false
+17 -2
View File
@@ -31,7 +31,22 @@ PUBLIC_AIA_FORMS_LIVE_APPROVED=false
PUBLIC_ALLOW_INDEXING=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. 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 ## 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.
+1
View File
@@ -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. - 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. - 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. - 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. 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`; 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. 1. Record a locked Node/npm/toolchain and reproducible artifact manifest.
2. Obtain sanitized Contabo/NixOS/nginx/TLS/DNS/Cloudflare facts. 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. 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. 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. 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. 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 # 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. **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. > **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 ## 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. 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.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.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 # 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). 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. 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 ## 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 | | 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 | | 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. | | 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. 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.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.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-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-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 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 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
1 gate_id subject verification_required owner_role evidence_source status approval_date authorization_required
16 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
17 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
18 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
19 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
20 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
+2 -2
View File
@@ -20,8 +20,8 @@ if (!config.enabled || !config.url || !config.title || !config.fallbackText || !
role="group" role="group"
aria-labelledby="jotform-iframe-heading" aria-labelledby="jotform-iframe-heading"
data-jotform-form-a data-jotform-form-a
data-form-mode="isolated-test" data-form-mode={config.mode}
data-form-environment="isolated-test" data-form-environment={config.environmentRole}
data-logical-form="general" data-logical-form="general"
data-form-context={config.context} data-form-context={config.context}
data-jotform-source="approved-form-a" data-jotform-source="approved-form-a"
@@ -26,6 +26,7 @@ const formAIframe = resolveFormAIframe({
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE, rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED, liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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, environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING, allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route: campaignRoute, route: campaignRoute,
@@ -6,6 +6,7 @@ const formAIframe = resolveFormAIframe({
rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE, rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED, liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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, environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING, allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route: '/lp/early-intervention', route: '/lp/early-intervention',
@@ -38,7 +39,7 @@ const formAIframe = resolveFormAIframe({
</div> </div>
</section> </section>
<div class="qr-process-band"> <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"> <div class="form-heading">
<h2 id="consultation-heading">Contact AIA about early intervention</h2> <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, rawMode: import.meta.env.PUBLIC_AIA_FORMS_MODE,
liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED, liveApproved: import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED,
isolatedTestApproved: import.meta.env.PUBLIC_AIA_FORMS_ISOLATED_TEST_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, environmentRole: import.meta.env.PUBLIC_AIA_FORMS_ENVIRONMENT,
allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING, allowIndexing: import.meta.env.PUBLIC_ALLOW_INDEXING,
route, route,
+31 -8
View File
@@ -25,9 +25,10 @@ export function formModeForRuntime(config: FormModeConfig): 'off' | 'mock' {
export type FormAIframeReason = export type FormAIframeReason =
| 'enabled' | 'enabled'
| 'mode-not-isolated-test' | 'mode-not-authorized'
| 'isolated-test-not-approved' | 'isolated-test-not-approved'
| 'live-approval-not-disabled' | 'live-approval-not-disabled'
| 'production-approval-not-granted'
| 'invalid-environment-role' | 'invalid-environment-role'
| 'indexing-enabled' | 'indexing-enabled'
| 'route-not-authorized' | 'route-not-authorized'
@@ -38,6 +39,8 @@ export interface FormAIframeConfig {
reason: FormAIframeReason; reason: FormAIframeReason;
route: FormAIframeRoute | string; route: FormAIframeRoute | string;
locale: 'en' | 'es' | string; locale: 'en' | 'es' | string;
mode?: 'isolated-test' | 'live';
environmentRole?: 'isolated-test' | 'production';
context?: FormAIframeContext; context?: FormAIframeContext;
url?: string; url?: string;
title?: string; title?: string;
@@ -49,6 +52,7 @@ export interface ResolveFormAIframeOptions {
rawMode: string | undefined; rawMode: string | undefined;
liveApproved: string | undefined; liveApproved: string | undefined;
isolatedTestApproved: string | undefined; isolatedTestApproved: string | undefined;
productionApproved: string | undefined;
environmentRole: string | undefined; environmentRole: string | undefined;
allowIndexing: string | undefined; allowIndexing: string | undefined;
route: string; route: string;
@@ -57,12 +61,13 @@ export interface ResolveFormAIframeOptions {
/** /**
* The owner-approved Form A identifier is deliberately only made into an * 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 * iframe URL after every explicit isolated-test or production gate has
* production setting and it is not enabled by an API URL or a live mode. * passed. It is never enabled by an API URL, hostname, or publication flag.
*/ */
export const formAIframeOrigin = 'https://form.jotform.com' as const; export const formAIframeOrigin = 'https://form.jotform.com' as const;
export const formAIframeId = '262946078314059' as const; export const formAIframeId = '262946078314059' as const;
const isolatedTestEnvironmentRole = 'isolated-test'; const isolatedTestEnvironmentRole = 'isolated-test';
const productionEnvironmentRole = 'production';
export type FormAIframeContext = 'contact' | 'landing'; export type FormAIframeContext = 'contact' | 'landing';
export type FormAIframeRoute = 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 * Campaign placements are an owner-approved override of their earlier
* logical Form B mapping. The shared compact landing presentation uses the * logical Form B mapping. The shared compact landing presentation uses the
* same fixed `landing` context; no route may supply its own context or URL. * 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 * Enables the real Form A iframe only through one of two explicit gates:
* test build. `live` is intentionally not a valid route to this adapter. * 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 { export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIframeConfig {
const disabled = (reason: Exclude<FormAIframeReason, 'enabled'>): FormAIframeConfig => ({ const disabled = (reason: Exclude<FormAIframeReason, 'enabled'>): FormAIframeConfig => ({
@@ -132,11 +139,25 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
locale: options.locale, locale: options.locale,
}); });
const config = resolveFormMode(options.rawMode, options.liveApproved); 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 (options.liveApproved?.trim().toLowerCase() !== 'false') return disabled('live-approval-not-disabled');
if (!isTrue(options.isolatedTestApproved)) return disabled('isolated-test-not-approved'); 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'); 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); const placement = formAIframePlacements.find((candidate) => candidate.route === options.route);
if (!placement) return disabled('route-not-authorized'); if (!placement) return disabled('route-not-authorized');
@@ -147,6 +168,8 @@ export function resolveFormAIframe(options: ResolveFormAIframeOptions): FormAIfr
reason: 'enabled', reason: 'enabled',
route: options.route, route: options.route,
locale: placement.locale, locale: placement.locale,
mode,
environmentRole: approvedEnvironment,
context: placement.context, context: placement.context,
url: buildFormAIframeUrl(placement.locale, placement.context), url: buildFormAIframeUrl(placement.locale, placement.context),
...formAIframeCopy(placement.locale), ...formAIframeCopy(placement.locale),
+7 -3
View File
@@ -19,9 +19,11 @@ import { setTimeout as delay } from 'node:timers/promises';
const root = fileURLToPath(new URL('../', import.meta.url)); const root = fileURLToPath(new URL('../', import.meta.url));
const moduleName = process.env.PLAYWRIGHT_MODULE ? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href : 'playwright'; 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'; 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 = [ const routes = [
{ route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' }, { route: '/contact', locale: 'en', context: 'contact', screenshot: 'contact' },
{ route: '/es/contact', locale: 'es', context: 'contact', screenshot: 'es-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('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('referrerpolicy'), 'no-referrer', route);
assert.equal(await iframe.getAttribute('sandbox'), 'allow-forms allow-scripts allow-same-origin', 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); assert.equal(await page.locator(`[data-jotform-form-a][data-form-context="${formContext}"]`).count(), 1, route);
const frameBox = await iframe.boundingBox(); const frameBox = await iframe.boundingBox();
const expectedFrameHeight = formContext === 'contact' const expectedFrameHeight = formContext === 'contact'
@@ -152,6 +155,7 @@ try {
const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 }); const response = await page.goto(origin + route, { waitUntil: 'domcontentloaded', timeout: 15000 });
assert.equal(response?.status(), 200, `${route} without JavaScript`); 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] 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'); 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`); 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'); const noscriptFallback = page.locator('[data-jotform-form-a] .jotform-iframe__fallback--noscript');
+35 -4
View File
@@ -15,6 +15,7 @@ const isolated = (overrides = {}) => ({
rawMode: 'isolated-test', rawMode: 'isolated-test',
liveApproved: 'false', liveApproved: 'false',
isolatedTestApproved: 'true', isolatedTestApproved: 'true',
productionApproved: 'false',
environmentRole: 'isolated-test', environmentRole: 'isolated-test',
allowIndexing: 'false', allowIndexing: 'false',
route: '/contact', route: '/contact',
@@ -22,6 +23,18 @@ const isolated = (overrides = {}) => ({
...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', () => { test('Form A iframe uses only fixed, approved route parameters', () => {
assert.equal(formAIframeOrigin, 'https://form.jotform.com'); assert.equal(formAIframeOrigin, 'https://form.jotform.com');
assert.equal(formAIframeId, '262946078314059'); 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()); const enabled = resolveFormAIframe(isolated());
assert.equal(enabled.enabled, true); assert.equal(enabled.enabled, true);
assert.equal(enabled.reason, 'enabled'); assert.equal(enabled.reason, 'enabled');
assert.equal(enabled.title, 'AIA general enquiries form'); assert.equal(enabled.title, 'AIA general enquiries form');
assert.equal(resolveFormAIframe(isolated({ rawMode: undefined })).reason, 'mode-not-isolated-test'); assert.equal(enabled.mode, 'isolated-test');
assert.equal(resolveFormAIframe(isolated({ rawMode: 'mock' })).reason, 'mode-not-isolated-test'); assert.equal(enabled.environmentRole, 'isolated-test');
assert.equal(resolveFormAIframe(isolated({ rawMode: 'live', liveApproved: 'true' })).reason, 'mode-not-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: undefined })).reason, 'live-approval-not-disabled');
assert.equal(resolveFormAIframe(isolated({ liveApproved: 'true' })).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'); 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' })); const campaign = resolveFormAIframe(isolated({ route: '/lp/aba-therapy' }));
assert.equal(campaign.url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en'); assert.equal(campaign.url, 'https://form.jotform.com/262946078314059?formContext=landing&language=en');
assert.equal(campaign.context, 'landing'); 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('isolated-test', undefined)), 'off');
assert.equal(formModeForRuntime(resolveFormMode('live', 'true')), '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, /resolveFormAIframe/);
assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/); assert.match(contact, /route = lang === 'es' \? '\/es\/contact' : '\/contact'/);
assert.match(contact, /JotformIframe/); assert.match(contact, /JotformIframe/);
assert.match(contact, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
assert.match(campaign, /resolveFormAIframe/); assert.match(campaign, /resolveFormAIframe/);
assert.match(campaign, /JotformIframe/); assert.match(campaign, /JotformIframe/);
assert.match(campaign, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
assert.match(campaign, /variant="embedded"/); assert.match(campaign, /variant="embedded"/);
assert.match(campaign, /\/lp\/aba-therapy/); 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\.'/); 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, /resolveFormAIframe/);
assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/); assert.match(earlyIntervention, /route: '\/lp\/early-intervention'/);
assert.match(earlyIntervention, /JotformIframe/); assert.match(earlyIntervention, /JotformIframe/);
assert.match(earlyIntervention, /PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED/);
assert.match(adapter, /referrerpolicy="no-referrer"/); assert.match(adapter, /referrerpolicy="no-referrer"/);
assert.match(adapter, /sandbox="allow-forms allow-scripts allow-same-origin"/); 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-frame/);
assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/); assert.match(adapter, /data-jotform-iframe-fallback hidden aria-live="polite" role="status"/);
assert.match(adapter, /<noscript>/); assert.match(adapter, /<noscript>/);
+64 -9
View File
@@ -36,18 +36,41 @@ function hasTerminalSlash(url) {
return pathname !== '/' && pathname.endsWith('/'); 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('&amp;', '&'));
}
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 = []; const signals = [];
if (/<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe'); if (!allowApprovedFormA && /<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 && /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 (/<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 (/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'); 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; return signals;
} }
export async function verifyArtifact(root, mode = 'staging') { 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 htmlFiles = (await fg('**/*.html', { cwd: root, absolute: true })).sort();
const routes = []; const routes = [];
const canonicalMap = new Map(); const canonicalMap = new Map();
@@ -56,6 +79,7 @@ export async function verifyArtifact(root, mode = 'staging') {
let hreflangLinks = 0; let hreflangLinks = 0;
let activeForms = 0; let activeForms = 0;
let secretSignals = []; let secretSignals = [];
let formAIframes = 0;
for (const file of htmlFiles) { for (const file of htmlFiles) {
const html = await readText(file); const html = await readText(file);
@@ -66,12 +90,22 @@ export async function verifyArtifact(root, mode = 'staging') {
const actions = formActions(html); const actions = formActions(html);
const nonSearchActions = actions.filter((action) => action && action !== '/search'); const nonSearchActions = actions.filter((action) => action && action !== '/search');
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash); 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 (!title) errors.push(`${route}: missing title`);
if (!canonical) errors.push(`${route}: missing canonical`); if (!canonical) errors.push(`${route}: missing canonical`);
if (!robots) errors.push(`${route}: missing robots directive`); 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 (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) { if (canonical) {
const prior = canonicalMap.get(canonical); const prior = canonicalMap.get(canonical);
if (prior) errors.push(`${route}: duplicate canonical ${canonical} (also ${prior})`); 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; hreflangLinks += hrefValues(html, 'alternate').filter((href) => href).length;
if (signals.length) secretSignals.push({ route, signals }); 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; if (nonSearchActions.length) activeForms += nonSearchActions.length;
routes.push({ route, title, robots, canonical, nativeForms: actions.length, nonSearchActions: 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 sitemap = await fs.readFile(sitemapFile, 'utf8').catch(() => '');
const sitemapRoutes = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/gi)].map((match) => match[1]); const sitemapRoutes = [...sitemap.matchAll(/<loc>([^<]+)<\/loc>/gi)].map((match) => match[1]);
const reviewIndex = htmlFiles.some((file) => routeFromHtml(file, root) === '/lp'); 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 (sitemapRoutes.length) errors.push(`staging sitemap contains ${sitemapRoutes.length} URLs`);
if (hreflangLinks) errors.push(`staging emits ${hreflangLinks} hreflang links`); if (hreflangLinks) errors.push(`staging emits ${hreflangLinks} hreflang links`);
if (!reviewIndex) errors.push('staging /lp review index is missing'); 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(createHash('sha256').update(await fs.readFile(file)).digest('hex'));
digest.update('\n'); 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 { return {
mode, mode,
htmlRoutes: htmlFiles.length, htmlRoutes: htmlFiles.length,
@@ -120,6 +174,7 @@ export async function verifyArtifact(root, mode = 'staging') {
hreflangLinks, hreflangLinks,
reviewIndex, reviewIndex,
activeNativeFormActions: activeForms, activeNativeFormActions: activeForms,
formAIframes,
artifactFiles: files.length, artifactFiles: files.length,
artifactSha256: digest.digest('hex'), artifactSha256: digest.digest('hex'),
routes, 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)) { 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 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'; 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); const result = await verifyArtifact(path.resolve(root), mode);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
if (!result.passed) process.exitCode = 1; if (!result.passed) process.exitCode = 1;
+46 -5
View File
@@ -7,15 +7,31 @@ import { verifyArtifact } from './phase10-artifact-verify.mjs';
async function fixture(mode = 'staging') { async function fixture(mode = 'staging') {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-phase10-artifact-')); 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' const pages = mode === 'staging'
? [['index.html', 'noindex,nofollow'], ['lp/index.html', 'noindex,nofollow']] ? [['index.html', 'noindex,nofollow', ''] , ['lp/index.html', 'noindex,nofollow', '']]
: [['index.html', 'index,follow']]; : mode === 'isolated-test'
for (const [file, robots] of pages) { ? [
...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); const target = path.join(root, file);
await fs.mkdir(path.dirname(target), { recursive: true }); 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; return root;
} }
@@ -33,6 +49,31 @@ test('indexing artifact requires sitemap and excludes review index', async () =>
assert.equal(result.reviewIndex, false); 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 () => { test('staging artifact rejects active native form actions', async () => {
const root = await fixture('staging'); const root = await fixture('staging');
await fs.appendFile(path.join(root, 'index.html'), '<form action="https://example.invalid/submit"></form>'); await fs.appendFile(path.join(root, 'index.html'), '<form action="https://example.invalid/submit"></form>');