@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,398 @@
|
|
|
1
|
+
import { createMcpConnector } from '#connectivity/transports';
|
|
2
|
+
import type { DiscoveredCapability } from '#connectivity';
|
|
3
|
+
import { credentialRefForConnection, WRITE_BUNDLE } from '#connectivity';
|
|
4
|
+
import { ConfigDocument, ensureSetupConnection, repaired } from '../../config-edit.ts';
|
|
5
|
+
import { emit, print, progress, style } from '../../output.ts';
|
|
6
|
+
import { nonInteractivePrompter, terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
7
|
+
import { openRuntime, type GlobalFlags } from '../../runtime.ts';
|
|
8
|
+
import { credentialApp, matchesRule, moveCredential, siblingAccountId } from './accounts.ts';
|
|
9
|
+
import { authorise, oauthProviderFor } from './authorise.ts';
|
|
10
|
+
import { preflight } from './requirements.ts';
|
|
11
|
+
import { ALREADY, NOTHING, renderOutcome, type ConnectOutcome } from './outcome.ts';
|
|
12
|
+
import { nextAfterEdit, publishRuntimeEdit } from '#cli/publish.ts';
|
|
13
|
+
import { ensureStaticCredential } from './setup.ts';
|
|
14
|
+
import { settleIdentity } from './settle.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `lanes link connect <provider>` — the one command that adds an account.
|
|
18
|
+
*
|
|
19
|
+
* The same command regardless of connectivity. A local provider declares a
|
|
20
|
+
* connection and stops; an MCP provider authorises, asks the upstream server
|
|
21
|
+
* what it exposes, and grants it. Core learns nothing about any vendor: the
|
|
22
|
+
* manifest says how to reach them and what to ask the operator for.
|
|
23
|
+
*
|
|
24
|
+
* The five numbered steps below are the whole command, and each one that grew
|
|
25
|
+
* past a paragraph moved out: `authorise.ts` gets the token, `setup.ts` asks the
|
|
26
|
+
* operator for what the vendor's console produced, `settle.ts` works out whose
|
|
27
|
+
* account it was, and `accounts.ts` knows which connections are siblings.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface ConnectOptions extends GlobalFlags {
|
|
31
|
+
readonly id?: string | undefined;
|
|
32
|
+
readonly displayName?: string | undefined;
|
|
33
|
+
/** Ask for the stored credential again — a key rotated, or a password revoked. */
|
|
34
|
+
readonly replace?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Answer nothing from a terminal: resolve every declared value from the
|
|
37
|
+
* credential store, and refuse with instructions where one is missing.
|
|
38
|
+
*/
|
|
39
|
+
readonly nonInteractive?: boolean | undefined;
|
|
40
|
+
/** The operator has already agreed to scopes broader than the provider needs. */
|
|
41
|
+
readonly acceptBroadScopes?: boolean | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Register an OAuth client of your own rather than using a hosted one.
|
|
44
|
+
*
|
|
45
|
+
* Sticky by consequence rather than by flag: it writes the `oauth_apps` entry,
|
|
46
|
+
* and a profile that declares one is never moved off it. So this is typed once
|
|
47
|
+
* and then forgotten, which is the right shape for a decision about a client
|
|
48
|
+
* that is shared by every connection of that vendor.
|
|
49
|
+
*/
|
|
50
|
+
readonly ownClient?: boolean | undefined;
|
|
51
|
+
/** Injected for tests. The broker is the only thing `connect` fetches. */
|
|
52
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
53
|
+
readonly json?: boolean | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The connection id used before the provider has said whose account this is.
|
|
59
|
+
*
|
|
60
|
+
* It is a placeholder rather than a name, and `setup.ts` reads it as one: a
|
|
61
|
+
* credential filed under it belongs to a `connect` that did not finish.
|
|
62
|
+
*/
|
|
63
|
+
const PROVISIONAL_ID = 'pending';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What discovery is actually doing, per kind, so the wait is explained.
|
|
67
|
+
*
|
|
68
|
+
* They differ enough to be worth saying: reading a local OpenAPI file is
|
|
69
|
+
* instant, while signing in to an IMAP server is a TLS handshake and a LOGIN
|
|
70
|
+
* against a host that sometimes takes its time.
|
|
71
|
+
*/
|
|
72
|
+
const DISCOVERY_NOTE: Record<string, string> = {
|
|
73
|
+
mcp: 'Discovering capabilities…',
|
|
74
|
+
http: 'Reading the API description…',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export async function connect(target: string, options: ConnectOptions): Promise<void> {
|
|
78
|
+
const outcome = await runConnect(target, options);
|
|
79
|
+
|
|
80
|
+
if (!outcome.ok) process.exitCode = 1;
|
|
81
|
+
|
|
82
|
+
return emit(options.json, outcome, () => renderOutcome(outcome));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runConnect(target: string, options: ConnectOptions): Promise<ConnectOutcome> {
|
|
86
|
+
const separator = target.indexOf('.');
|
|
87
|
+
const providerId = separator === -1 ? target : target.slice(0, separator);
|
|
88
|
+
const namedId = separator === -1 ? undefined : target.slice(separator + 1);
|
|
89
|
+
|
|
90
|
+
// The runtime is opened first because its registry includes workspace
|
|
91
|
+
// manifests — a custom provider must be as connectable as a built-in.
|
|
92
|
+
const runtime = await openRuntime(options);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const registry = runtime.registry;
|
|
96
|
+
const entry = registry.get(providerId);
|
|
97
|
+
|
|
98
|
+
// `lanes link connect icloud` — an account rather than a provider.
|
|
99
|
+
//
|
|
100
|
+
// Everyone models iCloud this way: Apple's own Settings, macOS Internet
|
|
101
|
+
// Accounts, Thunderbird, DAVx⁵. One authorisation, three services. It is
|
|
102
|
+
// three *providers* underneath because mail and calendars are different
|
|
103
|
+
// protocols, and because a policy line per provider is what lets someone
|
|
104
|
+
// allow `icloud_calendar.*` while never granting mail — but nobody should
|
|
105
|
+
// have to know that to connect their account.
|
|
106
|
+
if (!entry) {
|
|
107
|
+
const family = registry
|
|
108
|
+
.list()
|
|
109
|
+
.filter((candidate) => credentialApp(candidate.manifest) === providerId)
|
|
110
|
+
.map((candidate) => candidate.manifest.id);
|
|
111
|
+
|
|
112
|
+
if (family.length > 1) {
|
|
113
|
+
await runtime.close();
|
|
114
|
+
progress(
|
|
115
|
+
style.dim(`${providerId} is ${family.length} services on one account: ${family.join(', ')}`),
|
|
116
|
+
);
|
|
117
|
+
// In sequence, and the order matters: the first settles the account id
|
|
118
|
+
// and stores the credential, and the rest find both already there.
|
|
119
|
+
//
|
|
120
|
+
// The id travels as a flag because the family members are addressed by
|
|
121
|
+
// their own names: `connect icloud.will` parses `will` off a target
|
|
122
|
+
// that is then thrown away, and recursing with `options` alone dropped
|
|
123
|
+
// it — the command named an account and each member silently invented
|
|
124
|
+
// its own.
|
|
125
|
+
const inherited = { ...options, id: options.id ?? namedId };
|
|
126
|
+
const members: ConnectOutcome[] = [];
|
|
127
|
+
for (const member of family) members.push(await runConnect(member, inherited));
|
|
128
|
+
|
|
129
|
+
// The whole account succeeded only if every service did. A partial
|
|
130
|
+
// result is the case worth surfacing: one member blocked on a value
|
|
131
|
+
// leaves an account half connected, which `status` shows and prose does
|
|
132
|
+
// not.
|
|
133
|
+
return {
|
|
134
|
+
...NOTHING,
|
|
135
|
+
ok: members.every((outcome) => outcome.ok),
|
|
136
|
+
members,
|
|
137
|
+
...(members.find((outcome) => !outcome.ok)?.reason
|
|
138
|
+
? { reason: members.find((outcome) => !outcome.ok)!.reason }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!entry) {
|
|
145
|
+
const available = registry.list();
|
|
146
|
+
const builtin = available.filter((c) => c.origin === 'builtin').map((c) => c.manifest.id);
|
|
147
|
+
const custom = available.filter((c) => c.origin === 'workspace').map((c) => c.manifest.id);
|
|
148
|
+
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Unknown provider "${providerId}".\n` +
|
|
151
|
+
` built in: ${builtin.join(', ')}\n` +
|
|
152
|
+
(custom.length > 0
|
|
153
|
+
? ` yours: ${custom.join(', ')}\n`
|
|
154
|
+
: ` add your own: a manifest in ${runtime.resolution.workspaceRoot}/providers/\n`),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const manifest = entry.manifest;
|
|
159
|
+
const document = await ConfigDocument.open(runtime.resolution.workspaceRoot, runtime.resolution.profile);
|
|
160
|
+
const changes: string[] = [];
|
|
161
|
+
|
|
162
|
+
// 1. Authorise, if this provider needs it.
|
|
163
|
+
//
|
|
164
|
+
// Under a provisional id, because the credential has to land somewhere
|
|
165
|
+
// before we can ask the provider whose account it is. The real id is
|
|
166
|
+
// settled below and the credential moves with it.
|
|
167
|
+
//
|
|
168
|
+
// Unless a sibling already knows: when several providers of one vendor
|
|
169
|
+
// share a per-account credential, the second one should adopt the first's
|
|
170
|
+
// id rather than invent `pending` and ask for a password already held.
|
|
171
|
+
const adopted = siblingAccountId(manifest, runtime.config, registry);
|
|
172
|
+
const named = options.id ?? namedId;
|
|
173
|
+
const provisionalId = named ?? adopted ?? PROVISIONAL_ID;
|
|
174
|
+
|
|
175
|
+
const profile = runtime.resolution.profile;
|
|
176
|
+
|
|
177
|
+
// 0. With nobody to ask, resolve everything up front or refuse saying why.
|
|
178
|
+
//
|
|
179
|
+
// Before any write, so a refusal leaves the profile exactly as it was.
|
|
180
|
+
// The whole list comes back at once: discovering a missing value a
|
|
181
|
+
// prompt at a time would cost an agent one round trip per secret.
|
|
182
|
+
if (options.nonInteractive === true) {
|
|
183
|
+
const blocked = await preflight({
|
|
184
|
+
manifest,
|
|
185
|
+
connectionId: named ?? adopted,
|
|
186
|
+
profile,
|
|
187
|
+
credentials: runtime.credentials,
|
|
188
|
+
target,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (blocked) return { ...NOTHING, ok: false, ...blocked };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const prompter: Prompter =
|
|
195
|
+
options.nonInteractive === true
|
|
196
|
+
? nonInteractivePrompter(`lanes link setup plan ${providerId} --profile ${profile}`)
|
|
197
|
+
: terminalPrompter;
|
|
198
|
+
|
|
199
|
+
if (manifest.auth.kind === 'oauth') {
|
|
200
|
+
await authorise({
|
|
201
|
+
manifest,
|
|
202
|
+
connectionId: provisionalId,
|
|
203
|
+
credentials: runtime.credentials,
|
|
204
|
+
document,
|
|
205
|
+
changes,
|
|
206
|
+
firstForProvider: !runtime.config.connections.some((c) => c.provider === providerId),
|
|
207
|
+
target,
|
|
208
|
+
profile,
|
|
209
|
+
ownClient: options.ownClient === true,
|
|
210
|
+
prompter,
|
|
211
|
+
acceptBroadScopes: options.acceptBroadScopes === true,
|
|
212
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
213
|
+
});
|
|
214
|
+
} else if (manifest.auth.kind !== 'none') {
|
|
215
|
+
await ensureStaticCredential({
|
|
216
|
+
manifest,
|
|
217
|
+
connectionId: provisionalId,
|
|
218
|
+
credentials: runtime.credentials,
|
|
219
|
+
// Naming a connection is how someone says "this one, again" — which is
|
|
220
|
+
// what a rotated key or a revoked app-specific password calls for.
|
|
221
|
+
// `--replace` says the same thing about a family, where there is no
|
|
222
|
+
// single provider to name and the ids are not the operator's to know.
|
|
223
|
+
//
|
|
224
|
+
// Never non-interactively: there, "again" already happened. The new
|
|
225
|
+
// value was written with `secrets set` before this ran, so asking would
|
|
226
|
+
// be asking for something the store already holds.
|
|
227
|
+
replace: options.nonInteractive !== true && (options.replace === true || named !== undefined),
|
|
228
|
+
provisional: provisionalId === PROVISIONAL_ID,
|
|
229
|
+
prompter,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 2. Ask the provider whose account that was.
|
|
234
|
+
//
|
|
235
|
+
// This is what distinguishes reconnecting an existing account from
|
|
236
|
+
// adding a new one. Without it, a retried connect appends a second row
|
|
237
|
+
// rather than repairing the first — which is how `main2` and `main3`
|
|
238
|
+
// ended up in a config describing two mailboxes.
|
|
239
|
+
const { connectionId, account } = await settleIdentity({
|
|
240
|
+
manifest,
|
|
241
|
+
provisionalId,
|
|
242
|
+
explicitId: named,
|
|
243
|
+
account: options.displayName,
|
|
244
|
+
runtime,
|
|
245
|
+
prompter,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const connectionKey = `${providerId}.${connectionId}`;
|
|
249
|
+
|
|
250
|
+
if (connectionId !== provisionalId) {
|
|
251
|
+
// Wherever the manifest puts it — which is not always `<provider>/<id>`,
|
|
252
|
+
// and is a no-op when the manifest names one credential shared by every
|
|
253
|
+
// account.
|
|
254
|
+
const from = credentialRefForConnection(manifest, provisionalId);
|
|
255
|
+
const to = credentialRefForConnection(manifest, connectionId);
|
|
256
|
+
if (from && to && from !== to) await moveCredential(runtime.credentials, from, to);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 3. Ask the upstream what it exposes. A manifest never declares
|
|
260
|
+
// capabilities for a proxied server — the server is the source of truth,
|
|
261
|
+
// and a declared list would go stale the moment the vendor ships.
|
|
262
|
+
let discovered: DiscoveredCapability[] = [];
|
|
263
|
+
|
|
264
|
+
if (manifest.connector.kind !== 'local') {
|
|
265
|
+
progress(style.dim(DISCOVERY_NOTE[manifest.connector.kind] ?? 'Discovering capabilities…'));
|
|
266
|
+
|
|
267
|
+
// MCP is the one kind that does not use the runtime's connector here: it
|
|
268
|
+
// wants the token exactly as just written, without the refresh machinery
|
|
269
|
+
// that `resolveUpstreamToken` wraps around it. Every other kind carries
|
|
270
|
+
// whatever credential it needs from the factory.
|
|
271
|
+
const connector =
|
|
272
|
+
manifest.connector.kind === 'mcp'
|
|
273
|
+
? createMcpConnector({
|
|
274
|
+
endpoint: manifest.connector.endpoint,
|
|
275
|
+
accessToken: async () => {
|
|
276
|
+
const provider = oauthProviderFor(manifest, connectionId, runtime.credentials);
|
|
277
|
+
const tokens = (await provider.tokens()) as { access_token?: string } | undefined;
|
|
278
|
+
return tokens?.access_token ?? null;
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
: runtime.connectorFor(providerId, connectionId);
|
|
282
|
+
|
|
283
|
+
if (connector) {
|
|
284
|
+
// Discovery takes the manifest and nothing else. What a provider exposes
|
|
285
|
+
// is a property of the provider, not of an account — which is just as
|
|
286
|
+
// well, because the connection being created does not exist in config
|
|
287
|
+
// until step 4 below.
|
|
288
|
+
discovered = await connector.discover({ manifest });
|
|
289
|
+
|
|
290
|
+
await runtime.state.kv.set('discovery', providerId, JSON.stringify(discovered));
|
|
291
|
+
registry.setDiscovered(providerId, discovered);
|
|
292
|
+
}
|
|
293
|
+
} else if (entry.definition) {
|
|
294
|
+
// Local capabilities carry their bundle from the manifest. Resources are
|
|
295
|
+
// included alongside tools: they need a policy grant too, and leaving
|
|
296
|
+
// them out would register a resource nothing is allowed to read.
|
|
297
|
+
const bundleOf = (name: string): string | undefined =>
|
|
298
|
+
manifest.bundles?.find((candidate) => candidate.capabilities.includes(name))?.name;
|
|
299
|
+
|
|
300
|
+
discovered = entry.definition.capabilities.map((capability) => ({
|
|
301
|
+
name: capability.name,
|
|
302
|
+
description: capability.description,
|
|
303
|
+
inputSchema: {},
|
|
304
|
+
...(bundleOf(capability.name) ? { bundle: bundleOf(capability.name)! } : {}),
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 4. Declare the connection, or update the one this account already has.
|
|
309
|
+
const existingIndex = runtime.config.connections.findIndex(
|
|
310
|
+
(c) => `${c.provider}.${c.id}` === connectionKey,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (existingIndex === -1) {
|
|
314
|
+
// No `credential_ref`: it derives to `<provider>/<id>`, which is exactly
|
|
315
|
+
// where the OAuth provider already looks. Writing it would add a line per
|
|
316
|
+
// connection that can only ever agree or be a bug.
|
|
317
|
+
document.addTo(['connections'], { id: connectionId, provider: providerId, account });
|
|
318
|
+
changes.push(`connections += ${connectionKey} (${account})`);
|
|
319
|
+
} else {
|
|
320
|
+
// A reconnect. The credential was just replaced above; the declaration
|
|
321
|
+
// stays as it is, so re-running connect after an expiry is a no-op on the
|
|
322
|
+
// file rather than a second row.
|
|
323
|
+
if (runtime.config.connections[existingIndex]?.account !== account) {
|
|
324
|
+
document.setIn(['connections', existingIndex, 'account'], account);
|
|
325
|
+
changes.push(`connections.${connectionKey}.account = ${account}`);
|
|
326
|
+
}
|
|
327
|
+
changes.push(`re-authorised ${connectionKey}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 5. Grant it.
|
|
331
|
+
//
|
|
332
|
+
// One rule per provider, not one per capability. The pinned-per-tool
|
|
333
|
+
// form this replaced was 85 lines for four providers and unreadable, and
|
|
334
|
+
// what it bought — a vendor cannot widen your policy by shipping a new
|
|
335
|
+
// tool — is preserved instead by `doctor`, which reports capabilities
|
|
336
|
+
// that appeared after you connected.
|
|
337
|
+
const granted: string[] = [];
|
|
338
|
+
const rule = `${providerId}.*`;
|
|
339
|
+
|
|
340
|
+
if (!runtime.config.policy.allow.some((existing) => matchesRule(existing.capability, rule))) {
|
|
341
|
+
document.addTo(['policy', 'allow'], rule, { inline: true });
|
|
342
|
+
granted.push(rule);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 6. Repair the setup surface if this profile predates it.
|
|
346
|
+
//
|
|
347
|
+
// Connecting is the moment it matters: the operator is adding something
|
|
348
|
+
// an agent will be asked about, and a profile with no `setup` row serves
|
|
349
|
+
// no `setup_overview` — so the agent has nothing to read and invents a
|
|
350
|
+
// command instead. `doctor` reports this, but a deployed operator never
|
|
351
|
+
// runs it, which is how it stayed broken.
|
|
352
|
+
// Each half goes to the field that is for it. `emit` serialises both
|
|
353
|
+
// verbatim under `--json`: `changes` is a list of config edits, so a
|
|
354
|
+
// sentence in it is something a caller counting edits has to recognise
|
|
355
|
+
// and skip, and `granted` is the field that answers "what did this widen"
|
|
356
|
+
// — an audit reading it would have missed `setup.*` entirely.
|
|
357
|
+
const repair = ensureSetupConnection(document);
|
|
358
|
+
changes.push(...repair.changes);
|
|
359
|
+
granted.push(...repair.granted);
|
|
360
|
+
|
|
361
|
+
// The explanation is prose, so it goes where prose goes. Without it the
|
|
362
|
+
// operator has two lines naming a provider they never asked for.
|
|
363
|
+
const notes = repaired(repair)
|
|
364
|
+
? ['that is the setup surface — it lets an agent see what is connected here']
|
|
365
|
+
: [];
|
|
366
|
+
|
|
367
|
+
if (changes.length === 0 && granted.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
...NOTHING,
|
|
370
|
+
ok: true,
|
|
371
|
+
key: connectionKey,
|
|
372
|
+
account,
|
|
373
|
+
discovered: discovered.length,
|
|
374
|
+
next: ALREADY,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await document.save();
|
|
379
|
+
|
|
380
|
+
// Where the endpoint that has to serve this reads its config, and then a
|
|
381
|
+
// nudge to re-read it. Neither is a deploy (ADR-029).
|
|
382
|
+
const publish = await publishRuntimeEdit(runtime);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
key: connectionKey,
|
|
387
|
+
account,
|
|
388
|
+
changes,
|
|
389
|
+
granted,
|
|
390
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
391
|
+
discovered: discovered.length,
|
|
392
|
+
writable: discovered.filter((c) => c.bundle === WRITE_BUNDLE).length,
|
|
393
|
+
next: nextAfterEdit(publish),
|
|
394
|
+
};
|
|
395
|
+
} finally {
|
|
396
|
+
await runtime.close();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { SetupRequirement } from '#connectivity';
|
|
2
|
+
import { fail, ok, print, progress, style } from '../../output.ts';
|
|
3
|
+
import type { Blocked } from './requirements.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What `connect` did, and how it is drawn.
|
|
7
|
+
*
|
|
8
|
+
* Split from the five steps that produce it because they change for different
|
|
9
|
+
* reasons: the orchestration is about vendors and credentials, this is about
|
|
10
|
+
* what a caller is told. It is also the only way the rendering gets tested —
|
|
11
|
+
* `connect` needs a runtime, a config file, and a credential store to reach the
|
|
12
|
+
* last line, and none of that is needed to check that a refusal names the
|
|
13
|
+
* command that fixes it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ConnectOutcome {
|
|
17
|
+
readonly ok: boolean;
|
|
18
|
+
readonly key?: string;
|
|
19
|
+
readonly account?: string;
|
|
20
|
+
readonly changes: readonly string[];
|
|
21
|
+
readonly granted: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Prose about what was done, for a reader rather than for a filter.
|
|
24
|
+
*
|
|
25
|
+
* `changes` and `granted` are lists a `--json` caller counts and matches on,
|
|
26
|
+
* so a sentence in either is something it has to recognise and skip. A change
|
|
27
|
+
* the operator did not ask for still needs explaining — the setup repair adds
|
|
28
|
+
* a provider they never named — and this is where that explanation goes.
|
|
29
|
+
*/
|
|
30
|
+
readonly notes?: readonly string[];
|
|
31
|
+
readonly discovered: number;
|
|
32
|
+
/** How many of those write, so a grant can be narrowed knowingly. */
|
|
33
|
+
readonly writable?: number;
|
|
34
|
+
readonly reason?: Blocked['reason'];
|
|
35
|
+
readonly message?: string;
|
|
36
|
+
readonly needs?: readonly SetupRequirement[];
|
|
37
|
+
readonly then?: string;
|
|
38
|
+
/** One per service, when the target named an account several providers share. */
|
|
39
|
+
readonly members?: readonly ConnectOutcome[];
|
|
40
|
+
readonly next?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A result that changed nothing, for the paths that stop early. */
|
|
44
|
+
export const NOTHING = { changes: [], granted: [], discovered: 0 } as const;
|
|
45
|
+
|
|
46
|
+
export const ALREADY = 'Already connected — nothing changed.';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What `connect` says last.
|
|
50
|
+
*
|
|
51
|
+
* It used to be a guess: "restart the endpoint", or "run lanes link deploy" for
|
|
52
|
+
* a deployable target, because a running endpoint read its config once at
|
|
53
|
+
* startup and there was no way to reach it in between. Saying the wrong one was
|
|
54
|
+
* worse than saying nothing — told to restart `lanes link start` after
|
|
55
|
+
* authorising an account against a deployment, an operator restarts a server
|
|
56
|
+
* that was not serving it and watches the deployed one go on refusing.
|
|
57
|
+
*
|
|
58
|
+
* The guess is gone. `connect` publishes the config where the target reads it
|
|
59
|
+
* and asks the endpoint to re-read it, so the line reports what happened rather
|
|
60
|
+
* than predicting it — see `nextAfterEdit` in `#cli/publish.ts` and ADR-029.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export function renderOutcome(outcome: ConnectOutcome): void {
|
|
64
|
+
// Each member already rendered itself as it ran; a summary here would print
|
|
65
|
+
// the same facts a second time.
|
|
66
|
+
if (outcome.members) return;
|
|
67
|
+
|
|
68
|
+
if (!outcome.ok) return renderBlocked(outcome);
|
|
69
|
+
|
|
70
|
+
if (outcome.next === ALREADY) {
|
|
71
|
+
print(style.dim(`${outcome.key} is already connected.`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
print();
|
|
76
|
+
print(ok(`connected ${style.bold(outcome.key ?? '')}`));
|
|
77
|
+
for (const change of outcome.changes) print(` ${style.dim(change)}`);
|
|
78
|
+
for (const rule of outcome.granted) print(` ${style.dim(`policy.allow += ${rule}`)}`);
|
|
79
|
+
for (const note of outcome.notes ?? []) print(` ${style.dim(note)}`);
|
|
80
|
+
|
|
81
|
+
if (outcome.discovered > 0) {
|
|
82
|
+
print(` ${style.dim(`${outcome.discovered} capabilities discovered, all reachable`)}`);
|
|
83
|
+
|
|
84
|
+
if ((outcome.writable ?? 0) > 0) {
|
|
85
|
+
const provider = outcome.key?.split('.')[0] ?? '';
|
|
86
|
+
print(
|
|
87
|
+
` ${style.dim(`${outcome.writable} of them write — lanes link policy deny ${provider}.<capability> to withhold one`)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
print();
|
|
93
|
+
print(style.dim(`Next: ${outcome.next}`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A refusal, with every value it is waiting for and the command that ends it.
|
|
98
|
+
*
|
|
99
|
+
* All of it on stdout rather than stderr: this *is* the command's product when
|
|
100
|
+
* it cannot connect, and it is what somebody copies.
|
|
101
|
+
*/
|
|
102
|
+
function renderBlocked(outcome: ConnectOutcome): void {
|
|
103
|
+
progress();
|
|
104
|
+
print(fail(outcome.reason === 'needs_browser' ? 'a browser is needed' : 'more is needed first'));
|
|
105
|
+
|
|
106
|
+
for (const line of (outcome.message ?? '').split('\n')) print(` ${line}`);
|
|
107
|
+
|
|
108
|
+
for (const need of outcome.needs ?? []) {
|
|
109
|
+
print();
|
|
110
|
+
print(` ${style.bold(need.label)}`);
|
|
111
|
+
print(` ${style.dim(`→ ${need.ref}`)}`);
|
|
112
|
+
print(` ${need.command}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (outcome.then) {
|
|
116
|
+
print();
|
|
117
|
+
print(` ${outcome.then}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ProviderManifest } from '#connectivity';
|
|
2
|
+
import { setupRequirements, type SetupRequirement } from '#connectivity';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether what a provider requires is actually there.
|
|
6
|
+
*
|
|
7
|
+
* The requirements themselves are manifest-derived and live in
|
|
8
|
+
* `#connectivity`, so the read-only setup capability can reach them. This
|
|
9
|
+
* half needs the credential store and stays here: the MCP surface reports
|
|
10
|
+
* what setup *requires*, and only the control plane reports what is
|
|
11
|
+
* *satisfied* — ADR-007.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Of those, the ones not already in the store.
|
|
16
|
+
*
|
|
17
|
+
* CLI only. An MCP capability must never call this: reporting whether a
|
|
18
|
+
* credential is present would make the read-only setup surface an oracle on the
|
|
19
|
+
* credential store, which is the line ADR-007 draws. The surface says what is
|
|
20
|
+
* required; only the control plane says what is satisfied.
|
|
21
|
+
*/
|
|
22
|
+
export async function missingRequirements(
|
|
23
|
+
requirements: readonly SetupRequirement[],
|
|
24
|
+
credentials: { has(ref: string): Promise<boolean> },
|
|
25
|
+
): Promise<SetupRequirement[]> {
|
|
26
|
+
const missing: SetupRequirement[] = [];
|
|
27
|
+
|
|
28
|
+
for (const requirement of requirements) {
|
|
29
|
+
if (!(await credentials.has(requirement.ref))) missing.push(requirement);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return missing;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BlockedReason = 'needs_id' | 'needs_browser' | 'missing_credentials';
|
|
36
|
+
|
|
37
|
+
export interface Blocked {
|
|
38
|
+
readonly reason: BlockedReason;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
/** What to store first. Empty for the reasons that are not about a value. */
|
|
41
|
+
readonly needs: readonly SetupRequirement[];
|
|
42
|
+
/** The command to run once the above is done. */
|
|
43
|
+
readonly then: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Why a non-interactive `connect` cannot proceed, or `null` if it can.
|
|
48
|
+
*
|
|
49
|
+
* Every declared value is resolved here, before anything is written, so an
|
|
50
|
+
* agent learns the whole list in one call rather than discovering it a prompt
|
|
51
|
+
* at a time. That is the difference between one round trip and four.
|
|
52
|
+
*
|
|
53
|
+
* The OAuth case is a refusal rather than an attempt. `connect` would open a
|
|
54
|
+
* browser and then block on a loopback listener for five minutes; an agent's
|
|
55
|
+
* shell times out well before that, taking the listener with it, and the
|
|
56
|
+
* operator is left with no token and nothing explaining why. A browser consent
|
|
57
|
+
* belongs to whoever owns the browser, so we hand them the command instead.
|
|
58
|
+
*/
|
|
59
|
+
export async function preflight(input: {
|
|
60
|
+
readonly manifest: ProviderManifest;
|
|
61
|
+
readonly connectionId: string | undefined;
|
|
62
|
+
readonly profile: string;
|
|
63
|
+
readonly credentials: { has(ref: string): Promise<boolean> };
|
|
64
|
+
/** How the operator spelled the target — `icloud`, or `gmail.main`. */
|
|
65
|
+
readonly target: string;
|
|
66
|
+
}): Promise<Blocked | null> {
|
|
67
|
+
const { manifest, connectionId, profile, target } = input;
|
|
68
|
+
const rerun = `lanes link connect ${target} --profile ${profile}`;
|
|
69
|
+
|
|
70
|
+
if (manifest.auth.kind === 'oauth') {
|
|
71
|
+
return {
|
|
72
|
+
reason: 'needs_browser',
|
|
73
|
+
message:
|
|
74
|
+
`${manifest.name} authorises in a browser, which needs the person whose account it is.\n` +
|
|
75
|
+
` Nothing was written. Run this in a terminal on the machine serving this endpoint:`,
|
|
76
|
+
needs: [],
|
|
77
|
+
then: rerun,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { requirements, needsId } = setupRequirements(manifest, connectionId, profile);
|
|
82
|
+
|
|
83
|
+
if (needsId) {
|
|
84
|
+
return {
|
|
85
|
+
reason: 'needs_id',
|
|
86
|
+
message:
|
|
87
|
+
`${manifest.name} stores a credential per account, and the reference derives from the ` +
|
|
88
|
+
`connection id — so it has to be named before anything can be stored under it.`,
|
|
89
|
+
needs: requirements,
|
|
90
|
+
then: `${rerun} --id <name> --non-interactive`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const missing = await missingRequirements(requirements, input.credentials);
|
|
95
|
+
if (missing.length === 0) return null;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
reason: 'missing_credentials',
|
|
99
|
+
message: `${manifest.name} needs ${missing.length} value(s) in the credential store first.`,
|
|
100
|
+
needs: missing,
|
|
101
|
+
then: `${rerun}${connectionId ? ` --id ${connectionId}` : ''} --non-interactive`,
|
|
102
|
+
};
|
|
103
|
+
}
|