Claude-code-plugins-plus canva-data-handling

install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/canva-pack/skills/canva-data-handling" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-canva-data-handling && rm -rf "$T"
manifest: plugins/saas-packs/canva-pack/skills/canva-data-handling/SKILL.md
source content

Canva Data Handling

Overview

Handle Canva Connect API data responsibly. The API exposes user identifiers, design metadata, design content (via exports), uploaded assets, and comments. Apply proper classification, retention, and privacy controls.

Data Classification — Canva API Responses

Data TypeSource EndpointSensitivityHandling
User ID, Team ID
GET /v1/users/me
InternalDon't expose externally
User profile
GET /v1/users/me/profile
PIIEncrypt at rest, minimize
Design metadata
GET /v1/designs
BusinessStandard protection
Design contentExport URLs from
/v1/exports
ConfidentialTime-limited URLs, don't cache
OAuth tokens
/v1/oauth/token
SecretEncrypt, never log
Asset files
/v1/asset-uploads
BusinessValidate, scan for malware
Comments
/v1/designs/{id}/comment_threads
PIIMay contain personal data
Webhook payloadsIncoming POSTMixedVerify signature first

Token Protection

// NEVER log tokens — they grant full access to a user's Canva account
function redactCanvaData(data: any): any {
  const sensitiveKeys = [
    'access_token', 'refresh_token', 'authorization',
    'client_secret', 'code_verifier',
  ];

  if (typeof data !== 'object' || data === null) return data;

  const redacted = Array.isArray(data) ? [...data] : { ...data };
  for (const key of Object.keys(redacted)) {
    if (sensitiveKeys.includes(key.toLowerCase())) {
      redacted[key] = '[REDACTED]';
    } else if (typeof redacted[key] === 'object') {
      redacted[key] = redactCanvaData(redacted[key]);
    }
  }
  return redacted;
}

// Safe logging
console.log('Canva response:', JSON.stringify(redactCanvaData(apiResponse)));

Temporary URL Handling

Canva API responses include URLs with limited lifetimes. Never cache beyond expiry.

interface CanvaUrlPolicy {
  type: string;
  ttl: number;        // milliseconds
  cacheable: boolean;
}

const URL_POLICIES: Record<string, CanvaUrlPolicy> = {
  thumbnail:  { type: 'thumbnail',  ttl: 15 * 60 * 1000,      cacheable: false }, // 15 min
  edit_url:   { type: 'edit_url',   ttl: 30 * 24 * 60 * 60 * 1000, cacheable: true }, // 30 days
  view_url:   { type: 'view_url',   ttl: 30 * 24 * 60 * 60 * 1000, cacheable: true }, // 30 days
  export_url: { type: 'export_url', ttl: 24 * 60 * 60 * 1000, cacheable: false }, // 24 hours
};

// Track URL expiry
class CanvaUrlTracker {
  private urls = new Map<string, { url: string; expiresAt: number }>();

  store(id: string, type: string, url: string): void {
    const policy = URL_POLICIES[type];
    this.urls.set(`${id}:${type}`, {
      url,
      expiresAt: Date.now() + (policy?.ttl || 0),
    });
  }

  get(id: string, type: string): string | null {
    const entry = this.urls.get(`${id}:${type}`);
    if (!entry || Date.now() > entry.expiresAt) return null;
    return entry.url;
  }
}

Data Retention

Data TypeRetentionReason
OAuth tokensUntil user disconnectsActive session
Design metadata (cached)5-60 minutesPerformance cache
Export download URLsMax 24 hoursCanva-enforced expiry
API request logs30 daysDebugging
Error logs90 daysRoot cause analysis
Audit logs7 yearsCompliance
Webhook events30 daysProcessing/replay

Automatic Cleanup

async function cleanupCanvaData(): Promise<void> {
  const now = Date.now();

  // Remove expired export URLs
  await db.exportUrls.deleteMany({ expiresAt: { $lt: new Date(now) } });

  // Remove old API logs
  const thirtyDaysAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
  await db.canvaApiLogs.deleteMany({
    createdAt: { $lt: thirtyDaysAgo },
    type: { $nin: ['audit'] },
  });

  // Remove tokens for deleted/inactive users
  await db.canvaTokens.deleteMany({ userId: { $in: await getDeletedUserIds() } });
}

GDPR/CCPA Compliance

Data Subject Access Request

async function exportCanvaUserData(userId: string): Promise<object> {
  const tokens = await tokenStore.get(userId);

  return {
    source: 'Canva Connect API',
    exportedAt: new Date().toISOString(),
    data: {
      identity: tokens ? await canvaAPI('/users/me', tokens.accessToken) : null,
      hasActiveConnection: !!tokens,
      // Note: Canva stores the user's designs — their data is in Canva's system
      // Your app only stores: tokens, cached metadata, and integration state
    },
  };
}

Right to Deletion

async function deleteCanvaUserData(userId: string): Promise<void> {
  // 1. Revoke tokens (disconnects from Canva)
  const tokens = await tokenStore.get(userId);
  if (tokens) {
    await revokeCanvaToken(tokens.accessToken, clientId, clientSecret);
  }

  // 2. Delete stored tokens
  await tokenStore.delete(userId);

  // 3. Clear cached design metadata
  await cache.deletePattern(`canva:user:${userId}:*`);

  // 4. Audit log (required — do not delete)
  await auditLog.record({
    action: 'GDPR_DELETION',
    userId,
    service: 'canva',
    timestamp: new Date(),
  });
}

Error Handling

IssueCauseSolution
Token in logsMissing redactionWrap all logging with redactCanvaData
Expired URL servedNo expiry trackingUse CanvaUrlTracker
DSAR incompleteMissing data inventoryDocument all Canva data stored
Orphaned tokensUser deleted without cleanupRun periodic cleanup job

Resources

Next Steps

For enterprise access control, see

canva-enterprise-rbac
.