agentsclimarketplace

Adobe webhooks events

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/adobe-pack/skills/adobe-webhooks-events

'Implement Adobe I/O Events webhook registration, RSA-SHA256 signatureFrom its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill adobe-webhooks-events

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

8.3 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

Adobe Webhooks & Events

Overview

Implement Adobe I/O Events webhook endpoints with proper challenge-response handshake, RSA-SHA256 digital signature verification, and event routing for Creative Cloud Libraries, Experience Platform, and Firefly Services events.

Prerequisites

  • Adobe Developer Console project with Events API enabled
  • HTTPS endpoint accessible from the internet
  • @adobe/aio-lib-events installed (optional, for SDK approach)
  • Understanding of Adobe I/O Events architecture

Instructions

Step 1: Register Webhook via Adobe I/O Events API

// Register a webhook endpoint programmatically
import { getAccessToken } from '../adobe/client';

interface EventRegistration {
  name: string;
  description: string;
  webhookUrl: string;
  eventsOfInterest: Array<{
    provider_id: string;   // Event provider (e.g., Creative Cloud)
    event_code: string;    // Specific event type
  }>;
  deliveryType: 'webhook' | 'webhook_batch';
}

export async function registerWebhook(reg: EventRegistration): Promise<any> {
  const token = await getAccessToken();

  const response = await fetch(
    `https://api.adobe.io/events/${process.env.ADOBE_IMS_ORG_ID}/integrations/${process.env.ADOBE_INTEGRATION_ID}/registrations`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'x-api-key': process.env.ADOBE_CLIENT_ID!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        client_id: process.env.ADOBE_CLIENT_ID,
        name: reg.name,
        description: reg.description,
        webhook_url: reg.webhookUrl,
        events_of_interest: reg.eventsOfInterest,
        delivery_type: reg.deliveryType || 'webhook',
      }),
    }
  );

  if (!response.ok) throw new Error(`Registration failed: ${await response.text()}`);
  return response.json();
}

// Example: Register for Creative Cloud Library events
await registerWebhook({
  name: 'CC Library Updates',
  description: 'Track Creative Cloud Library changes',
  webhookUrl: 'https://api.yourapp.com/webhooks/adobe',
  eventsOfInterest: [
    { provider_id: 'ccstorage', event_code: 'library_create' },
    { provider_id: 'ccstorage', event_code: 'library_update' },
    { provider_id: 'ccstorage', event_code: 'library_delete' },
  ],
  deliveryType: 'webhook',
});

Step 2: Implement Challenge-Response Handshake

When registering a webhook, Adobe sends a GET request with a challenge query parameter. Your endpoint must respond with the challenge value:

import express from 'express';
const app = express();

app.get('/webhooks/adobe', (req, res) => {
  // Adobe challenge verification during registration
  const challenge = req.query.challenge as string;
  if (challenge) {
    console.log('Adobe webhook challenge received');
    return res.status(200).json({ challenge });
  }
  res.status(400).json({ error: 'Missing challenge parameter' });
});

Step 3: Verify RSA-SHA256 Digital Signatures

Adobe I/O Events uses RSA-SHA256 (not HMAC). Public keys are served from static.adobeioevents.com:

import crypto from 'crypto';

const publicKeyCache = new Map<string, string>();

async function fetchPublicKey(keyPath: string): Promise<string> {
  if (publicKeyCache.has(keyPath)) return publicKeyCache.get(keyPath)!;
  const res = await fetch(`https://static.adobeioevents.com${keyPath}`);
  if (!res.ok) throw new Error(`Failed to fetch Adobe public key: ${res.status}`);
  const key = await res.text();
  publicKeyCache.set(keyPath, key);
  return key;
}

async function verifyAdobeSignature(rawBody: Buffer, headers: Record<string, string>): Promise<boolean> {
  for (const idx of ['1', '2']) {
    const sig = headers[`x-adobe-digital-signature-${idx}`];
    const keyPath = headers[`x-adobe-public-key${idx}-path`];
    if (!sig || !keyPath) continue;

    try {
      const publicKey = await fetchPublicKey(keyPath);
      const verifier = crypto.createVerify('RSA-SHA256');
      verifier.update(rawBody);
      if (verifier.verify(publicKey, sig, 'base64')) return true;
    } catch (err) {
      console.warn(`Adobe signature-${idx} verification error:`, err);
    }
  }
  return false;
}

Step 4: Event Handler with Routing

// POST handler for incoming events
app.post('/webhooks/adobe',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // Verify signature
    if (!await verifyAdobeSignature(req.body, req.headers as any)) {
      console.error('Invalid Adobe webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());

    // Route by event type
    try {
      await routeAdobeEvent(event);
      res.status(200).json({ received: true });
    } catch (error: any) {
      console.error('Event processing failed:', error);
      res.status(500).json({ error: error.message });
    }
  }
);

// Event type definitions
type AdobeEventType =
  | 'library_create'
  | 'library_update'
  | 'library_delete'
  | 'asset_created'
  | 'asset_updated';

interface AdobeEvent {
  event_id: string;
  event: {
    type: AdobeEventType;
    activitystreams?: any;
    xdmEntity?: any;
  };
  recipient_client_id: string;
}

const eventHandlers: Partial<Record<AdobeEventType, (event: AdobeEvent) => Promise<void>>> = {
  library_create: async (event) => {
    console.log('New CC Library created:', event.event_id);
    // Sync library metadata to your database
  },
  library_update: async (event) => {
    console.log('CC Library updated:', event.event_id);
    // Refresh cached library data
  },
  library_delete: async (event) => {
    console.log('CC Library deleted:', event.event_id);
    // Remove from local cache/database
  },
};

async function routeAdobeEvent(event: AdobeEvent): Promise<void> {
  const handler = eventHandlers[event.event.type];
  if (handler) {
    await handler(event);
  } else {
    console.log(`Unhandled Adobe event type: ${event.event.type}`);
  }
}

Step 5: Idempotency (Prevent Duplicate Processing)

import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

async function processEventIdempotently(event: AdobeEvent): Promise<boolean> {
  const key = `adobe:event:${event.event_id}`;
  // SET NX with 7-day TTL — returns null if key already exists
  const result = await redis.set(key, '1', 'EX', 86400 * 7, 'NX');

  if (!result) {
    console.log(`Duplicate Adobe event skipped: ${event.event_id}`);
    return false; // Already processed
  }

  await routeAdobeEvent(event);
  return true;
}

Output

  • Webhook registered with Adobe I/O Events
  • Challenge-response handshake handler for registration
  • RSA-SHA256 signature verification with key caching
  • Event routing by type with handler pattern
  • Idempotency via Redis to prevent duplicate processing

Error Handling

IssueCauseSolution
Challenge response 400Missing JSON content-typeReturn { challenge } as JSON
Signature always invalidNot using raw bodyUse express.raw() before parsing
Events not arrivingRegistration failedCheck I/O Events dashboard for status
Duplicate eventsNo idempotencyTrack event_id in Redis/DB
Public key fetch failsNetwork/firewallWhitelist static.adobeioevents.com

Resources

Next Steps

For performance optimization, see adobe-performance-tuning.

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.