@aaronsb/google-workspace-mcp 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/build/__helpers__/testSetup.d.ts +1 -0
- package/build/__helpers__/testSetup.js +6 -0
- package/build/__helpers__/testSetup.js.map +1 -0
- package/build/__tests__/accounts/credentials.test.d.ts +1 -0
- package/build/__tests__/accounts/credentials.test.js +129 -0
- package/build/__tests__/accounts/credentials.test.js.map +1 -0
- package/build/__tests__/accounts/registry.test.d.ts +1 -0
- package/build/__tests__/accounts/registry.test.js +74 -0
- package/build/__tests__/accounts/registry.test.js.map +1 -0
- package/build/__tests__/executor/errors.test.d.ts +1 -0
- package/build/__tests__/executor/errors.test.js +42 -0
- package/build/__tests__/executor/errors.test.js.map +1 -0
- package/build/__tests__/executor/gws.test.d.ts +1 -0
- package/build/__tests__/executor/gws.test.js +178 -0
- package/build/__tests__/executor/gws.test.js.map +1 -0
- package/build/__tests__/executor/paths.test.d.ts +1 -0
- package/build/__tests__/executor/paths.test.js +60 -0
- package/build/__tests__/executor/paths.test.js.map +1 -0
- package/build/__tests__/executor/workspace.test.d.ts +1 -0
- package/build/__tests__/executor/workspace.test.js +117 -0
- package/build/__tests__/executor/workspace.test.js.map +1 -0
- package/build/__tests__/factory/generator.test.d.ts +1 -0
- package/build/__tests__/factory/generator.test.js +178 -0
- package/build/__tests__/factory/generator.test.js.map +1 -0
- package/build/__tests__/factory/patch-coverage.test.d.ts +10 -0
- package/build/__tests__/factory/patch-coverage.test.js +148 -0
- package/build/__tests__/factory/patch-coverage.test.js.map +1 -0
- package/build/__tests__/factory/safety.test.d.ts +1 -0
- package/build/__tests__/factory/safety.test.js +107 -0
- package/build/__tests__/factory/safety.test.js.map +1 -0
- package/build/__tests__/integration/executor.test.d.ts +5 -0
- package/build/__tests__/integration/executor.test.js +46 -0
- package/build/__tests__/integration/executor.test.js.map +1 -0
- package/build/__tests__/integration/handlers.test.d.ts +6 -0
- package/build/__tests__/integration/handlers.test.js +95 -0
- package/build/__tests__/integration/handlers.test.js.map +1 -0
- package/build/__tests__/integration/setup.d.ts +19 -0
- package/build/__tests__/integration/setup.js +61 -0
- package/build/__tests__/integration/setup.js.map +1 -0
- package/build/__tests__/server/formatting/markdown.test.d.ts +1 -0
- package/build/__tests__/server/formatting/markdown.test.js +149 -0
- package/build/__tests__/server/formatting/markdown.test.js.map +1 -0
- package/build/__tests__/server/formatting/next-steps.test.d.ts +1 -0
- package/build/__tests__/server/formatting/next-steps.test.js +42 -0
- package/build/__tests__/server/formatting/next-steps.test.js.map +1 -0
- package/build/__tests__/server/handler.test.d.ts +1 -0
- package/build/__tests__/server/handler.test.js +97 -0
- package/build/__tests__/server/handler.test.js.map +1 -0
- package/build/__tests__/server/handlers/__mocks__/executor.d.ts +147 -0
- package/build/__tests__/server/handlers/__mocks__/executor.js +114 -0
- package/build/__tests__/server/handlers/__mocks__/executor.js.map +1 -0
- package/build/__tests__/server/handlers/accounts.test.d.ts +1 -0
- package/build/__tests__/server/handlers/accounts.test.js +127 -0
- package/build/__tests__/server/handlers/accounts.test.js.map +1 -0
- package/build/__tests__/server/handlers/calendar.test.d.ts +1 -0
- package/build/__tests__/server/handlers/calendar.test.js +95 -0
- package/build/__tests__/server/handlers/calendar.test.js.map +1 -0
- package/build/__tests__/server/handlers/drive.test.d.ts +1 -0
- package/build/__tests__/server/handlers/drive.test.js +81 -0
- package/build/__tests__/server/handlers/drive.test.js.map +1 -0
- package/build/__tests__/server/handlers/email.test.d.ts +1 -0
- package/build/__tests__/server/handlers/email.test.js +99 -0
- package/build/__tests__/server/handlers/email.test.js.map +1 -0
- package/build/__tests__/server/handlers/validate.test.d.ts +1 -0
- package/build/__tests__/server/handlers/validate.test.js +88 -0
- package/build/__tests__/server/handlers/validate.test.js.map +1 -0
- package/build/__tests__/server/queue.test.d.ts +1 -0
- package/build/__tests__/server/queue.test.js +194 -0
- package/build/__tests__/server/queue.test.js.map +1 -0
- package/build/__tests__/server/server.test.d.ts +7 -0
- package/build/__tests__/server/server.test.js +135 -0
- package/build/__tests__/server/server.test.js.map +1 -0
- package/build/__tests__/server/tools.test.d.ts +1 -0
- package/build/__tests__/server/tools.test.js +91 -0
- package/build/__tests__/server/tools.test.js.map +1 -0
- package/build/accounts/auth.d.ts +24 -0
- package/build/accounts/auth.js +118 -0
- package/build/accounts/auth.js.map +1 -0
- package/build/accounts/credentials.d.ts +11 -0
- package/build/accounts/credentials.js +52 -0
- package/build/accounts/credentials.js.map +1 -0
- package/build/accounts/index.d.ts +6 -0
- package/build/accounts/index.js +4 -0
- package/build/accounts/index.js.map +1 -0
- package/build/accounts/registry.d.ts +11 -0
- package/build/accounts/registry.js +62 -0
- package/build/accounts/registry.js.map +1 -0
- package/build/executor/errors.d.ts +15 -0
- package/build/executor/errors.js +37 -0
- package/build/executor/errors.js.map +1 -0
- package/build/executor/file-output.d.ts +23 -0
- package/build/executor/file-output.js +67 -0
- package/build/executor/file-output.js.map +1 -0
- package/build/executor/gws.d.ts +23 -0
- package/build/executor/gws.js +144 -0
- package/build/executor/gws.js.map +1 -0
- package/build/executor/index.d.ts +4 -0
- package/build/executor/index.js +4 -0
- package/build/executor/index.js.map +1 -0
- package/build/executor/paths.d.ts +6 -0
- package/build/executor/paths.js +25 -0
- package/build/executor/paths.js.map +1 -0
- package/build/executor/workspace.d.ts +38 -0
- package/build/executor/workspace.js +146 -0
- package/build/executor/workspace.js.map +1 -0
- package/build/factory/defaults.d.ts +8 -0
- package/build/factory/defaults.js +101 -0
- package/build/factory/defaults.js.map +1 -0
- package/build/factory/generator.d.ts +23 -0
- package/build/factory/generator.js +253 -0
- package/build/factory/generator.js.map +1 -0
- package/build/factory/manifest.yaml +852 -0
- package/build/factory/patches.d.ts +6 -0
- package/build/factory/patches.js +13 -0
- package/build/factory/patches.js.map +1 -0
- package/build/factory/registry.d.ts +12 -0
- package/build/factory/registry.js +18 -0
- package/build/factory/registry.js.map +1 -0
- package/build/factory/safety.d.ts +68 -0
- package/build/factory/safety.js +157 -0
- package/build/factory/safety.js.map +1 -0
- package/build/factory/types.d.ts +102 -0
- package/build/factory/types.js +6 -0
- package/build/factory/types.js.map +1 -0
- package/build/factory/yaml.d.ts +2 -0
- package/build/factory/yaml.js +3 -0
- package/build/factory/yaml.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +14 -0
- package/build/index.js.map +1 -0
- package/build/server/formatting/markdown.d.ts +21 -0
- package/build/server/formatting/markdown.js +324 -0
- package/build/server/formatting/markdown.js.map +1 -0
- package/build/server/formatting/next-steps.d.ts +9 -0
- package/build/server/formatting/next-steps.js +123 -0
- package/build/server/formatting/next-steps.js.map +1 -0
- package/build/server/handler.d.ts +3 -0
- package/build/server/handler.js +24 -0
- package/build/server/handler.js.map +1 -0
- package/build/server/handlers/accounts.d.ts +2 -0
- package/build/server/handlers/accounts.js +181 -0
- package/build/server/handlers/accounts.js.map +1 -0
- package/build/server/handlers/calendar.d.ts +2 -0
- package/build/server/handlers/calendar.js +93 -0
- package/build/server/handlers/calendar.js.map +1 -0
- package/build/server/handlers/drive.d.ts +2 -0
- package/build/server/handlers/drive.js +74 -0
- package/build/server/handlers/drive.js.map +1 -0
- package/build/server/handlers/email.d.ts +2 -0
- package/build/server/handlers/email.js +115 -0
- package/build/server/handlers/email.js.map +1 -0
- package/build/server/handlers/validate.d.ts +3 -0
- package/build/server/handlers/validate.js +22 -0
- package/build/server/handlers/validate.js.map +1 -0
- package/build/server/handlers/workspace.d.ts +9 -0
- package/build/server/handlers/workspace.js +110 -0
- package/build/server/handlers/workspace.js.map +1 -0
- package/build/server/index.d.ts +4 -0
- package/build/server/index.js +4 -0
- package/build/server/index.js.map +1 -0
- package/build/server/queue.d.ts +11 -0
- package/build/server/queue.js +141 -0
- package/build/server/queue.js.map +1 -0
- package/build/server/server.d.ts +3 -0
- package/build/server/server.js +214 -0
- package/build/server/server.js.map +1 -0
- package/build/server/tools.d.ts +13 -0
- package/build/server/tools.js +99 -0
- package/build/server/tools.js.map +1 -0
- package/build/services/calendar/patch.d.ts +11 -0
- package/build/services/calendar/patch.js +116 -0
- package/build/services/calendar/patch.js.map +1 -0
- package/build/services/drive/patch.d.ts +10 -0
- package/build/services/drive/patch.js +131 -0
- package/build/services/drive/patch.js.map +1 -0
- package/build/services/gmail/attachments.d.ts +19 -0
- package/build/services/gmail/attachments.js +90 -0
- package/build/services/gmail/attachments.js.map +1 -0
- package/build/services/gmail/patch.d.ts +10 -0
- package/build/services/gmail/patch.js +226 -0
- package/build/services/gmail/patch.js.map +1 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aaron Bockelie (aaronsb@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Google Workspace MCP Server
|
|
2
|
+
|
|
3
|
+
Give AI agents full access to Google Workspace — Gmail, Calendar, Drive, and more — through a single MCP server that handles multi-account credential routing, response formatting for AI consumption, and contextual guidance.
|
|
4
|
+
|
|
5
|
+
Built on [Google's official Workspace CLI](https://github.com/googleworkspace/cli) (`gws`), which means API coverage grows as Google does. The server uses a manifest-driven factory that turns declarative YAML into fully functional MCP tools — adding a new Google API operation is a config change, not a code change.
|
|
6
|
+
|
|
7
|
+
## Why This MCP Server
|
|
8
|
+
|
|
9
|
+
**For users:** One install gives your AI agent real, authenticated access to your Google accounts. Search email, check your calendar, manage Drive files, chain multi-step workflows — all through natural conversation.
|
|
10
|
+
|
|
11
|
+
**For teams:** Multi-account support means your agent can work across personal and work accounts simultaneously, with per-account credential isolation and XDG-compliant storage.
|
|
12
|
+
|
|
13
|
+
**For developers:** The factory architecture means coverage expands fast. Google's Workspace CLI already supports 15+ services and hundreds of API operations. The manifest curates which ones are exposed, patches add domain-specific formatting, and the defaults handle everything else.
|
|
14
|
+
|
|
15
|
+
## What's Available
|
|
16
|
+
|
|
17
|
+
**5 tools, 32+ operations across 3 core services:**
|
|
18
|
+
|
|
19
|
+
| Tool | Operations | What It Does |
|
|
20
|
+
|------|-----------|--------------|
|
|
21
|
+
| `manage_email` | search, read, send, reply, replyAll, forward, triage, trash, untrash, modify, labels, threads, getThread | Full Gmail — search, read, compose, thread management, label management |
|
|
22
|
+
| `manage_calendar` | list, agenda, get, create, quickAdd, update, delete, calendars, freebusy | Calendar CRUD, natural language event creation, availability checks |
|
|
23
|
+
| `manage_drive` | search, get, upload, download, copy, delete, export, listPermissions, share, unshare | File management, Google Docs export, sharing and permissions |
|
|
24
|
+
| `manage_accounts` | list, authenticate, remove, status, refresh, scopes | Multi-account lifecycle — add accounts, manage credentials and scopes |
|
|
25
|
+
| `queue_operations` | — | Chain operations sequentially with `$N.field` result references |
|
|
26
|
+
|
|
27
|
+
Every response includes **next-steps** guidance — the agent always knows what it can do next.
|
|
28
|
+
|
|
29
|
+
## How It Works
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌─────────────────────────┐
|
|
33
|
+
MCP Client ──stdio──▶ │ manifest.yaml │
|
|
34
|
+
│ (52 operations declared) │
|
|
35
|
+
└────────┬────────────────┘
|
|
36
|
+
│
|
|
37
|
+
┌────────▼────────────────┐
|
|
38
|
+
│ Factory Generator │
|
|
39
|
+
│ schemas + handlers │
|
|
40
|
+
└────────┬────────────────┘
|
|
41
|
+
│
|
|
42
|
+
┌──────────────┼──────────────┐
|
|
43
|
+
▼ ▼ ▼
|
|
44
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
45
|
+
│ Gmail │ │ Calendar │ │ Drive │
|
|
46
|
+
│ Patch │ │ Patch │ │ Patch │
|
|
47
|
+
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
|
48
|
+
│ │ │
|
|
49
|
+
└──────┬──────┘──────┬──────┘
|
|
50
|
+
▼ ▼
|
|
51
|
+
Account Router ──▶ gws CLI ──▶ Google APIs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The **factory** reads a YAML manifest and generates MCP tool schemas and request handlers at startup. **Patches** add domain-specific behavior where needed — Gmail search hydration, calendar formatting, Drive file type detection. Operations without patches get sensible defaults automatically.
|
|
55
|
+
|
|
56
|
+
The underlying engine is Google's `@googleworkspace/cli` — a Rust binary that wraps the full Google Workspace API surface. The MCP server curates which operations to expose and shapes the responses for AI consumption.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
### MCPB Bundle (Claude Desktop and other MCP clients)
|
|
61
|
+
|
|
62
|
+
Download the `.mcpb` bundle for your platform from the [latest release](https://github.com/aaronsb/google-workspace-mcp/releases):
|
|
63
|
+
|
|
64
|
+
| Platform | File |
|
|
65
|
+
|----------|------|
|
|
66
|
+
| macOS (Apple Silicon) | `google-workspace-mcp-darwin-arm64.mcpb` |
|
|
67
|
+
| macOS (Intel) | `google-workspace-mcp-darwin-x64.mcpb` |
|
|
68
|
+
| Linux x64 | `google-workspace-mcp-linux-x64.mcpb` |
|
|
69
|
+
| Linux ARM64 | `google-workspace-mcp-linux-arm64.mcpb` |
|
|
70
|
+
| Windows x64 | `google-workspace-mcp-windows-x64.mcpb` |
|
|
71
|
+
|
|
72
|
+
In Claude Desktop, drag the `.mcpb` file into the app — it will prompt you for your Google OAuth credentials, then you're ready to go. Other MCP clients that support `.mcpb` extensions can install it the same way. The bundle includes everything: the server, the gws binary, and all dependencies.
|
|
73
|
+
|
|
74
|
+
### Claude Code / npm
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install @aaronsb/google-workspace-mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or run directly:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @aaronsb/google-workspace-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Prerequisites
|
|
87
|
+
|
|
88
|
+
1. **Node.js** 18+
|
|
89
|
+
2. **Google Cloud OAuth credentials** — create at [console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials):
|
|
90
|
+
- Create an OAuth 2.0 Client ID (Desktop application)
|
|
91
|
+
- Enable the APIs you want (Gmail, Calendar, Drive, Sheets, etc.)
|
|
92
|
+
|
|
93
|
+
3. Set environment variables:
|
|
94
|
+
```bash
|
|
95
|
+
export GOOGLE_CLIENT_ID="your-client-id"
|
|
96
|
+
export GOOGLE_CLIENT_SECRET="your-client-secret"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## MCP Client Configuration
|
|
100
|
+
|
|
101
|
+
### Claude Desktop
|
|
102
|
+
|
|
103
|
+
Add to `claude_desktop_config.json`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"google-workspace": {
|
|
109
|
+
"command": "npx",
|
|
110
|
+
"args": ["@aaronsb/google-workspace-mcp"],
|
|
111
|
+
"env": {
|
|
112
|
+
"GOOGLE_CLIENT_ID": "your-client-id",
|
|
113
|
+
"GOOGLE_CLIENT_SECRET": "your-client-secret"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Claude Code
|
|
121
|
+
|
|
122
|
+
Add to `.mcp.json`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"google-workspace": {
|
|
128
|
+
"command": "npx",
|
|
129
|
+
"args": ["@aaronsb/google-workspace-mcp"],
|
|
130
|
+
"env": {
|
|
131
|
+
"GOOGLE_CLIENT_ID": "your-client-id",
|
|
132
|
+
"GOOGLE_CLIENT_SECRET": "your-client-secret"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Usage
|
|
140
|
+
|
|
141
|
+
Add an account (opens browser for OAuth):
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
manage_accounts { "operation": "authenticate" }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then use any tool with your account email:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
manage_email { "operation": "triage", "email": "you@gmail.com" }
|
|
151
|
+
manage_calendar { "operation": "agenda", "email": "you@gmail.com" }
|
|
152
|
+
manage_drive { "operation": "search", "email": "you@gmail.com", "query": "quarterly report" }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Multi-Step Workflows
|
|
156
|
+
|
|
157
|
+
Chain operations with result references — the output of one step feeds the next:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"operations": [
|
|
162
|
+
{ "tool": "manage_email", "args": { "operation": "search", "email": "you@gmail.com", "query": "from:boss subject:review" }},
|
|
163
|
+
{ "tool": "manage_email", "args": { "operation": "read", "email": "you@gmail.com", "messageId": "$0.messageId" }}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Expanding Coverage
|
|
169
|
+
|
|
170
|
+
The server discovers operations from the gws CLI, which already supports 15+ Google services (Sheets, Docs, Tasks, People, Chat, and more). Adding coverage is a manifest edit:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
make manifest-discover # Find all 287+ available operations
|
|
174
|
+
make manifest-lint # Validate the curated manifest
|
|
175
|
+
make test # Verify everything works
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
New operations get default formatting automatically. Add a patch only when you need domain-specific presentation.
|
|
179
|
+
|
|
180
|
+
## Data Storage
|
|
181
|
+
|
|
182
|
+
Follows XDG Base Directory Specification:
|
|
183
|
+
|
|
184
|
+
| Data | Location |
|
|
185
|
+
|------|----------|
|
|
186
|
+
| Account registry | `~/.config/google-workspace-mcp/accounts.json` |
|
|
187
|
+
| Credentials | `~/.local/share/google-workspace-mcp/credentials/` |
|
|
188
|
+
|
|
189
|
+
Credentials are per-account files with standard OAuth tokens. No secrets are stored in the project directory.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testSetup.js","sourceRoot":"","sources":["../../src/__helpers__/testSetup.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,UAAU,CAAC,GAAG,EAAE;IACd,IAAI,CAAC,aAAa,EAAE,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as credentials from '../../accounts/credentials.js';
|
|
3
|
+
import { credentialPath } from '../../executor/paths.js';
|
|
4
|
+
import { execute } from '../../executor/gws.js';
|
|
5
|
+
jest.mock('node:fs/promises');
|
|
6
|
+
jest.mock('../../executor/gws.js');
|
|
7
|
+
const mockFs = jest.mocked(fs);
|
|
8
|
+
const mockExecute = execute;
|
|
9
|
+
describe('credentials', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
process.env.XDG_DATA_HOME = '/tmp/test-data';
|
|
12
|
+
});
|
|
13
|
+
describe('hasCredential', () => {
|
|
14
|
+
it('returns true when credential file exists', async () => {
|
|
15
|
+
mockFs.access.mockResolvedValue(undefined);
|
|
16
|
+
expect(await credentials.hasCredential('user@example.com')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('returns false when credential file does not exist', async () => {
|
|
19
|
+
mockFs.access.mockRejectedValue(new Error('ENOENT'));
|
|
20
|
+
expect(await credentials.hasCredential('user@example.com')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('readCredential', () => {
|
|
24
|
+
it('reads and parses credential file', async () => {
|
|
25
|
+
const cred = { type: 'authorized_user', client_id: 'id', client_secret: 'secret', refresh_token: 'token' };
|
|
26
|
+
mockFs.readFile.mockResolvedValue(JSON.stringify(cred));
|
|
27
|
+
const result = await credentials.readCredential('user@example.com');
|
|
28
|
+
expect(result).toEqual(cred);
|
|
29
|
+
expect(mockFs.readFile).toHaveBeenCalledWith(credentialPath('user@example.com'), 'utf-8');
|
|
30
|
+
});
|
|
31
|
+
it('throws when credential file does not exist', async () => {
|
|
32
|
+
mockFs.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
33
|
+
await expect(credentials.readCredential('user@example.com')).rejects.toThrow('ENOENT');
|
|
34
|
+
});
|
|
35
|
+
it('throws on malformed JSON', async () => {
|
|
36
|
+
mockFs.readFile.mockResolvedValue('not-json{{{');
|
|
37
|
+
await expect(credentials.readCredential('user@example.com')).rejects.toThrow();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('removeCredential', () => {
|
|
41
|
+
it('deletes credential file at the correct path', async () => {
|
|
42
|
+
mockFs.unlink.mockResolvedValue(undefined);
|
|
43
|
+
await credentials.removeCredential('user@example.com');
|
|
44
|
+
expect(mockFs.unlink).toHaveBeenCalledWith(credentialPath('user@example.com'));
|
|
45
|
+
});
|
|
46
|
+
it('ignores ENOENT errors', async () => {
|
|
47
|
+
const err = new Error('ENOENT');
|
|
48
|
+
err.code = 'ENOENT';
|
|
49
|
+
mockFs.unlink.mockRejectedValue(err);
|
|
50
|
+
await expect(credentials.removeCredential('user@example.com')).resolves.toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it('rethrows non-ENOENT errors', async () => {
|
|
53
|
+
const err = new Error('EACCES');
|
|
54
|
+
err.code = 'EACCES';
|
|
55
|
+
mockFs.unlink.mockRejectedValue(err);
|
|
56
|
+
await expect(credentials.removeCredential('user@example.com')).rejects.toThrow('EACCES');
|
|
57
|
+
});
|
|
58
|
+
it('rethrows errors without code property', async () => {
|
|
59
|
+
mockFs.unlink.mockRejectedValue(new Error('unknown fs error'));
|
|
60
|
+
await expect(credentials.removeCredential('user@example.com')).rejects.toThrow('unknown fs error');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('exportAndSaveCredential', () => {
|
|
64
|
+
const validCredential = {
|
|
65
|
+
type: 'authorized_user',
|
|
66
|
+
client_id: 'id-123',
|
|
67
|
+
client_secret: 'secret-456',
|
|
68
|
+
refresh_token: 'token-789',
|
|
69
|
+
};
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
72
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
73
|
+
});
|
|
74
|
+
it('calls gws auth export --unmasked', async () => {
|
|
75
|
+
mockExecute.mockResolvedValue({ success: true, data: validCredential, stderr: '' });
|
|
76
|
+
await credentials.exportAndSaveCredential('user@example.com');
|
|
77
|
+
expect(mockExecute).toHaveBeenCalledWith(['auth', 'export', '--unmasked']);
|
|
78
|
+
});
|
|
79
|
+
it('creates credentials directory with 0700 permissions', async () => {
|
|
80
|
+
mockExecute.mockResolvedValue({ success: true, data: validCredential, stderr: '' });
|
|
81
|
+
await credentials.exportAndSaveCredential('user@example.com');
|
|
82
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true, mode: 0o700 });
|
|
83
|
+
});
|
|
84
|
+
it('writes credential file with 0600 permissions', async () => {
|
|
85
|
+
mockExecute.mockResolvedValue({ success: true, data: validCredential, stderr: '' });
|
|
86
|
+
await credentials.exportAndSaveCredential('user@example.com');
|
|
87
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(credentialPath('user@example.com'), JSON.stringify(validCredential, null, 2), { mode: 0o600 });
|
|
88
|
+
});
|
|
89
|
+
it('returns the credential file path', async () => {
|
|
90
|
+
mockExecute.mockResolvedValue({ success: true, data: validCredential, stderr: '' });
|
|
91
|
+
const result = await credentials.exportAndSaveCredential('user@example.com');
|
|
92
|
+
expect(result).toBe(credentialPath('user@example.com'));
|
|
93
|
+
});
|
|
94
|
+
it('rejects when gws returns non-authorized_user type', async () => {
|
|
95
|
+
mockExecute.mockResolvedValue({
|
|
96
|
+
success: true,
|
|
97
|
+
data: { type: 'service_account', project_id: 'test' },
|
|
98
|
+
stderr: '',
|
|
99
|
+
});
|
|
100
|
+
await expect(credentials.exportAndSaveCredential('user@example.com')).rejects.toThrow('authorized_user');
|
|
101
|
+
});
|
|
102
|
+
it('rejects when gws returns null data', async () => {
|
|
103
|
+
mockExecute.mockResolvedValue({ success: true, data: null, stderr: '' });
|
|
104
|
+
await expect(credentials.exportAndSaveCredential('user@example.com')).rejects.toThrow('authorized_user');
|
|
105
|
+
});
|
|
106
|
+
it('rejects when gws returns empty object', async () => {
|
|
107
|
+
mockExecute.mockResolvedValue({ success: true, data: {}, stderr: '' });
|
|
108
|
+
await expect(credentials.exportAndSaveCredential('user@example.com')).rejects.toThrow('authorized_user');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('listCredentials', () => {
|
|
112
|
+
it('returns slugs from credential directory', async () => {
|
|
113
|
+
mockFs.readdir.mockResolvedValue(['user_at_example-com.json', 'other_at_test-com.json']);
|
|
114
|
+
const result = await credentials.listCredentials();
|
|
115
|
+
expect(result).toEqual(['user_at_example-com', 'other_at_test-com']);
|
|
116
|
+
});
|
|
117
|
+
it('returns empty array when directory does not exist', async () => {
|
|
118
|
+
mockFs.readdir.mockRejectedValue(new Error('ENOENT'));
|
|
119
|
+
const result = await credentials.listCredentials();
|
|
120
|
+
expect(result).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
it('filters non-json files', async () => {
|
|
123
|
+
mockFs.readdir.mockResolvedValue(['cred.json', 'readme.txt', '.DS_Store']);
|
|
124
|
+
const result = await credentials.listCredentials();
|
|
125
|
+
expect(result).toEqual(['cred']);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=credentials.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.test.js","sourceRoot":"","sources":["../../../src/__tests__/accounts/credentials.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { listAccounts, getAccount, addAccount, removeAccount } from '../../accounts/registry.js';
|
|
3
|
+
jest.mock('node:fs/promises');
|
|
4
|
+
jest.mock('../../accounts/credentials.js');
|
|
5
|
+
const mockFs = jest.mocked(fs);
|
|
6
|
+
const emptyAccounts = JSON.stringify({ accounts: [] });
|
|
7
|
+
const twoAccounts = JSON.stringify({
|
|
8
|
+
accounts: [
|
|
9
|
+
{ email: 'a@example.com', category: 'personal' },
|
|
10
|
+
{ email: 'b@example.com', category: 'work', description: 'Work account' },
|
|
11
|
+
],
|
|
12
|
+
});
|
|
13
|
+
describe('registry', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
process.env.XDG_CONFIG_HOME = '/tmp/test-config';
|
|
16
|
+
process.env.XDG_DATA_HOME = '/tmp/test-data';
|
|
17
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
18
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
19
|
+
});
|
|
20
|
+
describe('listAccounts', () => {
|
|
21
|
+
it('returns empty array when no accounts file', async () => {
|
|
22
|
+
mockFs.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
23
|
+
const { hasCredential } = await import('../../accounts/credentials.js');
|
|
24
|
+
const result = await listAccounts();
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
it('returns accounts with credential status', async () => {
|
|
28
|
+
mockFs.readFile.mockResolvedValue(twoAccounts);
|
|
29
|
+
const { hasCredential } = await import('../../accounts/credentials.js');
|
|
30
|
+
hasCredential.mockResolvedValue(true);
|
|
31
|
+
const result = await listAccounts();
|
|
32
|
+
expect(result).toHaveLength(2);
|
|
33
|
+
expect(result[0]).toMatchObject({ email: 'a@example.com', hasCredential: true });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('getAccount', () => {
|
|
37
|
+
it('finds account by email', async () => {
|
|
38
|
+
mockFs.readFile.mockResolvedValue(twoAccounts);
|
|
39
|
+
const result = await getAccount('b@example.com');
|
|
40
|
+
expect(result).toMatchObject({ email: 'b@example.com', category: 'work' });
|
|
41
|
+
});
|
|
42
|
+
it('returns undefined for unknown email', async () => {
|
|
43
|
+
mockFs.readFile.mockResolvedValue(twoAccounts);
|
|
44
|
+
const result = await getAccount('unknown@example.com');
|
|
45
|
+
expect(result).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('addAccount', () => {
|
|
49
|
+
it('adds new account and writes file', async () => {
|
|
50
|
+
mockFs.readFile.mockResolvedValue(emptyAccounts);
|
|
51
|
+
const result = await addAccount('new@example.com', 'personal', 'My account');
|
|
52
|
+
expect(result.email).toBe('new@example.com');
|
|
53
|
+
expect(mockFs.writeFile).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('throws when account already exists', async () => {
|
|
56
|
+
mockFs.readFile.mockResolvedValue(twoAccounts);
|
|
57
|
+
await expect(addAccount('a@example.com')).rejects.toThrow('already exists');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('removeAccount', () => {
|
|
61
|
+
it('removes account and credential', async () => {
|
|
62
|
+
mockFs.readFile.mockResolvedValue(twoAccounts);
|
|
63
|
+
const { removeCredential } = await import('../../accounts/credentials.js');
|
|
64
|
+
await removeAccount('a@example.com');
|
|
65
|
+
expect(mockFs.writeFile).toHaveBeenCalled();
|
|
66
|
+
expect(removeCredential).toHaveBeenCalledWith('a@example.com');
|
|
67
|
+
});
|
|
68
|
+
it('throws when account not found', async () => {
|
|
69
|
+
mockFs.readFile.mockResolvedValue(emptyAccounts);
|
|
70
|
+
await expect(removeAccount('missing@example.com')).rejects.toThrow('not found');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../../src/__tests__/accounts/registry.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { GwsError, GwsExitCode, parseGwsError } from '../../executor/errors.js';
|
|
2
|
+
describe('GwsError', () => {
|
|
3
|
+
it('stores exit code and reason', () => {
|
|
4
|
+
const err = new GwsError('bad auth', GwsExitCode.AuthError, 'authError', 'stderr output');
|
|
5
|
+
expect(err.message).toBe('bad auth');
|
|
6
|
+
expect(err.exitCode).toBe(GwsExitCode.AuthError);
|
|
7
|
+
expect(err.reason).toBe('authError');
|
|
8
|
+
expect(err.stderr).toBe('stderr output');
|
|
9
|
+
expect(err.name).toBe('GwsError');
|
|
10
|
+
});
|
|
11
|
+
it('is an instance of Error', () => {
|
|
12
|
+
const err = new GwsError('fail', GwsExitCode.InternalError);
|
|
13
|
+
expect(err).toBeInstanceOf(Error);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('parseGwsError', () => {
|
|
17
|
+
it('extracts structured error from JSON stdout', () => {
|
|
18
|
+
const stdout = JSON.stringify({
|
|
19
|
+
error: { code: 401, message: 'Auth failed', reason: 'authError' },
|
|
20
|
+
});
|
|
21
|
+
const err = parseGwsError(2, stdout, '');
|
|
22
|
+
expect(err.message).toBe('Auth failed');
|
|
23
|
+
expect(err.exitCode).toBe(GwsExitCode.AuthError);
|
|
24
|
+
expect(err.reason).toBe('authError');
|
|
25
|
+
});
|
|
26
|
+
it('falls back to stderr when stdout is not JSON', () => {
|
|
27
|
+
const err = parseGwsError(3, '', 'Unknown service: foobar');
|
|
28
|
+
expect(err.message).toBe('Unknown service: foobar');
|
|
29
|
+
expect(err.exitCode).toBe(GwsExitCode.ValidationError);
|
|
30
|
+
});
|
|
31
|
+
it('uses exit code label when stderr is empty', () => {
|
|
32
|
+
const err = parseGwsError(5, '', '');
|
|
33
|
+
expect(err.message).toContain('code 5');
|
|
34
|
+
expect(err.message).toContain('InternalError');
|
|
35
|
+
});
|
|
36
|
+
it('handles malformed JSON in stdout', () => {
|
|
37
|
+
const err = parseGwsError(1, '{not valid json', 'some error');
|
|
38
|
+
expect(err.message).toBe('some error');
|
|
39
|
+
expect(err.exitCode).toBe(GwsExitCode.ApiError);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=errors.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.js","sourceRoot":"","sources":["../../../src/__tests__/executor/errors.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|