@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.
Files changed (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. 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
+ }