The embedding EHR spans an apex plus an app/tenant subdomain; frame-ancestors
must list each origin separately. Split EMBED_ALLOWED_ORIGIN on whitespace so
"https://enwella.comhttps://app.enwella.com" both become sources. The deploy
already sets two space-separated origins; the single-value form emitted one
malformed token and the browser blocked the iframe.
Adds a JWT-authenticated entry point so the embedding app can mount
<docuseal-builder data-token="…"> without the host pre-establishing a
DocuSeal session via cross-origin cookies + an external auth gate.
- EmbedBuilderController (GET /embed/builder?token=…): verifies a short-lived
HS256 JWT against the owner account's API access token (the same key the
JSON API uses; raw value is recoverable via the encrypted `token` column),
signs that user in (a first-party session inside the iframe), records a
template-scoped grant in the session, and redirects into the regular
builder — /templates/:id/edit for an existing template, or /new?url=… to
download + create from `document_urls`. Requires `exp` and caps token
lifetime (replay bound). Only opens template_ids the account owns.
- EmbedScoped concern: confines an embed session to its own template (by id,
or by external_id for the /new→create→edit redirect) plus the create +
builder-support paths; refuses enumeration / other templates / the JSON
template API. Fails closed. Path rules mirror the allow-list the host app
enforced at the edge, so the builder keeps working while the cross-origin
cookie + gate machinery on the host side can be removed.
- builder.js shim now iframes /embed/builder?token=… (the server decides
edit vs. new) instead of a bare authenticated /new that 404s without a
pre-set session cookie.
- Request specs for token verification, ownership, exp/lifetime, and scope.
Lets the EnWella EHR mount <docuseal-builder> (and -form) without a
paid Pro license. Upstream's `EmbedScriptsController` returns an
"Upgrade to Pro" stub; we override it via the static-asset path so the
real shim wins without touching the controller.
JS shim (public/js/builder.js)
- Web component that decodes the JWT minted by the embedder (`name`,
`document_urls`, `external_id`, optional `template_id`).
- Iframes /templates/:id/edit for existing templates, /new?... for the
upload-from-URL flow.
- Relays a `save` postMessage from the inner builder up as a DOM
`CustomEvent('save')` on the outer element so @docuseal/react's
onSave callback fires.
- Discovers host from its own <script src> so embedders don't have to
set data-host. JWT signature not re-verified (session cookies +
shared HMAC at the calling app are authoritative).
Builder (app/javascript/template_builder/builder.vue)
- isEmbedded computed (window.parent !== window).
- Hide #title_container in embed (sheet already shows the consent name).
- Fire the `save` postMessage at the end of the manual SAVE handler so
the outer shim can dispatch the DOM event.
Rails glue
- application_controller.rb / config/application.rb: when
EMBED_ALLOWED_ORIGIN is set, add `frame-ancestors 'self' <origin>`
to the per-request CSP and drop the default `X-Frame-Options:
SAMEORIGIN` header (different scheme/host/port = different origin,
so SAMEORIGIN blocks the EHR's iframe even in same-domain prod).
- templates_uploads_controller.rb: persist `external_id` from the
upload params so the host app can later look the template up via
`GET /api/templates?external_id=...` (deterministic fallback link
path when the `template.created` webhook doesn't reach us).
- templates_uploads_controller.rb: skip the SSRF guard when fetching
documents in development — it rejects http / non-443 / localhost,
which is exactly the Active Storage URLs the EHR sends from
https://localhost:3000.
- templates_uploads/show.html.erb: carry `external_id` through the
resubmit form (used on the encrypted-PDF prompt path).
Local HTTPS dev (puma.rb, Procfile.dev, .env.example)
- Bind ssl://0.0.0.0:PORT when LOCAL_HTTPS_CERT/KEY are set (wired by
../bin/docuseal-dev to ../.certs/lvh.me.pem). EHR runs HTTPS too,
so HTTPS iframe avoids mixed-content blocking.
- Procfile.dev runs `puma -C config/puma.rb` directly so the ssl bind
applies.
- SSL_CERT_FILE example for trusting the mkcert root so DocuSeal's
outbound HTTPS to Rails (Active Storage downloads) verifies.