@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,146 @@
|
|
|
1
|
+
import { discoverOAuthProtectedResourceMetadata } from '@modelcontextprotocol/client';
|
|
2
|
+
import type { ProviderManifest } from '#connectivity';
|
|
3
|
+
import { progress, style, warn } from '../../output.ts';
|
|
4
|
+
import type { Prompter } from '../../prompt.ts';
|
|
5
|
+
import { describeScopes, shortScope } from '../../scopes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Saying what a grant will be able to do, while declining still costs nothing.
|
|
9
|
+
*
|
|
10
|
+
* Its own file because both halves of `authorise` run it — the SDK path and the
|
|
11
|
+
* direct one — and because the file it came out of had grown past the budget.
|
|
12
|
+
* The seam is real: everything here happens before any client is chosen and
|
|
13
|
+
* before any listener is opened.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Warn when a manifest's pinned scopes have drifted from the server's.
|
|
18
|
+
*
|
|
19
|
+
* Pinning is deliberate — it stops a vendor widening a grant by editing its own
|
|
20
|
+
* metadata — but a pinned list is a list that can go stale, and both directions
|
|
21
|
+
* of staleness are worth different words:
|
|
22
|
+
*
|
|
23
|
+
* - the server declares a scope we do not request → calls fail, and Google's
|
|
24
|
+
* servers say only "The caller does not have permission", which points
|
|
25
|
+
* nowhere near the cause. This is the failure that cost an afternoon.
|
|
26
|
+
* - we request one it no longer declares → probably harmless, possibly a scope
|
|
27
|
+
* that has been withdrawn; worth seeing, not worth stopping for.
|
|
28
|
+
*
|
|
29
|
+
* Advisory in both directions. Discovery failing must not block a connect that
|
|
30
|
+
* would otherwise work, so a broken probe is silent.
|
|
31
|
+
*/
|
|
32
|
+
async function reportScopeDrift(pinned: readonly string[], serverUrl: string): Promise<void> {
|
|
33
|
+
let advertised: readonly string[] = [];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const metadata = await discoverOAuthProtectedResourceMetadata(serverUrl);
|
|
37
|
+
advertised = (metadata as { scopes_supported?: string[] }).scopes_supported ?? [];
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (advertised.length === 0) return;
|
|
43
|
+
|
|
44
|
+
const missing = advertised.filter((scope) => !pinned.includes(scope));
|
|
45
|
+
const extra = pinned.filter((scope) => !advertised.includes(scope));
|
|
46
|
+
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
progress('');
|
|
49
|
+
progress(
|
|
50
|
+
style.dim(
|
|
51
|
+
`${new URL(serverUrl).host} advertises ${missing.length} scope(s) not requested: ` +
|
|
52
|
+
`${missing.map(shortScope).join(', ')}.`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
progress(
|
|
56
|
+
style.dim(
|
|
57
|
+
' Deliberate — an advertised scope is not necessarily a required one, and these are broader than the docs ask for. Worth revisiting only if calls fail on permission.',
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (extra.length > 0) {
|
|
63
|
+
progress('');
|
|
64
|
+
progress(style.dim(`Note: ${extra.map(shortScope).join(', ')} is no longer declared by the server.`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Show what is about to be granted, and stop on the broad ones.
|
|
70
|
+
*
|
|
71
|
+
* The consent screen that follows lists the same scopes in the vendor's own
|
|
72
|
+
* wording, where "Read, compose, send, and permanently delete all your email"
|
|
73
|
+
* sits in a list of five and reads like boilerplate. This is the same
|
|
74
|
+
* information a step earlier, in our words, with the account still unconnected
|
|
75
|
+
* — the last point where declining costs nothing.
|
|
76
|
+
*/
|
|
77
|
+
export async function confirmScopes(
|
|
78
|
+
manifest: ProviderManifest,
|
|
79
|
+
serverUrl: string,
|
|
80
|
+
prompter: Prompter,
|
|
81
|
+
acceptBroadScopes: boolean,
|
|
82
|
+
/**
|
|
83
|
+
* What will actually be asked for, which is not always what the manifest
|
|
84
|
+
* declares: a brokered flow appends the broker's identity scopes. Showing the
|
|
85
|
+
* manifest's set instead would put a scope on the vendor's screen that this
|
|
86
|
+
* gate never mentioned, which is precisely the surprise it exists to prevent.
|
|
87
|
+
*/
|
|
88
|
+
scopes: readonly string[] = manifest.auth.kind === 'oauth' ? manifest.auth.scopes : [],
|
|
89
|
+
): Promise<boolean> {
|
|
90
|
+
if (manifest.auth.kind !== 'oauth' || scopes.length === 0) return true;
|
|
91
|
+
|
|
92
|
+
await reportScopeDrift(scopes, serverUrl);
|
|
93
|
+
|
|
94
|
+
const described = describeScopes(scopes);
|
|
95
|
+
|
|
96
|
+
progress('');
|
|
97
|
+
progress(`${manifest.name} will be granted:`);
|
|
98
|
+
for (const { scope, meaning, broad } of described) {
|
|
99
|
+
const name = broad ? style.bold(shortScope(scope)) : shortScope(scope);
|
|
100
|
+
progress(` ${broad ? '!' : '·'} ${name}${meaning ? style.dim(` — ${meaning}`) : ''}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!described.some((entry) => entry.broad)) {
|
|
104
|
+
progress('');
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Why the broad ones cannot simply be dropped — otherwise the obvious next
|
|
109
|
+
// question is why we ask instead of asking for less.
|
|
110
|
+
progress('');
|
|
111
|
+
progress(
|
|
112
|
+
warn(
|
|
113
|
+
'The marked scopes are broader than this provider needs. Grant them only if you ' +
|
|
114
|
+
'mean to — policy can restrict what an agent calls, but it cannot un-grant a token.',
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
progress(
|
|
118
|
+
style.dim(
|
|
119
|
+
' Policy still applies: only capabilities you allow are reachable, and every call is audited.',
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
progress('');
|
|
123
|
+
|
|
124
|
+
// Answered ahead of time, by a person, in their own shell. The flag is long
|
|
125
|
+
// enough that repeating it is a deliberate act, and it lands in the history of
|
|
126
|
+
// whoever typed it — which is the property that matters, since the point of
|
|
127
|
+
// this gate is that the decision does not originate with the agent.
|
|
128
|
+
if (acceptBroadScopes) {
|
|
129
|
+
progress(style.dim('Broad scopes accepted with --accept-broad-scopes.'));
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fail closed rather than defaulting to no. A non-interactive run cannot
|
|
134
|
+
// answer, and inventing an answer here would remove the last point at which
|
|
135
|
+
// declining is free — the vendor's own consent screen is next, where the same
|
|
136
|
+
// sentence sits in a list of five and reads like boilerplate.
|
|
137
|
+
if (!prompter.interactive) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${manifest.name} asks for scopes broader than it needs, and this run is non-interactive.\n` +
|
|
140
|
+
` Nothing was authorised. Re-run in a terminal, or add --accept-broad-scopes if you mean to grant them.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return prompter.confirm('Authorise with these scopes?', false);
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createMcpConnector } from '#connectivity/transports';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import type { Config } from '#profile';
|
|
4
|
+
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
5
|
+
import { idFromAccount, resolveAccount } from '../../identity.ts';
|
|
6
|
+
import { style } from '../../output.ts';
|
|
7
|
+
import { terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
8
|
+
import { accountSiblings } from './accounts.ts';
|
|
9
|
+
import { oauthProviderFor } from './authorise.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Settle which connection this is, and whose account it belongs to.
|
|
13
|
+
*
|
|
14
|
+
* The order matters. An explicit `--id` wins, because someone who named it
|
|
15
|
+
* meant it. Otherwise the provider is asked who authorised — and if an existing
|
|
16
|
+
* connection already holds that account, we reuse *its* id, which is what turns
|
|
17
|
+
* a re-run of `connect` into a repair rather than a duplicate. Only when
|
|
18
|
+
* identity cannot be resolved at all do we ask.
|
|
19
|
+
*/
|
|
20
|
+
export async function settleIdentity(input: {
|
|
21
|
+
manifest: ProviderManifest;
|
|
22
|
+
provisionalId: string;
|
|
23
|
+
explicitId: string | undefined;
|
|
24
|
+
account: string | undefined;
|
|
25
|
+
runtime: {
|
|
26
|
+
config: Config;
|
|
27
|
+
credentials: SecretStore;
|
|
28
|
+
registry: { manifest(id: string): ProviderManifest | undefined };
|
|
29
|
+
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
30
|
+
};
|
|
31
|
+
prompter?: Prompter;
|
|
32
|
+
}): Promise<{ connectionId: string; account: string }> {
|
|
33
|
+
const { manifest, provisionalId, explicitId, runtime } = input;
|
|
34
|
+
const prompter = input.prompter ?? terminalPrompter;
|
|
35
|
+
|
|
36
|
+
// Siblings across the whole vendor account, not just this provider — so
|
|
37
|
+
// connecting iCloud Calendar after iCloud Mail lands on the same id, and
|
|
38
|
+
// therefore the same credential, rather than a second `will2`.
|
|
39
|
+
const siblings = accountSiblings(manifest, runtime.config, runtime.registry);
|
|
40
|
+
|
|
41
|
+
let account = input.account ?? null;
|
|
42
|
+
|
|
43
|
+
// A connector-resolved identity is also the credential check — it is the name
|
|
44
|
+
// the server *accepted* — so it is called outside `resolveAccount`, whose
|
|
45
|
+
// catch-all would turn a rejected password into a polite "which account is
|
|
46
|
+
// this?" and hand you a connection that cannot work.
|
|
47
|
+
if (!account && manifest.identity?.kind === 'connector') {
|
|
48
|
+
account = (await runtime.connectorFor(manifest.id, provisionalId)?.identify?.()) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!account) {
|
|
52
|
+
account = await resolveAccount(manifest, {
|
|
53
|
+
accessToken: async () => {
|
|
54
|
+
const provider = oauthProviderFor(manifest, provisionalId, runtime.credentials);
|
|
55
|
+
const tokens = (await provider.tokens()) as { access_token?: string } | undefined;
|
|
56
|
+
return tokens?.access_token ?? null;
|
|
57
|
+
},
|
|
58
|
+
// A protocol that authenticates by username has nothing to GET and no
|
|
59
|
+
// tool to call — it knows, once the server has accepted the login.
|
|
60
|
+
identify: async () =>
|
|
61
|
+
(await runtime.connectorFor(manifest.id, provisionalId)?.identify?.()) ?? null,
|
|
62
|
+
...(manifest.connector.kind === 'mcp'
|
|
63
|
+
? {
|
|
64
|
+
callTool: async (name: string, args: Record<string, unknown>) => {
|
|
65
|
+
const connector = createMcpConnector({
|
|
66
|
+
endpoint: (manifest.connector as { endpoint: string }).endpoint,
|
|
67
|
+
accessToken: async () => {
|
|
68
|
+
const provider = oauthProviderFor(manifest, provisionalId, runtime.credentials);
|
|
69
|
+
const tokens = (await provider.tokens()) as { access_token?: string } | undefined;
|
|
70
|
+
return tokens?.access_token ?? null;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return connector.invoke({ name, inputSchema: {}, description: '' } as never, args, {
|
|
74
|
+
manifest,
|
|
75
|
+
provider: undefined as never,
|
|
76
|
+
authorize: async (request) => request,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A provider that authenticates to nothing has no account to name, so asking
|
|
85
|
+
// is a question with no answer — and one that needs a terminal, which makes an
|
|
86
|
+
// otherwise scriptable connect interactive for no reason.
|
|
87
|
+
//
|
|
88
|
+
// The *label* is the provider's name; the *id* is `main`, like every other
|
|
89
|
+
// first connection. Deriving the id from the label gave `memory.memory`,
|
|
90
|
+
// `skills.skills`, `vault.vault` — and, on disk, a memory entry at
|
|
91
|
+
// `memory/memory/…`. `memory.main` reads the way `gmail.main` does.
|
|
92
|
+
const unaccounted = !account && manifest.auth.kind === 'none';
|
|
93
|
+
if (unaccounted) account = manifest.name;
|
|
94
|
+
|
|
95
|
+
if (!account) {
|
|
96
|
+
// Nothing to go on. Asking beats inventing `main2`, and the answer is the
|
|
97
|
+
// one piece of information the file cannot reconstruct later — which is
|
|
98
|
+
// also why a non-interactive run refuses rather than making one up. An
|
|
99
|
+
// invented label is the row an operator reads in `status` forever.
|
|
100
|
+
if (!prompter.interactive) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${manifest.name} could not report whose account this is, and this run is non-interactive.\n` +
|
|
103
|
+
` Nothing was written. Pass --display-name "<label>" to name it yourself.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
account =
|
|
108
|
+
(await prompter.ask(`Which account is this? ${style.dim('(label for this connection)')}`)) ||
|
|
109
|
+
`${manifest.name} ${provisionalId}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (explicitId) return { connectionId: explicitId, account };
|
|
113
|
+
|
|
114
|
+
if (unaccounted) {
|
|
115
|
+
return {
|
|
116
|
+
connectionId: idFromAccount(
|
|
117
|
+
'main',
|
|
118
|
+
siblings.map((candidate) => candidate.id),
|
|
119
|
+
),
|
|
120
|
+
account: account!,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const already = siblings.find(
|
|
125
|
+
(candidate) => candidate.account.toLowerCase() === account.toLowerCase(),
|
|
126
|
+
);
|
|
127
|
+
if (already) return { connectionId: already.id, account };
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
connectionId: idFromAccount(
|
|
131
|
+
account,
|
|
132
|
+
siblings.map((candidate) => candidate.id),
|
|
133
|
+
),
|
|
134
|
+
account,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { SecretStore } from '#secrets';
|
|
2
|
+
import type { ProviderManifest, SetupPrompt } from '#connectivity';
|
|
3
|
+
import { credentialRefForConnection } from '#connectivity';
|
|
4
|
+
import { ConfigDocument } from '../../config-edit.ts';
|
|
5
|
+
import { ok, progress, style } from '../../output.ts';
|
|
6
|
+
import { terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The console work a provider needs done, and the values it needs handed over.
|
|
10
|
+
*
|
|
11
|
+
* The only part of `connect` that talks to the operator about a vendor's own
|
|
12
|
+
* setup — which is why the prompts live here and nowhere else.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Print a provider's setup block, asking for nothing.
|
|
17
|
+
*
|
|
18
|
+
* Separate from `askForSetup` because the instructions and the prompt have
|
|
19
|
+
* different audiences. The prompt is needed once per profile — the OAuth client
|
|
20
|
+
* is shared, so a second Google provider has nothing to be asked for. The
|
|
21
|
+
* *instructions* are per product: each one names APIs to enable and scopes to
|
|
22
|
+
* add under DATA ACCESS, and those are console work nobody has done yet.
|
|
23
|
+
*
|
|
24
|
+
* Bundled together, the second provider silently got neither. Connecting
|
|
25
|
+
* `sheets` on a profile that already had Gmail skipped straight to the browser,
|
|
26
|
+
* and the two ways that fails are the two this repository keeps warning about —
|
|
27
|
+
* an unregistered scope is refused at consent, and a disabled API consents
|
|
28
|
+
* cleanly and then 403s on every call.
|
|
29
|
+
*/
|
|
30
|
+
export function printSetup(manifest: ProviderManifest, note: string): void {
|
|
31
|
+
const setup = manifest.setup;
|
|
32
|
+
if (!setup) return;
|
|
33
|
+
|
|
34
|
+
progress();
|
|
35
|
+
progress(style.bold(`Setting up ${manifest.name}`));
|
|
36
|
+
if (setup.summary) progress(setup.summary);
|
|
37
|
+
if (setup.docs_url) progress(style.dim(setup.docs_url));
|
|
38
|
+
progress();
|
|
39
|
+
setup.steps.forEach((step, index) => progress(` ${index + 1}. ${step}`));
|
|
40
|
+
progress();
|
|
41
|
+
progress(style.dim(note));
|
|
42
|
+
progress();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render a provider's setup block, then ask for what it declares.
|
|
47
|
+
*
|
|
48
|
+
* Shared by the OAuth-client path and the static-credential path, because they
|
|
49
|
+
* are the same conversation: show the operator where to generate something, then
|
|
50
|
+
* take it and put it in the store. The only difference is where it lands, which
|
|
51
|
+
* the caller decides.
|
|
52
|
+
*/
|
|
53
|
+
export async function askForSetup(
|
|
54
|
+
manifest: ProviderManifest,
|
|
55
|
+
prompts: readonly SetupPrompt[],
|
|
56
|
+
note: string,
|
|
57
|
+
prompter: Prompter = terminalPrompter,
|
|
58
|
+
): Promise<Map<string, string>> {
|
|
59
|
+
const setup = manifest.setup;
|
|
60
|
+
if (!setup) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Provider "${manifest.id}" needs a credential but declares no setup, so there is no way to learn what to ask you for. Add a setup block to its manifest.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
printSetup(manifest, note);
|
|
67
|
+
|
|
68
|
+
const answers = new Map<string, string>();
|
|
69
|
+
for (const prompt of prompts) {
|
|
70
|
+
const value = prompt.secret
|
|
71
|
+
? await prompter.askSecret(` ${prompt.label}`)
|
|
72
|
+
: await prompter.ask(` ${prompt.label}`);
|
|
73
|
+
if (!value) throw new Error(`${prompt.label} is required.`);
|
|
74
|
+
answers.set(prompt.key, value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return answers;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether the credential already in the store may stand, or has to be asked for
|
|
82
|
+
* again.
|
|
83
|
+
*
|
|
84
|
+
* Extracted because the two reasons to ask again are easy to state and were
|
|
85
|
+
* impossible to test: `ensureStaticCredential` reaches a terminal, and the
|
|
86
|
+
* property worth holding — that a re-run of `connect` can always correct a
|
|
87
|
+
* credential — is a property of this decision rather than of the prompting.
|
|
88
|
+
*
|
|
89
|
+
* `provisional` is the one that made `connect` unrecoverable. A credential
|
|
90
|
+
* under the provisional connection id exists only because an earlier `connect`
|
|
91
|
+
* stored one and then failed before settling whose account it was, so no server
|
|
92
|
+
* has ever accepted it — and treating it as established meant a mistyped
|
|
93
|
+
* app-specific password could not be corrected by any spelling of the command
|
|
94
|
+
* that stored it.
|
|
95
|
+
*/
|
|
96
|
+
export function reuseStoredCredential(input: {
|
|
97
|
+
readonly stored: boolean;
|
|
98
|
+
readonly replace: boolean;
|
|
99
|
+
readonly provisional: boolean;
|
|
100
|
+
}): boolean {
|
|
101
|
+
return input.stored && !input.replace && !input.provisional;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ask for a static credential — an API key, or an app-specific password.
|
|
106
|
+
*
|
|
107
|
+
* This did not exist, which meant the custom-provider path `docs/detailed/creating-a-provider.md`
|
|
108
|
+
* documents did not work: a manifest declaring `auth: {kind: header}` connected
|
|
109
|
+
* with no complaint, never asked for the key, and then reported itself
|
|
110
|
+
* unauthorized forever — with `doctor` advising the very command that had just
|
|
111
|
+
* declined to help.
|
|
112
|
+
*
|
|
113
|
+
* Idempotent where that is load-bearing rather than tidy: iCloud is three
|
|
114
|
+
* providers sharing one app-specific password, so the second and third
|
|
115
|
+
* `connect` must find it already there and say nothing. `reuseStoredCredential`
|
|
116
|
+
* holds the exceptions.
|
|
117
|
+
*
|
|
118
|
+
* Asking is not discarding. The stored value is read but never deleted here,
|
|
119
|
+
* and the write below happens only once every prompt has been answered — so
|
|
120
|
+
* Ctrl-C, or an empty answer, leaves a working credential exactly as it was.
|
|
121
|
+
* A command that threw away a secret the operator typed and then failed before
|
|
122
|
+
* replacing it would be worse than the trap it was fixing.
|
|
123
|
+
*/
|
|
124
|
+
export async function ensureStaticCredential(input: {
|
|
125
|
+
manifest: ProviderManifest;
|
|
126
|
+
connectionId: string;
|
|
127
|
+
credentials: SecretStore;
|
|
128
|
+
/** The operator asked for this one again — `--replace`, or naming the connection. */
|
|
129
|
+
replace: boolean;
|
|
130
|
+
/** The connection id is still the placeholder, so nothing has accepted this credential. */
|
|
131
|
+
provisional: boolean;
|
|
132
|
+
prompter?: Prompter;
|
|
133
|
+
}): Promise<void> {
|
|
134
|
+
const { manifest, connectionId, credentials, replace, provisional } = input;
|
|
135
|
+
const prompter = input.prompter ?? terminalPrompter;
|
|
136
|
+
const auth = manifest.auth;
|
|
137
|
+
if (auth.kind === 'none' || auth.kind === 'oauth' || auth.kind === 'strategy') return;
|
|
138
|
+
|
|
139
|
+
const ref = credentialRefForConnection(manifest, connectionId)!;
|
|
140
|
+
const stored = await credentials.has(ref);
|
|
141
|
+
|
|
142
|
+
if (reuseStoredCredential({ stored, replace, provisional })) {
|
|
143
|
+
progress(ok(`credential already stored (${ref})`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Asked for where "already stored" was expected, so say which of the two
|
|
148
|
+
// reasons it is. Only when something is actually there: on a first connect
|
|
149
|
+
// this line would describe a state that does not exist.
|
|
150
|
+
if (stored) {
|
|
151
|
+
progress(
|
|
152
|
+
style.dim(
|
|
153
|
+
provisional && !replace
|
|
154
|
+
? `An earlier connect left a credential at ${ref} without finishing, so it was never accepted. ` +
|
|
155
|
+
'Asking again — what is stored is replaced only once you have entered a new one.'
|
|
156
|
+
: `Replacing ${ref} — what is stored is overwritten only once you have entered a new one.`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const prompts = (manifest.setup?.prompts ?? []).filter((p) => p.scope === 'connection');
|
|
162
|
+
if (prompts.length === 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Provider "${manifest.id}" authenticates with a stored credential but declares no per-account setup prompts, ` +
|
|
165
|
+
`so there is nothing to ask you for. Either add prompts with "scope: connection", or place the value at ${ref} yourself.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const answers = await askForSetup(
|
|
170
|
+
manifest,
|
|
171
|
+
prompts,
|
|
172
|
+
`Stored at ${ref}, in the credential store — never in config.`,
|
|
173
|
+
prompter,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (auth.kind === 'basic') {
|
|
177
|
+
// RFC 7617's own encoding. Stored as the header carries it, so nothing
|
|
178
|
+
// downstream has to agree about a JSON shape.
|
|
179
|
+
const username = answers.get(prompts.find((p) => p.field === 'username')!.key)!;
|
|
180
|
+
const password = answers.get(prompts.find((p) => p.field === 'password')!.key)!;
|
|
181
|
+
await credentials.set(ref, `${username}:${password}`);
|
|
182
|
+
} else {
|
|
183
|
+
await credentials.set(ref, answers.get(prompts[0]!.key)!);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
progress(ok('credential stored'));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Prompt once per profile for a vendor-supplied client, and record only refs. */
|
|
190
|
+
export async function ensureOAuthApp(input: {
|
|
191
|
+
manifest: ProviderManifest;
|
|
192
|
+
credentials: SecretStore;
|
|
193
|
+
document: ConfigDocument;
|
|
194
|
+
changes: string[];
|
|
195
|
+
firstForProvider: boolean;
|
|
196
|
+
prompter?: Prompter;
|
|
197
|
+
}): Promise<void> {
|
|
198
|
+
const { manifest, credentials, document, changes } = input;
|
|
199
|
+
const prompter = input.prompter ?? terminalPrompter;
|
|
200
|
+
if (manifest.auth.kind !== 'oauth' || !manifest.auth.app) return;
|
|
201
|
+
|
|
202
|
+
const app = manifest.auth.app;
|
|
203
|
+
const [existingId, existingSecret] = await Promise.all([
|
|
204
|
+
credentials.get(`${app}/client_id`),
|
|
205
|
+
credentials.get(`${app}/client_secret`),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
if (existingId && existingSecret) {
|
|
209
|
+
// The client is stored, so there is nothing to ask for — but if this is the
|
|
210
|
+
// profile's first connection *of this provider*, its console setup has not
|
|
211
|
+
// been done. Show it, once. Gated on the provider being new rather than on
|
|
212
|
+
// the client being present, because refresh tokens on an unpublished Google
|
|
213
|
+
// app expire weekly and reprinting this on every re-authorisation would
|
|
214
|
+
// train someone to scroll past it.
|
|
215
|
+
if (input.firstForProvider) {
|
|
216
|
+
printSetup(
|
|
217
|
+
manifest,
|
|
218
|
+
`The ${app} OAuth client is already set up, so nothing is asked for here — but the ` +
|
|
219
|
+
'APIs and scopes above are per product, and this is the first connection of this one.',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
declareOwnClient(document, manifest, changes);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const setup = manifest.setup;
|
|
227
|
+
if (!setup) throw new Error(`Provider "${manifest.id}" needs a client but declares no setup.`);
|
|
228
|
+
|
|
229
|
+
const shared = setup.prompts.filter((p) => p.scope === 'shared');
|
|
230
|
+
const answers = await askForSetup(
|
|
231
|
+
manifest,
|
|
232
|
+
shared,
|
|
233
|
+
'Asked once per profile. Values go to the credential store, never to config.',
|
|
234
|
+
prompter,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
for (const prompt of shared) {
|
|
238
|
+
await credentials.set(prompt.credential_ref!, answers.get(prompt.key)!);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
declareOwnClient(document, manifest, changes);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Record that this profile uses a client of its own.
|
|
246
|
+
*
|
|
247
|
+
* The entry is not merely a pair of pointers — it is the switch. A provider
|
|
248
|
+
* that can also authorise against a broker reads its presence as "this profile
|
|
249
|
+
* registered a client, use it", so writing it is what makes `--own-client`
|
|
250
|
+
* stick without a second flag to remember. Which is also why it is called on
|
|
251
|
+
* both of `ensureOAuthApp`'s exits: someone whose credentials were already in
|
|
252
|
+
* the store but whose config lost the block would otherwise be moved onto the
|
|
253
|
+
* hosted client at the next connect.
|
|
254
|
+
*/
|
|
255
|
+
export function declareOwnClient(
|
|
256
|
+
document: ConfigDocument,
|
|
257
|
+
manifest: ProviderManifest,
|
|
258
|
+
changes: string[],
|
|
259
|
+
): void {
|
|
260
|
+
if (manifest.auth.kind !== 'oauth' || !manifest.auth.app) return;
|
|
261
|
+
const app = manifest.auth.app;
|
|
262
|
+
|
|
263
|
+
const shared = (manifest.setup?.prompts ?? []).filter((p) => p.scope === 'shared');
|
|
264
|
+
const idPrompt = shared.find((p) => p.key === 'client_id');
|
|
265
|
+
const secretPrompt = shared.find((p) => p.key === 'client_secret');
|
|
266
|
+
|
|
267
|
+
if (idPrompt && secretPrompt && document.getIn(['oauth_apps', app]) === undefined) {
|
|
268
|
+
document.setIn(['oauth_apps', app], {
|
|
269
|
+
client_id_ref: idPrompt.credential_ref,
|
|
270
|
+
client_secret_ref: secretPrompt.credential_ref,
|
|
271
|
+
});
|
|
272
|
+
// No back-pointer from the provider: the manifest already says which app it
|
|
273
|
+
// uses, and a second copy in config could only ever disagree with it.
|
|
274
|
+
changes.push(`oauth_apps.${app} declared`);
|
|
275
|
+
}
|
|
276
|
+
}
|