Construire une app
Le contrat entre Spunto et une app — manifeste, installation, appels de capability signés et statut. Le même qu’utilisent GitHub et Google Cloud.
Une app est un service HTTP que vous faites tourner, sur votre propre domaine, avec votre propre base de données. Spunto en connaît trois choses : une URL de base, un secret de signature que Spunto vous remet quand vous l’enregistrez, et le manifeste que l’app sert. Tout le reste — les tokens qu’elle obtient, les comptes qu’elle connecte, les machines qu’elle fait tourner — reste chez l’app.
Ce n’est pas une version simplifiée du fonctionnement des intégrations maison. GitHub (git), Google Cloud (runtime) et le runtime des machines que vous apportez sont des services séparés qui parlent exactement ce contrat ; rien sur cette page ne leur est réservé.
| Ce que c’est | |
|---|---|
| App | Votre service : une identité, et les capabilities qu’il implémente |
| Capability | Une interface que la plateforme appelle — git, runtime. Un groupe d’opérations, certaines obligatoires, d’autres optionnelles |
| Installation | Votre app connectée à une organisation, par l’un de ses owners ou admins. Le credential qui en résulte est à vous, vous le gardez |
| Grant | L’autorisation personnelle d’un membre, sous une installation — seulement pour ce qui ne peut pas se faire au nom de l’organisation (enregistrer la clé SSH d’un membre sur son propre compte, pousser en son nom) |
Les installations appartiennent aux organisations, et c’est voulu : ce qu’une app produit — un credential de clone, une machine — appartient à l’organisation, et doit continuer de fonctionner quand la personne qui l’a connectée s’en va. Le grant est l’exception, pas la moitié du modèle.
| Endpoint | Appelé par | Signé | Rôle |
|---|---|---|---|
GET /manifest | Spunto, de serveur à serveur | Non | Ce qu’est l’app et ce qu’elle implémente |
GET {installPath}?launch=… | Le navigateur d’un membre | Jeton de lancement | Commencer à connecter l’app à une organisation |
GET {grantPath}?launch=… | Le navigateur d’un membre | Jeton de lancement | Commencer l’autorisation personnelle d’un membre (optionnel) |
POST /oauth/complete | Spunto | Oui | Valider une installation ou un grant que Spunto confirme |
POST /status | Spunto | Oui | Ce que cette organisation a connecté |
POST /capabilities/{capability}/{operation} | Spunto | Oui | Faire le travail |
Les appels signés portent un HMAC calculé sur la méthode, le chemin et le corps → Requêtes signées.
GET /manifest{
"id": "acme-git",
"name": "Acme Git",
"description": "Repositories hosted on Acme Git.",
"publisher": "Acme",
"accent": "#2563eb",
"auth": "oauth",
"capabilities": [
{
"capability": "git",
"operations": ["cloneCredential", "listRepos", "listBranches", "userCredential"]
}
],
"userGrantOperations": ["userCredential"],
"publicUrl": "https://git.acme.dev/spunto",
"installPath": "/oauth/install",
"grantPath": "/oauth/user",
"managePath": "/settings"
}| Champ | |
|---|---|
id | Identifiant stable. C’est aussi votre client_id pour Connect with Spunto |
capabilities | Une entrée par capability, avec les opérations que vous implémentez réellement |
userGrantOperations | Les opérations qui exigent le grant d’un membre en plus de l’installation. À omettre s’il n’y en a pas |
auth | oauth, apiToken (un formulaire à la place de l’écran de consentement) ou none |
accent | La couleur de votre carte, en couleur CSS. Les apps ont une icône générique pour l’instant : Spunto n’affiche aucune image servie par une app |
publicUrl | Là où un navigateur joint l’app. installPath, grantPath et managePath sont des chemins sous cette URL |
managePath | Optionnel : la page de réglages de l’app pour une organisation qui l’a installée — affichée sous la forme Manage ↗ |
oauth.redirectUris | Optionnel : là où Connect with Spunto peut renvoyer un utilisateur |
Spunto relit le manifeste toutes les minutes. Ajouter une opération à votre app, c’est déployer votre app — aucune release de Spunto, et les boutons qui en dépendent apparaissent d’eux-mêmes.
Le dashboard affiche une fonctionnalité si et seulement si une app installée implémente l’opération qui la sous-tend. Un hébergeur git sans registerUserSshKey clone quand même — on ne propose simplement pas aux membres d’y enregistrer leur clé. Un runtime sans pause fait tourner des sandboxes quand même — le bouton Pause n’est simplement pas là. Déclarez ce que vous implémentez, rien de plus, et le produit s’adapte.
Une redirection de navigateur n’authentifie personne. Les paramètres d’une installation ne sont donc jamais lus dans une query string : Spunto les signe, et confirme le résultat de serveur à serveur.
Spunto lance le flux
Un owner ou un admin clique sur Connect dans la page Integrations. Son navigateur part vers {publicUrl}{installPath}?launch=<token> — un jeton de lancement signé avec votre secret, qui dit quelle organisation, quel utilisateur, où revenir, et qu’il expire dans dix minutes.
Votre app fait sa part
Vérifiez le jeton. Puis faites ce que « connecter » veut dire chez vous : une danse OAuth avec votre propre fournisseur, ou un formulaire qui demande une clé d’API. Spunto n’y prend aucune part et n’en voit rien.
Mettez le résultat en attente, renvoyez le navigateur
Stockez le résultat comme en attente, sous un id opaque, pour quelques minutes. Redirigez vers l’URL return du jeton, suivie de ?completion=<id>. Ne le validez pas encore.
Spunto confirme
Spunto appelle POST /oauth/complete avec l’organisation, l’utilisateur réellement connecté, l’id completion, et si cet utilisateur a le droit d’installer des apps (canInstall, vérifié à nouveau à ce moment-là). Ne validez que si l’organisation et l’utilisateur correspondent à ce que vous avez mis en attente et — pour une installation — si canInstall vaut true.
// POST /oauth/complete — signed
{ "orgId": "org_…", "userId": "usr_…", "completion": "c8f1…", "canInstall": true }
// 200
{ "result": { "kind": "install" } } // or "grant"C’est l’étape de confirmation qui rend inoffensif un lien transféré : un attaquant qui envoie son lien de lancement à quelqu’un d’autre ne peut pas rattacher le compte de cette personne à l’organisation de l’attaquant, puisque l’utilisateur avec lequel Spunto confirme est celui qui est vraiment connecté.
Un grant suit les mêmes quatre étapes, à partir de grantPath, avec un jeton de lancement dont le purpose vaut grant, et se valide pour ce seul membre.
Spunto ne garde aucune copie de ce qu’une organisation a connecté. Pour afficher la page Integrations, il demande :
// POST /status — signed
{ "orgId": "org_…", "userId": "usr_…" }
// 200
{
"result": {
"installations": [
{
"externalId": "48213",
"accountLogin": "acme",
"accountType": "Organization",
"accountAvatarUrl": "https://…",
"installedAt": "2026-10-02T09:14:00Z"
}
],
"grant": { "present": false, "externalId": null }
}
}Renvoyez toujours grant, même si votre app n’a pas de grants — { "present": false, "externalId": null }. La déconnexion se fait dans votre app (sur votre managePath, par exemple) ; c’est par le /status suivant que Spunto l’apprend.
Chaque opération est un POST signé, avec une seule enveloppe :
// POST /capabilities/git/cloneCredential — signed
{
"orgId": "org_…",
"userId": "usr_…",
"input": { "repo": "acme/api", "repos": ["acme/web"] }
}orgId dit quelle installation utiliser. userId est présent quand une personne est derrière l’appel, et c’est au nom de celle-ci qu’il faut agir pour une opération à grant. input est propre à l’opération — voir la référence de chaque capability.
Répondez par { "result": … } :
{ "result": { "scheme": "https-token", "username": "x-access-token", "token": "…", "expiresInSeconds": 3600 } }N’importe quel statut hors 2xx, avec un corps JSON { "error": "…", "code": "…" }. Le message est montré au membre. Deux codes restent distincts jusqu’au dashboard, parce qu’ils ne demandent pas la même chose au membre :
| Code | Signifie |
|---|---|
not_installed | Cette organisation n’a pas connecté l’app — un owner ou un admin doit le faire |
grant_required | L’opération exige l’autorisation propre de ce membre — c’est à lui de l’accorder |
Nos apps répondent aux deux par un 409 ; c’est le code qui compte. Tout le reste est signalé comme une défaillance de votre app.
Spunto attend une réponse pendant 10 secondes. L’appel lui-même n’est jamais rejoué : une seule implémentation répond, le résultat compte, et rejouer une requête à l’aveugle est la façon dont on crée deux exemplaires de quelque chose. Quand une capability réessaie bien — runtime.create, quand l’app était injoignable ou n’a pas répondu à temps —, elle le dit, et l’opération doit être idempotente sur l’id qu’on lui donne.
Un travail long (démarrer une machine, construire une image) ne tient pas en 10 secondes, et n’a pas à y tenir : répondez tout de suite avec un handle, et rendez compte de l’avancement par l’opération de statut que la capability définit.
Spunto appelle votre app sur son URL de base — qui peut être privée, un nom sur un réseau interne. Les navigateurs la joignent sur publicUrl. Ce sont souvent les mêmes ; ce n’est pas obligatoire.
Enregistrez-la
Dans Settings → Developer apps, un owner ou un admin donne à l’app un id — celui que déclare son manifeste, en lettres minuscules, chiffres et tirets — et son URL de base, en https. Spunto affiche le secret de signature une seule fois : mettez-le dans la configuration de votre app.
Essayez-la dans votre organisation
Check lit votre manifeste comme le fait le catalogue, fait un POST /status signé, et dit ce qui ne va pas s’il y a lieu. Une fois le contrôle passé, l’app apparaît sur la page Integrations de votre organisation, marquée Your app · not reviewed : installez-la et utilisez-la pour de vrai. Personne d’autre ne peut la voir, l’appeler ou s’y connecter.
Soumettez-la à la revue
Submit for review, avec une note pour la personne qui relit : ce que fait l’app, comment l’essayer. Spunto prend un instantané de votre manifeste à ce moment-là — c’est cette version qui est revue.
Publiée
Une fois approuvée, l’app est proposée à toutes les organisations, marquée Marketplace. Sinon, la raison s’affiche sur votre app ; corrigez et soumettez à nouveau.
Le manifeste est toujours lu en direct : une app publiée peut donc gagner des opérations dans les capabilities pour lesquelles elle a été approuvée, sans que personne ne les revoie. Tout le reste reste tel que revu jusqu’à l’approbation d’une nouvelle version :
| Pour les autres organisations | |
|---|---|
name, description, publisher, icon, accent | Tels que revus |
publicUrl, installPath, grantPath, managePath | Tels que revus |
| URL de base | Telle que revue |
capabilities | Seulement celles approuvées ; leurs opérations, selon le manifeste en direct |
oauth.redirectUris | Seulement celles approuvées |
Pour changer l’un d’eux, Submit an update : la version publiée reste en place pendant que la nouvelle est revue. Pour essayer un changement avant de le soumettre, enregistrez une seconde app — acme-git-staging — et gardez celle qui est publiée pour ce qui a été revu.
- Seules les capabilities
gitetruntimesont ouvertes aux apps pour l’instant. - Vos pages vivent sur votre propre origine :
publicUrlest une URLhttpsabsolue, ou laissée de côté pour utiliser l’URL de base. - Spunto n’appelle que des adresses publiques, et ne suit pas les redirections.
- Unlist retire une app publiée de la marketplace : les organisations qui l’utilisent la perdent aussitôt. Spunto peut aussi suspendre une app, ce qui la retire de toutes les organisations et refuse tout appel vers elle.
Hébergement git — git
Là où vivent les dépôts d’un projet : credentials de clone, sélecteurs de dépôt et de branche, push au nom du membre, statut de la pull request.
Runtime de sandboxes — runtime
Là où tournent les sandboxes : créer, démarrer, arrêter et détruire une boîte qui fait tourner l’agent de Spunto.
Le catalogue des capabilities appartient à la plateforme, et il est fermé : une app ne peut pas en inventer une, puisque rien ne l’appellerait jamais. Les harnais d’agent sont aussi des apps, mais tournent dans la plateforme — le flux d’une session est un chemin trop chaud pour passer par un proxy —, ils ne sont donc pas ouverts aux tiers.