Add two internal endpoints so an embedding app can run one DocuSeal
account per tenant, authenticated entirely by tokens (no shared Devise
admin session):
- POST /api/internal/provision_account — HMAC-signed (DOCUSEAL_PROVISION_SECRET)
handoff that idempotently creates an Account + owner User + access token
and returns them. Fails closed when the secret is unset.
- POST /api/internal/templates — X-Auth-Token-authed create-from-PDF that
builds a template under the caller's account via the same service the web
uploader uses, idempotent by external_id (scoped per account).
EmbedBuilderController/EmbedScoped already scope to user.account.templates,
so giving each tenant its own account makes that a real isolation boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EmbedBuilderController is the JWT re-entry point but runs behind
EmbedScoped's enforce_embed_scope! before_action. Once a prior open
pins an embed_scope into the session cookie, re-opening the builder
(the same template again, or a different one) hit `head :forbidden`
because /embed/builder was not in UNSCOPED_ALLOW — the concern locked
out its own re-entry endpoint. First open worked; every reopen 403'd.
The token, not the session, authenticates /embed/builder, so always
let it through; EmbedBuilderController then overwrites the scope from
the fresh token. Add a request spec covering the re-entry/re-scope case.
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.
- Eager account user so Account#default_template_folder can pick an author.
- Pass author: in template fixtures; create the foreign account's user.
- Positive scope control tolerates the missing webpack manifest (reaching the
view proves the guard allowed the in-scope template).
- Time.current over Time.now (Rails/TimeZone).
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.