@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,45 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { DRIVE_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Docs, on the same reasoning as `sheets`.
|
|
7
|
+
*
|
|
8
|
+
* One difference worth knowing: Docs has no `values`-style shortcut. Every edit,
|
|
9
|
+
* from inserting a word to restyling a heading, is a `batchUpdate` request, so
|
|
10
|
+
* that single operation is the entire write surface rather than the advanced
|
|
11
|
+
* half of it.
|
|
12
|
+
*/
|
|
13
|
+
const DOCS_SCOPES = [
|
|
14
|
+
'https://www.googleapis.com/auth/drive.readonly',
|
|
15
|
+
'https://www.googleapis.com/auth/drive.file',
|
|
16
|
+
'https://www.googleapis.com/auth/documents',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const docs = defineProvider({
|
|
20
|
+
id: 'docs',
|
|
21
|
+
name: 'Google Docs',
|
|
22
|
+
description:
|
|
23
|
+
'Read a document\'s structure and edit its content — insert, replace, and format text — via the Docs REST API.',
|
|
24
|
+
connector: {
|
|
25
|
+
kind: 'http',
|
|
26
|
+
base_url: 'https://docs.googleapis.com',
|
|
27
|
+
openapi: specPath('docs.v1.json'),
|
|
28
|
+
},
|
|
29
|
+
auth: {
|
|
30
|
+
kind: 'oauth',
|
|
31
|
+
registration: 'manual',
|
|
32
|
+
app: GOOGLE_APP,
|
|
33
|
+
scopes: DOCS_SCOPES,
|
|
34
|
+
...GOOGLE_OAUTH,
|
|
35
|
+
},
|
|
36
|
+
identity: DRIVE_IDENTITY,
|
|
37
|
+
setup: googleSetup('Docs', DOCS_SCOPES, {
|
|
38
|
+
apis: ['docs.googleapis.com', 'drive.googleapis.com'],
|
|
39
|
+
}),
|
|
40
|
+
// `requests` carries the text being written, so only the document id is kept.
|
|
41
|
+
redact: {
|
|
42
|
+
'documents.get': ['documentId', 'suggestionsViewMode'],
|
|
43
|
+
'documents.batchUpdate': ['documentId'],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three jobs `files.update` does that its name does not suggest.
|
|
3
|
+
*
|
|
4
|
+
* Drive spells "move", "rename", "trash", and "star" as one PATCH, and the
|
|
5
|
+
* generated description is the vendor's field list. So an agent asked to move
|
|
6
|
+
* or delete a file reads a tool list containing `files_update` and concludes
|
|
7
|
+
* neither is possible — which is exactly what happened.
|
|
8
|
+
*/
|
|
9
|
+
export const DRIVE_HINTS: Record<string, string> = {
|
|
10
|
+
'files.update': [
|
|
11
|
+
'This is also how a file is moved, renamed, and deleted.',
|
|
12
|
+
'Move it by passing `addParents` with the destination folder id and `removeParents`',
|
|
13
|
+
'with the current one — a Drive file has no path, only parents.',
|
|
14
|
+
'Rename it with `name`. Delete it with `trashed: true`, which is recoverable;',
|
|
15
|
+
'there is deliberately no permanent-delete tool here, because Drive has a trash',
|
|
16
|
+
'and an agent should not be able to bypass it. Restore with `trashed: false`.',
|
|
17
|
+
].join(' '),
|
|
18
|
+
|
|
19
|
+
'files.create': [
|
|
20
|
+
'This also creates the Google-native file types, which have no create tool of their own:',
|
|
21
|
+
'pass `mimeType: "application/vnd.google-apps.spreadsheet"` for a spreadsheet,',
|
|
22
|
+
'`.document` for a Doc, `.presentation` for Slides, or `.folder` for a folder,',
|
|
23
|
+
'and leave the body empty. The file comes back with an id the Sheets and Docs tools',
|
|
24
|
+
'then work on directly — creating a spreadsheet and filling it is this call followed by',
|
|
25
|
+
'`sheets_spreadsheets_values_update`.',
|
|
26
|
+
'Place it by passing `parents` with a folder id.',
|
|
27
|
+
].join(' '),
|
|
28
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { DRIVE_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
import { DRIVE_HINTS } from './hints.ts';
|
|
5
|
+
import { DRIVE_REDACT } from './redact.ts';
|
|
6
|
+
|
|
7
|
+
/** Drive over the REST API. Same reasoning as `gmail` — no preview gate. */
|
|
8
|
+
export const DRIVE_SCOPES = [
|
|
9
|
+
'https://www.googleapis.com/auth/drive.readonly',
|
|
10
|
+
'https://www.googleapis.com/auth/drive.file',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const drive = defineProvider({
|
|
14
|
+
id: 'drive',
|
|
15
|
+
name: 'Google Drive',
|
|
16
|
+
description:
|
|
17
|
+
'Search, read, and export files, and organise the ones this app created — rename, move, trash, copy, and share — via the Drive REST API.',
|
|
18
|
+
connector: {
|
|
19
|
+
kind: 'http',
|
|
20
|
+
base_url: 'https://www.googleapis.com/drive/v3',
|
|
21
|
+
openapi: specPath('drive.v3.json'),
|
|
22
|
+
},
|
|
23
|
+
auth: {
|
|
24
|
+
kind: 'oauth',
|
|
25
|
+
registration: 'manual',
|
|
26
|
+
app: GOOGLE_APP,
|
|
27
|
+
scopes: DRIVE_SCOPES,
|
|
28
|
+
...GOOGLE_OAUTH,
|
|
29
|
+
},
|
|
30
|
+
identity: DRIVE_IDENTITY,
|
|
31
|
+
setup: googleSetup('Drive', DRIVE_SCOPES),
|
|
32
|
+
redact: DRIVE_REDACT,
|
|
33
|
+
hints: DRIVE_HINTS,
|
|
34
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives into the audit log when a file is read or changed.
|
|
3
|
+
*
|
|
4
|
+
* Drive shipped without a redaction block, which meant `files.create` recorded
|
|
5
|
+
* that a file had been made and nothing about which one. Three write operations
|
|
6
|
+
* beside it make the gap worth closing rather than noting.
|
|
7
|
+
*
|
|
8
|
+
* `name` is withheld on `files.create` and `files.update` — the same call as
|
|
9
|
+
* Gmail's `labels.create`, where the name is the user's own words. `q` is
|
|
10
|
+
* withheld on `files.list` on the same ground Gmail withholds a search: the
|
|
11
|
+
* query is a question someone asked, not a record of what happened. Everything
|
|
12
|
+
* kept is an identifier or a flag: which file, which parents it moved between,
|
|
13
|
+
* whether it went to the trash.
|
|
14
|
+
*
|
|
15
|
+
* `permissions.create` keeps `emailAddress` and `domain`, and that departs from
|
|
16
|
+
* the withhold-addresses rule on purpose. Gmail withholds recipients because
|
|
17
|
+
* they are part of a message the log already refuses to record. Here the
|
|
18
|
+
* grantee *is* the change: an access log that can say a file was shared with
|
|
19
|
+
* somebody as a writer, but not with whom, has failed at the single question it
|
|
20
|
+
* exists to answer.
|
|
21
|
+
*/
|
|
22
|
+
export const DRIVE_REDACT: Record<string, string[]> = {
|
|
23
|
+
'files.list': ['pageSize', 'orderBy', 'spaces', 'includeItemsFromAllDrives'],
|
|
24
|
+
'files.get': ['fileId', 'acknowledgeAbuse'],
|
|
25
|
+
'files.export': ['fileId', 'mimeType'],
|
|
26
|
+
'permissions.list': ['fileId'],
|
|
27
|
+
'files.create': ['mimeType', 'parents'],
|
|
28
|
+
'files.update': ['fileId', 'addParents', 'removeParents', 'trashed', 'starred', 'mimeType'],
|
|
29
|
+
'files.copy': ['fileId', 'parents'],
|
|
30
|
+
'permissions.create': [
|
|
31
|
+
'fileId',
|
|
32
|
+
'role',
|
|
33
|
+
'type',
|
|
34
|
+
'domain',
|
|
35
|
+
'emailAddress',
|
|
36
|
+
'sendNotificationEmail',
|
|
37
|
+
'transferOwnership',
|
|
38
|
+
],
|
|
39
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { DRIVE_IDENTITY, GOOGLE_APP } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
import { DRIVE_SCOPES } from '../drive/index.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Drive via Google's MCP server. Workspace Developer Preview members only.
|
|
8
|
+
*
|
|
9
|
+
* Same story as Gmail. `drivemcp` advertises `auth/drive` too — full read-write
|
|
10
|
+
* over every file in the account — and granting it did not help either.
|
|
11
|
+
*/
|
|
12
|
+
export const driveMcp = defineProvider({
|
|
13
|
+
id: 'drive_mcp',
|
|
14
|
+
name: 'Google Drive (Google MCP)',
|
|
15
|
+
description:
|
|
16
|
+
'Search, read, and create files via Google\'s official Drive MCP server. Requires Workspace Developer Preview enrolment — use "drive" otherwise.',
|
|
17
|
+
connector: { kind: 'mcp', endpoint: 'https://drivemcp.googleapis.com/mcp/v1' },
|
|
18
|
+
auth: { kind: 'oauth', registration: 'manual', app: GOOGLE_APP, scopes: DRIVE_SCOPES },
|
|
19
|
+
identity: DRIVE_IDENTITY,
|
|
20
|
+
setup: googleSetup('Drive', DRIVE_SCOPES, { preview: true }),
|
|
21
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Gmail is, in the two shapes it comes in.
|
|
3
|
+
*
|
|
4
|
+
* One file so the manifest's `base_url` and the authored send capability cannot
|
|
5
|
+
* drift apart — `tools.test.ts` asserts the manifest matches the vendored spec's
|
|
6
|
+
* server, and a second copy of the host is how a change satisfies that test while
|
|
7
|
+
* breaking the hand-written call beside it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const GMAIL_HOST = 'https://gmail.googleapis.com';
|
|
11
|
+
|
|
12
|
+
/** What the generated tools use, and what an ordinary JSON call goes to. */
|
|
13
|
+
export const GMAIL_API = `${GMAIL_HOST}/gmail/v1/users/me`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The media-upload host, for a message too large to carry as a JSON string.
|
|
17
|
+
*
|
|
18
|
+
* A separate path rather than a query parameter: Google exposes uploads under
|
|
19
|
+
* `/upload/...`, and posting a large body to the ordinary endpoint does not fail
|
|
20
|
+
* loudly so much as get slower and then stop working.
|
|
21
|
+
*/
|
|
22
|
+
export const GMAIL_UPLOAD = `${GMAIL_HOST}/upload/gmail/v1/users/me`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The largest message Gmail will send, from its discovery document
|
|
26
|
+
* (`users.messages.send`, `maxSize: 36700160`).
|
|
27
|
+
*
|
|
28
|
+
* Note this is the *send* ceiling; `messages.import` allows 150 MiB. So it is a
|
|
29
|
+
* limit on putting mail into the world, not on storing it, which is why it is
|
|
30
|
+
* named for sending.
|
|
31
|
+
*/
|
|
32
|
+
export const GMAIL_MAX_MESSAGE_BYTES = 36_700_160;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Above this, submit through the upload host instead of as a JSON field.
|
|
36
|
+
*
|
|
37
|
+
* Google's guidance calls `uploadType=media` the route for files over roughly
|
|
38
|
+
* this size, and the arithmetic agrees: `raw` is base64url inside a JSON string,
|
|
39
|
+
* so a message near the 35 MiB ceiling would be ~47 MiB of text in a request body
|
|
40
|
+
* and would meet a generic limit long before Gmail's own.
|
|
41
|
+
*/
|
|
42
|
+
export const GMAIL_JSON_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { MailboxAttachmentSource } from '#connectivity/mail';
|
|
2
|
+
import { GMAIL_API } from './api.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Re-attaching an attachment that is already in the mailbox.
|
|
6
|
+
*
|
|
7
|
+
* Two round trips rather than one, deliberately. `attachments.get` returns
|
|
8
|
+
* `{attachmentId, size, data}` and nothing else — no filename, no MIME type — so
|
|
9
|
+
* fetching only the bytes would produce an attachment called "attachment" of no
|
|
10
|
+
* particular type. The message metadata is where the part headers live, so it is
|
|
11
|
+
* read first, and it also lets a caller name the attachment by filename or omit
|
|
12
|
+
* the id when there is only one.
|
|
13
|
+
*
|
|
14
|
+
* The sharp edge here is the encoding. Gmail returns `data` as base64**url** (RFC
|
|
15
|
+
* 4648 §5), not standard base64. Handing that to a MIME composer that expects the
|
|
16
|
+
* standard alphabet corrupts any file containing a byte that encodes to `-` or
|
|
17
|
+
* `_` — which is most of them — and the result still arrives, still has the right
|
|
18
|
+
* length, and still opens as a file. It is a silent, plausible failure, and it is
|
|
19
|
+
* a known trap in other implementations of this same feature.
|
|
20
|
+
*/
|
|
21
|
+
export function gmailAttachments(input: {
|
|
22
|
+
readonly authorize: (request: Request) => Promise<Request>;
|
|
23
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
24
|
+
readonly signal?: AbortSignal | undefined;
|
|
25
|
+
}): MailboxAttachmentSource {
|
|
26
|
+
const doFetch = input.fetch ?? globalThis.fetch;
|
|
27
|
+
|
|
28
|
+
const get = async (path: string): Promise<Record<string, unknown>> => {
|
|
29
|
+
const request = await input.authorize(
|
|
30
|
+
new Request(`${GMAIL_API}${path}`, {
|
|
31
|
+
headers: { accept: 'application/json' },
|
|
32
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const response = await doFetch(request);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Gmail answered ${response.status} for ${path}: ${await response.text()}`);
|
|
39
|
+
}
|
|
40
|
+
return (await response.json()) as Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return async ({ messageId, attachmentId }) => {
|
|
44
|
+
const message = await get(`/messages/${encodeURIComponent(messageId)}?format=full`);
|
|
45
|
+
const parts = attachmentParts(message['payload']);
|
|
46
|
+
|
|
47
|
+
if (parts.length === 0) throw new Error(`Message ${messageId} has no attachments.`);
|
|
48
|
+
|
|
49
|
+
const chosen = pick(parts, attachmentId, messageId);
|
|
50
|
+
const body = await get(
|
|
51
|
+
`/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(chosen.attachmentId)}`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const data = body['data'];
|
|
55
|
+
if (typeof data !== 'string') {
|
|
56
|
+
throw new Error(`Gmail returned no content for attachment ${chosen.attachmentId}.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
// base64url in, bytes out. See the note above.
|
|
61
|
+
bytes: new Uint8Array(Buffer.from(data, 'base64url')),
|
|
62
|
+
filename: chosen.filename,
|
|
63
|
+
contentType: chosen.mimeType,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AttachmentPart {
|
|
69
|
+
readonly attachmentId: string;
|
|
70
|
+
readonly filename: string | null;
|
|
71
|
+
readonly mimeType: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Every part that is a file, from anywhere in the tree.
|
|
76
|
+
*
|
|
77
|
+
* Recursive because a message with both a body and attachments nests them —
|
|
78
|
+
* `multipart/mixed` holding a `multipart/alternative` holding the text — so the
|
|
79
|
+
* files are rarely at the top level.
|
|
80
|
+
*/
|
|
81
|
+
function attachmentParts(payload: unknown): AttachmentPart[] {
|
|
82
|
+
if (payload === null || typeof payload !== 'object') return [];
|
|
83
|
+
|
|
84
|
+
const part = payload as {
|
|
85
|
+
filename?: unknown;
|
|
86
|
+
mimeType?: unknown;
|
|
87
|
+
body?: { attachmentId?: unknown };
|
|
88
|
+
parts?: unknown;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const found: AttachmentPart[] = [];
|
|
92
|
+
|
|
93
|
+
const attachmentId = part.body?.attachmentId;
|
|
94
|
+
if (typeof attachmentId === 'string' && attachmentId !== '') {
|
|
95
|
+
found.push({
|
|
96
|
+
attachmentId,
|
|
97
|
+
filename: typeof part.filename === 'string' && part.filename !== '' ? part.filename : null,
|
|
98
|
+
mimeType: typeof part.mimeType === 'string' ? part.mimeType : null,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (Array.isArray(part.parts)) {
|
|
103
|
+
for (const child of part.parts) found.push(...attachmentParts(child));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** By filename, by position, or by there being only one. Mirrors the IMAP side. */
|
|
110
|
+
function pick(
|
|
111
|
+
parts: readonly AttachmentPart[],
|
|
112
|
+
attachmentId: string | undefined,
|
|
113
|
+
messageId: string,
|
|
114
|
+
): AttachmentPart {
|
|
115
|
+
const names = parts.map((part, index) => part.filename ?? `#${index + 1}`);
|
|
116
|
+
|
|
117
|
+
if (attachmentId === undefined) {
|
|
118
|
+
if (parts.length === 1) return parts[0]!;
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Message ${messageId} has ${parts.length} attachments, so attachment_id is needed. They are: ${names.join(', ')}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Gmail's own opaque id, which is what `get_message` reports, so it is the
|
|
125
|
+
// most likely thing a caller passes.
|
|
126
|
+
const byId = parts.find((part) => part.attachmentId === attachmentId);
|
|
127
|
+
if (byId) return byId;
|
|
128
|
+
|
|
129
|
+
const byName = parts.find(
|
|
130
|
+
(part) => part.filename?.toLowerCase() === attachmentId.toLowerCase(),
|
|
131
|
+
);
|
|
132
|
+
if (byName) return byName;
|
|
133
|
+
|
|
134
|
+
const position = Number(attachmentId);
|
|
135
|
+
if (Number.isInteger(position) && position >= 1 && position <= parts.length) {
|
|
136
|
+
return parts[position - 1]!;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Message ${messageId} has no attachment "${attachmentId}". They are: ${names.join(', ')}.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Gmail's own document does not say about Gmail.
|
|
3
|
+
*
|
|
4
|
+
* Every mail-organising verb an agent is asked for — mark unread, archive, move
|
|
5
|
+
* to a folder, report spam — is the same operation with a different label id,
|
|
6
|
+
* and the generated description for that operation says only that
|
|
7
|
+
* `addLabelIds` is "a list of IDs of labels to add". Nothing names the ids, and
|
|
8
|
+
* nothing says that the ids *are* the feature.
|
|
9
|
+
*
|
|
10
|
+
* The cost of that omission is not a worse tool, it is a tool nobody finds. An
|
|
11
|
+
* operator asked for these four capabilities to be built; all four already
|
|
12
|
+
* existed and had for months. This file is the fix for that, and it is why the
|
|
13
|
+
* text below spells out the label ids rather than pointing at Google's docs:
|
|
14
|
+
* the reader is a model choosing a tool, and a pointer is a page it will not
|
|
15
|
+
* open.
|
|
16
|
+
*/
|
|
17
|
+
const LABEL_VERBS = [
|
|
18
|
+
'Gmail has no separate verb for read-state, archiving, or spam — each one is a label edit,',
|
|
19
|
+
'and this is the operation that makes it:',
|
|
20
|
+
'`addLabelIds: ["UNREAD"]` marks unread and `removeLabelIds: ["UNREAD"]` marks read;',
|
|
21
|
+
'`removeLabelIds: ["INBOX"]` archives;',
|
|
22
|
+
'`addLabelIds: ["SPAM"], removeLabelIds: ["INBOX"]` reports spam;',
|
|
23
|
+
'`addLabelIds: ["STARRED"]` stars.',
|
|
24
|
+
'Moving a message into a folder is the same shape — add that folder\'s label id and remove',
|
|
25
|
+
'"INBOX" — because a Gmail folder *is* a label. Call `labels_list` for the ids of the',
|
|
26
|
+
'ones the user made; the capitalised ones above are system labels and always exist.',
|
|
27
|
+
'To delete mail, use the `trash` operation rather than a label: it is recoverable and',
|
|
28
|
+
'there is no permanent-delete tool here by design.',
|
|
29
|
+
].join(' ');
|
|
30
|
+
|
|
31
|
+
export const GMAIL_HINTS: Record<string, string> = {
|
|
32
|
+
'users.messages.modify': LABEL_VERBS,
|
|
33
|
+
// The same text, deliberately. A model that finds one of these and not the
|
|
34
|
+
// others should not have to infer that the batch form works identically —
|
|
35
|
+
// and the batch form is the one worth reaching for on a mailbox sweep.
|
|
36
|
+
'users.messages.batchModify': `${LABEL_VERBS} This is the batch form: pass up to 1000 message ids in \`ids\`.`,
|
|
37
|
+
'users.threads.modify': `${LABEL_VERBS} This applies to every message in the thread at once.`,
|
|
38
|
+
|
|
39
|
+
// The distinction Gmail's UI draws with two buttons and its API draws not at
|
|
40
|
+
// all. Getting it wrong is silent in both directions: reporting spam when
|
|
41
|
+
// asked to block leaves the sender arriving tomorrow, and installing a filter
|
|
42
|
+
// when asked to report spam leaves a rule nobody remembers creating.
|
|
43
|
+
'users.settings.filters.create': [
|
|
44
|
+
'This is how a sender is blocked, as opposed to reported.',
|
|
45
|
+
'Reporting spam is a one-off label edit on messages that already exist —',
|
|
46
|
+
'use `messages_modify` with `addLabelIds: ["SPAM"]` for that.',
|
|
47
|
+
'A filter is a standing rule applied to mail that has not arrived yet:',
|
|
48
|
+
'`criteria: {from: "someone@example.com"}` with',
|
|
49
|
+
'`action: {addLabelIds: ["TRASH"]}` to bin it on arrival, or',
|
|
50
|
+
'`action: {addLabelIds: ["SPAM"]}` to route it to spam.',
|
|
51
|
+
'`criteria` also matches on `to`, `subject`, `query`, and `hasAttachment`.',
|
|
52
|
+
'The rule keeps running until it is deleted — it outlives this session,',
|
|
53
|
+
'so prefer the one-off label edit unless a standing rule is what was asked for.',
|
|
54
|
+
].join(' '),
|
|
55
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineProvider, defineProviderWithCapabilities } from '#connectivity';
|
|
2
|
+
import { GMAIL_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
|
|
3
|
+
import { googleSetup } from '../shared/setup.ts';
|
|
4
|
+
import { GMAIL_HOST } from './api.ts';
|
|
5
|
+
import { GMAIL_HINTS } from './hints.ts';
|
|
6
|
+
import { GMAIL_REDACT } from './redact.ts';
|
|
7
|
+
import { gmailSendMessage } from './send.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gmail over the REST API — the one that works for everybody.
|
|
11
|
+
*
|
|
12
|
+
* `gmail-mcp` proxies Google's own MCP server, which is the nicer thing on
|
|
13
|
+
* paper: their tools, their maintenance. It is also gated behind a Workspace
|
|
14
|
+
* Developer Preview that a personal @gmail.com account cannot enrol in, and the
|
|
15
|
+
* failure is silent — consent succeeds, tools/list succeeds, every call returns
|
|
16
|
+
* "The caller does not have permission".
|
|
17
|
+
*
|
|
18
|
+
* So the plain name is the working one. The REST API has no preview gate, no
|
|
19
|
+
* enrolment, and the same scopes; what it costs is that the tool list is
|
|
20
|
+
* generated from an OpenAPI document rather than curated by Google.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the REST provider needs to read, draft, and organise mail.
|
|
25
|
+
*
|
|
26
|
+
* `gmail.modify` is here for organising, and it is not a preference: Gmail has
|
|
27
|
+
* no verb for read-state, spam, or archive. Each one is a label edit on a
|
|
28
|
+
* message — remove `UNREAD`, add `SPAM`, remove `INBOX` — and `modify` is the
|
|
29
|
+
* only scope that permits editing a message's labels. `gmail.labels` sounds
|
|
30
|
+
* narrower and is not a substitute: it governs the label vocabulary, not its
|
|
31
|
+
* application. So the choice is `modify` or no organising at all.
|
|
32
|
+
*
|
|
33
|
+
* It is marked broad in `../shared/scopes.ts` and should stay marked, because it
|
|
34
|
+
* also grants send and trash. What it does not grant is permanent delete — that
|
|
35
|
+
* is `mail.google.com`, which nothing here asks for. Policy is what keeps the
|
|
36
|
+
* granted-but-unwanted verbs unreachable: the token can send, the tool surface
|
|
37
|
+
* cannot, and `lanes link policy deny` narrows it further.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* `gmail.settings.basic` is here for blocking a sender, and it is the one scope
|
|
41
|
+
* in this list that buys a *kind* of reach the others do not.
|
|
42
|
+
*
|
|
43
|
+
* Adding `SPAM` to a message trains Gmail against that sender and is what the
|
|
44
|
+
* Report-spam button does; it is already reachable under `modify`. Blocking is
|
|
45
|
+
* the other button, and it is a filter — a standing rule, created once, that
|
|
46
|
+
* keeps acting on mail that does not exist yet. `filters.create` and
|
|
47
|
+
* `filters.delete` accept no other scope. (`filters.list` accepts `readonly`,
|
|
48
|
+
* so seeing what exists costs nothing extra; only changing it costs this.)
|
|
49
|
+
*
|
|
50
|
+
* That standing quality is why it is marked broad in `../shared/scopes.ts`
|
|
51
|
+
* rather than waved through as narrower than `modify`. Every other write here
|
|
52
|
+
* acts on a message that already exists, and stops when the session does. A
|
|
53
|
+
* filter with `addLabelIds: ['TRASH']` keeps trashing mail after the token
|
|
54
|
+
* expires and after the connection is disabled — `lanes link policy deny`
|
|
55
|
+
* removes the tool, and cannot uninstall the rule. It is the argument that
|
|
56
|
+
* refuses `messages.delete`, one step removed.
|
|
57
|
+
*
|
|
58
|
+
* What it deliberately does not reach: `delegates.create` and
|
|
59
|
+
* `forwardingAddresses.create` are `gmail.settings.sharing`, so the two
|
|
60
|
+
* settings that silently exfiltrate a mailbox stay out, and a filter's
|
|
61
|
+
* `action.forward` only accepts an address already verified by hand.
|
|
62
|
+
*/
|
|
63
|
+
export const GMAIL_SCOPES = [
|
|
64
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
65
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
66
|
+
'https://www.googleapis.com/auth/gmail.modify',
|
|
67
|
+
'https://www.googleapis.com/auth/gmail.settings.basic',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const manifest = defineProvider({
|
|
71
|
+
id: 'gmail',
|
|
72
|
+
name: 'Gmail',
|
|
73
|
+
description:
|
|
74
|
+
'Read, search, send, draft, and organise mail — labels, read-state, spam, and trash — via the Gmail REST API.',
|
|
75
|
+
connector: {
|
|
76
|
+
kind: 'http',
|
|
77
|
+
base_url: GMAIL_HOST,
|
|
78
|
+
// Vendored, not fetched: a spec decides which paths are called with a real
|
|
79
|
+
// mailbox token, and `connect` grants everything a provider discovers.
|
|
80
|
+
openapi: specPath('gmail.v1.json'),
|
|
81
|
+
},
|
|
82
|
+
auth: {
|
|
83
|
+
kind: 'oauth',
|
|
84
|
+
registration: 'manual',
|
|
85
|
+
app: GOOGLE_APP,
|
|
86
|
+
scopes: GMAIL_SCOPES,
|
|
87
|
+
...GOOGLE_OAUTH,
|
|
88
|
+
},
|
|
89
|
+
// The REST API rather than the MCP server: it answers with the address under
|
|
90
|
+
// scopes we already hold, so labelling a connection costs no extra consent.
|
|
91
|
+
identity: GMAIL_IDENTITY,
|
|
92
|
+
setup: googleSetup('Gmail', GMAIL_SCOPES),
|
|
93
|
+
redact: GMAIL_REDACT,
|
|
94
|
+
hints: GMAIL_HINTS,
|
|
95
|
+
// Named here because an `http` connector otherwise assigns bundles by HTTP
|
|
96
|
+
// method during discovery, and an authored capability is never discovered.
|
|
97
|
+
bundles: [{ name: 'write', capabilities: ['send_message'] }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Almost all data, and one capability that has to be code.
|
|
102
|
+
*
|
|
103
|
+
* The exception is sending. Gmail's API takes a whole assembled RFC 2822 message
|
|
104
|
+
* as one base64url field, and no OpenAPI document describes composing one — so the
|
|
105
|
+
* generated tools leave that to the caller, which makes attaching a file
|
|
106
|
+
* impossible in practice rather than merely awkward. `send.ts` says the rest.
|
|
107
|
+
* Everything else about this provider is still the manifest above.
|
|
108
|
+
*/
|
|
109
|
+
export const gmail = defineProviderWithCapabilities({
|
|
110
|
+
manifest,
|
|
111
|
+
capabilities: [gmailSendMessage],
|
|
112
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only the organising capabilities, and only their identifiers.
|
|
3
|
+
*
|
|
4
|
+
* The default withholds every value, which is right for the reads — `q` is a
|
|
5
|
+
* search query, and "who did I email about the diagnosis" is the whole message.
|
|
6
|
+
* But it makes a write log useless: it records that a message was modified
|
|
7
|
+
* without recording which one, or into what. These keys are the difference
|
|
8
|
+
* between "something was marked spam" and "this was".
|
|
9
|
+
*
|
|
10
|
+
* Label *ids* are safe to keep and are the point of the entry — `UNREAD`,
|
|
11
|
+
* `SPAM`, `INBOX`, or an opaque `Label_12`. Label *names* are the user's own
|
|
12
|
+
* words, so `labels.create` logs that a label was made and not what it says.
|
|
13
|
+
*/
|
|
14
|
+
export const GMAIL_REDACT: Record<string, string[]> = {
|
|
15
|
+
'users.messages.modify': ['userId', 'id', 'addLabelIds', 'removeLabelIds'],
|
|
16
|
+
'users.messages.batchModify': ['userId', 'ids', 'addLabelIds', 'removeLabelIds'],
|
|
17
|
+
'users.messages.trash': ['userId', 'id'],
|
|
18
|
+
'users.messages.untrash': ['userId', 'id'],
|
|
19
|
+
'users.threads.modify': ['userId', 'id', 'addLabelIds', 'removeLabelIds'],
|
|
20
|
+
'users.threads.trash': ['userId', 'id'],
|
|
21
|
+
'users.threads.untrash': ['userId', 'id'],
|
|
22
|
+
'users.labels.create': ['userId'],
|
|
23
|
+
// `pathId` for the same reason as `drafts.update` below — the `Label` body
|
|
24
|
+
// carries an `id` too. This said `id` until the argument-name check in
|
|
25
|
+
// `cli/tools.test.ts` was written, which meant every label edit was logged
|
|
26
|
+
// without saying which label.
|
|
27
|
+
'users.labels.update': ['userId', 'pathId'],
|
|
28
|
+
'users.labels.delete': ['userId', 'id'],
|
|
29
|
+
'users.drafts.delete': ['userId', 'id'],
|
|
30
|
+
'users.settings.filters.list': ['userId'],
|
|
31
|
+
'users.settings.filters.delete': ['userId', 'id'],
|
|
32
|
+
// `criteria` kept, which is the same call as `drive.permissions.create`
|
|
33
|
+
// keeping `emailAddress`: a log saying a filter was installed but not against
|
|
34
|
+
// whom has failed at the only question it exists to answer. `action` is the
|
|
35
|
+
// effect — the label ids, and `forward`, which is an address worth naming on
|
|
36
|
+
// the one occasion it is set.
|
|
37
|
+
//
|
|
38
|
+
// The cost, stated rather than hidden: `criteria` also carries `query`,
|
|
39
|
+
// `subject`, and `negatedQuery`, so a filter built on words instead of a
|
|
40
|
+
// sender logs those words — the class Gmail's reads withhold on the "who did
|
|
41
|
+
// I email about the diagnosis" ground. The tiebreak is durability. A search
|
|
42
|
+
// is a question asked once; a filter is a rule that keeps running after the
|
|
43
|
+
// credential stops working, and this log is then the only record of who
|
|
44
|
+
// installed it. `annotate` would express the split properly, but it needs an
|
|
45
|
+
// authored capability and these are generated.
|
|
46
|
+
'users.settings.filters.create': ['userId', 'id', 'criteria', 'action'],
|
|
47
|
+
// Nothing. The recipients and the body *are* the message, and `attachments` is
|
|
48
|
+
// the one argument that may literally contain a file — keeping it verbatim
|
|
49
|
+
// would put base64 in the audit log. What was attached is recorded by the send
|
|
50
|
+
// path itself through `audit.annotate`: filename, size, type, SHA-256, and
|
|
51
|
+
// where the bytes came from. Identifiers, not content.
|
|
52
|
+
//
|
|
53
|
+
// Unprefixed because this capability is authored rather than generated, so its
|
|
54
|
+
// name has no `gmail.` to strip.
|
|
55
|
+
send_message: [],
|
|
56
|
+
};
|