Agent Skills: Adobe SDK Patterns

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/adobe-sdk-patterns

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/adobe-pack/skills/adobe-sdk-patterns

Skill Files

Browse the full folder contents for adobe-sdk-patterns.

Download Skill

Loading file tree…

plugins/saas-packs/adobe-pack/skills/adobe-sdk-patterns/SKILL.md

Skill Metadata

Name
adobe-sdk-patterns
Description
|

Adobe SDK Patterns

Overview

Production-ready patterns for Adobe SDK usage across Firefly Services (@adobe/firefly-apis, @adobe/photoshop-apis, @adobe/lightroom-apis), PDF Services (@adobe/pdfservices-node-sdk), and direct REST API calls.

Prerequisites

  • Completed adobe-install-auth setup
  • Familiarity with async/await patterns
  • Understanding of the Adobe API you are integrating

Instructions

Pattern 1: Singleton Auth Client with Token Caching

// src/adobe/client.ts
import { ServicePrincipalCredentials, PDFServices } from '@adobe/pdfservices-node-sdk';

let pdfServicesInstance: PDFServices | null = null;
let tokenCache: { token: string; expiresAt: number } | null = null;

export function getPDFServices(): PDFServices {
  if (!pdfServicesInstance) {
    const credentials = new ServicePrincipalCredentials({
      clientId: process.env.ADOBE_CLIENT_ID!,
      clientSecret: process.env.ADOBE_CLIENT_SECRET!,
    });
    pdfServicesInstance = new PDFServices({ credentials });
  }
  return pdfServicesInstance;
}

export async function getAccessToken(): Promise<string> {
  if (tokenCache && tokenCache.expiresAt > Date.now() + 300_000) {
    return tokenCache.token;
  }

  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!,
    }),
  });

  if (!res.ok) throw new Error(`Adobe IMS token error: ${res.status}`);
  const data = await res.json();
  tokenCache = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
  return tokenCache.token;
}

Pattern 2: Typed API Wrapper with Error Classification

// src/adobe/firefly-client.ts
export class AdobeApiError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly code: string,
    public readonly retryable: boolean,
    public readonly retryAfter?: number
  ) {
    super(message);
    this.name = 'AdobeApiError';
  }
}

export async function adobeApiFetch<T>(
  url: string,
  options: RequestInit & { apiKey?: string }
): Promise<T> {
  const token = await getAccessToken();
  const { apiKey, ...fetchOptions } = options;

  const response = await fetch(url, {
    ...fetchOptions,
    headers: {
      'Authorization': `Bearer ${token}`,
      'x-api-key': apiKey || process.env.ADOBE_CLIENT_ID!,
      'Content-Type': 'application/json',
      ...fetchOptions.headers,
    },
  });

  if (!response.ok) {
    const body = await response.text();
    const retryAfter = response.headers.get('Retry-After');
    throw new AdobeApiError(
      `Adobe API ${response.status}: ${body}`,
      response.status,
      response.status === 429 ? 'RATE_LIMITED' :
      response.status === 401 ? 'AUTH_EXPIRED' :
      response.status >= 500 ? 'SERVER_ERROR' : 'CLIENT_ERROR',
      response.status === 429 || response.status >= 500,
      retryAfter ? parseInt(retryAfter) : undefined
    );
  }

  return response.json();
}

Pattern 3: Retry with Exponential Backoff

// src/adobe/retry.ts
export async function withRetry<T>(
  operation: () => Promise<T>,
  config = { maxRetries: 3, baseDelayMs: 1000 }
): Promise<T> {
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (err: any) {
      if (attempt === config.maxRetries) throw err;

      // Only retry on transient errors
      if (err instanceof AdobeApiError && !err.retryable) throw err;

      // Honor Retry-After header from Adobe
      const delay = err.retryAfter
        ? err.retryAfter * 1000
        : config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;

      console.warn(`Adobe retry ${attempt + 1}/${config.maxRetries} in ${delay}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Pattern 4: Job Polling for Async APIs (Photoshop, Lightroom)

// src/adobe/polling.ts — Photoshop/Lightroom APIs are async (submit job, poll status)
interface AdobeJobStatus {
  status: 'pending' | 'running' | 'succeeded' | 'failed';
  _links?: { self: { href: string } };
  output?: any;
  error?: { code: string; message: string };
}

export async function pollAdobeJob(
  statusUrl: string,
  options = { intervalMs: 2000, timeoutMs: 120_000 }
): Promise<AdobeJobStatus> {
  const token = await getAccessToken();
  const deadline = Date.now() + options.timeoutMs;

  while (Date.now() < deadline) {
    const res = await fetch(statusUrl, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'x-api-key': process.env.ADOBE_CLIENT_ID!,
      },
    });

    const status: AdobeJobStatus = await res.json();

    if (status.status === 'succeeded') return status;
    if (status.status === 'failed') {
      throw new Error(`Adobe job failed: ${status.error?.message || 'Unknown error'}`);
    }

    await new Promise(r => setTimeout(r, options.intervalMs));
  }

  throw new Error('Adobe job polling timeout');
}

Pattern 5: Zod Validation for API Responses

import { z } from 'zod';

const FireflyImageOutputSchema = z.object({
  outputs: z.array(z.object({
    image: z.object({
      url: z.string().url(),
    }),
    seed: z.number(),
  })),
});

const PhotoshopJobSchema = z.object({
  status: z.enum(['pending', 'running', 'succeeded', 'failed']),
  _links: z.object({
    self: z.object({ href: z.string().url() }),
  }).optional(),
});

// Usage
const raw = await adobeApiFetch<unknown>(fireflyUrl, { method: 'POST', body });
const validated = FireflyImageOutputSchema.parse(raw);

Output

  • Type-safe client singleton with token caching
  • Error classification (retryable vs permanent)
  • Automatic retry with Adobe Retry-After header support
  • Async job polling for Photoshop/Lightroom operations
  • Zod runtime validation for API responses

Error Handling

| Pattern | Use Case | Benefit | |---------|----------|---------| | Token caching | All API calls | Avoids redundant IMS token requests | | Error classification | Retry decisions | Only retries transient failures | | Job polling | Photoshop/Lightroom | Handles async operation lifecycle | | Zod validation | All responses | Catches API contract changes at runtime |

Resources

Next Steps

Apply patterns in adobe-core-workflow-a for real-world usage.