-
Notifications
You must be signed in to change notification settings - Fork 0
feat(connections): add Google + Google Drive connectors, make Harmoniqs Cloud yellow #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,17 +68,19 @@ export interface ConnectionStatus { | |
| icon?: string | ||
| /** #327: display name from registry */ | ||
| name?: string | ||
| /** auth methods advertised to the UI — browser for google, token for others */ | ||
| auth_methods?: string[] | ||
| } | ||
|
|
||
| /** The connection cards this module serves; company-compute renders first. */ | ||
| export const CONNECTION_IDS: ConnectionType[] = ["company-compute", "pasqal-cloud", "slack", "github", "linear"] | ||
| export const CONNECTION_IDS: ConnectionType[] = ["company-compute", "pasqal-cloud", "slack", "github", "linear", "google", "google-drive"] | ||
|
|
||
| // --- Registry (issue #327): formalized built-in catalog with logos + custom --- | ||
|
|
||
| /** Inline SVG icons — full-color brand marks, 18×18 with explicit fills (not currentColor). */ | ||
| export const CONNECTION_ICONS: Record<string, string> = { | ||
| "company-compute": | ||
| '<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>', | ||
| '<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>', | ||
| "pasqal-cloud": | ||
| '<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>', | ||
| slack: | ||
|
|
@@ -87,14 +89,18 @@ export const CONNECTION_ICONS: Record<string, string> = { | |
| '<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>', | ||
| linear: | ||
| '<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>', | ||
| google: | ||
| '<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>', | ||
| "google-drive": | ||
| '<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>', | ||
| } | ||
|
|
||
| export interface ConnectionEntry { | ||
| id: string | ||
| kind: "built-in" | "custom" | ||
| name: string | ||
| icon: { kind: "svg"; svg: string } | { kind: "letter"; letter: string } | ||
| validator: "company-compute" | "pasqal" | "slack" | "github" | "linear" | "none" | ||
| validator: "company-compute" | "pasqal" | "slack" | "github" | "linear" | "google" | "google-drive" | "none" | ||
| authShape: "base-url-token" | "token-only" | "pasqal-credentials" | ||
| url?: string | ||
| } | ||
|
|
@@ -140,6 +146,22 @@ export const BUILT_IN_CATALOG: ConnectionEntry[] = [ | |
| validator: "linear", | ||
| authShape: "token-only", | ||
| }, | ||
| { | ||
| id: "google", | ||
| kind: "built-in", | ||
| name: "Google", | ||
| icon: { kind: "svg", svg: CONNECTION_ICONS["google"] }, | ||
| validator: "google", | ||
| authShape: "token-only", | ||
| }, | ||
| { | ||
| id: "google-drive", | ||
| kind: "built-in", | ||
| name: "Google Drive", | ||
| icon: { kind: "svg", svg: CONNECTION_ICONS["google-drive"] }, | ||
| validator: "google-drive", | ||
| authShape: "token-only", | ||
| }, | ||
| ] | ||
|
|
||
| export function getBuiltInEntry(id: string): ConnectionEntry | undefined { | ||
|
|
@@ -414,6 +436,7 @@ function renderStatus( | |
| if (icon) out.icon = icon | ||
| const name = nameForId(id) | ||
| if (name) out.name = name | ||
| if (id === "google" || id === "google-drive") out.auth_methods = ["browser"] | ||
| return out | ||
| } | ||
| let state: ConnectionState | ||
|
|
@@ -448,6 +471,7 @@ function renderStatus( | |
| if (icon) out.icon = icon | ||
| const name = nameForId(id) | ||
| if (name) out.name = name | ||
| if (id === "google" || id === "google-drive") out.auth_methods = ["browser"] | ||
| return out | ||
| } | ||
|
|
||
|
|
@@ -596,7 +620,8 @@ function kickStaleRevalidations(body: string, deps: { fetchImpl?: FetchImpl; pas | |
| try { | ||
| if (id === "company-compute") await backgroundRevalidateCompanyCompute(deps) | ||
| else if (id === "pasqal-cloud") await backgroundRevalidatePasqal(deps) | ||
| else if (id === "slack" || id === "github" || id === "linear") await backgroundRevalidateToken(id, deps) | ||
| else if (id === "slack" || id === "github" || id === "linear" || id === "google" || id === "google-drive") | ||
| await backgroundRevalidateToken(id, deps) | ||
| } catch { | ||
| // background refresh must never surface trouble; the next GET retries | ||
| } | ||
|
|
@@ -680,6 +705,8 @@ async function backgroundRevalidateToken(id: ConnectionType, deps: { fetchImpl?: | |
| let probe: ProbeResult | ||
| if (id === "slack") probe = await probeSlack(cred.token, deps.fetchImpl) | ||
| else if (id === "github") probe = await probeGithub(cred.token, deps.fetchImpl) | ||
| else if (id === "google") probe = await probeGoogle(cred.token, deps.fetchImpl) | ||
| else if (id === "google-drive") probe = await probeGoogleDrive(cred.token, deps.fetchImpl) | ||
| else probe = await probeLinear(cred.token, deps.fetchImpl) | ||
| const existing = whitelistPersisted(readCacheFile(connectionsFile())[id]) | ||
| if (probe.outcome === "unreachable") { | ||
|
|
@@ -830,6 +857,36 @@ export async function probeLinear(token: string, fetchImpl: FetchImpl = fetch): | |
| return { outcome: "unreachable" } | ||
| } | ||
|
|
||
| export async function probeGoogle(token: string, fetchImpl: FetchImpl = fetch): Promise<ProbeResult> { | ||
| let response: { status: number; json?: () => Promise<unknown> } | ||
| try { | ||
| response = await fetchImpl("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + encodeURIComponent(token), { | ||
| method: "GET", | ||
| headers: {}, | ||
| }) | ||
| } catch { | ||
| return { outcome: "unreachable" } | ||
| } | ||
| if (response.status === 200) return { outcome: "valid" } | ||
| if (response.status === 400 || response.status === 401 || response.status === 403) return { outcome: "invalid" } | ||
| return { outcome: "unreachable" } | ||
| } | ||
|
|
||
| export async function probeGoogleDrive(token: string, fetchImpl: FetchImpl = fetch): Promise<ProbeResult> { | ||
| let response: { status: number; json?: () => Promise<unknown> } | ||
| try { | ||
| response = await fetchImpl("https://www.googleapis.com/drive/v3/about?fields=user", { | ||
| method: "GET", | ||
| headers: { authorization: `Bearer ${token}` }, | ||
| }) | ||
| } catch { | ||
| return { outcome: "unreachable" } | ||
| } | ||
| if (response.status === 200) return { outcome: "valid" } | ||
| if (response.status === 400 || response.status === 401 || response.status === 403) return { outcome: "invalid" } | ||
| return { outcome: "unreachable" } | ||
|
Comment on lines
+885
to
+887
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
file="packages/opencode/src/server/amicode/connections.ts"
printf '%s\n' '--- target function and nearby code ---'
sed -n '820,900p' "$file"
printf '%s\n' '--- probeGoogleDrive references ---'
rg -n -C 4 'probeGoogleDrive|outcome: "invalid"|outcome: "unreachable"' "$file"
printf '%s\n' '--- relevant dispatch branches ---'
sed -n '590,635p' "$file"
sed -n '680,715p' "$file"
sed -n '1265,1295p' "$file"
sed -n '1635,1665p' "$file"Repository: harmoniqs/opencode Length of output: 16571 🌐 Web query:
💡 Result: To handle errors in the Google Drive API, you should inspect the HTTP status code and the JSON response body, which typically contains a reason field providing specific details about the error [1][2]. HTTP 401: Unauthorized This error indicates that the request does not contain a valid access token [1][2]. - Common Cause: An expired or invalid access token, or missing authorization for the required scopes [1][2]. - Resolution: Refresh the access token using a valid refresh token. If that fails, direct the user through the OAuth flow to obtain new credentials [1][2]. HTTP 403: Forbidden This error means the request was understood, but the server refuses to authorize it [1][2]. You should evaluate the reason field in the response JSON to determine the specific cause [1][2]. - Common Reasons: - Usage Limits/Rate Limits: You have exceeded the API quota or rate limits. If this occurs, implement an exponential backoff algorithm to retry the request with increasing delays [3][2]. - Insufficient Permissions: The authenticated user or service account lacks the necessary permissions for the resource [1][2]. - Quota/Usage Caps: The application owner may have set a specific usage cap (e.g., Queries per day). Review and remove these caps in the Google Cloud console if necessary [1][2]. - Storage Quota: The user has reached their Drive storage limit [4]. If using a service account, ensure you are interacting with shared drives appropriately, as service accounts cannot own files [4]. General Error Handling Strategy - Exponential Backoff: For time-based quota errors (e.g., 403 rate limit exceeded or 429 Too Many Requests), use a truncated exponential backoff strategy [3]. This involves retrying the request while exponentially increasing wait times between attempts until the request succeeds [3]. - Inspection: Always parse the error object returned by the API, which often provides structured information (e.g., domain, reason, location) that helps distinguish between authentication issues, permission problems, and quota exhaustion [1][2]. Citations:
Do not classify every Google Drive Google Drive uses 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // --- Pasqal validator spawn (amicode#169 / parent #159; #164 contract) --- | ||
| // The fork never sees SDK internals: the validator's one-line JSON + exit-code | ||
| // contract is the ENTIRE interface. Inputs ride env variables ONLY — never | ||
|
|
@@ -1179,7 +1236,7 @@ export async function submitCredentialResponse(rawBody: string, deps: MutationDe | |
| const body = parseMutationBody(rawBody) | ||
| if (!body) return synthesizeConnection("bad_request", "body must be JSON with an id and that id's credential fields") | ||
| if (body.id === "pasqal-cloud") return submitPasqalCredential(body, deps) | ||
| if (body.id === "slack" || body.id === "github" || body.id === "linear") { | ||
| if (body.id === "slack" || body.id === "github" || body.id === "linear" || body.id === "google" || body.id === "google-drive") { | ||
| return submitTokenCredential(body.id as ConnectionType, body, deps) | ||
| } | ||
| if (body.id !== "company-compute") { | ||
|
|
@@ -1228,6 +1285,8 @@ async function submitTokenCredential(id: ConnectionType, body: MutationBody, dep | |
| try { | ||
| if (id === "slack") probe = await probeSlack(token, deps.fetchImpl) | ||
| else if (id === "github") probe = await probeGithub(token, deps.fetchImpl) | ||
| else if (id === "google") probe = await probeGoogle(token, deps.fetchImpl) | ||
| else if (id === "google-drive") probe = await probeGoogleDrive(token, deps.fetchImpl) | ||
| else probe = await probeLinear(token, deps.fetchImpl) | ||
| } finally { | ||
| inflightOverlay.delete(id) | ||
|
|
@@ -1583,7 +1642,7 @@ export async function revalidateResponse(rawBody: string, deps: MutationDeps = { | |
| const id = parseIdBody(rawBody) | ||
| if (!id) return synthesizeConnection("bad_request", "body must be JSON {id} with a known connection id") | ||
| if (id === "pasqal-cloud") return revalidatePasqal(deps) | ||
| if (id === "slack" || id === "github" || id === "linear") { | ||
| if (id === "slack" || id === "github" || id === "linear" || id === "google" || id === "google-drive") { | ||
| const cred = readCredential(id) as { token?: string } | undefined | ||
| if (!cred || typeof cred.token !== "string" || cred.token === "") { | ||
| clearStatus(id) | ||
|
|
@@ -1594,6 +1653,8 @@ export async function revalidateResponse(rawBody: string, deps: MutationDeps = { | |
| try { | ||
| if (id === "slack") probe = await probeSlack(cred.token, deps.fetchImpl) | ||
| else if (id === "github") probe = await probeGithub(cred.token, deps.fetchImpl) | ||
| else if (id === "google") probe = await probeGoogle(cred.token, deps.fetchImpl) | ||
| else if (id === "google-drive") probe = await probeGoogleDrive(cred.token, deps.fetchImpl) | ||
| else probe = await probeLinear(cred.token, deps.fetchImpl) | ||
| } finally { | ||
| inflightOverlay.delete(id) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ import { randomBytes } from "node:crypto" | |
| import { homedir } from "node:os" | ||
| import path from "node:path" | ||
|
|
||
| export type BuiltInConnectionType = "company-compute" | "pasqal-cloud" | "slack" | "github" | "linear" | ||
| export type BuiltInConnectionType = "company-compute" | "pasqal-cloud" | "slack" | "github" | "linear" | "google" | "google-drive" | ||
| export type ConnectionType = BuiltInConnectionType | (string & {}) | ||
|
|
||
| /** FROZEN byte shape — every existing CLI consumer parses this unchanged. */ | ||
|
|
@@ -61,6 +61,16 @@ export function linearFile(): string { | |
| if (env && env.trim() !== "") return env | ||
| return path.join(homedir(), ".amico", "linear.json") | ||
| } | ||
| export function googleFile(): string { | ||
| const env = process.env.AMICO_GOOGLE_FILE | ||
| if (env && env.trim() !== "") return env | ||
| return path.join(homedir(), ".amico", "google.json") | ||
| } | ||
| export function googleDriveFile(): string { | ||
| const env = process.env.AMICO_GOOGLE_DRIVE_FILE | ||
| if (env && env.trim() !== "") return env | ||
| return path.join(homedir(), ".amico", "google-drive.json") | ||
| } | ||
|
|
||
| // --- poison guard: writing any object carrying a password-like key through | ||
| // this seam must be impossible. The encoders below are allowlist-only (they | ||
|
|
@@ -169,6 +179,36 @@ const BACKENDS: Record<string, Backend> = { | |
| return { token: d.token } | ||
| }, | ||
| }, | ||
| google: { | ||
| file: googleFile, | ||
| encode(value) { | ||
| rejectPoisonKeys(value) | ||
| const token = typeof value.token === "string" ? value.token.trim() : "" | ||
| if (token === "") throw new Error('google credential needs non-empty "token"') | ||
| return JSON.stringify({ token }, null, 2) + "\n" | ||
| }, | ||
| decode(raw) { | ||
| if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined | ||
| const d = raw as Record<string, unknown> | ||
| if (typeof d.token !== "string" || d.token === "") return undefined | ||
| return { token: d.token } | ||
| }, | ||
|
Comment on lines
+190
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject whitespace-only tokens during decoding.
Trim the decoded value and reject it when the result is empty. Proposed fix- if (typeof d.token !== "string" || d.token === "") return undefined
- return { token: d.token }
+ const token = typeof d.token === "string" ? d.token.trim() : ""
+ if (token === "") return undefined
+ return { token }Also applies to: 205-210 🤖 Prompt for AI Agents |
||
| }, | ||
| "google-drive": { | ||
| file: googleDriveFile, | ||
| encode(value) { | ||
| rejectPoisonKeys(value) | ||
| const token = typeof value.token === "string" ? value.token.trim() : "" | ||
| if (token === "") throw new Error('google-drive credential needs non-empty "token"') | ||
| return JSON.stringify({ token }, null, 2) + "\n" | ||
| }, | ||
| decode(raw) { | ||
| if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined | ||
| const d = raw as Record<string, unknown> | ||
| if (typeof d.token !== "string" || d.token === "") return undefined | ||
| return { token: d.token } | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| // --- atomic 0600-at-birth writer --- | ||
|
|
@@ -205,6 +245,8 @@ export function readCredential(type: "pasqal-cloud"): PasqalCredential | undefin | |
| export function readCredential(type: "slack"): TokenCredential | undefined | ||
| export function readCredential(type: "github"): TokenCredential | undefined | ||
| export function readCredential(type: "linear"): TokenCredential | undefined | ||
| export function readCredential(type: "google"): TokenCredential | undefined | ||
| export function readCredential(type: "google-drive"): TokenCredential | undefined | ||
| export function readCredential(type: string): Credential | undefined | ||
| export function readCredential(type: ConnectionType): Credential | undefined | ||
| export function readCredential(type: ConnectionType): Credential | undefined { | ||
|
|
@@ -226,6 +268,8 @@ export function writeCredential(type: "pasqal-cloud", value: PasqalCredential, h | |
| export function writeCredential(type: "slack", value: TokenCredential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: "github", value: TokenCredential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: "linear", value: TokenCredential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: "google", value: TokenCredential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: "google-drive", value: TokenCredential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: string, value: Credential, hooks?: WriteHooks): void | ||
| export function writeCredential(type: ConnectionType, value: Credential, hooks?: WriteHooks): void { | ||
| const backend = BACKENDS[type] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: harmoniqs/opencode
Length of output: 420
🌐 Web query:
Google OAuth 2.0 tokeninfo endpoint Authorization Bearer header access_token query parameter documentation💡 Result:
The Google OAuth 2.0 tokeninfo endpoint is a diagnostic tool used to introspect or validate access tokens and ID tokens [1][2][3]. It is officially hosted at the base URL: https://oauth2.googleapis.com/tokeninfo [1][4]. Documentation and Usage: - Endpoint: https://oauth2.googleapis.com/tokeninfo [1][4] - Supported Methods: GET or POST [1][2]. - Primary Purpose: Introspection/debugging for access tokens and ID tokens [1][2][3]. It is not intended for production-level, high-volume validation, as requests may be throttled [2]. Request Parameters: The tokeninfo endpoint accepts the token to be validated as a query parameter [2][3]. - For Access Tokens: Pass the token using the access_token query parameter [3][5]. Example: https://oauth2.googleapis.com/tokeninfo?access_token=YOUR_ACCESS_TOKEN [3] - For ID Tokens: Pass the token using the id_token query parameter [1][2]. Example: https://oauth2.googleapis.com/tokeninfo?id_token=YOUR_ID_TOKEN [2] Authorization Bearer Header: While Google APIs generally prefer tokens to be sent via the Authorization: Bearer header for resource access [6], the tokeninfo endpoint explicitly expects the token to be provided as a query string parameter (e.g.,?access_token=... or?id_token=...) [2][3][7]. Sending the token in the Authorization header is generally not the documented or supported method for this specific diagnostic endpoint [5][8][7]. Note on Security: Because sending tokens as URI query parameters can lead to them being inadvertently logged by servers or proxies, this method should only be used for debugging purposes [2][6]. For production authentication, prefer standard JWT validation techniques where the application validates the token locally using Google's published public keys [2].
Citations:
🏁 Script executed:
Repository: harmoniqs/opencode
Length of output: 50374
Keep the access token out of the request URL.
Google’s
tokeninfoendpoint expectsaccess_tokenas a query parameter and does not document bearer-header authentication. Use a Google resource endpoint that acceptsAuthorization: Bearerand supports the required scopes, or use a validation design that does not place the token in the URL.🤖 Prompt for AI Agents