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 local chiffré — une base de données SQLite, à l'intérieur de votre propre service (pas la base de données ShopiMind) — qui mémorise les installations, les secrets (clés API…) et les curseurs de synchronisation ; 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/hapietbetter-sqlite3. @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 — 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). |
inboundSecret | string | Le secret HMAC par-installation des routes entrantes, à transmettre à votre application. |
logger | Logger | Journalisation (rédige automatiquement les secrets). |
setExternalAccount | ({ id, name? }) => void | Associe l'installation ShopiMind à votre compte interne (pont de corrélation). |
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 = 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,
});
void 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 | — (requis) | Fichier SQLite du store. |
webhookSecret | — (requis) | Secret HMAC, communiqué par ShopiMind à l'enregistrement. |
credentialsKey | null | Clé AES-256 (64 hex) — 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. |
spmBaseUrl | — | Surcharge l'URL de base de l'API ShopiMind (défaut https://core.shopimind.com). |
createIntegrationApp renvoie un IntegrationApp avec start(), stop() et runSyncOnce(installationId, { full? }).
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 — AES-256-GCM, fail-closed), curseurs de synchro, historique des runs, journal des webhooks (charges rédigées d'après le config schema — 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.