mitsupi 1.4.0 → 1.6.0
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/AGENTS.md +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +28 -18
- package/analyze-edits.py +232 -0
- package/distributions/README.md +8 -0
- package/distributions/mitsupi-common/README.md +11 -0
- package/distributions/mitsupi-common/package.json +95 -0
- package/distributions/mitsupi-loaded/README.md +13 -0
- package/distributions/mitsupi-loaded/package.json +38 -0
- package/{pi-extensions → extensions}/answer.ts +11 -11
- package/extensions/btw.ts +963 -0
- package/{pi-extensions → extensions}/context.ts +2 -2
- package/{pi-extensions → extensions}/control.ts +20 -13
- package/{pi-extensions → extensions}/files.ts +6 -8
- package/{pi-extensions → extensions}/go-to-bed.ts +2 -2
- package/{pi-extensions → extensions}/loop.ts +18 -11
- package/extensions/multi-edit.ts +871 -0
- package/{pi-extensions → extensions}/review.ts +254 -82
- package/{pi-extensions → extensions}/session-breakdown.ts +551 -74
- package/extensions/split-fork.ts +130 -0
- package/{pi-extensions → extensions}/todos.ts +41 -34
- package/extensions/uv.ts +123 -0
- package/intercepted-commands/poetry +8 -1
- package/intercepted-commands/python +42 -2
- package/intercepted-commands/python3 +42 -2
- package/package.json +6 -3
- package/skills/google-workspace/SKILL.md +53 -8
- package/skills/google-workspace/scripts/auth.js +106 -28
- package/skills/google-workspace/scripts/common.js +201 -32
- package/skills/google-workspace/scripts/workspace.js +64 -9
- package/skills/native-web-search/SKILL.md +2 -0
- package/skills/native-web-search/search.mjs +102 -15
- package/skills/pi-share/SKILL.md +5 -2
- package/skills/pi-share/fetch-session.mjs +46 -15
- package/skills/summarize/SKILL.md +4 -3
- package/skills/summarize/to-markdown.mjs +3 -1
- package/skills/web-browser/SKILL.md +6 -0
- package/skills/web-browser/scripts/start.js +75 -27
- package/pi-extensions/uv.ts +0 -33
- /package/{pi-extensions → extensions}/notify.ts +0 -0
- /package/{pi-extensions → extensions}/prompt-editor.ts +0 -0
- /package/{pi-extensions → extensions}/whimsical.ts +0 -0
- /package/{pi-themes → themes}/nightowl.json +0 -0
|
@@ -9,15 +9,29 @@ Use this skill for Google Workspace tasks (Gmail, Drive, Calendar, Docs, Sheets,
|
|
|
9
9
|
|
|
10
10
|
## Files
|
|
11
11
|
|
|
12
|
-
- `scripts/auth.js` — OAuth login/status/clear
|
|
12
|
+
- `scripts/auth.js` — OAuth login/status/clear + account enumeration
|
|
13
13
|
- `scripts/workspace.js` — JavaScript execution based API runner
|
|
14
14
|
|
|
15
|
+
## Account model (multi-account)
|
|
16
|
+
|
|
17
|
+
This skill is **profile-based by email address**.
|
|
18
|
+
|
|
19
|
+
- There is **no default account**.
|
|
20
|
+
- Every API call must specify `--email <account@example.com>`.
|
|
21
|
+
- Tokens are stored per-email under `~/.pi/google-workspace/tokens/`.
|
|
22
|
+
|
|
23
|
+
Before running API calls, discover available signed-in accounts:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node scripts/auth.js accounts
|
|
27
|
+
```
|
|
28
|
+
|
|
15
29
|
## Usage
|
|
16
30
|
|
|
17
|
-
Always use `exec`.
|
|
31
|
+
Always use `exec` and always provide `--email`.
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
|
-
node scripts/workspace.js exec <<'JS'
|
|
34
|
+
node scripts/workspace.js exec --email user@example.com <<'JS'
|
|
21
35
|
const me = await workspace.whoAmI();
|
|
22
36
|
const files = await workspace.call('drive', 'files.list', {
|
|
23
37
|
pageSize: 5,
|
|
@@ -31,6 +45,7 @@ Available inside exec scripts:
|
|
|
31
45
|
|
|
32
46
|
- `auth` (authorized OAuth client)
|
|
33
47
|
- `google` (`googleapis` root)
|
|
48
|
+
- `workspace.accountEmail` (selected profile email)
|
|
34
49
|
- `workspace.call(service, methodPath, params, {version})`
|
|
35
50
|
- `workspace.service(service, {version})`
|
|
36
51
|
- `workspace.whoAmI()`
|
|
@@ -47,12 +62,41 @@ Optional flags:
|
|
|
47
62
|
2. Keep payloads small (`fields`, `maxResults`, minimal props).
|
|
48
63
|
3. Use `Promise.all` for independent requests.
|
|
49
64
|
4. Never print token contents.
|
|
50
|
-
5.
|
|
65
|
+
5. If the user did not specify an account, run `node scripts/auth.js accounts` and choose/confirm an explicit email.
|
|
66
|
+
6. If auth fails, first run `node scripts/auth.js accounts` to see known profiles.
|
|
67
|
+
7. If account mismatch is possible, run `workspace.whoAmI()` in the selected profile.
|
|
68
|
+
8. On 401/403/unauthorized errors, switch account (`--email ...`) or re-login that specific profile.
|
|
69
|
+
|
|
70
|
+
## Unauthorized/account-switch playbook
|
|
71
|
+
|
|
72
|
+
If a request fails with unauthorized/forbidden/insufficient permissions:
|
|
73
|
+
|
|
74
|
+
1. Enumerate profiles:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node scripts/auth.js accounts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. Retry with the intended account:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node scripts/workspace.js exec --email correct-user@example.com <<'JS'
|
|
84
|
+
return await workspace.whoAmI();
|
|
85
|
+
JS
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
3. If token is stale or missing scopes, re-login that account:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node scripts/auth.js login --email correct-user@example.com
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
4. Retry the original request with the same `--email`.
|
|
51
95
|
|
|
52
96
|
## Short Gmail counting example
|
|
53
97
|
|
|
54
98
|
```bash
|
|
55
|
-
node scripts/workspace.js exec <<'JS'
|
|
99
|
+
node scripts/workspace.js exec --email user@example.com <<'JS'
|
|
56
100
|
const gmail = google.gmail({ version: 'v1', auth });
|
|
57
101
|
|
|
58
102
|
let trash = 0;
|
|
@@ -76,7 +120,7 @@ JS
|
|
|
76
120
|
## Setup + auth
|
|
77
121
|
|
|
78
122
|
```bash
|
|
79
|
-
node scripts/auth.js login
|
|
123
|
+
node scripts/auth.js login --email user@example.com
|
|
80
124
|
```
|
|
81
125
|
|
|
82
126
|
Notes:
|
|
@@ -87,6 +131,7 @@ Notes:
|
|
|
87
131
|
- Useful diagnostics:
|
|
88
132
|
|
|
89
133
|
```bash
|
|
90
|
-
node scripts/auth.js
|
|
91
|
-
node scripts/auth.js
|
|
134
|
+
node scripts/auth.js accounts
|
|
135
|
+
node scripts/auth.js status --email user@example.com
|
|
136
|
+
node scripts/auth.js clear --email user@example.com
|
|
92
137
|
```
|
|
@@ -2,29 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
CREDENTIALS_PATH,
|
|
5
|
-
|
|
5
|
+
TOKENS_DIR,
|
|
6
6
|
DEFAULT_SCOPES,
|
|
7
7
|
authorize,
|
|
8
8
|
clearToken,
|
|
9
9
|
credentialsExist,
|
|
10
10
|
formatScopes,
|
|
11
11
|
getWorkspaceClientConfig,
|
|
12
|
+
listAccounts,
|
|
12
13
|
loadToken,
|
|
14
|
+
normalizeEmail,
|
|
13
15
|
resolveAuthMode,
|
|
16
|
+
tokenPathForEmail,
|
|
14
17
|
} = require('./common');
|
|
15
18
|
|
|
16
19
|
function printHelp() {
|
|
17
20
|
console.log(`Google Workspace auth helper
|
|
18
21
|
|
|
19
22
|
Usage:
|
|
20
|
-
node scripts/auth.js login [--scopes scope1,scope2,...]
|
|
21
|
-
node scripts/auth.js status
|
|
22
|
-
node scripts/auth.js clear
|
|
23
|
+
node scripts/auth.js login --email user@example.com [--scopes scope1,scope2,...]
|
|
24
|
+
node scripts/auth.js status --email user@example.com
|
|
25
|
+
node scripts/auth.js clear --email user@example.com
|
|
26
|
+
node scripts/auth.js accounts
|
|
23
27
|
|
|
24
28
|
Environment overrides:
|
|
25
29
|
GOOGLE_WORKSPACE_CONFIG_DIR
|
|
26
30
|
GOOGLE_WORKSPACE_CREDENTIALS
|
|
27
|
-
|
|
31
|
+
GOOGLE_WORKSPACE_TOKENS_DIR
|
|
28
32
|
GOOGLE_WORKSPACE_AUTH_MODE (local|cloud)
|
|
29
33
|
GOOGLE_WORKSPACE_CLIENT_ID (cloud mode)
|
|
30
34
|
GOOGLE_WORKSPACE_CLOUD_FUNCTION_URL (cloud mode)
|
|
@@ -36,10 +40,12 @@ function parseScopes(args) {
|
|
|
36
40
|
if (idx === -1) {
|
|
37
41
|
return DEFAULT_SCOPES;
|
|
38
42
|
}
|
|
43
|
+
|
|
39
44
|
const raw = args[idx + 1];
|
|
40
45
|
if (!raw) {
|
|
41
46
|
throw new Error('--scopes requires a value');
|
|
42
47
|
}
|
|
48
|
+
|
|
43
49
|
const scopes = formatScopes(raw);
|
|
44
50
|
if (scopes.length === 0) {
|
|
45
51
|
throw new Error('--scopes produced an empty scope list');
|
|
@@ -47,22 +53,63 @@ function parseScopes(args) {
|
|
|
47
53
|
return scopes;
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
function parseRequiredEmail(args) {
|
|
57
|
+
const idx = args.indexOf('--email');
|
|
58
|
+
if (idx === -1) {
|
|
59
|
+
throw new Error('Missing required --email <account@example.com>');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const raw = args[idx + 1];
|
|
63
|
+
if (!raw) {
|
|
64
|
+
throw new Error('--email requires a value');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalizeEmail(raw);
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
async function doLogin(args) {
|
|
71
|
+
const email = parseRequiredEmail(args);
|
|
51
72
|
const scopes = parseScopes(args);
|
|
52
|
-
|
|
73
|
+
|
|
74
|
+
await authorize({ email, scopes, interactive: true });
|
|
75
|
+
|
|
53
76
|
console.log('✅ Login successful. Token stored at:');
|
|
54
|
-
console.log(` ${
|
|
77
|
+
console.log(` ${tokenPathForEmail(email)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printTokenDetails(token) {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const expiry = token.expiry_date || null;
|
|
83
|
+
const expired = expiry ? expiry < now : null;
|
|
84
|
+
const scopeCount = formatScopes(token.scope).length;
|
|
85
|
+
|
|
86
|
+
console.log('Token details:');
|
|
87
|
+
console.log(` access_token: ${token.access_token ? 'present' : 'missing'}`);
|
|
88
|
+
console.log(` refresh_token: ${token.refresh_token ? 'present' : 'missing'}`);
|
|
89
|
+
console.log(` scopes: ${scopeCount}`);
|
|
90
|
+
|
|
91
|
+
if (expiry) {
|
|
92
|
+
console.log(` expiry_date: ${new Date(expiry).toISOString()}`);
|
|
93
|
+
console.log(` expired: ${expired ? 'yes' : 'no'}`);
|
|
94
|
+
} else {
|
|
95
|
+
console.log(' expiry_date: n/a');
|
|
96
|
+
}
|
|
55
97
|
}
|
|
56
98
|
|
|
57
|
-
function doStatus() {
|
|
99
|
+
function doStatus(args) {
|
|
100
|
+
const email = parseRequiredEmail(args);
|
|
101
|
+
|
|
58
102
|
console.log('Credentials file:');
|
|
59
103
|
console.log(` ${CREDENTIALS_PATH}`);
|
|
60
104
|
console.log(` Exists: ${credentialsExist() ? 'yes' : 'no'}`);
|
|
61
105
|
|
|
62
|
-
const token = loadToken();
|
|
106
|
+
const token = loadToken(email);
|
|
63
107
|
const mode = resolveAuthMode(token);
|
|
64
108
|
const workspaceCfg = getWorkspaceClientConfig();
|
|
65
109
|
|
|
110
|
+
console.log('\nSelected account:');
|
|
111
|
+
console.log(` ${email}`);
|
|
112
|
+
|
|
66
113
|
console.log('\nAuth mode:');
|
|
67
114
|
console.log(` ${mode}`);
|
|
68
115
|
if (mode === 'cloud') {
|
|
@@ -72,34 +119,60 @@ function doStatus() {
|
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
console.log('\nToken file:');
|
|
75
|
-
console.log(` ${
|
|
122
|
+
console.log(` ${tokenPathForEmail(email)}`);
|
|
76
123
|
console.log(` Exists: ${token ? 'yes' : 'no'}`);
|
|
77
124
|
|
|
78
125
|
if (!token) {
|
|
126
|
+
console.log('\nTip: sign in with:');
|
|
127
|
+
console.log(` node scripts/auth.js login --email ${email}`);
|
|
79
128
|
return;
|
|
80
129
|
}
|
|
81
130
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const scopeCount = formatScopes(token.scope).length;
|
|
131
|
+
console.log('');
|
|
132
|
+
printTokenDetails(token);
|
|
133
|
+
}
|
|
86
134
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.log(` refresh_token: ${token.refresh_token ? 'present' : 'missing'}`);
|
|
90
|
-
console.log(` scopes: ${scopeCount}`);
|
|
135
|
+
function doAccounts() {
|
|
136
|
+
const accounts = listAccounts();
|
|
91
137
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log('
|
|
138
|
+
console.log('Token store:');
|
|
139
|
+
console.log(` ${TOKENS_DIR}`);
|
|
140
|
+
|
|
141
|
+
if (accounts.length === 0) {
|
|
142
|
+
console.log('\nNo signed-in accounts found.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\nSigned-in accounts (${accounts.length}):`);
|
|
147
|
+
|
|
148
|
+
for (const account of accounts) {
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(`- email: ${account.email || '(unknown)'}`);
|
|
151
|
+
console.log(` tokenFile: ${account.path}`);
|
|
152
|
+
|
|
153
|
+
if (account.error) {
|
|
154
|
+
console.log(` error: ${account.error}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(` authMode: ${account.authMode || 'unknown'}`);
|
|
159
|
+
console.log(` access_token: ${account.hasAccessToken ? 'present' : 'missing'}`);
|
|
160
|
+
console.log(` refresh_token: ${account.hasRefreshToken ? 'present' : 'missing'}`);
|
|
161
|
+
console.log(` scopes: ${account.scopes.length}`);
|
|
162
|
+
|
|
163
|
+
if (account.expiryDate) {
|
|
164
|
+
console.log(` expiry_date: ${new Date(account.expiryDate).toISOString()}`);
|
|
165
|
+
console.log(` expired: ${account.expired ? 'yes' : 'no'}`);
|
|
166
|
+
} else {
|
|
167
|
+
console.log(' expiry_date: n/a');
|
|
168
|
+
}
|
|
97
169
|
}
|
|
98
170
|
}
|
|
99
171
|
|
|
100
|
-
function doClear() {
|
|
101
|
-
|
|
102
|
-
|
|
172
|
+
function doClear(args) {
|
|
173
|
+
const email = parseRequiredEmail(args);
|
|
174
|
+
clearToken(email);
|
|
175
|
+
console.log(`✅ Token cleared for ${email}.`);
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
async function main() {
|
|
@@ -116,12 +189,17 @@ async function main() {
|
|
|
116
189
|
}
|
|
117
190
|
|
|
118
191
|
if (command === 'status') {
|
|
119
|
-
doStatus();
|
|
192
|
+
doStatus(args);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (command === 'accounts') {
|
|
197
|
+
doAccounts();
|
|
120
198
|
return;
|
|
121
199
|
}
|
|
122
200
|
|
|
123
201
|
if (command === 'clear') {
|
|
124
|
-
doClear();
|
|
202
|
+
doClear(args);
|
|
125
203
|
return;
|
|
126
204
|
}
|
|
127
205
|
|
|
@@ -16,8 +16,8 @@ const CREDENTIALS_PATH =
|
|
|
16
16
|
process.env.GOOGLE_WORKSPACE_CREDENTIALS ||
|
|
17
17
|
path.join(CONFIG_DIR, 'credentials.json');
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
process.env.
|
|
19
|
+
const TOKENS_DIR =
|
|
20
|
+
process.env.GOOGLE_WORKSPACE_TOKENS_DIR || path.join(CONFIG_DIR, 'tokens');
|
|
21
21
|
|
|
22
22
|
const SKILL_ROOT = path.join(__dirname, '..');
|
|
23
23
|
|
|
@@ -26,6 +26,11 @@ const DEFAULT_CLIENT_ID =
|
|
|
26
26
|
const DEFAULT_CLOUD_FUNCTION_URL =
|
|
27
27
|
'https://google-workspace-extension.geminicli.com';
|
|
28
28
|
|
|
29
|
+
const REQUIRED_IDENTITY_SCOPES = [
|
|
30
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
31
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
32
|
+
];
|
|
33
|
+
|
|
29
34
|
const DEFAULT_SCOPES = [
|
|
30
35
|
'https://www.googleapis.com/auth/documents',
|
|
31
36
|
'https://www.googleapis.com/auth/drive',
|
|
@@ -33,7 +38,7 @@ const DEFAULT_SCOPES = [
|
|
|
33
38
|
'https://www.googleapis.com/auth/chat.spaces',
|
|
34
39
|
'https://www.googleapis.com/auth/chat.messages',
|
|
35
40
|
'https://www.googleapis.com/auth/chat.memberships',
|
|
36
|
-
|
|
41
|
+
...REQUIRED_IDENTITY_SCOPES,
|
|
37
42
|
'https://www.googleapis.com/auth/gmail.modify',
|
|
38
43
|
'https://www.googleapis.com/auth/directory.readonly',
|
|
39
44
|
'https://www.googleapis.com/auth/presentations.readonly',
|
|
@@ -57,6 +62,10 @@ function ensureConfigDir() {
|
|
|
57
62
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function ensureTokensDir() {
|
|
66
|
+
fs.mkdirSync(TOKENS_DIR, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
function readJson(filePath) {
|
|
61
70
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
62
71
|
}
|
|
@@ -122,8 +131,29 @@ function credentialsExist() {
|
|
|
122
131
|
return fs.existsSync(CREDENTIALS_PATH);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
function
|
|
126
|
-
|
|
134
|
+
function normalizeEmail(email) {
|
|
135
|
+
const normalized = String(email || '').trim().toLowerCase();
|
|
136
|
+
if (!normalized) {
|
|
137
|
+
throw new Error('Email is required. Pass --email <account@example.com>.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!/^[^@\s]+@[^@\s]+$/.test(normalized)) {
|
|
141
|
+
throw new Error(`Invalid email address: ${email}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function tokenFilenameForEmail(email) {
|
|
148
|
+
return `${encodeURIComponent(normalizeEmail(email))}.json`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function tokenPathForEmail(email) {
|
|
152
|
+
return path.join(TOKENS_DIR, tokenFilenameForEmail(email));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function tokenExists(email) {
|
|
156
|
+
return fs.existsSync(tokenPathForEmail(email));
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
function loadCredentialsFile() {
|
|
@@ -136,11 +166,84 @@ function loadCredentialsFile() {
|
|
|
136
166
|
return readJson(CREDENTIALS_PATH);
|
|
137
167
|
}
|
|
138
168
|
|
|
139
|
-
function loadToken() {
|
|
140
|
-
|
|
169
|
+
function loadToken(email) {
|
|
170
|
+
const tokenPath = tokenPathForEmail(email);
|
|
171
|
+
if (!fs.existsSync(tokenPath)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return readJson(tokenPath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function decodeEmailFromTokenFilename(filename) {
|
|
178
|
+
if (!filename.endsWith('.json')) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
return normalizeEmail(decodeURIComponent(filename.slice(0, -5)));
|
|
184
|
+
} catch {
|
|
141
185
|
return null;
|
|
142
186
|
}
|
|
143
|
-
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function listAccounts() {
|
|
190
|
+
if (!fs.existsSync(TOKENS_DIR)) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const results = [];
|
|
195
|
+
const entries = fs.readdirSync(TOKENS_DIR, { withFileTypes: true });
|
|
196
|
+
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const tokenPath = path.join(TOKENS_DIR, entry.name);
|
|
203
|
+
const emailFromFilename = decodeEmailFromTokenFilename(entry.name);
|
|
204
|
+
|
|
205
|
+
let token;
|
|
206
|
+
try {
|
|
207
|
+
token = readJson(tokenPath);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
results.push({
|
|
210
|
+
email: emailFromFilename,
|
|
211
|
+
path: tokenPath,
|
|
212
|
+
error: `Failed to read token JSON: ${error.message}`,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let email = emailFromFilename;
|
|
218
|
+
if (token && token.__email) {
|
|
219
|
+
try {
|
|
220
|
+
email = normalizeEmail(token.__email);
|
|
221
|
+
} catch {
|
|
222
|
+
// Ignore malformed __email metadata and keep filename-derived email.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const expiryDate = token.expiry_date || null;
|
|
227
|
+
|
|
228
|
+
results.push({
|
|
229
|
+
email,
|
|
230
|
+
path: tokenPath,
|
|
231
|
+
authMode: token.__authMode === 'local' || token.__authMode === 'cloud'
|
|
232
|
+
? token.__authMode
|
|
233
|
+
: null,
|
|
234
|
+
hasAccessToken: Boolean(token.access_token),
|
|
235
|
+
hasRefreshToken: Boolean(token.refresh_token),
|
|
236
|
+
scopes: formatScopes(token.scope),
|
|
237
|
+
expiryDate,
|
|
238
|
+
expired: expiryDate ? expiryDate < Date.now() : null,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results.sort((a, b) => {
|
|
243
|
+
const left = a.email || '';
|
|
244
|
+
const right = b.email || '';
|
|
245
|
+
return left.localeCompare(right);
|
|
246
|
+
});
|
|
144
247
|
}
|
|
145
248
|
|
|
146
249
|
function createOAuthClientFromCredentials(credentialsJson) {
|
|
@@ -192,23 +295,31 @@ function resolveAuthMode(token) {
|
|
|
192
295
|
return 'cloud';
|
|
193
296
|
}
|
|
194
297
|
|
|
195
|
-
function saveToken(token, mode) {
|
|
298
|
+
function saveToken(email, token, mode) {
|
|
196
299
|
ensureConfigDir();
|
|
300
|
+
ensureTokensDir();
|
|
301
|
+
|
|
302
|
+
const normalizedEmail = normalizeEmail(email);
|
|
303
|
+
const tokenPath = tokenPathForEmail(normalizedEmail);
|
|
304
|
+
|
|
197
305
|
const payload = {
|
|
198
306
|
...token,
|
|
199
307
|
__authMode: mode,
|
|
308
|
+
__email: normalizedEmail,
|
|
200
309
|
};
|
|
201
|
-
|
|
310
|
+
|
|
311
|
+
fs.writeFileSync(tokenPath, JSON.stringify(payload, null, 2));
|
|
202
312
|
try {
|
|
203
|
-
fs.chmodSync(
|
|
313
|
+
fs.chmodSync(tokenPath, 0o600);
|
|
204
314
|
} catch {
|
|
205
315
|
// Non-POSIX file systems can fail chmod. Ignore.
|
|
206
316
|
}
|
|
207
317
|
}
|
|
208
318
|
|
|
209
|
-
function clearToken() {
|
|
210
|
-
|
|
211
|
-
|
|
319
|
+
function clearToken(email) {
|
|
320
|
+
const tokenPath = tokenPathForEmail(email);
|
|
321
|
+
if (fs.existsSync(tokenPath)) {
|
|
322
|
+
fs.rmSync(tokenPath);
|
|
212
323
|
}
|
|
213
324
|
}
|
|
214
325
|
|
|
@@ -274,24 +385,57 @@ async function refreshViaCloudFunction(refreshToken) {
|
|
|
274
385
|
return response.json();
|
|
275
386
|
}
|
|
276
387
|
|
|
277
|
-
|
|
388
|
+
function mergeWithIdentityScopes(scopes) {
|
|
389
|
+
return Array.from(new Set([...formatScopes(scopes), ...REQUIRED_IDENTITY_SCOPES]));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function fetchAuthenticatedEmail(authClient) {
|
|
393
|
+
const google = getGoogleApis();
|
|
394
|
+
const oauth2 = google.oauth2({ version: 'v2', auth: authClient });
|
|
395
|
+
const response = await oauth2.userinfo.get();
|
|
396
|
+
const email = response?.data?.email;
|
|
397
|
+
if (!email) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
'Authentication succeeded, but Google did not return an email address. ' +
|
|
400
|
+
'Ensure userinfo.email scope is granted.',
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return normalizeEmail(email);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function ensureExpectedEmail(authClient, expectedEmail) {
|
|
407
|
+
const normalizedExpectedEmail = normalizeEmail(expectedEmail);
|
|
408
|
+
const authenticatedEmail = await fetchAuthenticatedEmail(authClient);
|
|
409
|
+
|
|
410
|
+
if (authenticatedEmail !== normalizedExpectedEmail) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Authenticated as ${authenticatedEmail}, but expected ${normalizedExpectedEmail}. ` +
|
|
413
|
+
'Sign in with the requested account and retry.',
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return authenticatedEmail;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function interactiveLoginLocal(scopes, email) {
|
|
278
421
|
const { authenticate } = loadRuntimeDeps();
|
|
279
422
|
console.error('ℹ️ Opening browser for Google OAuth login (local credentials)...');
|
|
280
423
|
|
|
281
424
|
const authClient = await authenticate({
|
|
282
425
|
keyfilePath: CREDENTIALS_PATH,
|
|
283
|
-
scopes,
|
|
426
|
+
scopes: mergeWithIdentityScopes(scopes),
|
|
284
427
|
});
|
|
285
428
|
|
|
286
429
|
if (!authClient.credentials || !authClient.credentials.access_token) {
|
|
287
430
|
throw new Error('Authentication failed: no access token returned.');
|
|
288
431
|
}
|
|
289
432
|
|
|
290
|
-
|
|
433
|
+
const normalizedEmail = await ensureExpectedEmail(authClient, email);
|
|
434
|
+
saveToken(normalizedEmail, authClient.credentials, 'local');
|
|
291
435
|
return authClient;
|
|
292
436
|
}
|
|
293
437
|
|
|
294
|
-
async function interactiveLoginCloud(scopes) {
|
|
438
|
+
async function interactiveLoginCloud(scopes, email) {
|
|
295
439
|
const client = createCloudOAuthClient();
|
|
296
440
|
const { cloudFunctionUrl } = getWorkspaceClientConfig();
|
|
297
441
|
|
|
@@ -312,7 +456,7 @@ async function interactiveLoginCloud(scopes) {
|
|
|
312
456
|
const authUrl = client.generateAuthUrl({
|
|
313
457
|
redirect_uri: cloudFunctionUrl,
|
|
314
458
|
access_type: 'offline',
|
|
315
|
-
scope: scopes,
|
|
459
|
+
scope: mergeWithIdentityScopes(scopes),
|
|
316
460
|
state,
|
|
317
461
|
prompt: 'consent',
|
|
318
462
|
});
|
|
@@ -436,15 +580,16 @@ async function interactiveLoginCloud(scopes) {
|
|
|
436
580
|
});
|
|
437
581
|
|
|
438
582
|
client.setCredentials(credentials);
|
|
439
|
-
|
|
583
|
+
const normalizedEmail = await ensureExpectedEmail(client, email);
|
|
584
|
+
saveToken(normalizedEmail, credentials, 'cloud');
|
|
440
585
|
return client;
|
|
441
586
|
}
|
|
442
587
|
|
|
443
|
-
async function interactiveLogin(scopes, mode) {
|
|
588
|
+
async function interactiveLogin(scopes, mode, email) {
|
|
444
589
|
if (mode === 'local') {
|
|
445
|
-
return interactiveLoginLocal(scopes);
|
|
590
|
+
return interactiveLoginLocal(scopes, email);
|
|
446
591
|
}
|
|
447
|
-
return interactiveLoginCloud(scopes);
|
|
592
|
+
return interactiveLoginCloud(scopes, email);
|
|
448
593
|
}
|
|
449
594
|
|
|
450
595
|
function stripInternalTokenFields(token) {
|
|
@@ -453,17 +598,20 @@ function stripInternalTokenFields(token) {
|
|
|
453
598
|
}
|
|
454
599
|
const clone = { ...token };
|
|
455
600
|
delete clone.__authMode;
|
|
601
|
+
delete clone.__email;
|
|
456
602
|
return clone;
|
|
457
603
|
}
|
|
458
604
|
|
|
459
605
|
async function authorize(options = {}) {
|
|
460
|
-
const
|
|
606
|
+
const email = normalizeEmail(options.email);
|
|
607
|
+
const scopes = mergeWithIdentityScopes(options.scopes || DEFAULT_SCOPES);
|
|
461
608
|
const interactive = options.interactive !== false;
|
|
462
609
|
|
|
463
610
|
ensureConfigDir();
|
|
611
|
+
ensureTokensDir();
|
|
464
612
|
loadRuntimeDeps();
|
|
465
613
|
|
|
466
|
-
const token = loadToken();
|
|
614
|
+
const token = loadToken(email);
|
|
467
615
|
const mode = resolveAuthMode(token);
|
|
468
616
|
|
|
469
617
|
const client =
|
|
@@ -473,9 +621,22 @@ async function authorize(options = {}) {
|
|
|
473
621
|
|
|
474
622
|
if (!token) {
|
|
475
623
|
if (!interactive) {
|
|
476
|
-
throw new Error(
|
|
624
|
+
throw new Error(
|
|
625
|
+
`No token found for ${email} at ${tokenPathForEmail(email)}. ` +
|
|
626
|
+
`Run: node scripts/auth.js login --email ${email}`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return interactiveLogin(scopes, mode, email);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (token.__email) {
|
|
633
|
+
const tokenEmail = normalizeEmail(token.__email);
|
|
634
|
+
if (tokenEmail !== email) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Token metadata mismatch. Requested ${email} but token is tagged as ${tokenEmail}. ` +
|
|
637
|
+
`Delete it and sign in again: node scripts/auth.js clear --email ${email}`,
|
|
638
|
+
);
|
|
477
639
|
}
|
|
478
|
-
return interactiveLogin(scopes, mode);
|
|
479
640
|
}
|
|
480
641
|
|
|
481
642
|
client.setCredentials(stripInternalTokenFields(token));
|
|
@@ -493,7 +654,7 @@ async function authorize(options = {}) {
|
|
|
493
654
|
refreshed.credentials.refresh_token || client.credentials.refresh_token,
|
|
494
655
|
};
|
|
495
656
|
client.setCredentials(merged);
|
|
496
|
-
saveToken(merged, 'local');
|
|
657
|
+
saveToken(email, merged, 'local');
|
|
497
658
|
return client;
|
|
498
659
|
}
|
|
499
660
|
|
|
@@ -505,15 +666,18 @@ async function authorize(options = {}) {
|
|
|
505
666
|
refresh_token: client.credentials.refresh_token,
|
|
506
667
|
};
|
|
507
668
|
client.setCredentials(merged);
|
|
508
|
-
saveToken(merged, 'cloud');
|
|
669
|
+
saveToken(email, merged, 'cloud');
|
|
509
670
|
return client;
|
|
510
671
|
}
|
|
511
672
|
|
|
512
673
|
if (!interactive) {
|
|
513
|
-
throw new Error(
|
|
674
|
+
throw new Error(
|
|
675
|
+
`Token for ${email} is expired and no refresh token is available. ` +
|
|
676
|
+
`Run: node scripts/auth.js login --email ${email}`,
|
|
677
|
+
);
|
|
514
678
|
}
|
|
515
679
|
|
|
516
|
-
return interactiveLogin(scopes, mode);
|
|
680
|
+
return interactiveLogin(scopes, mode, email);
|
|
517
681
|
}
|
|
518
682
|
|
|
519
683
|
function formatScopes(value) {
|
|
@@ -532,15 +696,20 @@ function formatScopes(value) {
|
|
|
532
696
|
module.exports = {
|
|
533
697
|
CONFIG_DIR,
|
|
534
698
|
CREDENTIALS_PATH,
|
|
535
|
-
|
|
699
|
+
TOKENS_DIR,
|
|
536
700
|
DEFAULT_SCOPES,
|
|
537
701
|
DEFAULT_VERSIONS,
|
|
702
|
+
REQUIRED_IDENTITY_SCOPES,
|
|
538
703
|
authorize,
|
|
539
704
|
clearToken,
|
|
540
705
|
credentialsExist,
|
|
541
706
|
formatScopes,
|
|
542
707
|
getGoogleApis,
|
|
543
708
|
getWorkspaceClientConfig,
|
|
709
|
+
listAccounts,
|
|
544
710
|
loadToken,
|
|
711
|
+
normalizeEmail,
|
|
545
712
|
resolveAuthMode,
|
|
713
|
+
tokenExists,
|
|
714
|
+
tokenPathForEmail,
|
|
546
715
|
};
|