@lanes-sh/link 0.6.10 → 0.7.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/.gcloudignore +25 -15
- package/README.md +62 -45
- package/instructions/skills/lanes-link/SKILL.md +25 -0
- package/package.json +3 -2
- package/src/audit/conformance.ts +1 -1
- package/src/cli/argv.ts +14 -1
- package/src/cli/commands/connect/acquire.ts +100 -0
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/declare.ts +19 -0
- package/src/cli/commands/connect/index.ts +54 -56
- package/src/cli/commands/connect/requirements.ts +23 -1
- package/src/cli/commands/connect/setup.ts +1 -1
- package/src/cli/commands/connect/variables.ts +220 -0
- package/src/cli/commands/knowledge/index.ts +22 -4
- package/src/cli/commands/knowledge/migrate.ts +61 -43
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/owner/entities.ts +244 -0
- package/src/cli/commands/owner/shared.ts +14 -0
- package/src/cli/commands/owner/vault.ts +2 -2
- package/src/cli/commands/owner.ts +13 -3
- package/src/cli/commands/setup.ts +12 -0
- package/src/cli/config-edit.ts +16 -6
- package/src/cli/config-repair.ts +10 -0
- package/src/cli/dispatch-owner.ts +30 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/runtime/open.ts +14 -7
- package/src/cli/runtime/registry.ts +6 -1
- package/src/cli/selection.ts +11 -0
- package/src/cli/usage.ts +13 -2
- package/src/connectivity/auth/strategy/index.ts +1 -1
- package/src/connectivity/capability.ts +1 -1
- package/src/connectivity/context.ts +1 -1
- package/src/connectivity/index.ts +1 -1
- package/src/connectivity/mail/attachments.ts +42 -73
- package/src/connectivity/mail/content-type.ts +71 -0
- package/src/connectivity/mail/index.ts +1 -1
- package/src/connectivity/manifest/provider.ts +50 -0
- package/src/connectivity/manifest/variables.ts +173 -0
- package/src/connectivity/transports/factory.ts +74 -1
- package/src/connectivity/transports/http/index.ts +8 -2
- package/src/connectivity/transports/http/keys.ts +83 -0
- package/src/connectivity/transports/imap/attachment.ts +53 -10
- package/src/connectivity/transports/imap/capabilities.ts +28 -0
- package/src/connectivity/transports/imap/download.ts +111 -0
- package/src/connectivity/transports/imap/index.ts +13 -0
- package/src/connectivity/transports/imap/operations.ts +1 -0
- package/src/deployments/adapters/s3.ts +1 -1
- package/src/deployments/gcp/provision.ts +1 -1
- package/src/deployments/knowledge.ts +44 -18
- package/src/deployments/prepare.ts +1 -1
- package/src/deployments/serving.ts +1 -1
- package/src/dispatch/dispatch.ts +17 -10
- package/src/dispatch/staging.ts +76 -1
- package/src/profile/knowledge.ts +19 -11
- package/src/profile/schema.ts +2 -2
- package/src/providers/README.md +189 -0
- package/src/providers/airtable/index.ts +10 -0
- package/src/providers/algolia/index.ts +10 -0
- package/src/providers/amplitude/index.ts +10 -0
- package/src/providers/apify/index.ts +10 -0
- package/src/providers/asana/index.ts +10 -0
- package/src/providers/assets/provider.ts +11 -4
- package/src/providers/atlassian/index.ts +19 -0
- package/src/providers/attio/index.ts +10 -0
- package/src/providers/betterstack/index.ts +10 -0
- package/src/providers/box/index.ts +64 -0
- package/src/providers/brightdata/index.ts +10 -0
- package/src/providers/buildkite/index.ts +10 -0
- package/src/providers/bunq/index.ts +2 -2
- package/src/providers/calendly/index.ts +10 -0
- package/src/providers/canva/index.ts +10 -0
- package/src/providers/circleci/index.ts +10 -0
- package/src/providers/clickup/index.ts +10 -0
- package/src/providers/close/index.ts +10 -0
- package/src/providers/cloudflare_bindings/index.ts +14 -0
- package/src/providers/cloudflare_observability/index.ts +10 -0
- package/src/providers/contentful/index.ts +10 -0
- package/src/providers/custom/load.ts +1 -1
- package/src/providers/datadog/index.ts +10 -0
- package/src/providers/discord/index.ts +1 -1
- package/src/providers/discord/specs/discord.v10.json +0 -88
- package/src/providers/discord/specs/vendor.ts +1 -1
- package/src/providers/dropbox/index.ts +10 -0
- package/src/providers/entities/catalogue.ts +366 -0
- package/src/providers/entities/find.ts +279 -0
- package/src/providers/entities/provider.ts +334 -0
- package/src/providers/entities/render.ts +142 -0
- package/src/providers/entities/store.ts +276 -0
- package/src/providers/entities/writes.ts +279 -0
- package/src/providers/example/provider.ts +1 -1
- package/src/providers/expensify/index.ts +10 -0
- package/src/providers/fastmail/calendar/index.ts +21 -0
- package/src/providers/fastmail/contacts/index.ts +17 -0
- package/src/providers/fastmail/index.ts +4 -0
- package/src/providers/fastmail/mail/index.ts +38 -0
- package/src/providers/fastmail/shared/setup.ts +52 -0
- package/src/providers/figma/index.ts +10 -0
- package/src/providers/fireflies/index.ts +10 -0
- package/src/providers/flagsmith/index.ts +10 -0
- package/src/providers/gamma/index.ts +10 -0
- package/src/providers/github/index.ts +2 -2
- package/src/providers/google/gmail/attachment.ts +16 -1
- package/src/providers/google/gmail-imap/index.ts +7 -2
- package/src/providers/google/shared/oauth.ts +1 -1
- package/src/providers/google/shared/service-account.ts +1 -1
- package/src/providers/google/shared/setup.ts +1 -1
- package/src/providers/google/specs/calendar.v3.json +0 -32
- package/src/providers/google/specs/docs.v1.json +0 -76
- package/src/providers/google/specs/drive.v3.json +0 -32
- package/src/providers/google/specs/gmail.v1.json +0 -76
- package/src/providers/google/specs/people.v1.json +0 -77
- package/src/providers/google/specs/sheets.v4.json +0 -76
- package/src/providers/google/specs/tasks.v1.json +0 -76
- package/src/providers/grafana/index.ts +10 -0
- package/src/providers/heroku/index.ts +10 -0
- package/src/providers/hubspot/index.ts +75 -0
- package/src/providers/hygraph/index.ts +10 -0
- package/src/providers/icloud/mail/index.ts +5 -0
- package/src/providers/icloud/shared/setup.ts +1 -1
- package/src/providers/index.ts +255 -0
- package/src/providers/insightly/index.ts +10 -0
- package/src/providers/jam/index.ts +10 -0
- package/src/providers/klaviyo/index.ts +10 -0
- package/src/providers/mailbox/index.ts +87 -0
- package/src/providers/memory/provider.ts +3 -8
- package/src/providers/mercury/index.ts +10 -0
- package/src/providers/microsoft/calendar/index.ts +49 -0
- package/src/providers/microsoft/contacts/index.ts +50 -0
- package/src/providers/microsoft/drive/index.ts +62 -0
- package/src/providers/microsoft/index.ts +12 -0
- package/src/providers/microsoft/mail/index.ts +76 -0
- package/src/providers/microsoft/shared/oauth.ts +58 -0
- package/src/providers/microsoft/shared/scopes.ts +56 -0
- package/src/providers/microsoft/shared/setup.ts +45 -0
- package/src/providers/microsoft/specs/microsoft-todo.v1.json +755 -0
- package/src/providers/microsoft/specs/onedrive.v1.json +993 -0
- package/src/providers/microsoft/specs/outlook-calendar.v1.json +1743 -0
- package/src/providers/microsoft/specs/outlook-contacts.v1.json +430 -0
- package/src/providers/microsoft/specs/outlook-mail.v1.json +901 -0
- package/src/providers/microsoft/specs/vendor.ts +232 -0
- package/src/providers/microsoft/todo/index.ts +54 -0
- package/src/providers/miro/index.ts +10 -0
- package/src/providers/mixpanel/index.ts +10 -0
- package/src/providers/monday/index.ts +10 -0
- package/src/providers/mux/index.ts +10 -0
- package/src/providers/navan/index.ts +10 -0
- package/src/providers/neon/index.ts +10 -0
- package/src/providers/netlify/index.ts +10 -0
- package/src/providers/nextcloud/calendar/index.ts +28 -0
- package/src/providers/nextcloud/contacts/index.ts +18 -0
- package/src/providers/nextcloud/index.ts +3 -0
- package/src/providers/nextcloud/shared/setup.ts +59 -0
- package/src/providers/owner.ts +15 -3
- package/src/providers/paddle/index.ts +10 -0
- package/src/providers/paypal/index.ts +10 -0
- package/src/providers/posthog/index.ts +10 -0
- package/src/providers/prisma/index.ts +10 -0
- package/src/providers/ramp/index.ts +10 -0
- package/src/providers/recurly/index.ts +10 -0
- package/src/providers/reddit/index.ts +1 -1
- package/src/providers/remote/index.ts +10 -0
- package/src/providers/render/index.ts +35 -0
- package/src/providers/replicate/index.ts +10 -0
- package/src/providers/resend/index.ts +10 -0
- package/src/providers/riverside/index.ts +10 -0
- package/src/providers/rootly/index.ts +10 -0
- package/src/providers/rudderstack/index.ts +10 -0
- package/src/providers/salesloft/index.ts +10 -0
- package/src/providers/sanity/index.ts +10 -0
- package/src/providers/scopes.ts +2 -0
- package/src/providers/sentry/index.ts +10 -0
- package/src/providers/setup/plan.ts +11 -0
- package/src/providers/setup/provider.ts +12 -0
- package/src/providers/shared/openapi.ts +70 -0
- package/src/providers/shared/slug.ts +28 -0
- package/src/providers/shared/vendor-operations.ts +43 -1
- package/src/providers/shared/vendor-spec.ts +98 -17
- package/src/providers/shortcut/index.ts +10 -0
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/square/index.ts +10 -0
- package/src/providers/storyblok/index.ts +10 -0
- package/src/providers/stripe/index.ts +10 -0
- package/src/providers/supabase/index.ts +10 -0
- package/src/providers/tasks/store.ts +2 -7
- package/src/providers/tavily/index.ts +10 -0
- package/src/providers/todoist/index.ts +10 -0
- package/src/providers/untested.ts +105 -0
- package/src/providers/vault/provider.ts +1 -1
- package/src/providers/vercel/index.ts +10 -0
- package/src/providers/vimeo/index.ts +10 -0
- package/src/providers/webflow/index.ts +10 -0
- package/src/providers/whimsical/index.ts +10 -0
- package/src/providers/wix/index.ts +10 -0
- package/src/providers/workable/index.ts +10 -0
- package/src/providers/yahoo_mail/index.ts +73 -0
- package/src/providers/zapier/index.ts +10 -0
- package/src/providers/zoho_mail/index.ts +79 -0
- package/src/secrets/document.ts +1 -1
- package/src/server/attachments.ts +129 -5
- package/src/server/index.ts +3 -3
- package/src/server/mcp/instructions.ts +88 -18
- package/src/server/stdio.ts +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Providers nobody has connected to a real account yet.
|
|
3
|
+
*
|
|
4
|
+
* A manifest can be right in every way this repository can check — the schema
|
|
5
|
+
* validates, the spec generates registrable tools inside the budget, the scopes
|
|
6
|
+
* are described, the endpoint answered an unauthenticated probe — and still not
|
|
7
|
+
* work. What none of that proves is the part only a live account proves: that
|
|
8
|
+
* the grant is the right one, that the identity probe reads a field the vendor
|
|
9
|
+
* actually returns, that a refresh token comes back, that the tool list is worth
|
|
10
|
+
* having.
|
|
11
|
+
*
|
|
12
|
+
* So this is an honest label rather than a warning. Everything here is expected
|
|
13
|
+
* to work and is shipped in that belief; none of it has been used in anger. A
|
|
14
|
+
* provider leaves this list when somebody connects it and it does what it says.
|
|
15
|
+
*
|
|
16
|
+
* One list, and the documentation is checked against it — `src/readme.test.ts`
|
|
17
|
+
* asserts the tables mark exactly these and no others, because a status that is
|
|
18
|
+
* maintained in three places is a status that is wrong in two of them.
|
|
19
|
+
*/
|
|
20
|
+
export const UNTESTED_PROVIDERS: ReadonlySet<string> = new Set([
|
|
21
|
+
'airtable',
|
|
22
|
+
'algolia',
|
|
23
|
+
'amplitude',
|
|
24
|
+
'apify',
|
|
25
|
+
'asana',
|
|
26
|
+
'attio',
|
|
27
|
+
'betterstack',
|
|
28
|
+
'brightdata',
|
|
29
|
+
'buildkite',
|
|
30
|
+
'calendly',
|
|
31
|
+
'canva',
|
|
32
|
+
'circleci',
|
|
33
|
+
'clickup',
|
|
34
|
+
'close',
|
|
35
|
+
'cloudflare_bindings',
|
|
36
|
+
'cloudflare_observability',
|
|
37
|
+
'contentful',
|
|
38
|
+
'datadog',
|
|
39
|
+
'dropbox',
|
|
40
|
+
'expensify',
|
|
41
|
+
'fastmail_calendar',
|
|
42
|
+
'fastmail_contacts',
|
|
43
|
+
'fastmail_mail',
|
|
44
|
+
'figma',
|
|
45
|
+
'fireflies',
|
|
46
|
+
'flagsmith',
|
|
47
|
+
'gamma',
|
|
48
|
+
'grafana',
|
|
49
|
+
'heroku',
|
|
50
|
+
'hygraph',
|
|
51
|
+
'insightly',
|
|
52
|
+
'jam',
|
|
53
|
+
'klaviyo',
|
|
54
|
+
'mercury',
|
|
55
|
+
'microsoft_todo',
|
|
56
|
+
'miro',
|
|
57
|
+
'mixpanel',
|
|
58
|
+
'monday',
|
|
59
|
+
'mux',
|
|
60
|
+
'navan',
|
|
61
|
+
'neon',
|
|
62
|
+
'netlify',
|
|
63
|
+
'onedrive',
|
|
64
|
+
'outlook_calendar',
|
|
65
|
+
'outlook_contacts',
|
|
66
|
+
'outlook_mail',
|
|
67
|
+
'paddle',
|
|
68
|
+
'paypal',
|
|
69
|
+
'posthog',
|
|
70
|
+
'prisma',
|
|
71
|
+
'ramp',
|
|
72
|
+
'recurly',
|
|
73
|
+
'remote',
|
|
74
|
+
'replicate',
|
|
75
|
+
'resend',
|
|
76
|
+
'riverside',
|
|
77
|
+
'rootly',
|
|
78
|
+
'rudderstack',
|
|
79
|
+
'salesloft',
|
|
80
|
+
'sanity',
|
|
81
|
+
'sentry',
|
|
82
|
+
'shortcut',
|
|
83
|
+
'square',
|
|
84
|
+
'storyblok',
|
|
85
|
+
'stripe',
|
|
86
|
+
'supabase',
|
|
87
|
+
'tavily',
|
|
88
|
+
'todoist',
|
|
89
|
+
'vercel',
|
|
90
|
+
'vimeo',
|
|
91
|
+
'webflow',
|
|
92
|
+
'whimsical',
|
|
93
|
+
'wix',
|
|
94
|
+
'workable',
|
|
95
|
+
'yahoo_mail',
|
|
96
|
+
'zapier',
|
|
97
|
+
'zoho_mail',
|
|
98
|
+
'atlassian',
|
|
99
|
+
'hubspot',
|
|
100
|
+
'box',
|
|
101
|
+
'render',
|
|
102
|
+
'mailbox',
|
|
103
|
+
'nextcloud_calendar',
|
|
104
|
+
'nextcloud_contacts',
|
|
105
|
+
]);
|
|
@@ -14,7 +14,7 @@ import { assertItemId, type VaultStore } from '#secrets';
|
|
|
14
14
|
* Three properties, each of which is a decision recorded in ADR-012:
|
|
15
15
|
*
|
|
16
16
|
* **Tools only, never resources.** Resources are listable and cacheable, and
|
|
17
|
-
* both are wrong for a secret (`docs/
|
|
17
|
+
* both are wrong for a secret (`https://lanes.sh/docs/link/capabilities`).
|
|
18
18
|
*
|
|
19
19
|
* **Per-item policy through the capability name.** Each stored item gets its own
|
|
20
20
|
* read capability — `vault.get.github_token` — so `deny: [vault.get.bank]` and
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Vercel registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const vercel = defineProvider({
|
|
5
|
+
id: 'vercel',
|
|
6
|
+
name: 'Vercel',
|
|
7
|
+
description: 'Projects, deployments, build logs, and domains, via Vercel\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.vercel.com' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Vimeo registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const vimeo = defineProvider({
|
|
5
|
+
id: 'vimeo',
|
|
6
|
+
name: 'Vimeo',
|
|
7
|
+
description: 'Videos, folders, showcases, and analytics, via Vimeo\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.vimeo.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Webflow registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const webflow = defineProvider({
|
|
5
|
+
id: 'webflow',
|
|
6
|
+
name: 'Webflow',
|
|
7
|
+
description: 'Sites, pages, CMS collections, and items, via Webflow\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.webflow.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Whimsical registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const whimsical = defineProvider({
|
|
5
|
+
id: 'whimsical',
|
|
6
|
+
name: 'Whimsical',
|
|
7
|
+
description: 'Boards, flowcharts, wireframes, and mind maps, via Whimsical\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.whimsical.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Wix registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const wix = defineProvider({
|
|
5
|
+
id: 'wix',
|
|
6
|
+
name: 'Wix',
|
|
7
|
+
description: 'Sites, stores, bookings, and CMS data, via Wix\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.wix.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Workable registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const workable = defineProvider({
|
|
5
|
+
id: 'workable',
|
|
6
|
+
name: 'Workable',
|
|
7
|
+
description: 'Jobs, candidates, and interviews, via Workable\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.workable.com/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Yahoo Mail over IMAP.
|
|
5
|
+
*
|
|
6
|
+
* Yahoo requires an app password for IMAP — the account password is refused
|
|
7
|
+
* outright, and has been since Yahoo turned off "less secure app access". The
|
|
8
|
+
* same hosts serve AOL and the other Yahoo-operated domains under their own
|
|
9
|
+
* names, which is why this is one provider rather than a family.
|
|
10
|
+
*/
|
|
11
|
+
export const yahooMail = defineProvider({
|
|
12
|
+
id: 'yahoo_mail',
|
|
13
|
+
name: 'Yahoo Mail',
|
|
14
|
+
description:
|
|
15
|
+
'Read, search, and send mail in a Yahoo mailbox over IMAP and SMTP, with an app password.',
|
|
16
|
+
connector: {
|
|
17
|
+
kind: 'imap',
|
|
18
|
+
host: 'imap.mail.yahoo.com',
|
|
19
|
+
port: 993,
|
|
20
|
+
smtp: { host: 'smtp.mail.yahoo.com', port: 587, starttls: true },
|
|
21
|
+
},
|
|
22
|
+
auth: { kind: 'basic' },
|
|
23
|
+
identity: { kind: 'connector' },
|
|
24
|
+
setup: {
|
|
25
|
+
summary:
|
|
26
|
+
'Yahoo Mail uses an app password, which is not your Yahoo account password. Yahoo refuses the ' +
|
|
27
|
+
'account password for IMAP entirely, so there is no other route to a Yahoo mailbox here.',
|
|
28
|
+
docs_url: 'https://help.yahoo.com/kb/SLN15241.html',
|
|
29
|
+
steps: [
|
|
30
|
+
'Sign in at https://login.yahoo.com/account/security.',
|
|
31
|
+
'Choose "Generate app password" (it may be under "Other ways to sign in"). Name it "Lanes Link" — the name is the only way to revoke this one later without cutting off your other devices.',
|
|
32
|
+
'Copy the sixteen characters. Yahoo shows them once, in four groups of four; the spaces are cosmetic and it is accepted either way.',
|
|
33
|
+
'You are asked for your full address next, which is the one you sign in with.',
|
|
34
|
+
],
|
|
35
|
+
troubleshooting:
|
|
36
|
+
'For Yahoo this is almost always the account password used where an app password belongs — Yahoo ' +
|
|
37
|
+
'refuses the account password for IMAP and reports it identically to a wrong one. Generate an app ' +
|
|
38
|
+
'password at https://login.yahoo.com/account/security and re-run: lanes link connect yahoo_mail --replace.',
|
|
39
|
+
prompts: [
|
|
40
|
+
{
|
|
41
|
+
key: 'username',
|
|
42
|
+
label: 'Yahoo address (the full email address)',
|
|
43
|
+
secret: false,
|
|
44
|
+
scope: 'connection' as const,
|
|
45
|
+
field: 'username' as const,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: 'password',
|
|
49
|
+
label: 'App password (sixteen characters)',
|
|
50
|
+
secret: true,
|
|
51
|
+
scope: 'connection' as const,
|
|
52
|
+
field: 'password' as const,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
redact: {
|
|
57
|
+
// Never the search terms — a query is content, and "who did I email about
|
|
58
|
+
// the diagnosis" is the whole message.
|
|
59
|
+
search_messages: ['mailbox', 'limit', 'unseen', 'flagged'],
|
|
60
|
+
// Identifiers only, and all of them: which mailbox, which message, which
|
|
61
|
+
// attachment. What the file turned out to be — name, size, type, digest —
|
|
62
|
+
// is recorded by the handler through `audit.annotate`, the same way the send
|
|
63
|
+
// path records what it attached.
|
|
64
|
+
get_attachment: ['mailbox', 'uid', 'message_id', 'attachment_id'],
|
|
65
|
+
get_message: ['mailbox', 'uid', 'include_body'],
|
|
66
|
+
mark_messages: ['mailbox', 'add_flags', 'remove_flags'],
|
|
67
|
+
move_messages: ['mailbox', 'destination', 'destination_flag'],
|
|
68
|
+
// Nothing. The recipients and the body are the message, and `attachments`
|
|
69
|
+
// may literally contain a file — what was attached is recorded by the send
|
|
70
|
+
// path itself through `audit.annotate`.
|
|
71
|
+
send_message: [],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/** Zapier registers us at connect time — nothing for an operator to set up. */
|
|
4
|
+
export const zapier = defineProvider({
|
|
5
|
+
id: 'zapier',
|
|
6
|
+
name: 'Zapier',
|
|
7
|
+
description: 'Zaps, and the actions they reach across thousands of apps, via Zapier\'s official MCP server.',
|
|
8
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.zapier.com/api/mcp/mcp' },
|
|
9
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
10
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zoho Mail over IMAP.
|
|
5
|
+
*
|
|
6
|
+
* The hosts below are Zoho's global ones. An account created in a different
|
|
7
|
+
* data centre answers on a regional hostname instead — `imap.zoho.eu`,
|
|
8
|
+
* `imap.zoho.in`, `imap.zoho.com.au` — and the failure is a connection refused
|
|
9
|
+
* rather than an authentication error, which reads like the password is wrong.
|
|
10
|
+
* A manifest holds one host, so a regional account is a manifest of its own in
|
|
11
|
+
* `providers.d/`: the same two hosts with the suffix changed. `troubleshooting`
|
|
12
|
+
* below is where somebody actually hits this.
|
|
13
|
+
*/
|
|
14
|
+
export const zohoMail = defineProvider({
|
|
15
|
+
id: 'zoho_mail',
|
|
16
|
+
name: 'Zoho Mail',
|
|
17
|
+
description:
|
|
18
|
+
'Read, search, and send mail in a Zoho mailbox over IMAP and SMTP, with an application-specific password.',
|
|
19
|
+
connector: {
|
|
20
|
+
kind: 'imap',
|
|
21
|
+
host: 'imap.zoho.com',
|
|
22
|
+
port: 993,
|
|
23
|
+
smtp: { host: 'smtp.zoho.com', port: 587, starttls: true },
|
|
24
|
+
},
|
|
25
|
+
auth: { kind: 'basic' },
|
|
26
|
+
identity: { kind: 'connector' },
|
|
27
|
+
setup: {
|
|
28
|
+
summary:
|
|
29
|
+
'Zoho Mail uses an application-specific password when two-factor authentication is on, which ' +
|
|
30
|
+
'is the supported way to reach IMAP. It is not your Zoho account password, and it does not expire.',
|
|
31
|
+
docs_url: 'https://www.zoho.com/accounts/help/multi-factor-authentication/application-specific-passwords.html',
|
|
32
|
+
steps: [
|
|
33
|
+
'Sign in at https://accounts.zoho.com and open Security → App Passwords.',
|
|
34
|
+
'Choose "Generate New Password" and name it "Lanes Link" — the name is the only way to revoke this one later without cutting off your other devices.',
|
|
35
|
+
'Copy the password Zoho shows. It is shown once.',
|
|
36
|
+
'IMAP has to be enabled for the mailbox: Zoho Mail → Settings → Mail Accounts → IMAP Access.',
|
|
37
|
+
'If your account is in a region other than the global one, the hosts differ — see the note below.',
|
|
38
|
+
],
|
|
39
|
+
troubleshooting:
|
|
40
|
+
'If the connection is refused rather than the password rejected, the account is likely in a regional ' +
|
|
41
|
+
'data centre: imap.zoho.eu, imap.zoho.in, or imap.zoho.com.au rather than imap.zoho.com. Declare those ' +
|
|
42
|
+
'hosts in a manifest of your own under providers.d/ — see docs/detailed/creating-a-provider.md. If the ' +
|
|
43
|
+
'password is rejected, it is usually the Zoho account password used where an application-specific one ' +
|
|
44
|
+
'belongs: generate one at https://accounts.zoho.com and re-run: lanes link connect zoho_mail --replace.',
|
|
45
|
+
prompts: [
|
|
46
|
+
{
|
|
47
|
+
key: 'username',
|
|
48
|
+
label: 'Zoho address (the full email address)',
|
|
49
|
+
secret: false,
|
|
50
|
+
scope: 'connection' as const,
|
|
51
|
+
field: 'username' as const,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'password',
|
|
55
|
+
label: 'Application-specific password',
|
|
56
|
+
secret: true,
|
|
57
|
+
scope: 'connection' as const,
|
|
58
|
+
field: 'password' as const,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
redact: {
|
|
63
|
+
// Never the search terms — a query is content, and "who did I email about
|
|
64
|
+
// the diagnosis" is the whole message.
|
|
65
|
+
search_messages: ['mailbox', 'limit', 'unseen', 'flagged'],
|
|
66
|
+
// Identifiers only, and all of them: which mailbox, which message, which
|
|
67
|
+
// attachment. What the file turned out to be — name, size, type, digest —
|
|
68
|
+
// is recorded by the handler through `audit.annotate`, the same way the send
|
|
69
|
+
// path records what it attached.
|
|
70
|
+
get_attachment: ['mailbox', 'uid', 'message_id', 'attachment_id'],
|
|
71
|
+
get_message: ['mailbox', 'uid', 'include_body'],
|
|
72
|
+
mark_messages: ['mailbox', 'add_flags', 'remove_flags'],
|
|
73
|
+
move_messages: ['mailbox', 'destination', 'destination_flag'],
|
|
74
|
+
// Nothing. The recipients and the body are the message, and `attachments`
|
|
75
|
+
// may literally contain a file — what was attached is recorded by the send
|
|
76
|
+
// path itself through `audit.annotate`.
|
|
77
|
+
send_message: [],
|
|
78
|
+
},
|
|
79
|
+
});
|
package/src/secrets/document.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { BlobStore } from '#stores/blobs';
|
|
|
17
17
|
*
|
|
18
18
|
* What is *not* merged is the two stores themselves. They keep separate
|
|
19
19
|
* documents, separate keys, and separate environment variables, because that is
|
|
20
|
-
* the boundary `docs/
|
|
20
|
+
* the boundary `https://lanes.sh/docs/link/security` is built on: one master secret reused across
|
|
21
21
|
* purposes turns any single compromise into a total one. Sharing the code that
|
|
22
22
|
* seals a document is the opposite of sharing the key that opens it.
|
|
23
23
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isHandle } from '#connectivity/mail';
|
|
1
2
|
import { mergeCapabilities, type ProfileRuntime } from './mcp/index.ts';
|
|
2
3
|
import type { Principal } from '#auth';
|
|
3
4
|
|
|
@@ -51,11 +52,20 @@ export interface StageOptions {
|
|
|
51
52
|
readonly clientLabel?: string | undefined;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* One path, both directions.
|
|
57
|
+
*
|
|
58
|
+
* `POST` takes bytes and gives back a handle; `GET` takes a handle and gives
|
|
59
|
+
* back the bytes. Same path, same query, same authorization, because they are
|
|
60
|
+
* the two ends of one idea and splitting them across two routes would invite
|
|
61
|
+
* the scoping to drift apart — which is the one thing that must not happen
|
|
62
|
+
* here, since the scoping is what keeps one account's files out of another's.
|
|
63
|
+
*/
|
|
64
|
+
export async function handleAttachments(options: StageOptions): Promise<Response> {
|
|
55
65
|
const { request } = options;
|
|
56
66
|
|
|
57
|
-
if (request.method !== 'POST') {
|
|
58
|
-
return problem(405, 'Stage an attachment with POST.');
|
|
67
|
+
if (request.method !== 'POST' && request.method !== 'GET') {
|
|
68
|
+
return problem(405, 'Stage an attachment with POST, or fetch one with GET.');
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
const url = new URL(request.url);
|
|
@@ -82,7 +92,8 @@ export async function stageAttachment(options: StageOptions): Promise<Response>
|
|
|
82
92
|
|
|
83
93
|
// The same policy that decides which connections a tool may name. Staging into
|
|
84
94
|
// an account the caller cannot act on would be a write they are not permitted
|
|
85
|
-
// to make, even though nothing is sent by it
|
|
95
|
+
// to make, even though nothing is sent by it — and fetching back out of one is
|
|
96
|
+
// a read they are not permitted to make, which matters more.
|
|
86
97
|
if (!reachable(runtime, options, profileName, target)) {
|
|
87
98
|
return problem(
|
|
88
99
|
403,
|
|
@@ -90,6 +101,10 @@ export async function stageAttachment(options: StageOptions): Promise<Response>
|
|
|
90
101
|
);
|
|
91
102
|
}
|
|
92
103
|
|
|
104
|
+
if (request.method === 'GET') {
|
|
105
|
+
return await fetchAttachment({ options, runtime, url, providerId, connectionId, target });
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
const bytes = await readCapped(request);
|
|
94
109
|
if (!bytes) {
|
|
95
110
|
return problem(413, `An attachment must be smaller than ${MAX_UPLOAD_BYTES} bytes.`);
|
|
@@ -124,6 +139,83 @@ export async function stageAttachment(options: StageOptions): Promise<Response>
|
|
|
124
139
|
});
|
|
125
140
|
}
|
|
126
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Handing a staged file back.
|
|
144
|
+
*
|
|
145
|
+
* The counterpart to the upload, and the thing that makes an attachment which
|
|
146
|
+
* arrived by mail reachable at all: `get_attachment` on a mail connection pulls
|
|
147
|
+
* the bytes out of the mailbox and stages them *in that same connection's*
|
|
148
|
+
* namespace, and this is where the client collects them. No provider gains
|
|
149
|
+
* reach it did not have — the bytes were already inside that account's area —
|
|
150
|
+
* and nothing crosses between two connections.
|
|
151
|
+
*
|
|
152
|
+
* The bytes go out over HTTP for the same reason they come in that way: a file
|
|
153
|
+
* in a tool result is base64 in the model's context, which for a 239 KB PDF is
|
|
154
|
+
* around 320,000 characters of something no model needs to read.
|
|
155
|
+
*/
|
|
156
|
+
async function fetchAttachment(input: {
|
|
157
|
+
readonly options: StageOptions;
|
|
158
|
+
readonly runtime: ProfileRuntime;
|
|
159
|
+
readonly url: URL;
|
|
160
|
+
readonly providerId: string;
|
|
161
|
+
readonly connectionId: string;
|
|
162
|
+
readonly target: string;
|
|
163
|
+
}): Promise<Response> {
|
|
164
|
+
const handle = input.url.searchParams.get('handle');
|
|
165
|
+
if (!handle) {
|
|
166
|
+
return problem(400, 'Name the file to fetch, as ?handle=att_… — the handle a stage or a get_attachment returned.');
|
|
167
|
+
}
|
|
168
|
+
if (!isHandle(handle)) {
|
|
169
|
+
return problem(400, `"${handle}" is not a handle.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const found = await input.runtime.dispatcher.fetchStagedAttachment({
|
|
173
|
+
principal: input.options.principal,
|
|
174
|
+
providerId: input.providerId,
|
|
175
|
+
connectionId: input.connectionId,
|
|
176
|
+
handle,
|
|
177
|
+
clientLabel: input.options.clientLabel,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// One answer for absent, expired, and staged-against-another-connection. The
|
|
181
|
+
// caller can act on none of the three differently, and telling them apart is
|
|
182
|
+
// how someone probes for handles belonging to an account they cannot reach.
|
|
183
|
+
if (!found) {
|
|
184
|
+
return problem(
|
|
185
|
+
404,
|
|
186
|
+
`No staged file "${handle}" for ${input.target}. A staged file lasts 24 hours and belongs to the connection it was staged for.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Response(found.bytes, {
|
|
191
|
+
headers: {
|
|
192
|
+
'content-type': found.contentType ?? 'application/octet-stream',
|
|
193
|
+
'content-length': String(found.bytes.byteLength),
|
|
194
|
+
// `attachment` rather than `inline`: this is a file the caller asked for,
|
|
195
|
+
// and a browser rendering someone's mail attachment in the tab is not it.
|
|
196
|
+
'content-disposition': dispositionFor(found.filename),
|
|
197
|
+
// Handles are unguessable and short-lived, but a shared cache holding
|
|
198
|
+
// somebody's mail attachment is not a trade worth making.
|
|
199
|
+
'cache-control': 'no-store',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A `Content-Disposition` that survives a non-ASCII filename.
|
|
206
|
+
*
|
|
207
|
+
* RFC 6266: the bare `filename` is the fallback for old clients and must stay
|
|
208
|
+
* ASCII, and `filename*` carries the real one percent-encoded. Mail attachments
|
|
209
|
+
* are exactly where this bites — an invoice from a German sender is called
|
|
210
|
+
* `Rechnung_Kaufmännisch.pdf` and a raw header would be rejected outright.
|
|
211
|
+
*/
|
|
212
|
+
function dispositionFor(filename: string | null): string {
|
|
213
|
+
if (!filename) return 'attachment';
|
|
214
|
+
|
|
215
|
+
const ascii = filename.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
|
|
216
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
127
219
|
/** Whether this principal can reach that connection for anything at all. */
|
|
128
220
|
function reachable(
|
|
129
221
|
runtime: ProfileRuntime,
|
|
@@ -188,10 +280,42 @@ async function readCapped(request: Request): Promise<Uint8Array | null> {
|
|
|
188
280
|
* `Content-Disposition` and stored beside the bytes, and neither wants a path.
|
|
189
281
|
*/
|
|
190
282
|
function filenameFrom(header: string | null): string {
|
|
191
|
-
const candidate = (header ?? '').split(/[/\\]/).pop()?.trim() ?? '';
|
|
283
|
+
const candidate = decodeHeader(header ?? '').split(/[/\\]/).pop()?.trim() ?? '';
|
|
192
284
|
return candidate === '' || candidate === '.' || candidate === '..' ? 'attachment' : candidate;
|
|
193
285
|
}
|
|
194
286
|
|
|
287
|
+
/**
|
|
288
|
+
* A header value as the bytes it actually was.
|
|
289
|
+
*
|
|
290
|
+
* An HTTP header carries bytes, and a runtime hands them back one character per
|
|
291
|
+
* byte — so a filename a client sent as UTF-8 arrives here as mojibake and is
|
|
292
|
+
* *stored* that way. `Rechnung_Kaufmännisch.pdf` became
|
|
293
|
+
* `Rechnung_Kaufmännisch.pdf` in the metadata sidecar, and stayed wrong on the
|
|
294
|
+
* way back out; it was invisible while there was no way to fetch a file back.
|
|
295
|
+
*
|
|
296
|
+
* Pure ASCII is returned untouched, which is almost every filename. Anything
|
|
297
|
+
* that is not valid UTF-8 keeps whatever it was rather than being replaced with
|
|
298
|
+
* question marks — a name we cannot decode is still the name the client chose.
|
|
299
|
+
*/
|
|
300
|
+
function decodeHeader(value: string): string {
|
|
301
|
+
// Nothing to do for ASCII, and a code point above 0xff means the runtime has
|
|
302
|
+
// already decoded it — reinterpreting that would be the corruption, not the fix.
|
|
303
|
+
let latin1 = false;
|
|
304
|
+
for (const character of value) {
|
|
305
|
+
const code = character.codePointAt(0) ?? 0;
|
|
306
|
+
if (code > 0xff) return value;
|
|
307
|
+
if (code > 0x7f) latin1 = true;
|
|
308
|
+
}
|
|
309
|
+
if (!latin1) return value;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const bytes = Uint8Array.from(value, (character) => character.charCodeAt(0));
|
|
313
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
314
|
+
} catch {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
195
319
|
function problem(status: number, message: string): Response {
|
|
196
320
|
return Response.json({ error: message }, { status });
|
|
197
321
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Authenticator } from '#auth';
|
|
2
2
|
import type { Logger } from '#connectivity';
|
|
3
3
|
import { capabilityIdForToolName } from '#server/mcp';
|
|
4
|
-
import { ATTACHMENTS_PATH,
|
|
4
|
+
import { ATTACHMENTS_PATH, handleAttachments } from './attachments.ts';
|
|
5
5
|
import { allowedHostnamesFor, rebindingRefusal } from './rebinding.ts';
|
|
6
6
|
import { ANY_ORIGIN, corsAware, type CorsPolicy } from './cors.ts';
|
|
7
7
|
import type { Generation } from './generation.ts';
|
|
@@ -234,7 +234,7 @@ export function createRequestHandler(options: ServerOptions): RequestHandler {
|
|
|
234
234
|
// into one named connection rather than a shared area, so a staged file
|
|
235
235
|
// stays as isolated as the account it was staged for.
|
|
236
236
|
if (url.pathname === ATTACHMENTS_PATH) {
|
|
237
|
-
return await
|
|
237
|
+
return await handleAttachments({
|
|
238
238
|
profiles: generation.profiles,
|
|
239
239
|
primary: options.primary,
|
|
240
240
|
principal: outcome.principal,
|
|
@@ -262,7 +262,7 @@ export function createRequestHandler(options: ServerOptions): RequestHandler {
|
|
|
262
262
|
// whose default is `'stateless'`, so those requests are served rather
|
|
263
263
|
// than refused. This check short-circuits and the refusal goes
|
|
264
264
|
// unrecorded. That is the second documented exception to
|
|
265
|
-
// `audit.every-invocation` in `docs/
|
|
265
|
+
// `audit.every-invocation` in `https://lanes.sh/docs/link/security`, asserted in
|
|
266
266
|
// `index.test.ts`. Closing it means cloning and parsing the body when
|
|
267
267
|
// the header is absent, which is what `stdio.ts` does for want of
|
|
268
268
|
// headers.
|