@@ -68,17 +68,19 @@ export interface ConnectionStatus {
6868 icon ?: string
6969 /** #327: display name from registry */
7070 name ?: string
71+ /** auth methods advertised to the UI — browser for google, token for others */
72+ auth_methods ?: string [ ]
7173}
7274
7375/** The connection cards this module serves; company-compute renders first. */
74- export const CONNECTION_IDS : ConnectionType [ ] = [ "company-compute" , "pasqal-cloud" , "slack" , "github" , "linear" ]
76+ export const CONNECTION_IDS : ConnectionType [ ] = [ "company-compute" , "pasqal-cloud" , "slack" , "github" , "linear" , "google" , "google-drive" ]
7577
7678// --- Registry (issue #327): formalized built-in catalog with logos + custom ---
7779
7880/** Inline SVG icons — full-color brand marks, 18×18 with explicit fills (not currentColor). */
7981export const CONNECTION_ICONS : Record < string , string > = {
8082 "company-compute" :
81- '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="#0EA5E9 "/><path fill="white" d="M6.3 11.8h4.2c.8 0 1.4-.6 1.4-1.4 0-.6-.4-1.1-.9-1.3A2.2 2.2 0 0 0 9 7.4a2.2 2.2 0 0 0-2 .9c-.6.1-1 .6-1 1.2 0 .7.6 1.3 1.3 1.3z"/><path fill="white" opacity="0.95" d="M8.1 9.4 9 8.2l1 1.2 1.5-1.8 1 1-2.5 3-2-2.4z"/></svg>' ,
83+ '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="#EAB308 "/><path fill="white" d="M6.3 11.8h4.2c.8 0 1.4-.6 1.4-1.4 0-.6-.4-1.1-.9-1.3A2.2 2.2 0 0 0 9 7.4a2.2 2.2 0 0 0-2 .9c-.6.1-1 .6-1 1.2 0 .7.6 1.3 1.3 1.3z"/><path fill="white" opacity="0.95" d="M8.1 9.4 9 8.2l1 1.2 1.5-1.8 1 1-2.5 3-2-2.4z"/></svg>' ,
8284 "pasqal-cloud" :
8385 '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="#1E1B4B"/><circle cx="9" cy="9" r="1.5" fill="white"/><ellipse cx="9" cy="9" rx="4.6" ry="1.65" fill="none" stroke="#FB713C" stroke-width="0.95" opacity="0.98"/><ellipse cx="9" cy="9" rx="4.6" ry="1.65" fill="none" stroke="#FB713C" stroke-width="0.95" opacity="0.98" transform="rotate(60 9 9)"/><ellipse cx="9" cy="9" rx="4.6" ry="1.65" fill="none" stroke="#FB713C" stroke-width="0.95" opacity="0.98" transform="rotate(-60 9 9)"/></svg>' ,
8486 slack :
@@ -87,14 +89,18 @@ export const CONNECTION_ICONS: Record<string, string> = {
8789 '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="white" stroke="#E5E7EB" stroke-width="0.5"/><path fill="#24292F" d="M9 3.2a5.9 5.9 0 0 0-1.86 11.48c.29.05.4-.13.4-.28V13.4c-1.55.34-1.87-.66-1.87-.66-.25-.64-.62-.81-.62-.81-.5-.34.04-.33.04-.33.56.04.85.57.85.57.5.85 1.3.6 1.62.46.05-.36.19-.6.35-.74-1.23-.14-2.52-.62-2.52-2.74 0-.6.22-1.1.57-1.48-.06-.14-.25-.7.05-1.45 0 0 .47-.15 1.54.56A5.34 5.34 0 0 1 9 6.35c.48 0 .96.06 1.41.19 1.07-.71 1.54-.56 1.54-.56.3.75.11 1.31.05 1.45.35.38.57.88.57 1.48 0 2.13-1.3 2.6-2.53 2.74.2.17.38.5.38 1.02v1.51c0 .16.1.34.4.28A5.9 5.9 0 0 0 9 3.2z"/></svg>' ,
8890 linear :
8991 '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="#5E6AD2"/><path fill="white" d="M6.4 5.6h1.7L10.3 9 8 12.5H6.3L8.6 9 6.4 5.6z"/><path fill="white" opacity="0.75" d="M11.2 5.6h1.2v6.9h-1.2z"/></svg>' ,
92+ google :
93+ '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="white" stroke="#E5E7EB" stroke-width="0.5"/><path fill="#4285F4" d="M9.2 13.3c1.5 0 2.5-.5 3.3-1.4l-1.5-1.3c-.3.4-.7.8-1.8.8-1.4 0-2.4-1-2.4-2.3s1-2.3 2.4-2.3c.6 0 1 .2 1.3.4l1.1-1.1C10.9 5.5 10 5 9.2 5 6.9 5 5 6.7 5 9s1.9 4 4.2 4z"/><path fill="#34A853" d="M13.6 9.2c0-.3 0-.5-.1-.8H9.2v1.5h2.5c-.1.6-.5 1.1-1 1.4l1.5 1.2c.9-.8 1.4-2 1.4-3.3z"/><path fill="#FBBC04" d="M7.8 11.3c-.2-.5-.3-1-.3-1.6s.1-1.1.3-1.6L6.2 6.8C5.7 7.7 5.5 8.3 5.5 9s.2 1.3.7 2.2l1.6-1z"/><path fill="#EA4335" d="M9.2 6.3c.8 0 1.3.3 1.6.6l1.2-1.2C11.1 5 10.1 4.6 9.2 4.6 6.9 4.6 5 6.3 5 8.6l1.6 1.3c.4-.9 1.2-1.6 2.6-1.6z"/></svg>' ,
94+ "google-drive" :
95+ '<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="18" rx="4" fill="white" stroke="#E5E7EB" stroke-width="0.5"/><path fill="#4285F4" d="M10.2 4.2 4.5 13.8h3.4l5.7-9.6z"/><path fill="#34A853" d="M10.2 4.2h3.4L9 12.1 7.3 9.2z"/><path fill="#FBBC04" d="M4.5 13.8 9 12.1 7.3 9.2 4.5 13.8z"/></svg>' ,
9096}
9197
9298export interface ConnectionEntry {
9399 id : string
94100 kind : "built-in" | "custom"
95101 name : string
96102 icon : { kind : "svg" ; svg : string } | { kind : "letter" ; letter : string }
97- validator : "company-compute" | "pasqal" | "slack" | "github" | "linear" | "none"
103+ validator : "company-compute" | "pasqal" | "slack" | "github" | "linear" | "google" | "google-drive" | " none"
98104 authShape : "base-url-token" | "token-only" | "pasqal-credentials"
99105 url ?: string
100106}
@@ -140,6 +146,22 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [
140146 validator : "linear" ,
141147 authShape : "token-only" ,
142148 } ,
149+ {
150+ id : "google" ,
151+ kind : "built-in" ,
152+ name : "Google" ,
153+ icon : { kind : "svg" , svg : CONNECTION_ICONS [ "google" ] } ,
154+ validator : "google" ,
155+ authShape : "token-only" ,
156+ } ,
157+ {
158+ id : "google-drive" ,
159+ kind : "built-in" ,
160+ name : "Google Drive" ,
161+ icon : { kind : "svg" , svg : CONNECTION_ICONS [ "google-drive" ] } ,
162+ validator : "google-drive" ,
163+ authShape : "token-only" ,
164+ } ,
143165]
144166
145167export function getBuiltInEntry ( id : string ) : ConnectionEntry | undefined {
@@ -414,6 +436,7 @@ function renderStatus(
414436 if ( icon ) out . icon = icon
415437 const name = nameForId ( id )
416438 if ( name ) out . name = name
439+ if ( id === "google" || id === "google-drive" ) out . auth_methods = [ "browser" ]
417440 return out
418441 }
419442 let state : ConnectionState
@@ -448,6 +471,7 @@ function renderStatus(
448471 if ( icon ) out . icon = icon
449472 const name = nameForId ( id )
450473 if ( name ) out . name = name
474+ if ( id === "google" || id === "google-drive" ) out . auth_methods = [ "browser" ]
451475 return out
452476}
453477
@@ -596,7 +620,8 @@ function kickStaleRevalidations(body: string, deps: { fetchImpl?: FetchImpl; pas
596620 try {
597621 if ( id === "company-compute" ) await backgroundRevalidateCompanyCompute ( deps )
598622 else if ( id === "pasqal-cloud" ) await backgroundRevalidatePasqal ( deps )
599- else if ( id === "slack" || id === "github" || id === "linear" ) await backgroundRevalidateToken ( id , deps )
623+ else if ( id === "slack" || id === "github" || id === "linear" || id === "google" || id === "google-drive" )
624+ await backgroundRevalidateToken ( id , deps )
600625 } catch {
601626 // background refresh must never surface trouble; the next GET retries
602627 }
@@ -680,6 +705,8 @@ async function backgroundRevalidateToken(id: ConnectionType, deps: { fetchImpl?:
680705 let probe : ProbeResult
681706 if ( id === "slack" ) probe = await probeSlack ( cred . token , deps . fetchImpl )
682707 else if ( id === "github" ) probe = await probeGithub ( cred . token , deps . fetchImpl )
708+ else if ( id === "google" ) probe = await probeGoogle ( cred . token , deps . fetchImpl )
709+ else if ( id === "google-drive" ) probe = await probeGoogleDrive ( cred . token , deps . fetchImpl )
683710 else probe = await probeLinear ( cred . token , deps . fetchImpl )
684711 const existing = whitelistPersisted ( readCacheFile ( connectionsFile ( ) ) [ id ] )
685712 if ( probe . outcome === "unreachable" ) {
@@ -830,6 +857,36 @@ export async function probeLinear(token: string, fetchImpl: FetchImpl = fetch):
830857 return { outcome : "unreachable" }
831858}
832859
860+ export async function probeGoogle ( token : string , fetchImpl : FetchImpl = fetch ) : Promise < ProbeResult > {
861+ let response : { status : number ; json ?: ( ) => Promise < unknown > }
862+ try {
863+ response = await fetchImpl ( "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + encodeURIComponent ( token ) , {
864+ method : "GET" ,
865+ headers : { } ,
866+ } )
867+ } catch {
868+ return { outcome : "unreachable" }
869+ }
870+ if ( response . status === 200 ) return { outcome : "valid" }
871+ if ( response . status === 400 || response . status === 401 || response . status === 403 ) return { outcome : "invalid" }
872+ return { outcome : "unreachable" }
873+ }
874+
875+ export async function probeGoogleDrive ( token : string , fetchImpl : FetchImpl = fetch ) : Promise < ProbeResult > {
876+ let response : { status : number ; json ?: ( ) => Promise < unknown > }
877+ try {
878+ response = await fetchImpl ( "https://www.googleapis.com/drive/v3/about?fields=user" , {
879+ method : "GET" ,
880+ headers : { authorization : `Bearer ${ token } ` } ,
881+ } )
882+ } catch {
883+ return { outcome : "unreachable" }
884+ }
885+ if ( response . status === 200 ) return { outcome : "valid" }
886+ if ( response . status === 400 || response . status === 401 || response . status === 403 ) return { outcome : "invalid" }
887+ return { outcome : "unreachable" }
888+ }
889+
833890// --- Pasqal validator spawn (amicode#169 / parent #159; #164 contract) ---
834891// The fork never sees SDK internals: the validator's one-line JSON + exit-code
835892// contract is the ENTIRE interface. Inputs ride env variables ONLY — never
@@ -1179,7 +1236,7 @@ export async function submitCredentialResponse(rawBody: string, deps: MutationDe
11791236 const body = parseMutationBody ( rawBody )
11801237 if ( ! body ) return synthesizeConnection ( "bad_request" , "body must be JSON with an id and that id's credential fields" )
11811238 if ( body . id === "pasqal-cloud" ) return submitPasqalCredential ( body , deps )
1182- if ( body . id === "slack" || body . id === "github" || body . id === "linear" ) {
1239+ if ( body . id === "slack" || body . id === "github" || body . id === "linear" || body . id === "google" || body . id === "google-drive" ) {
11831240 return submitTokenCredential ( body . id as ConnectionType , body , deps )
11841241 }
11851242 if ( body . id !== "company-compute" ) {
@@ -1228,6 +1285,8 @@ async function submitTokenCredential(id: ConnectionType, body: MutationBody, dep
12281285 try {
12291286 if ( id === "slack" ) probe = await probeSlack ( token , deps . fetchImpl )
12301287 else if ( id === "github" ) probe = await probeGithub ( token , deps . fetchImpl )
1288+ else if ( id === "google" ) probe = await probeGoogle ( token , deps . fetchImpl )
1289+ else if ( id === "google-drive" ) probe = await probeGoogleDrive ( token , deps . fetchImpl )
12311290 else probe = await probeLinear ( token , deps . fetchImpl )
12321291 } finally {
12331292 inflightOverlay . delete ( id )
@@ -1583,7 +1642,7 @@ export async function revalidateResponse(rawBody: string, deps: MutationDeps = {
15831642 const id = parseIdBody ( rawBody )
15841643 if ( ! id ) return synthesizeConnection ( "bad_request" , "body must be JSON {id} with a known connection id" )
15851644 if ( id === "pasqal-cloud" ) return revalidatePasqal ( deps )
1586- if ( id === "slack" || id === "github" || id === "linear" ) {
1645+ if ( id === "slack" || id === "github" || id === "linear" || id === "google" || id === "google-drive" ) {
15871646 const cred = readCredential ( id ) as { token ?: string } | undefined
15881647 if ( ! cred || typeof cred . token !== "string" || cred . token === "" ) {
15891648 clearStatus ( id )
@@ -1594,6 +1653,8 @@ export async function revalidateResponse(rawBody: string, deps: MutationDeps = {
15941653 try {
15951654 if ( id === "slack" ) probe = await probeSlack ( cred . token , deps . fetchImpl )
15961655 else if ( id === "github" ) probe = await probeGithub ( cred . token , deps . fetchImpl )
1656+ else if ( id === "google" ) probe = await probeGoogle ( cred . token , deps . fetchImpl )
1657+ else if ( id === "google-drive" ) probe = await probeGoogleDrive ( cred . token , deps . fetchImpl )
15971658 else probe = await probeLinear ( cred . token , deps . fetchImpl )
15981659 } finally {
15991660 inflightOverlay . delete ( id )
0 commit comments