mcp-scraper 0.69.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +6 -2
  3. package/dist/{analytics-repository-62CAZAIP.js → analytics-repository-6YLRSWYO.js} +3 -2
  4. package/dist/bin/api-server.cjs +18592 -12680
  5. package/dist/bin/api-server.js +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.js +2 -2
  10. package/dist/bin/mcp-stdio-server.cjs +10492 -9129
  11. package/dist/bin/mcp-stdio-server.js +9 -7
  12. package/dist/bin/paa-harvest.cjs +308 -41
  13. package/dist/bin/paa-harvest.js +4 -3
  14. package/dist/{chunk-M7D7WO75.js → chunk-2N4V4LVU.js} +3178 -2019
  15. package/dist/chunk-7RQULQF3.js +263 -0
  16. package/dist/{chunk-UL4ZKAWZ.js → chunk-A66DGFOU.js} +1 -1
  17. package/dist/{chunk-ES25GP6C.js → chunk-CXY5WV45.js} +24 -34
  18. package/dist/{chunk-Y46YNQMM.js → chunk-F5TK4KMT.js} +1 -1
  19. package/dist/{chunk-62LEXS5O.js → chunk-F6MUGMRN.js} +306 -44
  20. package/dist/{chunk-5RFQGIYC.js → chunk-FVL4GUTP.js} +2 -2
  21. package/dist/{chunk-OZIUFN6B.js → chunk-I45JV4EU.js} +2 -2
  22. package/dist/{chunk-2LXTOAKS.js → chunk-KIPIBPUB.js} +9 -1
  23. package/dist/chunk-MB72PA6S.js +2003 -0
  24. package/dist/chunk-OM7HVEJ3.js +26 -0
  25. package/dist/chunk-PJEEKOUM.js +404 -0
  26. package/dist/{chunk-RX2QAHML.js → chunk-RAFQEPJ4.js} +2 -2
  27. package/dist/{chunk-KO5CK5ZT.js → chunk-SR7GSLEA.js} +202 -441
  28. package/dist/chunk-T3MZISOF.js +240 -0
  29. package/dist/{chunk-ULYOCJUJ.js → chunk-TUTQHP4O.js} +1 -1
  30. package/dist/{chunk-65FTMB7G.js → chunk-WEFPBAAG.js} +180 -9
  31. package/dist/{chunk-LN6N3YLK.js → chunk-WFQ2E4WE.js} +1 -1
  32. package/dist/{db-ZGZAKYBW.js → db-566MOHHD.js} +7 -1
  33. package/dist/{extract-bundle-SXSJCPQI.js → extract-bundle-337ZDIIQ.js} +4 -4
  34. package/dist/gmail-service-E276SBJU.js +25 -0
  35. package/dist/index.cjs +322 -49
  36. package/dist/index.d.cts +15 -2
  37. package/dist/index.d.ts +15 -2
  38. package/dist/index.js +4 -3
  39. package/dist/{lead-list-enrichment-repository-CXLVIV3R.js → lead-list-enrichment-repository-WWEMJ7EN.js} +2 -2
  40. package/dist/{location-data-repository-G72M7DTN.js → location-data-repository-H4B2BWGY.js} +2 -2
  41. package/dist/{server-WCNYXILA.js → server-UGWC5YMD.js} +8376 -6616
  42. package/dist/{site-extract-repository-RLB3TFJ3.js → site-extract-repository-ACNAVNDN.js} +3 -3
  43. package/dist/{worker-KHSJG365.js → worker-JQORPCFQ.js} +9 -7
  44. package/package.json +1 -1
  45. package/dist/chunk-2L4C4DAZ.js +0 -103
@@ -0,0 +1,2003 @@
1
+ import {
2
+ getOwnedServiceConnection,
3
+ getSessionSecret,
4
+ reconcileDiscoveredNangoConnections,
5
+ recordServiceConnectionHealth,
6
+ setServiceConnectionProviderIdentity,
7
+ updateServiceConnectionTools
8
+ } from "./chunk-PJEEKOUM.js";
9
+ import {
10
+ createConnectedDataArtifact,
11
+ createConnectedDataBinaryArtifact
12
+ } from "./chunk-T3MZISOF.js";
13
+ import {
14
+ recordVendorUsage,
15
+ runWithCostContext
16
+ } from "./chunk-CXY5WV45.js";
17
+ import {
18
+ CONNECTED_COMPUTE_CREDITS_PER_SECOND,
19
+ CONNECTED_FUNCTION_RUN_CREDITS,
20
+ CONNECTED_FUNCTION_RUN_MC,
21
+ CONNECTED_PROXY_REQUEST_CREDITS,
22
+ CONNECTED_USAGE_RATE_POLICY_VERSION,
23
+ LedgerOperation,
24
+ MC_PER_CREDIT,
25
+ calculateConnectedUsageCharge
26
+ } from "./chunk-FVL4GUTP.js";
27
+ import {
28
+ debitMcIdempotent,
29
+ getDb,
30
+ getUserByEmail,
31
+ reconcileBalanceMc
32
+ } from "./chunk-WEFPBAAG.js";
33
+
34
+ // src/api/gmail-service.ts
35
+ import { createHash as createHash4, createHmac, timingSafeEqual } from "crypto";
36
+
37
+ // src/api/main-nango-transport.ts
38
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
39
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
40
+
41
+ // src/api/connected-usage-billing.ts
42
+ import { createHash, randomUUID } from "crypto";
43
+ import { z } from "zod";
44
+
45
+ // src/api/connected-cost-telemetry.ts
46
+ async function recordNangoConnectionCost(userId, connectionMonths) {
47
+ if (!Number.isFinite(connectionMonths) || connectionMonths <= 0) return;
48
+ await runWithCostContext(
49
+ { op: LedgerOperation.CONNECTED_ACCOUNT_FEE, userId: Number(userId) },
50
+ () => recordVendorUsage({
51
+ vendor: "nango_connection",
52
+ units: connectionMonths,
53
+ unitType: "connection_month"
54
+ })
55
+ );
56
+ }
57
+ async function recordNangoUsageCost(userId, usage) {
58
+ const dimensions = [
59
+ { vendor: "nango_function_run", units: usage.functionRuns, unitType: "function_run" },
60
+ { vendor: "nango_proxy_request", units: usage.proxyRequests, unitType: "proxy_request" },
61
+ { vendor: "nango_compute", units: usage.computeMilliseconds / 1e3, unitType: "compute_second" }
62
+ ];
63
+ await runWithCostContext(
64
+ { op: LedgerOperation.CONNECTED_USAGE, userId: Number(userId) },
65
+ async () => {
66
+ for (const dimension of dimensions) {
67
+ if (!Number.isFinite(dimension.units) || dimension.units <= 0) continue;
68
+ await recordVendorUsage(dimension);
69
+ }
70
+ }
71
+ );
72
+ }
73
+
74
+ // src/api/connected-usage-billing.ts
75
+ var CONNECTED_USAGE_BILLING_CLASS = "connected_usage";
76
+ var CONNECTED_USAGE_DEFAULT_SOURCE_SURFACE = "mastra_scheduler";
77
+ var ConnectedUsageSafeMetadataSchema = z.object({
78
+ toolName: z.string().trim().min(1).max(200).optional(),
79
+ operationKind: z.enum(["read", "action", "sync", "export", "proxy", "other"]).optional(),
80
+ outcome: z.enum(["success", "error", "partial"]).optional(),
81
+ requestId: z.string().trim().min(1).max(200).optional(),
82
+ occurrenceId: z.string().trim().min(1).max(200).optional(),
83
+ scheduleId: z.string().trim().min(1).max(200).optional(),
84
+ nangoOperationId: z.string().trim().min(1).max(200).optional(),
85
+ startedAt: z.string().datetime({ offset: true }).optional(),
86
+ completedAt: z.string().datetime({ offset: true }).optional()
87
+ }).strict();
88
+ var ConnectedUsageSettlementInputSchema = z.object({
89
+ identity: z.string().trim().min(1).max(320),
90
+ idempotencyKey: z.string().trim().min(1).max(500),
91
+ ratePolicyVersion: z.string().trim().min(1).max(100),
92
+ connectionId: z.string().trim().min(1).max(500),
93
+ providerConfigKey: z.string().trim().min(1).max(200),
94
+ functionRuns: z.number().int().safe().nonnegative(),
95
+ proxyRequests: z.number().int().safe().nonnegative(),
96
+ computeMilliseconds: z.number().int().safe().nonnegative(),
97
+ sourceSurface: z.enum(["main_mcp", "mastra_scheduler", "nango_reconciliation"]).default(CONNECTED_USAGE_DEFAULT_SOURCE_SURFACE),
98
+ metadata: ConnectedUsageSafeMetadataSchema.optional()
99
+ }).strict();
100
+ var ConnectedUsagePreflightInputSchema = z.object({
101
+ identity: z.string().trim().min(1).max(320),
102
+ ratePolicyVersion: z.string().trim().min(1).max(100)
103
+ }).strict();
104
+ var ConnectedUsageBillingError = class extends Error {
105
+ constructor(code, message, httpStatus, details = {}) {
106
+ super(message);
107
+ this.code = code;
108
+ this.httpStatus = httpStatus;
109
+ this.details = details;
110
+ this.name = "ConnectedUsageBillingError";
111
+ }
112
+ code;
113
+ httpStatus;
114
+ details;
115
+ };
116
+ function hash(value) {
117
+ return createHash("sha256").update(value).digest("hex");
118
+ }
119
+ function eventKey(idempotencyKey) {
120
+ return `connected-usage:${hash(idempotencyKey)}`;
121
+ }
122
+ function debitKey(idempotencyKey) {
123
+ return `connected-usage-debit:${hash(idempotencyKey)}`;
124
+ }
125
+ function normalizeIdentity(identity) {
126
+ return identity.trim().toLowerCase();
127
+ }
128
+ async function resolveUser(identity) {
129
+ const user = await getUserByEmail(normalizeIdentity(identity));
130
+ if (!user) throw new ConnectedUsageBillingError("user_not_found", "billing identity was not found", 404);
131
+ return user;
132
+ }
133
+ function canonicalFingerprint(args) {
134
+ const metadata = {
135
+ toolName: args.metadata.toolName ?? null,
136
+ operationKind: args.metadata.operationKind ?? null,
137
+ outcome: args.metadata.outcome ?? null,
138
+ requestId: args.metadata.requestId ?? null,
139
+ occurrenceId: args.metadata.occurrenceId ?? null,
140
+ scheduleId: args.metadata.scheduleId ?? null,
141
+ nangoOperationId: args.metadata.nangoOperationId ?? null,
142
+ startedAt: args.metadata.startedAt ?? null,
143
+ completedAt: args.metadata.completedAt ?? null
144
+ };
145
+ return hash(JSON.stringify({
146
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
147
+ connectionId: args.connectionId,
148
+ providerConfigKey: args.providerConfigKey,
149
+ usage: args.usage,
150
+ sourceSurface: args.sourceSurface,
151
+ metadata
152
+ }));
153
+ }
154
+ function encodeMetadata(metadata) {
155
+ const encoded = JSON.stringify(metadata);
156
+ if (encoded.length > 4e3) throw new ConnectedUsageBillingError("invalid_request", "connected usage metadata is too large", 400);
157
+ return encoded;
158
+ }
159
+ function decodeMetadata(row) {
160
+ try {
161
+ const parsed = JSON.parse(row.metadata ?? "");
162
+ if (parsed.contract !== "connected_usage_receipt" || parsed.ratePolicyVersion !== CONNECTED_USAGE_RATE_POLICY_VERSION || typeof parsed.fingerprint !== "string" || typeof parsed.connectionId !== "string" || typeof parsed.providerConfigKey !== "string" || !parsed.usage || !parsed.charge) {
163
+ throw new Error("invalid connected usage receipt metadata");
164
+ }
165
+ return parsed;
166
+ } catch {
167
+ throw new ConnectedUsageBillingError("idempotency_key_conflict", "the idempotency key is already bound to an incompatible billing receipt", 409);
168
+ }
169
+ }
170
+ async function readEvent(key) {
171
+ const result = await getDb().execute({
172
+ sql: "SELECT id, user_id, billing_class, source_surface, status, amount_mc, metadata FROM billing_events WHERE idempotency_key = ? LIMIT 1",
173
+ args: [key]
174
+ });
175
+ return result.rows[0] ? result.rows[0] : null;
176
+ }
177
+ async function hasOtherPendingReceipt(userId, excludingKey) {
178
+ const result = await getDb().execute({
179
+ sql: `SELECT 1
180
+ FROM billing_events
181
+ WHERE user_id = ?
182
+ AND billing_class = ?
183
+ AND status = 'cost_pending'
184
+ ${excludingKey ? "AND idempotency_key <> ?" : ""}
185
+ LIMIT 1`,
186
+ args: excludingKey ? [userId, CONNECTED_USAGE_BILLING_CLASS, excludingKey] : [userId, CONNECTED_USAGE_BILLING_CLASS]
187
+ });
188
+ return result.rows.length > 0;
189
+ }
190
+ function debitKeyFromEventKey(key) {
191
+ const prefix = "connected-usage:";
192
+ const digest = key.startsWith(prefix) ? key.slice(prefix.length) : "";
193
+ return /^[a-f0-9]{64}$/.test(digest) ? `connected-usage-debit:${digest}` : null;
194
+ }
195
+ async function retryPendingConnectedUsageReceipts(user) {
196
+ const result = await getDb().execute({
197
+ sql: `SELECT id, user_id, idempotency_key, billing_class, source_surface, status, amount_mc, metadata
198
+ FROM billing_events
199
+ WHERE user_id = ? AND billing_class = ? AND status = 'cost_pending'
200
+ ORDER BY created_at ASC
201
+ LIMIT 10`,
202
+ args: [user.id, CONNECTED_USAGE_BILLING_CLASS]
203
+ });
204
+ for (const raw of result.rows) {
205
+ const row = raw;
206
+ const key = String(row.idempotency_key ?? "");
207
+ const durableDebitKey = debitKeyFromEventKey(key);
208
+ let metadata;
209
+ try {
210
+ metadata = decodeMetadata(row);
211
+ } catch {
212
+ return;
213
+ }
214
+ if (!durableDebitKey || Number(row.user_id) !== user.id || row.billing_class !== CONNECTED_USAGE_BILLING_CLASS || Number(row.amount_mc) !== metadata.charge.amountMc) {
215
+ return;
216
+ }
217
+ if (metadata.charge.amountMc > 0) {
218
+ const debit = await debitMcIdempotent(
219
+ user.id,
220
+ metadata.charge.amountMc,
221
+ LedgerOperation.CONNECTED_USAGE,
222
+ `connected-usage:${metadata.providerConfigKey}`,
223
+ durableDebitKey
224
+ );
225
+ if (!debit.ok) return;
226
+ }
227
+ const settledMetadata = { ...metadata };
228
+ delete settledMetadata.reason;
229
+ await getDb().execute({
230
+ sql: `UPDATE billing_events
231
+ SET status = 'settled', metadata = ?, settled_at = datetime('now')
232
+ WHERE idempotency_key = ? AND user_id = ? AND billing_class = ? AND status = 'cost_pending'`,
233
+ args: [encodeMetadata(settledMetadata), key, user.id, CONNECTED_USAGE_BILLING_CLASS]
234
+ });
235
+ }
236
+ }
237
+ function assertRatePolicyVersion(ratePolicyVersion) {
238
+ if (ratePolicyVersion !== CONNECTED_USAGE_RATE_POLICY_VERSION) {
239
+ throw new ConnectedUsageBillingError(
240
+ "rate_policy_mismatch",
241
+ "connected usage receipt rate policy does not match the active policy",
242
+ 409,
243
+ { expectedRatePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION }
244
+ );
245
+ }
246
+ }
247
+ async function preflightConnectedUsage(rawInput) {
248
+ const parsed = ConnectedUsagePreflightInputSchema.safeParse(rawInput);
249
+ if (!parsed.success) {
250
+ throw new ConnectedUsageBillingError("invalid_request", "invalid connected usage preflight request", 400);
251
+ }
252
+ assertRatePolicyVersion(parsed.data.ratePolicyVersion);
253
+ const user = await resolveUser(parsed.data.identity);
254
+ await retryPendingConnectedUsageReceipts(user);
255
+ if (await hasOtherPendingReceipt(user.id)) {
256
+ throw new ConnectedUsageBillingError(
257
+ "pending_cost",
258
+ "a previous connected usage receipt has unresolved cost",
259
+ 409
260
+ );
261
+ }
262
+ const balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
263
+ if (balanceMc < CONNECTED_FUNCTION_RUN_MC) {
264
+ throw new ConnectedUsageBillingError(
265
+ "insufficient_credits",
266
+ "the wallet cannot cover the minimum connected function charge",
267
+ 402,
268
+ { balanceMc, requiredMc: CONNECTED_FUNCTION_RUN_MC }
269
+ );
270
+ }
271
+ return {
272
+ allowed: true,
273
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
274
+ minimumMc: CONNECTED_FUNCTION_RUN_MC,
275
+ minimumCredits: CONNECTED_FUNCTION_RUN_CREDITS,
276
+ balanceMc
277
+ };
278
+ }
279
+ function assertMatchingReceipt(row, userId, sourceSurface, fingerprint, amountMc) {
280
+ const metadata = decodeMetadata(row);
281
+ if (Number(row.user_id) !== userId || row.billing_class !== CONNECTED_USAGE_BILLING_CLASS || row.source_surface !== sourceSurface || Number(row.amount_mc) !== amountMc || metadata.fingerprint !== fingerprint) {
282
+ throw new ConnectedUsageBillingError("idempotency_key_conflict", "the idempotency key is already bound to different connected usage", 409);
283
+ }
284
+ return metadata;
285
+ }
286
+ function costBreakdown(usage, charge) {
287
+ return [
288
+ {
289
+ key: "function_runs",
290
+ label: "Connected function runs",
291
+ quantity: usage.functionRuns,
292
+ quantityUnit: "function_run",
293
+ rateCredits: CONNECTED_FUNCTION_RUN_CREDITS,
294
+ rateUnit: "function_run",
295
+ amountMc: charge.functionRunMc,
296
+ credits: charge.functionRunMc / MC_PER_CREDIT
297
+ },
298
+ {
299
+ key: "proxy_requests",
300
+ label: "Connected proxy requests",
301
+ quantity: usage.proxyRequests,
302
+ quantityUnit: "proxy_request",
303
+ rateCredits: CONNECTED_PROXY_REQUEST_CREDITS,
304
+ rateUnit: "proxy_request",
305
+ amountMc: charge.proxyRequestMc,
306
+ credits: charge.proxyRequestMc / MC_PER_CREDIT
307
+ },
308
+ {
309
+ key: "compute",
310
+ label: "Connected function compute",
311
+ quantity: usage.computeMilliseconds,
312
+ quantityUnit: "millisecond",
313
+ rateCredits: CONNECTED_COMPUTE_CREDITS_PER_SECOND,
314
+ rateUnit: "compute_second",
315
+ amountMc: charge.computeMc,
316
+ credits: charge.computeMc / MC_PER_CREDIT
317
+ }
318
+ ];
319
+ }
320
+ function mapSettlement(row, metadata, balanceMc, duplicate) {
321
+ const reason = row.status === "cost_pending" ? metadata.reason : void 0;
322
+ return {
323
+ id: row.id,
324
+ status: row.status === "settled" ? "settled" : "cost_pending",
325
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
326
+ connectionId: metadata.connectionId,
327
+ providerConfigKey: metadata.providerConfigKey,
328
+ amountMc: metadata.charge.amountMc,
329
+ credits: metadata.charge.credits,
330
+ functionRunMc: metadata.charge.functionRunMc,
331
+ proxyRequestMc: metadata.charge.proxyRequestMc,
332
+ computeMc: metadata.charge.computeMc,
333
+ usage: metadata.usage,
334
+ costBreakdown: costBreakdown(metadata.usage, metadata.charge),
335
+ balanceMc,
336
+ ...reason ? { reason } : {},
337
+ duplicate
338
+ };
339
+ }
340
+ async function settleConnectedUsage(rawInput) {
341
+ const parsed = ConnectedUsageSettlementInputSchema.safeParse(rawInput);
342
+ if (!parsed.success) {
343
+ throw new ConnectedUsageBillingError(
344
+ "invalid_request",
345
+ "invalid connected usage settlement request",
346
+ 400,
347
+ { fields: [...new Set(parsed.error.issues.map((issue) => issue.path.join(".")).filter(Boolean))] }
348
+ );
349
+ }
350
+ const input = parsed.data;
351
+ assertRatePolicyVersion(input.ratePolicyVersion);
352
+ const user = await resolveUser(input.identity);
353
+ const usage = {
354
+ functionRuns: input.functionRuns,
355
+ proxyRequests: input.proxyRequests,
356
+ computeMilliseconds: input.computeMilliseconds
357
+ };
358
+ const charge = calculateConnectedUsageCharge(usage);
359
+ const safeMetadata = input.metadata ?? {};
360
+ const fingerprint = canonicalFingerprint({
361
+ connectionId: input.connectionId,
362
+ providerConfigKey: input.providerConfigKey,
363
+ usage,
364
+ sourceSurface: input.sourceSurface,
365
+ metadata: safeMetadata
366
+ });
367
+ const storedMetadata = {
368
+ contract: "connected_usage_receipt",
369
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
370
+ fingerprint,
371
+ connectionId: input.connectionId,
372
+ providerConfigKey: input.providerConfigKey,
373
+ usage,
374
+ charge,
375
+ safeMetadata
376
+ };
377
+ const key = eventKey(input.idempotencyKey);
378
+ if (await hasOtherPendingReceipt(user.id, key)) {
379
+ throw new ConnectedUsageBillingError(
380
+ "pending_cost",
381
+ "a previous connected usage receipt has unresolved cost",
382
+ 409
383
+ );
384
+ }
385
+ const inserted = await getDb().execute({
386
+ sql: `INSERT OR IGNORE INTO billing_events
387
+ (id, user_id, idempotency_key, billing_class, source_surface, status, amount_mc, metadata)
388
+ VALUES (?, ?, ?, ?, ?, 'settling', ?, ?)`,
389
+ args: [randomUUID(), user.id, key, CONNECTED_USAGE_BILLING_CLASS, input.sourceSurface, charge.amountMc, encodeMetadata(storedMetadata)]
390
+ });
391
+ let event = await readEvent(key);
392
+ if (!event) throw new Error("connected usage receipt insert completed without a readable event");
393
+ let eventMetadata = assertMatchingReceipt(event, user.id, input.sourceSurface, fingerprint, charge.amountMc);
394
+ const duplicate = inserted.rowsAffected === 0;
395
+ if (event.status === "settled") {
396
+ return mapSettlement(event, eventMetadata, await reconcileBalanceMc(user.id, user.balance_mc), true);
397
+ }
398
+ if (!["settling", "cost_pending"].includes(event.status)) {
399
+ throw new ConnectedUsageBillingError("idempotency_key_conflict", "the connected usage receipt has an incompatible status", 409);
400
+ }
401
+ if (event.status === "cost_pending") {
402
+ await getDb().execute({
403
+ sql: "UPDATE billing_events SET status = 'settling', metadata = ? WHERE idempotency_key = ? AND status = 'cost_pending'",
404
+ args: [encodeMetadata(storedMetadata), key]
405
+ });
406
+ }
407
+ if (charge.amountMc === 0) {
408
+ await getDb().execute({
409
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'settling'",
410
+ args: [key]
411
+ });
412
+ } else {
413
+ const debit = await debitMcIdempotent(
414
+ user.id,
415
+ charge.amountMc,
416
+ LedgerOperation.CONNECTED_USAGE,
417
+ `connected-usage:${input.providerConfigKey}`,
418
+ debitKey(input.idempotencyKey)
419
+ );
420
+ if (!debit.ok) {
421
+ const pendingMetadata = { ...storedMetadata, reason: "insufficient_credits" };
422
+ await getDb().execute({
423
+ sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE idempotency_key = ? AND status = 'settling'",
424
+ args: [encodeMetadata(pendingMetadata), key]
425
+ });
426
+ event = await readEvent(key);
427
+ if (!event) throw new Error("connected usage pending receipt was lost");
428
+ eventMetadata = assertMatchingReceipt(event, user.id, input.sourceSurface, fingerprint, charge.amountMc);
429
+ return mapSettlement(event, eventMetadata, debit.balance_mc, duplicate || debit.duplicate);
430
+ }
431
+ await getDb().execute({
432
+ sql: "UPDATE billing_events SET status = 'settled', metadata = ?, settled_at = datetime('now') WHERE idempotency_key = ? AND status IN ('settling', 'cost_pending')",
433
+ args: [encodeMetadata(storedMetadata), key]
434
+ });
435
+ if (!debit.duplicate) await recordNangoUsageCost(user.id, usage);
436
+ }
437
+ event = await readEvent(key);
438
+ if (!event) throw new Error("connected usage settlement completed without an event receipt");
439
+ eventMetadata = assertMatchingReceipt(event, user.id, input.sourceSurface, fingerprint, charge.amountMc);
440
+ return mapSettlement(
441
+ event,
442
+ eventMetadata,
443
+ await reconcileBalanceMc(user.id, user.balance_mc),
444
+ duplicate
445
+ );
446
+ }
447
+ async function listConnectedUsageHistory(userId, limit = 100) {
448
+ const result = await getDb().execute({
449
+ sql: `SELECT id, user_id, billing_class, source_surface, status, amount_mc, metadata, created_at
450
+ FROM billing_events
451
+ WHERE user_id = ? AND billing_class = ? AND status IN ('settled', 'cost_pending')
452
+ ORDER BY created_at DESC
453
+ LIMIT ?`,
454
+ args: [userId, CONNECTED_USAGE_BILLING_CLASS, Math.max(1, Math.min(250, Math.round(limit)))]
455
+ });
456
+ const history = [];
457
+ for (const raw of result.rows) {
458
+ const row = raw;
459
+ try {
460
+ const receipt = decodeMetadata(row);
461
+ history.push({
462
+ id: row.id,
463
+ createdAt: String(row.created_at ?? ""),
464
+ status: row.status === "settled" ? "settled" : "cost_pending",
465
+ connectionId: receipt.connectionId,
466
+ providerConfigKey: receipt.providerConfigKey,
467
+ amountMc: receipt.charge.amountMc,
468
+ credits: receipt.charge.credits,
469
+ usage: receipt.usage,
470
+ costBreakdown: costBreakdown(receipt.usage, receipt.charge),
471
+ metadata: receipt.safeMetadata
472
+ });
473
+ } catch {
474
+ }
475
+ }
476
+ return history;
477
+ }
478
+
479
+ // src/api/connected-tool-policy.ts
480
+ function classifyConnectedTool(tool) {
481
+ if (tool.annotations?.readOnlyHint === true) return "read";
482
+ if (tool.annotations?.readOnlyHint === false) return "action";
483
+ return /^(get|list|search|query|read|download|fetch|find|lookup|inspect|describe|check)-/i.test(tool.name) ? "read" : "action";
484
+ }
485
+ function normalizedPolicyMap(value) {
486
+ return new Map(Object.entries(value ?? {}).map(([name, entries]) => [
487
+ name,
488
+ [...new Set(entries.map((entry) => entry.trim()).filter(Boolean))].sort()
489
+ ]));
490
+ }
491
+ function buildConnectedToolPolicyRegistry(args) {
492
+ const permissionPolicies = normalizedPolicyMap(args.requiredPermissionsByTool);
493
+ const featurePolicies = normalizedPolicyMap(args.requiredFeaturesByTool);
494
+ const granted = new Set(args.grantedPermissions ?? []);
495
+ const enabled = new Set(args.enabledFeatures ?? []);
496
+ const adminBlocked = new Set(args.adminBlockedTools ?? []);
497
+ const policies = /* @__PURE__ */ new Map();
498
+ for (const tool of args.tools) {
499
+ const classification = classifyConnectedTool(tool);
500
+ const requiredPermissions = permissionPolicies.get(tool.name) ?? [];
501
+ const requiredFeatures = featurePolicies.get(tool.name) ?? [];
502
+ const missingPermissions = requiredPermissions.filter((permission) => !granted.has(permission));
503
+ const missingFeatures = requiredFeatures.filter((feature) => !enabled.has(feature));
504
+ const blockedReason = !args.connectionActive ? "inactive_connection" : adminBlocked.has(tool.name) ? "admin_blocked" : classification === "action" && !args.actionsEnabled ? "actions_disabled" : requiredPermissions.length > 0 && args.permissionVerification === "unavailable" ? "permission_verification_unavailable" : missingPermissions.length > 0 ? "missing_permission" : missingFeatures.length > 0 ? "missing_app_feature" : null;
505
+ policies.set(tool.name, {
506
+ name: tool.name,
507
+ classification,
508
+ requiredPermissions,
509
+ requiredFeatures,
510
+ available: blockedReason === null,
511
+ blockedReason,
512
+ missingPermissions,
513
+ missingFeatures
514
+ });
515
+ }
516
+ return policies;
517
+ }
518
+ function connectedToolPolicyOrThrow(registry, toolName) {
519
+ const policy = registry.get(toolName);
520
+ if (!policy) throw new Error("connected_tool_policy_missing");
521
+ return policy;
522
+ }
523
+
524
+ // src/api/main-nango-transport.ts
525
+ var DEFAULT_NANGO_MCP_URL = "https://api.nango.dev/mcp";
526
+ var NANGO_TIMEOUT_MS = 2e4;
527
+ var NANGO_TOOL_CALL_TIMEOUT_MS = 24e4;
528
+ var NANGO_CONNECTION_PAGE_SIZE = 100;
529
+ var NANGO_CONNECTION_MAX_PAGES = 20;
530
+ var PROVIDER_IDENTITY_RETRY_MS = 6 * 60 * 60 * 1e3;
531
+ var MAIN_INTEGRATION_CONTRACT_VERSION = "2026-07-16.1";
532
+ var MAIN_INTEGRATION_CONTRACT_HASH = "f037859ec80c2ea2fac6e4a67c1a59f3200dd22d3642c97789b9bb3236723d3e";
533
+ var MainNangoTransportError = class extends Error {
534
+ status;
535
+ code;
536
+ retryable;
537
+ retryAfterMs;
538
+ constructor(message, status, code, retryable = status === 429 || status >= 500, retryAfterMs = null) {
539
+ super(message);
540
+ this.name = "MainNangoTransportError";
541
+ this.status = status;
542
+ this.code = code;
543
+ this.retryable = retryable;
544
+ this.retryAfterMs = retryAfterMs;
545
+ }
546
+ };
547
+ function nangoClientErrorText(error) {
548
+ if (error instanceof Error) return error.message;
549
+ if (typeof error === "string") return error;
550
+ try {
551
+ return JSON.stringify(error);
552
+ } catch {
553
+ return "";
554
+ }
555
+ }
556
+ function retryAfterMsFromText(value) {
557
+ const milliseconds = value.match(/retry[_ -]?after[_ -]?ms[^0-9]{0,20}(\d{1,7})/i);
558
+ if (milliseconds) return Math.min(Math.max(Number(milliseconds[1]), 1e3), 6e4);
559
+ const seconds = value.match(/retry[_ -]?after[^0-9]{0,20}(\d{1,4})/i);
560
+ if (seconds) return Math.min(Math.max(Number(seconds[1]), 1) * 1e3, 6e4);
561
+ return null;
562
+ }
563
+ function normalizeNangoClientError(error) {
564
+ if (error instanceof MainNangoTransportError) return error;
565
+ const detail = nangoClientErrorText(error);
566
+ const structured = error && typeof error === "object" ? error : null;
567
+ if (structured?.code === -32001 || structured?.name === "RequestTimeout" || /Request timed out|RequestTimeout|Maximum total timeout exceeded/i.test(detail)) {
568
+ return new MainNangoTransportError(
569
+ "The connected service operation timed out before returning a result. Retry failed items or use a smaller batch.",
570
+ 503,
571
+ "mcp_request_timeout",
572
+ true
573
+ );
574
+ }
575
+ if (/SERVICE_DISABLED|accessNotConfigured/i.test(detail)) {
576
+ return new MainNangoTransportError(
577
+ "The provider API required by this connection is disabled. Enable it in the provider project, wait for propagation, then retry.",
578
+ 409,
579
+ "provider_api_disabled",
580
+ false
581
+ );
582
+ }
583
+ if (/PERMISSION_DENIED|insufficientPermissions|insufficient permission/i.test(detail)) {
584
+ return new MainNangoTransportError(
585
+ "The connected account does not have permission to run this provider operation.",
586
+ 403,
587
+ "missing_provider_permission",
588
+ false
589
+ );
590
+ }
591
+ if (/RESOURCE_EXHAUSTED|rate.?limit|status["']?\s*:\s*429/i.test(detail)) {
592
+ return new MainNangoTransportError(
593
+ "The connected provider is temporarily rate limited.",
594
+ 429,
595
+ "upstream_rate_limited",
596
+ true,
597
+ retryAfterMsFromText(detail)
598
+ );
599
+ }
600
+ if (/UNAUTHENTICATED|invalid_grant|status["']?\s*:\s*401/i.test(detail)) {
601
+ return new MainNangoTransportError("The service connection requires reconnection.", 409, "connection_inactive", false);
602
+ }
603
+ return new MainNangoTransportError("The service connection is temporarily unavailable.", 503, "connection_transport_unavailable");
604
+ }
605
+ async function callNangoToolWithBudget(client, tool, input) {
606
+ return client.callTool(
607
+ { name: tool, arguments: input },
608
+ { timeout: NANGO_TOOL_CALL_TIMEOUT_MS }
609
+ );
610
+ }
611
+ function asRecord(value) {
612
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
613
+ }
614
+ function text(value) {
615
+ return typeof value === "string" && value.trim() ? value.trim() : null;
616
+ }
617
+ function arrayAt(payload, key) {
618
+ if (Array.isArray(payload)) return payload;
619
+ const root = asRecord(payload);
620
+ if (Array.isArray(root?.[key])) return root[key];
621
+ if (Array.isArray(root?.data)) return root.data;
622
+ const data = asRecord(root?.data);
623
+ return Array.isArray(data?.[key]) ? data[key] : [];
624
+ }
625
+ function nangoSecret() {
626
+ const secret = process.env.NANGO_SECRET_KEY?.trim();
627
+ if (!secret) throw new MainNangoTransportError("Nango is not configured.", 503, "nango_not_configured");
628
+ return secret;
629
+ }
630
+ function nangoMcpUrl() {
631
+ try {
632
+ return new URL(process.env.NANGO_MCP_URL?.trim() || DEFAULT_NANGO_MCP_URL);
633
+ } catch {
634
+ throw new MainNangoTransportError("Nango MCP URL is invalid.", 503, "nango_invalid_url");
635
+ }
636
+ }
637
+ async function nangoRequest(path, options = {}) {
638
+ const url = new URL(path, `${nangoMcpUrl().origin}/`);
639
+ for (const [key, value] of Object.entries(options.query ?? {})) {
640
+ if (value !== void 0) url.searchParams.set(key, String(value));
641
+ }
642
+ let response;
643
+ try {
644
+ response = await fetch(url, {
645
+ method: options.method ?? "GET",
646
+ headers: {
647
+ authorization: `Bearer ${nangoSecret()}`,
648
+ accept: "application/json",
649
+ ...options.body ? { "content-type": "application/json" } : {},
650
+ ...options.headers
651
+ },
652
+ body: options.body ? JSON.stringify(options.body) : void 0,
653
+ signal: AbortSignal.timeout(NANGO_TIMEOUT_MS)
654
+ });
655
+ } catch {
656
+ throw new MainNangoTransportError("Nango is temporarily unavailable.", 503, "connection_transport_unavailable");
657
+ }
658
+ const raw = await response.text();
659
+ let payload = {};
660
+ try {
661
+ payload = raw ? JSON.parse(raw) : {};
662
+ } catch {
663
+ throw new MainNangoTransportError("Nango returned an invalid response.", 502, "nango_invalid_response");
664
+ }
665
+ if (!response.ok) {
666
+ const status = response.status === 400 || response.status === 403 || response.status === 404 || response.status === 409 || response.status === 429 || response.status === 503 ? response.status : 502;
667
+ throw new MainNangoTransportError(
668
+ status === 429 ? "The connected provider is temporarily rate limited." : "Nango request failed.",
669
+ status,
670
+ status === 429 ? "upstream_rate_limited" : status === 404 ? "connection_not_found" : "connection_transport_unavailable",
671
+ status === 429 || status >= 500,
672
+ status === 429 ? retryAfterMsFromText(`retry-after: ${response.headers.get("retry-after") ?? ""}`) : null
673
+ );
674
+ }
675
+ return payload;
676
+ }
677
+ function normalizedIdentity(value) {
678
+ const valueText = text(value);
679
+ return valueText ? valueText.normalize("NFKC").toLowerCase() : null;
680
+ }
681
+ function connectionIdentityMatches(value, identity) {
682
+ const row = asRecord(value);
683
+ const tags = asRecord(row?.tags);
684
+ const endUser = asRecord(row?.end_user);
685
+ const expected = normalizedIdentity(identity);
686
+ if (!expected) return false;
687
+ const emails = [normalizedIdentity(tags?.end_user_email), normalizedIdentity(endUser?.email)].filter((candidate) => !!candidate);
688
+ if (emails.some((candidate) => candidate !== expected)) return false;
689
+ const ids = [normalizedIdentity(tags?.end_user_id), normalizedIdentity(endUser?.id)].filter((candidate) => !!candidate && candidate.includes("@"));
690
+ if (ids.some((candidate) => candidate !== expected)) return false;
691
+ return emails.length > 0 ? emails.includes(expected) : ids.includes(expected);
692
+ }
693
+ function discoveredConnection(value) {
694
+ const row = asRecord(value);
695
+ const upstreamConnectionId = text(row?.connection_id);
696
+ const providerConfigKey = text(row?.provider_config_key);
697
+ if (!upstreamConnectionId || !providerConfigKey) return null;
698
+ const errors = Array.isArray(row?.errors) ? row.errors : [];
699
+ const authError = errors.some((error) => text(asRecord(error)?.type) === "auth");
700
+ return {
701
+ upstreamConnectionId,
702
+ providerConfigKey,
703
+ provider: text(row?.provider) ?? providerConfigKey,
704
+ // Nango's end-user fields identify the MCP Scraper tenant, not the
705
+ // provider-side principal that authorized this connection.
706
+ label: null,
707
+ lifecycleStatus: authError ? "needs_reauth" : "connected",
708
+ reconnectRequired: authError,
709
+ createdAt: text(row?.created_at) ?? text(row?.created),
710
+ updatedAt: text(row?.updated_at)
711
+ };
712
+ }
713
+ var IDENTITY_SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set([
714
+ "facebook",
715
+ "github-getting-started",
716
+ "google-analytics",
717
+ "google-calendar",
718
+ "google-drive",
719
+ "google-mail",
720
+ "google-search-console",
721
+ "linkedin",
722
+ "meta-marketing-api",
723
+ "slack",
724
+ "twitter-v2",
725
+ "xero",
726
+ "youtube",
727
+ "zoom"
728
+ ]);
729
+ function scalarId(value) {
730
+ const valueText = text(value);
731
+ if (valueText) return valueText;
732
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
733
+ }
734
+ function providerEmail(value) {
735
+ const email = text(value)?.toLowerCase() ?? null;
736
+ return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : null;
737
+ }
738
+ function compactProviderIdentity(identity) {
739
+ return identity.id || identity.email || identity.name ? identity : null;
740
+ }
741
+ async function providerProxyJson(args) {
742
+ return nangoRequest(`/proxy${args.path}`, {
743
+ query: args.query,
744
+ headers: {
745
+ "connection-id": args.upstreamConnectionId,
746
+ "provider-config-key": args.providerConfigKey,
747
+ ...args.baseUrlOverride ? { "base-url-override": args.baseUrlOverride } : {}
748
+ }
749
+ });
750
+ }
751
+ var GMAIL_PROXY_PATHS = [
752
+ { method: "GET", pattern: /^\/gmail\/v1\/users\/me\/messages$/ },
753
+ { method: "GET", pattern: /^\/gmail\/v1\/users\/me\/messages\/[A-Za-z0-9_-]+$/ },
754
+ { method: "GET", pattern: /^\/gmail\/v1\/users\/me\/messages\/[A-Za-z0-9_-]+\/attachments\/[A-Za-z0-9_-]+$/ },
755
+ { method: "GET", pattern: /^\/gmail\/v1\/users\/me\/labels$/ },
756
+ { method: "POST", pattern: /^\/gmail\/v1\/users\/me\/messages\/batchModify$/ },
757
+ { method: "POST", pattern: /^\/gmail\/v1\/users\/me\/messages\/batchDelete$/ },
758
+ { method: "POST", pattern: /^\/gmail\/v1\/users\/me\/messages\/[A-Za-z0-9_-]+\/(?:trash|untrash)$/ }
759
+ ];
760
+ var GMAIL_QUERY_KEYS = /* @__PURE__ */ new Set(["q", "maxResults", "pageToken", "format", "metadataHeaders", "fields"]);
761
+ async function callGmailProviderProxyDirect(args) {
762
+ const connection = await requireOwnedActiveConnection(args.identity, args.connectionId);
763
+ if (connection.providerConfigKey !== "google-mail") {
764
+ throw new MainNangoTransportError("Service connection not found.", 404, "connection_not_found", false);
765
+ }
766
+ if (!GMAIL_PROXY_PATHS.some((rule) => rule.method === args.method && rule.pattern.test(args.path))) {
767
+ throw new MainNangoTransportError("The Gmail provider operation is not allowed.", 403, "tool_not_allowed", false);
768
+ }
769
+ if (Object.keys(args.query ?? {}).some((key) => !GMAIL_QUERY_KEYS.has(key))) {
770
+ throw new MainNangoTransportError("The Gmail provider query is not allowed.", 400, "invalid_request", false);
771
+ }
772
+ const isAction = args.method === "POST";
773
+ if (isAction && !connection.actionsEnabled) {
774
+ throw new MainNangoTransportError("Actions are disabled for this service connection.", 403, "actions_disabled", false);
775
+ }
776
+ await preflightConnectedUsage({ identity: args.identity, ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION });
777
+ const requestId = args.requestId?.trim() || randomUUID2();
778
+ const idempotencyKey = `main-gmail-proxy:${createHash2("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.method).update("\0").update(args.path).update("\0").update(requestId).digest("hex")}`;
779
+ const started = performance.now();
780
+ let result;
781
+ let providerError;
782
+ try {
783
+ result = await nangoRequest(`/proxy${args.path}`, {
784
+ method: args.method,
785
+ query: args.query,
786
+ body: args.body,
787
+ headers: {
788
+ "connection-id": connection.upstreamConnectionId,
789
+ "provider-config-key": "google-mail",
790
+ "base-url-override": "https://gmail.googleapis.com"
791
+ }
792
+ });
793
+ } catch (error) {
794
+ providerError = error;
795
+ }
796
+ await settleConnectedUsage({
797
+ identity: args.identity,
798
+ idempotencyKey,
799
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
800
+ connectionId: connection.id,
801
+ providerConfigKey: connection.providerConfigKey,
802
+ functionRuns: 0,
803
+ proxyRequests: 1,
804
+ computeMilliseconds: Math.max(0, Math.round(performance.now() - started)),
805
+ sourceSurface: "main_mcp",
806
+ metadata: {
807
+ operationKind: isAction ? "action" : "read",
808
+ outcome: providerError ? "error" : "success",
809
+ requestId: requestId.length <= 200 ? requestId : createHash2("sha256").update(requestId).digest("hex")
810
+ }
811
+ });
812
+ if (providerError) throw normalizeNangoClientError(providerError);
813
+ return result;
814
+ }
815
+ async function googleProviderAccountIdentity(args) {
816
+ const request = (path, query, baseUrlOverride) => providerProxyJson({ ...args, path, query, baseUrlOverride });
817
+ try {
818
+ if (args.providerConfigKey === "google-mail") {
819
+ const payload = asRecord(await request("/gmail/v1/users/me/profile", void 0, "https://gmail.googleapis.com"));
820
+ const email = providerEmail(payload?.emailAddress);
821
+ return compactProviderIdentity({ id: email, email, name: null });
822
+ }
823
+ if (args.providerConfigKey === "google-drive") {
824
+ const payload = asRecord(await request("/drive/v3/about", { fields: "user(permissionId,displayName,emailAddress)" }, "https://www.googleapis.com"));
825
+ const user = asRecord(payload?.user);
826
+ return compactProviderIdentity({
827
+ id: text(user?.permissionId),
828
+ email: providerEmail(user?.emailAddress),
829
+ name: text(user?.displayName)
830
+ });
831
+ }
832
+ if (args.providerConfigKey === "google-calendar") {
833
+ const payload = asRecord(await request("/calendar/v3/calendars/primary", void 0, "https://www.googleapis.com"));
834
+ const id = text(payload?.id);
835
+ return compactProviderIdentity({ id, email: providerEmail(id), name: text(payload?.summary) });
836
+ }
837
+ if (args.providerConfigKey === "youtube") {
838
+ const payload = asRecord(await request("/youtube/v3/channels", { part: "snippet", mine: "true", maxResults: 1 }, "https://www.googleapis.com"));
839
+ const first = Array.isArray(payload?.items) ? asRecord(payload.items[0]) : null;
840
+ return compactProviderIdentity({ id: text(first?.id), email: null, name: text(asRecord(first?.snippet)?.title) });
841
+ }
842
+ } catch {
843
+ }
844
+ try {
845
+ const payload = asRecord(await request("/v1/userinfo", void 0, "https://openidconnect.googleapis.com"));
846
+ return compactProviderIdentity({
847
+ id: text(payload?.sub),
848
+ email: payload?.email_verified === false ? null : providerEmail(payload?.email),
849
+ name: text(payload?.name)
850
+ });
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ async function getNangoProviderAccountIdentity(args) {
856
+ if (!IDENTITY_SUPPORTED_PROVIDERS.has(args.providerConfigKey)) return null;
857
+ if (args.providerConfigKey.startsWith("google-") || args.providerConfigKey === "youtube") {
858
+ return googleProviderAccountIdentity(args);
859
+ }
860
+ try {
861
+ if (args.providerConfigKey === "github-getting-started") {
862
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/user" }));
863
+ return compactProviderIdentity({ id: scalarId(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) ?? text(payload?.login) });
864
+ }
865
+ if (args.providerConfigKey === "slack") {
866
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/api/auth.test" }));
867
+ return compactProviderIdentity({ id: text(payload?.user_id), email: null, name: [text(payload?.user), text(payload?.team)].filter(Boolean).join(" \xB7 ") || null });
868
+ }
869
+ if (args.providerConfigKey === "zoom") {
870
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/v2/users/me" }));
871
+ const fullName = [text(payload?.first_name), text(payload?.last_name)].filter(Boolean).join(" ");
872
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.display_name) ?? (fullName || null) });
873
+ }
874
+ if (args.providerConfigKey === "linkedin") {
875
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/v2/userinfo" }));
876
+ return compactProviderIdentity({ id: text(payload?.sub), email: payload?.email_verified === false ? null : providerEmail(payload?.email), name: text(payload?.name) });
877
+ }
878
+ if (args.providerConfigKey === "meta-marketing-api" || args.providerConfigKey === "facebook") {
879
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/me", query: { fields: "id,name,email" } }));
880
+ return compactProviderIdentity({ id: text(payload?.id), email: providerEmail(payload?.email), name: text(payload?.name) });
881
+ }
882
+ if (args.providerConfigKey === "twitter-v2") {
883
+ const payload = asRecord(await providerProxyJson({ ...args, path: "/2/users/me", query: { "user.fields": "id,name,username" } }));
884
+ const data = asRecord(payload?.data);
885
+ return compactProviderIdentity({ id: text(data?.id), email: null, name: text(data?.username) ?? text(data?.name) });
886
+ }
887
+ if (args.providerConfigKey === "xero") {
888
+ const payload = await providerProxyJson({ ...args, path: "/connections" });
889
+ const first = Array.isArray(payload) ? asRecord(payload[0]) : null;
890
+ return compactProviderIdentity({ id: text(first?.tenantId), email: null, name: text(first?.tenantName) });
891
+ }
892
+ } catch {
893
+ return null;
894
+ }
895
+ return null;
896
+ }
897
+ async function discoverOwnedNangoConnections(identity, options = {}) {
898
+ const rows = [];
899
+ for (let page = 0; page < NANGO_CONNECTION_MAX_PAGES; page += 1) {
900
+ const payload = await nangoRequest("/connections", {
901
+ query: { limit: NANGO_CONNECTION_PAGE_SIZE, page }
902
+ });
903
+ const pageRows = arrayAt(payload, "connections");
904
+ rows.push(...pageRows);
905
+ if (pageRows.length < NANGO_CONNECTION_PAGE_SIZE) break;
906
+ }
907
+ const discovered = rows.filter((row) => connectionIdentityMatches(row, identity)).map(discoveredConnection).filter((connection) => connection !== null);
908
+ const reconciled = await reconcileDiscoveredNangoConnections(identity, discovered);
909
+ if (options.resolveProviderIdentity === false) return reconciled;
910
+ const remoteByKey = new Map(discovered.map((connection) => [
911
+ `${connection.providerConfigKey}\0${connection.upstreamConnectionId}`,
912
+ connection
913
+ ]));
914
+ await Promise.all(reconciled.map(async (connection) => {
915
+ if (connection.lifecycleStatus !== "connected" || !connection.upstreamConnectionId) return;
916
+ const remote = remoteByKey.get(`${connection.providerConfigKey}\0${connection.upstreamConnectionId}`);
917
+ if (!remote) return;
918
+ const sourceUpdatedAt = remote.updatedAt ?? remote.createdAt ?? connection.lifecycleStatus;
919
+ const hasProviderIdentity = Boolean(
920
+ connection.providerAccountId || connection.providerAccountEmail || connection.providerAccountName
921
+ );
922
+ const checkedAt = connection.providerIdentityCheckedAt ? Date.parse(`${connection.providerIdentityCheckedAt.replace(" ", "T")}Z`) : Number.NaN;
923
+ const unavailableIdentityIsFresh = !hasProviderIdentity && Number.isFinite(checkedAt) && Date.now() - checkedAt < PROVIDER_IDENTITY_RETRY_MS;
924
+ if (connection.providerIdentitySourceUpdatedAt === sourceUpdatedAt && (hasProviderIdentity || unavailableIdentityIsFresh)) return;
925
+ const providerIdentity = await getNangoProviderAccountIdentity({
926
+ upstreamConnectionId: connection.upstreamConnectionId,
927
+ providerConfigKey: connection.providerConfigKey
928
+ });
929
+ await setServiceConnectionProviderIdentity({
930
+ connectionId: connection.id,
931
+ providerAccountId: providerIdentity?.id,
932
+ providerAccountEmail: providerIdentity?.email,
933
+ providerAccountName: providerIdentity?.name,
934
+ sourceUpdatedAt
935
+ });
936
+ }));
937
+ return reconcileDiscoveredNangoConnections(identity, discovered);
938
+ }
939
+ async function listNangoCatalogDirect() {
940
+ const [integrationsPayload, providersPayload] = await Promise.all([
941
+ nangoRequest("/integrations"),
942
+ nangoRequest("/providers")
943
+ ]);
944
+ const providers = /* @__PURE__ */ new Map();
945
+ for (const value of arrayAt(providersPayload, "providers")) {
946
+ const row = asRecord(value);
947
+ const name = text(row?.name);
948
+ if (row && name) providers.set(name, row);
949
+ }
950
+ return arrayAt(integrationsPayload, "integrations").flatMap((value) => {
951
+ const row = asRecord(value);
952
+ const id = text(row?.unique_key) ?? text(row?.id);
953
+ const provider = text(row?.provider);
954
+ if (!id || !provider) return [];
955
+ const definition = providers.get(provider);
956
+ return [{
957
+ id,
958
+ provider,
959
+ displayName: text(row?.display_name) ?? text(definition?.display_name) ?? id,
960
+ authMode: text(definition?.auth_mode),
961
+ categories: Array.isArray(definition?.categories) ? definition.categories : [],
962
+ docsUrl: text(definition?.docs),
963
+ logoUrl: text(row?.logo),
964
+ safeDefaultAllowedTools: [],
965
+ actionTools: [],
966
+ setupStatus: "ready"
967
+ }];
968
+ });
969
+ }
970
+ function connectDefaults(providerConfigKey) {
971
+ const scopes = {
972
+ linkedin: "openid profile email w_member_social",
973
+ "github-getting-started": "repo workflow read:org read:user user:email",
974
+ "google-drive": "openid profile email https://www.googleapis.com/auth/drive",
975
+ "google-search-console": "openid profile email https://www.googleapis.com/auth/webmasters.readonly https://www.googleapis.com/auth/webmasters"
976
+ };
977
+ const scope = scopes[providerConfigKey];
978
+ return scope ? { [providerConfigKey]: { connection_config: { oauth_scopes_override: scope } } } : void 0;
979
+ }
980
+ function sessionPayload(value) {
981
+ const root = asRecord(value);
982
+ const data = asRecord(root?.data) ?? root;
983
+ const connectLink = text(data?.connect_link);
984
+ if (!connectLink) throw new MainNangoTransportError("Nango did not return a connect link.", 502, "nango_missing_connect_link");
985
+ return { connectLink, expiresAt: text(data?.expires_at) };
986
+ }
987
+ async function createNangoSessionDirect(identity, providerConfigKey) {
988
+ const defaults = connectDefaults(providerConfigKey);
989
+ return sessionPayload(await nangoRequest("/connect/sessions", {
990
+ method: "POST",
991
+ body: {
992
+ tags: { end_user_id: identity, end_user_email: identity },
993
+ allowed_integrations: [providerConfigKey],
994
+ ...defaults ? { integrations_config_defaults: defaults } : {}
995
+ }
996
+ }));
997
+ }
998
+ async function createNangoReconnectSessionDirect(identity, connectionId) {
999
+ const connection = await requireOwnedActiveConnection(identity, connectionId, true);
1000
+ const defaults = connectDefaults(connection.providerConfigKey);
1001
+ return sessionPayload(await nangoRequest("/connect/sessions/reconnect", {
1002
+ method: "POST",
1003
+ body: {
1004
+ connection_id: connection.upstreamConnectionId,
1005
+ integration_id: connection.providerConfigKey,
1006
+ tags: { end_user_id: identity, end_user_email: identity },
1007
+ ...defaults ? { integrations_config_defaults: defaults } : {}
1008
+ }
1009
+ }));
1010
+ }
1011
+ async function deleteNangoConnectionDirect(identity, connectionId) {
1012
+ const connection = await requireOwnedActiveConnection(identity, connectionId, true);
1013
+ try {
1014
+ await nangoRequest(`/connections/${encodeURIComponent(connection.upstreamConnectionId)}`, {
1015
+ method: "DELETE",
1016
+ query: { provider_config_key: connection.providerConfigKey }
1017
+ });
1018
+ } catch (error) {
1019
+ if (!(error instanceof MainNangoTransportError) || error.status !== 404) throw error;
1020
+ }
1021
+ }
1022
+ async function requireOwnedActiveConnection(identity, connectionId, allowNeedsReauth = false) {
1023
+ const connection = await getOwnedServiceConnection(identity, connectionId);
1024
+ if (!connection || connection.transport !== "nango" || !connection.upstreamConnectionId) {
1025
+ throw new MainNangoTransportError("Service connection not found.", 404, "connection_not_found", false);
1026
+ }
1027
+ if (connection.lifecycleStatus !== "connected" && !(allowNeedsReauth && connection.lifecycleStatus === "needs_reauth")) {
1028
+ throw new MainNangoTransportError("Service connection requires reconnection.", 409, "connection_inactive", false);
1029
+ }
1030
+ return connection;
1031
+ }
1032
+ async function withNangoClient(connection, run) {
1033
+ const transport = new StreamableHTTPClientTransport(nangoMcpUrl(), {
1034
+ requestInit: {
1035
+ headers: {
1036
+ Authorization: `Bearer ${nangoSecret()}`,
1037
+ "connection-id": connection.upstreamConnectionId,
1038
+ "provider-config-key": connection.providerConfigKey
1039
+ }
1040
+ }
1041
+ });
1042
+ const client = new Client({ name: "mcp-scraper-integrations", version: "1.0.0" }, { capabilities: {} });
1043
+ try {
1044
+ await client.connect(transport);
1045
+ const value = await run(client);
1046
+ await recordServiceConnectionHealth({
1047
+ connectionId: connection.id,
1048
+ operationalStatus: "available",
1049
+ evidenceSource: "nango_mcp"
1050
+ });
1051
+ return value;
1052
+ } catch (error) {
1053
+ const normalized = normalizeNangoClientError(error);
1054
+ await recordServiceConnectionHealth({
1055
+ connectionId: connection.id,
1056
+ operationalStatus: normalized.code === "connection_transport_unavailable" ? "unavailable" : "degraded",
1057
+ failureCode: normalized.code,
1058
+ retryable: normalized.retryable,
1059
+ evidenceSource: "nango_mcp"
1060
+ }).catch(() => void 0);
1061
+ throw normalized;
1062
+ } finally {
1063
+ await client.close().catch(() => void 0);
1064
+ }
1065
+ }
1066
+ async function probeNangoConnectionDirect(identity, connectionId) {
1067
+ const connection = await requireOwnedActiveConnection(identity, connectionId);
1068
+ const providerKey = connection.provider ?? connection.providerConfigKey;
1069
+ if (providerKey !== "google-search-console") return;
1070
+ await withNangoClient(connection, async (client) => {
1071
+ const listed = await client.listTools();
1072
+ if (!listed.tools.some((tool) => tool.name === "list-sites")) {
1073
+ throw new MainNangoTransportError("The Search Console health-check tool is unavailable.", 404, "live_tool_missing", false);
1074
+ }
1075
+ await client.callTool({ name: "list-sites", arguments: {} });
1076
+ });
1077
+ }
1078
+ async function listNangoToolsDirect(identity, connectionId) {
1079
+ const connection = await requireOwnedActiveConnection(identity, connectionId);
1080
+ const listed = await withNangoClient(connection, (client) => client.listTools());
1081
+ const tools = listed.tools.flatMap((raw) => {
1082
+ const inputSchema = asRecord(raw.inputSchema);
1083
+ if (!raw.name || !inputSchema) return [];
1084
+ return [{
1085
+ name: raw.name,
1086
+ ...raw.title ? { title: raw.title } : {},
1087
+ ...raw.description ? { description: raw.description } : {},
1088
+ inputSchema,
1089
+ ...asRecord(raw.outputSchema) ? { outputSchema: asRecord(raw.outputSchema) } : {},
1090
+ ...asRecord(raw.annotations) ? { annotations: asRecord(raw.annotations) } : {}
1091
+ }];
1092
+ });
1093
+ const policies = buildConnectedToolPolicyRegistry({
1094
+ tools,
1095
+ connectionActive: connection.lifecycleStatus === "connected",
1096
+ actionsEnabled: connection.actionsEnabled
1097
+ });
1098
+ const readTools = [...policies.values()].filter((policy) => policy.classification === "read").map((policy) => policy.name);
1099
+ const actionTools = [...policies.values()].filter((policy) => policy.classification === "action").map((policy) => policy.name);
1100
+ const revision = createHash2("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
1101
+ await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
1102
+ const refreshed = await getOwnedServiceConnection(identity, connection.id);
1103
+ return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
1104
+ }
1105
+ async function callNangoToolDirect(args) {
1106
+ const { connection, tools } = await listNangoToolsDirect(args.identity, args.connectionId);
1107
+ const selected = tools.find((tool) => tool.name === args.tool);
1108
+ if (!selected) throw new MainNangoTransportError("The requested provider tool is unavailable.", 404, "live_tool_missing", false);
1109
+ const policy = connectedToolPolicyOrThrow(buildConnectedToolPolicyRegistry({
1110
+ tools,
1111
+ connectionActive: connection.lifecycleStatus === "connected",
1112
+ actionsEnabled: connection.actionsEnabled
1113
+ }), selected.name);
1114
+ if (policy.classification !== args.classification) {
1115
+ throw new MainNangoTransportError("The provider tool classification does not match this operation.", 403, "tool_not_allowed", false);
1116
+ }
1117
+ if (!policy.available && policy.blockedReason === "actions_disabled") {
1118
+ throw new MainNangoTransportError("Actions are disabled for this service connection.", 403, "actions_disabled", false);
1119
+ }
1120
+ if (!policy.available) {
1121
+ throw new MainNangoTransportError("The provider tool is not authorized for this connection.", 403, "tool_not_allowed", false);
1122
+ }
1123
+ await preflightConnectedUsage({
1124
+ identity: args.identity,
1125
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
1126
+ });
1127
+ const requestId = args.requestId?.trim() || randomUUID2();
1128
+ const idempotencyKey = `main-nango:${createHash2("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
1129
+ const startedAt = /* @__PURE__ */ new Date();
1130
+ const started = performance.now();
1131
+ let result;
1132
+ let providerError;
1133
+ try {
1134
+ result = await withNangoClient(connection, (client) => callNangoToolWithBudget(client, args.tool, args.input));
1135
+ } catch (error) {
1136
+ providerError = error;
1137
+ }
1138
+ const completedAt = /* @__PURE__ */ new Date();
1139
+ await settleConnectedUsage({
1140
+ identity: args.identity,
1141
+ idempotencyKey,
1142
+ ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
1143
+ connectionId: connection.id,
1144
+ providerConfigKey: connection.providerConfigKey,
1145
+ functionRuns: 1,
1146
+ proxyRequests: 0,
1147
+ computeMilliseconds: Math.max(0, Math.round(performance.now() - started)),
1148
+ sourceSurface: "main_mcp",
1149
+ metadata: {
1150
+ toolName: args.tool,
1151
+ operationKind: args.operationKind ?? args.classification,
1152
+ outcome: providerError ? "error" : "partial",
1153
+ requestId: requestId.length <= 200 ? requestId : createHash2("sha256").update(requestId).digest("hex"),
1154
+ startedAt: startedAt.toISOString(),
1155
+ completedAt: completedAt.toISOString()
1156
+ }
1157
+ });
1158
+ if (providerError) throw providerError;
1159
+ return result;
1160
+ }
1161
+ async function describeNangoToolDirect(identity, connectionId, toolName) {
1162
+ const { connection, tools } = await listNangoToolsDirect(identity, connectionId);
1163
+ const tool = tools.find((candidate) => candidate.name === toolName);
1164
+ if (!tool) throw new MainNangoTransportError("The requested provider tool is unavailable.", 404, "live_tool_missing", false);
1165
+ const policy = connectedToolPolicyOrThrow(buildConnectedToolPolicyRegistry({
1166
+ tools,
1167
+ connectionActive: connection.lifecycleStatus === "connected",
1168
+ actionsEnabled: connection.actionsEnabled
1169
+ }), tool.name);
1170
+ const classification = classifyConnectedTool(tool);
1171
+ const callable = policy.available;
1172
+ const projected = {
1173
+ name: tool.name,
1174
+ ...tool.title ? { title: tool.title } : {},
1175
+ ...tool.description ? { description: tool.description } : {},
1176
+ inputSchema: tool.inputSchema,
1177
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
1178
+ ...tool.annotations ? { annotations: tool.annotations } : {}
1179
+ };
1180
+ return {
1181
+ ...projected,
1182
+ classification,
1183
+ callable,
1184
+ blockedReason: policy.blockedReason,
1185
+ requiredPermissions: policy.requiredPermissions,
1186
+ missingPermissions: policy.missingPermissions,
1187
+ requiredFeatures: policy.requiredFeatures,
1188
+ missingFeatures: policy.missingFeatures,
1189
+ transport: "nango",
1190
+ providerConfigKey: connection.providerConfigKey,
1191
+ providerContractVersion: MAIN_INTEGRATION_CONTRACT_VERSION,
1192
+ providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
1193
+ protocolVersion: null,
1194
+ schemaSource: "live_tools_list",
1195
+ schemaHash: createHash2("sha256").update(JSON.stringify(projected)).digest("hex"),
1196
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1197
+ };
1198
+ }
1199
+
1200
+ // src/api/gmail-selection-repository.ts
1201
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
1202
+ var GMAIL_SELECTION_TTL_MS = 24 * 60 * 60 * 1e3;
1203
+ var GMAIL_SELECTION_MAX_MESSAGES = 5e3;
1204
+ var GmailSelectionError = class extends Error {
1205
+ constructor(code, message, status = 400) {
1206
+ super(message);
1207
+ this.code = code;
1208
+ this.status = status;
1209
+ this.name = "GmailSelectionError";
1210
+ }
1211
+ code;
1212
+ status;
1213
+ };
1214
+ var schemaReady = null;
1215
+ var schemaDb = null;
1216
+ function stableGmailJson(value) {
1217
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1218
+ if (Array.isArray(value)) return `[${value.map(stableGmailJson).join(",")}]`;
1219
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableGmailJson(item)}`).join(",")}}`;
1220
+ }
1221
+ function gmailFingerprint(value) {
1222
+ return createHash3("sha256").update(stableGmailJson(value)).digest("hex");
1223
+ }
1224
+ async function ensureGmailSelectionSchema() {
1225
+ const db = getDb();
1226
+ if (schemaReady && schemaDb === db) return schemaReady;
1227
+ schemaDb = db;
1228
+ schemaReady = (async () => {
1229
+ await db.execute(`
1230
+ CREATE TABLE IF NOT EXISTS gmail_selections (
1231
+ selection_id TEXT PRIMARY KEY,
1232
+ owner_identity TEXT NOT NULL,
1233
+ connection_id TEXT NOT NULL,
1234
+ purpose TEXT NOT NULL,
1235
+ source_json TEXT NOT NULL,
1236
+ message_ids_json TEXT NOT NULL,
1237
+ message_count INTEGER NOT NULL,
1238
+ selection_sha256 TEXT NOT NULL,
1239
+ provider_snapshot_json TEXT,
1240
+ created_at TEXT NOT NULL,
1241
+ expires_at TEXT NOT NULL,
1242
+ UNIQUE(owner_identity, connection_id, purpose, selection_sha256)
1243
+ )
1244
+ `);
1245
+ await db.execute(`CREATE INDEX IF NOT EXISTS gmail_selections_owner_expiry ON gmail_selections(owner_identity, expires_at)`);
1246
+ await db.execute(`
1247
+ CREATE TABLE IF NOT EXISTS gmail_action_executions (
1248
+ action_id TEXT PRIMARY KEY,
1249
+ owner_identity TEXT NOT NULL,
1250
+ idempotency_key TEXT NOT NULL,
1251
+ fingerprint TEXT NOT NULL,
1252
+ status TEXT NOT NULL,
1253
+ result_json TEXT,
1254
+ created_at TEXT NOT NULL,
1255
+ updated_at TEXT NOT NULL,
1256
+ UNIQUE(owner_identity, idempotency_key)
1257
+ )
1258
+ `);
1259
+ await db.execute(`CREATE INDEX IF NOT EXISTS gmail_actions_owner_updated ON gmail_action_executions(owner_identity, updated_at)`);
1260
+ await db.execute(`CREATE TABLE IF NOT EXISTS gmail_action_chunks (
1261
+ action_id TEXT NOT NULL, chunk_key TEXT NOT NULL, status TEXT NOT NULL,
1262
+ result_json TEXT, updated_at TEXT NOT NULL,
1263
+ PRIMARY KEY(action_id, chunk_key)
1264
+ )`);
1265
+ await db.execute(`CREATE TABLE IF NOT EXISTS gmail_selection_exports (
1266
+ selection_id TEXT PRIMARY KEY, owner_identity TEXT NOT NULL, fingerprint TEXT NOT NULL,
1267
+ status TEXT NOT NULL, result_json TEXT, updated_at TEXT NOT NULL
1268
+ )`);
1269
+ })().catch((error) => {
1270
+ schemaReady = null;
1271
+ schemaDb = null;
1272
+ throw error;
1273
+ });
1274
+ return schemaReady;
1275
+ }
1276
+ function normalizeMessageIds(values) {
1277
+ const ids = [...new Set(values.map((value) => value.trim()).filter((value) => /^[A-Za-z0-9_-]+$/.test(value)))];
1278
+ ids.sort();
1279
+ return ids;
1280
+ }
1281
+ function rowToSelection(row) {
1282
+ return {
1283
+ selectionId: String(row.selection_id),
1284
+ ownerId: String(row.owner_identity),
1285
+ connectionId: String(row.connection_id),
1286
+ purpose: String(row.purpose),
1287
+ source: JSON.parse(String(row.source_json)),
1288
+ messageIds: JSON.parse(String(row.message_ids_json)),
1289
+ count: Number(row.message_count),
1290
+ selectionSha256: String(row.selection_sha256),
1291
+ createdAt: String(row.created_at),
1292
+ expiresAt: String(row.expires_at),
1293
+ providerSnapshot: typeof row.provider_snapshot_json === "string" ? JSON.parse(row.provider_snapshot_json) : null
1294
+ };
1295
+ }
1296
+ async function createOrGetGmailSelection(args) {
1297
+ await ensureGmailSelectionSchema();
1298
+ const messageIds = normalizeMessageIds(args.resolvedMessageIds);
1299
+ if (messageIds.length === 0) throw new GmailSelectionError("selection_empty", "No Gmail messages matched the selection.");
1300
+ if (messageIds.length > GMAIL_SELECTION_MAX_MESSAGES) {
1301
+ throw new GmailSelectionError("selection_too_large", `Narrow the Gmail selection to at most ${GMAIL_SELECTION_MAX_MESSAGES} messages.`);
1302
+ }
1303
+ const normalizedSource = args.source.kind === "query" ? { kind: "query", query: args.source.query.trim().replace(/\s+/g, " ") } : { kind: "message_ids", messageIds: normalizeMessageIds(args.source.messageIds) };
1304
+ const selectionSha256 = gmailFingerprint({ connectionId: args.connectionId, purpose: args.purpose, source: normalizedSource, messageIds });
1305
+ const existing = await getDb().execute({
1306
+ sql: `SELECT * FROM gmail_selections WHERE owner_identity = ? AND connection_id = ? AND purpose = ? AND selection_sha256 = ? AND expires_at > ? LIMIT 1`,
1307
+ args: [args.ownerId, args.connectionId, args.purpose, selectionSha256, (args.now ?? /* @__PURE__ */ new Date()).toISOString()]
1308
+ });
1309
+ if (existing.rows[0]) return rowToSelection(existing.rows[0]);
1310
+ const createdAt = args.now ?? /* @__PURE__ */ new Date();
1311
+ const record2 = {
1312
+ selectionId: `gsel_${randomUUID3().replace(/-/g, "")}`,
1313
+ ownerId: args.ownerId,
1314
+ connectionId: args.connectionId,
1315
+ purpose: args.purpose,
1316
+ source: normalizedSource,
1317
+ messageIds,
1318
+ count: messageIds.length,
1319
+ selectionSha256,
1320
+ createdAt: createdAt.toISOString(),
1321
+ expiresAt: new Date(createdAt.getTime() + GMAIL_SELECTION_TTL_MS).toISOString(),
1322
+ providerSnapshot: args.providerSnapshot ?? null
1323
+ };
1324
+ await getDb().execute({
1325
+ sql: `INSERT INTO gmail_selections (selection_id, owner_identity, connection_id, purpose, source_json, message_ids_json, message_count, selection_sha256, provider_snapshot_json, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1326
+ args: [record2.selectionId, record2.ownerId, record2.connectionId, record2.purpose, JSON.stringify(record2.source), JSON.stringify(record2.messageIds), record2.count, record2.selectionSha256, record2.providerSnapshot ? JSON.stringify(record2.providerSnapshot) : null, record2.createdAt, record2.expiresAt]
1327
+ });
1328
+ return record2;
1329
+ }
1330
+ async function getOwnedGmailSelection(args) {
1331
+ await ensureGmailSelectionSchema();
1332
+ const result = await getDb().execute({
1333
+ sql: `SELECT * FROM gmail_selections WHERE selection_id = ? AND owner_identity = ? AND connection_id = ? AND expires_at > ? LIMIT 1`,
1334
+ args: [args.selectionId, args.ownerId, args.connectionId, (args.now ?? /* @__PURE__ */ new Date()).toISOString()]
1335
+ });
1336
+ const row = result.rows[0];
1337
+ if (!row) throw new GmailSelectionError("selection_not_found", "Gmail selection not found.", 404);
1338
+ return rowToSelection(row);
1339
+ }
1340
+ function assertGmailSelectionReceipt(selection, expectedCount, selectionSha256) {
1341
+ if (expectedCount !== selection.count || selectionSha256 !== selection.selectionSha256) {
1342
+ throw new GmailSelectionError("selection_changed", "The Gmail selection receipt does not match the immutable selection.", 409);
1343
+ }
1344
+ }
1345
+ function rowToAction(row) {
1346
+ return {
1347
+ actionId: String(row.action_id),
1348
+ ownerId: String(row.owner_identity),
1349
+ idempotencyKey: String(row.idempotency_key),
1350
+ fingerprint: String(row.fingerprint),
1351
+ status: String(row.status),
1352
+ result: typeof row.result_json === "string" ? JSON.parse(row.result_json) : null,
1353
+ createdAt: String(row.created_at),
1354
+ updatedAt: String(row.updated_at)
1355
+ };
1356
+ }
1357
+ async function beginGmailAction(args) {
1358
+ await ensureGmailSelectionSchema();
1359
+ const key = args.idempotencyKey.trim();
1360
+ if (key.length < 8 || key.length > 200) throw new GmailSelectionError("idempotency_key_invalid", "Idempotency-Key must be 8-200 characters.");
1361
+ const existing = await getDb().execute({
1362
+ sql: `SELECT * FROM gmail_action_executions WHERE owner_identity = ? AND idempotency_key = ? LIMIT 1`,
1363
+ args: [args.ownerId, key]
1364
+ });
1365
+ if (existing.rows[0]) {
1366
+ const execution2 = rowToAction(existing.rows[0]);
1367
+ if (execution2.fingerprint !== args.fingerprint) {
1368
+ throw new GmailSelectionError("idempotency_conflict", "This Idempotency-Key is already bound to a different Gmail action.", 409);
1369
+ }
1370
+ if (execution2.status === "running" && !execution2.result && Date.now() - Date.parse(execution2.updatedAt) > 6e4) {
1371
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
1372
+ const claimed = await getDb().execute({ sql: `UPDATE gmail_action_executions SET updated_at = ? WHERE action_id = ? AND updated_at = ?`, args: [now2, execution2.actionId, execution2.updatedAt] });
1373
+ if (Number(claimed.rowsAffected ?? 0) === 1) return { execution: { ...execution2, updatedAt: now2 }, replay: false };
1374
+ }
1375
+ return { execution: execution2, replay: true };
1376
+ }
1377
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1378
+ const execution = {
1379
+ actionId: `gact_${randomUUID3().replace(/-/g, "")}`,
1380
+ ownerId: args.ownerId,
1381
+ idempotencyKey: key,
1382
+ fingerprint: args.fingerprint,
1383
+ status: "running",
1384
+ result: null,
1385
+ createdAt: now,
1386
+ updatedAt: now
1387
+ };
1388
+ try {
1389
+ await getDb().execute({
1390
+ sql: `INSERT INTO gmail_action_executions (action_id, owner_identity, idempotency_key, fingerprint, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?)`,
1391
+ args: [execution.actionId, execution.ownerId, execution.idempotencyKey, execution.fingerprint, now, now]
1392
+ });
1393
+ } catch {
1394
+ const raced = await getDb().execute({ sql: `SELECT * FROM gmail_action_executions WHERE owner_identity = ? AND idempotency_key = ? LIMIT 1`, args: [args.ownerId, key] });
1395
+ if (!raced.rows[0]) throw new GmailSelectionError("action_claim_failed", "The Gmail action could not be claimed.", 409);
1396
+ const existingExecution = rowToAction(raced.rows[0]);
1397
+ if (existingExecution.fingerprint !== args.fingerprint) throw new GmailSelectionError("idempotency_conflict", "This Idempotency-Key is already bound to a different Gmail action.", 409);
1398
+ return { execution: existingExecution, replay: true };
1399
+ }
1400
+ return { execution, replay: false };
1401
+ }
1402
+ async function touchGmailAction(actionId) {
1403
+ await ensureGmailSelectionSchema();
1404
+ await getDb().execute({ sql: `UPDATE gmail_action_executions SET updated_at = ? WHERE action_id = ? AND status = 'running'`, args: [(/* @__PURE__ */ new Date()).toISOString(), actionId] });
1405
+ }
1406
+ function rowToChunk(row) {
1407
+ return { actionId: String(row.action_id), chunkKey: String(row.chunk_key), status: String(row.status), result: typeof row.result_json === "string" ? JSON.parse(row.result_json) : null, updatedAt: String(row.updated_at) };
1408
+ }
1409
+ async function claimGmailActionChunk(actionId, chunkKey) {
1410
+ await ensureGmailSelectionSchema();
1411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1412
+ try {
1413
+ await getDb().execute({ sql: `INSERT INTO gmail_action_chunks (action_id, chunk_key, status, updated_at) VALUES (?, ?, 'running', ?)`, args: [actionId, chunkKey, now] });
1414
+ return { receipt: { actionId, chunkKey, status: "running", result: null, updatedAt: now }, claimed: true };
1415
+ } catch {
1416
+ const row = await getDb().execute({ sql: `SELECT * FROM gmail_action_chunks WHERE action_id = ? AND chunk_key = ? LIMIT 1`, args: [actionId, chunkKey] });
1417
+ if (!row.rows[0]) throw new GmailSelectionError("action_chunk_claim_failed", "The Gmail action chunk could not be claimed.", 409);
1418
+ return { receipt: rowToChunk(row.rows[0]), claimed: false };
1419
+ }
1420
+ }
1421
+ async function finishGmailActionChunk(actionId, chunkKey, status, result) {
1422
+ await ensureGmailSelectionSchema();
1423
+ await getDb().execute({ sql: `UPDATE gmail_action_chunks SET status = ?, result_json = ?, updated_at = ? WHERE action_id = ? AND chunk_key = ?`, args: [status, JSON.stringify(result), (/* @__PURE__ */ new Date()).toISOString(), actionId, chunkKey] });
1424
+ }
1425
+ async function beginGmailSelectionExport(args) {
1426
+ await ensureGmailSelectionSchema();
1427
+ const existing = await getDb().execute({ sql: `SELECT * FROM gmail_selection_exports WHERE selection_id = ? AND owner_identity = ? LIMIT 1`, args: [args.selectionId, args.ownerId] });
1428
+ if (existing.rows[0]) {
1429
+ const row = existing.rows[0];
1430
+ if (String(row.fingerprint) !== args.fingerprint) throw new GmailSelectionError("export_conflict", "This Gmail selection export receipt conflicts with the stored export.", 409);
1431
+ const replay = typeof row.result_json === "string" ? JSON.parse(row.result_json) : null;
1432
+ if (replay) return { replay, claimed: false };
1433
+ if (Date.now() - Date.parse(String(row.updated_at)) > 5 * 6e4) {
1434
+ const updated = await getDb().execute({ sql: `UPDATE gmail_selection_exports SET updated_at = ? WHERE selection_id = ? AND owner_identity = ? AND updated_at = ?`, args: [(/* @__PURE__ */ new Date()).toISOString(), args.selectionId, args.ownerId, String(row.updated_at)] });
1435
+ if (Number(updated.rowsAffected ?? 0) === 1) return { replay: null, claimed: true };
1436
+ }
1437
+ return { replay: null, claimed: false };
1438
+ }
1439
+ try {
1440
+ await getDb().execute({ sql: `INSERT INTO gmail_selection_exports (selection_id, owner_identity, fingerprint, status, updated_at) VALUES (?, ?, ?, 'running', ?)`, args: [args.selectionId, args.ownerId, args.fingerprint, (/* @__PURE__ */ new Date()).toISOString()] });
1441
+ return { replay: null, claimed: true };
1442
+ } catch {
1443
+ const raced = await getDb().execute({ sql: `SELECT * FROM gmail_selection_exports WHERE selection_id = ? AND owner_identity = ? LIMIT 1`, args: [args.selectionId, args.ownerId] });
1444
+ if (!raced.rows[0]) throw new GmailSelectionError("export_claim_failed", "The Gmail export could not be claimed.", 409);
1445
+ const row = raced.rows[0];
1446
+ if (String(row.fingerprint) !== args.fingerprint) throw new GmailSelectionError("export_conflict", "This Gmail selection export receipt conflicts with the stored export.", 409);
1447
+ return { replay: typeof row.result_json === "string" ? JSON.parse(row.result_json) : null, claimed: false };
1448
+ }
1449
+ }
1450
+ async function finishGmailSelectionExport(ownerId, selectionId, result) {
1451
+ await ensureGmailSelectionSchema();
1452
+ await getDb().execute({ sql: `UPDATE gmail_selection_exports SET status = 'complete', result_json = ?, updated_at = ? WHERE selection_id = ? AND owner_identity = ?`, args: [JSON.stringify(result), (/* @__PURE__ */ new Date()).toISOString(), selectionId, ownerId] });
1453
+ }
1454
+ async function finishGmailAction(actionId, status, result) {
1455
+ await ensureGmailSelectionSchema();
1456
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1457
+ await getDb().execute({
1458
+ sql: `UPDATE gmail_action_executions SET status = ?, result_json = ?, updated_at = ? WHERE action_id = ?`,
1459
+ args: [status, JSON.stringify(result), updatedAt, actionId]
1460
+ });
1461
+ const row = await getDb().execute({ sql: `SELECT * FROM gmail_action_executions WHERE action_id = ? LIMIT 1`, args: [actionId] });
1462
+ if (!row.rows[0]) throw new GmailSelectionError("action_not_found", "Gmail action not found.", 404);
1463
+ return rowToAction(row.rows[0]);
1464
+ }
1465
+
1466
+ // src/api/gmail-service.ts
1467
+ var GMAIL_MAIN_CONTRACT_VERSION = "2026-08-27.1";
1468
+ var GMAIL_MAIN_CONTRACT_HASH = "9c82b270bb70e7795814cac408a41786ec70d4d7a4bb0f4a11bcc1b7fd02088b";
1469
+ var GMAIL_INLINE_BODY_BYTES = 2e4;
1470
+ var GMAIL_ATTACHMENT_REF_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1471
+ var GmailServiceError = class extends Error {
1472
+ constructor(code, message, status = 400, retryable = status === 429 || status >= 500, retryAfterMs = null) {
1473
+ super(message);
1474
+ this.code = code;
1475
+ this.status = status;
1476
+ this.retryable = retryable;
1477
+ this.retryAfterMs = retryAfterMs;
1478
+ this.name = "GmailServiceError";
1479
+ }
1480
+ code;
1481
+ status;
1482
+ retryable;
1483
+ retryAfterMs;
1484
+ };
1485
+ function record(value) {
1486
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1487
+ }
1488
+ function stringValue(value) {
1489
+ return typeof value === "string" && value.length > 0 ? value : null;
1490
+ }
1491
+ function numberValue(value) {
1492
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1493
+ }
1494
+ function base64urlDecode(value) {
1495
+ if (!/^[A-Za-z0-9_-]*={0,2}$/.test(value)) throw new GmailServiceError("gmail_invalid_base64", "Gmail returned invalid encoded content.", 502);
1496
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
1497
+ return Buffer.from(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="), "base64");
1498
+ }
1499
+ function decodeQuotedPrintable(buffer) {
1500
+ const source = buffer.toString("latin1").replace(/=\r?\n/g, "");
1501
+ const bytes = [];
1502
+ for (let index = 0; index < source.length; index++) {
1503
+ if (source[index] === "=" && /^[0-9A-Fa-f]{2}$/.test(source.slice(index + 1, index + 3))) {
1504
+ bytes.push(Number.parseInt(source.slice(index + 1, index + 3), 16));
1505
+ index += 2;
1506
+ } else bytes.push(source.charCodeAt(index) & 255);
1507
+ }
1508
+ return Buffer.from(bytes);
1509
+ }
1510
+ function decodeEncodedWords(value) {
1511
+ return value.replace(/=\?([^?]+)\?([bq])\?([^?]+)\?=/gi, (_match, charset, encoding, body) => {
1512
+ try {
1513
+ const bytes = encoding.toLowerCase() === "b" ? Buffer.from(body, "base64") : decodeQuotedPrintable(Buffer.from(body.replace(/_/g, " "), "latin1"));
1514
+ return /^(?:utf-8|us-ascii)$/i.test(charset) ? bytes.toString("utf8") : bytes.toString("latin1");
1515
+ } catch {
1516
+ return body;
1517
+ }
1518
+ });
1519
+ }
1520
+ function normalizeHeaders(value) {
1521
+ if (!Array.isArray(value)) return [];
1522
+ return value.flatMap((item) => {
1523
+ const row = record(item);
1524
+ const name = stringValue(row?.name);
1525
+ const raw = stringValue(row?.value);
1526
+ if (!name || raw === null) return [];
1527
+ return [{ name, value: decodeEncodedWords(raw.replace(/\r?\n[ \t]+/g, " ")) }];
1528
+ });
1529
+ }
1530
+ function header(headers, name) {
1531
+ return headers.find((item) => item.name.toLowerCase() === name.toLowerCase())?.value ?? null;
1532
+ }
1533
+ function parseGmailAddresses(value) {
1534
+ if (!value) return [];
1535
+ const tokens = value.match(/(?:"(?:[^"\\]|\\.)*"|[^,])+/g) ?? [];
1536
+ return tokens.flatMap((token) => {
1537
+ const trimmed = token.trim();
1538
+ const angled = trimmed.match(/^(.*?)\s*<([^<>\s]+@[^<>\s]+)>$/);
1539
+ const email = (angled?.[2] ?? trimmed).replace(/^mailto:/i, "").trim().toLowerCase();
1540
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return [];
1541
+ const name = angled?.[1]?.trim().replace(/^"|"$/g, "") || null;
1542
+ return [{ name: name ? decodeEncodedWords(name) : null, email }];
1543
+ });
1544
+ }
1545
+ function transferDecoded(data, headers) {
1546
+ const decoded = base64urlDecode(data);
1547
+ const transfer = header(headers, "content-transfer-encoding")?.toLowerCase();
1548
+ if (transfer === "quoted-printable") return decodeQuotedPrintable(decoded);
1549
+ if (transfer === "base64") {
1550
+ const text2 = decoded.toString("ascii").replace(/\s/g, "");
1551
+ return /^[A-Za-z0-9+/]*={0,2}$/.test(text2) ? Buffer.from(text2, "base64") : decoded;
1552
+ }
1553
+ return decoded;
1554
+ }
1555
+ function sanitizeHtml(value) {
1556
+ const textOnly = value.replace(/<!--[\s\S]*?-->/g, "").replace(/<!doctype\b[^>]*>/gi, "").replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<(?:iframe|object|embed|svg|math)\b[^>]*>[\s\S]*?<\/(?:iframe|object|embed|svg|math)>/gi, "").replace(/<\/?(?:script|style|iframe|object|embed|svg|math|form|input|button|textarea|select|option|meta|link|base|img|audio|video|source|track)\b[^>]*>/gi, "").replace(/\son\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s(?:href|src|srcset|action|formaction|poster|background|xlink:href)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/<[^>]*>/g, " ");
1557
+ return textOnly.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\s+/g, " ").trim();
1558
+ }
1559
+ function refSecret() {
1560
+ return createHash4("sha256").update(getSessionSecret()).update("\0gmail-ref-v1").digest();
1561
+ }
1562
+ function signOpaque(payload) {
1563
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
1564
+ const signature = createHmac("sha256", refSecret()).update(encoded).digest("base64url");
1565
+ return `gref1.${encoded}.${signature}`;
1566
+ }
1567
+ function verifyOpaque(value, ownerId, kind) {
1568
+ const [version, encoded, signature] = value.split(".");
1569
+ if (version !== "gref1" || !encoded || !signature) throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1570
+ const expected = createHmac("sha256", refSecret()).update(encoded).digest();
1571
+ let supplied;
1572
+ try {
1573
+ supplied = Buffer.from(signature, "base64url");
1574
+ } catch {
1575
+ throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1576
+ }
1577
+ if (expected.length !== supplied.length || !timingSafeEqual(expected, supplied)) throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1578
+ let payload;
1579
+ try {
1580
+ payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
1581
+ } catch {
1582
+ throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1583
+ }
1584
+ if (payload.ownerId !== ownerId || payload.kind !== kind || typeof payload.expiresAt !== "number" || payload.expiresAt <= Date.now()) {
1585
+ throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1586
+ }
1587
+ return payload;
1588
+ }
1589
+ function attachmentRef(args) {
1590
+ return signOpaque({ kind: "attachment", ...args, expiresAt: Date.now() + GMAIL_ATTACHMENT_REF_TTL_MS });
1591
+ }
1592
+ function canonicalPart(args) {
1593
+ const headers = normalizeHeaders(args.part.headers);
1594
+ const body = record(args.part.body);
1595
+ const mimeType = stringValue(args.part.mimeType)?.toLowerCase() ?? "application/octet-stream";
1596
+ const filename = decodeEncodedWords(stringValue(args.part.filename) ?? "");
1597
+ const partId = stringValue(args.part.partId) ?? args.path;
1598
+ const data = stringValue(body?.data);
1599
+ const attachmentId = stringValue(body?.attachmentId);
1600
+ const contentId = header(headers, "content-id")?.replace(/^<|>$/g, "") ?? null;
1601
+ const inline = /^inline\b/i.test(header(headers, "content-disposition") ?? "") || !!contentId;
1602
+ const isText = mimeType.startsWith("text/");
1603
+ let text2 = null;
1604
+ let contentComplete = true;
1605
+ if (isText && data) {
1606
+ const decoded = transferDecoded(data, headers);
1607
+ text2 = decoded.toString("utf8");
1608
+ } else if (isText && attachmentId) contentComplete = false;
1609
+ const isAttachment = !!filename || !!attachmentId || inline && !!data && !isText;
1610
+ let opaqueRef = null;
1611
+ const decodedSize = numberValue(body?.size) ?? (data ? base64urlDecode(data).byteLength : 0);
1612
+ if (isAttachment) {
1613
+ const attachmentSize = decodedSize;
1614
+ opaqueRef = attachmentRef({ ownerId: args.ownerId, connectionId: args.connectionId, messageId: args.messageId, partId, attachmentId, filename: filename || `gmail-${args.messageId}-${partId}`, mimeType, size: attachmentSize });
1615
+ args.attachments.push({ filename: filename || `gmail-${args.messageId}-${partId}`, declaredMime: mimeType, size: attachmentSize, inline, contentId, partId, attachmentRef: opaqueRef });
1616
+ }
1617
+ const parts = Array.isArray(args.part.parts) ? args.part.parts.map((child, index) => canonicalPart({ ...args, part: record(child) ?? {}, path: `${args.path}.${index + 1}` })) : [];
1618
+ return { partId, mimeType, filename, headers, size: decodedSize, inline, contentId, contentComplete, text: text2, attachmentRef: opaqueRef, parts };
1619
+ }
1620
+ function collectText(part, mime) {
1621
+ const own = part.mimeType === mime && part.text !== null ? [part.text] : [];
1622
+ return [...own, ...part.parts.flatMap((child) => collectText(child, mime))];
1623
+ }
1624
+ function normalizeGmailMessage(args) {
1625
+ const source = record(args.payload);
1626
+ const messageId = stringValue(source?.id);
1627
+ const root = record(source?.payload);
1628
+ if (!messageId || !root) throw new GmailServiceError("gmail_invalid_message", "Gmail returned an invalid message.", 502);
1629
+ const attachments = [];
1630
+ const mime = canonicalPart({ part: root, ownerId: args.ownerId, connectionId: args.connectionId, messageId, path: "0", attachments });
1631
+ const headers = mime.headers;
1632
+ const plain = collectText(mime, "text/plain").join("\n").trim() || null;
1633
+ const htmlRaw = collectText(mime, "text/html").join("\n").trim() || null;
1634
+ return {
1635
+ messageId,
1636
+ threadId: stringValue(source?.threadId),
1637
+ messageIdHeader: header(headers, "message-id"),
1638
+ internalDate: stringValue(source?.internalDate) ? new Date(Number(source.internalDate)).toISOString() : null,
1639
+ headers,
1640
+ from: parseGmailAddresses(header(headers, "from")),
1641
+ to: parseGmailAddresses(header(headers, "to")),
1642
+ cc: parseGmailAddresses(header(headers, "cc")),
1643
+ bcc: parseGmailAddresses(header(headers, "bcc")),
1644
+ replyTo: parseGmailAddresses(header(headers, "reply-to")),
1645
+ subject: header(headers, "subject"),
1646
+ labels: Array.isArray(source?.labelIds) ? source.labelIds.filter((item) => typeof item === "string") : [],
1647
+ snippet: stringValue(source?.snippet),
1648
+ plainText: plain,
1649
+ sanitizedHtml: htmlRaw ? sanitizeHtml(htmlRaw) : null,
1650
+ contentComplete: allPartsComplete(mime),
1651
+ providerSizeEstimate: numberValue(source?.sizeEstimate),
1652
+ mime,
1653
+ attachments,
1654
+ provenance: { provider: "gmail", connectionId: args.connectionId, fetchedAt: (args.fetchedAt ?? /* @__PURE__ */ new Date()).toISOString(), format: "full" }
1655
+ };
1656
+ }
1657
+ function allPartsComplete(part) {
1658
+ return part.contentComplete && part.parts.every(allPartsComplete);
1659
+ }
1660
+ var GmailService = class {
1661
+ providerCall;
1662
+ createArtifact;
1663
+ createJsonlArtifact;
1664
+ constructor(dependencies = {}) {
1665
+ this.providerCall = dependencies.providerCall ?? callGmailProviderProxyDirect;
1666
+ this.createArtifact = dependencies.createArtifact ?? createConnectedDataBinaryArtifact;
1667
+ this.createJsonlArtifact = dependencies.createJsonlArtifact ?? createConnectedDataArtifact;
1668
+ }
1669
+ async searchMessages(args) {
1670
+ const query = args.query.trim();
1671
+ if (!query || query.length > 2e3) throw new GmailServiceError("invalid_request", "A Gmail query of at most 2,000 characters is required.");
1672
+ const requestedLimit = args.limit ?? 25;
1673
+ if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > 500) throw new GmailServiceError("invalid_request", "Gmail search limit must be an integer from 1 to 500.");
1674
+ const limit = requestedLimit;
1675
+ let pageToken;
1676
+ if (args.cursor) {
1677
+ const cursor = verifyOpaque(args.cursor, args.ownerId, "search_cursor");
1678
+ if (cursor.connectionId !== args.connectionId || cursor.query !== query) throw new GmailServiceError("cursor_not_found", "Gmail cursor not found.", 404, false);
1679
+ pageToken = stringValue(cursor.pageToken) ?? void 0;
1680
+ }
1681
+ const payload = record(await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "GET", path: "/gmail/v1/users/me/messages", query: { q: query, maxResults: limit, pageToken } }));
1682
+ const messages = Array.isArray(payload?.messages) ? payload.messages.flatMap((item) => {
1683
+ const row = record(item);
1684
+ const id = stringValue(row?.id);
1685
+ return id ? [{ messageId: id, threadId: stringValue(row?.threadId) }] : [];
1686
+ }) : [];
1687
+ const nextToken = stringValue(payload?.nextPageToken);
1688
+ return {
1689
+ messages,
1690
+ estimatedMatches: numberValue(payload?.resultSizeEstimate),
1691
+ complete: !nextToken,
1692
+ truncated: !!nextToken,
1693
+ nextCursor: nextToken ? signOpaque({ kind: "search_cursor", ownerId: args.ownerId, connectionId: args.connectionId, query, pageToken: nextToken, expiresAt: Date.now() + 24 * 60 * 60 * 1e3 }) : null
1694
+ };
1695
+ }
1696
+ async getMessage(args) {
1697
+ const complete = await this.getCompleteMessage(args);
1698
+ const needsArtifact = !!complete.canonicalArtifact;
1699
+ const inlinePlain = complete.plainText && Buffer.byteLength(complete.plainText) > GMAIL_INLINE_BODY_BYTES ? `${Buffer.from(complete.plainText).subarray(0, GMAIL_INLINE_BODY_BYTES).toString("utf8")}\u2026` : complete.plainText;
1700
+ return { ...complete, plainText: inlinePlain, contentComplete: complete.contentComplete && !needsArtifact };
1701
+ }
1702
+ async getCompleteMessage(args) {
1703
+ if (!/^[A-Za-z0-9_-]+$/.test(args.messageId)) throw new GmailServiceError("invalid_request", "A valid Gmail message ID is required.");
1704
+ const encodedId = encodeURIComponent(args.messageId);
1705
+ const identity = args.providerIdentity ?? args.ownerId;
1706
+ const full = await this.providerCall({ identity, connectionId: args.connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodedId}`, query: { format: "full" } });
1707
+ const normalized = normalizeGmailMessage({ ownerId: args.ownerId, connectionId: args.connectionId, payload: full });
1708
+ let rawArtifact = null;
1709
+ if (args.includeRawArtifact !== false) {
1710
+ const rawPayload = record(await this.providerCall({ identity, connectionId: args.connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodedId}`, query: { format: "raw" } }));
1711
+ const raw = stringValue(rawPayload?.raw);
1712
+ if (!raw) throw new GmailServiceError("gmail_raw_unavailable", "Gmail did not return the raw message.", 502);
1713
+ rawArtifact = await this.createArtifact({ ownerId: args.ownerId, exportId: `gmail-raw-${args.messageId}`, filename: `${args.messageId}.eml`, content: base64urlDecode(raw), contentType: "message/rfc822" });
1714
+ }
1715
+ const serialized = JSON.stringify(normalized);
1716
+ const needsArtifact = Buffer.byteLength(serialized) > GMAIL_INLINE_BODY_BYTES || !normalized.contentComplete;
1717
+ const canonicalArtifact = needsArtifact ? await this.createArtifact({ ownerId: args.ownerId, exportId: `gmail-message-${args.messageId}`, filename: `${args.messageId}.json`, content: serialized, contentType: "application/json" }) : null;
1718
+ return { ...normalized, rawArtifact, canonicalArtifact };
1719
+ }
1720
+ async getAttachment(args) {
1721
+ const ref = verifyOpaque(args.attachmentRef, args.ownerId, "attachment");
1722
+ const connectionId = stringValue(ref.connectionId);
1723
+ const messageId = stringValue(ref.messageId);
1724
+ if (!connectionId || !messageId) throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1725
+ let bytes;
1726
+ const attachmentId = stringValue(ref.attachmentId);
1727
+ if (attachmentId) {
1728
+ const payload = record(await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}` }));
1729
+ const data = stringValue(payload?.data);
1730
+ if (!data) throw new GmailServiceError("gmail_attachment_unavailable", "Gmail did not return attachment bytes.", 502);
1731
+ bytes = base64urlDecode(data);
1732
+ const providerSize = numberValue(payload?.size);
1733
+ if (providerSize !== null && providerSize !== bytes.byteLength) throw new GmailServiceError("gmail_attachment_size_mismatch", "Gmail attachment bytes did not match the declared size.", 502);
1734
+ } else {
1735
+ const full = record(await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}`, query: { format: "full" } }));
1736
+ const data = findPartData(record(full?.payload), stringValue(ref.partId) ?? "");
1737
+ if (!data) throw new GmailServiceError("reference_not_found", "Gmail reference not found.", 404, false);
1738
+ bytes = base64urlDecode(data);
1739
+ }
1740
+ const declaredMime = stringValue(ref.mimeType) ?? "application/octet-stream";
1741
+ const signedSize = numberValue(ref.size);
1742
+ if (signedSize !== null && signedSize > 0 && signedSize !== bytes.byteLength) throw new GmailServiceError("gmail_attachment_size_mismatch", "Gmail attachment bytes did not match the reviewed MIME part size.", 502);
1743
+ const observedMime = sniffMime(bytes, declaredMime);
1744
+ const filename = stringValue(ref.filename) ?? `gmail-${messageId}.bin`;
1745
+ const artifact = await this.createArtifact({ ownerId: args.ownerId, exportId: `gmail-attachment-${messageId}`, filename, content: bytes, contentType: observedMime });
1746
+ const text2 = observedMime.startsWith("text/") || observedMime === "application/json" ? bytes.subarray(0, GMAIL_INLINE_BODY_BYTES).toString("utf8") : null;
1747
+ return { filename, declaredMime, observedMime, bytes: bytes.byteLength, sha256: createHash4("sha256").update(bytes).digest("hex"), contentComplete: text2 !== null ? bytes.byteLength <= GMAIL_INLINE_BODY_BYTES : true, text: text2, artifact };
1748
+ }
1749
+ async prepareSelection(args) {
1750
+ let ids;
1751
+ let providerSnapshot = null;
1752
+ if (args.source.kind === "message_ids") {
1753
+ if (!args.source.messageIds.length || args.source.messageIds.some((id) => typeof id !== "string" || !/^[A-Za-z0-9_-]+$/.test(id.trim()))) throw new GmailServiceError("invalid_request", "Every explicit Gmail message ID must be valid.");
1754
+ ids = [...new Set(args.source.messageIds.map((id) => id.trim()))];
1755
+ for (const messageId of ids) {
1756
+ try {
1757
+ await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}`, query: { format: "metadata" } });
1758
+ } catch {
1759
+ throw new GmailServiceError("selection_message_not_found", "One or more explicit Gmail message IDs do not exist in this connection.", 404, false);
1760
+ }
1761
+ }
1762
+ } else {
1763
+ ids = [];
1764
+ let pageToken;
1765
+ let estimated = null;
1766
+ do {
1767
+ const payload = record(await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "GET", path: "/gmail/v1/users/me/messages", query: { q: args.source.query.trim(), maxResults: 500, pageToken } }));
1768
+ if (estimated === null) estimated = numberValue(payload?.resultSizeEstimate);
1769
+ for (const item of Array.isArray(payload?.messages) ? payload.messages : []) {
1770
+ const id = stringValue(record(item)?.id);
1771
+ if (id) ids.push(id);
1772
+ }
1773
+ if (new Set(ids).size > 5e3) throw new GmailServiceError("selection_too_large", "Narrow the Gmail selection to at most 5,000 messages.");
1774
+ pageToken = stringValue(payload?.nextPageToken) ?? void 0;
1775
+ } while (pageToken);
1776
+ if (args.exactRange) {
1777
+ const fromMs = Date.parse(args.exactRange.from);
1778
+ const toMs = Date.parse(args.exactRange.to);
1779
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) throw new GmailServiceError("invalid_request", "exactRange.from must be before exactRange.to.");
1780
+ const exactIds = [];
1781
+ for (const messageId of ids) {
1782
+ const full = await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "GET", path: `/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}`, query: { format: "full" } });
1783
+ const normalized = normalizeGmailMessage({ ownerId: args.ownerId, connectionId: args.connectionId, payload: full });
1784
+ const internalMs = normalized.internalDate ? Date.parse(normalized.internalDate) : Number.NaN;
1785
+ if (Number.isFinite(internalMs) && internalMs >= fromMs && internalMs < toMs) exactIds.push(messageId);
1786
+ }
1787
+ ids = exactIds;
1788
+ }
1789
+ providerSnapshot = { estimatedMatches: estimated, pagesComplete: true, ...args.exactRange ? { exactRange: args.exactRange } : {} };
1790
+ }
1791
+ return createOrGetGmailSelection({ ownerId: args.ownerId, connectionId: args.connectionId, source: args.source, purpose: args.purpose, resolvedMessageIds: ids, providerSnapshot });
1792
+ }
1793
+ async exportSelection(args) {
1794
+ const selection = await getOwnedGmailSelection(args);
1795
+ if (selection.purpose !== "export" && selection.purpose !== "scheduled_export") throw new GmailServiceError("selection_purpose_mismatch", "This Gmail selection was not prepared for export.", 409, false);
1796
+ assertGmailSelectionReceipt(selection, args.expectedCount, args.selectionSha256);
1797
+ const exportFingerprint = gmailFingerprint({ selectionId: selection.selectionId, selectionSha256: selection.selectionSha256, expectedCount: selection.count });
1798
+ const exportClaim = await beginGmailSelectionExport({ ownerId: args.ownerId, selectionId: selection.selectionId, fingerprint: exportFingerprint });
1799
+ if (exportClaim.replay) return { ...exportClaim.replay, replay: true };
1800
+ if (!exportClaim.claimed) throw new GmailServiceError("export_in_progress", "This Gmail selection export is already running; retry shortly with the same receipt.", 409, true);
1801
+ const messages = [];
1802
+ for (const messageId of selection.messageIds) messages.push(await this.getCompleteMessage({ ownerId: args.ownerId, providerIdentity: args.providerIdentity, connectionId: args.connectionId, messageId }));
1803
+ const exportId = `gexp_${selection.selectionSha256.slice(0, 32)}`;
1804
+ const manifest = { version: GMAIL_MAIN_CONTRACT_VERSION, selectionId: selection.selectionId, selectionSha256: selection.selectionSha256, count: messages.length };
1805
+ const content = [JSON.stringify({ type: "manifest", ...manifest }), ...messages.map((message) => JSON.stringify({ type: "message", message }))].join("\n") + "\n";
1806
+ const artifact = await this.createJsonlArtifact({ ownerId: args.ownerId, exportId, filename: `gmail-${selection.selectionId}.jsonl`, content, stableCreatedAt: selection.createdAt, stableKey: exportFingerprint.slice(0, 32) });
1807
+ const result = { exportId, selectionId: selection.selectionId, selectionSha256: selection.selectionSha256, count: messages.length, status: "complete", artifact, sampleMessages: messages.slice(0, 5).map(messagePreview) };
1808
+ await finishGmailSelectionExport(args.ownerId, selection.selectionId, result);
1809
+ return result;
1810
+ }
1811
+ async bulkManageSelection(args) {
1812
+ const selection = await getOwnedGmailSelection(args);
1813
+ if (selection.purpose !== "mailbox_action") throw new GmailServiceError("selection_purpose_mismatch", "This Gmail selection was not prepared for mailbox actions.", 409, false);
1814
+ assertGmailSelectionReceipt(selection, args.expectedCount, args.selectionSha256);
1815
+ const fingerprint = gmailFingerprint({ selectionSha256: selection.selectionSha256, operation: args.operation });
1816
+ const begun = await beginGmailAction({ ownerId: args.ownerId, idempotencyKey: args.idempotencyKey, fingerprint });
1817
+ if (begun.replay && begun.execution.result) return begun.execution.result;
1818
+ if (begun.replay) return { actionId: begun.execution.actionId, selectionId: selection.selectionId, status: "running", changedCount: 0, failedCount: 0, failedChunks: [], retryable: true };
1819
+ const failures = [];
1820
+ let changed = 0;
1821
+ const operation = manageLabels(args.operation);
1822
+ if (operation.mode === "batch") {
1823
+ if (operation.validateIds.length) await this.validateLabels(args.providerIdentity ?? args.ownerId, args.connectionId, operation.validateIds);
1824
+ const chunks = chunk(selection.messageIds, 1e3);
1825
+ for (let index = 0; index < chunks.length; index++) {
1826
+ await touchGmailAction(begun.execution.actionId);
1827
+ const chunkKey = `batch:${index}`;
1828
+ const claim = await claimGmailActionChunk(begun.execution.actionId, chunkKey);
1829
+ if (!claim.claimed) {
1830
+ if (claim.receipt.status === "complete") changed += Number(claim.receipt.result?.changedCount ?? chunks[index].length);
1831
+ else failures.push({ chunk: index, code: claim.receipt.status === "failed" ? String(claim.receipt.result?.code ?? "provider_error") : "outcome_unknown" });
1832
+ continue;
1833
+ }
1834
+ try {
1835
+ await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "POST", path: "/gmail/v1/users/me/messages/batchModify", body: { ids: chunks[index], addLabelIds: operation.addLabelIds, removeLabelIds: operation.removeLabelIds }, requestId: `${args.idempotencyKey}:${index}` });
1836
+ changed += chunks[index].length;
1837
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "complete", { changedCount: chunks[index].length });
1838
+ } catch (error) {
1839
+ const code = serviceErrorCode(error);
1840
+ failures.push({ chunk: index, code });
1841
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "failed", { code });
1842
+ }
1843
+ }
1844
+ } else {
1845
+ for (let index = 0; index < selection.messageIds.length; index++) {
1846
+ await touchGmailAction(begun.execution.actionId);
1847
+ const chunkKey = `single:${index}`;
1848
+ const claim = await claimGmailActionChunk(begun.execution.actionId, chunkKey);
1849
+ if (!claim.claimed) {
1850
+ if (claim.receipt.status === "complete") changed += 1;
1851
+ else if (failures.length < 20) failures.push({ chunk: index, code: claim.receipt.status === "failed" ? String(claim.receipt.result?.code ?? "provider_error") : "outcome_unknown" });
1852
+ continue;
1853
+ }
1854
+ try {
1855
+ await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "POST", path: `/gmail/v1/users/me/messages/${encodeURIComponent(selection.messageIds[index])}/${operation.path}`, body: {}, requestId: `${args.idempotencyKey}:${index}` });
1856
+ changed += 1;
1857
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "complete", { changedCount: 1 });
1858
+ } catch (error) {
1859
+ const code = serviceErrorCode(error);
1860
+ if (failures.length < 20) failures.push({ chunk: index, code });
1861
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "failed", { code });
1862
+ }
1863
+ }
1864
+ }
1865
+ const result = { actionId: begun.execution.actionId, selectionId: selection.selectionId, status: failures.length ? "partial" : "complete", changedCount: changed, failedCount: selection.count - changed, failedChunks: failures.slice(0, 20) };
1866
+ await finishGmailAction(begun.execution.actionId, failures.length ? "partial" : "complete", result);
1867
+ return result;
1868
+ }
1869
+ async permanentlyDeleteSelection(args) {
1870
+ if (args.confirmPermanentDelete !== true) throw new GmailServiceError("permanent_delete_confirmation_required", "Set confirmPermanentDelete to true after explicit user confirmation.");
1871
+ const selection = await getOwnedGmailSelection(args);
1872
+ assertGmailSelectionReceipt(selection, args.expectedCount, args.selectionSha256);
1873
+ if (selection.purpose !== "mailbox_action") throw new GmailServiceError("selection_purpose_mismatch", "This Gmail selection was not prepared for mailbox actions.", 409, false);
1874
+ const fingerprint = gmailFingerprint({ selectionSha256: selection.selectionSha256, operation: "permanent_delete" });
1875
+ const begun = await beginGmailAction({ ownerId: args.ownerId, idempotencyKey: args.idempotencyKey, fingerprint });
1876
+ if (begun.replay && begun.execution.result) return begun.execution.result;
1877
+ if (begun.replay) return { actionId: begun.execution.actionId, selectionId: selection.selectionId, status: "running", submittedCount: 0, failedCount: 0, failedChunks: [], retryable: true, providerGuarantee: "The original permanent-delete attempt is still being finalized; no provider chunk was replayed." };
1878
+ const failures = [];
1879
+ let submitted = 0;
1880
+ const chunks = chunk(selection.messageIds, 1e3);
1881
+ for (let index = 0; index < chunks.length; index++) {
1882
+ await touchGmailAction(begun.execution.actionId);
1883
+ const chunkKey = `delete:${index}`;
1884
+ const claim = await claimGmailActionChunk(begun.execution.actionId, chunkKey);
1885
+ if (!claim.claimed) {
1886
+ if (claim.receipt.status === "complete") submitted += Number(claim.receipt.result?.submittedCount ?? chunks[index].length);
1887
+ else failures.push({ chunk: index, code: claim.receipt.status === "failed" ? String(claim.receipt.result?.code ?? "provider_error") : "outcome_unknown" });
1888
+ continue;
1889
+ }
1890
+ try {
1891
+ await this.providerCall({ identity: args.providerIdentity ?? args.ownerId, connectionId: args.connectionId, method: "POST", path: "/gmail/v1/users/me/messages/batchDelete", body: { ids: chunks[index] }, requestId: `${args.idempotencyKey}:${index}` });
1892
+ submitted += chunks[index].length;
1893
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "complete", { submittedCount: chunks[index].length });
1894
+ } catch (error) {
1895
+ const code = serviceErrorCode(error);
1896
+ failures.push({ chunk: index, code });
1897
+ await finishGmailActionChunk(begun.execution.actionId, chunkKey, "failed", { code });
1898
+ }
1899
+ }
1900
+ const result = { actionId: begun.execution.actionId, selectionId: selection.selectionId, status: failures.length ? "partial" : "complete", submittedCount: submitted, failedCount: selection.count - submitted, failedChunks: failures.slice(0, 20), providerGuarantee: "Gmail accepts batch deletion but does not report whether every supplied message previously existed." };
1901
+ await finishGmailAction(begun.execution.actionId, failures.length ? "partial" : "complete", result);
1902
+ return result;
1903
+ }
1904
+ async validateLabels(ownerId, connectionId, requested) {
1905
+ const payload = record(await this.providerCall({ identity: ownerId, connectionId, method: "GET", path: "/gmail/v1/users/me/labels" }));
1906
+ const available = new Set((Array.isArray(payload?.labels) ? payload.labels : []).map((item) => stringValue(record(item)?.id)).filter((item) => !!item));
1907
+ const unknown = requested.filter((id) => !available.has(id));
1908
+ if (unknown.length) throw new GmailServiceError("gmail_label_not_found", "One or more Gmail label IDs were not found.", 404, false);
1909
+ }
1910
+ };
1911
+ function manageLabels(operation) {
1912
+ if (operation.kind === "trash") return { mode: "single", path: "trash" };
1913
+ if (operation.kind === "restore") return { mode: "single", path: "untrash" };
1914
+ if (operation.kind === "labels") {
1915
+ const addLabelIds = [...new Set(operation.addLabelIds ?? [])];
1916
+ const removeLabelIds = [...new Set(operation.removeLabelIds ?? [])];
1917
+ if (!addLabelIds.length && !removeLabelIds.length) throw new GmailServiceError("invalid_request", "At least one label must be added or removed.");
1918
+ if (addLabelIds.some((id) => removeLabelIds.includes(id))) throw new GmailServiceError("invalid_request", "The same label cannot be added and removed.");
1919
+ return { mode: "batch", addLabelIds, removeLabelIds, validateIds: [...addLabelIds, ...removeLabelIds] };
1920
+ }
1921
+ const mappings = {
1922
+ mark_read: { addLabelIds: [], removeLabelIds: ["UNREAD"] },
1923
+ mark_unread: { addLabelIds: ["UNREAD"], removeLabelIds: [] },
1924
+ archive: { addLabelIds: [], removeLabelIds: ["INBOX"] },
1925
+ move_to_inbox: { addLabelIds: ["INBOX"], removeLabelIds: [] }
1926
+ };
1927
+ return { mode: "batch", ...mappings[operation.kind], validateIds: [] };
1928
+ }
1929
+ function chunk(values, size) {
1930
+ const result = [];
1931
+ for (let index = 0; index < values.length; index += size) result.push(values.slice(index, index + size));
1932
+ return result;
1933
+ }
1934
+ function serviceErrorCode(error) {
1935
+ return error && typeof error === "object" && typeof error.code === "string" ? String(error.code) : "provider_error";
1936
+ }
1937
+ function messagePreview(message) {
1938
+ return { messageId: message.messageId, threadId: message.threadId, internalDate: message.internalDate, from: message.from, to: message.to, subject: message.subject, snippet: message.snippet, labels: message.labels, attachmentCount: message.attachments.length };
1939
+ }
1940
+ function findPartData(part, partId) {
1941
+ if (!part) return null;
1942
+ if (stringValue(part.partId) === partId) return stringValue(record(part.body)?.data);
1943
+ for (const child of Array.isArray(part.parts) ? part.parts : []) {
1944
+ const found = findPartData(record(child), partId);
1945
+ if (found !== null) return found;
1946
+ }
1947
+ return null;
1948
+ }
1949
+ function sniffMime(bytes, declared) {
1950
+ if (bytes.subarray(0, 5).toString("ascii") === "%PDF-") return "application/pdf";
1951
+ if (bytes.subarray(0, 4).equals(Buffer.from([80, 75, 3, 4]))) return "application/zip";
1952
+ if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
1953
+ if (bytes.subarray(0, 3).equals(Buffer.from([255, 216, 255]))) return "image/jpeg";
1954
+ if (declared.startsWith("text/")) {
1955
+ try {
1956
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1957
+ return declared;
1958
+ } catch {
1959
+ return "application/octet-stream";
1960
+ }
1961
+ }
1962
+ if (declared === "application/json") {
1963
+ try {
1964
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1965
+ return declared;
1966
+ } catch {
1967
+ return "application/octet-stream";
1968
+ }
1969
+ }
1970
+ return "application/octet-stream";
1971
+ }
1972
+
1973
+ export {
1974
+ recordNangoConnectionCost,
1975
+ ConnectedUsageSettlementInputSchema,
1976
+ ConnectedUsagePreflightInputSchema,
1977
+ ConnectedUsageBillingError,
1978
+ preflightConnectedUsage,
1979
+ settleConnectedUsage,
1980
+ listConnectedUsageHistory,
1981
+ MainNangoTransportError,
1982
+ discoverOwnedNangoConnections,
1983
+ listNangoCatalogDirect,
1984
+ createNangoSessionDirect,
1985
+ createNangoReconnectSessionDirect,
1986
+ deleteNangoConnectionDirect,
1987
+ probeNangoConnectionDirect,
1988
+ listNangoToolsDirect,
1989
+ callNangoToolDirect,
1990
+ describeNangoToolDirect,
1991
+ GmailSelectionError,
1992
+ gmailFingerprint,
1993
+ getOwnedGmailSelection,
1994
+ assertGmailSelectionReceipt,
1995
+ GMAIL_MAIN_CONTRACT_VERSION,
1996
+ GMAIL_MAIN_CONTRACT_HASH,
1997
+ GMAIL_INLINE_BODY_BYTES,
1998
+ GMAIL_ATTACHMENT_REF_TTL_MS,
1999
+ GmailServiceError,
2000
+ parseGmailAddresses,
2001
+ normalizeGmailMessage,
2002
+ GmailService
2003
+ };