n8n-nodes-onedrive-business 1.0.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.
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StateStore = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ /**
7
+ * State Store for OneDrive Business Trigger Node
8
+ *
9
+ * Provides persistent storage for:
10
+ * - Delta query links
11
+ * - Processed file versions (deduplication)
12
+ * - Last known item states (for event classification)
13
+ * - Webhook subscription information
14
+ *
15
+ * State MUST survive n8n restarts and redeployments to prevent:
16
+ * - Duplicate workflow executions
17
+ * - Re-processing old changes
18
+ * - Losing track of processed items
19
+ */
20
+ class StateStore {
21
+ constructor(nodeId, storageDir = '.n8n-state') {
22
+ this.state = null;
23
+ this.saveDebounceTimer = null;
24
+ this.nodeId = nodeId;
25
+ // Store state in .n8n-state directory within the n8n user directory
26
+ // This ensures persistence across restarts
27
+ const userHome = process.env.N8N_USER_FOLDER || process.env.HOME || process.env.USERPROFILE || '/tmp';
28
+ const stateDir = (0, path_1.join)(userHome, storageDir, 'onedrive-business');
29
+ this.stateFilePath = (0, path_1.join)(stateDir, `${nodeId}.json`);
30
+ }
31
+ /**
32
+ * Initialize and load state from disk
33
+ * Creates default state if file doesn't exist
34
+ */
35
+ async initialize(tenantId, driveId, userId, siteId) {
36
+ try {
37
+ await this.ensureStateDirectory();
38
+ // Try to load existing state
39
+ const existingState = await this.loadFromDisk();
40
+ if (existingState) {
41
+ // Validate that the state matches the current configuration
42
+ if (existingState.tenantId === tenantId &&
43
+ existingState.driveId === driveId) {
44
+ this.state = existingState;
45
+ return this.state;
46
+ }
47
+ // Configuration changed, reset state
48
+ console.log(`OneDrive Business state configuration changed for node ${this.nodeId}, resetting state`);
49
+ }
50
+ // Create fresh state
51
+ this.state = this.createDefaultState(tenantId, driveId, userId, siteId);
52
+ await this.saveToDisk();
53
+ return this.state;
54
+ }
55
+ catch (error) {
56
+ console.error(`Failed to initialize state for node ${this.nodeId}:`, error);
57
+ // Return default state even if loading fails
58
+ this.state = this.createDefaultState(tenantId, driveId, userId, siteId);
59
+ return this.state;
60
+ }
61
+ }
62
+ /**
63
+ * Get current state
64
+ * Throws if not initialized
65
+ */
66
+ getState() {
67
+ if (!this.state) {
68
+ throw new Error('State not initialized. Call initialize() first.');
69
+ }
70
+ return this.state;
71
+ }
72
+ /**
73
+ * Update delta link
74
+ */
75
+ async updateDeltaLink(deltaLink) {
76
+ if (!this.state) {
77
+ throw new Error('State not initialized');
78
+ }
79
+ this.state.deltaLink = deltaLink;
80
+ this.state.lastDeltaQuery = Date.now();
81
+ await this.saveToDiskDebounced();
82
+ }
83
+ /**
84
+ * Mark a file version as processed
85
+ * Returns true if this version was NOT already processed
86
+ */
87
+ async markVersionProcessed(itemId, eTag) {
88
+ if (!this.state) {
89
+ throw new Error('State not initialized');
90
+ }
91
+ const versionKey = `${itemId}_${eTag}`;
92
+ // Check if already processed
93
+ if (this.state.processedVersions[versionKey]) {
94
+ return false; // Already processed
95
+ }
96
+ // Mark as processed
97
+ this.state.processedVersions[versionKey] = true;
98
+ // Save asynchronously
99
+ await this.saveToDiskDebounced();
100
+ return true; // Newly processed
101
+ }
102
+ /**
103
+ * Check if a version has been processed
104
+ */
105
+ isVersionProcessed(itemId, eTag) {
106
+ if (!this.state) {
107
+ return false;
108
+ }
109
+ const versionKey = `${itemId}_${eTag}`;
110
+ return !!this.state.processedVersions[versionKey];
111
+ }
112
+ /**
113
+ * Update last known state of an item
114
+ * Used for event classification (created vs updated)
115
+ */
116
+ async updateLastKnownItem(itemId, eTag, lastModifiedDateTime) {
117
+ if (!this.state) {
118
+ throw new Error('State not initialized');
119
+ }
120
+ this.state.lastKnownItems[itemId] = {
121
+ eTag,
122
+ lastModifiedDateTime,
123
+ };
124
+ await this.saveToDiskDebounced();
125
+ }
126
+ /**
127
+ * Get last known state of an item
128
+ */
129
+ getLastKnownItem(itemId) {
130
+ if (!this.state) {
131
+ return null;
132
+ }
133
+ return this.state.lastKnownItems[itemId] || null;
134
+ }
135
+ /**
136
+ * Update webhook subscription information
137
+ */
138
+ async updateSubscription(subscriptionId, expirationTimestamp) {
139
+ if (!this.state) {
140
+ throw new Error('State not initialized');
141
+ }
142
+ this.state.subscriptionId = subscriptionId;
143
+ this.state.subscriptionExpiration = expirationTimestamp;
144
+ await this.saveToDiskDebounced();
145
+ }
146
+ /**
147
+ * Clear webhook subscription information
148
+ */
149
+ async clearSubscription() {
150
+ if (!this.state) {
151
+ throw new Error('State not initialized');
152
+ }
153
+ this.state.subscriptionId = undefined;
154
+ this.state.subscriptionExpiration = undefined;
155
+ await this.saveToDiskDebounced();
156
+ }
157
+ /**
158
+ * Clean up old processed versions to prevent unbounded growth
159
+ * Keeps only the most recent versions per item
160
+ */
161
+ async cleanupOldVersions(maxVersionsPerItem = 10) {
162
+ if (!this.state) {
163
+ throw new Error('State not initialized');
164
+ }
165
+ // Group versions by itemId
166
+ const itemVersions = {};
167
+ for (const versionKey of Object.keys(this.state.processedVersions)) {
168
+ const [itemId] = versionKey.split('_');
169
+ if (!itemVersions[itemId]) {
170
+ itemVersions[itemId] = [];
171
+ }
172
+ itemVersions[itemId].push(versionKey);
173
+ }
174
+ // Keep only recent versions
175
+ const cleaned = {};
176
+ for (const itemId of Object.keys(itemVersions)) {
177
+ const versions = itemVersions[itemId];
178
+ // Sort by eTag (not perfect, but good enough)
179
+ // In production, you might want to use timestamps
180
+ versions.sort();
181
+ // Keep the last N versions
182
+ const toKeep = versions.slice(-maxVersionsPerItem);
183
+ for (const versionKey of toKeep) {
184
+ cleaned[versionKey] = true;
185
+ }
186
+ }
187
+ this.state.processedVersions = cleaned;
188
+ await this.saveToDisk();
189
+ console.log(`Cleaned up old versions for node ${this.nodeId}. Kept ${Object.keys(cleaned).length} versions.`);
190
+ }
191
+ /**
192
+ * Create default state
193
+ */
194
+ createDefaultState(tenantId, driveId, userId, siteId) {
195
+ return {
196
+ tenantId,
197
+ driveId,
198
+ userId,
199
+ siteId,
200
+ deltaLink: null,
201
+ lastDeltaQuery: 0,
202
+ processedVersions: {},
203
+ lastKnownItems: {},
204
+ };
205
+ }
206
+ /**
207
+ * Ensure state directory exists
208
+ */
209
+ async ensureStateDirectory() {
210
+ const dir = this.stateFilePath.substring(0, this.stateFilePath.lastIndexOf('/'));
211
+ try {
212
+ await fs_1.promises.mkdir(dir, { recursive: true });
213
+ }
214
+ catch (error) {
215
+ if (error.code !== 'EEXIST') {
216
+ throw error;
217
+ }
218
+ }
219
+ }
220
+ /**
221
+ * Load state from disk
222
+ */
223
+ async loadFromDisk() {
224
+ try {
225
+ const data = await fs_1.promises.readFile(this.stateFilePath, 'utf-8');
226
+ return JSON.parse(data);
227
+ }
228
+ catch (error) {
229
+ if (error.code === 'ENOENT') {
230
+ // File doesn't exist yet
231
+ return null;
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+ /**
237
+ * Save state to disk immediately
238
+ */
239
+ async saveToDisk() {
240
+ if (!this.state) {
241
+ return;
242
+ }
243
+ try {
244
+ await this.ensureStateDirectory();
245
+ const data = JSON.stringify(this.state, null, 2);
246
+ await fs_1.promises.writeFile(this.stateFilePath, data, 'utf-8');
247
+ }
248
+ catch (error) {
249
+ console.error(`Failed to save state for node ${this.nodeId}:`, error);
250
+ // Don't throw - state is cached in memory
251
+ }
252
+ }
253
+ /**
254
+ * Save state to disk with debouncing
255
+ * Prevents excessive writes during rapid updates
256
+ */
257
+ async saveToDiskDebounced(debounceMs = 1000) {
258
+ // Clear existing timer
259
+ if (this.saveDebounceTimer) {
260
+ clearTimeout(this.saveDebounceTimer);
261
+ }
262
+ // Set new timer
263
+ this.saveDebounceTimer = setTimeout(async () => {
264
+ await this.saveToDisk();
265
+ this.saveDebounceTimer = null;
266
+ }, debounceMs);
267
+ }
268
+ /**
269
+ * Force save any pending changes
270
+ * Call this before deactivating the node
271
+ */
272
+ async flush() {
273
+ if (this.saveDebounceTimer) {
274
+ clearTimeout(this.saveDebounceTimer);
275
+ this.saveDebounceTimer = null;
276
+ }
277
+ await this.saveToDisk();
278
+ }
279
+ }
280
+ exports.StateStore = StateStore;
@@ -0,0 +1,78 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node/http" />
3
+ /// <reference types="n8n-workflow" />
4
+ import type { IncomingMessage, ServerResponse } from 'http';
5
+ import type { GraphSubscription } from '../types';
6
+ import { GraphClient } from './GraphClient';
7
+ import { StateStore } from './StateStore';
8
+ /**
9
+ * Webhook Handler for OneDrive Business
10
+ *
11
+ * Manages Microsoft Graph webhook subscriptions lifecycle:
12
+ * 1. Creates subscription on node activation
13
+ * 2. Handles validation token handshake
14
+ * 3. Auto-renews subscription before expiration
15
+ * 4. Deletes subscription on node deactivation
16
+ *
17
+ * CRITICAL ARCHITECTURE DECISION:
18
+ * The webhook does NOT emit workflow data directly.
19
+ * It only acts as a NOTIFICATION that changes occurred.
20
+ * The actual data is fetched via Delta Query by DeltaProcessor.
21
+ *
22
+ * WHY:
23
+ * - Webhook notifications don't contain full file metadata
24
+ * - Multiple notifications may come for one upload
25
+ * - Delta Query provides the single source of truth
26
+ * - Deduplication happens in DeltaProcessor, not here
27
+ */
28
+ export declare class WebhookHandler {
29
+ private graphClient;
30
+ private stateStore;
31
+ private subscriptionResource;
32
+ private clientState;
33
+ private readonly RENEWAL_BUFFER_MS;
34
+ private readonly SUBSCRIPTION_DURATION_DAYS;
35
+ constructor(graphClient: GraphClient, stateStore: StateStore, subscriptionResource: string);
36
+ /**
37
+ * Create or renew webhook subscription
38
+ * Called when trigger node is activated
39
+ */
40
+ createOrRenewSubscription(notificationUrl: string): Promise<GraphSubscription>;
41
+ /**
42
+ * Create a new webhook subscription
43
+ */
44
+ private createSubscription;
45
+ /**
46
+ * Renew an existing subscription
47
+ */
48
+ private renewSubscription;
49
+ /**
50
+ * Delete webhook subscription
51
+ * Called when trigger node is deactivated
52
+ */
53
+ deleteSubscription(): Promise<void>;
54
+ /**
55
+ * Handle incoming webhook request
56
+ *
57
+ * Microsoft Graph sends two types of requests:
58
+ * 1. Validation request (during subscription creation)
59
+ * 2. Notification request (when changes occur)
60
+ *
61
+ * CRITICAL: This method returns immediately and does NOT trigger workflows.
62
+ * It only validates the request and returns HTTP 200.
63
+ */
64
+ handleWebhookRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
65
+ /**
66
+ * Handle validation request from Microsoft Graph
67
+ * Must respond with the validation token in plain text
68
+ */
69
+ private handleValidationRequest;
70
+ /**
71
+ * Read request body from stream
72
+ */
73
+ private readRequestBody;
74
+ /**
75
+ * Get the client state for validation
76
+ */
77
+ getClientState(): string;
78
+ }
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookHandler = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ /**
6
+ * Webhook Handler for OneDrive Business
7
+ *
8
+ * Manages Microsoft Graph webhook subscriptions lifecycle:
9
+ * 1. Creates subscription on node activation
10
+ * 2. Handles validation token handshake
11
+ * 3. Auto-renews subscription before expiration
12
+ * 4. Deletes subscription on node deactivation
13
+ *
14
+ * CRITICAL ARCHITECTURE DECISION:
15
+ * The webhook does NOT emit workflow data directly.
16
+ * It only acts as a NOTIFICATION that changes occurred.
17
+ * The actual data is fetched via Delta Query by DeltaProcessor.
18
+ *
19
+ * WHY:
20
+ * - Webhook notifications don't contain full file metadata
21
+ * - Multiple notifications may come for one upload
22
+ * - Delta Query provides the single source of truth
23
+ * - Deduplication happens in DeltaProcessor, not here
24
+ */
25
+ class WebhookHandler {
26
+ constructor(graphClient, stateStore, subscriptionResource) {
27
+ // Subscription renewal: renew 2 hours before expiration
28
+ this.RENEWAL_BUFFER_MS = 2 * 60 * 60 * 1000;
29
+ // Subscription duration: 3 days (Microsoft Graph maximum)
30
+ this.SUBSCRIPTION_DURATION_DAYS = 3;
31
+ this.graphClient = graphClient;
32
+ this.stateStore = stateStore;
33
+ this.subscriptionResource = subscriptionResource;
34
+ // Generate unique client state for security validation
35
+ this.clientState = (0, helpers_1.generateSecureRandomString)(32);
36
+ }
37
+ /**
38
+ * Create or renew webhook subscription
39
+ * Called when trigger node is activated
40
+ */
41
+ async createOrRenewSubscription(notificationUrl) {
42
+ const state = this.stateStore.getState();
43
+ // Check if we have an existing subscription
44
+ if (state.subscriptionId && state.subscriptionExpiration) {
45
+ const now = Date.now();
46
+ const expirationTime = state.subscriptionExpiration;
47
+ // If subscription is still valid for more than RENEWAL_BUFFER, reuse it
48
+ if (expirationTime - now > this.RENEWAL_BUFFER_MS) {
49
+ console.log(`Reusing existing subscription ${state.subscriptionId}`);
50
+ // Fetch subscription details
51
+ try {
52
+ const subscription = await this.graphClient.get(`/subscriptions/${state.subscriptionId}`);
53
+ return subscription;
54
+ }
55
+ catch (error) {
56
+ console.log('Existing subscription not found, creating new one');
57
+ // Subscription doesn't exist, create new one
58
+ }
59
+ }
60
+ else {
61
+ // Subscription is expiring soon, renew it
62
+ console.log(`Renewing subscription ${state.subscriptionId}`);
63
+ try {
64
+ const renewed = await this.renewSubscription(state.subscriptionId);
65
+ return renewed;
66
+ }
67
+ catch (error) {
68
+ console.log('Failed to renew subscription, creating new one');
69
+ // Renewal failed, create new one
70
+ }
71
+ }
72
+ }
73
+ // Create new subscription
74
+ return await this.createSubscription(notificationUrl);
75
+ }
76
+ /**
77
+ * Create a new webhook subscription
78
+ */
79
+ async createSubscription(notificationUrl) {
80
+ const expirationDateTime = new Date();
81
+ expirationDateTime.setDate(expirationDateTime.getDate() + this.SUBSCRIPTION_DURATION_DAYS);
82
+ const subscriptionRequest = {
83
+ changeType: 'created,updated',
84
+ notificationUrl,
85
+ resource: this.subscriptionResource,
86
+ expirationDateTime: expirationDateTime.toISOString(),
87
+ clientState: this.clientState,
88
+ };
89
+ console.log('Creating Microsoft Graph subscription:', {
90
+ resource: this.subscriptionResource,
91
+ notificationUrl,
92
+ expirationDateTime: expirationDateTime.toISOString(),
93
+ });
94
+ const subscription = await this.graphClient.post('/subscriptions', subscriptionRequest);
95
+ // Store subscription info
96
+ await this.stateStore.updateSubscription(subscription.id, new Date(subscription.expirationDateTime).getTime());
97
+ console.log(`Created subscription ${subscription.id}`);
98
+ return subscription;
99
+ }
100
+ /**
101
+ * Renew an existing subscription
102
+ */
103
+ async renewSubscription(subscriptionId) {
104
+ const expirationDateTime = new Date();
105
+ expirationDateTime.setDate(expirationDateTime.getDate() + this.SUBSCRIPTION_DURATION_DAYS);
106
+ const subscription = await this.graphClient.patch(`/subscriptions/${subscriptionId}`, {
107
+ expirationDateTime: expirationDateTime.toISOString(),
108
+ });
109
+ // Update stored expiration
110
+ await this.stateStore.updateSubscription(subscription.id, new Date(subscription.expirationDateTime).getTime());
111
+ console.log(`Renewed subscription ${subscriptionId}`);
112
+ return subscription;
113
+ }
114
+ /**
115
+ * Delete webhook subscription
116
+ * Called when trigger node is deactivated
117
+ */
118
+ async deleteSubscription() {
119
+ const state = this.stateStore.getState();
120
+ if (!state.subscriptionId) {
121
+ console.log('No subscription to delete');
122
+ return;
123
+ }
124
+ try {
125
+ await this.graphClient.delete(`/subscriptions/${state.subscriptionId}`);
126
+ console.log(`Deleted subscription ${state.subscriptionId}`);
127
+ }
128
+ catch (error) {
129
+ // If subscription doesn't exist (404), that's fine
130
+ if (error.statusCode !== 404) {
131
+ console.error('Failed to delete subscription:', error);
132
+ }
133
+ }
134
+ // Clear subscription from state
135
+ await this.stateStore.clearSubscription();
136
+ }
137
+ /**
138
+ * Handle incoming webhook request
139
+ *
140
+ * Microsoft Graph sends two types of requests:
141
+ * 1. Validation request (during subscription creation)
142
+ * 2. Notification request (when changes occur)
143
+ *
144
+ * CRITICAL: This method returns immediately and does NOT trigger workflows.
145
+ * It only validates the request and returns HTTP 200.
146
+ */
147
+ async handleWebhookRequest(req, res) {
148
+ var _a;
149
+ // Handle validation request
150
+ const validationToken = (_a = req.query) === null || _a === void 0 ? void 0 : _a.validationToken;
151
+ if (validationToken) {
152
+ console.log('Received webhook validation request');
153
+ this.handleValidationRequest(res, validationToken);
154
+ return;
155
+ }
156
+ // Handle notification request
157
+ console.log('Received webhook notification');
158
+ try {
159
+ // Read body
160
+ const body = await this.readRequestBody(req);
161
+ const notification = JSON.parse(body);
162
+ // Validate client state
163
+ if (notification.value && notification.value.length > 0) {
164
+ const firstNotification = notification.value[0];
165
+ if (firstNotification.clientState !== this.clientState) {
166
+ console.error('Invalid clientState in webhook notification');
167
+ res.writeHead(401);
168
+ res.end();
169
+ return;
170
+ }
171
+ }
172
+ // Return 200 OK immediately
173
+ // DO NOT trigger workflow here
174
+ // The trigger node will poll delta query separately
175
+ res.writeHead(200);
176
+ res.end();
177
+ console.log('Webhook notification acknowledged');
178
+ }
179
+ catch (error) {
180
+ console.error('Error handling webhook notification:', error);
181
+ res.writeHead(500);
182
+ res.end();
183
+ }
184
+ }
185
+ /**
186
+ * Handle validation request from Microsoft Graph
187
+ * Must respond with the validation token in plain text
188
+ */
189
+ handleValidationRequest(res, validationToken) {
190
+ res.writeHead(200, {
191
+ 'Content-Type': 'text/plain',
192
+ });
193
+ res.end(validationToken);
194
+ console.log('Webhook validation successful');
195
+ }
196
+ /**
197
+ * Read request body from stream
198
+ */
199
+ readRequestBody(req) {
200
+ return new Promise((resolve, reject) => {
201
+ let body = '';
202
+ req.on('data', (chunk) => {
203
+ body += chunk.toString();
204
+ });
205
+ req.on('end', () => {
206
+ resolve(body);
207
+ });
208
+ req.on('error', (error) => {
209
+ reject(error);
210
+ });
211
+ });
212
+ }
213
+ /**
214
+ * Get the client state for validation
215
+ */
216
+ getClientState() {
217
+ return this.clientState;
218
+ }
219
+ }
220
+ exports.WebhookHandler = WebhookHandler;
@@ -0,0 +1,113 @@
1
+ import { DriveLocation } from '../types';
2
+ /**
3
+ * Helper Functions for OneDrive Business Node
4
+ *
5
+ * These utilities handle common operations like drive resolution,
6
+ * path sanitization, and binary data handling.
7
+ */
8
+ /**
9
+ * Resolve the drive path for Microsoft Graph API
10
+ *
11
+ * OneDrive Business drives are accessed through:
12
+ * - User context: /users/{userId}/drive
13
+ * - Site context: /sites/{siteId}/drive
14
+ *
15
+ * NEVER use /me/drive without tenant context
16
+ */
17
+ export declare function resolveDrivePath(location: DriveLocation): string;
18
+ /**
19
+ * Resolve the subscription resource for webhooks
20
+ *
21
+ * For delta queries and webhooks, we use:
22
+ * - User context: /users/{userId}/drive/root
23
+ * - Site context: /sites/{siteId}/drive
24
+ */
25
+ export declare function resolveSubscriptionResource(location: DriveLocation): string;
26
+ /**
27
+ * Sanitize file or folder path for OneDrive API
28
+ *
29
+ * - Removes leading/trailing slashes
30
+ * - Converts backslashes to forward slashes
31
+ * - Encodes special characters
32
+ */
33
+ export declare function sanitizePath(path: string): string;
34
+ /**
35
+ * Build the full item path for OneDrive API
36
+ *
37
+ * Constructs paths like:
38
+ * - /drive/root:/Documents/file.pdf
39
+ * - /drive/items/{itemId}
40
+ */
41
+ export declare function buildItemPath(drivePath: string, itemPath?: string, itemId?: string): string;
42
+ /**
43
+ * Extract file extension from filename
44
+ */
45
+ export declare function getFileExtension(filename: string): string;
46
+ /**
47
+ * Format bytes to human-readable size
48
+ */
49
+ export declare function formatBytes(bytes: number): string;
50
+ /**
51
+ * Calculate exponential backoff delay for retries
52
+ *
53
+ * Used for throttling (429) and transient errors
54
+ */
55
+ export declare function calculateBackoffDelay(retryCount: number, initialDelayMs?: number, maxDelayMs?: number, backoffMultiplier?: number): number;
56
+ /**
57
+ * Sleep for specified milliseconds
58
+ * Used in retry logic
59
+ */
60
+ export declare function sleep(ms: number): Promise<void>;
61
+ /**
62
+ * Parse Retry-After header from 429 responses
63
+ * Returns delay in milliseconds
64
+ */
65
+ export declare function parseRetryAfter(retryAfterHeader?: string): number;
66
+ /**
67
+ * Generate a cryptographically secure random string
68
+ * Used for clientState in webhook subscriptions
69
+ */
70
+ export declare function generateSecureRandomString(length?: number): string;
71
+ /**
72
+ * Validate Microsoft Graph item ID format
73
+ */
74
+ export declare function isValidItemId(itemId: string): boolean;
75
+ /**
76
+ * Check if an item is a file (vs folder)
77
+ */
78
+ export declare function isFile(item: {
79
+ file?: any;
80
+ folder?: any;
81
+ }): boolean;
82
+ /**
83
+ * Check if an item is a folder (vs file)
84
+ */
85
+ export declare function isFolder(item: {
86
+ file?: any;
87
+ folder?: any;
88
+ }): boolean;
89
+ /**
90
+ * Extract MIME type from DriveItem
91
+ */
92
+ export declare function getMimeType(item: {
93
+ file?: {
94
+ mimeType?: string;
95
+ };
96
+ }): string;
97
+ /**
98
+ * Build a version key for deduplication
99
+ * Format: ${itemId}_${eTag}
100
+ */
101
+ export declare function buildVersionKey(itemId: string, eTag: string): string;
102
+ /**
103
+ * Clean up old processed versions to prevent memory bloat
104
+ * Keeps only the most recent maxVersions per item
105
+ */
106
+ export declare function cleanupProcessedVersions(processedVersions: Record<string, boolean>, maxVersionsPerItem?: number): Record<string, boolean>;
107
+ /**
108
+ * Parse Graph API error response
109
+ */
110
+ export declare function parseGraphError(error: any): {
111
+ code: string;
112
+ message: string;
113
+ };