Skip to content

feat: per-operation refactor + Asset, Transform, and Admin operations#7

Merged
eitanp461 merged 37 commits into
masterfrom
feat/image-video-transformations
Jun 7, 2026
Merged

feat: per-operation refactor + Asset, Transform, and Admin operations#7
eitanp461 merged 37 commits into
masterfrom
feat/image-video-transformations

Conversation

@eitanp461

Copy link
Copy Markdown
Contributor

Summary

Expands the Cloudinary community node from a single declarative class into a maintainable, multi-resource node with full test coverage. Adds Asset, Transform, and Admin operations on top of the existing Upload flow, and restructures the codebase around per-operation handlers so new operations are additive.

This is a large branch; it groups into four themes below.

1. Architecture refactor

  • Split the monolithic Cloudinary.node.ts into a thin execute() loop plus a per-operation handler map keyed ${resource}:${operation} (operations/index.ts). Adding an operation no longer touches execute().
  • Moved all UI field definitions into descriptions/, driving the node UI through displayOptions.show on resource + operation.
  • Extracted shared helpers into cloudinary.utils.ts: signing, hand-rolled multipart (no form-data dep), URL builders, metadata serialization, error extraction. Zero runtime deps beyond the n8n-workflow peer dep (pure-JS SHA-256 in sha256.utils.ts).

2. Asset & Admin operations

  • Asset resource (asset_id-based): Get Asset, Update Display Name, plus Append-mode tag updates.
  • Delete Assets (bulk by public_id).
  • Admin API: asset Search and Get Tags / Metadata Fields, over HTTP Basic auth.
  • Field/output naming mirrors the Cloudinary API verbatim (type, public_id, resource_type, tags, context) so Search results pipe straight into later operations.

3. Transform resource (delivery-URL construction)

  • 8 image/video operations that build delivery URLs with no API call: Resize, Crop, Optimize (image/video), Convert, Trim Video, Video Thumbnail, Custom Transformation, plus a Multi-Step builder.
  • Per-transform logic lives in pure component builders (transform/shared.ts) shared between the standalone ops and the Multi-Step fixedCollection, so the two surfaces can't drift.
  • buildDeliveryUrl resolves shared / private-CDN / custom-CNAME hosts from optional privateCdn / secureDistribution credentials.
  • Video Player op: emits an embed URL (player.cloudinary.com/embed/) and a self-hosted player_config JSON. Signed delivery (s--SIG--) is intentionally not implemented yet (disclaimed in the UI).

4. Toolchain, CI & docs

  • Test suite on Vitest with mocked IExecuteFunctions asserting the request contract (URL / method / auth / body) — no network calls. Delivery-URL ops assert returned JSON instead.
  • Regenerated package-lock.json against the public npm registry; aligned node descriptor, peer dep range, .nvmrc, and CI.
  • Corrected the codex node identifier to the community-package namespace; surfaced video alongside images in node search.
  • Extensive contributor notes added to CLAUDE.md (three interaction flows, the two-surface field-definition split, backwards-compatibility rules).

Testing

  • npm test — Vitest suite (utils + per-operation handler contracts).
  • npm run lint / npm run build clean.
  • No secrets or internal infrastructure in the tree or git history; local example workflows live under the gitignored examples/.

eitanp461 and others added 14 commits May 27, 2026 16:56
Restructure the Cloudinary node from a single monolithic execute() into a
declarative handler map: one file per operation grouped by resource, with
field definitions split into dedicated description modules. execute() is now
a thin dispatch loop over input items.

Add an Admin "Search Assets" operation that emits one item per matching
asset, auto-injects a resource_type clause from the selected types, supports
automatic pagination ("Return All"), and surfaces rate-limit and invalid
expression errors with actionable messages.

Harden structured-metadata serialization: multi-value fields render as a
bracketed list of quoted strings and delimiter characters are escaped in
every value, matching Cloudinary's documented metadata format. Sanitize the
multipart upload filename so it can't break request framing.

Introduce a Vitest test suite covering the metadata/signature/URL helpers
and the per-operation request contracts, and share common request headers
and auth across handlers.
- vitest.config.ts: update n8n-workflow alias from dist/index.js to
  dist/cjs/index.js. n8n-workflow@2.16 reorganised its build output and
  no longer ships a top-level dist/index.js, so the previous alias
  resolved to nothing and every test file failed at import time with
  "Cannot find package 'n8n-workflow'".
- .nvmrc: bump from v22.11.0 to v24.16.0 (current Active LTS, Krypton).
  Vitest 4 transitively pulls in std-env@4 (ESM-only) and require()s it
  from its CJS config loader; unflagged require(ESM) needs Node >=20.19
  or >=22.12, so v22.11.0 hit ERR_REQUIRE_ESM on `vitest run`. Moving to
  24.16.0 also matches engines.node already declared in package.json.

All 86 tests pass after these changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Cloudinary.node.ts: switch inputs/outputs to the 'main' string
  literals and drop the NodeConnectionType import. Change group from
  the bogus ['Cloudinary'] to ['transform'] so the node lands in the
  right category in the n8n node picker.
- package.json: pin the n8n-workflow peer range to ^2.0.0 instead of
  '*'. The previous wildcard let any host install try to load this
  node against incompatible majors; we already build and test against
  the 2.x API.
- package-lock.json: regenerated to match.
- .github/workflows/release.yml: drive setup-node from .nvmrc instead
  of hardcoding '22', and enable npm cache so releases install faster
  and stay in lockstep with the local toolchain.
- CLAUDE.md: rewrite the backwards-compatibility section to spell out
  the frozen-by-string vs frozen-by-meaning vs free-to-change axes,
  call out option values and displayOptions narrowing as breaking,
  document the typeVersion escape hatch, and separate workflow-JSON
  compatibility from runtime-host compatibility. Minor tightening
  elsewhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 1 of the asset-crud reorganization. UI-only relabel — `value` strings
(`updateAsset`, `admin`) are frozen-by-string per CLAUDE.md so saved
workflows still resolve. `Search Assets` belongs with the entity it
operates on; moved its operation entry, handler-map key, and field
`displayOptions.show.resource` from `admin` to `updateAsset`. Search is
not yet released, so no compatibility shim is needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the first new operation in the Asset CRUD effort. Identifies the
asset by its immutable asset_id and hits `GET /resources/:asset_id` —
a single-field form instead of the legacy three-field
(resource_type, type, public_id) shape.

Scope notes:
- Deliberately asset_id-only. No `identifyBy` mode selector — for
  new ops, a one-field form is strictly cleaner UX than gating it
  behind a picker most users wouldn't need.
- Existing `updateTags` / `updateMetadata` are not modified; their
  public_id surface stays exactly as it shipped. asset_id support for
  those ops is the job of the upcoming `asset` resource (see plan).

Adds `buildResourceByAssetIdUrl` utility, the `getOptions` collection
(colors, faces, image_metadata, pages, phash, coordinates,
accessibility_analysis, derived_next_cursor), and per-handler +
URL-builder tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an Asset → Delete Assets op backed by the documented public_id
endpoint:

    DELETE /v1_1/{cloud}/resources/{resource_type}/{type}?public_ids=...

Cloudinary's delete endpoint is public_id-based, not asset_id-based —
an earlier draft assumed otherwise and failed at runtime with
"Illegal ids given". The field accepts:

  - a single public_id (no commas needed),
  - a comma-separated list, or
  - an array from an n8n expression (e.g. `{{ $items().map(...) }}`).

Arrays are pre-joined to a CSV before being placed in `qs` because
n8n's default query serializer turns JS arrays into `public_ids[0]=...`
(bracketed indices), which Cloudinary rejects with "public_ids must be
a list of strings or a comma separated string". Joining to CSV
side-steps the serializer entirely.

New helpers in cloudinary.utils.ts:
  - buildResourceDeleteUrl(cloud, resourceType, type)
  - splitCsvIds(csv) — trims and drops blanks

Adds 8 handler tests covering URL/method, single-string, CSV trimming,
array-from-expression, non-image resource routing, options merging,
and empty-input guards, plus unit tests for the new utils.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Splits the existing Update Asset surface into two resources:
- Asset: asset_id-keyed Get/Update/Search/Delete plus tag and metadata
  operations. asset_id is a single global key, so callers do not need
  to juggle (resource_type, type) coordinates.
- Asset (Legacy): kept at the bottom of the dropdown; only the
  public_id-keyed operations that shipped on master are exposed. Saved
  workflows continue to load unchanged.

Update Asset Tags gains a Mode selector with backward-compatible default:
- Set (default): existing behavior, replaces the tag list via the
  resource endpoint with Basic auth.
- Append: new path hitting POST /:resource_type/tags with command=add
  and signed auth, preserving existing tags. Shared appendTags helper
  lives in operations/tagAppend.ts and is wired into both resources'
  updateTags handlers.

The tag-action endpoint scopes lookups by (resource_type, type) and
silently returns public_ids: [] for assets stored under a non-default
type. Append mode now exposes a Type field (labeled to match the asset
object's "type" property) and threads it into the signed body, so
authenticated/private assets resolve correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The codex "node" field declared n8n-nodes-base.cloudinary, the reserved
namespace for n8n's built-in core nodes. This package's real node type is
n8n-nodes-cloudinary.cloudinary (package name + node "name").

n8n's loader binds a .node.json codex to its node by filename adjacency
(Cloudinary.node.json <-> Cloudinary.node.js) and never reads this field, so
the change is cosmetic — but the previous value was misleading, implying this
community node lived in n8n-nodes-base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The node already uploads and manages videos, but nothing advertised it: a
"video" search in the n8n node panel didn't list Cloudinary, and the docs
links and description only mentioned images.

- Add a codex "alias" array (video, image, media, transform, transcode,
  optimize, upload, CDN, asset, DAM). The node creator's search keys only on
  displayName and codex.alias, so this is what makes Cloudinary match "video"
  (the same mechanism the Google Gemini and MiniMax nodes use).
- Mention images and videos in the node description (tooltip + the AI-agent
  tool schema exposed via usableAsTool).
- Add the video best practices guide to primaryDocumentation.

All UI-only metadata, so no typeVersion bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces transform:optimizeImage, resizeImage, cropImage, convertImage,
optimizeVideo, trimVideo, videoThumbnail, customTransformation, and multiStep.
All operations build a Cloudinary delivery URL (no API call); callers chain
n8n's HTTP Request node to materialize bytes.

Delivery host resolution mirrors the SDK: default res.cloudinary.com (shared),
<cloud>-res.cloudinary.com (private CDN), or custom CNAME — driven by new
optional privateCdn / secureDistribution credential fields (backwards-compat;
default off reproduces today's behaviour.

Component builders in operations/transform/shared.ts are shared between
standalone ops and the multi-step chain; both consumers map to the same
builder to prevent drift. Handlers carry 25 new Vitest cases (156 total)
asserting returned JSON rather than a request spy.

Also includes: trailingMediaFormat fix for public_ids that embed their own
extension, codex alias additions (resize/crop/thumbnail), and CLAUDE.md
documentation for the third flow + no-HTTP test pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)
…l chaining

Transform resource increment on top of the 8 delivery-URL operations:

- Video Player: new operation that builds a player.cloudinary.com embed URL
  plus a player_config object (autoplay, loop, muted, source types, poster,
  colors, skin, fluid/aspect ratio, font, and advanced toggles). Emits no
  HTTP request. Embed params use camelCase, matching the embed-page docs
  (snake_case is also accepted by the embedder — verified in-browser).
- Multi-Step: crop step now supports crop-by-aspect-ratio
  (cropAspectWidth + aspectRatio) alongside crop-by-dimensions
  (cropWidth/cropHeight), mapping each to the shared crop builder.
- Video Thumbnail: optional base transformation (thumbnailBaseTransformation)
  prepended before the frame selector, so a thumbnail can chain from a
  previous Trim/Multi-Step transform.
- USER_AGENT now reports n8n-nodes-cloudinary/<version> sourced from
  package.json instead of a hardcoded string.
- Bump version 0.0.9 -> 0.1.0; gitignore examples/ (kept local-only) and
  drop the previously committed social-video POC; CLAUDE.md documents the
  two-surface field-definition split for transform fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eitanp461 eitanp461 requested a review from sveta-slepner June 2, 2026 08:18
eitanp461 and others added 7 commits June 2, 2026 11:35
Restructure the always-loaded agent context to cut per-turn token cost.
CLAUDE.md becomes a one-line `@AGENTS.md` import stub (committed plain
text — no symlink, so it survives Windows/CI), and AGENTS.md holds a lean
core: commands, six one-line core rules, and a one-paragraph architecture
map. The deep reference (three flows, transform builders, backwards-compat
taxonomy, conventions) moves verbatim into docs/, linked with plain
markdown so it loads only when a task touches that area.

AGENTS.md is the cross-agent standard, read directly by Codex/Cursor/etc.;
the CLAUDE.md stub keeps Claude Code working with no duplication.

Always-loaded footprint: ~109 lines -> ~43.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI lint failed on node-param-description-miscased-json at the new
thumbnailBaseTransformation field. The rule flags the word "json" in
descriptions and wants "JSON" — but here it is the n8n expression variable
$json, which is correctly lowercase. The autofix (and an earlier manual
edit) rewrote it to $JSON, which is undefined in n8n and would hand users
a broken chaining example.

Keep $json and suppress the false positive on that single line with a
documented eslint-disable, so the guidance stays runtime-correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address PR review findings on the image/video Transform feature:

- fetch/social delivery URLs no longer get a spurious file extension.
  The format suffix (explicit Convert/Thumbnail format or one recovered
  from a dotted public_id) is now appended only for stored-asset types
  (upload/private/authenticated), whose public_id is opaque. For fetch
  and social sources the identifier is a remote URL / external id, so the
  conversion rides in the transformation (e.g. f_webp) instead. Keeps one
  invariant: result.format is set iff the URL carries a trailing .<format>.

- fetch remote URLs are now smart-escaped before going into the path, so
  query strings and fragments (?sig=...#v1) are percent-encoded and reach
  Cloudinary as the source URL instead of being split off by the browser.
  Path-safe chars stay readable, mirroring the Cloudinary SDKs.

- Multi-Step descriptor: step-specific fields are gated by displayOptions
  on stepType/cropMode, and the Quality field gets its QUALITY_OPTIONS so
  an Optimize step can pick a quality level.

- Comments: drop internal/working-context detail (delivery-code claim and
  "see plan") in favor of public, observed behavior.

- Release: publish with --provenance and set n8n-workflow peerDependency
  to "*" per the n8n community-node standard.

Adds regression tests for stored-vs-fetch extension handling and for
fetch URL escaping (query string, fragment, spaces).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Based on an agentic-POV review of what an AI agent actually sees when
building a flow with this node:

- Node description now advertises transform/optimize/deliver and video
  player capabilities, not just upload/manage — it is the agent's first
  filter when the node is offered as a tool (usableAsTool).
- Each Transform operation description states its output (secure_url plus a
  reusable transformation string; embed_url plus player_config for the Video
  Player), so an agent can chain steps without running the node first.
- The delivery type field leads with the recommended "Upload" and makes the
  signed-URL limitation prominent (those delivery URLs fail to load).
  Description only — no behavior change.
- Resource selector options get one-line purpose descriptions, the legacy
  resource is relabeled "Asset (Legacy, by Public ID)" and its operations
  marked Deprecated, reducing wrong-resource picks (e.g. Library was easy to
  mistake for asset listing).
- Resize/Crop field help notes they do not auto-optimize, and to chain
  Optimize Image or use Multi-Step for f_auto/q_auto.

All changes are UI / tool-schema text — no saved-workflow contract impact.
The Multi-Step Quality dropdown flagged during review was already fixed in
e3d0730, so no change was needed there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the hand-rolled buildDeliveryUrl() with @cloudinary/url-gen so the
SDK owns URL path construction and appends the ?_a= analytics slug automatically
(sdkCode I = n8n, product B = Integrations, registered in Appendix C of the
Cloudinary SDK analytics spec).

Key details:
- forceVersion: false suppresses the SDK default v1 on nested public IDs
- fixFetchUrl() post-processes fetch URLs to encode & and # that the SDK leaves
  bare, preventing CDN/browser misparse of the fetch source URL
- Analytics is correctly suppressed when the fetch source URL contains a
  literal ? (per the Cloudinary analytics spec)
- ? in non-fetch public IDs now encodes as %3F — correct since a bare ? in a
  delivery path starts the query string at the CDN level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A Cloudinary CNAME (secure_distribution) is only available on private-CDN
accounts, so the two fields always appear together in a real account. Hiding
secureDistribution behind privateCdn: true in the credential UI prevents the
impossible state and lets the SDK config be a direct translation of credentials
with no inference needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
n8n does not mandate zero runtime dependencies for community nodes — the rule
was a project-level choice that is now superseded. Future maintainers will see
the dependencies section in package.json directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread nodes/Cloudinary/operations/widget/videoPlayer.ts Outdated
Comment thread nodes/Cloudinary/Cloudinary.node.json Outdated
Comment thread nodes/Cloudinary/Cloudinary.node.json
Comment thread nodes/Cloudinary/Cloudinary.node.ts
Comment thread README.md
eitanp461 and others added 7 commits June 3, 2026 23:10
- Move Video Player from the Transform resource to a new singular `Widget`
  resource: handler relocated to operations/widget/, controls split into
  widget.fields.ts, shared Public ID/Type fields broadened to both resources,
  tests and architecture docs updated, handler re-keyed `widget:videoPlayer`.
- Revert the codex `node` identifier to `n8n-nodes-base.cloudinary` to match
  the n8n codex-file spec and published community-node practice.

Also bundles accompanying working-tree changes (utils refactor, removal of the
updateDisplayName operation, multiStep→combineTransformations handler key, and
package metadata cleanup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prefix Transform operation names/actions with Compose:/Image:/Video: so
the alphabetical sort clusters them by category, and curate the resource
order (Upload and Transform lead, legacy sinks to the bottom). Update
field descriptions that reference the renamed operations. Option values
are unchanged, so saved workflows are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hermetic verification that descriptor changes stay additive against the
deployed npm baseline (origin/master, published 0.0.9). Extracts the n8n
public contract (node/credential names, param name/type/default/
displayOptions, option values) from the compiled node and diffs a baseline
worktree against the current checkout, exiting non-zero on any break so it
can gate releases.

The generated verdict + snapshots are written to .local/ (gitignored) as
regenerable, point-in-time output rather than committed prose — re-run
docs/compat-check/run.sh to refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire docs/compat-check/run.sh into the PR Check workflow via a new
npm run compat-check script. The check builds the deployed baseline in a
worktree, so the checkout now uses fetch-depth: 0 to make that commit
history available. Also adds the unit-test suite to the gate, since the
compat argument relies on metadata-serialization tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verbose, unambiguous name for the dir, npm script, and generated report.
No behavioral change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Generate a video player embed" -> "Generate a video player" in the
Add-action panel. Display label only; the operation value (videoPlayer)
is unchanged, so saved workflows are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cloudinary omits parameters with empty values from the string to sign it
computes server-side. The node was signing every param, so a blank field
(e.g. an empty metadata from "{}") was included in our signature but not the
server's, producing "Invalid Signature". Filter empty (empty-string/null/undefined)
values before signing, and also exclude resource_type/cloud_name, which the
signing spec never includes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
eitanp461 and others added 9 commits June 4, 2026 15:54
Add the pad family (pad/lpad/mpad) to the Resize fit options on both the
standalone Image: Resize op and the Combine Transformations resize step.
Padding is a resize-family operation (keeps the whole image), so it belongs
here rather than as an aspect-ratio path that would duplicate Crop's c_fill.

Each pad mode gains a Pad Background control: Black (default), Auto
(b_auto), Blurred (b_blurred), or a solid color. Hex colors are encoded as
b_rgb:RRGGBB; named colors pass through. The shared resizeComponents builder
and padBackgroundSuffix helper keep the standalone op and Multi-Step in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface crop and resize for videos, not just images. The transformation
syntax (c_<fit>/c_fill, dimensions, aspect ratio, focus, pad backgrounds)
is identical across media types, so the shared component builders already
emit valid video output — only the surfacing was image-only.

- resizeImage.ts / cropImage.ts now expose a resourceType-parameterized
  factory and export both the image and video handlers from it, keeping
  the transformation logic single-sourced in the builders.
- Resize/Crop fields are gated on both operations rather than duplicated.
  Generative Fill stays image-only (gated on cropImage), so the video op
  never surfaces it and the handler's gen-fill branch never fires.
- Matches the existing per-media-type operation idiom (Image/Video
  Optimize) and n8n's own Edit Image node (named ops, not a toggle).

Also clarify in docs/backwards-compat.md that the frozen contract is what
ships on master, not whatever exists on a feature branch — the entire
Transform resource is unreleased, so these identifiers were free to design.

Backward-compatibility-check: 0 breaking changes (all additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an optional Continue From Transformation field to every single-purpose
Transform op. Wiring an upstream op's transformation output ({{ $json.transformation }})
into it prepends that string before the op's own transformation, compounding the
two into one delivery URL — the cross-node counterpart to Combine Transformations.

The prepend happens once in buildTransformResult; readTransformInput exposes the
trimmed value as TransformInput.continueFrom, so ops get chaining for free with no
per-handler logic. Excluded from Custom Transformation and Combine Transformations,
which control their own strings. Replaces videoThumbnail's bespoke (unreleased)
thumbnailBaseTransformation field with this shared one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The player Transformation field was dropped from the embed URL on the false
premise that it only affected the poster. The embed page parses the query into a
nested object and feeds the video config's transformation to
new cloudinary.Transformation(obj), so a raw string must ride under
source[transformation][raw_transformation] to reach the stream (verified against
the player source and in-browser with e_blur:1000); a flat value is ignored.

Also fail fast on the streaming-profile conflict: an HLS/DASH source type can't be
combined with a transformation that pins a format (an f_ component, e.g. an Optimize
step's f_auto:video) — Cloudinary rejects 'streaming profile for non-streaming
formats'. The handler now throws a clear NodeOperationError instead of letting it
surface as a cryptic in-player error. Field descriptions updated accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document, for both humans (README) and agents (docs/transforms.md, AGENTS.md), the
three ways transforms compose (single op / in-node Combine Transformations /
cross-node Continue From Transformation) and the contract that ties them: an op's
transformation output maps to the next op's continueFromTransformation input.

Also document the Video Player as a transformation consumer — the embed-URL
raw_transformation mapping and the HLS/DASH vs format-selection incompatibility —
so the next reader doesn't re-derive what this session worked out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lict

Setting an Aspect Ratio makes the player re-crop the video to those
proportions, merging its own crop into the source transformation. With the
default smart crop that merge places an auto-gravity component wrongly and the
player rejects it ("g_auto must be in a transformation component by itself").

- Expose the player's cropMode as a Crop Mode field (smart/fill/pad), gated on
  Aspect Ratio being set; emit source[cropMode] / player_config.cropMode only
  when an aspect ratio is set and the mode is non-default.
- Fail fast when Aspect Ratio is set + Crop Mode is smart (default) + a
  Transformation is present, pointing at the two fixes (Fill/Pad, or clear
  Aspect Ratio) — mirrors the existing HLS/format guard.
- Document the conflict in README and docs/transforms.md; field help avoids
  qualifier jargon for non-technical users.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The baseline npm ci / build ran with --silent, so when that step fails on the
CI runner (it passes locally on macOS + Node 22/24) the actual error is hidden
and the check aborts under set -e with a bare exit 1. Drop --silent on the
baseline install/build and log the runner's node/npm versions, so the failing
command's output is visible. The candidate build stays silent (it isn't the
one failing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0.0.9 baseline (fbdfa17) committed a package-lock.json whose `resolved`
tarball URLs point at Cloudinary's private AWS CodeArtifact registry. `npm ci`
trusts those hosts verbatim, so the baseline install failed with E401 on the
public PR runner (no CodeArtifact token) — aborting the whole check under set
-e, unrelated to any candidate change. It passed locally only because dev
machines carry cached CodeArtifact creds.

Rewrite any private CodeArtifact host in the baseline lockfile back to the
public npm registry before `npm ci`, and pin the install to public npm.
Integrity hashes are host-independent so the install still verifies; the
baseline is built only to extract its UI contract, so the tarball source is
immaterial as long as versions match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eitanp461 eitanp461 merged commit 9b2f924 into master Jun 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant