agentsclimarketplace

Adobe known pitfalls

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/adobe-pack/skills/adobe-known-pitfalls

'Identify and avoid Adobe-specific anti-patterns: using deprecated JWT auth,From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill adobe-known-pitfalls

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

10.3 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it

Adobe Known Pitfalls

Overview

The 10 most common mistakes when integrating with Adobe APIs, based on real production issues. Each pitfall includes the anti-pattern, why it fails, and the correct approach.

Prerequisites

  • Access to your Adobe integration codebase
  • Understanding of Adobe API architecture (OAuth, async jobs, rate limits)

Instructions

Pitfall 1: Still Using JWT (Service Account) Credentials

Status: CRITICAL — JWT credentials reached End of Life June 2025.

// WRONG: JWT auth (no longer works as of 2025)
import jwt from 'jsonwebtoken';
import fs from 'fs';

const privateKey = fs.readFileSync('private.key');
const jwtToken = jwt.sign({
  exp: Math.round(Date.now() / 1000) + 86400,
  iss: orgId,
  sub: technicalAccountId,
  aud: `https://ims-na1.adobelogin.com/c/${clientId}`,
}, privateKey, { algorithm: 'RS256' });

// RIGHT: OAuth Server-to-Server (current standard)
const res = await fetch('https://ims-na1.adobelogin.com/ims/token/v3', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    client_id: process.env.ADOBE_CLIENT_ID!,
    client_secret: process.env.ADOBE_CLIENT_SECRET!,
    grant_type: 'client_credentials',
    scope: process.env.ADOBE_SCOPES!,
  }),
});

Pitfall 2: Not Caching IMS Access Tokens

IMS tokens are valid for 24 hours. Generating a new token per request wastes 200-500ms:

// WRONG: New token every request (200-500ms overhead each time)
async function callFirefly(prompt: string) {
  const tokenRes = await fetch('https://ims-na1.adobelogin.com/ims/token/v3', { ... });
  const { access_token } = await tokenRes.json();
  // ... use access_token
}

// RIGHT: Cache token with expiry check
let cached: { token: string; expiresAt: number } | null = null;

async function getToken(): Promise<string> {
  if (cached && cached.expiresAt > Date.now() + 300_000) return cached.token;
  const res = await fetch('https://ims-na1.adobelogin.com/ims/token/v3', { ... });
  const data = await res.json();
  cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
  return cached.token;
}

Pitfall 3: Using Firefly Sync Endpoint for Batch Operations

// WRONG: Sequential sync calls (each blocks 5-20s)
for (const prompt of prompts) {
  const result = await fetch('https://firefly-api.adobe.io/v3/images/generate', {
    method: 'POST', ...
  });
  results.push(await result.json());
}
// Total time: N * 5-20s = very slow

// RIGHT: Async endpoint with parallel submission
const jobs = await Promise.all(
  prompts.map(prompt =>
    fetch('https://firefly-api.adobe.io/v3/images/generate-async', {
      method: 'POST', ...
    }).then(r => r.json())
  )
);
// Poll all jobs in parallel
const results = await Promise.all(jobs.map(j => pollJob(j.statusUrl)));
// Total time: max(5-20s) = much faster

Pitfall 4: Ignoring Firefly Content Policy Errors

// WRONG: Treat all 400 errors the same
try {
  const result = await generateImage({ prompt: 'Photo of Nike shoes' });
} catch (e) {
  console.log('Generation failed');  // No idea why
}

// RIGHT: Handle content policy specifically
try {
  const result = await generateImage({ prompt });
} catch (e: any) {
  if (e.status === 400 && e.message?.includes('content policy')) {
    // Save the credit — don't retry, fix the prompt
    throw new Error(
      'Firefly content policy violation. ' +
      'Remove trademarks, real people, or explicit content from prompt.'
    );
  }
  throw e; // Other errors might be retryable
}

Pitfall 5: Uploading Files Directly to Photoshop/Lightroom API

// WRONG: Trying to upload file directly (not supported)
const formData = new FormData();
formData.append('image', fs.readFileSync('photo.jpg'));
await fetch('https://image.adobe.io/v2/remove-background', {
  method: 'POST',
  body: formData,  // Photoshop API doesn't accept direct uploads
});

// RIGHT: Use pre-signed cloud storage URLs
const inputUrl = await s3.getSignedUrl('getObject', {
  Bucket: 'my-bucket', Key: 'photo.jpg', Expires: 3600,
});
const outputUrl = await s3.getSignedUrl('putObject', {
  Bucket: 'my-bucket', Key: 'output.png', Expires: 3600,
});

await fetch('https://image.adobe.io/v2/remove-background', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'x-api-key': clientId, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    input: { href: inputUrl, storage: 'external' },
    output: { href: outputUrl, storage: 'external', type: 'image/png' },
  }),
});

Pitfall 6: Not Polling Async Job Status

Photoshop and Lightroom APIs return immediately with a job ID. You must poll for results:

// WRONG: Treating response as the final result
const res = await fetch('https://image.adobe.io/v2/remove-background', { ... });
const result = await res.json();
console.log('Done!', result);  // result is just { _links: { self: { href: ... } } }

// RIGHT: Poll the status URL until completion
const submission = await res.json();
let job;
do {
  await new Promise(r => setTimeout(r, 2000));
  const pollRes = await fetch(submission._links.self.href, {
    headers: { Authorization: `Bearer ${token}`, 'x-api-key': clientId },
  });
  job = await pollRes.json();
} while (job.status !== 'succeeded' && job.status !== 'failed');

if (job.status === 'failed') throw new Error(job.error?.message);

Pitfall 7: Leaking Adobe Credentials in Source Code

// WRONG: Hardcoded credentials (Adobe OAuth secrets start with p8_)
const client_secret = 'p8_XYZ_your_actual_secret_here_do_not_do_this';

// WRONG: Committed .env file
// git add .env && git commit -m "add config"

// RIGHT: Environment variables + .gitignore
const client_secret = process.env.ADOBE_CLIENT_SECRET!;
// .gitignore includes: .env, .env.local, .env.*.local

Pitfall 8: Not Handling PDF Services Quota

// WRONG: No quota awareness (free tier = 500 tx/month)
async function extractAllPdfs(paths: string[]) {
  for (const path of paths) {
    await extractPdf(path);  // Silently fails after 500th call
  }
}

// RIGHT: Track and enforce quota
let txCount = 0;
async function trackedExtract(path: string) {
  if (txCount >= 490) {  // Leave buffer
    throw new Error('Approaching PDF Services monthly limit. 10 transactions remaining.');
  }
  const result = await extractPdf(path);
  txCount++;
  return result;
}

Pitfall 9: Using Deprecated Photoshop Endpoints

// WRONG: v1 endpoint (deprecated)
await fetch('https://image.adobe.io/sensei/cutout', { ... });

// RIGHT: v2 endpoint (current)
await fetch('https://image.adobe.io/v2/remove-background', { ... });

Pitfall 10: Missing Webhook Signature Verification

// WRONG: Trust any incoming request (attackers can forge events)
app.post('/webhooks/adobe', (req, res) => {
  processEvent(req.body);
  res.sendStatus(200);
});

// RIGHT: Verify RSA-SHA256 signature from Adobe I/O Events
app.post('/webhooks/adobe', express.raw({ type: 'application/json' }), async (req, res) => {
  // Adobe uses RSA-SHA256 digital signatures (NOT HMAC)
  const sig = req.headers['x-adobe-digital-signature-1'];
  const keyPath = req.headers['x-adobe-public-key1-path'];

  const publicKey = await fetch(`https://static.adobeioevents.com${keyPath}`).then(r => r.text());
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(req.body);

  if (!verifier.verify(publicKey, sig, 'base64')) {
    return res.sendStatus(401);
  }

  processEvent(JSON.parse(req.body.toString()));
  res.sendStatus(200);
});

Quick Pitfall Scanner

# Run against your codebase
echo "=== Adobe Pitfall Scan ==="

# 1. JWT credentials (deprecated)
grep -rn "jsonwebtoken\|jwt\.sign\|RS256" --include="*.ts" --include="*.js" src/ && echo "FOUND: JWT auth (deprecated)" || echo "OK: No JWT"

# 2. Uncached token generation
grep -rn "ims/token/v3" --include="*.ts" src/ | wc -l | xargs -I{} echo "Token endpoint calls: {} (should be 1 — in auth.ts only)"

# 3. Hardcoded secrets
grep -rn "p8_" --include="*.ts" --include="*.js" src/ && echo "FOUND: Hardcoded Adobe secret" || echo "OK: No hardcoded secrets"

# 4. Deprecated endpoints
grep -rn "sensei/cutout" --include="*.ts" src/ && echo "FOUND: Deprecated Photoshop endpoint" || echo "OK: No deprecated endpoints"

# 5. Missing webhook verification
grep -rn "webhooks/adobe" --include="*.ts" src/ | grep -v "digital-signature\|verify\|RSA" && echo "WARNING: Webhook handler may lack signature verification"

Quick Reference Card

PitfallRiskDetectionFix
JWT authBroken authGrep for jwt.signMigrate to OAuth S2S
No token cachePerf (-500ms/req)Multiple ims/token callsCache with expiry
Sync Firefly for batchSlow (N*20s)Sequential generate callsUse async endpoint
Ignore content policyWasted creditsCatch 400 without reasonPre-screen prompts
Direct file upload400 errorsFormData to PhotoshopPre-signed URLs
No job pollingMissing resultsNo poll loop after submitPoll _links.self
Leaked p8_ secretCredential compromiseGrep for p8_Env vars + .gitignore
No quota trackingSilent failuresNo counterTrack per-month usage
Old PS endpoint404 errors/sensei/cutout/v2/remove-background
No webhook verifySecurity holeNo signature checkRSA-SHA256 verification

Resources

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.