@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,151 @@
1
+ import { READ_BUNDLE, WRITE_BUNDLE, type DiscoveredCapability } from '#connectivity';
2
+ import { DEFAULT_MAX_RANGE_DAYS, OPERATIONS, object } from './operations.ts';
3
+
4
+ /**
5
+ * What a DAV connection exposes, as data.
6
+ *
7
+ * Fixed, like `imap`: WebDAV describes collections, never operations. The list
8
+ * is conditioned on the service so a manifest pointing at a calendar never
9
+ * shows contact tools.
10
+ *
11
+ * Extracted from `discover()` because it is a hundred and thirty lines of
12
+ * declaration wrapped around three lines of behaviour, and reading the
13
+ * behaviour meant scrolling past all of it.
14
+ */
15
+ export function davCapabilities(service: 'caldav' | 'carddav'): DiscoveredCapability[] {
16
+ const calendar = service === 'caldav';
17
+
18
+ return calendar
19
+ ? [
20
+ {
21
+ name: OPERATIONS.listCalendars,
22
+ description: 'List the calendars in the account.',
23
+ bundle: READ_BUNDLE,
24
+ inputSchema: object({}),
25
+ target: { operation: OPERATIONS.listCalendars },
26
+ },
27
+ {
28
+ name: OPERATIONS.listEvents,
29
+ description:
30
+ 'Events between two dates, with repeating events already expanded into instances.',
31
+ bundle: READ_BUNDLE,
32
+ inputSchema: object(
33
+ {
34
+ calendar: {
35
+ type: 'string',
36
+ description: 'Calendar name from list_calendars. Defaults to every calendar.',
37
+ },
38
+ start: { type: 'string', description: 'ISO 8601 date or date-time.' },
39
+ end: {
40
+ type: 'string',
41
+ description: `ISO 8601. At most ${DEFAULT_MAX_RANGE_DAYS} days after start.`,
42
+ },
43
+ limit: { type: 'integer', minimum: 1, maximum: 200, default: 50 },
44
+ },
45
+ ['start', 'end'],
46
+ ),
47
+ target: { operation: OPERATIONS.listEvents },
48
+ },
49
+ {
50
+ name: OPERATIONS.getEvent,
51
+ description: 'One event by its UID, optionally with its raw iCalendar text.',
52
+ bundle: READ_BUNDLE,
53
+ inputSchema: object(
54
+ {
55
+ calendar: { type: 'string' },
56
+ uid: { type: 'string', description: 'UID as list_events reports it.' },
57
+ include_raw: { type: 'boolean', default: false },
58
+ },
59
+ ['uid'],
60
+ ),
61
+ target: { operation: OPERATIONS.getEvent },
62
+ },
63
+ {
64
+ name: OPERATIONS.updateEvent,
65
+ description:
66
+ 'Change an existing event. Only the fields you pass are touched; attendees, alarms and repetition are preserved.',
67
+ bundle: WRITE_BUNDLE,
68
+ inputSchema: object(
69
+ {
70
+ calendar: { type: 'string' },
71
+ uid: { type: 'string' },
72
+ summary: { type: 'string' },
73
+ start: { type: 'string', description: 'ISO 8601 date or date-time.' },
74
+ end: { type: 'string', description: 'ISO 8601 date or date-time.' },
75
+ all_day: { type: 'boolean' },
76
+ location: { type: 'string', description: 'Empty string clears it.' },
77
+ description: { type: 'string', description: 'Empty string clears it.' },
78
+ },
79
+ ['uid'],
80
+ ),
81
+ target: { operation: OPERATIONS.updateEvent },
82
+ },
83
+ {
84
+ name: OPERATIONS.deleteEvent,
85
+ description: 'Delete an event. This goes to the calendar\'s trash, not permanently.',
86
+ bundle: WRITE_BUNDLE,
87
+ inputSchema: object(
88
+ { calendar: { type: 'string' }, uid: { type: 'string' } },
89
+ ['uid'],
90
+ ),
91
+ target: { operation: OPERATIONS.deleteEvent },
92
+ },
93
+ {
94
+ name: OPERATIONS.createEvent,
95
+ description: 'Create an event in a calendar.',
96
+ bundle: WRITE_BUNDLE,
97
+ inputSchema: object(
98
+ {
99
+ calendar: { type: 'string' },
100
+ summary: { type: 'string' },
101
+ start: { type: 'string', description: 'ISO 8601 date or date-time.' },
102
+ end: { type: 'string', description: 'ISO 8601 date or date-time.' },
103
+ all_day: { type: 'boolean', default: false },
104
+ location: { type: 'string' },
105
+ description: { type: 'string' },
106
+ },
107
+ ['calendar', 'summary', 'start', 'end'],
108
+ ),
109
+ target: { operation: OPERATIONS.createEvent },
110
+ },
111
+ ]
112
+ : [
113
+ {
114
+ name: OPERATIONS.listAddressbooks,
115
+ description: 'List the address books in the account.',
116
+ bundle: READ_BUNDLE,
117
+ inputSchema: object({}),
118
+ target: { operation: OPERATIONS.listAddressbooks },
119
+ },
120
+ {
121
+ name: OPERATIONS.searchContacts,
122
+ description: 'Search contacts by name or email address.',
123
+ bundle: READ_BUNDLE,
124
+ inputSchema: object(
125
+ {
126
+ query: { type: 'string', description: 'Matched against name and email.' },
127
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 25 },
128
+ },
129
+ ['query'],
130
+ ),
131
+ target: { operation: OPERATIONS.searchContacts },
132
+ },
133
+ {
134
+ name: OPERATIONS.createContact,
135
+ description: 'Add a contact to an address book.',
136
+ bundle: WRITE_BUNDLE,
137
+ inputSchema: object(
138
+ {
139
+ addressbook: { type: 'string', description: 'Defaults to the first one.' },
140
+ full_name: { type: 'string' },
141
+ emails: { type: 'array', items: { type: 'string' } },
142
+ phones: { type: 'array', items: { type: 'string' } },
143
+ organization: { type: 'string' },
144
+ note: { type: 'string' },
145
+ },
146
+ ['full_name'],
147
+ ),
148
+ target: { operation: OPERATIONS.createContact },
149
+ },
150
+ ];
151
+ }
@@ -0,0 +1,226 @@
1
+ import type { ConnectorContext, ToolResult } from '#connectivity';
2
+ import { parseEvents } from './ical.ts';
3
+ import { PRINCIPAL_BODY, davRequest, hrefOfProperty, type DavReply } from './request.ts';
4
+ import { DEFAULT_MAX_RANGE_DAYS, HOME_KEY } from './operations.ts';
5
+ import {
6
+ CALDAV,
7
+ CARDDAV,
8
+ DAV,
9
+ escapeXml,
10
+ find,
11
+ findAll,
12
+ parseDav,
13
+ successful,
14
+ textOf,
15
+ } from './xml.ts';
16
+ import type { Collection, DavConnectorOptions } from './index.ts';
17
+
18
+ /**
19
+ * A DAV session for one connection.
20
+ *
21
+ * Holds the three things every operation needs and nothing else: how to make an
22
+ * authorised request, where this account's collections live, and how to find a
23
+ * single object by uid. `request`, `collections` and `locate` are public
24
+ * because the calendar and contact operations are free functions in their own
25
+ * files — the class is the transport, not the feature set.
26
+ */
27
+ export class DavClient {
28
+ constructor(
29
+ private readonly options: DavConnectorOptions,
30
+ private readonly doFetch: typeof globalThis.fetch,
31
+ private readonly context: ConnectorContext,
32
+ ) {}
33
+
34
+ /** The longest event window this server will answer — the provider's fact. */
35
+ get maxRangeDays(): number {
36
+ return this.options.maxRangeDays ?? DEFAULT_MAX_RANGE_DAYS;
37
+ }
38
+
39
+ private get calendar(): boolean {
40
+ return this.options.service === 'caldav';
41
+ }
42
+
43
+ request(
44
+ url: string,
45
+ method: string,
46
+ body?: string,
47
+ headers: Record<string, string> = {},
48
+ ): Promise<DavReply> {
49
+ return davRequest(this.doFetch, (request: Request) => this.context.authorize(request), url, method, body, headers);
50
+ }
51
+
52
+ /**
53
+ * The account's collection home, resolved once and remembered per connection.
54
+ *
55
+ * Three round trips — well-known, principal, home-set — which is why it is
56
+ * cached; and cached in per-connection state rather than in the capability
57
+ * target, because that cache is shared by every account of the provider.
58
+ * Re-resolved whenever it is missing or has stopped working, so an Apple
59
+ * partition migration heals itself instead of needing a re-connect.
60
+ */
61
+ private async home(): Promise<string> {
62
+ const remembered = await this.context.provider.state.get(HOME_KEY);
63
+ if (remembered) return remembered;
64
+
65
+ const resolved = await this.resolveHome();
66
+ await this.context.provider.state.set(HOME_KEY, resolved);
67
+ return resolved;
68
+ }
69
+
70
+ private async resolveHome(): Promise<string> {
71
+ const namespace = this.calendar ? CALDAV : CARDDAV;
72
+ const property = this.calendar ? 'calendar-home-set' : 'addressbook-home-set';
73
+
74
+ // Two places to ask, in order. RFC 6764's well-known path is a bootstrap
75
+ // *hint*, and a server is free not to answer there — iCloud's contacts host
76
+ // returns a 207 whose propstat is 404, which is "not here" wearing the
77
+ // costume of a success. Falling back to the root is what every DAV client
78
+ // does, and it is where iCloud actually answers.
79
+ const candidates = [
80
+ new URL(this.calendar ? '/.well-known/caldav' : '/.well-known/carddav', this.options.baseUrl)
81
+ .href,
82
+ new URL('/', this.options.baseUrl).href,
83
+ ];
84
+
85
+ let principalHref: string | undefined;
86
+ let principalFoundAt: string | undefined;
87
+ let lastStatus = 0;
88
+
89
+ for (const candidate of candidates) {
90
+ const reply = await this.request(candidate, 'PROPFIND', PRINCIPAL_BODY, { depth: '0' });
91
+ lastStatus = reply.status;
92
+ if (reply.status >= 400) continue;
93
+
94
+ const href = hrefOfProperty(parseDav(reply.text), `{${DAV}}current-user-principal`);
95
+ if (href) {
96
+ principalHref = href;
97
+ principalFoundAt = reply.url;
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (lastStatus === 401 || lastStatus === 403) {
103
+ throw new Error(
104
+ `The server refused discovery (${lastStatus}).` +
105
+ (this.options.troubleshooting ? ` ${this.options.troubleshooting}` : ''),
106
+ );
107
+ }
108
+ if (!principalHref || !principalFoundAt) {
109
+ throw new Error('The server did not say which principal you are.');
110
+ }
111
+
112
+ const principalUrl = new URL(principalHref, principalFoundAt).href;
113
+ const homeBody =
114
+ `<?xml version="1.0" encoding="utf-8"?>` +
115
+ `<d:propfind xmlns:d="DAV:" xmlns:x="${namespace}">` +
116
+ `<d:prop><x:${property}/></d:prop></d:propfind>`;
117
+
118
+ const second = await this.request(principalUrl, 'PROPFIND', homeBody, { depth: '0' });
119
+ const homeHref = hrefOfProperty(parseDav(second.text), `{${namespace}}${property}`);
120
+
121
+ if (!homeHref) throw new Error('The server did not say where your collections live.');
122
+ return new URL(homeHref, second.url).href;
123
+ }
124
+
125
+ /**
126
+ * The collections in the home, filtered to the ones this service can use.
127
+ *
128
+ * The `VEVENT` filter is not cosmetic: iCloud publishes Reminders lists as
129
+ * CalDAV collections holding `VTODO`, and without this they show up as
130
+ * calendars that are always empty — which is Home Assistant's single
131
+ * most-reported CalDAV complaint.
132
+ */
133
+ async collections(): Promise<Collection[]> {
134
+ const namespace = this.calendar ? CALDAV : CARDDAV;
135
+ const body =
136
+ `<?xml version="1.0" encoding="utf-8"?>` +
137
+ `<d:propfind xmlns:d="DAV:" xmlns:x="${namespace}">` +
138
+ `<d:prop><d:resourcetype/><d:displayname/>` +
139
+ (this.calendar ? `<x:supported-calendar-component-set/>` : '') +
140
+ `</d:prop></d:propfind>`;
141
+
142
+ const result = await this.request(await this.home(), 'PROPFIND', body, { depth: '1' });
143
+ const responses = findAll(parseDav(result.text), `{${DAV}}response`);
144
+ const collections: Collection[] = [];
145
+
146
+ for (const response of responses) {
147
+ const href = textOf(response, `{${DAV}}href`);
148
+ if (!href) continue;
149
+
150
+ const props = successful(response);
151
+ if (props.length === 0) continue;
152
+
153
+ const isRightType = props.some(
154
+ (prop) =>
155
+ findAll(prop, `{${namespace}}${this.calendar ? 'calendar' : 'addressbook'}`).length > 0,
156
+ );
157
+ if (!isRightType) continue;
158
+
159
+ // A collection that declares its components but not VEVENT is a Reminders
160
+ // list. Silence on the question is treated as "yes": some servers omit the
161
+ // property entirely, and dropping those would hide real calendars.
162
+ if (this.calendar) {
163
+ const declared = props.some(
164
+ (prop) => findAll(prop, `{${CALDAV}}supported-calendar-component-set`).length > 0,
165
+ );
166
+ const holdsEvents = props.some((prop) =>
167
+ findAll(prop, `{${CALDAV}}comp`).some((comp) => comp.attributes['name'] === 'VEVENT'),
168
+ );
169
+ if (declared && !holdsEvents) continue;
170
+ }
171
+
172
+ collections.push({
173
+ // A display name is optional, and iCloud omits it on the default
174
+ // address book. The last path segment is a poor name but a usable one;
175
+ // the whole href is neither, and this value is what a caller passes back
176
+ // in as `calendar` or `addressbook`.
177
+ name:
178
+ props.map((prop) => textOf(prop, `{${DAV}}displayname`)).find(Boolean) ??
179
+ href.split('/').filter(Boolean).pop() ??
180
+ href,
181
+ href: new URL(href, result.url).href,
182
+ });
183
+ }
184
+
185
+ return collections;
186
+ }
187
+
188
+ async locate(
189
+ uid: string,
190
+ calendarName?: string,
191
+ ): Promise<{ href: string; etag: string | undefined; ical: string; calendar: string } | null> {
192
+ const escaped = escapeXml(uid);
193
+ const body =
194
+ `<?xml version="1.0" encoding="utf-8"?>` +
195
+ `<c:calendar-query xmlns:d="DAV:" xmlns:c="${CALDAV}">` +
196
+ `<d:prop><d:getetag/><c:calendar-data/></d:prop>` +
197
+ `<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT">` +
198
+ `<c:prop-filter name="UID"><c:text-match collation="i;octet">${escaped}</c:text-match></c:prop-filter>` +
199
+ `</c:comp-filter></c:comp-filter></c:filter></c:calendar-query>`;
200
+
201
+ const calendars = (await this.collections()).filter(
202
+ (collection) => !calendarName || collection.name === calendarName,
203
+ );
204
+
205
+ for (const collection of calendars) {
206
+ const reply = await this.request(collection.href, 'REPORT', body, { depth: '1' });
207
+ if (reply.status >= 400) continue;
208
+
209
+ for (const response of findAll(parseDav(reply.text), `{${DAV}}response`)) {
210
+ const href = textOf(response, `{${DAV}}href`);
211
+ const ical = textOf(response, `{${CALDAV}}calendar-data`);
212
+ if (!href || !ical) continue;
213
+
214
+ return {
215
+ href: new URL(href, reply.url).href,
216
+ etag: textOf(response, `{${DAV}}getetag`),
217
+ ical,
218
+ calendar: collection.name,
219
+ };
220
+ }
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ }
@@ -0,0 +1,75 @@
1
+ import type { ToolResult } from '#connectivity';
2
+ import { buildContact, parseContacts } from './ical.ts';
3
+ import { error, json } from './request.ts';
4
+ import { CARDDAV, DAV, escapeXml, findAll, parseDav, textOf } from './xml.ts';
5
+ import type { DavClient } from './client.ts';
6
+
7
+ /** CardDAV's two operations. Same shape as `calendar.ts`, and same reason. */
8
+
9
+ export async function createContact(
10
+ dav: DavClient,
11
+ args: Readonly<Record<string, unknown>>,
12
+ ): Promise<ToolResult> {
13
+ const books = await dav.collections();
14
+ const wanted = args['addressbook'] ? String(args['addressbook']) : undefined;
15
+ const book = wanted ? books.find((entry) => entry.name === wanted) : books[0];
16
+ if (!book) return error(wanted ? `No address book named "${wanted}".` : 'No address books.');
17
+
18
+ const uid = crypto.randomUUID();
19
+ const vcard = buildContact({
20
+ uid,
21
+ fullName: String(args['full_name'] ?? ''),
22
+ emails: args['emails'] as string[] | undefined,
23
+ phones: args['phones'] as string[] | undefined,
24
+ organization: args['organization'] as string | undefined,
25
+ note: args['note'] as string | undefined,
26
+ });
27
+
28
+ const href = new URL(`${uid}.vcf`, `${book.href.replace(/\/?$/, '/')}`).href;
29
+ const reply = await dav.request(href, 'PUT', vcard, {
30
+ 'content-type': 'text/vcard; charset=utf-8',
31
+ 'if-none-match': '*',
32
+ });
33
+
34
+ if (reply.status >= 400) {
35
+ return error(`The server refused the contact (${reply.status}). ${reply.text.slice(0, 200)}`);
36
+ }
37
+
38
+ return json({ created: true, uid, addressbook: book.name });
39
+ }
40
+
41
+ export async function searchContacts(
42
+ dav: DavClient,
43
+ args: Readonly<Record<string, unknown>>,
44
+ ): Promise<ToolResult> {
45
+ const query = String(args['query'] ?? '');
46
+ if (!query) return error('query is required.');
47
+ const limit = Math.min(Number(args['limit'] ?? 25) || 25, 100);
48
+
49
+ const escaped = escapeXml(query);
50
+ const body =
51
+ `<?xml version="1.0" encoding="utf-8"?>` +
52
+ `<c:addressbook-query xmlns:d="DAV:" xmlns:c="${CARDDAV}">` +
53
+ `<d:prop><d:getetag/><c:address-data/></d:prop>` +
54
+ `<c:filter test="anyof">` +
55
+ `<c:prop-filter name="FN"><c:text-match collation="i;unicode-casemap" match-type="contains">${escaped}</c:text-match></c:prop-filter>` +
56
+ `<c:prop-filter name="EMAIL"><c:text-match collation="i;unicode-casemap" match-type="contains">${escaped}</c:text-match></c:prop-filter>` +
57
+ `</c:filter></c:addressbook-query>`;
58
+
59
+ const contacts: unknown[] = [];
60
+
61
+ for (const collection of await dav.collections()) {
62
+ const result = await dav.request(collection.href, 'REPORT', body, { depth: '1' });
63
+ if (result.status >= 400) continue;
64
+
65
+ for (const response of findAll(parseDav(result.text), `{${DAV}}response`)) {
66
+ const data = textOf(response, `{${CARDDAV}}address-data`);
67
+ if (!data) continue;
68
+ for (const contact of parseContacts(data)) {
69
+ contacts.push({ ...contact, addressbook: collection.name });
70
+ }
71
+ }
72
+ }
73
+
74
+ return json({ query, contacts: contacts.slice(0, limit) });
75
+ }