CommunityRedacción y edicióngithub.com

khadinakbarlabs/constructor-de-apps-shopify

Plugin de desarrollo de aplicaciones Shopify de código abierto con 32 habilidades para Claude Code, Codex, Cursor, OpenCode, Gemini CLI y agentes de codificación de IA.

¿Qué es constructor-de-apps-shopify?

Este plugin de código abierto proporciona un conjunto completo de 32 habilidades para el desarrollo de aplicaciones Shopify, diseñado para ser utilizado con agentes de codificación de IA como Claude Code, Codex, Cursor, OpenCode y Gemini CLI. Cubre a fondo los flujos de autenticación de aplicaciones Shopify, incluyendo el moderno Token Exchange (recomendado para 2024+), Managed Installation para aplicaciones headless, y el OAuth 2.0 Authorization Code Grant heredado. La habilidad guía a los desarrolladores a través de la configuración del proyecto, la implementación de la autenticación en Remix y Express.js, el manejo de tokens de acceso en línea y fuera de línea, y la integración con almacenes de sesiones como Prisma, Redis y DynamoDB. También incluye secciones sobre proxy de aplicaciones, webhooks y las mejores prácticas de seguridad, proporcionando fragmentos de código listos para usar y ejemplos de configuración. Esto permite a un agente de IA generar o depurar rápidamente aplicaciones Shopify con los patrones de autenticación correctos, ahorrando tiempo a los desarrolladores y reduciendo errores de configuración en la implementación.

Compatible con✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI✓OpenCode
npx skills add khadinakbarlabs/shopify-app-builder

Installed? Explore more Redacción y edición skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Autenticación de Aplicaciones Shopify

Shopify proporciona múltiples flujos de autenticación dependiendo del tipo de aplicación y el caso de uso. El estándar moderno es Token Exchange (2024+) para aplicaciones renderizadas en servidor, Managed Installation para aplicaciones headless, y OAuth 2.0 Authorization Code Grant para implementaciones heredadas/personalizadas. Todos los flujos resultan en un token de acceso para la API de administración de GraphQL de Shopify.

Resumen de Flujos de Autenticación

1. Token Exchange (Recomendado para 2024+)

Caso de Uso: Aplicaciones renderizadas en servidor (Remix, Next.js con SSR), aplicaciones de Shopify CLI Flujo: El comerciante instala la aplicación → Shopify genera un token de intercambio temporal → La aplicación lo intercambia por un token de acceso Seguridad: Sin exposición del secreto del cliente; usa rotación de estilo PKCE por solicitud Duración del Token: Los tokens de acceso son de corta duración; los tokens de actualización rotan automáticamente

Diagrama de Token Exchange:

1. El comerciante hace clic en "Instalar" en el panel de administración de Shopify
2. Shopify redirige: https://tu-app.com/auth/callback?code=EXCHANGE_TOKEN
3. La aplicación valida HMAC, intercambia el código por un token de acceso (privado, solo del lado del servidor)
4. La API de administración de Shopify otorga ámbitos; el token se almacena en la sesión/base de datos
5. El token se actualiza automáticamente en la siguiente solicitud si ha expirado

Implementación en Remix (Token Exchange):

// shopify.app.ts (Configuración de la aplicación)
import { shopifyApp } from '@shopify/shopify-app-remix/server';
import { restResources } from '@shopify/shopify-api/rest/admin/2026-07';
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY || '',
  apiSecret: process.env.SHOPIFY_API_SECRET || '',
  scopes: process.env.SCOPES?.split(',') || [
    'write_products',
    'read_products',
    'write_orders',
    'read_orders',
    'write_customers',
    'read_customers',
    'write_discounts',
    'read_discounts',
    'write_fulfillments',
    'read_fulfillments',
    'write_inventory',
    'read_inventory',
  ],
  appUrl: process.env.SHOPIFY_APP_URL || 'http://localhost:3000',
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
  webhooks: {
    path: '/webhooks',
  },
  isEmbeddedApp: true, // Panel de administración de Polaris
  sessionStorage: new PrismaSessionStorage(prisma),
  restResources, // Incluye ayudantes de API REST
});

export default shopify;

Variables de Entorno (.env):

SHOPIFY_API_KEY=tu-clave-api-publica-del-panel-de-socios
SHOPIFY_API_SECRET=tu-secreto-de-api-privado
SHOPIFY_APP_URL=https://tu-dominio.ngrok.io  # o URL de producción
SCOPES=write_products,read_products,write_orders,read_orders

Rutas de Autenticación (routes/auth.$.tsx - Ruta comodín):

import { redirect } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { authenticate } = await import('~/shopify.server');
  return authenticate.admin(request); // Token Exchange ocurre aquí
};

Manejo de Callback (routes/auth.callback.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  // La sesión contiene:
  // - session.accessToken (válido para llamadas a la API)
  // - session.shop (dominio de la tienda del comerciante)
  // - session.scope (ámbitos otorgados)
  // - session.state (datos personalizados opcionales)

  return redirect('/app'); // Redirige al panel de control después de la autenticación
};

Uso del Token en Llamadas a la API (routes/app.products.tsx):

import { json } from '@shopify/remix-oxygen';
import { authenticate } from '~/shopify.server';
import { GraphQLClient } from 'graphql-request';

export const loader = async ({ request }) => {
  const { session, admin } = await authenticate.admin(request);

  // Método 1: Usar el ayudante de administración de Shopify (recomendado)
  const response = await admin.graphql(`
    query ObtenerProductos {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
          }
        }
      }
    }
  `);

  // Método 2: Llamada GraphQL manual
  const client = new GraphQLClient(
    `https://${session.shop}/admin/api/2026-07/graphql.json`,
    {
      headers: {
        'X-Shopify-Access-Token': session.accessToken,
        'Content-Type': 'application/json',
      },
    }
  );

  const data = await client.request(/* ... */);

  return json({ products: response.data?.products?.edges || [] });
};

Actualización de Token (Automática): Los tokens de Token Exchange se actualizan automáticamente a través del middleware de sesión de Shopify. No se necesita actualización manual:

// Los tokens se actualizan de forma transparente en cada solicitud
const response = await admin.graphql(query); // Maneja la actualización internamente

2. Managed Installation (Aplicaciones Headless/Personalizadas)

Caso de Uso: Tienda headless, aplicaciones móviles, integraciones de terceros Flujo: El comerciante autoriza la aplicación → Shopify genera un token de acceso permanente (sin rotación de secreto) Duración del Token: De larga duración; no requiere actualización Seguridad: El token es permanente; almacenar de forma segura en variable de entorno

Configuración de Managed Installation:

En el Panel de Socios de Shopify:

  1. Configuración de la aplicación > Credenciales de la API
  2. Selecciona "Managed installation" bajo Ámbitos de acceso a la API de administración
  3. El comerciante concede permiso una vez
  4. Copia el token de acceso a tu entorno
# .env
SHOPIFY_ADMIN_ACCESS_TOKEN=<SHOPIFY_ADMIN_ACCESS_TOKEN>
SHOPIFY_SHOP_URL=ejemplo-tienda.myshopify.com

Uso del Token de Managed Installation:

import { GraphQLClient } from 'graphql-request';

const client = new GraphQLClient(
  `https://${process.env.SHOPIFY_SHOP_URL}/admin/api/2026-07/graphql.json`,
  {
    headers: {
      'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || '',
    },
  }
);

// El token nunca expira; llama a la API en cualquier momento
const query = `query { products(first: 10) { edges { node { id title } } } }`;
const products = await client.request(query);

3. OAuth 2.0 Authorization Code Grant (Heredado, Aún Soportado)

Caso de Uso: Aplicaciones públicas con flujo OAuth tradicional Flujo: El comerciante hace clic en "Instalar" → La aplicación redirige a Shopify OAuth → El comerciante autoriza → La aplicación recibe un código → La aplicación intercambia el código por un token Duración del Token: Token de acceso de larga duración (sin caducidad a menos que sea revocado) Seguridad: Se requiere el secreto del cliente; PKCE opcional

Diagrama del Flujo OAuth:

1. El comerciante visita: https://tu-app.com/auth
2. La aplicación redirige a: https://tu-tienda.myshopify.com/admin/oauth/authorize?client_id=KEY&scope=write_products&redirect_uri=https://tu-app.com/auth/callback&state=ALEATORIO
3. El comerciante autoriza la aplicación en el panel de administración de Shopify
4. Shopify redirige de vuelta: https://tu-app.com/auth/callback?code=CODIGO_AUTORIZACION&hmac=FIRMA&state=ALEATORIO&shop=tu-tienda.myshopify.com
5. La aplicación valida HMAC y state
6. La aplicación intercambia el código por un token (del lado del servidor, usando el secreto del cliente)
7. La aplicación almacena el token en la base de datos

Ejemplo OAuth con Express.js:

import express from 'express';
import axios from 'axios';
import crypto from 'crypto';

const app = express();

const API_KEY = process.env.SHOPIFY_API_KEY || '';
const API_SECRET = process.env.SHOPIFY_API_SECRET || '';
const REDIRECT_URI = process.env.REDIRECT_URI || 'https://tu-app.com/auth/callback';
const SCOPES = 'write_products,read_products';

// Paso 1: Redirigir al comerciante a Shopify OAuth
app.get('/auth', (req, res) => {
  const shop = req.query.shop as string;

  if (!shop || !shop.includes('.myshopify.com')) {
    return res.status(400).send('Falta el parámetro shop o no es válido');
  }

  const state = crypto.randomBytes(16).toString('hex');
  const nonce = crypto.randomBytes(16).toString('hex');

  // Almacenar state en la sesión (o base de datos) para su validación
  req.session.state = state;
  req.session.nonce = nonce;

  const authUrl = new URL(
    `/admin/oauth/authorize`,
    `https://${shop}`
  );
  authUrl.searchParams.append('client_id', API_KEY);
  authUrl.searchParams.append('scope', SCOPES);
  authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.append('state', state);

  res.redirect(authUrl.toString());
});

// Paso 2: Manejar el callback de OAuth
app.get('/auth/callback', async (req, res) => {
  const { code, hmac, shop, state } = req.query;

  // Validar HMAC
  const message = Object.entries(req.query)
    .filter(([key]) => key !== 'hmac')
    .map(([key, value]) => `${key}=${value}`)
    .sort()
    .join('&');

  const hash = crypto
    .createHmac('sha256', API_SECRET)
    .update(message, 'utf8')
    .digest('base64');

  if (hash !== hmac) {
    return res.status(401).send('Se detectó una solicitud no autorizada');
  }

  // Validar state
  if (state !== req.session.state) {
    return res.status(401).send('Discrepancia de state');
  }

  try {
    // Intercambiar código por token de acceso
    const response = await axios.post(
      `https://${shop}/admin/oauth/access_token`,
      {
        client_id: API_KEY,
        client_secret: API_SECRET,
        code,
      }
    );

    const { access_token, scope } = response.data;

    // Almacenar token de acceso (en base de datos, no en sesión, para persistencia)
    await storeAccessToken(shop as string, access_token, scope);

    // Redirigir al panel de control de la aplicación
    res.redirect(`/app?shop=${shop}`);
  } catch (error) {
    console.error('Error en el intercambio de token:', error);
    res.status(500).send('Autenticación fallida');
  }
});

// Función auxiliar para almacenar el token
async function storeAccessToken(shop: string, token: string, scope: string) {
  // Almacenar en base de datos (ejemplo usando almacenamiento simulado)
  const db = {
    shops: {} as Record<string, { token: string; scope: string }>,
  };
  db.shops[shop] = { token, scope };
}

app.listen(3000);

Adaptadores de Almacenamiento de Sesión

Los tokens de acceso deben almacenarse de forma persistente. Shopify proporciona adaptadores para backends de almacenamiento comunes:

Prisma (Recomendado para Remix)

Esquema (prisma/schema.prisma):

model Sesion {
  id        String    @id
  shop      String
  state     String
  isOnline  Boolean   @default(false)
  accessToken String
  refreshToken String?
  scope     String
  expiresAt DateTime?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  @@unique([shop, state])
  @@index([shop])
}

Configuración (shopify.app.ts):

import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { prisma } from '~/db.server';

const shopify = shopifyApp({
  // ...
  sessionStorage: new PrismaSessionStorage(prisma),
});

Redis

import { RedisSessionStorage } from '@shopify/shopify-app-session-storage-redis';
import redis from 'redis';

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
});

const sessionStorage = new RedisSessionStorage({
  client: redisClient,
  prefix: 'shopify_session:',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

En Memoria (Solo Desarrollo)

import { MemorySessionStorage } from '@shopify/shopify-app-session-storage';

const sessionStorage = new MemorySessionStorage();

const shopify = shopifyApp({
  // ...
  sessionStorage, // PRECAUCIÓN: Las sesiones se pierden al reiniciar la aplicación; solo desarrollo
});

DynamoDB

import { DynamoDBSessionStorage } from '@shopify/shopify-app-session-storage-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const dynamoDBClient = new DynamoDBClient({ region: 'us-east-1' });

const sessionStorage = new DynamoDBSessionStorage({
  client: dynamoDBClient,
  tableName: 'shopify-sessions',
});

const shopify = shopifyApp({
  // ...
  sessionStorage,
});

Tokens en Línea vs. Fuera de Línea

Token en Línea:

  • Ámbito: Permisos del usuario actual (típicamente usuario administrador)
  • Caducidad: 24 horas
  • Caso de Uso: Acciones basadas en navegador (panel de administración de Polaris)
  • Limitaciones: No puede ejecutar trabajos en segundo plano; limitado cuando el usuario cierra sesión

Token Fuera de Línea:

  • Ámbito: Ámbitos otorgados a la aplicación
  • Caducidad: Ninguna; permanente hasta que sea revocado
  • Caso de Uso: Trabajos en segundo plano, webhooks, tareas programadas
  • Limitaciones: Ninguna; usar por defecto para operaciones de la aplicación

Solicitar Token Fuera de Línea en Token Exchange:

// shopify.app.ts
const shopify = shopifyApp({
  // ...
  auth: {
    path: '/auth',
    callbackPath: '/auth/callback',
  },
});

// Remix solicita automáticamente un token fuera de línea por defecto
// No se necesita ninguna acción; usar session.accessToken para llamadas a la API

Usar Token Fuera de Línea para Webhooks:

// webhooks/products-update.ts
export const webhooks = {
  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/app-uninstalled',
  },
  PRODUCTS_UPDATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: '/webhooks/products-update',
  },
};

export default defineWebhooksConfig(
  async (request, { admin, session }) => {
    const { body, query } = await graphql.query(request, {
      query: GET_PRODUCT,
      variables: { id: 'gid://shopify/Product/123' },
    });

    // session.accessToken es un token fuera de línea; válido aquí
    console.log(`Webhook procesado con token para la tienda: ${session.shop}`);
  },
  webhooksConfig
);

Autenticación de Proxy de Aplicación

Caso de Uso: Solicitudes de tienda (orientadas al público) al backend de la aplicación Autenticación: Validación de firma HMAC (como webhooks) Flujo: Tienda → Solicitud de proxy Liquid → Backend de la aplicación (valida HMAC) → Respuesta

Configuración de Proxy de Aplicación (shopify.app.toml):

[[extensions]]
type = "app_proxy"
name = "API de Tienda"
url = "/api/proxy"
subpath = "loyalty"  # Las solicitudes a /apps/lealtad/* se enrutan aquí

Manejador de Proxy de Aplicación (Remix routes/api/proxy.ts):

import { json } from '@shopify/remix-oxygen';
import crypto from 'crypto';

export const loader = async ({ request }) => {
  const url = new URL(request.url);
  const hmac = url.searchParams.get('hmac') || '';
  const timestamp = url.searchParams.get('_t') || '';
  const shop = url.searchParams.get('shop') || '';
  const queryParams = new URLSearchParams(url.searchParams);
  queryParams.delete('hmac');
  queryParams.sort();
  const message = queryParams.toString();
  const digest = crypto
    .createHmac('sha256', API_SECRET)
    .update(message)
    .digest('hex');

  if (digest !== hmac || (Date.now() - Number(timestamp) > 60000)) {
    return json({ error: 'Firma HMAC inválida o tiempo de espera agotado' }, 401);
  }

  return json({ productos: [] });
};

Skills relacionados