{"_rev":"4-5a72c86f0eb2730d27e206c8628b18eb","time":{"created":"2026-04-15T01:36:44.738Z","modified":"2026-04-15T01:36:45.271Z","1.0.0":"2026-04-11T10:42:33.380Z","1.0.1":"2026-04-15T01:36:45.019Z"},"_id":"@coderstudiolabs/google-chat-space-finder","name":"@coderstudiolabs/google-chat-space-finder","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@coderstudiolabs/google-chat-space-finder","version":"1.0.1","description":"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.","main":"src/index.js","bin":{"chat-space":"src/cli.js"},"scripts":{"start":"node src/cli.js"},"keywords":["google-chat","space-id","dm","oauth","google-workspace"],"author":{"name":"rcdelacruz"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/rcdelacruz/google-chat-space-finder.git"},"bugs":{"url":"https://github.com/rcdelacruz/google-chat-space-finder/issues"},"homepage":"https://github.com/rcdelacruz/google-chat-space-finder#readme","engines":{"node":">=18"},"dependencies":{"dotenv":"^17.4.1"},"_id":"@coderstudiolabs/google-chat-space-finder@1.0.1","gitHead":"ba00d68c03c13489013c6b3d69c1262f9a14dbd0","_nodeVersion":"22.17.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-75KqxL9s/SpDDCWzxxi1c62tsy3TzG8gB6Lqy+DfvVvqk/JquuYSl+sEi+msmgvlNNUjjbGf/kEF+Hb6gaOJmg==","shasum":"6b6d3d6b260a9a3abf29647c37f7babb4bdbe164","tarball":"https://registry.npmjs.org/@coderstudiolabs/google-chat-space-finder/-/google-chat-space-finder-1.0.1.tgz","fileCount":6,"unpackedSize":40106,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIB6h/tRGrL+Gpj2Rtp/ZyydLgQHLok5o+Mm+9dmLR3l0AiAOlYCbWr4c/w9Lk58qTap99N74Oyw13UpPasR1FaFhCg=="}]},"_npmUser":{"name":"brewery-apps","email":"rcdelacruz@proton.me"},"directories":{},"maintainers":[{"name":"brewery-apps","email":"rcdelacruz@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/google-chat-space-finder_1.0.1_1776217004868_0.13665385038834144"},"_hasShrinkwrap":false}},"maintainers":[{"name":"brewery-apps","email":"rcdelacruz@proton.me"}],"description":"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.","homepage":"https://github.com/rcdelacruz/google-chat-space-finder#readme","keywords":["google-chat","space-id","dm","oauth","google-workspace"],"repository":{"type":"git","url":"git+https://github.com/rcdelacruz/google-chat-space-finder.git"},"author":{"name":"rcdelacruz"},"bugs":{"url":"https://github.com/rcdelacruz/google-chat-space-finder/issues"},"license":"MIT","readme":"# google-chat-space-finder\n\nResolve 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.\n\nWorks as a CLI tool or a Node.js library.\n\n## Prerequisites\n\n### 1. Enable Google APIs\n\nGo to [Google Cloud Console](https://console.cloud.google.com/) → **APIs & Services > Library** and enable the following:\n\n| API | Purpose |\n| --- | ------- |\n| **Google Chat API** | Look up and create DM spaces |\n| **People API** | Resolve an email to a Google account ID via the org directory |\n\n> **Note:** You do NOT need to be a Google Workspace admin. The People API `searchDirectoryPeople` endpoint works for any Workspace user within your org.\n\n### 2. Configure the OAuth Consent Screen\n\nGo to **APIs & Services > OAuth consent screen**:\n\n- User type: **Internal** (recommended — limits access to your org)\n- Add the following scopes:\n  - `https://www.googleapis.com/auth/chat.spaces.readonly`\n  - `https://www.googleapis.com/auth/chat.spaces`\n  - `https://www.googleapis.com/auth/directory.readonly`\n\n### 3. Create OAuth 2.0 Credentials\n\nGo to **APIs & Services > Credentials > Create Credentials > OAuth client ID**:\n\n- Application type: **Desktop app**\n- After creation, copy the **Client ID** and **Client Secret**\n- Add `http://localhost:3003/callback` (or your custom port) as an authorized redirect URI\n\n## Installation\n\n```sh\nnpm install -g @coderstudiolabs/google-chat-space-finder\n```\n\nCopy the example env file and fill in your credentials:\n\n```sh\ncp .env.example .env\n```\n\n---\n\n## CLI Usage\n\n```sh\nexport GOOGLE_CLIENT_ID=your_client_id\nexport GOOGLE_CLIENT_SECRET=your_client_secret\n\n# Interactive\nchat-space\n\n# Pass email as argument (scriptable)\nchat-space user@example.com\n```\n\n### Environment Variables\n\n| Variable                    | Required | Default                            | Description                                                                 |\n| --------------------------- | -------- | ---------------------------------- | --------------------------------------------------------------------------- |\n| `AUTH_GOOGLE_CLIENT_ID`     | Yes      | —                                  | OAuth 2.0 client ID (also accepted: `GOOGLE_CLIENT_ID`)                     |\n| `AUTH_GOOGLE_CLIENT_SECRET` | Yes      | —                                  | OAuth 2.0 client secret (also accepted: `GOOGLE_CLIENT_SECRET`)             |\n| `AUTH_GOOGLE_CALLBACK_URL`  | No       | `http://localhost:3003/callback`   | OAuth redirect URI (also accepted: `GOOGLE_OAUTH_REDIRECT_URI`)             |\n| `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`) |\n| `GOOGLE_OAUTH_PORT`         | No       | `3003`                             | Local port for OAuth callback                                               |\n\n---\n\n## Usage in JavaScript Projects\n\n> **Important:** This package uses the Google OAuth flow, which requires a server-side component to keep your `GOOGLE_CLIENT_SECRET` safe. **Never expose your client secret in frontend or mobile code.** The recommended pattern for all UI frameworks is:\n>\n> 1. Your backend handles authentication and space ID resolution using this package.\n> 2. Your frontend/mobile app calls your backend API to get the space ID or URLs.\n\n---\n\n### Next.js\n\nInstall as a server-side dependency:\n\n```sh\nnpm install @coderstudiolabs/google-chat-space-finder\n```\n\n**1. Add environment variables to `.env.local`:**\n\n```env\nGOOGLE_CLIENT_ID=your_client_id\nGOOGLE_CLIENT_SECRET=your_client_secret\n```\n\n**2. Create an API route that resolves the space ID:**\n\n```js\n// app/api/chat-space/route.js  (Next.js App Router)\nimport { resolveUserId, getDMSpace, buildSpaceUrls, refreshAccessToken } from '@coderstudiolabs/google-chat-space-finder';\n\nexport async function POST(request) {\n  const { email, accessToken, refreshToken } = await request.json();\n\n  if (!email || !accessToken) {\n    return Response.json({ error: 'email and accessToken are required' }, { status: 400 });\n  }\n\n  try {\n    // Refresh the token if needed before making API calls\n    let token = accessToken;\n    if (refreshToken) {\n      const refreshed = await refreshAccessToken({\n        refreshToken,\n        clientId: process.env.GOOGLE_CLIENT_ID,\n        clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n      });\n      token = refreshed.access_token;\n    }\n\n    const userId = await resolveUserId(email, token);\n    const space = await getDMSpace(userId, token);\n    const spaceId = space.name.replace('spaces/', '');\n    const { webUrl, androidUrl } = buildSpaceUrls(spaceId);\n\n    return Response.json({ spaceId, webUrl, androidUrl });\n  } catch (err) {\n    return Response.json({ error: err.message }, { status: 500 });\n  }\n}\n```\n\n**3. Create an API route for the OAuth callback:**\n\n```js\n// app/api/auth/google/callback/route.js\nimport { exchangeCodeForTokens } from '@coderstudiolabs/google-chat-space-finder';\n\nexport async function GET(request) {\n  const { searchParams } = new URL(request.url);\n  const code = searchParams.get('code');\n\n  if (!code) {\n    return Response.json({ error: 'Missing authorization code' }, { status: 400 });\n  }\n\n  const tokens = await exchangeCodeForTokens({\n    code,\n    clientId: process.env.GOOGLE_CLIENT_ID,\n    clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n    redirectUri: process.env.GOOGLE_OAUTH_REDIRECT_URI,\n  });\n\n  // Store tokens securely (session, database, etc.) — never send refresh_token to the client\n  // Example: set an HTTP-only cookie or save to your database\n  return Response.json({ access_token: tokens.access_token, expires_in: tokens.expires_in });\n}\n```\n\n**4. Call from your React component:**\n\n```jsx\n// components/SendMessageButton.jsx\n'use client';\n\nexport default function SendMessageButton({ email }) {\n  async function handleClick() {\n    const res = await fetch('/api/chat-space', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ email, accessToken: 'stored_access_token' }),\n    });\n    const { webUrl, error } = await res.json();\n    if (error) return alert(error);\n    window.open(webUrl, '_blank');\n  }\n\n  return <button onClick={handleClick}>Send Message via Google Chat</button>;\n}\n```\n\n---\n\n### React (Vite / Create React App)\n\nSame pattern as Next.js — you need a separate backend. React itself runs in the browser and cannot securely hold your client secret.\n\n**Backend (Express):**\n\n```sh\nnpm install @coderstudiolabs/google-chat-space-finder express cors\n```\n\n```js\n// server.js\nconst express = require('express');\nconst cors = require('cors');\nconst { resolveUserId, getDMSpace, buildSpaceUrls } = require('@coderstudiolabs/google-chat-space-finder');\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\napp.post('/api/chat-space', async (req, res) => {\n  const { email, accessToken } = req.body;\n  try {\n    const userId = await resolveUserId(email, accessToken);\n    const space = await getDMSpace(userId, accessToken);\n    const spaceId = space.name.replace('spaces/', '');\n    res.json(buildSpaceUrls(spaceId));\n  } catch (err) {\n    res.status(500).json({ error: err.message });\n  }\n});\n\napp.listen(4000, () => console.log('Server running on http://localhost:4000'));\n```\n\n**React component:**\n\n```jsx\n// SendMessageButton.jsx\nimport { useState } from 'react';\n\nexport default function SendMessageButton({ email }) {\n  const [loading, setLoading] = useState(false);\n\n  async function handleClick() {\n    setLoading(true);\n    try {\n      const res = await fetch('http://localhost:4000/api/chat-space', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ email, accessToken: 'stored_access_token' }),\n      });\n      const { webUrl, error } = await res.json();\n      if (error) throw new Error(error);\n      window.open(webUrl, '_blank');\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  return (\n    <button onClick={handleClick} disabled={loading}>\n      {loading ? 'Loading...' : 'Send Message via Google Chat'}\n    </button>\n  );\n}\n```\n\n---\n\n### React Native\n\nReact 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.\n\n**Backend:** Use the Express example above (or any server), then call it from React Native:\n\n```jsx\n// SendMessageButton.jsx\nimport { Linking, Platform, TouchableOpacity, Text } from 'react-native';\n\nasync function openGoogleChat(email) {\n  // 1. Ask your backend for the space URLs\n  const res = await fetch('https://your-api.com/api/chat-space', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ email, accessToken: 'stored_access_token' }),\n  });\n  const { webUrl, androidUrl, error } = await res.json();\n  if (error) throw new Error(error);\n\n  // 2. Open the correct URL for the platform\n  const url = Platform.OS === 'android' ? androidUrl : webUrl;\n  const supported = await Linking.canOpenURL(url);\n\n  if (supported) {\n    await Linking.openURL(url);\n  } else {\n    // Fallback to web if the app is not installed\n    await Linking.openURL(webUrl);\n  }\n}\n\nexport default function SendMessageButton({ email }) {\n  return (\n    <TouchableOpacity onPress={() => openGoogleChat(email)}>\n      <Text>Send Message via Google Chat</Text>\n    </TouchableOpacity>\n  );\n}\n```\n\n> **Android note:** The `androidUrl` is an intent URL that opens the Google Chat app directly. On iOS, `webUrl` opens Google Chat in the browser or the app via universal links.\n\n---\n\n### Plain Node.js / Backend Scripts\n\n```js\nconst { findDMSpace } = require('@coderstudiolabs/google-chat-space-finder');\n\nconst result = await findDMSpace({\n  clientId: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  email: 'user@example.com',\n});\n\nconsole.log(result.spaceId);    // e.g. \"pMtH38AAAAE\"\nconsole.log(result.webUrl);     // https://chat.google.com/room/pMtH38AAAAE\nconsole.log(result.androidUrl); // intent://...\nconsole.log(result.tokens);     // { access_token, refresh_token, expires_in, ... }\n```\n\n### Domain Filtering (Optional)\n\nRestrict lookups to specific email domains. Any email outside the allowed list is rejected before any API call is made.\n\n**CLI — set via environment variable:**\n\n```sh\n# Single domain\nAUTH_GOOGLE_ALLOWED_DOMAINS=example.com\n\n# Multiple domains (comma-separated)\nAUTH_GOOGLE_ALLOWED_DOMAINS=example.com,acme.com\n```\n\nOr in your `.env` file:\n\n```env\nAUTH_GOOGLE_ALLOWED_DOMAINS=example.com\n```\n\nWhen set:\n- Passing email as a CLI argument rejects it **immediately**, before OAuth\n- Entering email interactively rejects it **after OAuth**, before any API calls\n\n**Library — pass `allowedDomains` array:**\n\n```js\nconst result = await findDMSpace({\n  clientId: process.env.AUTH_GOOGLE_CLIENT_ID,\n  clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET,\n  email: 'user@example.com',\n  allowedDomains: ['example.com', 'acme.com'],\n});\n// throws: Email \"user@gmail.com\" is not from an allowed domain. Allowed: example.com, acme.com\n```\n\nIf `allowedDomains` is not set or is an empty array, all domains are accepted.\n\n### Token Refresh\n\nThe `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:\n\n```js\nconst { refreshAccessToken } = require('@coderstudiolabs/google-chat-space-finder');\n\nconst newTokens = await refreshAccessToken({\n  refreshToken: savedRefreshToken,\n  clientId: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n});\n```\n\n---\n\n## API Reference\n\n| Export                    | Description                                                        |\n| ------------------------- | ------------------------------------------------------------------ |\n| `findDMSpace(opts)`       | Full auth → resolve → lookup flow                                  |\n| `buildAuthUrl()`          | Build the Google OAuth2 authorization URL                          |\n| `waitForAuthCode(port)`   | Start local server and wait for OAuth callback                     |\n| `exchangeCodeForTokens()` | Exchange auth code for access/refresh tokens                       |\n| `refreshAccessToken()`    | Get a new access token using a refresh token                       |\n| `resolveUserId()`         | Resolve a Google account ID from an email address                  |\n| `getDMSpace()`            | Find or create a DM space by Google account ID                     |\n| `buildSpaceUrls()`        | Build web and Android deep-link URLs for a space                   |\n| `isValidEmail()`          | Validate an email address format                                   |\n| `isAllowedDomain()`       | Check if an email belongs to one of the allowed domains            |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}