fix(premiere-plugin): v1.0.1 — actually load + connect under CEP 12

End-to-end debugging against a live Premiere Pro 2025 + auth-enabled mam-api
surfaced four real bugs that made v1.0.0 install cleanly but never load,
plus the missing auth flow. All four are fixed and the panel is verified
connected (status dot green, Reconnect button shown, project list populated).

  - manifest.xml: a comment in the <Resources> block contained "--" (inside
    "--enable-nodejs"/"--mixed-context"), which is illegal per the XML spec.
    CEP 12's strict parser logged
        ERROR XPATH Double hyphen within comment
    and skipped the panel entirely. Comment rewritten without double hyphens.

  - manifest.xml: lacked the Version="X.Y" attribute on <ExtensionManifest>
    and used a non-standard AbstractionLayers/empty <ExtensionList/>
    structure. CEP rejected it with
        Unsupported Manifest version ''
    Manifest rewritten to the standard CSXS 7.0 schema (ExtensionList +
    DispatchInfoList + RequiredRuntimeList), matching the working AMPP
    panel template.

  - main.js: re-declared `const csInterface = new CSInterface()` at top
    level even though CSInterface.js already declared the same binding.
    CEP 12 shares script-realm lexical scope across <script> tags, so the
    second const threw
        Identifier 'csInterface' has already been declared
    The throw fired before setupEventListeners(), so the Connect button's
    click handler was never attached. This is the root cause of the
    original "clicking Connect does nothing" symptom; everything else was
    secondary. Removed the duplicate declaration; main.js now uses the
    binding from CSInterface.js.

  - No auth support against AUTH_ENABLED=true servers. mam-api supports
    Bearer tokens (POST /api/v1/tokens), so added:
      • API token input field (password-masked) next to Server URL
      • localStorage persistence on every keystroke
      • window.fetch monkey-patch that injects
          Authorization: Bearer <token>
        on every request whose URL starts with the configured server.
        Signed S3 download URLs are NOT touched.

Drive-by fixes that came out of the same debugging pass:
  - Server URL input listener was 'change' (fires on blur); switched to
    'input' so typing-then-clicking-Connect immediately commits.
  - restoreSettings() now strips trailing slashes from the stored URL so
    older saved values like 'http://host/' stop producing //api/v1 404s.
  - CSS selector `input[type="text"].server-url` didn't match the new
    password input → the token field was unstyled and effectively invisible.
    Generalized to `input.server-url`; restructured the connection bar into
    `.connection-controls--stacked` (flex column) of two `.server-input-row`
    rows so two input fields fit cleanly.
  - Build scripts now parse ExtensionBundleVersion from both element form
    (<ExtensionBundleVersion>X</...>) and attribute form
    (ExtensionBundleVersion="X"), since the manifest rewrite switched
    schemas.

Version bumped 1.0.0 → 1.0.1. New artifacts committed at
services/premiere-plugin/build/releases/v1.0.1/ (.exe 2 MB, .zxp 35 KB).
v1.0.0 left in place so editors who downloaded it can verify they're on
the broken version.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-05-23 19:24:10 -04:00
parent d1fcfcc8fd
commit eadafffb18
9 changed files with 182 additions and 85 deletions

View file

@ -1,71 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ExtensionManifest xmlns="http://ns.adobe.com/CSXS/manifest" <ExtensionManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ExtensionBundleId="net.wilddragon.dragonflight.panel"
xsi:schemaLocation="http://ns.adobe.com/CSXS/manifest http://ns.adobe.com/CSXS/manifest"> ExtensionBundleName="Wild Dragon MAM"
<ExtensionBundleVersion>1.0.0</ExtensionBundleVersion> ExtensionBundleVersion="1.0.1"
<ExtensionBundleId>net.wilddragon.dragonflight.panel</ExtensionBundleId> Version="7.0">
<Author>Wild Dragon MAM</Author> <ExtensionList>
<Extension Id="net.wilddragon.dragonflight.panel" Version="1.0" />
<License> </ExtensionList>
<Text>Copyright 2026 Wild Dragon. All rights reserved.</Text> <ExecutionEnvironment>
</License> <HostList>
<Host Name="PPRO" Version="[22.0,99.9]" />
<AbstractionLayers> </HostList>
<AbstractionLayerRequest Name="PProPanel"/> <LocaleList>
</AbstractionLayers> <Locale Code="All" />
</LocaleList>
<RequiredRuntimeList>
<RequiredRuntime Name="CSXS" Version="10.0" />
</RequiredRuntimeList>
</ExecutionEnvironment>
<DispatchInfoList>
<Extension Id="net.wilddragon.dragonflight.panel"> <Extension Id="net.wilddragon.dragonflight.panel">
<Type>Panel</Type> <DispatchInfo>
<HostList>
<Host Name="PPRO" Version="[22.0,99.9]"/>
</HostList>
<LocalizedStrings>
<LocalizedString locale="en_US">
<Token name="ExtensionWindowTitle">Wild Dragon MAM</Token>
<Token name="ExtensionShortDescription">Media Asset Management for Premiere Pro</Token>
<Token name="ExtensionLongDescription">Browse, search, and import proxy files from Wild Dragon MAM directly into your Premiere Pro project</Token>
</LocalizedString>
</LocalizedStrings>
<UI>
<Type>Panel</Type>
<Geometry>
<Size>
<Width>400</Width>
<Height>700</Height>
</Size>
<MinSize>
<Width>350</Width>
<Height>500</Height>
</MinSize>
</Geometry>
</UI>
<!--
Resources:
- MainPath: HTML entry point for the CEP panel UI
- ScriptPath: ExtendScript (.jsx) auto-loaded into the Premiere Pro host
- --enable-nodejs: unlocks Node.js require() for binary file downloads
- --mixed-context: allows Node.js and DOM to share the same V8 context
-->
<Resources> <Resources>
<MainPath>index.html</MainPath> <MainPath>./index.html</MainPath>
<ScriptPath>jsx/premiere.jsx</ScriptPath> <ScriptPath>./jsx/premiere.jsx</ScriptPath>
<CEFCommandLine> <CEFCommandLine>
<Parameter>--enable-nodejs</Parameter> <Parameter>--enable-nodejs</Parameter>
<Parameter>--mixed-context</Parameter> <Parameter>--mixed-context</Parameter>
</CEFCommandLine> <Parameter>--allow-file-access-from-files</Parameter>
<Parameter>--allow-file-access</Parameter>
</CEFCommandLine>
</Resources> </Resources>
<Lifecycle>
<CEPVersion>11.0</CEPVersion> <AutoVisible>true</AutoVisible>
<RequiredRuntimeVersion>11.0</RequiredRuntimeVersion> </Lifecycle>
<UI>
<Type>Panel</Type>
<Menu>Wild Dragon MAM</Menu>
<Geometry>
<Size>
<Height>700</Height>
<Width>400</Width>
</Size>
<MinSize>
<Height>500</Height>
<Width>385</Width>
</MinSize>
<MaxSize>
<Height>2000</Height>
<Width>800</Width>
</MaxSize>
</Geometry>
</UI>
</DispatchInfo>
</Extension> </Extension>
</DispatchInfoList>
<ExtensionList/>
<ExecutionEnvironment>
<HostList>
<Host Name="PPRO" Version="[22.0,99.9]"/>
</HostList>
</ExecutionEnvironment>
</ExtensionManifest> </ExtensionManifest>

View file

@ -12,15 +12,19 @@ $manifestPath = Join-Path $PSScriptRoot '..\CSXS\manifest.xml'
if (-not (Test-Path $manifestPath)) { if (-not (Test-Path $manifestPath)) {
throw "Manifest not found at $manifestPath" throw "Manifest not found at $manifestPath"
} }
# Regex over XML because the manifest's <Resources> comment contains '--' # Regex over XML because [xml] is strict about things Adobe CEP tolerates.
# (the --enable-nodejs/--mixed-context CEF flags), which is illegal per the # Accept either the element form (<ExtensionBundleVersion>X.Y.Z</...>) or the
# XML spec — .NET's strict parser rejects the doc even though Adobe CEP # attribute form (ExtensionBundleVersion="X.Y.Z") used by CSXS 7.0+ manifests.
# tolerates it.
$manifestRaw = Get-Content -Raw -Path $manifestPath $manifestRaw = Get-Content -Raw -Path $manifestPath
if ($manifestRaw -notmatch '<ExtensionBundleVersion>([^<]+)</ExtensionBundleVersion>') { $version = $null
throw 'Could not read <ExtensionBundleVersion> from manifest.xml' if ($manifestRaw -match '<ExtensionBundleVersion>([^<]+)</ExtensionBundleVersion>') {
$version = $Matches[1].Trim()
} elseif ($manifestRaw -match 'ExtensionBundleVersion\s*=\s*"([^"]+)"') {
$version = $Matches[1].Trim()
}
if (-not $version) {
throw 'Could not read ExtensionBundleVersion from manifest.xml'
} }
$version = $Matches[1].Trim()
Write-Host "Dragonflight Premiere panel - Windows installer build v$version" Write-Host "Dragonflight Premiere panel - Windows installer build v$version"
$iscc = Get-Command 'ISCC.exe' -ErrorAction SilentlyContinue $iscc = Get-Command 'ISCC.exe' -ErrorAction SilentlyContinue

View file

@ -29,8 +29,12 @@ const EXCLUDE = new Set(['build', 'install-windows.ps1', '.git', '.gitignore', '
function readVersion() { function readVersion() {
const xml = readFileSync(MANIFEST, 'utf8'); const xml = readFileSync(MANIFEST, 'utf8');
const m = xml.match(/<ExtensionBundleVersion>([^<]+)<\/ExtensionBundleVersion>/); // Accept either <ExtensionBundleVersion>X.Y.Z</ExtensionBundleVersion> (the
if (!m) throw new Error(`Could not find <ExtensionBundleVersion> in ${MANIFEST}`); // namespace-style manifest) or ExtensionBundleVersion="X.Y.Z" (the attribute
// form used by the CSXS 7.0+ schema).
const m = xml.match(/<ExtensionBundleVersion>([^<]+)<\/ExtensionBundleVersion>/)
|| xml.match(/\bExtensionBundleVersion\s*=\s*"([^"]+)"/);
if (!m) throw new Error(`Could not find ExtensionBundleVersion in ${MANIFEST}`);
return m[1].trim(); return m[1].trim();
} }

View file

@ -0,0 +1,43 @@
# Dragonflight Premiere Panel — v1.0.1
Built 2026-05-23 from `feat/premiere-installer`. First release that
actually loads under Premiere Pro 2025 (CEP 12) and connects to an
auth-enabled mam-api.
## What changed since v1.0.0
| Problem | Fix |
|---------|-----|
| `manifest.xml` had `--` inside an XML comment → CEP 12 refused to parse it | Comment rewritten without double hyphens |
| `manifest.xml` lacked a `Version="X.Y"` attribute on the root and used a non-standard `AbstractionLayers`/`ExtensionList` structure → CEP rejected with `Unsupported Manifest version ''` | Rewritten to the standard CSXS 7.0 schema (`ExtensionList` + `DispatchInfoList`), matching the working AMPP panel template |
| `main.js` re-declared `const csInterface` at top-level — CSInterface.js already declared the same binding. CEP 12 shares script-realm lexical scope across `<script>` tags, so main.js threw `Identifier 'csInterface' has already been declared` and never ran. The click handler for Connect was never attached → "nothing happens." | Removed the duplicate declaration; main.js now uses the binding set up by CSInterface.js |
| No way to authenticate against mam-api when `AUTH_ENABLED=true` (panel only sent `credentials: 'include'`, but it has no session cookie) | Added an "API Token" password field next to Server URL; persists to localStorage; `window.fetch` is monkey-patched to inject `Authorization: Bearer <token>` on every request to the configured server |
| Server URL field had a `change`-event listener that only fired on blur — quick "type URL → click Connect" cycles never saved the URL | Switched to `input` event so every keystroke commits. Also normalizes any trailing slash on restore so old saved `http://host/` values stop producing `//api/v1` 404s |
| New token input was `type="password"`, but the CSS selector was `input[type="text"].server-url` → unstyled, invisible against the dark theme. The 3-column grid layout also crammed the new row into a button-width slot | Connection bar restructured into a `.connection-controls--stacked` flex column of two `.server-input-row` flex rows; CSS input selector generalized to `input.server-url` |
| Build scripts' version-extraction regex only matched the old `<ExtensionBundleVersion>X</...>` element form | Now matches both element and attribute forms |
## Artifacts
| File | Size | Install |
|------|------|---------|
| [dragonflight-premiere-panel-1.0.1-windows-setup.exe](dragonflight-premiere-panel-1.0.1-windows-setup.exe) | 2.0 MB | Windows — double-click |
| [dragonflight-premiere-panel-1.0.1.zxp](dragonflight-premiere-panel-1.0.1.zxp) | 35 KB | macOS + Windows — [Anastasiy's ZXP Installer](https://install.anastasiy.com/) |
## Configuring after install
1. Open Premiere → **Window → Extensions → Wild Dragon MAM**
2. **Server URL**: e.g. `http://10.0.0.25:47434`
3. **API token**: generate one via
```
curl -c cookies.txt -X POST $API/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"USER","password":"PASS"}'
curl -b cookies.txt -X POST $API/api/v1/tokens \
-H 'Content-Type: application/json' \
-d '{"name":"premiere-panel"}'
```
Paste the returned `token` value (starts with `wd_`) into the panel.
4. Click **Connect** — status dot turns green and projects populate the dropdown.
If your `mam-api` runs with `AUTH_ENABLED` unset/false, the token field can be left empty.

View file

@ -79,7 +79,19 @@ html, body {
gap: 8px; gap: 8px;
} }
input[type="text"].server-url { .connection-controls--stacked {
display: flex;
flex-direction: column;
gap: 8px;
}
.server-input-row {
display: flex;
align-items: center;
gap: 8px;
}
input.server-url {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
@ -91,13 +103,13 @@ input[type="text"].server-url {
transition: border-color 0.2s; transition: border-color 0.2s;
} }
input[type="text"].server-url:focus { input.server-url:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.1); box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.1);
} }
input[type="text"].server-url::placeholder { input.server-url::placeholder {
color: var(--text-secondary); color: var(--text-secondary);
} }

View file

@ -10,18 +10,28 @@
<div id="panel-container"> <div id="panel-container">
<!-- Connection Bar --> <!-- Connection Bar -->
<div class="connection-bar"> <div class="connection-bar">
<div class="connection-controls"> <div class="connection-controls connection-controls--stacked">
<div class="server-input-group"> <div class="server-input-row">
<input <input
type="text" type="text"
id="server-url" id="server-url"
class="server-url" class="server-url"
placeholder="http://localhost:7434" placeholder="http://10.0.0.25:47434"
title="MAM server URL" title="MAM server URL"
> >
<div class="status-indicator" id="status-indicator"></div> <div class="status-indicator" id="status-indicator"></div>
</div> </div>
<button id="connect-btn" class="connect-btn">Connect</button> <div class="server-input-row">
<input
type="password"
id="api-token"
class="server-url"
placeholder="API token (wd_…)"
title="API token — create with POST /api/v1/tokens"
autocomplete="off"
>
<button id="connect-btn" class="connect-btn">Connect</button>
</div>
</div> </div>
</div> </div>

View file

@ -3,8 +3,10 @@
* Main JavaScript file for the CEP panel * Main JavaScript file for the CEP panel
*/ */
// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls // Adobe CEP interface — CSInterface.js already declared `const csInterface`
const csInterface = new CSInterface(); // at the script-realm scope (top-level `const` is shared across non-module
// <script> tags), so re-declaring it here throws SyntaxError under CEP 12.
// We just use the binding that CSInterface.js provides.
// ============================================================================ // ============================================================================
// State Management // State Management
@ -12,6 +14,7 @@ const csInterface = new CSInterface();
const state = { const state = {
serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434', serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434',
apiToken: localStorage.getItem('mam_api_token') || '',
isConnected: false, isConnected: false,
isConnecting: false, isConnecting: false,
selectedAsset: null, selectedAsset: null,
@ -41,6 +44,7 @@ let elements = {};
function initDOMElements() { function initDOMElements() {
elements = { elements = {
serverUrlInput: document.getElementById('server-url'), serverUrlInput: document.getElementById('server-url'),
apiTokenInput: document.getElementById('api-token'),
connectBtn: document.getElementById('connect-btn'), connectBtn: document.getElementById('connect-btn'),
statusIndicator: document.getElementById('status-indicator'), statusIndicator: document.getElementById('status-indicator'),
searchInput: document.getElementById('search-input'), searchInput: document.getElementById('search-input'),
@ -126,11 +130,17 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
function setupEventListeners() { function setupEventListeners() {
elements.serverUrlInput.addEventListener('change', (e) => { // Commit serverUrl on every keystroke so clicking Connect right after
// typing works even before the input loses focus.
elements.serverUrlInput.addEventListener('input', (e) => {
state.serverUrl = e.target.value.trim().replace(/\/$/, ''); state.serverUrl = e.target.value.trim().replace(/\/$/, '');
localStorage.setItem('mam_server_url', state.serverUrl); localStorage.setItem('mam_server_url', state.serverUrl);
state.thumbCache = {}; state.thumbCache = {};
}); });
elements.apiTokenInput.addEventListener('input', (e) => {
state.apiToken = e.target.value.trim();
localStorage.setItem('mam_api_token', state.apiToken);
});
elements.connectBtn.addEventListener('click', connectToServer); elements.connectBtn.addEventListener('click', connectToServer);
elements.searchInput.addEventListener('input', debounce(handleSearch, 300)); elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
elements.projectFilter.addEventListener('change', handleProjectFilter); elements.projectFilter.addEventListener('change', handleProjectFilter);
@ -147,9 +157,36 @@ function setupEventListeners() {
} }
function restoreSettings() { function restoreSettings() {
// Normalize any trailing slash that the old `change`-event listener may
// have missed on previous sessions — otherwise fetches produce //api/v1
// and nginx 404s.
state.serverUrl = state.serverUrl.replace(/\/+$/, '');
localStorage.setItem('mam_server_url', state.serverUrl);
elements.serverUrlInput.value = state.serverUrl; elements.serverUrlInput.value = state.serverUrl;
elements.apiTokenInput.value = state.apiToken;
} }
// ============================================================================
// Auth: monkey-patch fetch to inject Bearer token on every same-origin call
// to the configured MAM server. Mam-api requires this when AUTH_ENABLED=true.
// ============================================================================
const _originalFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
init = init || {};
const url = typeof input === 'string' ? input : (input && input.url) || '';
// Only attach the token for requests aimed at the configured MAM server.
// Signed S3 download URLs etc. must NOT get our Authorization header.
if (state.apiToken && state.serverUrl && url.startsWith(state.serverUrl)) {
const headers = new Headers(init.headers || {});
if (!headers.has('Authorization')) {
headers.set('Authorization', 'Bearer ' + state.apiToken);
}
init.headers = headers;
}
return _originalFetch(input, init);
};
// ============================================================================ // ============================================================================
// Server Connection // Server Connection
// ============================================================================ // ============================================================================