@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,118 @@
|
|
|
1
|
+
import type { ProviderManifest } from '#connectivity';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import { BROKERED, BrokerError, brokerRefresh } from './broker.ts';
|
|
4
|
+
import type { CredentialOAuthProvider } from './provider.ts';
|
|
5
|
+
|
|
6
|
+
/** What a stored OAuth credential carries beyond the tokens themselves. */
|
|
7
|
+
interface StoredTokens {
|
|
8
|
+
readonly refresh_token?: string;
|
|
9
|
+
/** An identity assertion, presented to the broker so it knows whose refresh this is. */
|
|
10
|
+
readonly id_token?: string;
|
|
11
|
+
/** Which client minted this. Absent means the profile's own. */
|
|
12
|
+
readonly authorized_via?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function refreshDirectly(
|
|
16
|
+
manifest: ProviderManifest,
|
|
17
|
+
provider: CredentialOAuthProvider,
|
|
18
|
+
tokenUrl: string,
|
|
19
|
+
credentials: SecretStore,
|
|
20
|
+
fetchImpl: typeof globalThis.fetch = globalThis.fetch,
|
|
21
|
+
): Promise<unknown> {
|
|
22
|
+
const existing = (await provider.tokens()) as StoredTokens | undefined;
|
|
23
|
+
const refreshToken = existing?.refresh_token;
|
|
24
|
+
|
|
25
|
+
if (!refreshToken) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`No refresh token stored for ${manifest.id}. Run: lanes link connect ${manifest.id}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const auth = manifest.auth.kind === 'oauth' ? manifest.auth : undefined;
|
|
32
|
+
|
|
33
|
+
// Which client issued this token is a property of the *token*, not of the
|
|
34
|
+
// profile. A refresh token minted by one client is refused by another, so an
|
|
35
|
+
// operator who registers a client of their own after the fact must not drag
|
|
36
|
+
// existing connections onto it — they keep refreshing where they were issued,
|
|
37
|
+
// and only a fresh `connect` moves them.
|
|
38
|
+
const broker = auth?.broker;
|
|
39
|
+
const brokered = broker !== undefined && existing?.authorized_via === BROKERED;
|
|
40
|
+
|
|
41
|
+
const refreshed = brokered
|
|
42
|
+
? await viaBroker(manifest, broker.url, refreshToken, existing, fetchImpl)
|
|
43
|
+
: await viaStoredClient(manifest, auth?.app, tokenUrl, refreshToken, credentials, fetchImpl);
|
|
44
|
+
|
|
45
|
+
// `existing` first, so what the response does not mention survives it. Neither
|
|
46
|
+
// the vendor nor the broker echoes `refresh_token`, `id_token`, or
|
|
47
|
+
// `authorized_via` unless there is a new one, and dropping any of the three
|
|
48
|
+
// would leave the *next* refresh with no token, no attribution, or pointed at
|
|
49
|
+
// the wrong client.
|
|
50
|
+
await provider.saveTokens({ ...existing, ...refreshed, refresh_token: refreshToken } as never);
|
|
51
|
+
return (await provider.tokens()) as unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function viaBroker(
|
|
55
|
+
manifest: ProviderManifest,
|
|
56
|
+
url: string,
|
|
57
|
+
refreshToken: string,
|
|
58
|
+
existing: StoredTokens,
|
|
59
|
+
fetchImpl: typeof globalThis.fetch,
|
|
60
|
+
): Promise<Record<string, unknown>> {
|
|
61
|
+
try {
|
|
62
|
+
return (await brokerRefresh(
|
|
63
|
+
url,
|
|
64
|
+
{ refreshToken, ...(existing.id_token ? { idToken: existing.id_token } : {}) },
|
|
65
|
+
fetchImpl,
|
|
66
|
+
)) as Record<string, unknown>;
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
// This surfaces to an agent in the middle of a request, so it must name a
|
|
69
|
+
// command the *owner* runs and nothing the agent could mistake for its own
|
|
70
|
+
// next step. Same shape as the stored-client message below, deliberately:
|
|
71
|
+
// where the credential came from is not the reader's problem here.
|
|
72
|
+
const notice = cause instanceof BrokerError && cause.notice ? `\n${cause.notice}` : '';
|
|
73
|
+
throw new Error(
|
|
74
|
+
`The credential for ${manifest.id} could not be refreshed. ` +
|
|
75
|
+
`Re-authorise with: lanes link connect ${manifest.id}\n${String(
|
|
76
|
+
cause instanceof Error ? cause.message : cause,
|
|
77
|
+
).slice(0, 200)}${notice}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function viaStoredClient(
|
|
83
|
+
manifest: ProviderManifest,
|
|
84
|
+
app: string | undefined,
|
|
85
|
+
tokenUrl: string,
|
|
86
|
+
refreshToken: string,
|
|
87
|
+
credentials: SecretStore,
|
|
88
|
+
fetchImpl: typeof globalThis.fetch,
|
|
89
|
+
): Promise<Record<string, unknown>> {
|
|
90
|
+
const [clientId, clientSecret] = app
|
|
91
|
+
? await Promise.all([
|
|
92
|
+
credentials.get(`${app}/client_id`),
|
|
93
|
+
credentials.get(`${app}/client_secret`),
|
|
94
|
+
])
|
|
95
|
+
: [null, null];
|
|
96
|
+
|
|
97
|
+
const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken });
|
|
98
|
+
if (clientId) body.set('client_id', clientId);
|
|
99
|
+
if (clientSecret) body.set('client_secret', clientSecret);
|
|
100
|
+
|
|
101
|
+
const response = await fetchImpl(tokenUrl, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
104
|
+
body,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
// A revoked or expired refresh token is the common case here, and the fix
|
|
110
|
+
// is always the same, so say it rather than surfacing the raw grant error.
|
|
111
|
+
throw new Error(
|
|
112
|
+
`The credential for ${manifest.id} could not be refreshed (${response.status}). ` +
|
|
113
|
+
`Re-authorise with: lanes link connect ${manifest.id}\n${text.slice(0, 200)}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
118
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { credentialRefForConnection } from '#connectivity';
|
|
2
|
+
import type { ProviderRegistry } from '#registry';
|
|
3
|
+
import type { SecretStore } from '#secrets';
|
|
4
|
+
import { resolveApiKey } from './api-key/index.ts';
|
|
5
|
+
import { resolveBasic } from './basic/index.ts';
|
|
6
|
+
import { resolveBearer } from './bearer/index.ts';
|
|
7
|
+
import { resolveHeader } from './header/index.ts';
|
|
8
|
+
import { resolveNone } from './none/index.ts';
|
|
9
|
+
import { resolveUpstreamToken } from './oauth-authcode/index.ts';
|
|
10
|
+
import { refuseStrategy } from './strategy/index.ts';
|
|
11
|
+
import type { ResolvedCredential } from './credential.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reading the store, once, for whichever credential type the manifest declares.
|
|
15
|
+
*
|
|
16
|
+
* One reader feeding two consumers: `authorize.ts` turns the result into
|
|
17
|
+
* headers for anything HTTP-shaped, and a non-HTTP connector takes this
|
|
18
|
+
* directly as a constructor option — IMAP has no `Request` to hand an
|
|
19
|
+
* authorizer and no headers to get back.
|
|
20
|
+
*
|
|
21
|
+
* Each method's own parsing lives in its folder, so what remains here is the
|
|
22
|
+
* dispatch. Adding a credential type is a folder and a case.
|
|
23
|
+
*/
|
|
24
|
+
export function credentialResolver(
|
|
25
|
+
registry: ProviderRegistry,
|
|
26
|
+
secrets: SecretStore,
|
|
27
|
+
): (providerId: string, connectionId: string) => Promise<ResolvedCredential> {
|
|
28
|
+
return async (providerId, connectionId) => {
|
|
29
|
+
const manifest = registry.manifest(providerId);
|
|
30
|
+
if (!manifest) return resolveNone();
|
|
31
|
+
|
|
32
|
+
const auth = manifest.auth;
|
|
33
|
+
if (auth.kind === 'none') return resolveNone();
|
|
34
|
+
|
|
35
|
+
if (auth.kind === 'oauth') {
|
|
36
|
+
const accessToken = await resolveUpstreamToken(manifest, connectionId, secrets);
|
|
37
|
+
return accessToken ? { kind: 'oauth', accessToken } : resolveNone();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (auth.kind === 'strategy') refuseStrategy(auth.strategy);
|
|
41
|
+
|
|
42
|
+
const ref = credentialRefForConnection(manifest, connectionId)!;
|
|
43
|
+
const value = await secrets.get(ref);
|
|
44
|
+
if (!value) {
|
|
45
|
+
throw new Error(`No credential stored at ${ref}. Run: lanes link connect ${providerId}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
switch (auth.kind) {
|
|
49
|
+
case 'basic':
|
|
50
|
+
return resolveBasic(value, ref, providerId);
|
|
51
|
+
case 'bearer':
|
|
52
|
+
return resolveBearer(value, auth.header);
|
|
53
|
+
case 'header':
|
|
54
|
+
return resolveHeader(value, auth.header);
|
|
55
|
+
case 'api_key':
|
|
56
|
+
return resolveApiKey(value, { header: auth.header, query: auth.query });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type { ResolvedCredential } from './credential.ts';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The escape hatch: auth no declarative form should try to express.
|
|
3
|
+
*
|
|
4
|
+
* bunq generates an RSA keypair, runs a three-step handshake, signs every
|
|
5
|
+
* request, and verifies the response signature. That earns code. Nothing is
|
|
6
|
+
* registered yet, so this fails loudly rather than sending an unauthenticated
|
|
7
|
+
* request that would fail confusingly upstream.
|
|
8
|
+
*/
|
|
9
|
+
export function refuseStrategy(strategy: string): never {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Auth strategy "${strategy}" is not registered. ` +
|
|
12
|
+
`Strategies are the only place per-vendor code belongs; see docs/detailed/creating-a-provider.md.`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { RedactionRule } from '#audit';
|
|
3
|
+
import type { ProviderContext } from './context.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A capability is one tool, resource, or prompt a provider exposes.
|
|
7
|
+
*
|
|
8
|
+
* Do not make everything a tool. Use resources for read-oriented document or
|
|
9
|
+
* structured context, tools for actions and parameterised queries, prompts for
|
|
10
|
+
* reusable procedures. Decide per capability and record the reasoning in
|
|
11
|
+
* `docs/detailed/providers.md` — ADR-006.
|
|
12
|
+
*/
|
|
13
|
+
export type Capability = ToolCapability | ResourceCapability | PromptCapability;
|
|
14
|
+
|
|
15
|
+
export interface CapabilityBase {
|
|
16
|
+
/**
|
|
17
|
+
* Unqualified name, e.g. `search`. Core qualifies it as `<provider>.<name>`
|
|
18
|
+
* — `gmail.search` — and that qualified form is what policy rules and audit
|
|
19
|
+
* events reference.
|
|
20
|
+
*/
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly title?: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
/**
|
|
25
|
+
* What survives into the audit log. Defaults to recording argument names and
|
|
26
|
+
* value types but no values, because a search query routinely contains the
|
|
27
|
+
* very content the caller may not be allowed to read.
|
|
28
|
+
*
|
|
29
|
+
* On the base rather than on tools alone: a resource read is dispatched with
|
|
30
|
+
* its URI as an argument, and a memory address is exactly as worth recording
|
|
31
|
+
* — and exactly as worth withholding — as a message id.
|
|
32
|
+
*/
|
|
33
|
+
readonly redact?: RedactionRule;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ToolResult = {
|
|
37
|
+
readonly content: ReadonlyArray<
|
|
38
|
+
| { readonly type: 'text'; readonly text: string }
|
|
39
|
+
| { readonly type: 'resource_link'; readonly uri: string; readonly name?: string }
|
|
40
|
+
>;
|
|
41
|
+
readonly isError?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface ToolCapability<Schema extends z.ZodType = z.ZodType> extends CapabilityBase {
|
|
45
|
+
readonly kind: 'tool';
|
|
46
|
+
/**
|
|
47
|
+
* The provider's own arguments only.
|
|
48
|
+
*
|
|
49
|
+
* Do NOT declare a `connection` argument. Core injects it, populates its enum
|
|
50
|
+
* per profile from resolved policy, and resolves it to a `ConnectionInfo`
|
|
51
|
+
* before the handler runs — so a client cannot even discover connections it
|
|
52
|
+
* has no grant for. That is ADR-001, and keeping it out of provider code is
|
|
53
|
+
* what makes one tool set scale to any number of accounts.
|
|
54
|
+
*/
|
|
55
|
+
readonly inputSchema: Schema;
|
|
56
|
+
handler(input: z.infer<Schema>, context: ProviderContext): Promise<ToolResult>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ResourceContents {
|
|
60
|
+
readonly uri: string;
|
|
61
|
+
readonly mimeType?: string;
|
|
62
|
+
readonly text: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read-oriented context addressed by a stable identifier — ADR-006.
|
|
67
|
+
*
|
|
68
|
+
* Both operations are reached through the same dispatch path tools use, so a
|
|
69
|
+
* resource read is policy-checked and audited exactly like a tool call. Core
|
|
70
|
+
* selects between them by whether it passes a `uri` argument: present means
|
|
71
|
+
* `read`, absent means `list`. Providers never see that distinction; they
|
|
72
|
+
* implement two methods.
|
|
73
|
+
*/
|
|
74
|
+
export interface ResourceCapability extends CapabilityBase {
|
|
75
|
+
readonly kind: 'resource';
|
|
76
|
+
/** RFC 6570 style, e.g. `example://notes/{id}`. A fixed URI is also valid. */
|
|
77
|
+
readonly uriTemplate: string;
|
|
78
|
+
readonly mimeType?: string;
|
|
79
|
+
/** Enumerate concrete resources. Omit when the space is unbounded. */
|
|
80
|
+
list?(context: ProviderContext): Promise<ReadonlyArray<{ uri: string; name: string }>>;
|
|
81
|
+
read(
|
|
82
|
+
uri: string,
|
|
83
|
+
params: Readonly<Record<string, string>>,
|
|
84
|
+
context: ProviderContext,
|
|
85
|
+
): Promise<ResourceContents>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A reusable procedure — the `skills` provider, and nothing else so far.
|
|
90
|
+
*
|
|
91
|
+
* The shape was fixed in M1 and left unimplemented so the primitive could not
|
|
92
|
+
* be claimed by anything else in the meantime; M4 gave it a runtime path.
|
|
93
|
+
*/
|
|
94
|
+
export interface PromptCapability extends CapabilityBase {
|
|
95
|
+
readonly kind: 'prompt';
|
|
96
|
+
readonly arguments?: ReadonlyArray<{
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly description: string;
|
|
99
|
+
readonly required?: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
render(args: Readonly<Record<string, string>>, context: ProviderContext): Promise<PromptResult>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PromptMessage {
|
|
105
|
+
readonly role: 'user' | 'assistant';
|
|
106
|
+
readonly text: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface PromptResult {
|
|
110
|
+
readonly messages: ReadonlyArray<PromptMessage>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What `ResourceCapability.read` produced, as dispatch hands it back. */
|
|
114
|
+
export interface ResourceResult {
|
|
115
|
+
readonly contents: ReadonlyArray<ResourceContents>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What `ResourceCapability.list` produced. */
|
|
119
|
+
export interface ResourceListResult {
|
|
120
|
+
readonly resources: ReadonlyArray<{ readonly uri: string; readonly name: string }>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* What one invocation produced, whichever primitive it was.
|
|
125
|
+
*
|
|
126
|
+
* A union rather than a widened `ToolResult` so that every existing connector —
|
|
127
|
+
* `http`, `mcp`, `imap`, `dav`, all of which serve tools only — keeps compiling
|
|
128
|
+
* unchanged: `ToolResult` is still a member, so their return type still
|
|
129
|
+
* satisfies `Connector.invoke`.
|
|
130
|
+
*
|
|
131
|
+
* Discriminated by which key is present, and the four keys are deliberately
|
|
132
|
+
* checked through the guards below rather than by hand. `content` and
|
|
133
|
+
* `contents` differ by one letter, and getting that wrong reads as an empty
|
|
134
|
+
* result rather than as a type error.
|
|
135
|
+
*/
|
|
136
|
+
export type CapabilityResult = ToolResult | ResourceResult | ResourceListResult | PromptResult;
|
|
137
|
+
|
|
138
|
+
export function isToolResult(result: CapabilityResult): result is ToolResult {
|
|
139
|
+
return 'content' in result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isResourceResult(result: CapabilityResult): result is ResourceResult {
|
|
143
|
+
return 'contents' in result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isResourceListResult(result: CapabilityResult): result is ResourceListResult {
|
|
147
|
+
return 'resources' in result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isPromptResult(result: CapabilityResult): result is PromptResult {
|
|
151
|
+
return 'messages' in result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isTool(capability: Capability): capability is ToolCapability {
|
|
155
|
+
return capability.kind === 'tool';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isResource(capability: Capability): capability is ResourceCapability {
|
|
159
|
+
return capability.kind === 'resource';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isPrompt(capability: Capability): capability is PromptCapability {
|
|
163
|
+
return capability.kind === 'prompt';
|
|
164
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { ConnectorConfig, ProviderManifest } from './manifest/index.ts';
|
|
2
|
+
import type { CapabilityResult, ProviderContext, ToolResult } from './index.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A connector turns a connectivity type into capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Adding a *provider* means adding a manifest, never a connector. Adding a
|
|
8
|
+
* *kind* means meeting a protocol we cannot otherwise reach, which is rare and
|
|
9
|
+
* belongs in review — the test is whether the code is about a protocol or about
|
|
10
|
+
* a vendor. `http.ts` is 215 lines serving Gmail, Drive, and every REST API
|
|
11
|
+
* ever; `imap.ts` serves iCloud, Fastmail, and any Dovecot. Neither mentions a
|
|
12
|
+
* vendor. The moment a kind would, it is the wrong shape and the answer is a
|
|
13
|
+
* manifest, or at most an `AuthStrategy`.
|
|
14
|
+
*
|
|
15
|
+
* `imap` and `dav` were added because iCloud has no MCP server, no OAuth, and
|
|
16
|
+
* no REST API, and because no manifest can describe IMAP — there is no
|
|
17
|
+
* machine-readable description of it to point at. See ADR-010.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The connectivity kinds, derived from the schema so it stays the only list. */
|
|
21
|
+
export type ConnectorKind = ConnectorConfig['kind'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A capability as a connector reports it.
|
|
25
|
+
*
|
|
26
|
+
* `inputSchema` is **JSON Schema**, not Zod: it comes from an upstream MCP
|
|
27
|
+
* server or an OpenAPI document, neither of which we author. The MCP layer
|
|
28
|
+
* converts it at registration with the SDK's `fromJsonSchema`. Local providers
|
|
29
|
+
* still author Zod and are converted the other way.
|
|
30
|
+
*/
|
|
31
|
+
export interface DiscoveredCapability {
|
|
32
|
+
/** Unqualified — `search`. Core qualifies it as `<provider>.<name>`. */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly title?: string;
|
|
35
|
+
readonly description: string;
|
|
36
|
+
readonly inputSchema: Record<string, unknown>;
|
|
37
|
+
/** Which bundle this belongs to, decided by the connector. */
|
|
38
|
+
readonly bundle?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Opaque routing detail the connector needs to invoke this again — an
|
|
41
|
+
* upstream tool name, or an OpenAPI method and path. Cached alongside the
|
|
42
|
+
* capability so a cold instance can serve without re-discovering.
|
|
43
|
+
*/
|
|
44
|
+
readonly target?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What discovery is allowed to see: the manifest, and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrower than `ConnectorContext`. Discovery runs at `connect`
|
|
51
|
+
* time, *before* the connection exists in config — so there is no
|
|
52
|
+
* `ProviderContext` to hand over, and the three call sites used to fake one with
|
|
53
|
+
* `provider: undefined as never`. Narrowing the parameter says the true thing
|
|
54
|
+
* instead: what a provider exposes is a property of the provider, not of any one
|
|
55
|
+
* account. A connector needing a credential here takes it as a constructor
|
|
56
|
+
* option, the way `createMcpConnector` already takes `accessToken`.
|
|
57
|
+
*/
|
|
58
|
+
export interface DiscoveryContext {
|
|
59
|
+
readonly manifest: ProviderManifest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ConnectorContext extends DiscoveryContext {
|
|
63
|
+
/** Everything a provider is allowed to reach, unchanged from M1. */
|
|
64
|
+
readonly provider: ProviderContext;
|
|
65
|
+
/**
|
|
66
|
+
* Prepare an outbound request: attach whatever the manifest's auth kind
|
|
67
|
+
* requires. Supplied by core so a connector never handles raw credentials.
|
|
68
|
+
*/
|
|
69
|
+
authorize(request: Request): Promise<Request>;
|
|
70
|
+
/** Verify a response, where the auth strategy demands it (bunq signs replies). */
|
|
71
|
+
verify?(response: Response): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A connector, parameterised by what its capabilities can produce.
|
|
76
|
+
*
|
|
77
|
+
* The default is `ToolResult`, because a remote kind serves tools and nothing
|
|
78
|
+
* else: an upstream MCP server's `tools/list` and an OpenAPI document both
|
|
79
|
+
* describe operations, so there is no resource or prompt for `mcp`, `http`,
|
|
80
|
+
* `imap`, or `dav` to return. `local` is the exception and declares
|
|
81
|
+
* `Connector<CapabilityResult>`.
|
|
82
|
+
*
|
|
83
|
+
* Parameterised rather than simply widened so those four keep their precise
|
|
84
|
+
* return type — a caller of `createImapConnector` still gets a `ToolResult`
|
|
85
|
+
* back and does not have to narrow a union that can only ever hold one member.
|
|
86
|
+
* Core dispatches through `AnyConnector`, which every kind satisfies because a
|
|
87
|
+
* method's return type is covariant.
|
|
88
|
+
*/
|
|
89
|
+
export interface Connector<Result extends CapabilityResult = ToolResult> {
|
|
90
|
+
readonly kind: ConnectorKind;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enumerate what this connection exposes.
|
|
94
|
+
*
|
|
95
|
+
* Called at `connect` time and on refresh, never on the dispatch path — the
|
|
96
|
+
* server is stateless and reads capabilities from the database cache, so a
|
|
97
|
+
* cold instance serves without a discovery round trip.
|
|
98
|
+
*/
|
|
99
|
+
discover(context: DiscoveryContext): Promise<DiscoveredCapability[]>;
|
|
100
|
+
|
|
101
|
+
/** Invoke one capability. Policy has already allowed it; this only performs it. */
|
|
102
|
+
invoke(
|
|
103
|
+
capability: DiscoveredCapability,
|
|
104
|
+
args: Readonly<Record<string, unknown>>,
|
|
105
|
+
context: ConnectorContext,
|
|
106
|
+
): Promise<Result>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whose account this connection turned out to be.
|
|
110
|
+
*
|
|
111
|
+
* For a protocol that authenticates by username there is nothing to probe:
|
|
112
|
+
* the answer is the name the *server accepted*, which is stronger than the one
|
|
113
|
+
* the operator typed. Nullary because the connector was built for exactly one
|
|
114
|
+
* connection and has no business naming another.
|
|
115
|
+
*/
|
|
116
|
+
identify?(): Promise<string | null>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Release a long-lived session, if this kind holds one.
|
|
120
|
+
*
|
|
121
|
+
* `mcp` and `http` are request-shaped and implement nothing here. `imap` holds
|
|
122
|
+
* a socket, because Apple throttles reconnection far harder than it throttles
|
|
123
|
+
* an open session.
|
|
124
|
+
*/
|
|
125
|
+
close?(): Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Any connector at all — what core holds, since it dispatches to all of them.
|
|
130
|
+
*
|
|
131
|
+
* Every `Connector<ToolResult>` is one of these, so the four remote kinds need
|
|
132
|
+
* no annotation to fit.
|
|
133
|
+
*/
|
|
134
|
+
export type AnyConnector = Connector<CapabilityResult>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pluggable auth, for what no declarative form should express.
|
|
138
|
+
*
|
|
139
|
+
* The only place per-vendor code is permitted outside `local` providers. Keep
|
|
140
|
+
* it to auth: the moment a strategy starts translating endpoints, the 612-line
|
|
141
|
+
* problem this milestone removed has come back.
|
|
142
|
+
*/
|
|
143
|
+
export interface AuthStrategy {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One-time setup at connect time — key generation, a handshake, whatever the
|
|
148
|
+
* vendor demands. Anything durable goes into the credential store.
|
|
149
|
+
*/
|
|
150
|
+
setup?(context: AuthStrategyContext): Promise<void>;
|
|
151
|
+
|
|
152
|
+
/** Per-request: sign it, add headers, refresh a session if one has expired. */
|
|
153
|
+
authorize(request: Request, context: AuthStrategyContext): Promise<Request>;
|
|
154
|
+
|
|
155
|
+
/** Optional response check — bunq signs its replies and expects them verified. */
|
|
156
|
+
verify?(response: Response, context: AuthStrategyContext): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface AuthStrategyContext {
|
|
160
|
+
readonly manifest: ProviderManifest;
|
|
161
|
+
readonly connectionId: string;
|
|
162
|
+
/** Read-only, scoped to this connection — the same boundary providers get. */
|
|
163
|
+
readonly credentials: ProviderContext['credentials'];
|
|
164
|
+
/**
|
|
165
|
+
* Writable credential access, available **only during `setup`**.
|
|
166
|
+
*
|
|
167
|
+
* A handshake has to persist what it produces (a keypair, a session token),
|
|
168
|
+
* which per-request code must never be able to do. Absent outside setup, so
|
|
169
|
+
* the restriction is structural rather than a rule to remember.
|
|
170
|
+
*/
|
|
171
|
+
readonly write?: (ref: string, value: string) => Promise<void>;
|
|
172
|
+
readonly state: ProviderContext['state'];
|
|
173
|
+
readonly log: ProviderContext['log'];
|
|
174
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
175
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AuditLogger } from '#audit';
|
|
2
|
+
import type { ScopedSecrets } from '#secrets';
|
|
3
|
+
import type { BlobStore } from '#stores/blobs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Key/value state for one provider on one connection.
|
|
7
|
+
*
|
|
8
|
+
* Already namespaced by core before a provider receives it, so there is no key
|
|
9
|
+
* a provider can construct that reaches another provider's state or another
|
|
10
|
+
* connection's state within its own provider.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScopedStore {
|
|
13
|
+
get(key: string): Promise<string | null>;
|
|
14
|
+
set(key: string, value: string): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
keys(): Promise<string[]>;
|
|
17
|
+
|
|
18
|
+
getJson<T>(key: string): Promise<T | null>;
|
|
19
|
+
setJson(key: string, value: unknown): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Logger {
|
|
23
|
+
debug(message: string, detail?: Record<string, unknown>): void;
|
|
24
|
+
info(message: string, detail?: Record<string, unknown>): void;
|
|
25
|
+
warn(message: string, detail?: Record<string, unknown>): void;
|
|
26
|
+
error(message: string, detail?: Record<string, unknown>): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The connection a capability was invoked for. */
|
|
30
|
+
export interface ConnectionInfo {
|
|
31
|
+
/** Connection id, unique within the provider. e.g. `main`. */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
/** Fully qualified: `gmail.main`. */
|
|
34
|
+
readonly key: string;
|
|
35
|
+
readonly displayName: string;
|
|
36
|
+
/** Validated against the provider's own `connectionSchema`. */
|
|
37
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Everything a provider is given, and nothing else.
|
|
42
|
+
*
|
|
43
|
+
* Providers receive scoped capabilities, never raw backends. There is no
|
|
44
|
+
* `RuntimeState` here, no `SecretStore`, no config, no policy engine, and no
|
|
45
|
+
* way to reach another connection.
|
|
46
|
+
*
|
|
47
|
+
* Note what this does NOT protect against: provider code is trusted code. It
|
|
48
|
+
* runs in-process and holds its connection's credential, so a malicious
|
|
49
|
+
* provider can do anything that credential permits. There is no provider
|
|
50
|
+
* sandbox in M1. Installing a third-party provider is equivalent to running
|
|
51
|
+
* arbitrary code with access to that account, and
|
|
52
|
+
* `docs/detailed/creating-a-provider.md` says so plainly.
|
|
53
|
+
*/
|
|
54
|
+
export interface ProviderContext {
|
|
55
|
+
readonly connection: ConnectionInfo;
|
|
56
|
+
readonly state: ScopedStore;
|
|
57
|
+
readonly storage: BlobStore;
|
|
58
|
+
readonly credentials: ScopedSecrets;
|
|
59
|
+
readonly audit: AuditLogger;
|
|
60
|
+
readonly log: Logger;
|
|
61
|
+
/** Aborted when the client disconnects or a limit is hit. */
|
|
62
|
+
readonly signal: AbortSignal;
|
|
63
|
+
/**
|
|
64
|
+
* Attach this connection's credential to an outbound request.
|
|
65
|
+
*
|
|
66
|
+
* The same authorizer a connector is given, offered here for a provider that
|
|
67
|
+
* has to call its own vendor's API directly — a capability authored precisely
|
|
68
|
+
* because the generic transport cannot express the call. It is a
|
|
69
|
+
* connection-scoped capability rather than a raw backend, which is the line
|
|
70
|
+
* this interface draws: a provider still cannot read the credential, name a
|
|
71
|
+
* different connection, or reach a store it did not declare.
|
|
72
|
+
*
|
|
73
|
+
* Optional because `local` providers hold no third-party account and the test
|
|
74
|
+
* harness builds a context without one.
|
|
75
|
+
*/
|
|
76
|
+
authorize?(request: Request): Promise<Request>;
|
|
77
|
+
}
|