gogcli-mcp 2.30.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 +15345 -18417
- package/dist/lib.js +13720 -9481
- package/manifest.json +2 -2
- package/mint.yaml +37 -33
- package/package.json +5 -5
- 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/gmail-dispatch-guard.ts +106 -0
- package/src/gmail-results.ts +1 -1
- package/src/index.ts +3 -4
- package/src/lib.ts +24 -9
- package/src/pagination.ts +1 -1
- package/src/runner.ts +23 -175
- package/src/tools/api.ts +7 -7
- package/src/tools/appscript.ts +16 -16
- package/src/tools/auth.ts +16 -16
- package/src/tools/calendar.ts +13 -13
- package/src/tools/chat.ts +25 -25
- package/src/tools/classroom.ts +49 -49
- package/src/tools/contacts.ts +9 -9
- package/src/tools/docs.ts +13 -13
- package/src/tools/drive.ts +23 -25
- package/src/tools/gmail.ts +145 -32
- package/src/tools/sheets.ts +15 -15
- package/src/tools/slides.ts +13 -13
- package/src/tools/tasks.ts +13 -13
- package/src/tools/utils.ts +14 -61
- 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/gmail-dispatch-guard.test.ts +132 -0
- package/tests/runner-file-args.test.ts +1 -13
- package/tests/runner.test.ts +8 -95
- package/tests/sdk-single-copy.test.ts +11 -37
- package/tests/tools/appscript.test.ts +1 -1
- package/tests/tools/auth-401-shapes.test.ts +2 -3
- package/tests/tools/auth.test.ts +5 -4
- package/tests/tools/chat.test.ts +1 -1
- package/tests/tools/drive.test.ts +11 -3
- package/tests/tools/gmail.test.ts +244 -14
- package/tests/tools/sheets.test.ts +1 -1
- package/tests/tools/utils.test.ts +1 -50
- package/tests/zod-single-copy.test.ts +8 -16
- package/tsconfig.json +1 -4
- package/vitest.config.ts +2 -14
- package/src/auth-log.ts +0 -205
- package/src/connector-auth.ts +0 -303
- package/src/connector-runtime.ts +0 -887
- 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 -129
- package/tests/auth-log.test.ts +0 -530
- package/tests/connector-auth.test.ts +0 -559
- package/tests/connector-runtime.test.ts +0 -1644
- 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
package/src/tools/slides.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
|
|
4
4
|
|
|
@@ -6,13 +6,13 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
6
6
|
server.registerTool('gog_slides_export', {
|
|
7
7
|
description: 'Export a Google Slides presentation to a local file (pdf or pptx).',
|
|
8
8
|
annotations: { readOnlyHint: true },
|
|
9
|
-
inputSchema: {
|
|
9
|
+
inputSchema: z.object({
|
|
10
10
|
presentationId: z.string().describe('Presentation ID'),
|
|
11
11
|
out: z.string().optional().describe('Output file path'),
|
|
12
12
|
format: z.enum(['pdf', 'pptx']).optional().describe('Export format (default: pptx)'),
|
|
13
13
|
overwrite: z.boolean().optional().describe('Overwrite the output file if it already exists (gog refuses otherwise)'),
|
|
14
14
|
account: accountParam,
|
|
15
|
-
},
|
|
15
|
+
}),
|
|
16
16
|
}, async ({ presentationId, out, format, overwrite, account }) => {
|
|
17
17
|
const args = ['slides', 'export', presentationId];
|
|
18
18
|
if (out) args.push(`--out=${out}`);
|
|
@@ -24,22 +24,22 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
24
24
|
server.registerTool('gog_slides_info', {
|
|
25
25
|
description: 'Get metadata for a Google Slides presentation (title, ID, slide count, etc.).',
|
|
26
26
|
annotations: { readOnlyHint: true },
|
|
27
|
-
inputSchema: {
|
|
27
|
+
inputSchema: z.object({
|
|
28
28
|
presentationId: z.string().describe('Presentation ID'),
|
|
29
29
|
account: accountParam,
|
|
30
|
-
},
|
|
30
|
+
}),
|
|
31
31
|
}, async ({ presentationId, account }) => {
|
|
32
32
|
return runOrDiagnose(['slides', 'info', presentationId], { account });
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
server.registerTool('gog_slides_create', {
|
|
36
36
|
description: 'Create a new Google Slides presentation, optionally in a folder or copying from a template.',
|
|
37
|
-
inputSchema: {
|
|
37
|
+
inputSchema: z.object({
|
|
38
38
|
title: z.string().describe('Presentation title'),
|
|
39
39
|
parent: z.string().optional().describe('Destination folder ID'),
|
|
40
40
|
template: z.string().optional().describe('Template presentation ID to copy from'),
|
|
41
41
|
account: accountParam,
|
|
42
|
-
},
|
|
42
|
+
}),
|
|
43
43
|
}, async ({ title, parent, template, account }) => {
|
|
44
44
|
const args = ['slides', 'create', title];
|
|
45
45
|
if (parent) args.push(`--parent=${parent}`);
|
|
@@ -49,12 +49,12 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
49
49
|
|
|
50
50
|
server.registerTool('gog_slides_copy', {
|
|
51
51
|
description: 'Copy a Google Slides presentation to a new presentation with the given title.',
|
|
52
|
-
inputSchema: {
|
|
52
|
+
inputSchema: z.object({
|
|
53
53
|
presentationId: z.string().describe('Presentation ID to copy'),
|
|
54
54
|
title: z.string().describe('Title for the new copy'),
|
|
55
55
|
parent: z.string().optional().describe('Destination folder ID'),
|
|
56
56
|
account: accountParam,
|
|
57
|
-
},
|
|
57
|
+
}),
|
|
58
58
|
}, async ({ presentationId, title, parent, account }) => {
|
|
59
59
|
const args = ['slides', 'copy', presentationId, title];
|
|
60
60
|
if (parent) args.push(`--parent=${parent}`);
|
|
@@ -64,10 +64,10 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
64
64
|
server.registerTool('gog_slides_list_slides', {
|
|
65
65
|
description: 'List slides in a Google Slides presentation.',
|
|
66
66
|
annotations: { readOnlyHint: true },
|
|
67
|
-
inputSchema: {
|
|
67
|
+
inputSchema: z.object({
|
|
68
68
|
presentationId: z.string().describe('Presentation ID'),
|
|
69
69
|
account: accountParam,
|
|
70
|
-
},
|
|
70
|
+
}),
|
|
71
71
|
}, async ({ presentationId, account }) => {
|
|
72
72
|
return runOrDiagnose(['slides', 'list-slides', presentationId], { account });
|
|
73
73
|
});
|
|
@@ -75,12 +75,12 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
75
75
|
server.registerTool('gog_slides_read_slide', {
|
|
76
76
|
description: 'Read the content of a slide (text, shapes, speaker notes). Set detail=true to also include normalized element geometry, styled text runs, paragraphs, table-cell content, and image source URLs.',
|
|
77
77
|
annotations: { readOnlyHint: true },
|
|
78
|
-
inputSchema: {
|
|
78
|
+
inputSchema: z.object({
|
|
79
79
|
presentationId: z.string().describe('Presentation ID'),
|
|
80
80
|
slideId: z.string().describe('Slide ID to read'),
|
|
81
81
|
detail: z.boolean().optional().describe('Include normalized element geometry, styled text runs, paragraphs, table-cell content, and image source URLs'),
|
|
82
82
|
account: accountParam,
|
|
83
|
-
},
|
|
83
|
+
}),
|
|
84
84
|
}, async ({ presentationId, slideId, detail, account }) => {
|
|
85
85
|
const args = ['slides', 'read-slide', presentationId, slideId];
|
|
86
86
|
if (detail) args.push('--detail');
|
package/src/tools/tasks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
|
|
4
4
|
|
|
@@ -6,9 +6,9 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
6
6
|
server.registerTool('gog_tasks_lists', {
|
|
7
7
|
description: 'List all Google Task lists.',
|
|
8
8
|
annotations: { readOnlyHint: true },
|
|
9
|
-
inputSchema: {
|
|
9
|
+
inputSchema: z.object({
|
|
10
10
|
account: accountParam,
|
|
11
|
-
},
|
|
11
|
+
}),
|
|
12
12
|
}, async ({ account }) => {
|
|
13
13
|
return runOrDiagnose(['tasks', 'lists', 'list'], { account });
|
|
14
14
|
});
|
|
@@ -16,10 +16,10 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
16
16
|
server.registerTool('gog_tasks_list', {
|
|
17
17
|
description: 'List tasks in a Google Task list.',
|
|
18
18
|
annotations: { readOnlyHint: true },
|
|
19
|
-
inputSchema: {
|
|
19
|
+
inputSchema: z.object({
|
|
20
20
|
tasklistId: z.string().describe('Task list ID (use gog_tasks_lists to find IDs)'),
|
|
21
21
|
account: accountParam,
|
|
22
|
-
},
|
|
22
|
+
}),
|
|
23
23
|
}, async ({ tasklistId, account }) => {
|
|
24
24
|
return runOrDiagnose(['tasks', 'list', tasklistId], { account });
|
|
25
25
|
});
|
|
@@ -27,11 +27,11 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
27
27
|
server.registerTool('gog_tasks_get', {
|
|
28
28
|
description: 'Get a specific task by ID.',
|
|
29
29
|
annotations: { readOnlyHint: true },
|
|
30
|
-
inputSchema: {
|
|
30
|
+
inputSchema: z.object({
|
|
31
31
|
tasklistId: z.string().describe('Task list ID'),
|
|
32
32
|
taskId: z.string().describe('Task ID'),
|
|
33
33
|
account: accountParam,
|
|
34
|
-
},
|
|
34
|
+
}),
|
|
35
35
|
}, async ({ tasklistId, taskId, account }) => {
|
|
36
36
|
return runOrDiagnose(['tasks', 'get', tasklistId, taskId], { account });
|
|
37
37
|
});
|
|
@@ -39,13 +39,13 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
39
39
|
server.registerTool('gog_tasks_add', {
|
|
40
40
|
description: 'Add a task to a Google Task list.',
|
|
41
41
|
annotations: { destructiveHint: false },
|
|
42
|
-
inputSchema: {
|
|
42
|
+
inputSchema: z.object({
|
|
43
43
|
tasklistId: z.string().describe('Task list ID'),
|
|
44
44
|
title: z.string().describe('Task title'),
|
|
45
45
|
notes: z.string().optional().describe('Task notes/description'),
|
|
46
46
|
due: z.string().optional().describe('Due date (YYYY-MM-DD or RFC3339)'),
|
|
47
47
|
account: accountParam,
|
|
48
|
-
},
|
|
48
|
+
}),
|
|
49
49
|
}, async ({ tasklistId, title, notes, due, account }) => {
|
|
50
50
|
const args = ['tasks', 'add', tasklistId, `--title=${title}`];
|
|
51
51
|
if (notes) args.push(`--notes=${notes}`);
|
|
@@ -56,11 +56,11 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
56
56
|
server.registerTool('gog_tasks_done', {
|
|
57
57
|
description: 'Mark a task as completed.',
|
|
58
58
|
annotations: { destructiveHint: true },
|
|
59
|
-
inputSchema: {
|
|
59
|
+
inputSchema: z.object({
|
|
60
60
|
tasklistId: z.string().describe('Task list ID'),
|
|
61
61
|
taskId: z.string().describe('Task ID'),
|
|
62
62
|
account: accountParam,
|
|
63
|
-
},
|
|
63
|
+
}),
|
|
64
64
|
}, async ({ tasklistId, taskId, account }) => {
|
|
65
65
|
return runOrDiagnose(['tasks', 'done', tasklistId, taskId], { account });
|
|
66
66
|
});
|
|
@@ -68,11 +68,11 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
68
68
|
server.registerTool('gog_tasks_delete', {
|
|
69
69
|
description: 'Delete a task.',
|
|
70
70
|
annotations: { destructiveHint: true },
|
|
71
|
-
inputSchema: {
|
|
71
|
+
inputSchema: z.object({
|
|
72
72
|
tasklistId: z.string().describe('Task list ID'),
|
|
73
73
|
taskId: z.string().describe('Task ID'),
|
|
74
74
|
account: accountParam,
|
|
75
|
-
},
|
|
75
|
+
}),
|
|
76
76
|
}, async ({ tasklistId, taskId, account }) => {
|
|
77
77
|
// gog gates this delete behind a confirmation; the runner injects
|
|
78
78
|
// --no-input, so without --force it refuses at runtime.
|
package/src/tools/utils.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
3
|
-
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { McpServer, CallToolResult } from '@modelcontextprotocol/server';
|
|
4
3
|
import { errorResult, rawTextResult, minifiedResult, stripMediaUrls } from '@chrischall/mcp-utils';
|
|
5
|
-
import { run
|
|
6
|
-
import type { GogArg
|
|
4
|
+
import { run } from '../runner.js';
|
|
5
|
+
import type { GogArg } from '../runner.js';
|
|
7
6
|
import { normalizeTimestamps } from '../timestamps.js';
|
|
8
7
|
import { stripConsumedPageToken } from '../pagination.js';
|
|
9
8
|
|
|
@@ -16,23 +15,20 @@ import { stripConsumedPageToken } from '../pagination.js';
|
|
|
16
15
|
// "\n" cannot round-trip byte-for-byte through the file path. Second, the file
|
|
17
16
|
// path costs a temp dir, a write, and a delete per call.
|
|
18
17
|
//
|
|
19
|
-
// The value
|
|
20
|
-
// payloads could leave argv
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// be at most 4096 chars") now take the file path and its newline trim. The
|
|
25
|
-
// runner's plain-arg cap is now 64 KiB, so a 4096-byte inline value is nowhere
|
|
26
|
-
// near being rejected.
|
|
18
|
+
// The value is the historical per-arg cap (4096) that applied BEFORE large
|
|
19
|
+
// payloads could leave argv. That is deliberate: every body that used to
|
|
20
|
+
// round-trip inline byte-for-byte still does, so there is no trailing-newline
|
|
21
|
+
// regression for any body that already worked — only bodies that previously
|
|
22
|
+
// exceeded the cap and hard-failed now take the file path and its newline trim.
|
|
27
23
|
export const PAYLOAD_INLINE_MAX = 4096;
|
|
28
24
|
|
|
29
25
|
// The ONE place the inline-vs-file decision is made. Every tool that has a
|
|
30
26
|
// gog `--x` / `--x-file` flag pair routes its value through here so the
|
|
31
27
|
// threshold cannot drift between tools.
|
|
32
28
|
//
|
|
33
|
-
// Measures BYTES, not characters: the
|
|
34
|
-
//
|
|
35
|
-
//
|
|
29
|
+
// Measures BYTES, not characters: the Linux kernel's MAX_ARG_STRLEN is
|
|
30
|
+
// byte-based, so a multibyte-heavy body (CJK, emoji) would slip past a
|
|
31
|
+
// `.length` check at up to 4x its real argv cost.
|
|
36
32
|
export function payloadArg(
|
|
37
33
|
inlineFlag: string,
|
|
38
34
|
fileFlag: string,
|
|
@@ -149,7 +145,7 @@ export function registerRunTool(
|
|
|
149
145
|
server.registerTool(`gog_${service}_run`, {
|
|
150
146
|
description,
|
|
151
147
|
annotations: { destructiveHint: true },
|
|
152
|
-
inputSchema,
|
|
148
|
+
inputSchema: z.object(inputSchema),
|
|
153
149
|
}, async (rawArgs) => {
|
|
154
150
|
const { subcommand, args, account } = rawArgs as { subcommand: string; args: string[]; account?: string };
|
|
155
151
|
return runOrDiagnose([service, subcommand, ...args], { account });
|
|
@@ -237,31 +233,6 @@ const GRID_LIMIT_HINT =
|
|
|
237
233
|
'\n\nThe target range is outside the sheet\'s current grid. Add the missing rows or columns ' +
|
|
238
234
|
'first with gog_sheets_insert (dimension: rows or cols), then retry the write.';
|
|
239
235
|
|
|
240
|
-
// The hint for each RUNNER-authored failure kind (see RunnerTransportError in
|
|
241
|
-
// runner.ts). These are chosen by the error's TYPE, never by reading its text.
|
|
242
|
-
//
|
|
243
|
-
// transport-auth is the one that motivated all of this. The runner answers a
|
|
244
|
-
// bad bearer with the single word "unauthorized"; read as prose that is
|
|
245
|
-
// indistinguishable from Google rejecting a credential, and the caller was
|
|
246
|
-
// being told all session to re-authorize an account that had never been asked
|
|
247
|
-
// for anything. So this hint names the real cause and says outright that
|
|
248
|
-
// re-authorizing cannot help. It deliberately does NOT contain the literal
|
|
249
|
-
// `gog_auth_add`, which is the token the rest of the auth guidance keys on.
|
|
250
|
-
const RUNNER_TRANSPORT_AUTH_HINT =
|
|
251
|
-
'\n\nThis is the CONNECTOR\'s own transport auth failing, not your Google sign-in. The gog-runner ' +
|
|
252
|
-
'backend rejected the bearer token this server sent, so the request never reached gog and no Google ' +
|
|
253
|
-
'credential was checked — the Google account is not the problem and re-authorizing it cannot fix this. ' +
|
|
254
|
-
'An operator must make the Worker secret GOG_RUNNER_KEY equal RUNNER_KEY on the Fly app ' +
|
|
255
|
-
'(wrangler secret put GOG_RUNNER_KEY / fly secrets set RUNNER_KEY), then retry.';
|
|
256
|
-
|
|
257
|
-
const RUNNER_TRANSPORT_HINTS: Record<RunnerFailureKind, string> = {
|
|
258
|
-
'transport-auth': RUNNER_TRANSPORT_AUTH_HINT,
|
|
259
|
-
// The request itself was malformed, so the runner will refuse it identically
|
|
260
|
-
// every time. Nothing to advise beyond the message the runner already gave.
|
|
261
|
-
'transport-request': '',
|
|
262
|
-
'transport-retryable': TRANSIENT_HINT,
|
|
263
|
-
};
|
|
264
|
-
|
|
265
236
|
// Reduce `gog auth list --json` output to just the configured email addresses.
|
|
266
237
|
// The raw JSON also carries OAuth scopes, the Google subject id, and creation
|
|
267
238
|
// timestamps — none of which belong in an error surfaced to the model, and
|
|
@@ -292,21 +263,6 @@ export function formatAccountList(raw: string): string {
|
|
|
292
263
|
export async function diagnose(err: unknown): Promise<CallToolResult> {
|
|
293
264
|
const errText = errorText(err);
|
|
294
265
|
|
|
295
|
-
// STRUCTURE BEFORE PROSE. A RunnerTransportError is this connector's own
|
|
296
|
-
// transport failing — its bearer, its request validation, its drain. Nothing
|
|
297
|
-
// was shown to Google, so none of the patterns below may be consulted for it:
|
|
298
|
-
// they exist to read gog's/Google's words, and the runner's words are not
|
|
299
|
-
// those. Read as prose, the runner's `unauthorized` matched
|
|
300
|
-
// DEFINITE_AUTH_PATTERN and produced AUTH_HINT — a human being told to
|
|
301
|
-
// re-authorize a healthy account over what was really a key mismatch.
|
|
302
|
-
//
|
|
303
|
-
// This short-circuits the ladder rather than joining it, so it is not a new
|
|
304
|
-
// rung in the precedence order documented below; that order still governs
|
|
305
|
-
// every error that genuinely came from gog.
|
|
306
|
-
const transportHint = isRunnerTransportError(err)
|
|
307
|
-
? RUNNER_TRANSPORT_HINTS[err.kind]
|
|
308
|
-
: undefined;
|
|
309
|
-
|
|
310
266
|
const isInvalidGrant = INVALID_GRANT_PATTERN.test(errText);
|
|
311
267
|
|
|
312
268
|
// Precedence, and the reason for it. Reporting needs-auth is EXPENSIVE to be
|
|
@@ -322,10 +278,7 @@ export async function diagnose(err: unknown): Promise<CallToolResult> {
|
|
|
322
278
|
const isTransientError = !DEFINITE_AUTH_PATTERN.test(errText) && TRANSIENT_ERROR_PATTERN.test(errText);
|
|
323
279
|
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
324
280
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
325
|
-
|
|
326
|
-
// "this failure is ours and there is nothing to advise" — and `||` would fall
|
|
327
|
-
// through to the prose ladder for exactly the errors that must never reach it.
|
|
328
|
-
const hint = transportHint ?? (isInvalidGrant
|
|
281
|
+
const hint = isInvalidGrant
|
|
329
282
|
? INVALID_GRANT_HINT
|
|
330
283
|
: isAuthError
|
|
331
284
|
? AUTH_HINT
|
|
@@ -333,7 +286,7 @@ export async function diagnose(err: unknown): Promise<CallToolResult> {
|
|
|
333
286
|
? TRANSIENT_HINT
|
|
334
287
|
: isGridLimitError
|
|
335
288
|
? GRID_LIMIT_HINT
|
|
336
|
-
: ''
|
|
289
|
+
: '';
|
|
337
290
|
try {
|
|
338
291
|
const accounts = formatAccountList(await run(['auth', 'list']));
|
|
339
292
|
return errorResult(`${errText}\n\nConfigured accounts:\n${accounts || '(none)'}${hint}`);
|
|
@@ -125,9 +125,8 @@ describe('inlineAttachmentArgs', () => {
|
|
|
125
125
|
// The budget belongs to the REQUEST, not to the attachments. `payloadArg`
|
|
126
126
|
// turns any body over 4 KiB into a GogFileArg that rides in the same JSON
|
|
127
127
|
// body at ~1:1, so a near-max attachment set plus a multi-MiB body overruns
|
|
128
|
-
// the
|
|
129
|
-
//
|
|
130
|
-
// prevent, so the sibling args are measured rather than assumed small.
|
|
128
|
+
// the budget even though each input is inside its own documented limit, so
|
|
129
|
+
// the sibling args are measured rather than assumed small.
|
|
131
130
|
it('counts the message body against the same budget as the attachments', () => {
|
|
132
131
|
// Three files just under the 8 MiB per-file cap, summing to just under the
|
|
133
132
|
// per-message total — i.e. every input inside its own documented limit.
|
|
@@ -176,20 +175,18 @@ describe('inlineAttachmentArgs', () => {
|
|
|
176
175
|
|
|
177
176
|
// THE INVARIANT behind the per-message ceiling, asserted rather than trusted.
|
|
178
177
|
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
it('keeps a full message under the Fly runner request-body cap once base64-inflated', () => {
|
|
187
|
-
const RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024; // fly-gog-runner/server.mjs
|
|
178
|
+
// Every payload rides base64-encoded inside ONE JSON body, capped at a 32 MiB
|
|
179
|
+
// request budget. Base64 inflates by 4/3, so a ceiling expressed in decoded
|
|
180
|
+
// bytes has to be derived from the wire cap or it documents a size that cannot
|
|
181
|
+
// actually be sent. A 25 MiB total encoded to 34,952,536 chars against a
|
|
182
|
+
// 33,554,432 cap, so the limit was unreachable.
|
|
183
|
+
it('keeps a full message under the 32 MiB request budget once base64-inflated', () => {
|
|
184
|
+
const REQUEST_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
188
185
|
const encodedLength = (decoded: number): number => 4 * Math.ceil(decoded / 3);
|
|
189
186
|
|
|
190
187
|
// The payload budget must leave the JSON structure room inside the cap…
|
|
191
|
-
expect(MAX_REQUEST_PAYLOAD_WIRE_BYTES).toBeLessThan(
|
|
192
|
-
expect(
|
|
188
|
+
expect(MAX_REQUEST_PAYLOAD_WIRE_BYTES).toBeLessThan(REQUEST_MAX_BODY_BYTES);
|
|
189
|
+
expect(REQUEST_MAX_BODY_BYTES - MAX_REQUEST_PAYLOAD_WIRE_BYTES).toBeGreaterThanOrEqual(128 * 1024);
|
|
193
190
|
// …and a full attachment set must fit inside that budget once inflated.
|
|
194
191
|
expect(encodedLength(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)).toBeLessThanOrEqual(MAX_REQUEST_PAYLOAD_WIRE_BYTES);
|
|
195
192
|
// The advertised number must itself be sendable — floor, not round.
|