@coderstudiolabs/google-chat-space-finder
v1.0.1
Published
Resolve a Google Chat DM space ID from an email address. Handles OAuth2, People API, and Directory API lookups so you can deep-link, send programmatic messages, or build integrations without touching the Google Chat UI.
Downloads
229
Maintainers
Readme
google-chat-space-finder
Resolve a Google Chat DM space ID from an email address. Handles OAuth2, People API, and Directory API lookups so you can deep-link into conversations, send programmatic messages, or build integrations — without ever touching the Google Chat UI.
Works as a CLI tool or a Node.js library.
Prerequisites
1. Enable Google APIs
Go to Google Cloud Console → APIs & Services > Library and enable the following:
| API | Purpose | | --- | ------- | | Google Chat API | Look up and create DM spaces | | People API | Resolve an email to a Google account ID via the org directory |
Note: You do NOT need to be a Google Workspace admin. The People API
searchDirectoryPeopleendpoint works for any Workspace user within your org.
2. Configure the OAuth Consent Screen
Go to APIs & Services > OAuth consent screen:
- User type: Internal (recommended — limits access to your org)
- Add the following scopes:
https://www.googleapis.com/auth/chat.spaces.readonlyhttps://www.googleapis.com/auth/chat.spaceshttps://www.googleapis.com/auth/directory.readonly
3. Create OAuth 2.0 Credentials
Go to APIs & Services > Credentials > Create Credentials > OAuth client ID:
- Application type: Desktop app
- After creation, copy the Client ID and Client Secret
- Add
http://localhost:3003/callback(or your custom port) as an authorized redirect URI
Installation
npm install -g @coderstudiolabs/google-chat-space-finderCopy the example env file and fill in your credentials:
cp .env.example .envCLI Usage
export GOOGLE_CLIENT_ID=your_client_id
export GOOGLE_CLIENT_SECRET=your_client_secret
# Interactive
chat-space
# Pass email as argument (scriptable)
chat-space [email protected]Environment Variables
| Variable | Required | Default | Description |
| --------------------------- | -------- | ---------------------------------- | --------------------------------------------------------------------------- |
| AUTH_GOOGLE_CLIENT_ID | Yes | — | OAuth 2.0 client ID (also accepted: GOOGLE_CLIENT_ID) |
| AUTH_GOOGLE_CLIENT_SECRET | Yes | — | OAuth 2.0 client secret (also accepted: GOOGLE_CLIENT_SECRET) |
| AUTH_GOOGLE_CALLBACK_URL | No | http://localhost:3003/callback | OAuth redirect URI (also accepted: GOOGLE_OAUTH_REDIRECT_URI) |
| AUTH_GOOGLE_ALLOWED_DOMAINS | No | — | Comma-separated list of allowed email domains, e.g. example.com,acme.com. Rejects emails outside these domains. (also accepted: GOOGLE_ALLOWED_DOMAINS) |
| GOOGLE_OAUTH_PORT | No | 3003 | Local port for OAuth callback |
Usage in JavaScript Projects
Important: This package uses the Google OAuth flow, which requires a server-side component to keep your
GOOGLE_CLIENT_SECRETsafe. Never expose your client secret in frontend or mobile code. The recommended pattern for all UI frameworks is:
- Your backend handles authentication and space ID resolution using this package.
- Your frontend/mobile app calls your backend API to get the space ID or URLs.
Next.js
Install as a server-side dependency:
npm install @coderstudiolabs/google-chat-space-finder1. Add environment variables to .env.local:
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret2. Create an API route that resolves the space ID:
// app/api/chat-space/route.js (Next.js App Router)
import { resolveUserId, getDMSpace, buildSpaceUrls, refreshAccessToken } from '@coderstudiolabs/google-chat-space-finder';
export async function POST(request) {
const { email, accessToken, refreshToken } = await request.json();
if (!email || !accessToken) {
return Response.json({ error: 'email and accessToken are required' }, { status: 400 });
}
try {
// Refresh the token if needed before making API calls
let token = accessToken;
if (refreshToken) {
const refreshed = await refreshAccessToken({
refreshToken,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
});
token = refreshed.access_token;
}
const userId = await resolveUserId(email, token);
const space = await getDMSpace(userId, token);
const spaceId = space.name.replace('spaces/', '');
const { webUrl, androidUrl } = buildSpaceUrls(spaceId);
return Response.json({ spaceId, webUrl, androidUrl });
} catch (err) {
return Response.json({ error: err.message }, { status: 500 });
}
}3. Create an API route for the OAuth callback:
// app/api/auth/google/callback/route.js
import { exchangeCodeForTokens } from '@coderstudiolabs/google-chat-space-finder';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (!code) {
return Response.json({ error: 'Missing authorization code' }, { status: 400 });
}
const tokens = await exchangeCodeForTokens({
code,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: process.env.GOOGLE_OAUTH_REDIRECT_URI,
});
// Store tokens securely (session, database, etc.) — never send refresh_token to the client
// Example: set an HTTP-only cookie or save to your database
return Response.json({ access_token: tokens.access_token, expires_in: tokens.expires_in });
}4. Call from your React component:
// components/SendMessageButton.jsx
'use client';
export default function SendMessageButton({ email }) {
async function handleClick() {
const res = await fetch('/api/chat-space', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, accessToken: 'stored_access_token' }),
});
const { webUrl, error } = await res.json();
if (error) return alert(error);
window.open(webUrl, '_blank');
}
return <button onClick={handleClick}>Send Message via Google Chat</button>;
}React (Vite / Create React App)
Same pattern as Next.js — you need a separate backend. React itself runs in the browser and cannot securely hold your client secret.
Backend (Express):
npm install @coderstudiolabs/google-chat-space-finder express cors// server.js
const express = require('express');
const cors = require('cors');
const { resolveUserId, getDMSpace, buildSpaceUrls } = require('@coderstudiolabs/google-chat-space-finder');
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/chat-space', async (req, res) => {
const { email, accessToken } = req.body;
try {
const userId = await resolveUserId(email, accessToken);
const space = await getDMSpace(userId, accessToken);
const spaceId = space.name.replace('spaces/', '');
res.json(buildSpaceUrls(spaceId));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(4000, () => console.log('Server running on http://localhost:4000'));React component:
// SendMessageButton.jsx
import { useState } from 'react';
export default function SendMessageButton({ email }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
try {
const res = await fetch('http://localhost:4000/api/chat-space', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, accessToken: 'stored_access_token' }),
});
const { webUrl, error } = await res.json();
if (error) throw new Error(error);
window.open(webUrl, '_blank');
} finally {
setLoading(false);
}
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Loading...' : 'Send Message via Google Chat'}
</button>
);
}React Native
React Native cannot use this package directly — it runs on mobile and has no access to Node.js APIs. The space ID lookup must go through your backend.
Backend: Use the Express example above (or any server), then call it from React Native:
// SendMessageButton.jsx
import { Linking, Platform, TouchableOpacity, Text } from 'react-native';
async function openGoogleChat(email) {
// 1. Ask your backend for the space URLs
const res = await fetch('https://your-api.com/api/chat-space', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, accessToken: 'stored_access_token' }),
});
const { webUrl, androidUrl, error } = await res.json();
if (error) throw new Error(error);
// 2. Open the correct URL for the platform
const url = Platform.OS === 'android' ? androidUrl : webUrl;
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
// Fallback to web if the app is not installed
await Linking.openURL(webUrl);
}
}
export default function SendMessageButton({ email }) {
return (
<TouchableOpacity onPress={() => openGoogleChat(email)}>
<Text>Send Message via Google Chat</Text>
</TouchableOpacity>
);
}Android note: The
androidUrlis an intent URL that opens the Google Chat app directly. On iOS,webUrlopens Google Chat in the browser or the app via universal links.
Plain Node.js / Backend Scripts
const { findDMSpace } = require('@coderstudiolabs/google-chat-space-finder');
const result = await findDMSpace({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
email: '[email protected]',
});
console.log(result.spaceId); // e.g. "pMtH38AAAAE"
console.log(result.webUrl); // https://chat.google.com/room/pMtH38AAAAE
console.log(result.androidUrl); // intent://...
console.log(result.tokens); // { access_token, refresh_token, expires_in, ... }Domain Filtering (Optional)
Restrict lookups to specific email domains. Any email outside the allowed list is rejected before any API call is made.
CLI — set via environment variable:
# Single domain
AUTH_GOOGLE_ALLOWED_DOMAINS=example.com
# Multiple domains (comma-separated)
AUTH_GOOGLE_ALLOWED_DOMAINS=example.com,acme.comOr in your .env file:
AUTH_GOOGLE_ALLOWED_DOMAINS=example.comWhen set:
- Passing email as a CLI argument rejects it immediately, before OAuth
- Entering email interactively rejects it after OAuth, before any API calls
Library — pass allowedDomains array:
const result = await findDMSpace({
clientId: process.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,
email: '[email protected]',
allowedDomains: ['example.com', 'acme.com'],
});
// throws: Email "[email protected]" is not from an allowed domain. Allowed: example.com, acme.comIf allowedDomains is not set or is an empty array, all domains are accepted.
Token Refresh
The tokens object returned by findDMSpace and exchangeCodeForTokens includes a refresh_token. Save it and use it to get a new access token without re-authenticating:
const { refreshAccessToken } = require('@coderstudiolabs/google-chat-space-finder');
const newTokens = await refreshAccessToken({
refreshToken: savedRefreshToken,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
});API Reference
| Export | Description |
| ------------------------- | ------------------------------------------------------------------ |
| findDMSpace(opts) | Full auth → resolve → lookup flow |
| buildAuthUrl() | Build the Google OAuth2 authorization URL |
| waitForAuthCode(port) | Start local server and wait for OAuth callback |
| exchangeCodeForTokens() | Exchange auth code for access/refresh tokens |
| refreshAccessToken() | Get a new access token using a refresh token |
| resolveUserId() | Resolve a Google account ID from an email address |
| getDMSpace() | Find or create a DM space by Google account ID |
| buildSpaceUrls() | Build web and Android deep-link URLs for a space |
| isValidEmail() | Validate an email address format |
| isAllowedDomain() | Check if an email belongs to one of the allowed domains |
License
MIT
