@vibesdotdev/integrations-microsoft 0.0.0-stage → 1.0.0-rc.1

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 (143) hide show
  1. package/LICENSE +126 -0
  2. package/README.md +120 -2
  3. package/dist/api-client-plugin.d.ts +3 -0
  4. package/dist/api-client-plugin.d.ts.map +1 -0
  5. package/dist/api-client-plugin.js +11 -0
  6. package/dist/api-client-plugin.js.map +1 -0
  7. package/dist/client/helper.d.ts +17 -0
  8. package/dist/client/helper.d.ts.map +1 -0
  9. package/dist/client/helper.js +18 -0
  10. package/dist/client/helper.js.map +1 -0
  11. package/dist/client/index.d.ts +3 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +3 -0
  14. package/dist/client/index.js.map +1 -0
  15. package/dist/client/runtime-client.d.ts +87 -0
  16. package/dist/client/runtime-client.d.ts.map +1 -0
  17. package/dist/client/runtime-client.js +47 -0
  18. package/dist/client/runtime-client.js.map +1 -0
  19. package/dist/cloud.plugin.d.ts +3 -0
  20. package/dist/cloud.plugin.d.ts.map +1 -0
  21. package/dist/cloud.plugin.js +19 -0
  22. package/dist/cloud.plugin.js.map +1 -0
  23. package/dist/email/index.d.ts +13 -0
  24. package/dist/email/index.d.ts.map +1 -0
  25. package/dist/email/index.js +13 -0
  26. package/dist/email/index.js.map +1 -0
  27. package/dist/email/microsoft/index.d.ts +4 -0
  28. package/dist/email/microsoft/index.d.ts.map +1 -0
  29. package/dist/email/microsoft/index.js +4 -0
  30. package/dist/email/microsoft/index.js.map +1 -0
  31. package/dist/email/microsoft/microsoft.descriptor.d.ts +85 -0
  32. package/dist/email/microsoft/microsoft.descriptor.d.ts.map +1 -0
  33. package/dist/email/microsoft/microsoft.descriptor.js +79 -0
  34. package/dist/email/microsoft/microsoft.descriptor.js.map +1 -0
  35. package/dist/email/microsoft/microsoft.impl.d.ts +100 -0
  36. package/dist/email/microsoft/microsoft.impl.d.ts.map +1 -0
  37. package/dist/email/microsoft/microsoft.impl.js +323 -0
  38. package/dist/email/microsoft/microsoft.impl.js.map +1 -0
  39. package/dist/email/microsoft/outlook.mime.d.ts +18 -0
  40. package/dist/email/microsoft/outlook.mime.d.ts.map +1 -0
  41. package/dist/email/microsoft/outlook.mime.js +61 -0
  42. package/dist/email/microsoft/outlook.mime.js.map +1 -0
  43. package/dist/email/microsoft-email.plugin.d.ts +10 -0
  44. package/dist/email/microsoft-email.plugin.d.ts.map +1 -0
  45. package/dist/email/microsoft-email.plugin.js +29 -0
  46. package/dist/email/microsoft-email.plugin.js.map +1 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +8 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/integrations-microsoft.plugin.d.ts +4 -0
  52. package/dist/integrations-microsoft.plugin.d.ts.map +1 -0
  53. package/dist/integrations-microsoft.plugin.js +15 -0
  54. package/dist/integrations-microsoft.plugin.js.map +1 -0
  55. package/dist/microsoft/graph-drive-fallback.d.ts +11 -0
  56. package/dist/microsoft/graph-drive-fallback.d.ts.map +1 -0
  57. package/dist/microsoft/graph-drive-fallback.js +90 -0
  58. package/dist/microsoft/graph-drive-fallback.js.map +1 -0
  59. package/dist/microsoft/graph-schemas.d.ts +2 -0
  60. package/dist/microsoft/graph-schemas.d.ts.map +1 -0
  61. package/dist/microsoft/graph-schemas.js +2 -0
  62. package/dist/microsoft/graph-schemas.js.map +1 -0
  63. package/dist/microsoft/graph-sharepoint-client.d.ts +219 -0
  64. package/dist/microsoft/graph-sharepoint-client.d.ts.map +1 -0
  65. package/dist/microsoft/graph-sharepoint-client.js +644 -0
  66. package/dist/microsoft/graph-sharepoint-client.js.map +1 -0
  67. package/dist/microsoft/index.d.ts +8 -0
  68. package/dist/microsoft/index.d.ts.map +1 -0
  69. package/dist/microsoft/index.js +8 -0
  70. package/dist/microsoft/index.js.map +1 -0
  71. package/dist/microsoft/microsoft-delegated-implementation.d.ts +20 -0
  72. package/dist/microsoft/microsoft-delegated-implementation.d.ts.map +1 -0
  73. package/dist/microsoft/microsoft-delegated-implementation.js +69 -0
  74. package/dist/microsoft/microsoft-delegated-implementation.js.map +1 -0
  75. package/dist/microsoft/microsoft-implementation.d.ts +22 -0
  76. package/dist/microsoft/microsoft-implementation.d.ts.map +1 -0
  77. package/dist/microsoft/microsoft-implementation.js +78 -0
  78. package/dist/microsoft/microsoft-implementation.js.map +1 -0
  79. package/dist/microsoft/secrets-fetch.d.ts +20 -0
  80. package/dist/microsoft/secrets-fetch.d.ts.map +1 -0
  81. package/dist/microsoft/secrets-fetch.js +47 -0
  82. package/dist/microsoft/secrets-fetch.js.map +1 -0
  83. package/dist/microsoft/sharepoint-operations.d.ts +23 -0
  84. package/dist/microsoft/sharepoint-operations.d.ts.map +1 -0
  85. package/dist/microsoft/sharepoint-operations.js +123 -0
  86. package/dist/microsoft/sharepoint-operations.js.map +1 -0
  87. package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts +85 -0
  88. package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts.map +1 -0
  89. package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js +109 -0
  90. package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js.map +1 -0
  91. package/dist/microsoft/system.integration.microsoft.descriptor.d.ts +69 -0
  92. package/dist/microsoft/system.integration.microsoft.descriptor.d.ts.map +1 -0
  93. package/dist/microsoft/system.integration.microsoft.descriptor.js +114 -0
  94. package/dist/microsoft/system.integration.microsoft.descriptor.js.map +1 -0
  95. package/dist/microsoft.consumer.plugin.d.ts +4 -0
  96. package/dist/microsoft.consumer.plugin.d.ts.map +1 -0
  97. package/dist/microsoft.consumer.plugin.js +13 -0
  98. package/dist/microsoft.consumer.plugin.js.map +1 -0
  99. package/dist/schemas/boundary.d.ts +69 -0
  100. package/dist/schemas/boundary.d.ts.map +1 -0
  101. package/dist/schemas/boundary.js +94 -0
  102. package/dist/schemas/boundary.js.map +1 -0
  103. package/dist/schemas/graph.d.ts +139 -0
  104. package/dist/schemas/graph.d.ts.map +1 -0
  105. package/dist/schemas/graph.js +161 -0
  106. package/dist/schemas/graph.js.map +1 -0
  107. package/dist/schemas/oauth.d.ts +131 -0
  108. package/dist/schemas/oauth.d.ts.map +1 -0
  109. package/dist/schemas/oauth.js +27 -0
  110. package/dist/schemas/oauth.js.map +1 -0
  111. package/dist/schemas/operations.d.ts +239 -0
  112. package/dist/schemas/operations.d.ts.map +1 -0
  113. package/dist/schemas/operations.js +29 -0
  114. package/dist/schemas/operations.js.map +1 -0
  115. package/dist/schemas/outlook.d.ts +241 -0
  116. package/dist/schemas/outlook.d.ts.map +1 -0
  117. package/dist/schemas/outlook.js +29 -0
  118. package/dist/schemas/outlook.js.map +1 -0
  119. package/dist/schemas/sharepoint.d.ts +67 -0
  120. package/dist/schemas/sharepoint.d.ts.map +1 -0
  121. package/dist/schemas/sharepoint.js +71 -0
  122. package/dist/schemas/sharepoint.js.map +1 -0
  123. package/dist/server/oauth.d.ts +9 -0
  124. package/dist/server/oauth.d.ts.map +1 -0
  125. package/dist/server/oauth.js +67 -0
  126. package/dist/server/oauth.js.map +1 -0
  127. package/dist/sharepoint/index.d.ts +11 -0
  128. package/dist/sharepoint/index.d.ts.map +1 -0
  129. package/dist/sharepoint/index.js +11 -0
  130. package/dist/sharepoint/index.js.map +1 -0
  131. package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts +10 -0
  132. package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts.map +1 -0
  133. package/dist/sharepoint/sharepoint.adapter.descriptor.js +32 -0
  134. package/dist/sharepoint/sharepoint.adapter.descriptor.js.map +1 -0
  135. package/dist/sharepoint/sharepoint.adapter.impl.d.ts +195 -0
  136. package/dist/sharepoint/sharepoint.adapter.impl.d.ts.map +1 -0
  137. package/dist/sharepoint/sharepoint.adapter.impl.js +645 -0
  138. package/dist/sharepoint/sharepoint.adapter.impl.js.map +1 -0
  139. package/dist/sharepoint/sharepoint.schemas.d.ts +2 -0
  140. package/dist/sharepoint/sharepoint.schemas.d.ts.map +1 -0
  141. package/dist/sharepoint/sharepoint.schemas.js +2 -0
  142. package/dist/sharepoint/sharepoint.schemas.js.map +1 -0
  143. package/package.json +146 -4
@@ -0,0 +1,644 @@
1
+ import { GraphJsonSchema, GraphJsonCollectionSchema, GraphDownloadSchema, GraphUploadSessionSchema } from "../schemas/graph.js";
2
+ import { settle, settleNow } from '@vibesdotdev/kernel';
3
+ import * as z from 'zod/v4';
4
+ import { CapabilityError } from '@vibesdotdev/kernel';
5
+ import { admitMicrosoft, MicrosoftGraphError, graphFailure, microsoftFailure, closeMicrosoftResponse } from "../schemas/boundary.js";
6
+ import { GraphSiteSchema, SharePointSiteSchema, GraphDriveSchema, GraphDriveItemSchema, DriveItemEntrySchema, DriveItemSearchResultSchema } from "./graph-schemas.js";
7
+ const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0';
8
+ /**
9
+ * Sentinel "site id" that targets the signed-in user's personal OneDrive
10
+ * (`/me/drive`) instead of a SharePoint site. Delegated only — app-only tokens
11
+ * have no `/me`. Used as a drive id by the explorer for the "OneDrive" entry.
12
+ */
13
+ export const ONEDRIVE_SITE_ID = 'me';
14
+ /** Graph recommends a resumable upload session above ~4 MiB. */
15
+ const SIMPLE_UPLOAD_MAX = 4 * 1024 * 1024;
16
+ /** Upload-session chunk size: 5 MiB, a multiple of 320 KiB (a Graph requirement). */
17
+ const UPLOAD_CHUNK = 327680 * 16;
18
+ const GRAPH_GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ /** Percent-encode each path segment while preserving `/` separators. */
20
+ function encodeDrivePath(path) {
21
+ return path
22
+ .split('/')
23
+ .filter((seg) => seg.length > 0)
24
+ .map(encodeURIComponent)
25
+ .join('/');
26
+ }
27
+ /** Normalize an upload body to bytes (buffering a stream when needed). */
28
+ async function toUint8Array(body) {
29
+ if (body instanceof ArrayBuffer)
30
+ return new Uint8Array(body);
31
+ if (body instanceof Blob) {
32
+ return new Uint8Array(await body.arrayBuffer());
33
+ }
34
+ const reader = body.getReader();
35
+ const chunks = [];
36
+ let total = 0;
37
+ for (;;) {
38
+ const { done, value } = await reader.read();
39
+ if (done)
40
+ break;
41
+ if (value) {
42
+ chunks.push(value);
43
+ total += value.byteLength;
44
+ }
45
+ }
46
+ const out = new Uint8Array(total);
47
+ let offset = 0;
48
+ for (const c of chunks) {
49
+ out.set(c, offset);
50
+ offset += c.byteLength;
51
+ }
52
+ return out;
53
+ }
54
+ function looksLikeGraphDriveId(id) {
55
+ return GRAPH_GUID_RE.test(id) || id.startsWith('b!') || id.startsWith('u!');
56
+ }
57
+ /**
58
+ * Microsoft Graph API client for SharePoint operations.
59
+ *
60
+ * Provides listSites(), browseDrive(), and downloadFile() methods.
61
+ * Uses the runtime-supplied authenticated transport for registered
62
+ * providers. App-only access requires the Sites.Read.All (read) /
63
+ * Sites.ReadWrite.All (write) + Files.Read.All/Files.ReadWrite.All APPLICATION
64
+ * permissions, admin-consented into the org's tenant — Sites.Selected does NOT
65
+ * satisfy the `GET /sites?search=*` site enumeration listSites() relies on.
66
+ */
67
+ export class GraphSharePointClient {
68
+ integration;
69
+ authenticatedFetch;
70
+ fetchFn;
71
+ constructor(config) {
72
+ if (!config.integration && !config.authenticatedFetch) {
73
+ throw new CapabilityError('Microsoft Graph requires an authenticated transport.', 'UNAVAILABLE', { reason: 'microsoft-graph-auth-transport-required' });
74
+ }
75
+ this.integration = config.integration;
76
+ this.authenticatedFetch = config.authenticatedFetch;
77
+ // Bind the default global fetch to globalThis: it's invoked as `this.fetchFn(...)`,
78
+ // which would otherwise call native fetch with `this` = this client instance →
79
+ // workerd throws "Illegal invocation". An injected fetchFn is used as-is; tests
80
+ // and pre-authenticated upload-session requests retain ownership of that boundary.
81
+ this.fetchFn = config.fetchFn ?? fetch.bind(globalThis);
82
+ }
83
+ /**
84
+ * Build a download URL operation result without streaming binary
85
+ * content through the dispatch path. Calls
86
+ * `/sites/{site-id}/drive/items/{item-id}` with `?$select=id,@microsoft.graph.downloadUrl`
87
+ * and returns the temporary URL Graph mints (~1h validity). Consumers
88
+ * fetch bytes directly; this keeps the JSON-only operations dispatch
89
+ * binary-free.
90
+ */
91
+ async getDownloadUrl(siteId, itemId) {
92
+ // Do NOT use $select here: `@microsoft.graph.downloadUrl` is a default-returned
93
+ // instance annotation that Graph OMITS under $select (the same trap documented
94
+ // on getDownloadUrlByPath below). $select silently dropped the URL, so every
95
+ // item-id download — and thus every drawing OCR byte-fetch — 500'd with
96
+ // "Graph did not return @microsoft.graph.downloadUrl". Fetch the full item.
97
+ const url = `${this.driveRoot(siteId)}/items/${itemId}`;
98
+ const response = await this.executeRequest(url, GraphDownloadSchema);
99
+ const downloadUrl = response['@microsoft.graph.downloadUrl'];
100
+ if (!downloadUrl) {
101
+ throw new Error(`Graph did not return @microsoft.graph.downloadUrl for site '${siteId}' item '${itemId}'.`);
102
+ }
103
+ // Graph's downloadUrl is a pre-signed Azure Blob SAS URL; expiry is
104
+ // embedded in the SAS but not surfaced as an ISO string. Return null
105
+ // for now and let callers refresh on 403/expired errors.
106
+ return { url: downloadUrl, expiresAt: null };
107
+ }
108
+ /**
109
+ * Pre-authenticated download URL for a drive item addressed by its PATH (vs
110
+ * item id). Returns Graph's `@microsoft.graph.downloadUrl` — a short-lived
111
+ * Azure Blob SAS URL that needs no auth header, so it works directly in a
112
+ * browser `<img>`/`<iframe>`/download across origins (the object store has no
113
+ * stable anonymous URL otherwise). `null` if the item is missing.
114
+ */
115
+ async getDownloadUrlByPath(siteId, path) {
116
+ // GET the full driveItem (no $select): `@microsoft.graph.downloadUrl` is a
117
+ // default-returned instance annotation for files but is unreliable under
118
+ // $select (the `@`-annotation is often omitted), which silently dropped the
119
+ // URL and forced the proxy fallback.
120
+ const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`;
121
+ const attempt = await settle(() => this.executeRequest(url, GraphDownloadSchema));
122
+ if (attempt.ok)
123
+ return attempt.value['@microsoft.graph.downloadUrl'] ?? null;
124
+ if (attempt.cause instanceof MicrosoftGraphError && attempt.cause.status === 404)
125
+ return null;
126
+ throw attempt.cause;
127
+ }
128
+ /**
129
+ * List SharePoint sites the app has access to.
130
+ *
131
+ * Returns normalized site objects with id, name, and webUrl.
132
+ * App-only requires the Sites.Read.All application permission so the
133
+ * `GET /sites?search=*` enumeration returns the tenant's sites.
134
+ *
135
+ * GET /sites?search=*&$select=id,name,webUrl,displayName
136
+ *
137
+ * @returns Array of SharePoint sites
138
+ */
139
+ async listSites() {
140
+ // `search=*` lists all sites the caller can reach — required for app-only
141
+ // (client-credentials) tokens, which otherwise get no sites from a bare
142
+ // GET /sites; harmless for delegated (returns the user's accessible sites).
143
+ const url = `${GRAPH_API_BASE}/sites?search=*&$select=id,name,webUrl,displayName`;
144
+ const response = await this.executeRequest(url, GraphJsonCollectionSchema);
145
+ const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
146
+ return allItems
147
+ .map((item) => {
148
+ const parsed = GraphSiteSchema.safeParse(item);
149
+ if (!parsed.success)
150
+ throw new CapabilityError('Microsoft Graph returned a malformed resource.', 'UNAVAILABLE', { reason: 'microsoft-graph-response-invalid', cause: parsed.error });
151
+ const site = parsed.data;
152
+ return SharePointSiteSchema.parse({
153
+ id: site.id,
154
+ name: site.displayName || site.name,
155
+ webUrl: site.webUrl
156
+ });
157
+ })
158
+ .filter((site) => site !== null);
159
+ }
160
+ /**
161
+ * Return a site's default document library as a real Graph drive.
162
+ *
163
+ * Storage consumers pass a `driveId`, not a site id. SharePoint site ids are
164
+ * often composite strings (`host,siteCollectionId,webId`) that are brittle in
165
+ * agent/tool contexts, while Graph drive ids are the stable primitive accepted
166
+ * by `/drives/{id}` across browse/search/download operations.
167
+ */
168
+ async getDefaultDrive(siteId) {
169
+ const raw = await this.executeRequest(`${GRAPH_API_BASE}/sites/${siteId}/drive?$select=id,name,webUrl,driveType`, GraphDriveSchema);
170
+ const parsed = GraphDriveSchema.safeParse(raw);
171
+ if (!parsed.success) {
172
+ throw new Error(`Graph default drive for site '${siteId}' was not a valid drive.`);
173
+ }
174
+ return parsed.data;
175
+ }
176
+ /**
177
+ * Browse folder contents within a SharePoint site's default drive.
178
+ *
179
+ * GET /sites/{site-id}/drive/root/children
180
+ *
181
+ * When folderPath is provided:
182
+ * GET /sites/{site-id}/drive/root:/{folderPath}:/children
183
+ *
184
+ * @param siteId - SharePoint site ID
185
+ * @param folderPath - Optional path relative to drive root (e.g. "Documents/Reports")
186
+ * @returns Array of drive item entries (files and folders)
187
+ */
188
+ async browseDrive(siteId, folderPath) {
189
+ let url;
190
+ if (folderPath && folderPath.length > 0) {
191
+ // Encode per path segment (preserve literal `/` separators) like every
192
+ // other path method here — a flat encodeURIComponent flattens nested
193
+ // folder paths and breaks browseDrive for subfolders.
194
+ const encoded = encodeDrivePath(folderPath);
195
+ url = `${this.driveRoot(siteId)}/root:/${encoded}:/children`;
196
+ }
197
+ else {
198
+ url = `${this.driveRoot(siteId)}/root/children`;
199
+ }
200
+ const attempt = await settle(() => this.executeRequest(url, GraphJsonCollectionSchema));
201
+ if (!attempt.ok) {
202
+ const failure = attempt.cause;
203
+ if (siteId === ONEDRIVE_SITE_ID && !folderPath && failure instanceof MicrosoftGraphError && failure.status === 404)
204
+ return [];
205
+ throw failure;
206
+ }
207
+ const response = attempt.value;
208
+ const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
209
+ return allItems
210
+ .map((item) => this.normalizeDriveItem(item))
211
+ .filter((entry) => entry !== null);
212
+ }
213
+ /**
214
+ * Browse folder contents by stable DriveItem id.
215
+ *
216
+ * Use this when a caller already has an item id from search/list, or when
217
+ * Graph path addressing rejects an otherwise valid folder path.
218
+ */
219
+ async browseDriveItem(siteId, itemId) {
220
+ const response = await this.executeRequest(`${this.driveRoot(siteId)}/items/${itemId}/children`, GraphJsonCollectionSchema);
221
+ const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
222
+ return allItems
223
+ .map((item) => this.normalizeDriveItem(item))
224
+ .filter((entry) => entry !== null);
225
+ }
226
+ /**
227
+ * RECURSIVELY search a site/OneDrive drive for items matching `query`,
228
+ * at ANY depth, in a single Graph call.
229
+ *
230
+ * Uses Graph's server-side drive search:
231
+ * GET /sites/{site-id}/drive/root/search(q='{query}')
232
+ * GET /me/drive/root/search(q='{query}') (OneDrive sentinel)
233
+ *
234
+ * Graph indexes the whole drive server-side and matches on file/folder
235
+ * NAME and (for supported types) CONTENT — so a partial job number or
236
+ * filename matches files buried several folders deep (e.g. a PDF under
237
+ * "Projects to Bid/2026/Acme/Drawings") that the one-level {@link browseDrive}
238
+ * can never reach. `$select` includes `parentReference` so each hit carries
239
+ * its folder path for breadcrumbs / relinking.
240
+ *
241
+ * `scopePath` (optional, drive-root-relative) narrows the search to a
242
+ * subtree by addressing the folder before `:/search` — e.g.
243
+ * GET /sites/{id}/drive/root:/Projects to Bid:/search(q='...')
244
+ * Use it to keep results inside the org's bid folder.
245
+ *
246
+ * @param siteId SharePoint site id, or {@link ONEDRIVE_SITE_ID} for OneDrive
247
+ * @param query free-text query (job number, partial filename, content)
248
+ * @param opts.scopePath optional folder subtree to search within
249
+ * @param opts.limit cap on hits returned (default 25); paging stops early
250
+ * @returns Array of search hits with folder path metadata
251
+ */
252
+ async searchDrive(siteId, query, opts) {
253
+ const trimmed = query.trim();
254
+ if (trimmed.length === 0)
255
+ return [];
256
+ const limit = Math.max(1, Math.min(opts?.limit ?? 25, 200));
257
+ // Graph's search(q='...') is a function call: the q value is
258
+ // single-quoted in the URL; escape embedded single quotes by doubling.
259
+ const q = encodeURIComponent(trimmed.replace(/'/g, "''"));
260
+ const base = this.driveRoot(siteId);
261
+ const root = opts?.scopePath && opts.scopePath.length > 0
262
+ ? `${base}/root:/${encodeDrivePath(opts.scopePath)}:`
263
+ : `${base}/root`;
264
+ const select = '$select=id,name,size,file,folder,webUrl,lastModifiedDateTime,parentReference';
265
+ const url = `${root}/search(q='${q}')?${select}&$top=${Math.min(limit, 100)}`;
266
+ const attempt = await settle(() => this.executeRequest(url, GraphJsonCollectionSchema));
267
+ if (!attempt.ok) {
268
+ const failure = attempt.cause;
269
+ if (siteId === ONEDRIVE_SITE_ID && !opts?.scopePath && failure instanceof MicrosoftGraphError && failure.status === 404)
270
+ return [];
271
+ throw failure;
272
+ }
273
+ const response = attempt.value;
274
+ // Page until we have `limit` hits or run out — but never crawl forever.
275
+ const raw = [...response.value];
276
+ let link = response['@odata.nextLink'];
277
+ while (link && raw.length < limit) {
278
+ const page = await this.executeRequest(link, GraphJsonCollectionSchema);
279
+ raw.push(...page.value);
280
+ link = page['@odata.nextLink'];
281
+ }
282
+ return raw
283
+ .slice(0, limit)
284
+ .map((item) => this.normalizeSearchResult(item, siteId))
285
+ .filter((entry) => entry !== null);
286
+ }
287
+ /**
288
+ * Download file content from a SharePoint drive.
289
+ *
290
+ * Read Graph's item metadata, then fetch its preauthenticated download URL
291
+ * without forwarding Graph authorization across origins.
292
+ *
293
+ * Returns the raw binary content as a Blob.
294
+ * Callers can convert to ArrayBuffer, ReadableStream, or base64
295
+ * depending on their needs.
296
+ *
297
+ * @param siteId - SharePoint site ID
298
+ * @param itemId - Drive item ID (file)
299
+ * @returns Blob with file content
300
+ */
301
+ async downloadFile(siteId, itemId) {
302
+ const root = `${this.driveRoot(siteId)}/items/${itemId}`;
303
+ const metadata = await this.executeRequest(root, GraphDownloadSchema);
304
+ const response = await this.downloadResponse(metadata, `${root}/content`);
305
+ const content = await settle(() => response.blob());
306
+ if (!content.ok)
307
+ throw microsoftFailure(content.cause);
308
+ return content.value;
309
+ }
310
+ // Write operations (object protocol — used by SharePointStorageAdapter.objectStore)
311
+ /**
312
+ * Upload bytes to `path` (relative to the site drive root), creating or
313
+ * overwriting the file. Simple PUT below ~4 MiB; resumable upload session
314
+ * (chunked) at or above it.
315
+ */
316
+ async uploadFile(siteId, path, body, opts) {
317
+ const bytes = await toUint8Array(body);
318
+ const raw = bytes.byteLength < SIMPLE_UPLOAD_MAX
319
+ ? await this.simpleUpload(siteId, path, bytes, opts?.contentType)
320
+ : await this.resumableUpload(siteId, path, bytes, opts?.conflictBehavior);
321
+ const normalized = this.normalizeDriveItem(raw);
322
+ if (!normalized) {
323
+ throw new Error(`Graph upload to '${path}' returned an unparseable drive item.`);
324
+ }
325
+ return normalized;
326
+ }
327
+ async simpleUpload(siteId, path, bytes, contentType) {
328
+ const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}:/content`;
329
+ const accessToken = await this.acquireAccessToken();
330
+ const response = await this.fetchResponse(url, {
331
+ method: 'PUT',
332
+ headers: {
333
+ Authorization: `Bearer ${accessToken}`,
334
+ 'Content-Type': contentType ?? 'application/octet-stream'
335
+ },
336
+ body: new Blob([bytes])
337
+ });
338
+ if (!response.ok) {
339
+ throw await graphFailure(response);
340
+ }
341
+ return this.readGraphResponse(response, GraphJsonSchema);
342
+ }
343
+ async resumableUpload(siteId, path, bytes, conflictBehavior) {
344
+ const sessionUrl = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}:/createUploadSession`;
345
+ const session = await this.executeWriteJson(sessionUrl, 'POST', {
346
+ item: { '@microsoft.graph.conflictBehavior': conflictBehavior ?? 'replace' }
347
+ });
348
+ const uploadUrl = this.admitGraphResponse(GraphUploadSessionSchema, session).uploadUrl;
349
+ if (!uploadUrl) {
350
+ throw new Error('Graph createUploadSession returned no uploadUrl.');
351
+ }
352
+ const total = bytes.byteLength;
353
+ let offset = 0;
354
+ let last = null;
355
+ while (offset < total) {
356
+ const end = Math.min(offset + UPLOAD_CHUNK, total);
357
+ const chunk = bytes.subarray(offset, end);
358
+ // The upload-session URL is pre-authenticated — no Authorization header.
359
+ last = await this.fetchResponse(uploadUrl, {
360
+ method: 'PUT',
361
+ // Content-Length is set by fetch from the body (setting it manually
362
+ // throws on workerd). Content-Range is the required upload-session header.
363
+ headers: { 'Content-Range': `bytes ${offset}-${end - 1}/${total}` },
364
+ body: new Blob([chunk])
365
+ }, false);
366
+ if (!last.ok && last.status !== 202) {
367
+ throw await graphFailure(last);
368
+ }
369
+ offset = end;
370
+ if (offset < total)
371
+ await closeMicrosoftResponse(last);
372
+ }
373
+ if (!last) {
374
+ throw new Error('Graph resumable upload produced no response.');
375
+ }
376
+ // The final chunk's 200/201 response carries the completed DriveItem.
377
+ return this.readGraphResponse(last, GraphJsonSchema);
378
+ }
379
+ /**
380
+ * Create a folder named `name` under `parentPath` (drive-root-relative; empty
381
+ * for the root). `conflictBehavior` defaults to `fail` so existing folders
382
+ * are not clobbered.
383
+ */
384
+ async createFolder(siteId, parentPath, name, conflictBehavior = 'fail') {
385
+ const url = parentPath && parentPath.length > 0
386
+ ? `${this.driveRoot(siteId)}/root:/${encodeDrivePath(parentPath)}:/children`
387
+ : `${this.driveRoot(siteId)}/root/children`;
388
+ const raw = await this.executeWriteJson(url, 'POST', {
389
+ name,
390
+ folder: {},
391
+ '@microsoft.graph.conflictBehavior': conflictBehavior
392
+ });
393
+ const normalized = this.normalizeDriveItem(raw);
394
+ if (!normalized) {
395
+ throw new Error(`Graph createFolder '${name}' returned an unparseable drive item.`);
396
+ }
397
+ return normalized;
398
+ }
399
+ /** Delete a drive item by its id. */
400
+ async deleteItem(siteId, itemId) {
401
+ await this.executeDelete(`${this.driveRoot(siteId)}/items/${itemId}`);
402
+ }
403
+ /** Delete a drive item by its drive-root-relative path. */
404
+ async deletePath(siteId, path) {
405
+ await this.executeDelete(`${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`);
406
+ }
407
+ /** Fetch metadata for the item at `path`, or `null` if it does not exist. */
408
+ async headPath(siteId, path) {
409
+ const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}?$select=id,name,size,file,lastModifiedDateTime,webUrl`;
410
+ const accessToken = await this.acquireAccessToken();
411
+ const response = await this.fetchResponse(url, {
412
+ method: 'GET',
413
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }
414
+ });
415
+ if (response.status === 404) {
416
+ await closeMicrosoftResponse(response);
417
+ return null;
418
+ }
419
+ if (!response.ok) {
420
+ throw await graphFailure(response);
421
+ }
422
+ const item = await this.readGraphResponse(response, GraphDriveItemSchema);
423
+ return {
424
+ id: item.id, size: item.size, mimeType: item.file?.mimeType ?? '',
425
+ lastModified: item.lastModifiedDateTime ?? '', webUrl: item.webUrl
426
+ };
427
+ }
428
+ /** Stream the bytes of the item at `path`, or `null` if it does not exist. */
429
+ async getStream(siteId, path) {
430
+ const root = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`;
431
+ const metadata = await settle(() => this.executeRequest(root, GraphDownloadSchema));
432
+ if (!metadata.ok) {
433
+ if (metadata.cause instanceof MicrosoftGraphError && metadata.cause.status === 404)
434
+ return null;
435
+ throw metadata.cause;
436
+ }
437
+ const response = await this.downloadResponse(metadata.value, `${root}:/content`);
438
+ return response.body;
439
+ }
440
+ /** Metadata URLs and Graph's proxy fallback both leave credentials at Graph. */
441
+ async downloadResponse(metadata, contentUrl) {
442
+ let downloadUrl = metadata['@microsoft.graph.downloadUrl'];
443
+ let response;
444
+ if (downloadUrl) {
445
+ response = await this.fetchResponse(this.downloadTarget(downloadUrl, contentUrl), {
446
+ method: 'GET', headers: { Accept: 'application/octet-stream' }
447
+ }, false);
448
+ }
449
+ else {
450
+ const accessToken = await this.acquireAccessToken();
451
+ response = await this.fetchResponse(contentUrl, {
452
+ method: 'GET', redirect: 'manual',
453
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/octet-stream' }
454
+ });
455
+ const location = response.headers.get('location');
456
+ if ([301, 302, 303, 307, 308].includes(response.status) && location !== null) {
457
+ // Release Graph's body before allocating the separate credential-free request.
458
+ await closeMicrosoftResponse(response);
459
+ downloadUrl = this.downloadTarget(location, contentUrl);
460
+ response = await this.fetchResponse(downloadUrl, {
461
+ method: 'GET', headers: { Accept: 'application/octet-stream' }
462
+ }, false);
463
+ }
464
+ }
465
+ if (!response.ok)
466
+ throw await graphFailure(response);
467
+ return response;
468
+ }
469
+ downloadTarget(location, base) {
470
+ const target = settleNow(() => new URL(location, base));
471
+ if (!target.ok || target.value.protocol !== 'https:' || target.value.username || target.value.password) {
472
+ throw new CapabilityError('Microsoft Graph returned an invalid download URL.', 'INVALID_INPUT', {
473
+ reason: 'microsoft-graph-download-url-invalid'
474
+ });
475
+ }
476
+ return target.value.href;
477
+ }
478
+ // Shared write helpers
479
+ async executeWriteJson(url, method, jsonBody) {
480
+ const accessToken = await this.acquireAccessToken();
481
+ const response = await this.fetchResponse(url, {
482
+ method,
483
+ headers: {
484
+ Authorization: `Bearer ${accessToken}`,
485
+ 'Content-Type': 'application/json',
486
+ Accept: 'application/json'
487
+ },
488
+ body: JSON.stringify(jsonBody)
489
+ });
490
+ if (!response.ok) {
491
+ throw await graphFailure(response);
492
+ }
493
+ return this.readGraphResponse(response, GraphJsonSchema);
494
+ }
495
+ async executeDelete(url) {
496
+ const accessToken = await this.acquireAccessToken();
497
+ const response = await this.fetchResponse(url, {
498
+ method: 'DELETE',
499
+ headers: { Authorization: `Bearer ${accessToken}` }
500
+ });
501
+ if (!response.ok && response.status !== 204) {
502
+ throw await graphFailure(response);
503
+ }
504
+ await closeMicrosoftResponse(response);
505
+ }
506
+ /** Retrieve a token only for legacy direct callers; registered providers use Secrets. */
507
+ async acquireAccessToken() {
508
+ if (this.authenticatedFetch)
509
+ return '';
510
+ const integration = this.integration;
511
+ if (!integration) {
512
+ throw new CapabilityError('Microsoft Graph has no token acquirer.', 'UNAVAILABLE', { reason: 'microsoft-graph-auth-transport-required' });
513
+ }
514
+ const scopes = ['https://graph.microsoft.com/.default'];
515
+ const result = await integration.acquireToken(scopes);
516
+ return result.accessToken;
517
+ }
518
+ /**
519
+ * Graph drive base for the storage drive identifier. The sentinel
520
+ * {@link ONEDRIVE_SITE_ID} (`'me'`) targets the signed-in user's personal
521
+ * OneDrive (`/me/drive`, delegated only). Composite SharePoint site ids keep
522
+ * using `/sites/{id}/drive` for backwards compatibility; real Graph drive ids
523
+ * use `/drives/{id}`.
524
+ */
525
+ driveRoot(siteOrDriveId) {
526
+ if (siteOrDriveId === ONEDRIVE_SITE_ID)
527
+ return `${GRAPH_API_BASE}/me/drive`;
528
+ if (looksLikeGraphDriveId(siteOrDriveId)) {
529
+ return `${GRAPH_API_BASE}/drives/${siteOrDriveId}`;
530
+ }
531
+ return `${GRAPH_API_BASE}/sites/${siteOrDriveId}/drive`;
532
+ }
533
+ /**
534
+ * Execute a JSON-bearing Graph API request with auth header.
535
+ */
536
+ async executeRequest(url, schema) {
537
+ // Pagination URLs carry our bearer: only the owning Graph origin is admitted.
538
+ const target = new URL(url);
539
+ if (target.origin !== 'https://graph.microsoft.com')
540
+ throw new CapabilityError('Invalid Microsoft Graph continuation origin.', 'INVALID_INPUT', { reason: 'microsoft-graph-origin-invalid' });
541
+ const accessToken = await this.acquireAccessToken();
542
+ const response = await this.fetchResponse(target.href, {
543
+ method: 'GET', redirect: 'error',
544
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }
545
+ });
546
+ if (!response.ok)
547
+ throw await graphFailure(response);
548
+ return this.readGraphResponse(response, schema);
549
+ }
550
+ /** All Graph transports preserve typed, value-safe failures. */
551
+ async fetchResponse(url, init, authenticated = true) {
552
+ const fetcher = authenticated ? this.authenticatedFetch ?? this.fetchFn : this.fetchFn;
553
+ const headers = new Headers(init.headers);
554
+ if (authenticated && this.authenticatedFetch)
555
+ headers.delete('authorization');
556
+ const response = await settle(() => fetcher(url, { ...init, headers }));
557
+ if (!response.ok)
558
+ throw microsoftFailure(response.cause);
559
+ return response.value;
560
+ }
561
+ admitGraphResponse(schema, value) {
562
+ const parsed = schema.safeParse(value);
563
+ if (parsed.success)
564
+ return parsed.data;
565
+ throw new CapabilityError('Microsoft Graph returned an invalid response.', 'UNAVAILABLE', {
566
+ reason: 'microsoft-graph-response-invalid', cause: parsed.error
567
+ });
568
+ }
569
+ async readGraphResponse(response, schema) {
570
+ const decoded = await settle(() => response.json());
571
+ if (!decoded.ok && decoded.cause instanceof CapabilityError)
572
+ throw decoded.cause;
573
+ if (!decoded.ok)
574
+ throw new CapabilityError('Microsoft Graph returned invalid JSON.', 'UNAVAILABLE', {
575
+ reason: 'microsoft-graph-response-invalid', cause: microsoftFailure(decoded.cause)
576
+ });
577
+ return this.admitGraphResponse(schema, decoded.value);
578
+ }
579
+ /**
580
+ * Follow @odata.nextLink for paginated responses.
581
+ */
582
+ async fetchAllPages(initialItems, nextLink) {
583
+ const allItems = [...initialItems];
584
+ let link = nextLink;
585
+ while (link) {
586
+ const response = await this.executeRequest(link, GraphJsonCollectionSchema);
587
+ allItems.push(...response.value);
588
+ link = response['@odata.nextLink'];
589
+ }
590
+ return allItems;
591
+ }
592
+ /**
593
+ * Normalize a raw Graph DriveItem into our simplified DriveItemEntry.
594
+ */
595
+ normalizeDriveItem(item) {
596
+ const parsed = GraphDriveItemSchema.safeParse(item);
597
+ if (!parsed.success)
598
+ throw new CapabilityError('Microsoft Graph returned a malformed resource.', 'UNAVAILABLE', { reason: 'microsoft-graph-response-invalid', cause: parsed.error });
599
+ const driveItem = parsed.data;
600
+ return DriveItemEntrySchema.parse({
601
+ id: driveItem.id,
602
+ name: driveItem.name,
603
+ size: driveItem.size,
604
+ lastModified: driveItem.lastModifiedDateTime ?? '',
605
+ mimeType: driveItem.file?.mimeType ?? '',
606
+ isFolder: driveItem.folder !== undefined,
607
+ webUrl: driveItem.webUrl
608
+ });
609
+ }
610
+ /**
611
+ * Normalize a raw Graph search-hit DriveItem into a DriveItemSearchResult,
612
+ * deriving the drive-root-relative `parentPath` from `parentReference.path`.
613
+ *
614
+ * Graph's `parentReference.path` looks like `/drive/root:/Projects to Bid/2026`
615
+ * (URL-encoded segments). We strip the `/drive/root:` (or `/drives/{id}/root:`)
616
+ * prefix and decode each segment so consumers get `Projects to Bid/2026`.
617
+ */
618
+ normalizeSearchResult(item, siteId) {
619
+ const base = this.normalizeDriveItem(item);
620
+ if (!base)
621
+ return null;
622
+ const resource = admitMicrosoft(GraphDriveItemSchema, item);
623
+ const rawPath = resource.parentReference?.path ?? '';
624
+ const parentId = resource.parentReference?.id ?? '';
625
+ // `/drive/root:/A/B` or `/drives/{id}/root:/A/B` → `A/B`. No `root:` → ''.
626
+ const afterRoot = rawPath.includes('root:') ? rawPath.split('root:')[1] ?? '' : '';
627
+ const parentPath = afterRoot
628
+ .split('/')
629
+ .filter((seg) => seg.length > 0)
630
+ .map((seg) => {
631
+ try {
632
+ return decodeURIComponent(seg);
633
+ }
634
+ catch (error) {
635
+ throw new CapabilityError('Microsoft Graph returned an invalid encoded path.', 'UNAVAILABLE', {
636
+ reason: 'microsoft-graph-response-invalid', cause: microsoftFailure(error)
637
+ });
638
+ }
639
+ })
640
+ .join('/');
641
+ return DriveItemSearchResultSchema.parse({ ...base, parentPath, parentId, siteId });
642
+ }
643
+ }
644
+ //# sourceMappingURL=graph-sharepoint-client.js.map