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
AppVotre service : une identité, et les capabilities qu’il implémente
CapabilityUne interface que la plateforme appelle — git, runtime. Un groupe d’opérations, certaines obligatoires, d’autres optionnelles
InstallationVotre app connectée à une organisation, par l’un de ses owners ou admins. Le credential qui en résulte est à vous, vous le gardez
GrantL’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.

EndpointAppelé parSignéRôle
GET /manifestSpunto, de serveur à serveurNonCe qu’est l’app et ce qu’elle implémente
GET {installPath}?launch=…Le navigateur d’un membreJeton de lancementCommencer à connecter l’app à une organisation
GET {grantPath}?launch=…Le navigateur d’un membreJeton de lancementCommencer l’autorisation personnelle d’un membre (optionnel)
POST /oauth/completeSpuntoOuiValider une installation ou un grant que Spunto confirme
POST /statusSpuntoOuiCe que cette organisation a connecté
POST /capabilities/{capability}/{operation}SpuntoOuiFaire 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
idIdentifiant stable. C’est aussi votre client_id pour Connect with Spunto
capabilitiesUne entrée par capability, avec les opérations que vous implémentez réellement
userGrantOperationsLes opérations qui exigent le grant d’un membre en plus de l’installation. À omettre s’il n’y en a pas
authoauth, apiToken (un formulaire à la place de l’écran de consentement) ou none
accentLa 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
publicUrlLà où un navigateur joint l’app. installPath, grantPath et managePath sont des chemins sous cette URL
managePathOptionnel : la page de réglages de l’app pour une organisation qui l’a installée — affichée sous la forme Manage ↗
oauth.redirectUrisOptionnel : 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.

Un manifeste est accepté entier ou pas du tout

Une app qui revendique une capability sans l’une de ses opérations obligatoires, ou qui nomme une capability qui n’existe pas, est rejetée en entier, pas tronquée. En accepter la moitié transformerait une faute de frappe en une app qui a l’air installée et échoue la première fois qu’une sandbox en a besoin.

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.

1

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.

2

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.

3

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.

4

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 :

CodeSignifie
not_installedCette organisation n’a pas connecté l’app — un owner ou un admin doit le faire
grant_requiredL’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.

1

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.

2

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.

3

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.

4

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, accentTels que revus
publicUrl, installPath, grantPath, managePathTels que revus
URL de baseTelle que revue
capabilitiesSeulement celles approuvées ; leurs opérations, selon le manifeste en direct
oauth.redirectUrisSeulement 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 git et runtime sont ouvertes aux apps pour l’instant.
  • Vos pages vivent sur votre propre origine : publicUrl est une URL https absolue, 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.

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.