@lanes-sh/link 0.1.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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ProviderContext, ToolCapability, ToolResult } from '#connectivity';
|
|
3
|
+
import {
|
|
4
|
+
attachmentRefSchema,
|
|
5
|
+
composeMime,
|
|
6
|
+
receiptFor,
|
|
7
|
+
resolveAttachments,
|
|
8
|
+
type ResolvedAttachment,
|
|
9
|
+
} from '#connectivity/mail';
|
|
10
|
+
import {
|
|
11
|
+
GMAIL_API,
|
|
12
|
+
GMAIL_JSON_LIMIT_BYTES,
|
|
13
|
+
GMAIL_MAX_MESSAGE_BYTES,
|
|
14
|
+
GMAIL_UPLOAD,
|
|
15
|
+
} from './api.ts';
|
|
16
|
+
import { gmailAttachments } from './attachment.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sending mail, composed here rather than by the caller.
|
|
20
|
+
*
|
|
21
|
+
* The one capability on this provider that is code instead of data, and the
|
|
22
|
+
* reason is narrow: Gmail's API takes a whole assembled RFC 2822 message as one
|
|
23
|
+
* base64url field. Nothing in an OpenAPI document describes composing one, so the
|
|
24
|
+
* generated tools hand that job to the caller — which for a plain message is
|
|
25
|
+
* merely absurd, and for a message with an attachment is impossible. A 239 KB PDF
|
|
26
|
+
* is about 320,000 characters of base64, past what a model can write in one
|
|
27
|
+
* message. That is a real failure someone hit, not a hypothetical.
|
|
28
|
+
*
|
|
29
|
+
* Note there was no send tool here at all before this. The omission was
|
|
30
|
+
* deliberate — "the token can send, the tool surface cannot" — and reversing it
|
|
31
|
+
* is the point of the change rather than an accident of it. `gmail.compose`
|
|
32
|
+
* already permits it, `draft_only` keeps the review step available, and
|
|
33
|
+
* `lanes link policy deny gmail.send_message` still takes it away.
|
|
34
|
+
*
|
|
35
|
+
* `draft_id` extends the same argument to revising one. The generated
|
|
36
|
+
* `drafts.update` had the identical defect its sibling `drafts.create` was
|
|
37
|
+
* removed for — a `Draft` body whose `message.raw` the caller assembles — so
|
|
38
|
+
* correcting a draft that carried a file meant deleting it and composing the
|
|
39
|
+
* whole thing again, re-fetching every attachment on the way. Here the message
|
|
40
|
+
* is recomposed as it always was and the only difference is the address it goes
|
|
41
|
+
* to: a `PUT` at the draft rather than a `POST` at the collection.
|
|
42
|
+
*
|
|
43
|
+
* Sending after a revision is two requests rather than one. Gmail's
|
|
44
|
+
* `drafts.send` does accept a whole `Draft`, which would fold the update into
|
|
45
|
+
* the send, but the merge semantics are not something this code can verify from
|
|
46
|
+
* here — and the failure mode if they are not what one assumed is that the
|
|
47
|
+
* *previous* draft goes out, silently, to real recipients. Updating and then
|
|
48
|
+
* sending by id costs a round trip on a path already measured in seconds, and
|
|
49
|
+
* both halves are documented single-purpose calls.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const schema = z.object({
|
|
53
|
+
to: z.array(z.string()).min(1).describe('Recipients.'),
|
|
54
|
+
cc: z.array(z.string()).optional(),
|
|
55
|
+
bcc: z.array(z.string()).optional(),
|
|
56
|
+
subject: z.string(),
|
|
57
|
+
text: z.string().optional().describe('Plain-text body.'),
|
|
58
|
+
html: z.string().optional().describe('HTML body. Send text as well where you can.'),
|
|
59
|
+
in_reply_to: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Message-ID being replied to, which threads the reply.'),
|
|
63
|
+
from_name: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe(
|
|
67
|
+
'Name to show beside the address. Rarely needed here — omitted, Gmail fills in the account\'s own name — so pass it only to send under a different one.',
|
|
68
|
+
),
|
|
69
|
+
attachments: z
|
|
70
|
+
.array(attachmentRefSchema)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(
|
|
73
|
+
'Files to attach, each named by reference. This endpoint reads the bytes itself — never encode a file into this call.',
|
|
74
|
+
),
|
|
75
|
+
// The warning in the description is there because the failure happened. A
|
|
76
|
+
// draft carrying a 204 KB PDF took 19 seconds — the bytes were pulled back
|
|
77
|
+
// out of the mailbox and re-uploaded — and the client, having no result it
|
|
78
|
+
// trusted, listed the drafts and then sent the identical call twice more.
|
|
79
|
+
// Three drafts, all of them ours to explain. Nothing here retries, so the
|
|
80
|
+
// repair is telling the caller what a slow call means and giving it
|
|
81
|
+
// `drafts.list` and `drafts.delete` to act on, rather than inventing an
|
|
82
|
+
// idempotency key this endpoint has nowhere to keep.
|
|
83
|
+
draft_only: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
'Save as a draft instead of sending, so it can be reviewed first. This call is not idempotent and a message with an attachment can take tens of seconds, because the bytes are fetched and uploaded here: if it appears to time out, the draft may already exist. List drafts to check before calling again, and delete the surplus one rather than leaving both.',
|
|
88
|
+
),
|
|
89
|
+
draft_id: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
'Revise this existing draft instead of writing a new one — pass the id a previous call returned. Every other argument describes the message as it should now read, because the draft is replaced rather than merged into: name the attachments again to keep them. With draft_only the revision stays a draft; without it, the revised draft is sent.',
|
|
94
|
+
),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Built rather than declared, so `fetch` can be substituted in a test.
|
|
99
|
+
*
|
|
100
|
+
* The same seam every transport here offers. Without it a test has to reach for
|
|
101
|
+
* the global, and a test that patches `globalThis` leaks into whatever runs
|
|
102
|
+
* beside it.
|
|
103
|
+
*/
|
|
104
|
+
export function gmailSend(
|
|
105
|
+
options: { readonly fetch?: typeof globalThis.fetch } = {},
|
|
106
|
+
): ToolCapability<typeof schema> {
|
|
107
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
kind: 'tool',
|
|
111
|
+
name: 'send_message',
|
|
112
|
+
description:
|
|
113
|
+
'Send a message, with attachments, or save it as a draft. Attachments are named by reference — a path on this machine, an HTTPS URL, a staged handle, or another message in this mailbox — and this endpoint fetches the bytes itself, so never encode a file into the call.',
|
|
114
|
+
inputSchema: schema,
|
|
115
|
+
|
|
116
|
+
async handler(input, context): Promise<ToolResult> {
|
|
117
|
+
const authorize = context.authorize;
|
|
118
|
+
if (!authorize) {
|
|
119
|
+
// Only reachable if this capability is ever registered on a provider with
|
|
120
|
+
// no credential, which the manifest makes impossible. Stated rather than
|
|
121
|
+
// asserted non-null, so the failure names its cause.
|
|
122
|
+
return fail('This connection has no credential, so it cannot send.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let attachments: ResolvedAttachment[];
|
|
126
|
+
try {
|
|
127
|
+
attachments = await resolveAttachments(input.attachments, {
|
|
128
|
+
// Three quarters of the ceiling: attachments travel base64, so this is
|
|
129
|
+
// the raw weight that fits once encoded. The composed message is measured
|
|
130
|
+
// exactly below.
|
|
131
|
+
maxTotalBytes: Math.floor((GMAIL_MAX_MESSAGE_BYTES * 3) / 4),
|
|
132
|
+
signal: context.signal,
|
|
133
|
+
mailbox: gmailAttachments({ authorize, fetch: doFetch, signal: context.signal }),
|
|
134
|
+
storage: context.storage,
|
|
135
|
+
});
|
|
136
|
+
} catch (failure) {
|
|
137
|
+
return fail((failure as Error).message);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Normally no `From` at all: Gmail fills it from the credential, name
|
|
141
|
+
// included, which is better than anything guessable here — and inventing an
|
|
142
|
+
// address risks writing one this account may not send as. A name is written
|
|
143
|
+
// only when someone asked for one, and then the address has to come with it,
|
|
144
|
+
// since a display name alone is not a header.
|
|
145
|
+
const configured = context.connection.config['from_name'];
|
|
146
|
+
const fromName =
|
|
147
|
+
input.from_name ??
|
|
148
|
+
(typeof configured === 'string' && configured.trim() !== '' ? configured : undefined);
|
|
149
|
+
const address = context.connection.displayName;
|
|
150
|
+
|
|
151
|
+
const composed = await composeMime({
|
|
152
|
+
...(fromName && address.includes('@') ? { from: address, fromName } : {}),
|
|
153
|
+
message: {
|
|
154
|
+
to: input.to,
|
|
155
|
+
cc: input.cc,
|
|
156
|
+
bcc: input.bcc,
|
|
157
|
+
subject: input.subject,
|
|
158
|
+
text: input.text,
|
|
159
|
+
html: input.html,
|
|
160
|
+
inReplyTo: input.in_reply_to,
|
|
161
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (composed.raw.byteLength > GMAIL_MAX_MESSAGE_BYTES) {
|
|
166
|
+
return fail(
|
|
167
|
+
`The composed message is ${composed.raw.byteLength} bytes and Gmail accepts ${GMAIL_MAX_MESSAGE_BYTES}. Attachments are base64 in transit, so they weigh about a third more than on disk.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Recorded before submitting, so a file read off disk is in the log even if
|
|
172
|
+
// the send then fails. `redact` cannot express this — it keeps an argument
|
|
173
|
+
// verbatim, and `attachments` may itself hold a base64 file.
|
|
174
|
+
if (attachments.length > 0) {
|
|
175
|
+
context.audit.annotate({
|
|
176
|
+
attachments: attachments.map((attachment) => ({
|
|
177
|
+
filename: attachment.filename,
|
|
178
|
+
bytes: attachment.bytes.byteLength,
|
|
179
|
+
content_type: attachment.contentType,
|
|
180
|
+
sha256: attachment.sha256,
|
|
181
|
+
origin: attachment.origin,
|
|
182
|
+
})),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const drafting = input.draft_only === true;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
let sent = await submit({
|
|
190
|
+
raw: composed.raw,
|
|
191
|
+
draftOnly: drafting,
|
|
192
|
+
draftId: input.draft_id,
|
|
193
|
+
authorize,
|
|
194
|
+
fetch: doFetch,
|
|
195
|
+
context,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// A revision that is not staying a draft is sent by id, now that the
|
|
199
|
+
// draft holds the new message. Its own failure is reported separately
|
|
200
|
+
// from the update's, because the two leave the mailbox in different
|
|
201
|
+
// states and only one of them is worth retrying as composed.
|
|
202
|
+
if (input.draft_id !== undefined && !drafting) {
|
|
203
|
+
try {
|
|
204
|
+
sent = await sendDraft({
|
|
205
|
+
draftId: input.draft_id,
|
|
206
|
+
authorize,
|
|
207
|
+
fetch: doFetch,
|
|
208
|
+
context,
|
|
209
|
+
});
|
|
210
|
+
} catch (failure) {
|
|
211
|
+
return fail(
|
|
212
|
+
`The draft was revised but not sent: ${(failure as Error).message} The new message is saved as draft ${input.draft_id}, so send that rather than composing it again.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: 'text',
|
|
221
|
+
text: JSON.stringify(
|
|
222
|
+
{
|
|
223
|
+
...(drafting
|
|
224
|
+
? { drafted: true, draft_id: sent['id'] ?? input.draft_id ?? null }
|
|
225
|
+
: { sent: true }),
|
|
226
|
+
message_id: messageIdOf(sent) ?? composed.messageId,
|
|
227
|
+
recipients: input.to.length + (input.cc?.length ?? 0) + (input.bcc?.length ?? 0),
|
|
228
|
+
// Names, sizes and digests, never content — see `receiptFor`.
|
|
229
|
+
...(attachments.length > 0
|
|
230
|
+
? { attachments: attachments.map(receiptFor) }
|
|
231
|
+
: {}),
|
|
232
|
+
},
|
|
233
|
+
null,
|
|
234
|
+
2,
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
} catch (failure) {
|
|
240
|
+
return fail((failure as Error).message);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The wired capability, as the provider declares it. */
|
|
247
|
+
export const gmailSendMessage = gmailSend();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Hand the assembled message to Gmail.
|
|
251
|
+
*
|
|
252
|
+
* Two routes, chosen by size. A small message goes as base64url in a JSON field,
|
|
253
|
+
* which is the ordinary endpoint and the overwhelmingly common case. A large one
|
|
254
|
+
* goes to the upload host as `message/rfc822` with the raw MIME as the body —
|
|
255
|
+
* `uploadType=media`, no multipart assembly, no resumable session — which is what
|
|
256
|
+
* lifts the ceiling from "what fits in a JSON string" to Gmail's own 35 MiB.
|
|
257
|
+
*/
|
|
258
|
+
async function submit(input: {
|
|
259
|
+
readonly raw: Uint8Array;
|
|
260
|
+
readonly draftOnly: boolean;
|
|
261
|
+
readonly draftId: string | undefined;
|
|
262
|
+
readonly authorize: (request: Request) => Promise<Request>;
|
|
263
|
+
readonly fetch: typeof globalThis.fetch;
|
|
264
|
+
readonly context: ProviderContext;
|
|
265
|
+
}): Promise<Record<string, unknown>> {
|
|
266
|
+
const { raw, draftOnly, draftId, authorize, context } = input;
|
|
267
|
+
const large = raw.byteLength > GMAIL_JSON_LIMIT_BYTES;
|
|
268
|
+
|
|
269
|
+
// Revising is a `PUT` at the draft's own address; the other two are a `POST`
|
|
270
|
+
// at a collection. The upload host takes the same path either way, so size
|
|
271
|
+
// still chooses the host and nothing else.
|
|
272
|
+
const revising = draftId !== undefined;
|
|
273
|
+
const path = revising
|
|
274
|
+
? `/drafts/${encodeURIComponent(draftId)}`
|
|
275
|
+
: draftOnly
|
|
276
|
+
? '/drafts'
|
|
277
|
+
: '/messages/send';
|
|
278
|
+
const url = large ? `${GMAIL_UPLOAD}${path}?uploadType=media` : `${GMAIL_API}${path}`;
|
|
279
|
+
|
|
280
|
+
const encoded = large ? undefined : Buffer.from(raw).toString('base64url');
|
|
281
|
+
// A draft nests the message, whether it is being created or replaced; only a
|
|
282
|
+
// straight send carries `raw` at the top level.
|
|
283
|
+
const body: Uint8Array | string = large
|
|
284
|
+
? raw
|
|
285
|
+
: JSON.stringify(draftOnly || revising ? { message: { raw: encoded } } : { raw: encoded });
|
|
286
|
+
|
|
287
|
+
const request = await authorize(
|
|
288
|
+
new Request(url, {
|
|
289
|
+
method: revising ? 'PUT' : 'POST',
|
|
290
|
+
headers: {
|
|
291
|
+
'content-type': large ? 'message/rfc822' : 'application/json',
|
|
292
|
+
accept: 'application/json',
|
|
293
|
+
},
|
|
294
|
+
body,
|
|
295
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const response = await input.fetch(request);
|
|
300
|
+
const text = await response.text();
|
|
301
|
+
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
throw new Error(`Gmail refused the message with ${response.status}: ${text}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
308
|
+
} catch {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Send a draft that already exists, by id.
|
|
315
|
+
*
|
|
316
|
+
* Always small — the body is one identifier — so there is no upload-host branch
|
|
317
|
+
* here even when the draft it names carries 30 MiB. The bytes were already
|
|
318
|
+
* handed over by the `PUT` that preceded this.
|
|
319
|
+
*/
|
|
320
|
+
async function sendDraft(input: {
|
|
321
|
+
readonly draftId: string;
|
|
322
|
+
readonly authorize: (request: Request) => Promise<Request>;
|
|
323
|
+
readonly fetch: typeof globalThis.fetch;
|
|
324
|
+
readonly context: ProviderContext;
|
|
325
|
+
}): Promise<Record<string, unknown>> {
|
|
326
|
+
const request = await input.authorize(
|
|
327
|
+
new Request(`${GMAIL_API}/drafts/send`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
330
|
+
body: JSON.stringify({ id: input.draftId }),
|
|
331
|
+
...(input.context.signal ? { signal: input.context.signal } : {}),
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const response = await input.fetch(request);
|
|
336
|
+
const text = await response.text();
|
|
337
|
+
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw new Error(`Gmail refused to send the draft with ${response.status}: ${text}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
344
|
+
} catch {
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Gmail answers with its own id; a draft nests the message inside. */
|
|
350
|
+
function messageIdOf(body: Record<string, unknown>): string | null {
|
|
351
|
+
const id = body['id'];
|
|
352
|
+
if (typeof id === 'string' && !('message' in body)) return id;
|
|
353
|
+
|
|
354
|
+
const message = body['message'];
|
|
355
|
+
if (message !== null && typeof message === 'object') {
|
|
356
|
+
const nested = (message as { id?: unknown }).id;
|
|
357
|
+
if (typeof nested === 'string') return nested;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return typeof id === 'string' ? id : null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function fail(message: string): ToolResult {
|
|
364
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
365
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { GMAIL_IDENTITY, GOOGLE_APP } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gmail via Google's MCP server. Workspace Developer Preview members only.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately not `GMAIL_SCOPES`. The server *advertises* five, including
|
|
9
|
+
* `mail.google.com` — read, send, and permanently delete. Requesting the full
|
|
10
|
+
* advertised set was tried, on the theory that the servers reject a subset, and
|
|
11
|
+
* it made no difference: with all five granted, `tools/list` succeeds and every
|
|
12
|
+
* `tools/call` still answers "The caller does not have permission". The gate is
|
|
13
|
+
* enrolment in Google's Workspace Developer Preview, not scope.
|
|
14
|
+
*
|
|
15
|
+
* That is the opposite of the REST provider's situation, where scope is exactly
|
|
16
|
+
* the gate — hence two lists. Widening this one buys nothing that has been
|
|
17
|
+
* demonstrated, so it stays at what Google documents. If calls still fail once
|
|
18
|
+
* preview access is granted, widening is the next thing to try — `connect` warns
|
|
19
|
+
* about the gap either way.
|
|
20
|
+
*/
|
|
21
|
+
const GMAIL_MCP_SCOPES = [
|
|
22
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
23
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const gmailMcp = defineProvider({
|
|
27
|
+
id: 'gmail_mcp',
|
|
28
|
+
name: 'Gmail (Google MCP)',
|
|
29
|
+
description:
|
|
30
|
+
'Read and compose mail via Google\'s official Gmail MCP server. Requires Workspace Developer Preview enrolment — use "gmail" otherwise.',
|
|
31
|
+
connector: { kind: 'mcp', endpoint: 'https://gmailmcp.googleapis.com/mcp/v1' },
|
|
32
|
+
auth: { kind: 'oauth', registration: 'manual', app: GOOGLE_APP, scopes: GMAIL_MCP_SCOPES },
|
|
33
|
+
identity: GMAIL_IDENTITY,
|
|
34
|
+
setup: googleSetup('Gmail', GMAIL_MCP_SCOPES, { preview: true }),
|
|
35
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Google's nine providers. Each folder holds all of its own vendor knowledge. */
|
|
2
|
+
export { calendar } from './calendar/index.ts';
|
|
3
|
+
export { contacts } from './contacts/index.ts';
|
|
4
|
+
export { docs } from './docs/index.ts';
|
|
5
|
+
export { drive, DRIVE_SCOPES } from './drive/index.ts';
|
|
6
|
+
export { driveMcp } from './drive-mcp/index.ts';
|
|
7
|
+
export { gmail, GMAIL_SCOPES } from './gmail/index.ts';
|
|
8
|
+
export { gmailMcp } from './gmail-mcp/index.ts';
|
|
9
|
+
export { sheets } from './sheets/index.ts';
|
|
10
|
+
export { tasks } from './tasks/index.ts';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { AuthBroker } from '#connectivity';
|
|
2
|
+
import { brokerOriginOverride } from '#connectivity/auth/index.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Google's OAuth endpoints, shared by every Google provider.
|
|
6
|
+
*
|
|
7
|
+
* A REST API announces nothing about where its authorization server lives, so
|
|
8
|
+
* an `http` connector has to be told. `access_type=offline` and
|
|
9
|
+
* `prompt=consent` are what make Google return a refresh token at all — without
|
|
10
|
+
* them the connection works for an hour and then dies.
|
|
11
|
+
*/
|
|
12
|
+
export const GOOGLE_APP = 'google';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The Google OAuth client Lanes operates, and where its secret stays.
|
|
16
|
+
*
|
|
17
|
+
* Its own Cloud project, separate from the one that signs people into Lanes:
|
|
18
|
+
* this client asks for Gmail and Drive and carries a verification review that
|
|
19
|
+
* can be rejected, and a rejection must not be able to take sign-in with it.
|
|
20
|
+
*
|
|
21
|
+
* The secret is not confidential in Google's sense — an installed app cannot
|
|
22
|
+
* hold one — and is held back anyway. Shipping it would hand anyone the shared
|
|
23
|
+
* quota and standing of a single client that every Lanes Link user depends on,
|
|
24
|
+
* and there is no way to withdraw one copy of a secret.
|
|
25
|
+
*
|
|
26
|
+
* What this buys the operator is the whole of `setup/google.md`: no project, no
|
|
27
|
+
* console, no scope list to transcribe, and no seven-day refresh-token expiry,
|
|
28
|
+
* because that expiry is a property of a project left in "Testing" and this one
|
|
29
|
+
* is not. What it costs is recorded in ADR-028 and in the guarantee table in
|
|
30
|
+
* `docs/detailed/security.md` — chiefly that the exchange stops being local.
|
|
31
|
+
*/
|
|
32
|
+
const BROKER_ORIGIN = 'https://api.lanes.sh';
|
|
33
|
+
const BROKER_PATH = '/v1/auth/link/google';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The same client, reached at a different origin.
|
|
37
|
+
*
|
|
38
|
+
* Two callers need it: the CLI performing the first exchange and the dispatcher
|
|
39
|
+
* refreshing while it serves. Both read `manifest.auth.broker.url`, so building
|
|
40
|
+
* it once here is what keeps them from disagreeing about where the broker is —
|
|
41
|
+
* and what makes `LANES_LINK_BROKER_ORIGIN` reach both at once. The path stays
|
|
42
|
+
* fixed; only the host in front of it moves. See `../../../connectivity/auth/
|
|
43
|
+
* oauth-authcode/broker.ts` for what an override is allowed to be.
|
|
44
|
+
*/
|
|
45
|
+
export function googleBroker(env?: Record<string, string | undefined>): AuthBroker {
|
|
46
|
+
return {
|
|
47
|
+
url: `${brokerOriginOverride(env) ?? BROKER_ORIGIN}${BROKER_PATH}`,
|
|
48
|
+
operator: 'Lanes',
|
|
49
|
+
docs_url: 'https://lanes.sh/link#google',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const GOOGLE_BROKER: AuthBroker = googleBroker();
|
|
54
|
+
|
|
55
|
+
export const GOOGLE_OAUTH = {
|
|
56
|
+
authorize_url: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
57
|
+
token_url: 'https://oauth2.googleapis.com/token',
|
|
58
|
+
//
|
|
59
|
+
// `select_account` matters as much as the rest: with it omitted Google reuses
|
|
60
|
+
// whichever account the browser is already signed into and never offers a
|
|
61
|
+
// choice, so connecting a second mailbox silently re-authorises the first.
|
|
62
|
+
authorize_params: { access_type: 'offline', prompt: 'select_account consent' },
|
|
63
|
+
// Every REST provider here spreads this block, so one line turns brokering on
|
|
64
|
+
// for all seven. `gmail_mcp` and `drive_mcp` write their auth longhand and do
|
|
65
|
+
// not spread it, which is what keeps them bring-your-own — the SDK owns their
|
|
66
|
+
// exchange and `defineProvider` refuses a broker on an mcp connector.
|
|
67
|
+
broker: GOOGLE_BROKER,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where a Google provider reads its account label from.
|
|
72
|
+
*
|
|
73
|
+
* Drive's endpoint, deliberately, for Sheets and Docs as well: neither has a
|
|
74
|
+
* "who am I", and `drive.readonly` is requested anyway, so labelling the
|
|
75
|
+
* connection costs no extra consent.
|
|
76
|
+
*/
|
|
77
|
+
export const DRIVE_IDENTITY = {
|
|
78
|
+
kind: 'http',
|
|
79
|
+
url: 'https://www.googleapis.com/drive/v3/about?fields=user',
|
|
80
|
+
field: 'user.emailAddress',
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
export const GMAIL_IDENTITY = {
|
|
84
|
+
kind: 'http',
|
|
85
|
+
url: 'https://gmail.googleapis.com/gmail/v1/users/me/profile',
|
|
86
|
+
field: 'emailAddress',
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Calendar answers this itself, rather than borrowing Drive's endpoint.
|
|
91
|
+
*
|
|
92
|
+
* The entry for someone's own calendar is keyed by their address, so its `id`
|
|
93
|
+
* *is* the label — and it reads under `calendar.readonly`, which this provider
|
|
94
|
+
* requests anyway. Following Sheets and Docs to `DRIVE_IDENTITY` would mean
|
|
95
|
+
* asking for `drive.readonly` purely to print a name.
|
|
96
|
+
*/
|
|
97
|
+
export const CALENDAR_IDENTITY = {
|
|
98
|
+
kind: 'http',
|
|
99
|
+
url: 'https://www.googleapis.com/calendar/v3/users/me/calendarList/primary',
|
|
100
|
+
field: 'id',
|
|
101
|
+
} as const;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Contacts asks People who the token belongs to — using the very operation that
|
|
105
|
+
* cannot be a *tool*.
|
|
106
|
+
*
|
|
107
|
+
* `people.people.get` is unreachable through the generated path, because
|
|
108
|
+
* `{resourceName}` has its slash percent-encoded and `people/me` stops being a
|
|
109
|
+
* path (see the note in `../specs/vendor.ts`). A literal URL has no placeholder
|
|
110
|
+
* to encode, so the same call works here. `pluck` steps through the first array
|
|
111
|
+
* element, which is what resolves `emailAddresses.value`.
|
|
112
|
+
*/
|
|
113
|
+
export const PEOPLE_IDENTITY = {
|
|
114
|
+
kind: 'http',
|
|
115
|
+
url: 'https://people.googleapis.com/v1/people/me?personFields=emailAddresses',
|
|
116
|
+
field: 'emailAddresses.value',
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
/** A vendored spec, addressed from this folder rather than by counting `..`. */
|
|
120
|
+
export function specPath(name: string): string {
|
|
121
|
+
return new URL(`../specs/${name}`, import.meta.url).pathname;
|
|
122
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ScopeMeaning } from '../../scopes.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the Google scopes we ship actually permit, in plain words.
|
|
5
|
+
*
|
|
6
|
+
* Vendor knowledge, so it lives with the vendor. It used to sit in the CLI,
|
|
7
|
+
* which is where it is *displayed* — but a table of Google scope semantics in
|
|
8
|
+
* `src/cli/` is exactly the leak this layout exists to close: `lanes link
|
|
9
|
+
* connect` should be able to explain any provider's grant without the CLI
|
|
10
|
+
* having learned that provider's vocabulary.
|
|
11
|
+
*
|
|
12
|
+
* Confined to display. Nothing routes on it, and a scope missing from this table
|
|
13
|
+
* is printed unannotated rather than mis-described — silence is the safe failure
|
|
14
|
+
* here.
|
|
15
|
+
*/
|
|
16
|
+
export const GOOGLE_SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
17
|
+
// Asked for only when authorising against the client Lanes operates, which
|
|
18
|
+
// needs to tell one caller's refresh from another's. Neither reaches any
|
|
19
|
+
// Google service, and describing them matters precisely because they are the
|
|
20
|
+
// two the operator did not ask for.
|
|
21
|
+
openid: { meaning: 'a signed statement of which Google account this is — no access to anything' },
|
|
22
|
+
email: {
|
|
23
|
+
meaning: 'your address, so the hosted client can tell whose connection this is. Not the mailbox',
|
|
24
|
+
},
|
|
25
|
+
'https://mail.google.com/': {
|
|
26
|
+
meaning: 'full mailbox — read, send, and permanently delete any message',
|
|
27
|
+
broad: true,
|
|
28
|
+
},
|
|
29
|
+
'https://www.googleapis.com/auth/gmail.modify': {
|
|
30
|
+
meaning: 'read, send, and modify any message; no permanent delete',
|
|
31
|
+
broad: true,
|
|
32
|
+
},
|
|
33
|
+
'https://www.googleapis.com/auth/gmail.readonly': { meaning: 'read mail and settings' },
|
|
34
|
+
'https://www.googleapis.com/auth/gmail.compose': { meaning: 'create and send drafts' },
|
|
35
|
+
'https://www.googleapis.com/auth/gmail.settings.basic': {
|
|
36
|
+
// Broad for what it *outlives*, not for how much it touches. A filter keeps
|
|
37
|
+
// acting on mail that has not arrived yet, so this is the only grant here
|
|
38
|
+
// that survives the session, the token, and the connection being disabled.
|
|
39
|
+
meaning: 'create and delete filters, and change your send-as identities — a filter is a standing rule that keeps acting after the session ends',
|
|
40
|
+
broad: true,
|
|
41
|
+
},
|
|
42
|
+
'https://www.googleapis.com/auth/gmail.metadata': {
|
|
43
|
+
meaning: 'headers and labels only, no message bodies',
|
|
44
|
+
},
|
|
45
|
+
'https://www.googleapis.com/auth/drive': {
|
|
46
|
+
meaning: 'every file in the account — read, write, and delete',
|
|
47
|
+
broad: true,
|
|
48
|
+
},
|
|
49
|
+
'https://www.googleapis.com/auth/drive.readonly': { meaning: 'read every file in the account' },
|
|
50
|
+
'https://www.googleapis.com/auth/drive.file': {
|
|
51
|
+
meaning: 'only files this app creates or you pick',
|
|
52
|
+
},
|
|
53
|
+
// Broad, and narrower than `auth/drive` — one file type rather than the whole
|
|
54
|
+
// account. Still every document of that type, including ones this app has
|
|
55
|
+
// never seen, which is the line `drive.file` does not cross.
|
|
56
|
+
'https://www.googleapis.com/auth/spreadsheets': {
|
|
57
|
+
meaning: 'every spreadsheet in the account — read, edit, create, and delete',
|
|
58
|
+
broad: true,
|
|
59
|
+
},
|
|
60
|
+
'https://www.googleapis.com/auth/documents': {
|
|
61
|
+
meaning: 'every document in the account — read, edit, create, and delete',
|
|
62
|
+
broad: true,
|
|
63
|
+
},
|
|
64
|
+
// Calendar's `auth/drive`: it does not merely edit what is on a calendar, it
|
|
65
|
+
// creates and deletes calendars and changes who they are shared with. Listed
|
|
66
|
+
// so the table can say what is refused, never requested.
|
|
67
|
+
'https://www.googleapis.com/auth/calendar': {
|
|
68
|
+
meaning: 'every calendar you can access — read, edit, share, and delete',
|
|
69
|
+
broad: true,
|
|
70
|
+
},
|
|
71
|
+
'https://www.googleapis.com/auth/calendar.readonly': {
|
|
72
|
+
meaning: 'read every calendar you can access',
|
|
73
|
+
},
|
|
74
|
+
// Broad on the same reading as `spreadsheets` — every event on every
|
|
75
|
+
// calendar, including ones this app has never seen. Narrower than
|
|
76
|
+
// `auth/calendar`, which also reaches the calendars themselves.
|
|
77
|
+
'https://www.googleapis.com/auth/calendar.events': {
|
|
78
|
+
meaning: 'every event on every calendar — read, create, change, and delete',
|
|
79
|
+
broad: true,
|
|
80
|
+
},
|
|
81
|
+
// Broad because Google publishes nothing narrower that can write. `tasks` and
|
|
82
|
+
// `tasks.readonly` are the entire vocabulary, so adding one task means
|
|
83
|
+
// holding write and delete over every list in the account.
|
|
84
|
+
'https://www.googleapis.com/auth/tasks': {
|
|
85
|
+
meaning: 'every task list — read, create, edit, organise, and delete',
|
|
86
|
+
broad: true,
|
|
87
|
+
},
|
|
88
|
+
'https://www.googleapis.com/auth/tasks.readonly': { meaning: 'read your task lists and tasks' },
|
|
89
|
+
// Refused, and listed for the same reason as `auth/calendar`: a contact card
|
|
90
|
+
// deleted through the API is gone.
|
|
91
|
+
'https://www.googleapis.com/auth/contacts': {
|
|
92
|
+
meaning: 'read, edit, and permanently delete your contacts',
|
|
93
|
+
broad: true,
|
|
94
|
+
},
|
|
95
|
+
'https://www.googleapis.com/auth/contacts.readonly': { meaning: 'read your saved contacts' },
|
|
96
|
+
'https://www.googleapis.com/auth/contacts.other.readonly': {
|
|
97
|
+
meaning: 'read the addresses Gmail saved automatically in "Other contacts"',
|
|
98
|
+
},
|
|
99
|
+
};
|