gogcli-mcp 3.0.0 → 4.0.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +273 -748
- package/dist/lib.js +375 -785
- package/manifest.json +2 -2
- package/mint.yaml +37 -33
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/attachments.ts +28 -34
- package/src/blob-upload.ts +165 -134
- package/src/blob-urls.ts +3 -5
- package/src/bootstrap-auth.ts +97 -0
- package/src/index.ts +3 -4
- package/src/lib.ts +14 -9
- package/src/runner.ts +23 -175
- package/src/tools/appscript.ts +1 -1
- package/src/tools/auth.ts +5 -5
- package/src/tools/drive.ts +1 -3
- package/src/tools/gmail.ts +9 -9
- package/src/tools/utils.ts +12 -58
- package/tests/attachments.test.ts +11 -14
- package/tests/blob-upload.test.ts +235 -160
- package/tests/bootstrap-auth.test.ts +245 -0
- package/tests/runner-file-args.test.ts +1 -13
- package/tests/runner.test.ts +8 -95
- package/tests/sdk-single-copy.test.ts +4 -12
- package/tests/tools/auth-401-shapes.test.ts +2 -3
- package/tests/tools/auth.test.ts +5 -4
- package/tests/tools/drive.test.ts +11 -3
- package/tests/tools/gmail.test.ts +2 -2
- package/tests/tools/utils.test.ts +1 -50
- package/tests/zod-single-copy.test.ts +6 -14
- package/tsconfig.json +1 -2
- package/vitest.config.ts +2 -12
- package/src/auth-log.ts +0 -205
- package/src/connector-auth.ts +0 -319
- package/src/connector-login.ts +0 -87
- package/src/connector-runtime.ts +0 -910
- package/src/google-probe.ts +0 -113
- package/src/google-token.ts +0 -391
- package/src/remote-runner.ts +0 -77
- package/src/worker.ts +0 -117
- package/tests/auth-log.test.ts +0 -530
- package/tests/connector-auth.test.ts +0 -559
- package/tests/connector-login.test.ts +0 -151
- package/tests/connector-runtime.test.ts +0 -1664
- package/tests/google-probe.test.ts +0 -116
- package/tests/google-token.test.ts +0 -425
- package/tests/remote-runner.test.ts +0 -202
- package/tests/worker.test.ts +0 -167
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
5
|
-
import { useRemoteGogRunner } from '../src/remote-runner.js';
|
|
6
|
-
import { run, runBinary, runExecutor, setDefaultGogExecutor } from '../src/runner.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The whole point of this seam is that a host without the `gog` binary can
|
|
10
|
-
* still serve. Every way it has silently failed so far: a half-configured env
|
|
11
|
-
* that falls back to spawning; a bin that never calls `useRemoteGogRunner()`;
|
|
12
|
-
* and — the one that shipped in 2.19.x — parking the executor in an
|
|
13
|
-
* AsyncLocalStorage that the tool calls never inherit, so the seam reported
|
|
14
|
-
* success and then spawned anyway.
|
|
15
|
-
*
|
|
16
|
-
* They share a shape worth naming: every one of them fails by falling back to
|
|
17
|
-
* the local binary, which on a laptop is invisible and on a host is total. So
|
|
18
|
-
* these tests assert on where a call ACTUALLY went, never on whether the wiring
|
|
19
|
-
* step was performed.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
vi.unstubAllGlobals();
|
|
24
|
-
vi.unstubAllEnvs();
|
|
25
|
-
// The executor is process-wide BY DESIGN now, so unlike an ALS store it does
|
|
26
|
-
// not unwind when a test ends — without this, the first test to install one
|
|
27
|
-
// would silently hand it to every test that ran after it.
|
|
28
|
-
setDefaultGogExecutor(undefined);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('useRemoteGogRunner', () => {
|
|
32
|
-
it('does nothing unless BOTH variables are set, so local installs are untouched', () => {
|
|
33
|
-
expect(useRemoteGogRunner({})).toBe(false);
|
|
34
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test' })).toBe(false);
|
|
35
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_KEY: 'k' })).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('treats blanks, placeholders and stringified nothings as unset', () => {
|
|
39
|
-
// MCP hosts pass env blocks through verbatim, so all three of these arrive
|
|
40
|
-
// in practice: a blank, a literal `${...}` that never expanded, and the
|
|
41
|
-
// string "undefined" from a host that stringified a missing value. The
|
|
42
|
-
// shared readEnvVar knows all three; a hand-rolled trim knew only the first
|
|
43
|
-
// two, which is why this uses the shared one.
|
|
44
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: ' ', GOG_RUNNER_KEY: 'k' })).toBe(false);
|
|
45
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: '${GOG_RUNNER_KEY}' })).toBe(false);
|
|
46
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'undefined' })).toBe(false);
|
|
47
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'null', GOG_RUNNER_KEY: 'k' })).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('addresses and authenticates the backend correctly', async () => {
|
|
51
|
-
// Asserted THROUGH run(), not by reading whichever slot the executor is
|
|
52
|
-
// parked in. The previous version of this test fetched
|
|
53
|
-
// `runExecutor.getStore()` and called what it found, so it was really a
|
|
54
|
-
// test that `enterWith` had been called — which stayed green while the
|
|
55
|
-
// shipped bins routed every call to a local binary (see the next test).
|
|
56
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'ok' }), { status: 200 }));
|
|
57
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
58
|
-
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
59
|
-
|
|
60
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test/', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
61
|
-
|
|
62
|
-
// Cross a macrotask boundary, the way a stdio tool call does.
|
|
63
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
64
|
-
expect(await run(['--version'])).toBe('ok');
|
|
65
|
-
|
|
66
|
-
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
67
|
-
// Trailing slash trimmed, so the endpoint is never `…//run`.
|
|
68
|
-
expect(url).toBe('https://r.test/run');
|
|
69
|
-
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer secret');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('still routes remotely from a context the store never reached', async () => {
|
|
73
|
-
// THE ONE THE TEST ABOVE CANNOT CATCH, and it shipped broken.
|
|
74
|
-
//
|
|
75
|
-
// `enterWith` mutates the store of the async resource that is current when
|
|
76
|
-
// it runs. The test above calls useRemoteGogRunner() itself, so everything
|
|
77
|
-
// it awaits afterwards is a descendant of that resource and inherits the
|
|
78
|
-
// store — it passes whether or not the seam works where it matters.
|
|
79
|
-
//
|
|
80
|
-
// A bin does something different: it calls useRemoteGogRunner() while the
|
|
81
|
-
// ES module is evaluating, and then `await runMcp(...)` starts serving. The
|
|
82
|
-
// tool calls arrive later as I/O events on the transport's own async
|
|
83
|
-
// resources, which are NOT descendants of module evaluation, so
|
|
84
|
-
// `getStore()` is undefined by the time `run()` asks. Verified against the
|
|
85
|
-
// published 2.19.1 bin: `useRemoteGogRunner()` returned true, and at call
|
|
86
|
-
// time the store was undefined — every hosted gog MCP answered
|
|
87
|
-
// "gog executable not found" while pointed at a healthy backend.
|
|
88
|
-
//
|
|
89
|
-
// `runExecutor.exit()` is that condition exactly, and deterministically:
|
|
90
|
-
// run the call where the ALS store does not apply.
|
|
91
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'remote' }), { status: 200 }));
|
|
92
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
93
|
-
// So the spawn fallback cannot accidentally succeed on a machine that has
|
|
94
|
-
// gog installed and make this pass for the wrong reason.
|
|
95
|
-
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
96
|
-
|
|
97
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
98
|
-
|
|
99
|
-
const output = await runExecutor.exit(() => run(['auth', 'status']));
|
|
100
|
-
expect(output).toBe('remote');
|
|
101
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('forwards its own GOG_ACCESS_TOKEN so a hosted gog acts as its caller', async () => {
|
|
105
|
-
// Under mcp-host's perUserChild, THIS PROCESS belongs to one caller and
|
|
106
|
-
// holds their token (#230). The backend has one Google identity on its
|
|
107
|
-
// volume, so without forwarding, every caller of a hosted gog acts as
|
|
108
|
-
// whoever seeded it.
|
|
109
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'ok' }), { status: 200 }));
|
|
110
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
111
|
-
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
112
|
-
|
|
113
|
-
expect(
|
|
114
|
-
useRemoteGogRunner({
|
|
115
|
-
GOG_RUNNER_URL: 'https://r.test',
|
|
116
|
-
GOG_RUNNER_KEY: 'secret',
|
|
117
|
-
GOG_ACCESS_TOKEN: 'ya29.the-caller',
|
|
118
|
-
}),
|
|
119
|
-
).toBe(true);
|
|
120
|
-
|
|
121
|
-
await runExecutor.exit(() => run(['auth', 'status']));
|
|
122
|
-
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
123
|
-
expect(JSON.parse(init.body as string).accessToken).toBe('ya29.the-caller');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('sends no token when the caller has not supplied one', async () => {
|
|
127
|
-
// Absent, not empty: the backend distinguishes "act as this caller" from
|
|
128
|
-
// "act as the box", and every registration that predates per-caller auth is
|
|
129
|
-
// the second case.
|
|
130
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ stdout: 'ok' }), { status: 200 }));
|
|
131
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
132
|
-
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
133
|
-
|
|
134
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
135
|
-
|
|
136
|
-
await runExecutor.exit(() => run(['auth', 'status']));
|
|
137
|
-
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
138
|
-
// The KEY must be absent, not present-and-empty. Asserted on its own rather
|
|
139
|
-
// than by matching the whole body: `assembleArgs` folds in ambient settings
|
|
140
|
-
// like GOG_ACCOUNT, so a whole-body match passes or fails depending on the
|
|
141
|
-
// developer's shell.
|
|
142
|
-
expect(JSON.parse(init.body as string)).not.toHaveProperty('accessToken');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('lets a per-request store beat the process-wide default', async () => {
|
|
146
|
-
// The Worker path scopes every request in `runExecutor.run({ executor })`
|
|
147
|
-
// because one isolate serves many callers (connector-runtime.ts). A
|
|
148
|
-
// process-wide default must never shadow that: it is the fallback for a
|
|
149
|
-
// stdio bin, not a second answer to "whose backend is this".
|
|
150
|
-
const perRequest = vi.fn(async () => 'per-request');
|
|
151
|
-
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ stdout: 'default' }), { status: 200 })));
|
|
152
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
153
|
-
|
|
154
|
-
const output = await runExecutor.run({ executor: perRequest }, () => run(['auth', 'status']));
|
|
155
|
-
expect(output).toBe('per-request');
|
|
156
|
-
expect(perRequest).toHaveBeenCalledTimes(1);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('still refuses binary retrieval when the default is a remote executor', async () => {
|
|
160
|
-
// runBinary's guard asks the same question ("is a text-only forward
|
|
161
|
-
// installed?") and must read the same answer, or a hosted bin would fall
|
|
162
|
-
// through to spawning for bytes while every text call went remote.
|
|
163
|
-
vi.stubEnv('GOG_PATH', '/nonexistent/gog');
|
|
164
|
-
expect(useRemoteGogRunner({ GOG_RUNNER_URL: 'https://r.test', GOG_RUNNER_KEY: 'secret' })).toBe(true);
|
|
165
|
-
|
|
166
|
-
await expect(runExecutor.exit(() => runBinary(['drive', 'read']))).rejects.toThrow(/text-only/);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* The seam is opt-in PER BIN — `useRemoteGogRunner()` is a call in each
|
|
172
|
-
* package's `index.ts`, and a package that omits it spawns the binary no
|
|
173
|
-
* matter what the host sets. That shipped: 2.19.0 wired base, sheets, docs,
|
|
174
|
-
* drive and gmail, and left calendar, classroom, contacts and slides behind,
|
|
175
|
-
* so those four answered every tool call on mcp-host with "gog executable not
|
|
176
|
-
* found" — a host with no binary and no way to ask for the backend.
|
|
177
|
-
*
|
|
178
|
-
* Nothing caught it because each package's suite covers its TOOLS, and
|
|
179
|
-
* `src/index.ts` is excluded from the coverage gate in every package (it is
|
|
180
|
-
* the bin: it boots a server and cannot be imported under test). So this
|
|
181
|
-
* asserts on the source text instead, across the whole workspace — the one
|
|
182
|
-
* check that scales to the next sub-package, which will otherwise be added by
|
|
183
|
-
* copying an index.ts that predates the seam.
|
|
184
|
-
*/
|
|
185
|
-
describe('every package bin', () => {
|
|
186
|
-
const packagesDir = fileURLToPath(new URL('../../', import.meta.url));
|
|
187
|
-
const bins = readdirSync(packagesDir)
|
|
188
|
-
.filter((name) => name.startsWith('gogcli-mcp'))
|
|
189
|
-
.map((name) => [name, join(packagesDir, name, 'src', 'index.ts')] as const);
|
|
190
|
-
|
|
191
|
-
it('finds every package (guards the glob itself against silently matching nothing)', () => {
|
|
192
|
-
expect(bins.length).toBeGreaterThanOrEqual(9);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it.each(bins)('%s installs the remote executor before starting the server', (_name, path) => {
|
|
196
|
-
const source = readFileSync(path, 'utf8');
|
|
197
|
-
expect(source).toContain('useRemoteGogRunner()');
|
|
198
|
-
// Order matters as much as presence: `runMcp` starts serving, so a call
|
|
199
|
-
// placed after it could be beaten by a tool call and fall back to spawning.
|
|
200
|
-
expect(source.indexOf('useRemoteGogRunner()')).toBeLessThan(source.indexOf('runMcp({'));
|
|
201
|
-
});
|
|
202
|
-
});
|
package/tests/worker.test.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { SELF } from 'cloudflare:test';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
4
|
-
import type { ToolRegistrar } from '@chrischall/mcp-utils';
|
|
5
|
-
import {
|
|
6
|
-
BASE_TOOL_REGISTRARS,
|
|
7
|
-
registerAuthTools,
|
|
8
|
-
registerSheetsTools,
|
|
9
|
-
registerGmailTools,
|
|
10
|
-
registerDriveTools,
|
|
11
|
-
registerDocsTools,
|
|
12
|
-
} from '../src/lib.js';
|
|
13
|
-
import { registerExtraSheetsTools } from '../../gogcli-mcp-sheets/src/tools/sheets-extra.js';
|
|
14
|
-
import { registerExtraGmailTools } from '../../gogcli-mcp-gmail/src/tools/gmail-extra.js';
|
|
15
|
-
import { registerExtraDriveTools } from '../../gogcli-mcp-drive/src/tools/drive-extra.js';
|
|
16
|
-
import { registerExtraDocsTools } from '../../gogcli-mcp-docs/src/tools/docs-extra.js';
|
|
17
|
-
|
|
18
|
-
// Each per-service MCP path exposes auth + <service> base + <service> extras —
|
|
19
|
-
// the same tool set that sub-package's stdio server exposes. Mirror worker.ts's
|
|
20
|
-
// wiring. Min counts are conservative floors (base+auth alone is ~13-15).
|
|
21
|
-
const SERVICE_PATHS: Array<{
|
|
22
|
-
path: string;
|
|
23
|
-
regs: ToolRegistrar[];
|
|
24
|
-
baseTool: string;
|
|
25
|
-
minTools: number;
|
|
26
|
-
}> = [
|
|
27
|
-
{ path: '/mcp/sheets', regs: [registerAuthTools, registerSheetsTools, registerExtraSheetsTools], baseTool: 'gog_sheets_get', minTools: 40 },
|
|
28
|
-
{ path: '/mcp/gmail', regs: [registerAuthTools, registerGmailTools, registerExtraGmailTools], baseTool: 'gog_gmail_search', minTools: 40 },
|
|
29
|
-
{ path: '/mcp/drive', regs: [registerAuthTools, registerDriveTools, registerExtraDriveTools], baseTool: 'gog_drive_ls', minTools: 35 },
|
|
30
|
-
{ path: '/mcp/docs', regs: [registerAuthTools, registerDocsTools, registerExtraDocsTools], baseTool: 'gog_docs_cat', minTools: 60 },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Handshake + tool-surface test for the gogcli Cloudflare remote connector, run
|
|
34
|
-
// inside the real Workers runtime (Miniflare) via `@cloudflare/vitest-pool-workers`
|
|
35
|
-
// against `wrangler.jsonc`. It proves things that don't require a live Fly
|
|
36
|
-
// backend or an authenticated session:
|
|
37
|
-
// 1. the OAuth default handler serves discovery + the login page;
|
|
38
|
-
// 2. an unauthenticated `/mcp` request is rejected before any tool code runs;
|
|
39
|
-
// 3. the base registrars register the full gog tool surface.
|
|
40
|
-
//
|
|
41
|
-
// The full authenticated `initialize` + `tools/list` over `/mcp` requires a real
|
|
42
|
-
// OAuth access token minted via `workers-oauth-provider`'s KV-backed grant flow
|
|
43
|
-
// (POST /authorize with a real connector key → auth code → POST /token), which
|
|
44
|
-
// would mean a live Fly login or extensive KV mocking — out of scope for a
|
|
45
|
-
// hermetic in-process test. So #3 asserts tool registration through the same
|
|
46
|
-
// in-memory MCP harness the stdio suite uses, wired exactly as `worker.ts` wires
|
|
47
|
-
// it (BASE_TOOL_REGISTRARS), rather than through the token-gated `/mcp` route.
|
|
48
|
-
|
|
49
|
-
describe('gogcli Cloudflare connector — OAuth surface', () => {
|
|
50
|
-
it('serves the OAuth authorization-server discovery document', async () => {
|
|
51
|
-
const res = await SELF.fetch('https://example.com/.well-known/oauth-authorization-server');
|
|
52
|
-
expect(res.status).toBe(200);
|
|
53
|
-
const meta = (await res.json()) as { authorization_endpoint?: string; token_endpoint?: string };
|
|
54
|
-
expect(meta.authorization_endpoint).toContain('/authorize');
|
|
55
|
-
expect(meta.token_endpoint).toContain('/token');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('rejects an unauthenticated /mcp request', async () => {
|
|
59
|
-
const res = await SELF.fetch('https://example.com/mcp', {
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: { 'content-type': 'application/json' },
|
|
62
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }),
|
|
63
|
-
});
|
|
64
|
-
expect(res.status).toBe(401);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Each per-service path is wired as an auth-gated API route (→ 401), not a
|
|
68
|
-
// 404 from the default handler. (A 401 confirms the path is registered and
|
|
69
|
-
// token-gated; correct routing to the SERVICE agent vs the base agent is
|
|
70
|
-
// verified live post-deploy via an authenticated tools/list.)
|
|
71
|
-
for (const { path } of SERVICE_PATHS) {
|
|
72
|
-
it(`rejects an unauthenticated ${path} request`, async () => {
|
|
73
|
-
const res = await SELF.fetch(`https://example.com${path}`, {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: { 'content-type': 'application/json' },
|
|
76
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }),
|
|
77
|
-
});
|
|
78
|
-
expect(res.status).toBe(401);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
it('GET /authorize renders the gogcli login page with the connector-key field', async () => {
|
|
83
|
-
// `/authorize` is only reachable for a REGISTERED client, so the request has
|
|
84
|
-
// to be preceded by a real dynamic client registration.
|
|
85
|
-
//
|
|
86
|
-
// It did not always: workers-oauth-provider 0.8.x parsed the request without
|
|
87
|
-
// a `client_id` (it only called validateRedirectUriScheme unconditionally,
|
|
88
|
-
// which is why `redirect_uri` was already here). 0.10.x moved the
|
|
89
|
-
// `client_id is required` check and the client lookup ahead of everything
|
|
90
|
-
// else in parseAuthRequest, so the old URL now throws before the login page
|
|
91
|
-
// is ever rendered — an AuthorizationError from inside the library, not from
|
|
92
|
-
// any code in this repo. Registering first is the fix, and it exercises the
|
|
93
|
-
// path a real client actually takes.
|
|
94
|
-
const redirectUri = 'https://example.com/callback';
|
|
95
|
-
const registration = await SELF.fetch('https://example.com/register', {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: { 'content-type': 'application/json' },
|
|
98
|
-
body: JSON.stringify({
|
|
99
|
-
client_name: 'worker test client',
|
|
100
|
-
redirect_uris: [redirectUri],
|
|
101
|
-
token_endpoint_auth_method: 'none',
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
104
|
-
expect(registration.status).toBe(201);
|
|
105
|
-
const { client_id: clientId } = (await registration.json()) as { client_id: string };
|
|
106
|
-
expect(clientId).toBeTruthy();
|
|
107
|
-
|
|
108
|
-
// PKCE is mandatory for a public client (`token_endpoint_auth_method: none`)
|
|
109
|
-
// on the authorization-code flow, and the provider enforces it during the
|
|
110
|
-
// same parse. The challenge is never redeemed here — this test stops at the
|
|
111
|
-
// rendered page — so any well-formed S256 value will do.
|
|
112
|
-
const codeChallenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';
|
|
113
|
-
const res = await SELF.fetch(
|
|
114
|
-
'https://example.com/authorize?response_type=code&state=abc' +
|
|
115
|
-
`&client_id=${encodeURIComponent(clientId)}` +
|
|
116
|
-
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
117
|
-
`&code_challenge=${codeChallenge}&code_challenge_method=S256`,
|
|
118
|
-
);
|
|
119
|
-
expect(res.status).toBe(200);
|
|
120
|
-
expect(res.headers.get('content-type')).toContain('text/html');
|
|
121
|
-
const html = await res.text();
|
|
122
|
-
expect(html).toContain('gogcli');
|
|
123
|
-
expect(html).toContain('gogcli connector key');
|
|
124
|
-
expect(html).toContain('type="password"');
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('gogcli Cloudflare connector — tool surface', () => {
|
|
129
|
-
it('registers the base gog tool set via the same registrars as worker.ts', async () => {
|
|
130
|
-
// Mirror src/worker.ts's `tools` wiring: register every BASE_TOOL_REGISTRARS
|
|
131
|
-
// onto the in-memory harness server (no executor needed just to list tools).
|
|
132
|
-
const harness = await createTestHarness((server) => {
|
|
133
|
-
for (const register of BASE_TOOL_REGISTRARS) register(server);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const names = (await harness.listTools()).map((t) => t.name);
|
|
138
|
-
// Spot-check representative tools from multiple services + a run escape hatch.
|
|
139
|
-
expect(names).toContain('gog_sheets_get');
|
|
140
|
-
expect(names).toContain('gog_gmail_search');
|
|
141
|
-
expect(names).toContain('gog_drive_ls');
|
|
142
|
-
expect(names).toContain('gog_sheets_run');
|
|
143
|
-
// The base package exposes a broad multi-service surface.
|
|
144
|
-
expect(names.length).toBeGreaterThan(50);
|
|
145
|
-
} finally {
|
|
146
|
-
await harness.close();
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Each per-service path registers auth + that service's base + its EXTRAS —
|
|
151
|
-
// proving the extended tool set is wired (well past the ~13 base+auth tools).
|
|
152
|
-
for (const { path, regs, baseTool, minTools } of SERVICE_PATHS) {
|
|
153
|
-
it(`${path} registers the extended ${baseTool.split('_')[1]} tool set (base + extras)`, async () => {
|
|
154
|
-
const harness = await createTestHarness((server) => {
|
|
155
|
-
for (const register of regs) register(server);
|
|
156
|
-
});
|
|
157
|
-
try {
|
|
158
|
-
const names = (await harness.listTools()).map((t) => t.name);
|
|
159
|
-
expect(names).toContain(baseTool); // the service's base op
|
|
160
|
-
expect(names).toContain('gog_auth_list'); // auth tools included
|
|
161
|
-
expect(names.length).toBeGreaterThan(minTools); // extras present, not just base
|
|
162
|
-
} finally {
|
|
163
|
-
await harness.close();
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
});
|