From adobe-pack
Adds ESLint rules and GitHub Actions scans for Adobe integrations: detects p8_ credentials, Firefly content violations, PDF quotas, OAuth scopes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/adobe-pack:adobe-policy-guardrailsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Automated policy enforcement for Adobe integrations: credential pattern scanning (Adobe OAuth secrets use `p8_` prefix), Firefly content policy pre-screening, PDF Services quota guardrails, and OAuth scope validation.
Automated policy enforcement for Adobe integrations: credential pattern scanning (Adobe OAuth secrets use p8_ prefix), Firefly content policy pre-screening, PDF Services quota guardrails, and OAuth scope validation.
Adobe OAuth Server-to-Server secrets follow the p8_ prefix pattern:
# .github/workflows/adobe-security.yml
name: Adobe Security Scan
on: [push, pull_request]
jobs:
credential-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for Adobe credential patterns
run: |
EXIT_CODE=0
# Adobe OAuth client secrets (p8_ prefix)
if grep -rE "p8_[A-Za-z0-9_-]{20,}" --include="*.ts" --include="*.js" --include="*.py" --include="*.json" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v node_modules | grep -v '.git'; then
echo "::error::Adobe client_secret pattern (p8_) found in source code"
EXIT_CODE=1
fi
# Adobe IMS access tokens (JWT format)
if grep -rE "eyJ[A-Za-z0-9_-]{100,}\.[A-Za-z0-9_-]{100,}" --include="*.ts" --include="*.js" . 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '\.test\.' | grep -v '__mock'; then
echo "::warning::Potential Adobe access token found in source (may be test fixture)"
fi
# Org IDs (format: HEXSTRING@AdobeOrg)
if grep -rE "[A-F0-9]{24}@AdobeOrg" --include="*.ts" --include="*.js" --include="*.json" . 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.env.example'; then
echo "::warning::Adobe Org ID found in source — consider using env var"
fi
exit $EXIT_CODE
// src/adobe/guardrails/content-policy.ts
// Pre-screen prompts before sending to Firefly API to avoid wasted credits
interface ContentPolicyResult {
allowed: boolean;
violations: string[];
suggestions: string[];
}
const CONTENT_RULES = [
{
name: 'real-people',
pattern: /\b(photo of|portrait of|picture of)\s+(a\s+)?(real|actual|specific)\s+(person|man|woman|child)/i,
message: 'Firefly cannot generate images of specific real people',
suggestion: 'Use generic descriptions like "a professional in a business suit"',
},
{
name: 'trademarks',
pattern: /\b(nike|adidas|apple|google|microsoft|disney|marvel|coca.?cola|pepsi|starbucks|mcdonalds)\b/i,
message: 'Firefly will reject prompts containing brand trademarks',
suggestion: 'Use generic descriptions like "athletic shoes" or "tech company logo style"',
},
{
name: 'explicit-content',
pattern: /\b(nude|naked|explicit|pornograph|gore|violent|bloody|graphic death)\b/i,
message: 'Firefly rejects explicit or violent content',
suggestion: 'Use appropriate imagery descriptions',
},
{
name: 'celebrity',
pattern: /\b(celebrity|famous|actor|actress|politician|president|singer|musician)\s+(name|like|resembling)/i,
message: 'Firefly cannot generate images of identifiable celebrities',
suggestion: 'Describe the style or aesthetic without naming individuals',
},
];
export function screenFireflyPrompt(prompt: string): ContentPolicyResult {
const violations: string[] = [];
const suggestions: string[] = [];
for (const rule of CONTENT_RULES) {
if (rule.pattern.test(prompt)) {
violations.push(`[${rule.name}] ${rule.message}`);
suggestions.push(rule.suggestion);
}
}
return {
allowed: violations.length === 0,
violations,
suggestions,
};
}
// Usage in API layer
export function guardFireflyPrompt(prompt: string): void {
const result = screenFireflyPrompt(prompt);
if (!result.allowed) {
throw new Error(
`Firefly content policy pre-check failed:\n` +
result.violations.join('\n') +
'\n\nSuggestions:\n' +
result.suggestions.join('\n')
);
}
}
// src/adobe/guardrails/pdf-quota.ts
// Enforce PDF Services monthly transaction limits
class PdfQuotaGuard {
private monthlyLimit: number;
private transactionsUsed: number = 0;
private monthStart: Date;
constructor(tier: 'free' | 'paid' = 'free') {
this.monthlyLimit = tier === 'free' ? 500 : Infinity;
this.monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
}
check(): { allowed: boolean; remaining: number; warning: boolean } {
// Reset counter on new month
const now = new Date();
if (now.getMonth() !== this.monthStart.getMonth()) {
this.transactionsUsed = 0;
this.monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
}
const remaining = this.monthlyLimit - this.transactionsUsed;
return {
allowed: remaining > 0,
remaining,
warning: remaining < this.monthlyLimit * 0.2,
};
}
record(): void {
const status = this.check();
if (!status.allowed) {
throw new Error(`PDF Services quota exhausted (${this.monthlyLimit} transactions/month)`);
}
this.transactionsUsed++;
if (status.warning) {
console.warn(`PDF Services: ${status.remaining - 1} transactions remaining this month`);
}
}
}
export const pdfQuota = new PdfQuotaGuard(
process.env.ADOBE_PDF_TIER === 'paid' ? 'paid' : 'free'
);
// Verify that the requested scopes match what the environment should use
function validateAdobeScopes(scopes: string, environment: string): void {
const scopeList = scopes.split(',').map(s => s.trim());
// Development should only have minimal scopes
if (environment === 'development') {
const prodOnlyScopes = ['ff_apis'];
const violations = scopeList.filter(s => prodOnlyScopes.includes(s));
if (violations.length > 0) {
console.warn(`Adobe scope warning: ${violations.join(', ')} should not be in development`);
}
}
// Required scopes that should always be present
const required = ['openid', 'AdobeID'];
const missing = required.filter(s => !scopeList.includes(s));
if (missing.length > 0) {
throw new Error(`Adobe required scopes missing: ${missing.join(', ')}`);
}
}
// Prevent dangerous operations based on environment
const BLOCKED_IN_PROD: Record<string, string> = {
'delete-all-assets': 'Mass deletion blocked in production',
'reset-quota-counter': 'Quota reset blocked in production',
'use-test-credentials': 'Test credentials blocked in production',
};
function guardAdobeOperation(operation: string): void {
const isProd = process.env.NODE_ENV === 'production';
if (isProd && BLOCKED_IN_PROD[operation]) {
throw new Error(`BLOCKED: ${BLOCKED_IN_PROD[operation]}`);
}
}
p8_, JWTs, Org IDs)| Issue | Cause | Solution |
|---|---|---|
| Secret scan false positive | Test fixture contains pattern | Exclude test dirs from scan |
| Prompt wrongly rejected | Pattern too broad | Refine regex; allow legitimate uses |
| Quota counter reset | Server restart | Persist counter in Redis/DB |
| Scope validation fails | Wrong env var | Check NODE_ENV and ADOBE_SCOPES |
For architecture blueprints, see adobe-architecture-variants.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin adobe-packIdentifies Adobe API pitfalls like deprecated JWT auth, uncached IMS tokens, Firefly policy violations, and secret leaks, with TypeScript fixes.
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.