From adobe-pack
Provides production-ready patterns for Adobe Firefly Services SDK, PDF Services SDK, and REST APIs in TypeScript and Python. Use for integrations, refactoring SDK usage, or team standards.
How this skill is triggered — by the user, by Claude, or both
Slash command
/adobe-pack:adobe-sdk-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
adobe-install-auth setup// 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;
}
// 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();
}
// 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');
}
// 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');
}
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);
Retry-After header support| 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 |
Apply patterns in adobe-core-workflow-a for real-world usage.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin adobe-packSets up local dev loop for Adobe APIs (Firefly/PDF/Photoshop) with App Builder CLI, Node.js SDKs, hot reload, and Vitest mocks.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.