Le kit d'intégration
Le kit @shopimind/integration-kit-js est le runtime d'une intégration ShopiMind, le kit d'intégration JavaScript (des kits miroirs PHP et Go sont prévus ultérieurement). Vous décrivez votre intégration avec un seul objet (defineIntegration({...})), et le kit fournit tout le reste :
- un serveur de webhooks (Hapi) qui reçoit les événements de cycle de vie ;
- la vérification de signature HMAC de chaque requête entrante (vous n'écrivez aucun code de sécurité) ;
- un store chiffré qui mémorise les installations, les secrets (clés API…, chiffrés quel que soit le backend) et les curseurs de synchronisation : SQLite par défaut, ou votre base PostgreSQL (voir Persistance & stores), à l'intérieur de votre propre service (pas la base de données ShopiMind) ; vous n'écrivez aucun SQL ;
- un planificateur de synchronisation incrémentale ;
- un client SDK ShopiMind déjà authentifié, prêt à l'emploi, exposé sous
ctx.spm.
Le kit dépend du SDK @shopimind/sdk-js et le ré-exporte (export * from '@shopimind/sdk-js'), si bien que vous importez les ressources et les types du SDK depuis le kit.
Le kit est une dépendance, pas un squelette à cloner
Vous installez le kit (@shopimind/integration-kit-js) et vous écrivez votre intégration par-dessus. Pas de copier-coller d'un starter : vous bénéficiez des mises à jour du kit comme de n'importe quelle dépendance npm.
Installation
npm install @shopimind/integration-kit-js @shopimind/sdk-js dotenv- ESM uniquement, Node.js ≥ 18.17. Dépendances du kit :
@hapi/hapi(le driver de store,better-sqlite3oupg, est une peer dependency optionnelle : installez celui de votre backend). @shopimind/sdk-jsest le SDK que le kit ré-exporte pour appeler l'API ShopiMind. Le kit construit le client authentifié à votre place.
L'objet defineIntegration
Toute votre intégration tient dans un unique appel defineIntegration. Le kit le valide au démarrage (slug conforme, entités de synchro uniques, sources fournies pour les étapes per-source).
import { defineIntegration } from '@shopimind/integration-kit-js';
import type { Integration, IntegrationContext, ProvisioningPlan, RemoteOption } from '@shopimind/integration-kit-js';
import type { MySettings } from './settings.js';
export const myIntegration: Integration<MySettings> = defineIntegration({
slug: 'my-app', // ^[a-z0-9_-]+$
meta: {
name: 'My App',
version: '1.0.0',
categories: ['crm'], // neutral slugs, resolved at registration
},
configSchema: { steps: [/* … */] },
parseSettings: (raw) => ({ /* typed settings */ }),
testConnection: async (ctx) => true,
remoteData: { /* dynamic options for a select */ },
provisioning: (ctx) => ({ dataSources: [/* … */] }),
widgets: [/* … */],
syncSteps: [/* … */],
inbound: { /* incoming routes called by your app */ },
hooks: { onActivate: async (ctx) => {} },
});Le contrat Integration<S>
| Champ | Type | Rôle |
|---|---|---|
slug | string | Identifiant unique de l'intégration (^[a-z0-9_-]+$). |
meta | IntegrationMeta | Métadonnées (voir ci-dessous). |
configSchema | ConfigSchema | Le formulaire de configuration présenté au marchand. Voir Configuration. |
parseSettings | (raw) => S | Transforme les valeurs de config brutes en vos réglages typés S. |
testConnection | (ctx) => Promise<boolean> | Valide les identifiants de l'intégration. Appelé à l'activation et depuis l'UI. |
remoteData? | Record<string, (ctx) => Promise<RemoteOption[]>> | Alimente les select dynamiques du formulaire. Voir Configuration. |
provisioning? | (ctx) => ProvisioningPlan | Sources de données, custom data et événements à créer. Voir Synchroniser les données. |
widgets? | WidgetDeclaration[] | Vos widgets. |
syncSteps | SyncStep<S>[] | Les étapes de synchronisation. |
inbound? | Record<string, (ctx, payload) => Promise<void> | void> | Les routes entrantes que votre application appelle pour déclencher un événement ou pousser une donnée en temps réel. |
hooks? | LifecycleHooks<S> | Réactions au cycle de vie (voir Webhooks). |
Pas de champ migrations
Le contrat Integration ne comporte pas de champ migrations. Le store interne du kit gère ses propres migrations.
meta : IntegrationMeta
| Champ | Type | |
|---|---|---|
name | string | Nom affiché. |
version | string | Version de l'intégration (ex. 1.0.0). |
categories? | string[] | Slugs de catégories (ex. ['crm'], ['pos']), résolus à l'enregistrement. |
icon_url? | string | Icône. |
short_description? / description? | string | Descriptions. |
documentation_url? | string | Lien vers votre doc. |
Le contexte ctx (IntegrationContext)
Le kit injecte un contexte dans chacun de vos callbacks (testConnection, remoteData, provisioning, syncSteps[].run, inbound, hooks) :
| Champ | Type | Rôle |
|---|---|---|
installationId | string | Jeton opaque de l'installation, émis par ShopiMind. À ne jamais interpréter. |
settings | S | Vos réglages typés (résultat de parseSettings). |
spm | SpmHttpClient | Le client SDK brut déjà authentifié. Appelez sur lui les méthodes statiques du SDK, par ex. SpmCustomers.bulkSave(ctx.spm, items, { chunk: true }) ou SpmEvents.trigger(ctx.spm, codeName, payload). Voir Synchroniser les données. |
sendBulk | (fn, items) => Promise<{ sent; rejected; rejected_items }> | Envoi en masse sûr : envoie par lots, lève une exception en cas d'échec de transport et fait remonter les rejets par item (le moteur retient le curseur sur les rejets, donc pas de perte silencieuse). La manière recommandée de pousser. Voir Synchroniser les données. |
withSource | (sourceKey) => SourceHandle | Renvoie un SourceHandle { id, tag(items), send(fn, items) } : tag ajoute id_data_source à chaque item, send tague puis pousse en toute sécurité. Pour les intégrations catalogue/POS. Voir Synchroniser les données. |
customData | (name) => CustomDataHandle | Renvoie un CustomDataHandle { id, save(records) } pour la définition de custom data name déclarée dans provisioning.customData. save réalise un upsert sûr des enregistrements (par lots, sûr au transport, fait remonter les rejets). Voir Synchroniser les données. |
state | IntegrationStateRepo | Stockage clé-valeur par installation (chiffrable). Depuis le kit v2, toutes ses méthodes (get, set, setSecret, delete) renvoient des Promises. Attendez-les avec await. |
inboundSecret | string | Le secret HMAC par-installation des routes entrantes, à transmettre à votre application. |
logger | Logger | Journalisation (rédige automatiquement les secrets). |
setExternalAccount | ({ id, name? }) => Promise<void> | Associe l'installation ShopiMind à votre compte interne (pont de corrélation). Asynchrone depuis le kit v2, à attendre avec await. |
Le SDK ne lève jamais d'exception sur les erreurs HTTP
Les appels au SDK renvoient une enveloppe { ok, statusCode, data, error } ; ils ne lèvent jamais d'exception sur une erreur HTTP. Utilisez les helpers ré-exportés SpmHelpers.unwrapOrThrow(env) et SpmHelpers.extractCounts(env) lorsque vous travaillez avec le client brut ctx.spm. Pour pousser des données, préférez ctx.sendBulk / withSource / customData, qui gèrent déjà pour vous les échecs de transport et les rejets par item.
Monter le runtime : main.ts
createIntegrationApp(integration, options) assemble le serveur, le store, le planificateur et le client SDK. Le kit construit lui-même le client SDK authentifié à partir de la clé API de la boutique reçue à l'installation. Vous n'instanciez jamais le SDK dans une intégration. Voici la forme de référence :
import 'dotenv/config';
import { createIntegrationApp } from '@shopimind/integration-kit-js';
import { myIntegration } from './integration.js';
const env = process.env;
const required = (key: string): string => {
const v = env[key];
if (!v) throw new Error(`Required environment variable: ${key}`);
return v;
};
const app = await createIntegrationApp(myIntegration, {
databasePath: env.DATABASE_PATH ?? './data/store.sqlite',
webhookSecret: required('WEBHOOK_SECRET'),
credentialsKey: env.CREDENTIALS_KEY ?? null,
// Secrets are fail-closed: without CREDENTIALS_KEY the app refuses to start.
// Allow cleartext ONLY in local dev.
allowPlaintextSecrets: env.NODE_ENV !== 'production',
adminToken: env.ADMIN_TOKEN ?? null,
backfillDays: env.BACKFILL_DAYS ? Number(env.BACKFILL_DAYS) : 365,
syncIntervalMinutes: env.SYNC_INTERVAL_MINUTES ? Number(env.SYNC_INTERVAL_MINUTES) : 15,
port: env.PORT ? Number(env.PORT) : 8080,
});
await app.start().catch((e: unknown) => {
console.error('Integration startup failed', e);
process.exit(1);
});
process.on('SIGTERM', () => void app.stop().then(() => process.exit(0)));
process.on('SIGINT', () => void app.stop().then(() => process.exit(0)));Le kit possède le client SDK
Il n'y a aucun import du SDK ni de fabrique de gateway à câbler. Le kit construit le SpmHttpClient (ctx.spm) pour chaque installation en utilisant la clé API de la boutique. Une option makeSpmClient existe uniquement pour les tests : le code de production ne la définit jamais.
Options de createIntegrationApp
| Option | Défaut | Rôle |
|---|---|---|
databasePath | — | Fichier SQLite du store (le défaut zéro-config, sucre pour le store SQLite). Exactement l'un de databasePath / store est requis. |
store | — | Un IntegrationStore explicite, p. ex. createPostgresStore(...) pour votre base PostgreSQL. Voir Persistance & stores. |
webhookSecret | — (requis) | Secret HMAC, communiqué par ShopiMind à l'enregistrement. |
credentialsKey | null | Clé AES-256 (64 hex) pour le chiffrement des secrets au repos. Obligatoire (fail-closed) : sans elle, l'app refuse de démarrer, sauf si allowPlaintextSecrets est activé. |
allowPlaintextSecrets | false | Opt-in DEV uniquement : stocker les secrets en clair quand aucune credentialsKey n'est fournie (émet un avertissement bruyant). À ne jamais utiliser hors développement local. |
adminToken | null | Protège les routes /admin/* et déverrouille la console d'exploitation. |
adminPort / adminHost | — / 127.0.0.1 | Sert la surface admin sur un listener séparé (loopback par défaut) au lieu du serveur public. Voir Console d'exploitation. |
adminSecureCookie | false | Marque le cookie de session admin Secure (servez la console en HTTPS). |
port / host | 8080 / 0.0.0.0 | Serveur HTTP. |
backfillDays | 365 | Profondeur de la synchro initiale. |
autoSync / syncIntervalMinutes | true / 15 | Planificateur de synchro incrémentale (≤ 0 le désactive). |
autoBackfillOnActivate | true | Lance une synchro complète juste après l'activation. |
signatureToleranceSeconds | 300 | Fenêtre anti-rejeu de la signature. |
retentionDays | 90 | Fenêtre de purge des tables de journal et d'anti-rejeu. |
rejectedRetentionDays | = retentionDays | Rétention du dead-letter (rejected_item). |
auditRetentionDays | 365 | Rétention du journal d'audit admin. |
overlapSeconds | 0 | Recouvrement défensif : recule le since de chaque fenêtre incrémentale, pour ne pas rater un élément pile à la frontière (les envois sont idempotents). |
stopDrainTimeoutMs | 10000 | Délai que stop() accorde aux synchros en cours avant de fermer le store. Gardez-le sous le délai de grâce de votre orchestrateur (Kubernetes : terminationGracePeriodSeconds). |
spmBaseUrl | — | Surcharge l'URL de base de l'API ShopiMind (défaut https://core.shopimind.com). |
createIntegrationApp est asynchrone depuis le kit v2 (await) : il charge le driver du store et applique les migrations avant de vous rendre la main. Il renvoie un IntegrationApp avec start(), stop() et runSyncOnce(installationId, { full? }).
Arrêt propre
stop() cesse d'accepter de nouvelles synchros, attend celles qui sont en cours (dans la limite de stopDrainTimeoutMs), puis ferme le store. Câblez-le sur SIGTERM : une synchro coupée en plein vol laisse un run inachevé dans vos journaux.
Variables d'environnement
Le kit ne lit pas process.env lui-même : c'est votre main.ts qui mappe l'environnement vers les options. Convention :
WEBHOOK_SECRET= # HMAC, provided by ShopiMind at registration (mandatory)
CREDENTIALS_KEY= # AES-256 (64 hex) — encryption at rest; MANDATORY in production
ADMIN_TOKEN= # protects the /admin/* routes and unlocks the operations console
ADMIN_PORT= # optional: serve /admin on its own listener (recommended in production)
PORT=8080
SYNC_INTERVAL_MINUTES=15
BACKFILL_DAYS=365
SHOPIMIND_API_URL= # overrides the API base (maps to spmBaseUrl; default https://core.shopimind.com)
DATABASE_PATH=./data/store.sqliteRoutes exposées par le kit
| Méthode | Chemin | Rôle | Auth |
|---|---|---|---|
POST | /webhook/receive | Tous les événements de cycle de vie | Signature HMAC ShopiMind (kit) |
POST | /webhook/test-connection | Test de connexion depuis l'UI ShopiMind | Signature HMAC ShopiMind (kit) |
POST | /webhook/remote-data/{resource} | Options dynamiques d'un select | Signature HMAC ShopiMind (kit) |
POST | /inbound/{action} | Routes entrantes appelées par votre app | Signature HMAC par-installation (kit) |
GET | /health | Sonde de disponibilité | — |
GET | /admin/ui | La console d'exploitation | jeton admin / session |
* | /admin/* | API d'exploitation (installations, synchro, dead-letter, audit…) | jeton admin / session |
La surface admin complète (lectures, actions, session et CSRF) est documentée sur la page Console d'exploitation.
Console d'exploitation
Définir adminToken déverrouille aussi une console d'exploitation autonome à /admin/ui : une vue en direct, côté intégrateur, de vos installations, runs de synchro, webhooks, dead-letter et journal d'audit, plus des actions sûres (synchro, reprovisioning, purge, reveal). Elle ne lit que votre store local, masque la PII par défaut et ne renvoie jamais de secret. En production, placez-la sur son propre listener avec adminPort et servez-la en HTTPS.
Ce que le kit gère pour vous
- Sécurité entrante : vérification HMAC sur le corps brut, en temps constant, avec fenêtre anti-rejeu par horodatage, aussi bien pour les webhooks ShopiMind (
X-Shopimind-Signature) que pour les appels entrants de votre app (x-integration-*, secret par-installation). L'anti-rejeu est obligatoire et imposé par le serveur (pas d'opt-in appelant) : un appel entrant ou un webhook de cycle de vie rejoué est court-circuité, et les transitions de cycle de vie sont idempotentes. - Store chiffré : installations, état de l'intégration (clé API chiffrée en AES-256-GCM, fail-closed), curseurs de synchro, historique des runs, journal des webhooks (charges rédigées d'après le config schema, donc aucun secret en clair). Les tables de journal et d'anti-rejeu sont purgées après
retentionDays. - Cycle de vie : install → activate → config_updated → deactivate → uninstall, avec gestion du statut et de la clé API.
- Synchronisation : planificateur, curseur sûr (anti perte de données), pagination en flux et concurrence bornée. Voir Synchroniser les données.
Pour aller plus loin
- Configuration : déclarer votre
config_schema. - Synchroniser les données :
SyncStep,ctx.sendBulk,ctx.withSource,ctx.customData, provisioning. - Cycle de vie & webhooks : événements, signature, hooks.
- Middleware entrant : exposer des routes appelées par votre app.
- Console d'exploitation : observer et piloter votre intégration depuis l'UI d'administration embarquée.
- Exemple complet (Hiboutik) : une intégration de bout en bout.