Vai al contenuto principale

Panoramica

Le connessioni sono credenziali che un utente detiene per un servizio esterno (Linear, GitHub, Slack, …). La tua app dichiara come vengono ottenute tali credenziali — un provider di connessione — e le utilizza in fase di esecuzione per effettuare chiamate autenticate all’API di terze parti. OAuth 2.0 è integrato. I servizi con un proprio flusso di connessione — una pagina di accesso ospitata, un ID dell’account recapitato tramite webhook — usano un provider custom, in cui la tua app gestisce il flusso e Twenty conserva l’utente, lo spazio di lavoro e la connessione salvata. Tipi di credenziali futuri (token di accesso personali, chiavi API, autenticazione di base) si integreranno nella stessa interfaccia — le app che già usano defineConnectionProvider({ type: 'oauth', ... }) non dovranno migrare.
Un provider di connessione descrive l’handshake OAuth di cui la tua app ha bisogno. Per un provider WORKSPACE (quello predefinito), un membro fa clic su “Aggiungi connessione” nelle impostazioni della tua app, completa la schermata di consenso del provider e viene creata una riga ConnectedAccount nel suo spazio di lavoro. Consulta “Connessioni dello spazio di lavoro o personali” per i provider USER.Una configurazione funzionante richiede due file — il provider di connessione e una dichiarazione serverVariables corrispondente su defineApplication che contiene le credenziali client OAuth.
src/connection-providers/linear-connection.ts
src/application.config.ts
Punti chiave:
  • name è la stringa identificativa univoca usata in listConnections({ providerName }) (kebab-case, deve corrispondere a ^[a-z][a-z0-9-]*$).
  • displayName viene mostrato nella scheda Generale dell’app e nell’elenco degli strumenti AI.
  • scope stabilisce chi si connette: 'WORKSPACE' (il valore predefinito) per le connessioni condivise con l’intero spazio di lavoro, 'USER' per le connessioni che ciascun membro crea per sé. Consulta “Connessioni dello spazio di lavoro o personali” qui sotto.
  • logo è un percorso a una risorsa inclusa nella cartella public/ della tua app, fornita tramite le risorse pubbliche dell’app. Quando omessa, la scheda Generale utilizza un segnaposto creato da displayName.
  • clientIdVariable / clientSecretVariable sono nomi, non valori — devono corrispondere alle chiavi dichiarate in defineApplication.serverVariables. I client_id e client_secret effettivi vengono inseriti dall’amministratore del server tramite l’interfaccia di registrazione dell’app e non vengono mai aggiunti al tuo repository.
  • Usa serverVariables (non applicationVariables) — le credenziali OAuth sono a livello di server e c’è una sola app OAuth per ogni server Twenty.
  • Finché entrambe le serverVariables non sono compilate, la scheda Generale dell’app mostra un suggerimento “richiede l’amministratore del server” e il pulsante “Aggiungi connessione” è disattivato.
  • type è 'oauth' (con un blocco oauth) o 'custom' (con un blocco custom, vedi “Flussi di connessione personalizzati”). Il discriminatore è compatibile con versioni future: tipi futuri ('pat', 'api-key', …) aggiungerà nuovi blocchi di sotto-configurazione accanto a questi.
L’URL di callback OAuth che il tuo provider deve inserire nella whitelist è:
Alcuni servizi connettono gli account tramite un proprio flusso anziché OAuth: Unipile, Plaid o Merge inviano l’utente a una pagina ospitata e poi segnalano l’account connesso a un URL fornito dall’utente, talvolta dai loro server anziché dal browser. Dichiarali con type: 'custom' e due funzioni logiche:
src/connection-providers/linkedin-connection.ts
Aggiungi connessione e Riconnetti funzionano come per OAuth. Twenty verifica che il membro possa connettersi, firma uno stato di breve durata per il tentativo ed esegue authorizeLogicFunction con un callbackUrl che lo contiene. Restituisci dove inviare l’utente:
src/logic-functions/handlers/linkedin-authorize-handler.ts
Passa callbackUrl al servizio senza modificarlo. Twenty lo accetta come reindirizzamento del browser (GET) e come notifica dal server (POST) ed esegue callbackLogicFunction per ogni richiesta con il relativo metodo, parametri di query (senza state), intestazioni, corpo analizzato e rawBody (i byte esatti della richiesta, null per GET). Restituisce l’account dimostrato dalla richiesta oppure null quando non ne contiene alcuno — in genere, il browser che torna mentre l’ID dell’account viene trasmesso tramite notifica:
src/logic-functions/handlers/linkedin-callback-handler.ts
Lo stato firmato in callbackUrl dimostra solo che la richiesta contiene tale URL, non che sia stata inviata dal servizio. Quando il servizio firma le proprie notifiche, verifica la firma su rawBody prima di considerare attendibile il corpo. Le intestazioni sono in minuscolo e cookie, authorization e proxy-authorization non vengono mai inoltrati:
src/logic-functions/handlers/hosted-callback-handler.ts
Il callback restituisce:Entrambe le funzioni vengono eseguite mentre il membro attende un reindirizzamento, quindi il loro timeout è limitato a 10 secondi. Un errore o un valore restituito non valido interrompe la connessione: non viene salvato nulla e onConnectLogicFunction non viene eseguita. onConnectLogicFunction viene eseguita quando una connessione viene creata, riconnessa o ripristinata, ma non di nuovo quando il servizio segnala un account già connesso. Lo stato scade dopo un’ora, quindi una notifica che arriva più tardi viene rifiutata. I servizi spesso ritentano le notifiche: una ripetizione per un account già connesso aggiorna quella connessione, quindi restituire di nuovo lo stesso account è sicuro.Twenty non aggiorna né revoca le connessioni personalizzate. Rilascia ciò che il servizio conserva per l’account in onDisconnectLogicFunction.Il type di un provider, come il relativo scope, non può cambiare dopo la sincronizzazione: per spostare un servizio tra oauth e custom, dichiara un nuovo provider con un nuovo universalIdentifier.I provider personalizzati sono disponibili con Twenty 2.52.0.
Quando un utente fa clic su Riconnetti, nulla gli impedisce di autorizzare un account upstream diverso da quello con cui è stata effettuata la connessione: lo spazio di lavoro Slack B invece dello spazio di lavoro A, un’altra organizzazione GitHub, un altro account Google. Senza un modo per distinguere i due account, Twenty sovrascriverebbe i token di A con quelli di B e tutto ciò che la tua app ha creato su quella connessione inizierebbe silenziosamente a comunicare con B.Imposta identifyAccountLogicFunction su una funzione logica nella stessa app che restituisce un ID stabile per l’account associato al token appena emesso:
src/connection-providers/slack-connection.ts
Quando viene eseguita. In modo sincrono all’interno del callback OAuth, subito dopo che il codice di autorizzazione viene scambiato con token e prima che venga salvato qualsiasi elemento. L’utente è in attesa del reindirizzamento, quindi, quando l’app viene installata o sincronizzata, Twenty limita timeoutSeconds della funzione a 10, indipendentemente da quanto dichiarato dal manifest; limita la richiesta a una sola chiamata all’endpoint “chi sono” del provider. L’hook on-connect, se presente, viene comunque eseguito successivamente, una volta salvata la connessione.Payload. Il gestore riceve il token di accesso provvisorio. Non è ancora stato reso persistente, quindi getConnection non può restituirlo; usalo direttamente e non registrarlo nei log.
Risultato. Restituisci l’id dell’account e, facoltativamente, un handle da visualizzare:
  • externalAccountId deve essere stabile per tutta la durata dell’account upstream (un id del team, un id dell’organizzazione, un id dell’utente), mai un nome che l’utente può modificare. Twenty lo rimuove e lo memorizza nella connessione al primo collegamento.
  • handle, quando restituito, sostituisce l’handle che Twenty altrimenti deriverebbe (l’email OIDC di id_token o l’email dell’utente che si collega). Restituisci qualcosa che un utente riconosca, ad esempio il nome dello spazio di lavoro Slack.
  • Restituisci { externalAccountId: null } quando il provider non dispone di un id stabile da offrire. Twenty si comporta quindi come se non fosse stata dichiarata alcuna funzione di identificazione: non viene memorizzato alcun id, una riconnessione cancella qualsiasi id memorizzato in precedenza e le riconnessioni non vengono verificate. Un semplice null o un return mancante viene trattato come errore, quindi un gestore che dimentica di restituire un valore non può disabilitare silenziosamente il controllo.
  • Se la funzione genera un’eccezione, supera il timeout o restituisce qualsiasi altra cosa, la connessione non viene salvata e l’utente torna alla pagina delle impostazioni con un errore. Genera un’eccezione solo quando non puoi effettivamente identificare l’account.
Rifiuto della riconnessione. Durante una riconnessione, Twenty confronta l’externalAccountId restituito con quello memorizzato nella connessione. Se differiscono, il callback viene rifiutato prima che venga scritto alcunché: i token esistenti, l’handle e l’ambito restano invariati, l’hook on-connect non viene eseguito e l’utente vede quale account ha autorizzato e a quale account appartiene la connessione. Le connessioni create prima che l’app dichiarasse la funzione non hanno ancora alcun id memorizzato; la loro successiva riconnessione lo memorizza.Twenty non rimuove i duplicati in base a externalAccountId: collegare due volte lo stesso account upstream tramite Aggiungi connessione crea due connessioni. L’id è esposto in listConnections e getConnection, quindi un’app che desidera una connessione per account upstream può rilevare autonomamente i duplicati (ad esempio nel proprio hook on-connect).Scrittura della funzione. La maggior parte dei provider espone un singolo endpoint che descrive il token corrente. Per Slack si tratta di auth.test, che restituisce lo spazio di lavoro (team_id, team) e l’utente associato al token:
src/logic-functions/slack-identify-account.ts
Altri provider seguono lo stesso schema, indipendentemente dal fatto che il loro endpoint “chi sono” sia REST o GraphQL. Lancia un’eccezione quando il provider rifiuta il token e restituisci l’id come stringa: converti gli id numerici, come gli id utente GitHub, con String(id).L’identificazione è disponibile con Twenty 2.48.0. Se la tua app dipende da questa funzionalità, dichiara "engines": { "twenty": ">=2.48.0" } nel relativo package.json.
Alcuni provider ti forniscono dati al momento della connessione che devi memorizzare prima che la connessione sia utilizzabile — l’esempio classico è Slack, dove la risposta OAuth identifica il team_id dell’area di lavoro che fungerà da chiave per gli eventi in ingresso. Imposta onConnectLogicFunction per fare riferimento a una funzione logica nella stessa app (tramite il suo universalIdentifier), e questa verrà eseguita subito dopo la creazione di ConnectedAccount.
src/connection-providers/slack-connection.ts
L’hook viene eseguito in modo asincrono nell’area di lavoro che si sta connettendo (viene messo in coda, non atteso), quindi un hook lento o che fallisce non blocca né interrompe mai il callback OAuth — rendilo idempotente e fai in modo che gestisca autonomamente i propri tentativi di ripetizione. Il gestore riceve:
Da lì usa getConnection(connectedAccountId) per leggere il token di accesso aggiornato e chiamare l’API del provider (ad es. Slack auth.test) o salvare una mappatura con il key-value store.
Qualsiasi elemento che un’app rivendica al momento della connessione deve essere rilasciato quando la connessione viene meno. Un’integrazione Slack che, ad esempio, rivendica un team_id al momento della connessione deve rilasciare tale rivendicazione, in modo che un’altra area di lavoro possa connettere lo stesso team Slack. Imposta onDisconnectLogicFunction in modo che faccia riferimento a una funzione logica nella stessa app; verrà eseguita quando la connessione viene disconnessa e di nuovo quando viene eliminata.
src/connection-providers/slack-connection.ts
A differenza dell’hook on-connect, viene eseguito inline nello spazio di lavoro in fase di disconnessione, senza tentativi: un errore viene acquisito e la disconnessione o l’eliminazione viene comunque completata. L’handler riceve la stessa struttura del payload:
In caso di disconnessione, ConnectedAccount e il relativo token sono ancora presenti, quindi getConnection(connectedAccountId) viene risolto e il gestore può rilasciare tutto ciò che ha registrato in remoto. In caso di eliminazione, le credenziali potrebbero essere già state rimosse (un membro può eliminare una connessione che aveva disconnesso in precedenza), quindi rendi il gestore idempotente e conserva lo stato necessario (un team_id, un id di sottoscrizione esterno) nell’archivio chiave-valore, scritto al momento della connessione e associato a connectedAccountId. Mantienilo breve: viene eseguito all’interno della richiesta, quindi il relativo timeoutSeconds rappresenta il limite massimo della durata di tale richiesta.L’hook viene attivato quando una connessione viene disconnessa o rimossa autonomamente. La disinstallazione dell’app elimina le sue connessioni tramite una cascata del database, quindi in questo caso l’hook non viene eseguito. Dichiara una uninstallLogicFunction su defineApplication per quel percorso: viene eseguita prima che i metadati dell’app vengano eliminati, così può ancora chiamare listConnections ed eseguire il cleanup di ciò che resta.
All’interno di un gestore di funzione di logica, listConnections({ providerName }) restituisce le righe ConnectedAccount di questa app per il provider indicato, con token di accesso aggiornati.
src/logic-functions/handlers/create-linear-issue-handler.ts
Ogni connessione ha:Punti chiave:
  • Passa { providerName } per filtrare per provider; omettilo per ottenere tutte le connessioni possedute da questa app su tutti i provider.
  • Il server rinnova in modo trasparente il token di accesso prima di restituire la risposta. Il tuo handler vede sempre un token utilizzabile (oppure authFailedAt impostato).
  • getConnection(id) è l’equivalente a singola riga.
La piattaforma imposta autonomamente authFailedAt solo quando il rinnovo OAuth non riesce. I provider i cui token non vengono mai rinnovati (ad esempio, un token bot di Slack) non riescono solo al momento della chiamata all’interno dei gestori, pertanto la riga della connessione nelle impostazioni continuerebbe a mostrare Connesso mentre ogni chiamata fallisce. Quando il codice rileva un rifiuto di autenticazione definitivo, segnalalo:
Ciò imposta authFailedAt e il motivo facoltativo sulla connessione. La riga nelle impostazioni passa a Riconnessione necessaria con un pulsante Riconnetti, la pagina dei dettagli della connessione mostra il motivo e getConnection inizia a generare AppConnectionAuthFailedError per quella riga. Il flag e il motivo vengono cancellati automaticamente quando l’utente si riconnette. Un reason più lungo di 1000 caratteri viene rifiutato, quindi tronca i payload del provider prima di passarli.Segnala solo gli errori che una riconnessione risolverebbe effettivamente (un rifiuto della classe invalid_auth dal provider), mai errori di rete transitori. Un’app può indicare solo le proprie connessioni e un utente della richiesta può indicare solo le proprie credenziali personali.La mutazione alla base di questo helper è disponibile con Twenty 2.40.0, quindi chiamarla su un server meno recente non riesce. Se l’app la usa, dichiara la dipendenza nel tuo package.json: "engines": { "twenty": ">=2.40.0" }.
Ogni connessione ha uno di due ambiti:
  • Condivisa nello spazio di lavoro ('WORKSPACE') — qualsiasi membro dello spazio di lavoro può usare la credenziale. Anche i trigger cron/database la vedono, poiché non hanno un utente della richiesta.
  • Solo per me ('USER') — la credenziale appartiene al membro che effettua la connessione. Nelle esecuzioni effettuate per conto di un membro (trigger della route HTTP con isAuthRequired: true), listConnections restituisce le connessioni condivise più quelle del membro stesso e findConnectionForRequest seleziona la connessione personale del membro, ricorrendo a una condivisa quando non ne possiede una. Nelle esecuzioni senza un utente della richiesta (ad esempio, trigger cron), listConnections restituisce ogni connessione, incluse quelle personali di ciascun membro.
Il scope del provider stabilisce quale ambito riceveranno le sue nuove connessioni:
  • scope: 'WORKSPACE' (il valore predefinito) — l’app funziona con connessioni condivise con lo spazio di lavoro. Aggiungi connessione nelle impostazioni dello spazio di lavoro dell’app crea una connessione condivisa nello spazio di lavoro.
  • scope: 'USER' — ciascun membro connette il proprio account, creando connessioni personali. I membri li aggiungono, ricollegano e rimuovono dalla pagina dell’app in Impostazioni > Utente > Preferenze app, che richiede l’autorizzazione App utente. Le impostazioni dello spazio di lavoro dell’app non dispongono del pulsante Aggiungi connessione per un provider USER.
Preferenze app non è ancora abilitato in ogni spazio di lavoro: dove non lo è, i membri non hanno un posto in cui aggiungere una connessione USER, quindi non fare affidamento su una per una funzionalità di cui ogni spazio di lavoro ha bisogno ora.
Un provider ha esattamente un ambito. Se la tua app necessita sia di una credenziale condivisa sia di una per membro per lo stesso servizio (ad esempio, un token di bot e l’account di ciascun membro), dichiara due provider.Una connessione ha sempre l’ambito del proprio provider e l’ambito di un provider fa parte della sua identità: una versione successiva non può modificare lo scope di un provider esistente e l’aggiornamento viene rifiutato se tenta di farlo. Per spostare un servizio da un ambito all’altro, dichiara un nuovo provider con un nuovo universalIdentifier e rimuovi quello vecchio. L’aggiornamento elimina quindi il vecchio provider e ogni connessione effettuata con esso tramite il normale percorso di disconnessione (onDisconnectLogicFunction, revoca del token, pulizia del canale), e i membri si connettono nuovamente tramite il nuovo provider.L’aggiunta o il ricollegamento di una connessione Solo per me richiede l’autorizzazione App utente, mentre l’aggiunta o il ricollegamento di una connessione Condivisa nell’area di lavoro richiede l’autorizzazione App dell’area di lavoro.Usa quella giusta per ciascun handler:
Sono consentite più connessioni per (utente, provider), quindi lo stesso utente può avere affiancate “Linear personale” e “Linear di lavoro”.L’ambito del provider è disponibile a partire da Twenty 2.49.0. Se la tua app dichiara scope, dichiara "engines": { "twenty": ">=2.49.0" } nel relativo package.json. Nella stessa versione, il scope di una connessione sostituisce la relativa visibility, che rimane disponibile come alias minuscolo deprecato. listConnections e getConnection di twenty-sdk 2.49.0 leggono scope, pertanto le app sviluppate con esso richiedono un server 2.49.0.
Per ciascun provider di connessione, l’amministratore del server deve prima registrare un’app OAuth presso il servizio di terze parti.
  1. Vai alle impostazioni sviluppatore del provider (ad es. https://linear.app/settings/api/applications/new).
  2. Imposta la Redirect URI su \<SERVER_URL>/auth/apps/callback.
  3. Copia il Client ID e il Client Secret generati.
  4. Apri l’app installata in Twenty come amministratore del server → imposta i valori nelle corrispondenti serverVariables.
  5. I membri dello spazio di lavoro possono quindi aggiungere connessioni ai provider WORKSPACE dalla sezione Connessioni dell’app.