@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.
- package/LICENSE +126 -0
- package/README.md +120 -2
- package/dist/api-client-plugin.d.ts +3 -0
- package/dist/api-client-plugin.d.ts.map +1 -0
- package/dist/api-client-plugin.js +11 -0
- package/dist/api-client-plugin.js.map +1 -0
- package/dist/client/helper.d.ts +17 -0
- package/dist/client/helper.d.ts.map +1 -0
- package/dist/client/helper.js +18 -0
- package/dist/client/helper.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/runtime-client.d.ts +87 -0
- package/dist/client/runtime-client.d.ts.map +1 -0
- package/dist/client/runtime-client.js +47 -0
- package/dist/client/runtime-client.js.map +1 -0
- package/dist/cloud.plugin.d.ts +3 -0
- package/dist/cloud.plugin.d.ts.map +1 -0
- package/dist/cloud.plugin.js +19 -0
- package/dist/cloud.plugin.js.map +1 -0
- package/dist/email/index.d.ts +13 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +13 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/microsoft/index.d.ts +4 -0
- package/dist/email/microsoft/index.d.ts.map +1 -0
- package/dist/email/microsoft/index.js +4 -0
- package/dist/email/microsoft/index.js.map +1 -0
- package/dist/email/microsoft/microsoft.descriptor.d.ts +85 -0
- package/dist/email/microsoft/microsoft.descriptor.d.ts.map +1 -0
- package/dist/email/microsoft/microsoft.descriptor.js +79 -0
- package/dist/email/microsoft/microsoft.descriptor.js.map +1 -0
- package/dist/email/microsoft/microsoft.impl.d.ts +100 -0
- package/dist/email/microsoft/microsoft.impl.d.ts.map +1 -0
- package/dist/email/microsoft/microsoft.impl.js +323 -0
- package/dist/email/microsoft/microsoft.impl.js.map +1 -0
- package/dist/email/microsoft/outlook.mime.d.ts +18 -0
- package/dist/email/microsoft/outlook.mime.d.ts.map +1 -0
- package/dist/email/microsoft/outlook.mime.js +61 -0
- package/dist/email/microsoft/outlook.mime.js.map +1 -0
- package/dist/email/microsoft-email.plugin.d.ts +10 -0
- package/dist/email/microsoft-email.plugin.d.ts.map +1 -0
- package/dist/email/microsoft-email.plugin.js +29 -0
- package/dist/email/microsoft-email.plugin.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations-microsoft.plugin.d.ts +4 -0
- package/dist/integrations-microsoft.plugin.d.ts.map +1 -0
- package/dist/integrations-microsoft.plugin.js +15 -0
- package/dist/integrations-microsoft.plugin.js.map +1 -0
- package/dist/microsoft/graph-drive-fallback.d.ts +11 -0
- package/dist/microsoft/graph-drive-fallback.d.ts.map +1 -0
- package/dist/microsoft/graph-drive-fallback.js +90 -0
- package/dist/microsoft/graph-drive-fallback.js.map +1 -0
- package/dist/microsoft/graph-schemas.d.ts +2 -0
- package/dist/microsoft/graph-schemas.d.ts.map +1 -0
- package/dist/microsoft/graph-schemas.js +2 -0
- package/dist/microsoft/graph-schemas.js.map +1 -0
- package/dist/microsoft/graph-sharepoint-client.d.ts +219 -0
- package/dist/microsoft/graph-sharepoint-client.d.ts.map +1 -0
- package/dist/microsoft/graph-sharepoint-client.js +644 -0
- package/dist/microsoft/graph-sharepoint-client.js.map +1 -0
- package/dist/microsoft/index.d.ts +8 -0
- package/dist/microsoft/index.d.ts.map +1 -0
- package/dist/microsoft/index.js +8 -0
- package/dist/microsoft/index.js.map +1 -0
- package/dist/microsoft/microsoft-delegated-implementation.d.ts +20 -0
- package/dist/microsoft/microsoft-delegated-implementation.d.ts.map +1 -0
- package/dist/microsoft/microsoft-delegated-implementation.js +69 -0
- package/dist/microsoft/microsoft-delegated-implementation.js.map +1 -0
- package/dist/microsoft/microsoft-implementation.d.ts +22 -0
- package/dist/microsoft/microsoft-implementation.d.ts.map +1 -0
- package/dist/microsoft/microsoft-implementation.js +78 -0
- package/dist/microsoft/microsoft-implementation.js.map +1 -0
- package/dist/microsoft/secrets-fetch.d.ts +20 -0
- package/dist/microsoft/secrets-fetch.d.ts.map +1 -0
- package/dist/microsoft/secrets-fetch.js +47 -0
- package/dist/microsoft/secrets-fetch.js.map +1 -0
- package/dist/microsoft/sharepoint-operations.d.ts +23 -0
- package/dist/microsoft/sharepoint-operations.d.ts.map +1 -0
- package/dist/microsoft/sharepoint-operations.js +123 -0
- package/dist/microsoft/sharepoint-operations.js.map +1 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts +85 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts.map +1 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js +109 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js.map +1 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.d.ts +69 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.d.ts.map +1 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.js +114 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.js.map +1 -0
- package/dist/microsoft.consumer.plugin.d.ts +4 -0
- package/dist/microsoft.consumer.plugin.d.ts.map +1 -0
- package/dist/microsoft.consumer.plugin.js +13 -0
- package/dist/microsoft.consumer.plugin.js.map +1 -0
- package/dist/schemas/boundary.d.ts +69 -0
- package/dist/schemas/boundary.d.ts.map +1 -0
- package/dist/schemas/boundary.js +94 -0
- package/dist/schemas/boundary.js.map +1 -0
- package/dist/schemas/graph.d.ts +139 -0
- package/dist/schemas/graph.d.ts.map +1 -0
- package/dist/schemas/graph.js +161 -0
- package/dist/schemas/graph.js.map +1 -0
- package/dist/schemas/oauth.d.ts +131 -0
- package/dist/schemas/oauth.d.ts.map +1 -0
- package/dist/schemas/oauth.js +27 -0
- package/dist/schemas/oauth.js.map +1 -0
- package/dist/schemas/operations.d.ts +239 -0
- package/dist/schemas/operations.d.ts.map +1 -0
- package/dist/schemas/operations.js +29 -0
- package/dist/schemas/operations.js.map +1 -0
- package/dist/schemas/outlook.d.ts +241 -0
- package/dist/schemas/outlook.d.ts.map +1 -0
- package/dist/schemas/outlook.js +29 -0
- package/dist/schemas/outlook.js.map +1 -0
- package/dist/schemas/sharepoint.d.ts +67 -0
- package/dist/schemas/sharepoint.d.ts.map +1 -0
- package/dist/schemas/sharepoint.js +71 -0
- package/dist/schemas/sharepoint.js.map +1 -0
- package/dist/server/oauth.d.ts +9 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +67 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/sharepoint/index.d.ts +11 -0
- package/dist/sharepoint/index.d.ts.map +1 -0
- package/dist/sharepoint/index.js +11 -0
- package/dist/sharepoint/index.js.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts +10 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.js +32 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.js.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.impl.d.ts +195 -0
- package/dist/sharepoint/sharepoint.adapter.impl.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.impl.js +645 -0
- package/dist/sharepoint/sharepoint.adapter.impl.js.map +1 -0
- package/dist/sharepoint/sharepoint.schemas.d.ts +2 -0
- package/dist/sharepoint/sharepoint.schemas.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.schemas.js +2 -0
- package/dist/sharepoint/sharepoint.schemas.js.map +1 -0
- package/package.json +146 -4
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharePoint Storage Adapter Implementation
|
|
3
|
+
*
|
|
4
|
+
* Wraps the GraphSharePointClient in the StorageAdapterInterface.
|
|
5
|
+
* Provides document protocol operations: list, get, put, delete
|
|
6
|
+
* against SharePoint drives via Microsoft Graph API.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* const adapter = await runtime.query('storage/adapter')
|
|
11
|
+
* .withId('sharepoint').resolve();
|
|
12
|
+
* const store = adapter.documentStore(containerId, schema);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { CapabilityError, CaughtFailureSchema, settle, valueFreeFailure, createRuntimeImplementation } from '@vibesdotdev/kernel';
|
|
16
|
+
import { admitMicrosoft, MicrosoftGraphError, microsoftFailure } from "../schemas/boundary.js";
|
|
17
|
+
import { IntegrationProviderResolvedConfigurationSchema } from '@vibesdotdev/integrations/schemas/integration-descriptor';
|
|
18
|
+
import { createMicrosoftSecretsFetch, microsoftAppCredentialBinding, microsoftDelegatedCredentialBinding } from "../microsoft/secrets-fetch.js";
|
|
19
|
+
import { MicrosoftConfigSchema } from "../schemas/boundary.js";
|
|
20
|
+
import { SharePointAdapterOverrideSchema, SharePointFolderManifestSchema } from "../schemas/sharepoint.js";
|
|
21
|
+
import { SharePointAdapterConfigSchema } from "./sharepoint.schemas.js";
|
|
22
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
23
|
+
import { z } from 'zod/v4';
|
|
24
|
+
import { GraphSharePointClient, ONEDRIVE_SITE_ID } from "../microsoft/graph-sharepoint-client.js";
|
|
25
|
+
import { browseDriveWithPathFallback, searchDriveWithPathFallback } from "../microsoft/graph-drive-fallback.js";
|
|
26
|
+
import { SharePointDocumentRecordSchema, SharePointUploadMetadataSchema } from "./sharepoint.schemas.js";
|
|
27
|
+
function describeError(error) {
|
|
28
|
+
const failure = microsoftFailure(error);
|
|
29
|
+
return `${failure.code}: ${failure.reason}`;
|
|
30
|
+
}
|
|
31
|
+
function logWarn(runtime, message, meta) {
|
|
32
|
+
if (!runtime)
|
|
33
|
+
throw new CapabilityError('Partial Microsoft results require a runtime observer.', 'UNAVAILABLE', { reason: 'integration-runtime-required' });
|
|
34
|
+
getLogger('integrations-microsoft', runtime).child('sharepoint').warn(message, meta);
|
|
35
|
+
}
|
|
36
|
+
// SharePoint Document Store
|
|
37
|
+
/**
|
|
38
|
+
* Document store backed by a SharePoint drive folder.
|
|
39
|
+
*
|
|
40
|
+
* The "containerId" is the SharePoint site ID.
|
|
41
|
+
* The store provides list/get/put/delete operations
|
|
42
|
+
* mapped to Graph API drive item endpoints.
|
|
43
|
+
*/
|
|
44
|
+
class SharePointDocumentStore {
|
|
45
|
+
schema;
|
|
46
|
+
client;
|
|
47
|
+
siteId;
|
|
48
|
+
folderPath;
|
|
49
|
+
runtime;
|
|
50
|
+
constructor(client, siteId, folderPath, runtime, schema) {
|
|
51
|
+
this.schema = schema;
|
|
52
|
+
this.client = client;
|
|
53
|
+
this.siteId = siteId;
|
|
54
|
+
this.folderPath = folderPath;
|
|
55
|
+
this.runtime = runtime;
|
|
56
|
+
}
|
|
57
|
+
async load() {
|
|
58
|
+
const items = await this.client.browseDrive(this.siteId, this.folderPath);
|
|
59
|
+
return admitMicrosoft(this.schema, items
|
|
60
|
+
.filter((item) => !item.isFolder)
|
|
61
|
+
.map((item) => ({
|
|
62
|
+
id: item.id,
|
|
63
|
+
name: item.name,
|
|
64
|
+
size: item.size,
|
|
65
|
+
modifiedAt: item.lastModified,
|
|
66
|
+
mimeType: item.mimeType,
|
|
67
|
+
webUrl: item.webUrl,
|
|
68
|
+
containerId: this.siteId
|
|
69
|
+
})));
|
|
70
|
+
}
|
|
71
|
+
async save(_data) {
|
|
72
|
+
throw new Error('SharePoint document store does not support bulk save. Use put() for individual uploads.');
|
|
73
|
+
}
|
|
74
|
+
async exists() {
|
|
75
|
+
const attempt = await settle(() => this.client.browseDrive(this.siteId, this.folderPath));
|
|
76
|
+
if (attempt.ok)
|
|
77
|
+
return true;
|
|
78
|
+
if (attempt.cause instanceof MicrosoftGraphError && attempt.cause.status === 404)
|
|
79
|
+
return false;
|
|
80
|
+
throw attempt.cause;
|
|
81
|
+
}
|
|
82
|
+
async delete() {
|
|
83
|
+
throw new Error('SharePoint document store does not support bulk delete. Use deleteItem() on the adapter.');
|
|
84
|
+
}
|
|
85
|
+
getPath() {
|
|
86
|
+
const folder = this.folderPath ? `/${this.folderPath}` : '';
|
|
87
|
+
return `sharepoint://${this.siteId}${folder}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// SharePoint Object Store (object protocol — binary blobs)
|
|
91
|
+
/**
|
|
92
|
+
* Key-addressed object store backed by a SharePoint site drive, scoped to a
|
|
93
|
+
* base folder. Keys are drive-relative paths under `basePath`. URLs prefer
|
|
94
|
+
* temporary signed Graph URLs and fall back to proxy streaming via `get()`.
|
|
95
|
+
*/
|
|
96
|
+
class SharePointObjectStore {
|
|
97
|
+
resolveClient;
|
|
98
|
+
siteId;
|
|
99
|
+
basePath;
|
|
100
|
+
runtime;
|
|
101
|
+
constructor(resolveClient, siteId, basePath, runtime) {
|
|
102
|
+
this.resolveClient = resolveClient;
|
|
103
|
+
this.siteId = siteId;
|
|
104
|
+
this.basePath = basePath;
|
|
105
|
+
this.runtime = runtime;
|
|
106
|
+
}
|
|
107
|
+
/** Map an opaque object key to a drive-relative path under `basePath`. */
|
|
108
|
+
fullPath(key) {
|
|
109
|
+
const base = this.basePath.replace(/^\/+|\/+$/g, '');
|
|
110
|
+
const k = key.replace(/^\/+/, '');
|
|
111
|
+
return base ? `${base}/${k}` : k;
|
|
112
|
+
}
|
|
113
|
+
async put(key, body, opts) {
|
|
114
|
+
const client = await this.resolveClient();
|
|
115
|
+
const item = await client.uploadFile(this.siteId, this.fullPath(key), body, {
|
|
116
|
+
contentType: opts?.contentType,
|
|
117
|
+
conflictBehavior: 'replace'
|
|
118
|
+
});
|
|
119
|
+
return { key, size: item.size, contentType: item.mimeType || opts?.contentType };
|
|
120
|
+
}
|
|
121
|
+
async get(key) {
|
|
122
|
+
const client = await this.resolveClient();
|
|
123
|
+
return client.getStream(this.siteId, this.fullPath(key));
|
|
124
|
+
}
|
|
125
|
+
async head(key) {
|
|
126
|
+
const client = await this.resolveClient();
|
|
127
|
+
const meta = await client.headPath(this.siteId, this.fullPath(key));
|
|
128
|
+
if (!meta)
|
|
129
|
+
return null;
|
|
130
|
+
return {
|
|
131
|
+
key,
|
|
132
|
+
size: meta.size,
|
|
133
|
+
contentType: meta.mimeType || undefined,
|
|
134
|
+
lastModified: meta.lastModified || undefined
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async delete(key) {
|
|
138
|
+
const client = await this.resolveClient();
|
|
139
|
+
await client.deletePath(this.siteId, this.fullPath(key));
|
|
140
|
+
}
|
|
141
|
+
async list(prefix = '', _opts) {
|
|
142
|
+
const client = await this.resolveClient();
|
|
143
|
+
const prefixRel = prefix.replace(/^\/+|\/+$/g, '');
|
|
144
|
+
const folder = this.fullPath(prefixRel);
|
|
145
|
+
const entries = await client.browseDrive(this.siteId, folder || undefined);
|
|
146
|
+
const objects = entries
|
|
147
|
+
.filter((entry) => !entry.isFolder)
|
|
148
|
+
.map((entry) => ({
|
|
149
|
+
key: prefixRel ? `${prefixRel}/${entry.name}` : entry.name,
|
|
150
|
+
size: entry.size,
|
|
151
|
+
lastModified: entry.lastModified || undefined
|
|
152
|
+
}));
|
|
153
|
+
// Shallow listing (immediate children), matching browseDrive.
|
|
154
|
+
return { objects, hasMore: false };
|
|
155
|
+
}
|
|
156
|
+
async url(key, _opts) {
|
|
157
|
+
// Prefer Graph's pre-authenticated download URL — a short-lived Azure Blob
|
|
158
|
+
// SAS that needs no auth header — so the browser can <img>/<iframe>/download
|
|
159
|
+
// it directly, cross-origin. This is what makes SharePoint/OneDrive file
|
|
160
|
+
// preview + download work from the explorer (incl. the cross-app casework
|
|
161
|
+
// host, where a same-origin proxy couldn't carry the platform session).
|
|
162
|
+
// Fall back to proxy serving (get()) only when Graph returns no URL.
|
|
163
|
+
const client = await this.resolveClient();
|
|
164
|
+
const signed = await client.getDownloadUrlByPath(this.siteId, this.fullPath(key));
|
|
165
|
+
if (signed)
|
|
166
|
+
return { kind: 'signed', url: signed };
|
|
167
|
+
return { kind: 'proxy', url: this.fullPath(key) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// SharePoint Storage Adapter
|
|
171
|
+
/**
|
|
172
|
+
* Storage adapter wrapping Microsoft Graph SharePoint client.
|
|
173
|
+
*
|
|
174
|
+
* Supports the document protocol for browsing, uploading,
|
|
175
|
+
* downloading, and deleting documents in SharePoint drives.
|
|
176
|
+
*
|
|
177
|
+
* The adapter requires a GraphSharePointClient instance
|
|
178
|
+
* which supplies its runtime-owned authenticated transport.
|
|
179
|
+
*/
|
|
180
|
+
export class SharePointStorageAdapter {
|
|
181
|
+
id;
|
|
182
|
+
capabilities;
|
|
183
|
+
config = {};
|
|
184
|
+
client = null;
|
|
185
|
+
constructor(descriptor) {
|
|
186
|
+
this.id = descriptor.id;
|
|
187
|
+
this.capabilities = {
|
|
188
|
+
protocols: descriptor.protocols ?? ['document'],
|
|
189
|
+
capabilities: descriptor.capabilities ?? ['list', 'read', 'write', 'browse', 'search'],
|
|
190
|
+
requiresPairSchema: descriptor.requiresPairSchema ?? false,
|
|
191
|
+
supportsTransactions: descriptor.supportsTransactions ?? false,
|
|
192
|
+
supportsTTL: false,
|
|
193
|
+
supportsFTS: false,
|
|
194
|
+
persistent: descriptor.persistent ?? true,
|
|
195
|
+
supportsSubscription: false,
|
|
196
|
+
supportsHierarchy: descriptor.supportsHierarchy ?? true,
|
|
197
|
+
supportsComputedFields: false,
|
|
198
|
+
supportsSideEffects: false,
|
|
199
|
+
supportsRawQueries: false
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
configure(config) {
|
|
203
|
+
this.config = { ...admitMicrosoft(SharePointAdapterConfigSchema, config), runtime: config.runtime };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Set the GraphSharePointClient instance.
|
|
207
|
+
* Must be called before using the adapter.
|
|
208
|
+
*/
|
|
209
|
+
setClient(client) {
|
|
210
|
+
this.client = client;
|
|
211
|
+
}
|
|
212
|
+
isConfigured() {
|
|
213
|
+
return this.client !== null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Ensure the GraphSharePointClient is available.
|
|
217
|
+
*/
|
|
218
|
+
ensureClient() {
|
|
219
|
+
if (!this.client) {
|
|
220
|
+
throw new Error('SharePoint storage adapter is not configured. Call setClient() with a GraphSharePointClient instance.');
|
|
221
|
+
}
|
|
222
|
+
return this.client;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the GraphSharePointClient for a browse call.
|
|
226
|
+
*
|
|
227
|
+
* Prefers an explicitly-injected client (`setClient()`). Otherwise lazily
|
|
228
|
+
* resolves the delegated Microsoft integration provider from the runtime and
|
|
229
|
+
* builds a request-scoped reference-only client from it. Inside a request context
|
|
230
|
+
* (e.g. the platform-web `/api/storage/adapters/sharepoint/*` route wraps the
|
|
231
|
+
* call in `runWithRequestContext`) it binds to the calling user's delegated
|
|
232
|
+
* Graph token. This mirrors the integration-ops dispatch path so browse works
|
|
233
|
+
* without the singleton needing a pre-set client.
|
|
234
|
+
*/
|
|
235
|
+
async resolveBrowseClient(integrationId, runtime = this.config.runtime) {
|
|
236
|
+
if (this.client)
|
|
237
|
+
return this.client;
|
|
238
|
+
// Default to the app-only org integration (platform owns the org's
|
|
239
|
+
// SharePoint via its app creds). Per-user delegated access is opt-in via
|
|
240
|
+
// `integrationId = 'integration.microsoft.delegated'`. The BROWSE path sets
|
|
241
|
+
// it on `this.config` via configure(); the OBJECT path can't mutate the
|
|
242
|
+
// shared `this.config` (it would race across concurrent scopes), so it
|
|
243
|
+
// threads the id explicitly here — without it, uploads/reads to a delegated
|
|
244
|
+
// drive would fall back to app-only and Graph rejects `/me` ("only valid
|
|
245
|
+
// with delegated authentication flow").
|
|
246
|
+
const resolvedId = integrationId ??
|
|
247
|
+
this.config.integrationId ??
|
|
248
|
+
'integration.microsoft';
|
|
249
|
+
if (!runtime) {
|
|
250
|
+
throw new CapabilityError('SharePoint storage adapter needs the runtime it is configured on to resolve a Microsoft credential.', 'UNAVAILABLE', { reason: 'runtime-unavailable', retryable: false });
|
|
251
|
+
}
|
|
252
|
+
const acquirer = await runtime
|
|
253
|
+
.query('integration/provider')
|
|
254
|
+
.withId(resolvedId)
|
|
255
|
+
.resolve()
|
|
256
|
+
.catch((error) => {
|
|
257
|
+
// Only confirmed absence of this provider means "no credential"; any other failure propagates.
|
|
258
|
+
if (error instanceof Error && CapabilityError.isAbsenceOf(error, 'integration/provider', resolvedId))
|
|
259
|
+
return null;
|
|
260
|
+
throw error;
|
|
261
|
+
});
|
|
262
|
+
if (!acquirer)
|
|
263
|
+
throw new CapabilityError('Microsoft provider was not found.', 'NOT_FOUND', { reason: 'microsoft-provider-not-found' });
|
|
264
|
+
if (!acquirer.getCredentialUseConfiguration) {
|
|
265
|
+
throw new CapabilityError('Microsoft provider reference delivery is unavailable.', 'UNAVAILABLE', { reason: 'microsoft-credential-references-required' });
|
|
266
|
+
}
|
|
267
|
+
const readConfiguration = acquirer.getCredentialUseConfiguration.bind(acquirer);
|
|
268
|
+
// Re-read live references for every request; this client never caches a
|
|
269
|
+
// calling user's connection or a tenant's credential.
|
|
270
|
+
const authenticatedFetch = async (input, init) => {
|
|
271
|
+
const resolved = admitMicrosoft(IntegrationProviderResolvedConfigurationSchema, await readConfiguration());
|
|
272
|
+
const credentials = resolved.credentialReferences;
|
|
273
|
+
const delegated = credentials.accessToken;
|
|
274
|
+
let binding;
|
|
275
|
+
if (delegated?.length)
|
|
276
|
+
binding = microsoftDelegatedCredentialBinding(delegated);
|
|
277
|
+
else {
|
|
278
|
+
const clientId = credentials.clientId ?? [];
|
|
279
|
+
const clientSecret = credentials.clientSecret ?? [];
|
|
280
|
+
const tenantId = credentials.tenantId ?? [];
|
|
281
|
+
const config = admitMicrosoft(MicrosoftConfigSchema, resolved.config);
|
|
282
|
+
binding = microsoftAppCredentialBinding({ clientId, clientSecret, tenantId, tenant: config.tenantId || undefined, scopes: config.scopes ?? ['https://graph.microsoft.com/.default'] });
|
|
283
|
+
}
|
|
284
|
+
return createMicrosoftSecretsFetch(runtime, binding)(input, init);
|
|
285
|
+
};
|
|
286
|
+
return new GraphSharePointClient({ authenticatedFetch, fetchFn: createMicrosoftSecretsFetch(runtime) });
|
|
287
|
+
}
|
|
288
|
+
// Document Store
|
|
289
|
+
/**
|
|
290
|
+
* Get a document store for a SharePoint container.
|
|
291
|
+
*
|
|
292
|
+
* @param namespace - The SharePoint site ID (container ID)
|
|
293
|
+
* @param schema - Zod schema (validated on load)
|
|
294
|
+
* @param manifest - Optional folder path string for subfolder browsing
|
|
295
|
+
*/
|
|
296
|
+
documentStore(namespace, schema, manifest) {
|
|
297
|
+
const client = this.ensureClient();
|
|
298
|
+
const folderPath = admitMicrosoft(SharePointFolderManifestSchema, manifest);
|
|
299
|
+
const siteId = this.config.siteId ?? namespace;
|
|
300
|
+
return new SharePointDocumentStore(client, siteId, folderPath, this.config.runtime, schema);
|
|
301
|
+
}
|
|
302
|
+
// Object Store (object protocol — binary blobs)
|
|
303
|
+
/**
|
|
304
|
+
* Get a key-addressed object store scoped to a SharePoint site + base folder.
|
|
305
|
+
* Keys are drive-relative paths under `basePath`. The Graph client is resolved
|
|
306
|
+
* lazily per call — the injected client (`setClient`) when present, else the
|
|
307
|
+
* configured Microsoft integration in the current request context (app-only
|
|
308
|
+
* or delegated, per `config.integrationId`).
|
|
309
|
+
*
|
|
310
|
+
* @param namespace Optional site id (falls back to `config.siteId`).
|
|
311
|
+
* @param config Optional `{ siteId?, basePath? }` overrides.
|
|
312
|
+
*/
|
|
313
|
+
objectStore(namespace, config) {
|
|
314
|
+
const overrides = admitMicrosoft(SharePointAdapterOverrideSchema, config ?? {});
|
|
315
|
+
const siteId = overrides.siteId ?? this.config.siteId ?? namespace ?? '';
|
|
316
|
+
const basePath = overrides.basePath ??
|
|
317
|
+
this.config.basePath ??
|
|
318
|
+
'';
|
|
319
|
+
// Object ops receive their config PER CALL (the resolved adapter is shared,
|
|
320
|
+
// so configure() would race across scopes) — thread the integration id into
|
|
321
|
+
// the lazily-built client so a delegated drive uses the user's token, not
|
|
322
|
+
// app-only (Graph rejects `/me` under app-only auth).
|
|
323
|
+
const integrationId = overrides.integrationId ??
|
|
324
|
+
this.config.integrationId;
|
|
325
|
+
const runtime = (config?.runtime ?? this.config.runtime);
|
|
326
|
+
return new SharePointObjectStore(() => this.resolveBrowseClient(integrationId, config?.runtime ?? this.config.runtime), siteId, basePath, runtime);
|
|
327
|
+
}
|
|
328
|
+
// Browse (canonical `storage/adapter` browse interface)
|
|
329
|
+
/**
|
|
330
|
+
* Enumerate the SharePoint sites this credential can reach.
|
|
331
|
+
*
|
|
332
|
+
* Each site is surfaced as a {@link StorageDrive} with `kind: 'site'`.
|
|
333
|
+
* Consumers use the returned `id` as `driveId` for {@link list}.
|
|
334
|
+
*/
|
|
335
|
+
async listDrives() {
|
|
336
|
+
const client = await this.resolveBrowseClient();
|
|
337
|
+
const integrationId = this.config.integrationId ?? 'integration.microsoft';
|
|
338
|
+
const isDelegated = integrationId === 'integration.microsoft.delegated';
|
|
339
|
+
const drives = [];
|
|
340
|
+
// Delegated (per-user) → the signed-in user's personal OneDrive. App-only
|
|
341
|
+
// tokens have no `/me`, so OneDrive is delegated-only.
|
|
342
|
+
if (isDelegated) {
|
|
343
|
+
drives.push({ id: ONEDRIVE_SITE_ID, name: 'OneDrive', kind: 'drive' });
|
|
344
|
+
}
|
|
345
|
+
// SharePoint sites the credential can reach. Surface each site's default
|
|
346
|
+
// document library as the storage drive id; callers can pass that stable
|
|
347
|
+
// Graph drive id back to browse/search without treating site ids as drives.
|
|
348
|
+
// Sites that enumerate but whose default drive is missing/inaccessible are
|
|
349
|
+
// skipped unless every app-only site fails, which indicates a real config
|
|
350
|
+
// problem rather than stale delegated visibility.
|
|
351
|
+
const sites = await settle(() => client.listSites());
|
|
352
|
+
if (!sites.ok) {
|
|
353
|
+
if (!isDelegated)
|
|
354
|
+
throw sites.cause;
|
|
355
|
+
logWarn(this.config.runtime, 'SharePoint enumeration failed; OneDrive remains available', { failure: valueFreeFailure(sites.cause) });
|
|
356
|
+
return drives;
|
|
357
|
+
}
|
|
358
|
+
const failures = [];
|
|
359
|
+
for (const site of sites.value) {
|
|
360
|
+
const result = await settle(() => client.getDefaultDrive(site.id));
|
|
361
|
+
if (!result.ok) {
|
|
362
|
+
failures.push(result.cause);
|
|
363
|
+
logWarn(this.config.runtime, 'SharePoint default drive unavailable', { failure: valueFreeFailure(result.cause) });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const drive = result.value;
|
|
367
|
+
const name = drive.name && drive.name !== site.name ? `${site.name} / ${drive.name}` : site.name;
|
|
368
|
+
drives.push({ id: drive.id, name, kind: 'site' });
|
|
369
|
+
}
|
|
370
|
+
if (!isDelegated && sites.value.length > 0 && failures.length === sites.value.length) {
|
|
371
|
+
throw new CapabilityError('All SharePoint default drives failed.', 'UNAVAILABLE', {
|
|
372
|
+
reason: 'microsoft-drives-unavailable', cause: new AggregateError(failures), details: { failedCount: failures.length }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return drives;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Enumerate files and folders inside a SharePoint site's default drive.
|
|
379
|
+
*
|
|
380
|
+
* @param driveId - SharePoint site id (from {@link listDrives}).
|
|
381
|
+
* @param path - Path relative to the drive root. Defaults to the root
|
|
382
|
+
* when omitted or empty.
|
|
383
|
+
* @returns Mixed `file` and `directory` entries at that path.
|
|
384
|
+
*/
|
|
385
|
+
async list(driveId, path) {
|
|
386
|
+
const client = await this.resolveBrowseClient();
|
|
387
|
+
const siteId = this.config.siteId ?? driveId;
|
|
388
|
+
const folderPath = path && path.length > 0 ? path : undefined;
|
|
389
|
+
const items = await browseDriveWithPathFallback(client, siteId, folderPath);
|
|
390
|
+
const prefix = folderPath ? `${folderPath.replace(/\/+$/, '')}/` : '';
|
|
391
|
+
return items.map((item) => ({
|
|
392
|
+
id: item.id,
|
|
393
|
+
name: item.name,
|
|
394
|
+
path: `${prefix}${item.name}`,
|
|
395
|
+
kind: item.isFolder ? 'directory' : 'file',
|
|
396
|
+
sizeBytes: item.isFolder ? undefined : item.size,
|
|
397
|
+
modifiedAt: item.lastModified || undefined
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Search for files matching a text query within a SharePoint drive.
|
|
402
|
+
*
|
|
403
|
+
* Delegates to Graph's server-side recursive drive search so results can
|
|
404
|
+
* be deeply nested (not limited to one folder level like {@link list}).
|
|
405
|
+
* Only files are returned — Graph search hits are always file-type items.
|
|
406
|
+
*
|
|
407
|
+
* @param driveId - SharePoint site id (from {@link listDrives}).
|
|
408
|
+
* @param query - Free-text search query (file name + content for indexed types).
|
|
409
|
+
* @param opts.path - Optional drive-root-relative subtree to scope the search.
|
|
410
|
+
* @param opts.limit - Maximum number of results (default 50, max 200).
|
|
411
|
+
* @returns Matching entries as {@link StorageEntry}s.
|
|
412
|
+
*/
|
|
413
|
+
async search(driveId, query, opts) {
|
|
414
|
+
const client = await this.resolveBrowseClient();
|
|
415
|
+
const siteId = this.config.siteId ?? driveId;
|
|
416
|
+
const hits = await searchDriveWithPathFallback(client, siteId, query, {
|
|
417
|
+
scopePath: opts?.path,
|
|
418
|
+
limit: opts?.limit
|
|
419
|
+
});
|
|
420
|
+
return hits.map((hit) => {
|
|
421
|
+
const prefix = hit.parentPath ? `${hit.parentPath}/` : '';
|
|
422
|
+
return {
|
|
423
|
+
id: hit.id,
|
|
424
|
+
name: hit.name,
|
|
425
|
+
path: `${prefix}${hit.name}`,
|
|
426
|
+
kind: hit.isFolder ? 'directory' : 'file',
|
|
427
|
+
sizeBytes: hit.isFolder ? undefined : hit.size,
|
|
428
|
+
modifiedAt: hit.lastModified || undefined
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Mint a fresh pre-authenticated download URL for a file entry (canonical
|
|
434
|
+
* `storage/adapter` source-mint extension). By ITEM ID first — Graph search
|
|
435
|
+
* hits can carry partial parent paths (observed: bare filenames for nested
|
|
436
|
+
* files), and ids survive moves/renames — falling back to path addressing.
|
|
437
|
+
* Config is per-call for the same reason as {@link objectStore}: the
|
|
438
|
+
* resolved adapter is shared, so `configure()` would race across scopes.
|
|
439
|
+
*/
|
|
440
|
+
async sourceUrlForEntry(driveId, ref, config) {
|
|
441
|
+
const overrides = admitMicrosoft(SharePointAdapterOverrideSchema, config ?? {});
|
|
442
|
+
const integrationId = overrides.integrationId ??
|
|
443
|
+
this.config.integrationId;
|
|
444
|
+
const client = await this.resolveBrowseClient(integrationId, config?.runtime ?? this.config.runtime);
|
|
445
|
+
const siteId = overrides.siteId ?? this.config.siteId ?? driveId;
|
|
446
|
+
const runtime = (config?.runtime ?? this.config.runtime);
|
|
447
|
+
if (ref.itemId) {
|
|
448
|
+
const attempt = await settle(() => client.getDownloadUrl(siteId, ref.itemId));
|
|
449
|
+
if (attempt.ok)
|
|
450
|
+
return attempt.value.url;
|
|
451
|
+
if (!(attempt.cause instanceof MicrosoftGraphError) || attempt.cause.status !== 404)
|
|
452
|
+
throw attempt.cause;
|
|
453
|
+
logWarn(runtime, 'SharePoint item id is absent; trying path addressing', { failure: valueFreeFailure(attempt.cause) });
|
|
454
|
+
}
|
|
455
|
+
if (ref.path) {
|
|
456
|
+
return client.getDownloadUrlByPath(siteId, ref.path);
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
async listItem(driveId, itemId) {
|
|
461
|
+
const client = await this.resolveBrowseClient();
|
|
462
|
+
const siteId = this.config.siteId ?? driveId;
|
|
463
|
+
const items = await client.browseDriveItem(siteId, itemId);
|
|
464
|
+
return items.map((item) => ({
|
|
465
|
+
id: item.id,
|
|
466
|
+
name: item.name,
|
|
467
|
+
path: item.name,
|
|
468
|
+
kind: item.isFolder ? 'directory' : 'file',
|
|
469
|
+
sizeBytes: item.isFolder ? undefined : item.size,
|
|
470
|
+
modifiedAt: item.lastModified || undefined
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
// SharePoint-specific Document Operations
|
|
474
|
+
/**
|
|
475
|
+
* List documents (files only) in a SharePoint site, returning the
|
|
476
|
+
* SharePoint-typed record shape. Distinct from the canonical
|
|
477
|
+
* {@link list} browse method which returns {@link StorageEntry}s of
|
|
478
|
+
* both files and folders.
|
|
479
|
+
*
|
|
480
|
+
* @param containerId - SharePoint site ID
|
|
481
|
+
*/
|
|
482
|
+
async listDocuments(containerId) {
|
|
483
|
+
const client = this.ensureClient();
|
|
484
|
+
const siteId = this.config.siteId ?? containerId;
|
|
485
|
+
const items = await client.browseDrive(siteId);
|
|
486
|
+
return items
|
|
487
|
+
.filter((item) => !item.isFolder)
|
|
488
|
+
.map((item) => ({
|
|
489
|
+
id: item.id,
|
|
490
|
+
name: item.name,
|
|
491
|
+
size: item.size,
|
|
492
|
+
modifiedAt: item.lastModified,
|
|
493
|
+
mimeType: item.mimeType,
|
|
494
|
+
webUrl: item.webUrl,
|
|
495
|
+
containerId: siteId
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get a document with its content stream.
|
|
500
|
+
*
|
|
501
|
+
* @param documentId - Drive item ID
|
|
502
|
+
* @param siteId - Optional site ID (falls back to configured siteId)
|
|
503
|
+
* @returns Object with metadata and content Blob
|
|
504
|
+
*/
|
|
505
|
+
async get(documentId, siteId) {
|
|
506
|
+
const client = this.ensureClient();
|
|
507
|
+
const sid = this.config.siteId ?? siteId ?? '';
|
|
508
|
+
const [blob, items] = await Promise.all([
|
|
509
|
+
client.downloadFile(sid, documentId),
|
|
510
|
+
client.browseDrive(sid)
|
|
511
|
+
]);
|
|
512
|
+
const item = items.find((i) => i.id === documentId);
|
|
513
|
+
if (!item) {
|
|
514
|
+
throw new Error(`Document ${documentId} not found in SharePoint`);
|
|
515
|
+
}
|
|
516
|
+
const metadata = {
|
|
517
|
+
id: item.id,
|
|
518
|
+
name: item.name,
|
|
519
|
+
size: item.size,
|
|
520
|
+
modifiedAt: item.lastModified,
|
|
521
|
+
mimeType: item.mimeType,
|
|
522
|
+
webUrl: item.webUrl,
|
|
523
|
+
containerId: sid
|
|
524
|
+
};
|
|
525
|
+
return { metadata, content: blob };
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Upload a document to SharePoint.
|
|
529
|
+
*
|
|
530
|
+
* Uses the Graph API upload endpoint:
|
|
531
|
+
* PUT /sites/{site-id}/drive/root:/{folderPath}/{filename}:/content
|
|
532
|
+
*
|
|
533
|
+
* @param containerId - SharePoint site ID
|
|
534
|
+
* @param metadata - Upload metadata (name, contentType, conflictBehavior)
|
|
535
|
+
* @param content - File content as ArrayBuffer or Blob
|
|
536
|
+
* @returns Created document record
|
|
537
|
+
*/
|
|
538
|
+
async put(containerId, metadata, content) {
|
|
539
|
+
const client = this.ensureClient();
|
|
540
|
+
const parsed = SharePointUploadMetadataSchema.parse(metadata);
|
|
541
|
+
const sid = this.config.siteId ?? containerId;
|
|
542
|
+
const item = await client.uploadFile(sid, parsed.name, content, {
|
|
543
|
+
contentType: parsed.contentType,
|
|
544
|
+
conflictBehavior: 'replace'
|
|
545
|
+
});
|
|
546
|
+
return SharePointDocumentRecordSchema.parse({
|
|
547
|
+
id: item.id,
|
|
548
|
+
name: item.name,
|
|
549
|
+
size: item.size,
|
|
550
|
+
modifiedAt: item.lastModified || new Date().toISOString(),
|
|
551
|
+
mimeType: item.mimeType,
|
|
552
|
+
webUrl: item.webUrl,
|
|
553
|
+
containerId: sid
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Delete a document from SharePoint.
|
|
558
|
+
*
|
|
559
|
+
* DELETE /sites/{site-id}/drive/items/{item-id}
|
|
560
|
+
*
|
|
561
|
+
* @param documentId - Drive item ID to delete
|
|
562
|
+
* @param siteId - Optional site ID (falls back to configured siteId)
|
|
563
|
+
*/
|
|
564
|
+
async delete(documentId, siteId) {
|
|
565
|
+
const client = this.ensureClient();
|
|
566
|
+
const sid = this.config.siteId ?? siteId ?? '';
|
|
567
|
+
await client.deleteItem(sid, documentId);
|
|
568
|
+
}
|
|
569
|
+
// Not Supported (adapter only implements document protocol)
|
|
570
|
+
keyValueStore(_namespace, _keySchema, _valueSchema) {
|
|
571
|
+
throw new Error('SharePoint adapter does not support key-value protocol');
|
|
572
|
+
}
|
|
573
|
+
repository(_tableName, _schema, _options) {
|
|
574
|
+
throw new Error('SharePoint adapter does not support repository protocol');
|
|
575
|
+
}
|
|
576
|
+
record(_descriptor) {
|
|
577
|
+
throw new Error('SharePoint adapter does not support record protocol');
|
|
578
|
+
}
|
|
579
|
+
// Database Access
|
|
580
|
+
async ensureReady() {
|
|
581
|
+
if (!this.client) {
|
|
582
|
+
throw new Error('SharePoint storage adapter not configured. Call setClient() with a GraphSharePointClient.');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
getDatabaseConnection() {
|
|
586
|
+
throw new Error('SharePoint adapter does not provide a database connection. Use the browse (list/listDrives) or document (listDocuments/get/put/delete) APIs.');
|
|
587
|
+
}
|
|
588
|
+
// Lifecycle
|
|
589
|
+
async initialize() {
|
|
590
|
+
if (!this.client) {
|
|
591
|
+
throw new Error('SharePoint storage adapter not configured. Call setClient() with a GraphSharePointClient.');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async close() {
|
|
595
|
+
// No persistent connections to close
|
|
596
|
+
}
|
|
597
|
+
async healthCheck() {
|
|
598
|
+
if (!this.client) {
|
|
599
|
+
return {
|
|
600
|
+
healthy: false,
|
|
601
|
+
error: 'No GraphSharePointClient configured'
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const start = Date.now();
|
|
605
|
+
const result = await settle(() => this.client.listSites());
|
|
606
|
+
if (result.ok)
|
|
607
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
608
|
+
return { healthy: false, error: describeError(result.cause) };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Implementation Export (runtime registration)
|
|
612
|
+
const defaultDescriptor = {
|
|
613
|
+
id: 'sharepoint',
|
|
614
|
+
kind: 'storage/adapter',
|
|
615
|
+
name: 'SharePoint Storage',
|
|
616
|
+
description: 'Document + object storage backed by Microsoft SharePoint via Graph API.',
|
|
617
|
+
protocols: ['document', 'object'],
|
|
618
|
+
capabilities: ['list', 'read', 'write', 'browse', 'search'],
|
|
619
|
+
requiresPairSchema: false,
|
|
620
|
+
supportsTransactions: false,
|
|
621
|
+
supportsTTL: false,
|
|
622
|
+
supportsSearch: true,
|
|
623
|
+
supportsFTS: false,
|
|
624
|
+
persistent: true,
|
|
625
|
+
supportsSubscription: false,
|
|
626
|
+
supportsHierarchy: true,
|
|
627
|
+
supportsComputedFields: false,
|
|
628
|
+
supportsSideEffects: false,
|
|
629
|
+
supportsRawQueries: false,
|
|
630
|
+
enabled: true
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Builds one adapter instance for one runtime. The adapter holds per-configure
|
|
634
|
+
* state (`config.runtime`, injected client), so the plugin's loader calls this
|
|
635
|
+
* once per runtime; a module-level instance would cross-talk between runtimes.
|
|
636
|
+
*/
|
|
637
|
+
export function createSharePointAdapterImplementation() {
|
|
638
|
+
return createRuntimeImplementation({
|
|
639
|
+
id: 'sharepoint',
|
|
640
|
+
kind: 'storage/adapter',
|
|
641
|
+
implementation: new SharePointStorageAdapter(defaultDescriptor)
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
export default createSharePointAdapterImplementation;
|
|
645
|
+
//# sourceMappingURL=sharepoint.adapter.impl.js.map
|