@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,161 @@
1
+ import type { ToolResult } from '#connectivity';
2
+ import { DAV, find, findAll, parseDav, successful, textOf, type DavNode } from './xml.ts';
3
+ import type { DavConnectorOptions } from './index.ts';
4
+
5
+ /**
6
+ * The HTTP half of DAV: one request helper, the redirect rule, and the
7
+ * connect-time credential check.
8
+ *
9
+ * Everything here is protocol plumbing that knows nothing about calendars or
10
+ * contacts, which is why it is separate from both.
11
+ */
12
+
13
+ /**
14
+ * Check the credential against the server, returning the username it accepted.
15
+ *
16
+ * This is the one place the connector builds an Authorization header itself,
17
+ * because there is no `ConnectorContext` at connect time to do it — the
18
+ * connection does not exist in config yet. It never reads the credential
19
+ * *store*: what it gets is already resolved and bound to one account.
20
+ */
21
+ export async function validate(
22
+ options: DavConnectorOptions,
23
+ doFetch: typeof globalThis.fetch,
24
+ ): Promise<string | null> {
25
+ if (!options.credential) return null;
26
+
27
+ const { username, password } = await options.credential();
28
+ const encoded = Buffer.from(`${username}:${password}`, 'utf8').toString('base64');
29
+ const wellKnown = new URL(
30
+ options.service === 'caldav' ? '/.well-known/caldav' : '/.well-known/carddav',
31
+ options.baseUrl,
32
+ ).href;
33
+
34
+ const reply = await davRequest(
35
+ doFetch,
36
+ async (request) => {
37
+ const authorised = new Request(request, { headers: new Headers(request.headers) });
38
+ authorised.headers.set('authorization', `Basic ${encoded}`);
39
+ return authorised;
40
+ },
41
+ wellKnown,
42
+ 'PROPFIND',
43
+ PRINCIPAL_BODY,
44
+ { depth: '0' },
45
+ );
46
+
47
+ if (reply.status === 401 || reply.status === 403) {
48
+ throw new Error(
49
+ `The server rejected the credential for ${username}.` +
50
+ (options.troubleshooting ? `\n ${options.troubleshooting}` : ''),
51
+ );
52
+ }
53
+ if (reply.status >= 400) {
54
+ throw new Error(`The server refused discovery (${reply.status}).`);
55
+ }
56
+
57
+ return username;
58
+ }
59
+
60
+ /**
61
+ * The `href` inside a named property, from a propstat that actually succeeded.
62
+ *
63
+ * A multistatus reports status *per property*, so the presence of an element is
64
+ * not the presence of a value: iCloud answers `<current-user-principal/>`, empty,
65
+ * under a 404 propstat. Reading that as an answer is how discovery silently
66
+ * concludes you have no principal.
67
+ */
68
+ export function hrefOfProperty(tree: DavNode, qname: string): string | undefined {
69
+ for (const response of findAll(tree, `{${DAV}}response`)) {
70
+ for (const propstat of successful(response)) {
71
+ const property = find(propstat, qname);
72
+ const href = property ? textOf(property, `{${DAV}}href`) : undefined;
73
+ if (href) return href;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ export function error(message: string): ToolResult {
80
+ return { content: [{ type: 'text', text: message }], isError: true };
81
+ }
82
+
83
+ export function json(value: unknown): ToolResult {
84
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
85
+ }
86
+
87
+ interface Collection {
88
+ readonly name: string;
89
+ readonly href: string;
90
+ }
91
+
92
+ export interface DavReply {
93
+ readonly status: number;
94
+ readonly text: string;
95
+ readonly url: string;
96
+ }
97
+
98
+ export const PRINCIPAL_BODY =
99
+ `<?xml version="1.0" encoding="utf-8"?>` +
100
+ `<d:propfind xmlns:d="DAV:"><d:prop><d:current-user-principal/></d:prop></d:propfind>`;
101
+
102
+ /**
103
+ * A DAV request, with the redirect policy this protocol needs.
104
+ *
105
+ * Redirects are followed by hand. `fetch`'s automatic handling would resend the
106
+ * Authorization header to wherever it was pointed — an app-specific password
107
+ * handed to whoever the Location names — and it does not reliably repeat a body
108
+ * for a non-standard method, which every request here has.
109
+ */
110
+ export async function davRequest(
111
+ doFetch: typeof globalThis.fetch,
112
+ authorize: (request: Request) => Promise<Request>,
113
+ url: string,
114
+ method: string,
115
+ body?: string,
116
+ headers: Record<string, string> = {},
117
+ ): Promise<DavReply> {
118
+ let target = url;
119
+
120
+ for (let hop = 0; hop < 5; hop++) {
121
+ const request = await authorize(
122
+ new Request(target, {
123
+ method,
124
+ headers: { 'content-type': 'application/xml; charset=utf-8', ...headers },
125
+ ...(body ? { body } : {}),
126
+ redirect: 'manual',
127
+ }),
128
+ );
129
+
130
+ const response = await doFetch(request);
131
+
132
+ if (![301, 302, 307, 308].includes(response.status)) {
133
+ return { status: response.status, text: await response.text(), url: target };
134
+ }
135
+
136
+ const location = response.headers.get('location');
137
+ if (!location) break;
138
+
139
+ const next = new URL(location, target);
140
+ // iCloud relocates to a numbered partition of its own domain. Following
141
+ // *off* the registrable domain would hand the credential to a stranger.
142
+ if (next.protocol !== 'https:' || !sameSite(next.hostname, new URL(target).hostname)) {
143
+ throw new Error(
144
+ `The server redirected to ${next.origin}, which is not the host you configured. Refusing to send credentials there.`,
145
+ );
146
+ }
147
+
148
+ target = next.href;
149
+ }
150
+
151
+ throw new Error('Too many redirects while talking to the DAV server.');
152
+ }
153
+
154
+
155
+
156
+ /** Whether a redirect stayed within the same registrable domain. */
157
+ export function sameSite(next: string, current: string): boolean {
158
+ if (next === current) return true;
159
+ const tail = current.split('.').slice(-2).join('.');
160
+ return next === tail || next.endsWith(`.${tail}`);
161
+ }
@@ -0,0 +1,123 @@
1
+ import { parseXml, XmlElement, type XmlNode } from '@rgrove/parse-xml';
2
+
3
+ /**
4
+ * Reading WebDAV's `multistatus` replies.
5
+ *
6
+ * Namespaces are resolved to `{uri}local` rather than matched by prefix,
7
+ * because the prefix is the server's choice and servers disagree freely:
8
+ * iCloud writes `<D:href>`, Fastmail `<d:href>`, Nextcloud `<href>` with a
9
+ * default namespace. Anything matching on `D:` works against one vendor and
10
+ * silently finds nothing against the next — and "found nothing" is
11
+ * indistinguishable from "you have no calendars".
12
+ *
13
+ * Building the request side is templates plus escaping, not a serialiser: there
14
+ * are four bodies, each fixed apart from a date range or a search string.
15
+ */
16
+
17
+ /** DAV namespace URIs, as constants because they are typed wrong so easily. */
18
+ export const DAV = 'DAV:';
19
+ export const CALDAV = 'urn:ietf:params:xml:ns:caldav';
20
+ export const CARDDAV = 'urn:ietf:params:xml:ns:carddav';
21
+
22
+ export interface DavNode {
23
+ /** `{DAV:}href` — namespace URI in braces, then the local name. */
24
+ readonly qname: string;
25
+ readonly text: string;
26
+ /**
27
+ * Kept because DAV puts real answers in attributes, not only in text:
28
+ * `<C:comp name="VEVENT"/>` is how a collection says whether it holds events
29
+ * or reminders, and that distinction decides whether it is a calendar at all.
30
+ */
31
+ readonly attributes: Readonly<Record<string, string>>;
32
+ readonly children: readonly DavNode[];
33
+ }
34
+
35
+ /**
36
+ * Parse a DAV response body.
37
+ *
38
+ * A `<!DOCTYPE` is refused outright rather than parsed. That is the entire
39
+ * defence against both XXE and the billion-laughs expansion, it costs one line,
40
+ * and no DAV server has any reason to send one.
41
+ */
42
+ export function parseDav(xml: string): DavNode {
43
+ if (/<!DOCTYPE/i.test(xml)) {
44
+ throw new Error('The server sent XML with a DOCTYPE, which this client refuses to parse.');
45
+ }
46
+
47
+ const document = parseXml(xml, { ignoreUndefinedEntities: true });
48
+ const root = document.children.find((node): node is XmlElement => node instanceof XmlElement);
49
+ if (!root) throw new Error('The server sent XML with no root element.');
50
+
51
+ return normalise(root, new Map([['xml', 'http://www.w3.org/XML/1998/namespace']]));
52
+ }
53
+
54
+ function normalise(element: XmlElement, inherited: ReadonlyMap<string, string>): DavNode {
55
+ const prefixes = new Map(inherited);
56
+
57
+ for (const [name, value] of Object.entries(element.attributes)) {
58
+ if (name === 'xmlns') prefixes.set('', value);
59
+ else if (name.startsWith('xmlns:')) prefixes.set(name.slice(6), value);
60
+ }
61
+
62
+ const colon = element.name.indexOf(':');
63
+ const prefix = colon === -1 ? '' : element.name.slice(0, colon);
64
+ const local = colon === -1 ? element.name : element.name.slice(colon + 1);
65
+ const uri = prefixes.get(prefix) ?? '';
66
+
67
+ return {
68
+ qname: `{${uri}}${local}`,
69
+ text: element.text,
70
+ attributes: element.attributes,
71
+ children: element.children
72
+ .filter((child: XmlNode): child is XmlElement => child instanceof XmlElement)
73
+ .map((child) => normalise(child, prefixes)),
74
+ };
75
+ }
76
+
77
+ /** Every descendant with this qualified name, at any depth. */
78
+ export function findAll(node: DavNode, qname: string): DavNode[] {
79
+ const found: DavNode[] = [];
80
+ const walk = (current: DavNode): void => {
81
+ if (current.qname === qname) found.push(current);
82
+ for (const child of current.children) walk(child);
83
+ };
84
+ walk(node);
85
+ return found;
86
+ }
87
+
88
+ /** The first descendant with this qualified name. */
89
+ export function find(node: DavNode, qname: string): DavNode | undefined {
90
+ return findAll(node, qname)[0];
91
+ }
92
+
93
+ /** The text of the first descendant with this qualified name. */
94
+ export function textOf(node: DavNode, qname: string): string | undefined {
95
+ const found = find(node, qname);
96
+ const text = found?.text.trim();
97
+ return text ? text : undefined;
98
+ }
99
+
100
+ /**
101
+ * Whether a `propstat` block actually succeeded.
102
+ *
103
+ * A multistatus reports per-property status, so a `<prop>` present in the reply
104
+ * is not a property the server *has* — a 404 propstat carries the same element
105
+ * names, empty. Reading those as values is how a calendar list comes back full
106
+ * of blanks.
107
+ */
108
+ export function successful(response: DavNode): DavNode[] {
109
+ return findAll(response, `{${DAV}}propstat`).filter((propstat) => {
110
+ const status = textOf(propstat, `{${DAV}}status`) ?? '';
111
+ return / 2\d\d /.test(status) || status.includes('200');
112
+ });
113
+ }
114
+
115
+ /** Escape text destined for an XML body. */
116
+ export function escapeXml(value: string): string {
117
+ return value
118
+ .replace(/&/g, '&amp;')
119
+ .replace(/</g, '&lt;')
120
+ .replace(/>/g, '&gt;')
121
+ .replace(/"/g, '&quot;')
122
+ .replace(/'/g, '&apos;');
123
+ }
@@ -0,0 +1,181 @@
1
+ import type { AnyConnector, ProviderDefinition, ProviderManifest } from '#connectivity';
2
+ import type { SecretStore } from '#secrets';
3
+ import type { ProviderRegistry } from '#registry';
4
+ import { basicCredential } from '#connectivity/auth/basic/index.ts';
5
+ import { resolveUpstreamToken } from '#connectivity/auth/oauth-authcode/index.ts';
6
+ import { createCompositeConnector } from './composite/index.ts';
7
+ import { createDavConnector } from './dav/index.ts';
8
+ import { createFsConnector } from './fs/index.ts';
9
+ import { createHttpConnector } from './http/index.ts';
10
+ import { createImapConnector } from './imap/index.ts';
11
+ import { createLocalConnector } from './local/index.ts';
12
+ import { createMcpConnector } from './mcp/index.ts';
13
+
14
+ /**
15
+ * Which transport a provider gets, and one instance of it per connection.
16
+ *
17
+ * The single place that turns a declared `connector.kind` into running code —
18
+ * so the connectivity types are a closed set you can read off one switch, and
19
+ * adding one is a folder beside this file plus a case below.
20
+ */
21
+
22
+ export interface ConnectorFactoryOptions {
23
+ readonly registry: ProviderRegistry;
24
+ readonly credentials: SecretStore;
25
+ }
26
+
27
+ /**
28
+ * Build connectors, memoised per provider *and* connection.
29
+ *
30
+ * The connection used to be fixed when the factory was constructed, which meant
31
+ * every caller built a fresh factory — and therefore a fresh cache — for every
32
+ * lookup, so nothing was ever reused. That went unnoticed because `mcp` and
33
+ * `http` are request-shaped and a redundant instance costs nothing. `imap` holds
34
+ * a socket, so the same bug would have meant a TLS handshake and a LOGIN per
35
+ * tool call, against a server that throttles exactly that.
36
+ *
37
+ * Taking the connection as an argument is what makes one factory per runtime
38
+ * possible, which is what makes the cache real.
39
+ */
40
+ export interface ConnectorFactory {
41
+ (providerId: string, connectionId: string): AnyConnector | undefined;
42
+ /**
43
+ * Close every connector that holds a session.
44
+ *
45
+ * Only a stateful kind implements `close`; for the rest this is a no-op. A
46
+ * failure to close is swallowed — the process is going away, and a socket that
47
+ * cannot be shut down cleanly is not a reason to fail the command that was
48
+ * already finished.
49
+ */
50
+ closeAll(): Promise<void>;
51
+ }
52
+
53
+ export function connectorFactory(options: ConnectorFactoryOptions): ConnectorFactory {
54
+ const cache = new Map<string, AnyConnector>();
55
+
56
+ const factory = (providerId: string, connectionId: string): AnyConnector | undefined => {
57
+ const key = `${providerId}.${connectionId}`;
58
+ const cached = cache.get(key);
59
+ if (cached) return cached;
60
+
61
+ const entry = options.registry.get(providerId);
62
+ if (!entry) return undefined;
63
+
64
+ const connector = wrap(entry.definition, build(entry.manifest, entry.definition, connectionId, options));
65
+ if (connector) cache.set(key, connector);
66
+ return connector;
67
+ };
68
+
69
+ factory.closeAll = async (): Promise<void> => {
70
+ await Promise.all(
71
+ [...cache.values()].map(async (connector) => {
72
+ try {
73
+ await connector.close?.();
74
+ } catch {
75
+ /* see above */
76
+ }
77
+ }),
78
+ );
79
+ cache.clear();
80
+ };
81
+
82
+ return factory;
83
+ }
84
+
85
+ /**
86
+ * Layer a provider's own capabilities over a remote connector.
87
+ *
88
+ * Only when it has both, which is neither of the two common cases: `local` is
89
+ * already all authored code, and a manifest-only provider has nothing to layer.
90
+ * What is left is a remote provider carrying a handful of authored capabilities
91
+ * because the vendor's API can do something its document cannot describe — see
92
+ * `composite/index.ts`.
93
+ */
94
+ function wrap(
95
+ definition: ProviderDefinition | undefined,
96
+ remote: AnyConnector | undefined,
97
+ ): AnyConnector | undefined {
98
+ if (!remote || !definition || definition.capabilities.length === 0) return remote;
99
+ if (remote.kind === 'local') return remote;
100
+
101
+ return createCompositeConnector({ definition, remote });
102
+ }
103
+
104
+ function build(
105
+ manifest: ProviderManifest,
106
+ definition: ProviderDefinition | undefined,
107
+ connectionId: string,
108
+ options: ConnectorFactoryOptions,
109
+ ): AnyConnector | undefined {
110
+ switch (manifest.connector.kind) {
111
+ case 'local':
112
+ return definition ? createLocalConnector(definition) : undefined;
113
+
114
+ case 'mcp':
115
+ return createMcpConnector({
116
+ endpoint: manifest.connector.endpoint,
117
+ accessToken: () => resolveUpstreamToken(manifest, connectionId, options.credentials),
118
+ });
119
+
120
+ case 'imap':
121
+ return createImapConnector({
122
+ host: manifest.connector.host,
123
+ port: manifest.connector.port,
124
+ maxBodyBytes: manifest.connector.max_body_bytes,
125
+ ...(manifest.setup?.troubleshooting !== undefined
126
+ ? { troubleshooting: manifest.setup.troubleshooting }
127
+ : {}),
128
+ // Mapped rather than passed through: the manifest is snake_case and the
129
+ // transport option is not, the same way `max_body_bytes` is above.
130
+ ...(manifest.connector.smtp
131
+ ? {
132
+ smtp: {
133
+ host: manifest.connector.smtp.host,
134
+ port: manifest.connector.smtp.port,
135
+ starttls: manifest.connector.smtp.starttls,
136
+ maxMessageBytes: manifest.connector.smtp.max_message_bytes,
137
+ },
138
+ }
139
+ : {}),
140
+ // The same seam `createMcpConnector` uses for its token: a credential
141
+ // arrives as a constructor option, bound to one account, because IMAP
142
+ // has no Request to hand an authorizer and no headers to get back.
143
+ credential: () => basicCredential(manifest, connectionId, options.credentials),
144
+ });
145
+
146
+ case 'dav':
147
+ // No credential option: DAV is HTTPS, so `context.authorize` attaches
148
+ // Basic auth and the connector never sees one.
149
+ return createDavConnector({
150
+ baseUrl: manifest.connector.base_url,
151
+ service: manifest.connector.service,
152
+ maxRangeDays: manifest.connector.max_range_days,
153
+ ...(manifest.setup?.troubleshooting !== undefined
154
+ ? { troubleshooting: manifest.setup.troubleshooting }
155
+ : {}),
156
+ // Only for the connect-time credential check and the account label;
157
+ // every operation still goes through `context.authorize`.
158
+ credential: () => basicCredential(manifest, connectionId, options.credentials),
159
+ });
160
+
161
+ case 'fs':
162
+ return createFsConnector({
163
+ root: manifest.connector.root,
164
+ maxFileBytes: manifest.connector.max_file_bytes,
165
+ exclude: manifest.connector.exclude,
166
+ ...(manifest.connector.placeholder ? { placeholder: manifest.connector.placeholder } : {}),
167
+ });
168
+
169
+ case 'http':
170
+ return createHttpConnector({
171
+ baseUrl: manifest.connector.base_url,
172
+ openapi: manifest.connector.openapi,
173
+ ...(manifest.connector.operations?.include?.length
174
+ ? { include: manifest.connector.operations.include }
175
+ : {}),
176
+ ...(manifest.connector.operations?.exclude?.length
177
+ ? { exclude: manifest.connector.operations.exclude }
178
+ : {}),
179
+ });
180
+ }
181
+ }
@@ -0,0 +1,88 @@
1
+ import { READ_BUNDLE, WRITE_BUNDLE, type DiscoveredCapability } from '#connectivity';
2
+ import { OPERATIONS, object, pathArgument } from './operations.ts';
3
+
4
+ /** What an `fs` connection exposes, as data. Fixed — a folder is a folder. */
5
+ export function fsCapabilities(): DiscoveredCapability[] {
6
+ return [
7
+ {
8
+ name: OPERATIONS.listFiles,
9
+ description: 'List the entries in a folder.',
10
+ bundle: READ_BUNDLE,
11
+ inputSchema: object({
12
+ path: pathArgument,
13
+ recursive: { type: 'boolean', default: false },
14
+ limit: { type: 'integer', minimum: 1, maximum: 500, default: 200 },
15
+ }),
16
+ target: { operation: OPERATIONS.listFiles },
17
+ },
18
+ {
19
+ name: OPERATIONS.searchFiles,
20
+ description: 'Find files by name, and optionally by the text inside them.',
21
+ bundle: READ_BUNDLE,
22
+ inputSchema: object(
23
+ {
24
+ query: { type: 'string', description: 'Matched against file names.' },
25
+ contains: { type: 'string', description: 'Also require this text in the file.' },
26
+ path: pathArgument,
27
+ limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 },
28
+ },
29
+ ['query'],
30
+ ),
31
+ target: { operation: OPERATIONS.searchFiles },
32
+ },
33
+ {
34
+ name: OPERATIONS.readFile,
35
+ description: 'Read a text file. Binary files report their type and size instead.',
36
+ bundle: READ_BUNDLE,
37
+ inputSchema: object({ path: pathArgument }, ['path']),
38
+ target: { operation: OPERATIONS.readFile },
39
+ },
40
+ {
41
+ name: OPERATIONS.fileInfo,
42
+ description: 'Size, timestamps, and whether the file is downloaded from the cloud.',
43
+ bundle: READ_BUNDLE,
44
+ inputSchema: object({ path: pathArgument }, ['path']),
45
+ target: { operation: OPERATIONS.fileInfo },
46
+ },
47
+ {
48
+ name: OPERATIONS.writeFile,
49
+ description: 'Write a text file, creating it or replacing its contents.',
50
+ bundle: WRITE_BUNDLE,
51
+ inputSchema: object(
52
+ {
53
+ path: pathArgument,
54
+ content: { type: 'string' },
55
+ overwrite: {
56
+ type: 'boolean',
57
+ default: false,
58
+ description: 'Required to replace a file that already exists.',
59
+ },
60
+ },
61
+ ['path', 'content'],
62
+ ),
63
+ target: { operation: OPERATIONS.writeFile },
64
+ },
65
+ {
66
+ name: OPERATIONS.moveFile,
67
+ description: 'Move or rename a file or folder within the root.',
68
+ bundle: WRITE_BUNDLE,
69
+ inputSchema: object({ from: pathArgument, to: pathArgument }, ['from', 'to']),
70
+ target: { operation: OPERATIONS.moveFile },
71
+ },
72
+ {
73
+ name: OPERATIONS.createFolder,
74
+ description: 'Create a folder, including any missing parents.',
75
+ bundle: WRITE_BUNDLE,
76
+ inputSchema: object({ path: pathArgument }, ['path']),
77
+ target: { operation: OPERATIONS.createFolder },
78
+ },
79
+ {
80
+ name: OPERATIONS.trashFile,
81
+ description:
82
+ 'Move a file to the system Trash. Recoverable — nothing here deletes permanently.',
83
+ bundle: WRITE_BUNDLE,
84
+ inputSchema: object({ path: pathArgument }, ['path']),
85
+ target: { operation: OPERATIONS.trashFile },
86
+ },
87
+ ];
88
+ }