Restructure docs/ into architecture/, modules/, and development/ directories. Add thorough documentation for Compass Core platform and HPS Compass modules. Rewrite CLAUDE.md as a lean quick-reference that points to the full docs. Rename files to lowercase, consolidate old docs, add gotchas section. Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
10 KiB
Executable File
Google Drive Integration
The Google Drive module gives Compass users a native file browser that reads from and writes to Google Workspace. It uses domain-wide delegation via a service account, which means no per-user OAuth consent flow -- the service account impersonates workspace users directly using JWT-based authentication.
This design was chosen because construction companies typically have a Google Workspace domain and want all project documents accessible in Compass without each user having to individually authorize access. The tradeoff is that setup requires a Workspace admin to configure domain-wide delegation in the Google Admin console.
architecture overview
src/lib/google/
config.ts # encryption key, service account type, API URLs
auth/
service-account.ts # JWT creation + token exchange (Web Crypto API)
token-cache.ts # in-memory per-user token cache
client/
drive-client.ts # REST API v3 wrapper with retry + rate limiting
types.ts # DriveFile, DriveFileList, DriveAbout, etc.
mapper.ts # DriveFile -> FileItem conversion
src/app/actions/google-drive.ts # 17 server actions
src/db/schema-google.ts # google_auth, google_starred_files
src/components/files/ # file browser UI (14 components)
domain-wide delegation
Standard OAuth requires each user to click "Allow" in a consent screen. Domain-wide delegation skips this: a service account is authorized (once, by an admin) to impersonate any user in the Workspace domain.
The flow:
- Admin creates a service account in Google Cloud Console
- Admin grants the service account domain-wide delegation in Google Admin
- Admin pastes the service account JSON key into Compass settings
- Compass encrypts and stores the key in the
google_authtable - For each API request, Compass creates a JWT claiming to be the user, signs it with the service account's private key, and exchanges it for an access token
This means Compass sees exactly what each user would see in Google Drive. If Alice can't access a folder in Workspace, she can't access it in Compass either.
JWT creation with Web Crypto
auth/service-account.ts builds JWTs without any Node.js crypto dependencies, using only the Web Crypto API. This is necessary because Compass runs on Cloudflare Workers, which doesn't have Node.js built-ins.
The JWT contains:
const payload = {
iss: serviceAccountKey.client_email, // service account email
sub: userEmail, // user to impersonate
scope: scopes.join(" "), // "https://www.googleapis.com/auth/drive"
aud: GOOGLE_TOKEN_URL,
iat: now,
exp: now + 3600, // 1 hour
}
The private key is imported from PEM format into a CryptoKey object for RS256 signing:
async function importPrivateKey(pem: string): Promise<CryptoKey> {
const pemBody = pem
.replace(/-----BEGIN PRIVATE KEY-----/g, "")
.replace(/-----END PRIVATE KEY-----/g, "")
.replace(/\s/g, "")
const binaryString = atob(pemBody)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return crypto.subtle.importKey(
"pkcs8", bytes.buffer,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false, ["sign"]
)
}
The signed JWT is exchanged for a standard OAuth access token via Google's token endpoint with the urn:ietf:params:oauth:grant-type:jwt-bearer grant type.
drive client
client/drive-client.ts wraps the Google Drive REST API v3. Each method accepts a userEmail parameter for impersonation.
The client has:
Token caching. Access tokens are cached in memory per user email with a 1-hour TTL. The cache is per-worker-isolate, so different Cloudflare Workers instances maintain separate caches. On 401 responses, the cached token is cleared and the request retries.
Rate limiting. Reuses the same ConcurrencyLimiter from the NetSuite module (defaulting to 10 concurrent requests). On 429 responses, concurrency is adaptively reduced.
Retry with exponential backoff. Up to 3 attempts for 401 (auth refresh), 429 (rate limit), and 5xx (server error). Non-retryable errors (400, 403, 404) fail immediately.
Available operations:
listFiles-- list files in a folder with query filtering, ordering, pagination, shared drive supportgetFile-- get a single file's metadatacreateFolder-- create a folder in a specified parentinitiateResumableUpload-- start a resumable upload session (returns a session URI)downloadFile-- download file contentexportFile-- export a Google Docs/Sheets/Slides file to a different format (e.g., PDF)renameFile-- rename a filemoveFile-- move a file between parentstrashFile/restoreFile-- soft delete and restoregetStorageQuota-- get the user's storage usagesearchFiles-- full-text search across fileslistSharedDrives-- enumerate shared drives the user can access
two-layer permissions
Every file operation goes through two permission checks:
-
Compass RBAC -- the server action checks
requirePermission(user, "document", "read|create|update|delete"). This determines whether the user's Compass role allows the operation at all. -
Google Workspace permissions -- the API request is made as the impersonated user. Google enforces whatever sharing permissions apply to that user's account. If a user doesn't have edit access to a folder in Drive, the
createFoldercall will fail with a 403 from Google.
The Google email used for impersonation defaults to the user's login email but can be overridden via the googleEmail column on the users table. This handles cases where a user's Compass login email differs from their Workspace email.
function resolveGoogleEmail(user: AuthUser): string {
return user.googleEmail ?? user.email
}
mapper
mapper.ts converts Google Drive API responses into Compass's FileItem type for the UI. The mapper handles:
- MIME type to file type classification (folder, document, spreadsheet, image, video, etc.)
- Permission role mapping (Google's
writer/ownerbecomeeditor, everything else becomesviewer) - Owner and sharing info extraction
- Local starred file tracking (via the
google_starred_filestable, since Google's star API is per-Google-account, not per-Compass-user)
Google Docs, Sheets, and Slides are "native" files that can't be downloaded directly. The mapper includes getExportMimeType to determine what format to export them as (Docs -> PDF, Sheets -> XLSX, Slides -> PDF).
server actions
src/app/actions/google-drive.ts exports 17 actions:
Connection management:
getGoogleDriveConnectionStatus-- checks if a service account key is storedconnectGoogleDrive-- validates and stores the service account key (makes a test API call first)disconnectGoogleDrive-- deletes the stored keylistAvailableSharedDrives-- lists shared drives for the setup UIselectSharedDrive-- sets the default shared drive for the org
File operations:
listDriveFiles-- list files in a folder (or shared drive root)listDriveFilesForView-- list files for special views: shared, recent, starred, trashsearchDriveFiles-- full-text searchgetDriveFileInfo-- get metadata for a single filelistDriveFolders-- list only folders (for move dialog)createDriveFolder-- create a new folderrenameDriveFile-- rename a file or foldermoveDriveFile-- move a file between folderstrashDriveFile/restoreDriveFile-- soft delete and restoregetUploadSessionUrl-- initiate a resumable upload and return the session URLgetDriveStorageQuota-- get storage usage info
User preferences:
toggleStarFile-- star/unstar a file (stored locally, not in Google)getStarredFileIds-- get the current user's starred file IDsupdateUserGoogleEmail-- set a user's Google impersonation email override
Each action follows the same structure: authenticate, check RBAC, resolve the Google email, build a DriveClient, execute the operation, map results.
schema
src/db/schema-google.ts defines two tables:
google_auth -- organization-level Google connection. One row per org. Stores the encrypted service account key, workspace domain, optional shared drive selection, and who connected it.
google_starred_files -- per-user file starring. References user ID and stores the Google file ID. Stars are local to Compass because Google's starring is per-Google-account, not per-app.
file browser UI
src/components/files/ contains 14 components that make up the file browser:
file-browser.tsx-- main container, manages folder navigation state and view modefile-grid.tsx/file-list.tsx-- grid and list view layoutsfile-item.tsx/file-row.tsx-- individual file rendering for grid and list viewsfile-icon.tsx-- MIME type to icon mappingfile-breadcrumb.tsx-- folder path breadcrumb navigationfile-toolbar.tsx-- view toggles, search, upload buttonfile-context-menu.tsx-- right-click menu with rename, move, trash, star, open in Drivefile-drop-zone.tsx-- drag-and-drop file upload areafile-upload-dialog.tsx-- upload progress dialogfile-new-folder-dialog.tsx-- folder creation dialogfile-rename-dialog.tsx/file-move-dialog.tsx-- rename and move dialogsstorage-indicator.tsx-- storage usage bar
encryption
The service account key JSON is encrypted at rest using the same shared AES-GCM encryption used by the NetSuite module (src/lib/crypto.ts), but with a Google-specific PBKDF2 salt (compass-google-service-account). The encryption key comes from the GOOGLE_SERVICE_ACCOUNT_ENCRYPTION_KEY environment variable.
This means the service account's private RSA key is never stored in plaintext in D1. Decryption happens per-request when the DriveClient is constructed.
setup reference
For the full setup guide (Google Cloud Console configuration, domain-wide delegation setup, admin permissions), see docs/google-drive/GOOGLE-DRIVE-INTEGRATION.md.