Two-factor authentication for pos-cli and the Instance API, error responses keep the page's content type, dynamic cache without a layout
September 16, 2026
NEW
-
Two-factor authentication for
pos-cliand the Instance API: When the Partner Portal account that owns an API token belongs to a Partner with two-factor authentication enabled, the token now needs a two-factor session before it can be used against an instance. The requirement is set per Partner on the Partner Portal; nothing changes for Partners who have not enabled it.- Affected commands. Every
pos-clicommand that talks to an instance:deploy,sync,exec,exec-graphql,exec-liquid,constants,data export/import,migrations,logs,pull, and the GUI. Read-only commands are included, since a GraphQL query andpos-cli data exportboth return instance data. - Prompting.
deployandsyncask for a code before uploading anything; other commands ask when the instance refuses the request, then retry it. The code is exchanged with the Partner Portal directly and never sent to the instance. - Sessions. A session lasts 8 hours and is scoped to a single instance, so a session for staging does not work against production. It is cached per environment in
.posundertwo_factor_session, and writing it sets the file's permissions to0600. - Automation. Pass
--otp-code(or setPOS_PORTAL_OTP_CODE) to skip the prompt. In CI, setPOS_PORTAL_SESSION_TOKENto a session minted elsewhere. - Exemptions. API keys issued by the instance itself are unaffected, as they belong to no Partner Portal user.
- Note: update
pos-clibefore enabling the requirement for your Partner - older versions cannot complete the exchange.
Custom API clients receive a
401naming the reason, so it can be distinguished from an expired or revoked token: - Affected commands. Every
{"error":"two_factor_required","errors":["A two-factor session is required to use this instance"]}
A 401 without an error key still means the token is invalid, expired, or revoked.
IMPROVED
-
Error responses keep the content type the page already set: A page sets its content type before rendering, either from its own format or via
{% response_headers %}. Anything rendered afterwards used to negotiate the format again against theAcceptheader, which failed when the two disagreed. The format is now taken from what the response already committed, and unrecognized formats render astext/plain. Errors on.jspages are returned as a JSON string literal instead of bare text, so the browser no longer parses an error message as script. -
Clearer response when an instance has no object storage configured: Requesting a presigned upload URL returned no response at all, and
pos-cli syncreportedfetch failed. The endpoint now returns501with adirect_upload_unavailableerror, andpos-clifalls back to uploading assets through the instance. -
Host-relative asset URLs with local storage: When an instance serves its own assets from disk,
asset_urlnow returns a host-relative URL. Previously anASSET_HOSTinherited from another environment pointed those URLs at a CDN that never received the files, and an unsetASSET_HOSTraised an error on every page callingasset_url. Instances using a CDN are unaffected.
FIXED
-
Dynamically cached pages without a layout: A page combining
dynamic_cachewith an emptylayoutrendered correctly on the first request, then failed on every cache hit because reapplying the layout to the cached body required a layout to exist. Pages that declare no layout are now served from cache as-is. This affected cachedsitemap.xmlpages and any other page declaringlayout: "". -
Redundant Liquid context on dynamic cache hits: The layout built a second Liquid context and reparsed the layout value on every request. It now reuses the context built for the page body.
-
Partner Portal URLs on non-default ports: An instance registered against a Partner Portal running on a non-default port dropped the port when storing the URL, sending later token validations to
:80/:443. These failed with a connection error that surfaced as a plain401, indistinguishable from an invalid token. Local setups and private stacks were affected; production was not. -
Swapped exception names in low-level error logs: IP spoofing attempts were logged as missing parameter errors and vice versa. The
403and406statuses returned were correct and are unchanged.