@open-mercato/channel-gmail 0.6.4

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 (79) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/AGENTS.md +47 -0
  3. package/build.mjs +7 -0
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js +17 -0
  7. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js.map +7 -0
  8. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js +16 -0
  9. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js.map +7 -0
  10. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js +16 -0
  11. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js.map +7 -0
  12. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js +17 -0
  13. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js.map +7 -0
  14. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js +26 -0
  15. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js.map +7 -0
  16. package/dist/modules/channel_gmail/acl.js +10 -0
  17. package/dist/modules/channel_gmail/acl.js.map +7 -0
  18. package/dist/modules/channel_gmail/di.js +23 -0
  19. package/dist/modules/channel_gmail/di.js.map +7 -0
  20. package/dist/modules/channel_gmail/index.js +9 -0
  21. package/dist/modules/channel_gmail/index.js.map +7 -0
  22. package/dist/modules/channel_gmail/integration.js +69 -0
  23. package/dist/modules/channel_gmail/integration.js.map +7 -0
  24. package/dist/modules/channel_gmail/lib/adapter.js +542 -0
  25. package/dist/modules/channel_gmail/lib/adapter.js.map +7 -0
  26. package/dist/modules/channel_gmail/lib/capabilities.js +10 -0
  27. package/dist/modules/channel_gmail/lib/capabilities.js.map +7 -0
  28. package/dist/modules/channel_gmail/lib/convert-outbound.js +84 -0
  29. package/dist/modules/channel_gmail/lib/convert-outbound.js.map +7 -0
  30. package/dist/modules/channel_gmail/lib/credentials.js +48 -0
  31. package/dist/modules/channel_gmail/lib/credentials.js.map +7 -0
  32. package/dist/modules/channel_gmail/lib/gmail-client.js +160 -0
  33. package/dist/modules/channel_gmail/lib/gmail-client.js.map +7 -0
  34. package/dist/modules/channel_gmail/lib/health.js +10 -0
  35. package/dist/modules/channel_gmail/lib/health.js.map +7 -0
  36. package/dist/modules/channel_gmail/lib/normalize-inbound.js +28 -0
  37. package/dist/modules/channel_gmail/lib/normalize-inbound.js.map +7 -0
  38. package/dist/modules/channel_gmail/lib/oauth.js +77 -0
  39. package/dist/modules/channel_gmail/lib/oauth.js.map +7 -0
  40. package/dist/modules/channel_gmail/setup.js +25 -0
  41. package/dist/modules/channel_gmail/setup.js.map +7 -0
  42. package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js +24 -0
  43. package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js.map +7 -0
  44. package/dist/modules/channel_gmail/widgets/injection/connect/widget.js +17 -0
  45. package/dist/modules/channel_gmail/widgets/injection/connect/widget.js.map +7 -0
  46. package/dist/modules/channel_gmail/widgets/injection-table.js +14 -0
  47. package/dist/modules/channel_gmail/widgets/injection-table.js.map +7 -0
  48. package/jest.config.cjs +34 -0
  49. package/package.json +95 -0
  50. package/src/index.ts +1 -0
  51. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.ts +24 -0
  52. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.ts +23 -0
  53. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.ts +23 -0
  54. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.ts +39 -0
  55. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.ts +48 -0
  56. package/src/modules/channel_gmail/acl.ts +6 -0
  57. package/src/modules/channel_gmail/di.ts +21 -0
  58. package/src/modules/channel_gmail/index.ts +6 -0
  59. package/src/modules/channel_gmail/integration.ts +67 -0
  60. package/src/modules/channel_gmail/lib/__tests__/adapter.test.ts +838 -0
  61. package/src/modules/channel_gmail/lib/__tests__/convert-outbound.test.ts +128 -0
  62. package/src/modules/channel_gmail/lib/__tests__/credentials.test.ts +76 -0
  63. package/src/modules/channel_gmail/lib/__tests__/gmail-client.test.ts +209 -0
  64. package/src/modules/channel_gmail/lib/__tests__/normalize-inbound.test.ts +106 -0
  65. package/src/modules/channel_gmail/lib/__tests__/oauth.test.ts +148 -0
  66. package/src/modules/channel_gmail/lib/adapter.ts +734 -0
  67. package/src/modules/channel_gmail/lib/capabilities.ts +22 -0
  68. package/src/modules/channel_gmail/lib/convert-outbound.ts +136 -0
  69. package/src/modules/channel_gmail/lib/credentials.ts +90 -0
  70. package/src/modules/channel_gmail/lib/gmail-client.ts +305 -0
  71. package/src/modules/channel_gmail/lib/health.ts +14 -0
  72. package/src/modules/channel_gmail/lib/normalize-inbound.ts +57 -0
  73. package/src/modules/channel_gmail/lib/oauth.ts +128 -0
  74. package/src/modules/channel_gmail/setup.ts +36 -0
  75. package/src/modules/channel_gmail/widgets/injection/connect/widget.client.tsx +28 -0
  76. package/src/modules/channel_gmail/widgets/injection/connect/widget.ts +16 -0
  77. package/src/modules/channel_gmail/widgets/injection-table.ts +12 -0
  78. package/tsconfig.json +9 -0
  79. package/watch.mjs +7 -0
@@ -0,0 +1,542 @@
1
+ import { gmailCapabilities } from "./capabilities.js";
2
+ import {
3
+ gmailChannelStateSchema,
4
+ gmailClientCredentialsSchema,
5
+ gmailUserCredentialsSchema,
6
+ parseScopes
7
+ } from "./credentials.js";
8
+ import {
9
+ decodeBase64Url,
10
+ encodeBase64Url,
11
+ getGmailApiClient,
12
+ GmailApiError
13
+ } from "./gmail-client.js";
14
+ import {
15
+ getGoogleOAuthClient,
16
+ tokenResponseToExpiresAt
17
+ } from "./oauth.js";
18
+ import {
19
+ convertOutboundForGmail
20
+ } from "./convert-outbound.js";
21
+ import { normalizeInboundGmailMessage } from "./normalize-inbound.js";
22
+ import { emailResolveContact } from "@open-mercato/core/modules/communication_channels/lib/email-contact";
23
+ import { encodeCursor } from "@open-mercato/core/modules/communication_channels/lib/email-mime";
24
+ class GmailChannelAdapter {
25
+ constructor() {
26
+ this.providerKey = "gmail";
27
+ this.channelType = "email";
28
+ this.capabilities = gmailCapabilities;
29
+ }
30
+ async sendMessage(input) {
31
+ const userCredentials = parseUserCredentialsOrThrow(input.credentials);
32
+ let native;
33
+ try {
34
+ native = await convertOutboundForGmail({
35
+ body: input.content.html ?? input.content.text ?? "",
36
+ bodyFormat: input.content.bodyFormat ?? (input.content.html ? "html" : "text"),
37
+ attachments: input.content.attachments,
38
+ channelMetadata: input.metadata,
39
+ fromAddress: userCredentials.email ?? "me"
40
+ });
41
+ } catch (error) {
42
+ const message = error instanceof Error ? error.message : "Outbound conversion failed";
43
+ return { externalMessageId: "", status: "failed", error: message };
44
+ }
45
+ const nativeMeta = native.metadata;
46
+ const rawBase64Url = encodeBase64Url(nativeMeta.rawMessage);
47
+ try {
48
+ const response = await getGmailApiClient().sendRawMessage(
49
+ { accessToken: userCredentials.accessToken },
50
+ { rawBase64Url, threadId: nativeMeta.threadId }
51
+ );
52
+ return {
53
+ externalMessageId: nativeMeta.messageId ?? response.id,
54
+ conversationId: response.threadId,
55
+ status: "sent",
56
+ metadata: { gmailMessageId: response.id, gmailThreadId: response.threadId, labelIds: response.labelIds ?? [] }
57
+ };
58
+ } catch (error) {
59
+ if (error instanceof GmailApiError && error.status === 401) {
60
+ return { externalMessageId: "", status: "failed", error: "requires_reauth" };
61
+ }
62
+ const message = error instanceof Error ? error.message : "Gmail send failed";
63
+ return { externalMessageId: "", status: "failed", error: message };
64
+ }
65
+ }
66
+ async verifyWebhook(_input) {
67
+ return { raw: {}, eventType: "other", metadata: { reason: "gmail-uses-polling-not-push" } };
68
+ }
69
+ async getStatus(_input) {
70
+ return { status: "sent" };
71
+ }
72
+ async convertOutbound(input) {
73
+ return convertOutboundForGmail({ ...input, fromAddress: "me" });
74
+ }
75
+ async normalizeInbound(raw) {
76
+ const payload = raw.raw;
77
+ const rawMessage = pickRawMimeBuffer(payload);
78
+ const gmailMessageId = typeof payload.gmailMessageId === "string" ? payload.gmailMessageId : "unknown";
79
+ const gmailThreadId = typeof payload.gmailThreadId === "string" ? payload.gmailThreadId : gmailMessageId;
80
+ const labelIds = Array.isArray(payload.labelIds) ? payload.labelIds.filter((v) => typeof v === "string") : [];
81
+ const accountIdentifier = typeof payload.accountIdentifier === "string" ? payload.accountIdentifier : "unknown@gmail";
82
+ return normalizeInboundGmailMessage({
83
+ rawMessage,
84
+ gmailMessageId,
85
+ gmailThreadId,
86
+ gmailLabelIds: labelIds,
87
+ accountIdentifier
88
+ });
89
+ }
90
+ async buildOAuthAuthorizeUrl(input) {
91
+ const client = parseClientCredentialsOrThrow(input.credentials);
92
+ const scopes = parseScopes(client.scopes);
93
+ const url = getGoogleOAuthClient().buildAuthorizeUrl({
94
+ clientId: client.clientId,
95
+ redirectUri: input.redirectUri,
96
+ state: input.state,
97
+ scopes,
98
+ loginHint: input.loginHint
99
+ });
100
+ return { authorizeUrl: url, extra: { scopes } };
101
+ }
102
+ async exchangeOAuthCode(input) {
103
+ const client = parseClientCredentialsOrThrow(input.credentials);
104
+ const token = await getGoogleOAuthClient().exchangeCode({
105
+ clientId: client.clientId,
106
+ clientSecret: client.clientSecret,
107
+ redirectUri: input.redirectUri,
108
+ code: input.code
109
+ });
110
+ let email;
111
+ let displayName;
112
+ try {
113
+ const userInfo = await getGoogleOAuthClient().fetchUserInfo(token.access_token);
114
+ email = userInfo.email;
115
+ displayName = userInfo.name ?? userInfo.email;
116
+ } catch {
117
+ }
118
+ const expiresAt = tokenResponseToExpiresAt(token);
119
+ const credentials = {
120
+ accessToken: token.access_token,
121
+ refreshToken: token.refresh_token,
122
+ expiresAt: expiresAt?.toISOString(),
123
+ scopes: token.scope ? token.scope.split(" ").filter(Boolean) : void 0,
124
+ email
125
+ };
126
+ return {
127
+ credentials,
128
+ externalIdentifier: email,
129
+ displayName: displayName ?? email,
130
+ expiresAt
131
+ };
132
+ }
133
+ async refreshCredentials(input) {
134
+ const current = parseUserCredentialsOrThrow(input.credentials);
135
+ if (!current.refreshToken) {
136
+ throw new Error("requires_reauth");
137
+ }
138
+ const clientFromState = resolveGmailOAuthClient(input);
139
+ const token = await getGoogleOAuthClient().refreshToken({
140
+ clientId: clientFromState.clientId,
141
+ clientSecret: clientFromState.clientSecret,
142
+ refreshToken: current.refreshToken
143
+ });
144
+ const expiresAt = tokenResponseToExpiresAt(token);
145
+ const refreshed = {
146
+ accessToken: token.access_token,
147
+ // Google does NOT always return a new refresh token — keep the existing one.
148
+ refreshToken: token.refresh_token ?? current.refreshToken,
149
+ expiresAt: expiresAt?.toISOString(),
150
+ scopes: token.scope ? token.scope.split(" ").filter(Boolean) : current.scopes,
151
+ email: current.email
152
+ };
153
+ return {
154
+ credentials: refreshed,
155
+ expiresAt
156
+ };
157
+ }
158
+ async fetchHistory(input) {
159
+ const userCredentials = parseUserCredentialsOrThrow(input.credentials);
160
+ const channelState = gmailChannelStateSchema.parse(input.channelState ?? {});
161
+ const auth = { accessToken: userCredentials.accessToken };
162
+ const api = getGmailApiClient();
163
+ const limit = input.limit ?? 50;
164
+ if (channelState.pendingMessagesHistoryIdSnapshot && !channelState.pendingMessagesPageToken) {
165
+ return await this.startMessagesListFallback(
166
+ api,
167
+ auth,
168
+ userCredentials.email ?? "me",
169
+ channelState.pendingMessagesHistoryIdSnapshot,
170
+ limit
171
+ );
172
+ }
173
+ if (!channelState.historyId && !channelState.pendingMessagesPageToken) {
174
+ const profile = await api.getProfile(auth);
175
+ const nextState = {
176
+ historyId: profile.historyId,
177
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
178
+ };
179
+ return {
180
+ messages: [],
181
+ nextCursor: encodeCursor(nextState),
182
+ hasMore: false
183
+ };
184
+ }
185
+ if (channelState.pendingMessagesPageToken) {
186
+ return await this.continueMessagesListDrain(
187
+ api,
188
+ auth,
189
+ userCredentials.email ?? "me",
190
+ channelState,
191
+ limit
192
+ );
193
+ }
194
+ let messages = [];
195
+ try {
196
+ return await this.continueHistoryListDrain(
197
+ api,
198
+ auth,
199
+ userCredentials.email ?? "me",
200
+ channelState,
201
+ String(channelState.historyId),
202
+ limit
203
+ );
204
+ } catch (error) {
205
+ if (error instanceof GmailApiError && error.status === 404) {
206
+ const profile = await api.getProfile(auth);
207
+ return await this.startMessagesListFallback(
208
+ api,
209
+ auth,
210
+ userCredentials.email ?? "me",
211
+ profile.historyId,
212
+ limit
213
+ );
214
+ }
215
+ throw error;
216
+ }
217
+ }
218
+ /**
219
+ * Spec C § Phase C2 — Register Gmail Pub/Sub watch.
220
+ *
221
+ * Calls `gmail.users.watch` with the operator-configured Pub/Sub topic.
222
+ * Returns `historyId` (cursor for subsequent `history.list` calls) and
223
+ * `expiration` (ms since epoch — Gmail caps at ~7 days). Persists both
224
+ * onto `CommunicationChannel.channelState` via the hub's
225
+ * `push.register` command.
226
+ */
227
+ async registerPush(input) {
228
+ const userCredentials = parseUserCredentialsOrThrow(input.credentials);
229
+ const auth = { accessToken: userCredentials.accessToken };
230
+ const api = getGmailApiClient();
231
+ const topicName = input.providerConfig?.pubsubTopic ?? "";
232
+ if (!topicName) {
233
+ return {
234
+ providerKey: this.providerKey,
235
+ status: "failed",
236
+ channelStatePatch: {
237
+ pushStatus: "failed",
238
+ lastPushError: {
239
+ code: "missing_topic",
240
+ message: "Pub/Sub topic not configured",
241
+ at: (/* @__PURE__ */ new Date()).toISOString()
242
+ }
243
+ },
244
+ error: {
245
+ code: "missing_topic",
246
+ message: "OM_GMAIL_PUBSUB_TOPIC not configured for this tenant"
247
+ }
248
+ };
249
+ }
250
+ try {
251
+ const result = await api.watchInbox(auth, { topicName, labelIds: ["INBOX"] });
252
+ const expirationMs = Number(result.expiration);
253
+ return {
254
+ providerKey: this.providerKey,
255
+ status: "active",
256
+ channelStatePatch: {
257
+ historyId: result.historyId,
258
+ watchExpirationMs: Number.isFinite(expirationMs) ? expirationMs : Date.now() + 6 * 24 * 3600 * 1e3,
259
+ pubsubTopic: topicName,
260
+ pushStatus: "active",
261
+ lastPushError: null
262
+ },
263
+ recommendedPollIntervalSeconds: 1800
264
+ };
265
+ } catch (error) {
266
+ const status = error instanceof GmailApiError ? error.status : 0;
267
+ const detail = error instanceof Error ? error.message : "watch failed";
268
+ return {
269
+ providerKey: this.providerKey,
270
+ status: "failed",
271
+ channelStatePatch: {
272
+ pushStatus: "failed",
273
+ lastPushError: {
274
+ code: `gmail_watch_${status || "error"}`,
275
+ message: detail.slice(0, 500),
276
+ at: (/* @__PURE__ */ new Date()).toISOString()
277
+ }
278
+ },
279
+ error: { code: `gmail_watch_${status || "error"}`, message: detail }
280
+ };
281
+ }
282
+ }
283
+ /**
284
+ * Spec C § Phase C2 — Tear down Gmail watch via `gmail.users.stop`.
285
+ * Idempotent: a 404 (no active watch) is swallowed.
286
+ */
287
+ async unregisterPush(input) {
288
+ const userCredentials = parseUserCredentialsOrThrow(input.credentials);
289
+ const auth = { accessToken: userCredentials.accessToken };
290
+ const api = getGmailApiClient();
291
+ try {
292
+ await api.stopWatch(auth);
293
+ } catch (error) {
294
+ if (error instanceof GmailApiError && error.status === 404) return;
295
+ throw error;
296
+ }
297
+ }
298
+ /**
299
+ * Spec C § Phase C2 — Convert a verified Pub/Sub notification into a
300
+ * `HistoryPage`. The notification body itself is just `{ emailAddress,
301
+ * historyId }`; the actual messages come from `history.list` against
302
+ * `channelState.historyId`. We delegate to `fetchHistory` so the
303
+ * pagination / 404-fallback logic stays in one place.
304
+ */
305
+ async applyPushNotification(input) {
306
+ return this.fetchHistory({
307
+ conversationId: "INBOX",
308
+ credentials: input.credentials,
309
+ channelState: input.channelState,
310
+ scope: input.scope,
311
+ // Push notifications are bursty (1/s max per Gmail user); use a
312
+ // smaller per-call limit so the worker drains quickly between
313
+ // notifications without holding the API quota.
314
+ limit: 50
315
+ });
316
+ }
317
+ async continueHistoryListDrain(api, auth, accountIdentifier, channelState, startHistoryId, limit) {
318
+ const collected = [];
319
+ const seen = /* @__PURE__ */ new Set();
320
+ let pageToken = channelState.pendingHistoryPageToken;
321
+ let lastResponseHistoryId;
322
+ let drained = false;
323
+ while (true) {
324
+ const history = await api.listHistory(auth, {
325
+ startHistoryId,
326
+ pageToken
327
+ });
328
+ lastResponseHistoryId = history.historyId ?? lastResponseHistoryId;
329
+ for (const ref of collectMessageRefs(history.history ?? [])) {
330
+ if (seen.has(ref.id)) continue;
331
+ seen.add(ref.id);
332
+ collected.push(ref);
333
+ }
334
+ if (!history.nextPageToken) {
335
+ drained = true;
336
+ break;
337
+ }
338
+ pageToken = history.nextPageToken;
339
+ if (collected.length >= limit) break;
340
+ }
341
+ const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, collected, accountIdentifier);
342
+ const nextState = {
343
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
344
+ };
345
+ if (hardFailed) {
346
+ nextState.historyId = startHistoryId;
347
+ } else if (drained) {
348
+ nextState.historyId = lastResponseHistoryId ?? startHistoryId;
349
+ } else {
350
+ nextState.historyId = startHistoryId;
351
+ nextState.pendingHistoryPageToken = pageToken;
352
+ }
353
+ return {
354
+ messages,
355
+ nextCursor: encodeCursor(nextState),
356
+ // Re-enqueue immediately when a transient failure left work behind.
357
+ hasMore: hardFailed || !drained
358
+ };
359
+ }
360
+ async startMessagesListFallback(api, auth, accountIdentifier, historyIdSnapshot, limit) {
361
+ const list = await api.listMessages(auth, {
362
+ labelIds: ["INBOX"],
363
+ maxResults: limit
364
+ });
365
+ const refs = (list.messages ?? []).map((m) => ({
366
+ id: m.id,
367
+ threadId: m.threadId,
368
+ labelIds: ["INBOX"]
369
+ }));
370
+ const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, refs, accountIdentifier);
371
+ const drained = !list.nextPageToken;
372
+ const nextState = {
373
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
374
+ };
375
+ if (hardFailed) {
376
+ nextState.pendingMessagesHistoryIdSnapshot = historyIdSnapshot;
377
+ } else if (drained) {
378
+ nextState.historyId = historyIdSnapshot;
379
+ } else {
380
+ nextState.pendingMessagesPageToken = list.nextPageToken;
381
+ nextState.pendingMessagesHistoryIdSnapshot = historyIdSnapshot;
382
+ }
383
+ return {
384
+ messages,
385
+ nextCursor: encodeCursor(nextState),
386
+ hasMore: hardFailed || !drained
387
+ };
388
+ }
389
+ async continueMessagesListDrain(api, auth, accountIdentifier, channelState, limit) {
390
+ const list = await api.listMessages(auth, {
391
+ labelIds: ["INBOX"],
392
+ maxResults: limit,
393
+ pageToken: channelState.pendingMessagesPageToken
394
+ });
395
+ const refs = (list.messages ?? []).map((m) => ({
396
+ id: m.id,
397
+ threadId: m.threadId,
398
+ labelIds: ["INBOX"]
399
+ }));
400
+ const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, refs, accountIdentifier);
401
+ const drained = !list.nextPageToken;
402
+ const nextState = {
403
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
404
+ };
405
+ if (hardFailed) {
406
+ nextState.pendingMessagesPageToken = channelState.pendingMessagesPageToken;
407
+ nextState.pendingMessagesHistoryIdSnapshot = channelState.pendingMessagesHistoryIdSnapshot;
408
+ } else if (drained) {
409
+ nextState.historyId = channelState.pendingMessagesHistoryIdSnapshot ?? channelState.historyId;
410
+ } else {
411
+ nextState.pendingMessagesPageToken = list.nextPageToken;
412
+ nextState.pendingMessagesHistoryIdSnapshot = channelState.pendingMessagesHistoryIdSnapshot;
413
+ }
414
+ return {
415
+ messages,
416
+ nextCursor: encodeCursor(nextState),
417
+ hasMore: hardFailed || !drained
418
+ };
419
+ }
420
+ async deleteMessage(input) {
421
+ const userCredentials = parseUserCredentialsOrThrow(input.credentials);
422
+ const api = getGmailApiClient();
423
+ await api.trashMessage({ accessToken: userCredentials.accessToken }, input.externalMessageId);
424
+ }
425
+ async resolveContact(input) {
426
+ return emailResolveContact(input);
427
+ }
428
+ /**
429
+ * Fetch + normalize each collected message ref.
430
+ *
431
+ * L3 fix: a non-404/410 error on `getMessageRaw` (e.g. a transient 500/403)
432
+ * used to re-throw and abort the whole tick, discarding messages that had
433
+ * already normalized in the same page. Worse, because the cursor could be
434
+ * advanced from a different source (a push notification carrying a higher
435
+ * historyId), the transiently-failed messages could be skipped permanently.
436
+ *
437
+ * We now treat a hard failure as a stop point: keep the messages normalized
438
+ * BEFORE the failure and signal `hardFailed: true` so the caller pins the
439
+ * persisted `historyId` (does NOT advance past the failure) and re-fetches on
440
+ * the next tick. 404/410 stay skipped (the message is genuinely gone).
441
+ */
442
+ async fetchAndNormalize(api, auth, refs, accountIdentifier) {
443
+ const out = [];
444
+ for (const ref of refs) {
445
+ let raw;
446
+ try {
447
+ raw = await api.getMessageRaw(auth, ref.id);
448
+ } catch (error) {
449
+ if (error instanceof GmailApiError && (error.status === 404 || error.status === 410)) continue;
450
+ return { messages: out, hardFailed: true };
451
+ }
452
+ const rawBuffer = decodeBase64Url(raw.raw);
453
+ const fallbackDate = raw.internalDate ? new Date(Number(raw.internalDate)) : void 0;
454
+ const normalized = await normalizeInboundGmailMessage({
455
+ rawMessage: rawBuffer,
456
+ gmailMessageId: raw.id,
457
+ gmailThreadId: raw.threadId,
458
+ gmailLabelIds: raw.labelIds ?? ref.labelIds ?? [],
459
+ accountIdentifier,
460
+ fallbackDate
461
+ });
462
+ out.push(normalized);
463
+ }
464
+ return { messages: out, hardFailed: false };
465
+ }
466
+ }
467
+ function collectMessageRefs(history) {
468
+ const seen = /* @__PURE__ */ new Set();
469
+ const refs = [];
470
+ for (const entry of history) {
471
+ for (const added of entry.messagesAdded ?? []) {
472
+ if (seen.has(added.message.id)) continue;
473
+ seen.add(added.message.id);
474
+ refs.push({ id: added.message.id, threadId: added.message.threadId, labelIds: added.message.labelIds });
475
+ }
476
+ }
477
+ return refs;
478
+ }
479
+ function parseUserCredentialsOrThrow(value) {
480
+ const parsed = gmailUserCredentialsSchema.safeParse(value);
481
+ if (!parsed.success) {
482
+ const first = parsed.error.issues[0];
483
+ throw new Error(`Invalid Gmail credentials: ${first?.message ?? "unknown validation error"}`);
484
+ }
485
+ return parsed.data;
486
+ }
487
+ function parseClientCredentialsOrThrow(value) {
488
+ const parsed = gmailClientCredentialsSchema.safeParse(value);
489
+ if (!parsed.success) {
490
+ const first = parsed.error.issues[0];
491
+ throw new Error(`Invalid Gmail OAuth client credentials: ${first?.message ?? "unknown validation error"}`);
492
+ }
493
+ return parsed.data;
494
+ }
495
+ let warnedLegacyClientPath = false;
496
+ function resolveGmailOAuthClient(input) {
497
+ if (input.oauthClient) {
498
+ const client = input.oauthClient;
499
+ if (!client.clientId) {
500
+ throw new Error("[internal] Invalid Gmail OAuth client credentials: OAuth Client ID required");
501
+ }
502
+ if (!client.clientSecret) {
503
+ throw new Error("[internal] Invalid Gmail OAuth client credentials: clientSecret required");
504
+ }
505
+ return {
506
+ clientId: client.clientId,
507
+ clientSecret: client.clientSecret,
508
+ // `GmailClientCredentials.scopes` is the wire format the legacy
509
+ // `credentials._client` blob carried — comma/space-separated string.
510
+ // Spec A's `OAuthClientConfig.scopes` is the canonical `string[]`.
511
+ // `parseScopes` accepts either separator, so join with a single space.
512
+ ...client.scopes !== void 0 ? { scopes: client.scopes.join(" ") } : {}
513
+ };
514
+ }
515
+ if (!warnedLegacyClientPath) {
516
+ warnedLegacyClientPath = true;
517
+ console.warn(
518
+ "[channel-gmail] reading OAuth client config from credentials._client is deprecated; pass via RefreshCredentialsInput.oauthClient instead (Spec A)."
519
+ );
520
+ }
521
+ return parseClientCredentialsOrThrow(
522
+ input.credentials._client ?? input.credentials
523
+ );
524
+ }
525
+ function pickRawMimeBuffer(payload) {
526
+ if (typeof payload.rawBase64Url === "string") return decodeBase64Url(payload.rawBase64Url);
527
+ const value = payload.rawBody;
528
+ if (Buffer.isBuffer(value)) return value;
529
+ if (value instanceof Uint8Array) return Buffer.from(value);
530
+ if (typeof value === "string") return Buffer.from(value, "utf-8");
531
+ throw new Error("[internal] Gmail normalizeInbound requires `raw.rawBase64Url` or `raw.rawBody`");
532
+ }
533
+ let cachedAdapter = null;
534
+ function getGmailChannelAdapter() {
535
+ if (!cachedAdapter) cachedAdapter = new GmailChannelAdapter();
536
+ return cachedAdapter;
537
+ }
538
+ export {
539
+ GmailChannelAdapter,
540
+ getGmailChannelAdapter
541
+ };
542
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_gmail/lib/adapter.ts"],
4
+ "sourcesContent": ["import type {\n ApplyPushNotificationInput,\n ChannelAdapter,\n ChannelNativeContent,\n ConvertOutboundInput,\n BuildOAuthAuthorizeUrlInput,\n BuildOAuthAuthorizeUrlResult,\n DeleteChannelMessageInput,\n ExchangeOAuthCodeInput,\n ExchangeOAuthCodeResult,\n FetchHistoryInput,\n GetMessageStatusInput,\n HistoryPage,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n PushRegistration,\n RefreshCredentialsInput,\n RefreshedCredentials,\n RegisterPushInput,\n ResolveContactInput,\n ContactHint,\n SendMessageInput,\n SendMessageResult,\n UnregisterPushInput,\n VerifyWebhookInput,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport { gmailCapabilities } from './capabilities'\nimport {\n gmailChannelStateSchema,\n gmailClientCredentialsSchema,\n gmailUserCredentialsSchema,\n parseScopes,\n type GmailChannelState,\n type GmailClientCredentials,\n type GmailUserCredentials,\n} from './credentials'\nimport {\n decodeBase64Url,\n encodeBase64Url,\n getGmailApiClient,\n GmailApiError,\n type GmailGetMessageRawResponse,\n} from './gmail-client'\nimport {\n getGoogleOAuthClient,\n tokenResponseToExpiresAt,\n} from './oauth'\nimport {\n convertOutboundForGmail,\n type GmailEmailNativeMetadata,\n} from './convert-outbound'\nimport { normalizeInboundGmailMessage } from './normalize-inbound'\nimport { emailResolveContact } from '@open-mercato/core/modules/communication_channels/lib/email-contact'\nimport { encodeCursor } from '@open-mercato/core/modules/communication_channels/lib/email-mime'\n\n/**\n * Gmail `ChannelAdapter`. OAuth2-based, polling-driven (`realtimePush: false`).\n *\n * Credential shape on `CommunicationChannel.credentials`:\n * - Per-user: `{ accessToken, refreshToken?, expiresAt?, scopes?, email? }`\n * - Tenant OAuth client config sits on `IntegrationCredentials.credentials`\n * for the `gmail` provider: `{ clientId, clientSecret, scopes? }`. The hub\n * looks it up by `(tenantId, providerKey='gmail')` and passes it to\n * `buildOAuthAuthorizeUrl` + `exchangeOAuthCode` + `refreshCredentials`.\n *\n * Sync model:\n * - First poll on a fresh channel reads `gmail.users.getProfile.historyId`\n * and persists it; no message fetch is done on the bootstrap call (the\n * existing inbox is intentionally not back-filled, matching the spec).\n * - Subsequent polls call `gmail.users.history.list?startHistoryId=\u2026` and\n * fetch each `messagesAdded` entry's RAW payload.\n * - If the server returns `404` for the history id (Gmail keeps ~7 days),\n * we fall back to `gmail.users.messages.list?labelIds=INBOX` and persist\n * the new historyId from the next `getProfile` call.\n */\nclass GmailChannelAdapter implements ChannelAdapter {\n readonly providerKey = 'gmail'\n readonly channelType = 'email'\n readonly capabilities = gmailCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n const userCredentials = parseUserCredentialsOrThrow(input.credentials)\n let native: ChannelNativeContent\n try {\n native = await convertOutboundForGmail({\n body: input.content.html ?? input.content.text ?? '',\n bodyFormat: input.content.bodyFormat ?? (input.content.html ? 'html' : 'text'),\n attachments: input.content.attachments,\n channelMetadata: input.metadata,\n fromAddress: userCredentials.email ?? 'me',\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Outbound conversion failed'\n return { externalMessageId: '', status: 'failed', error: message }\n }\n\n const nativeMeta = native.metadata as unknown as GmailEmailNativeMetadata\n const rawBase64Url = encodeBase64Url(nativeMeta.rawMessage)\n\n try {\n const response = await getGmailApiClient().sendRawMessage(\n { accessToken: userCredentials.accessToken },\n { rawBase64Url, threadId: nativeMeta.threadId },\n )\n return {\n externalMessageId: nativeMeta.messageId ?? response.id,\n conversationId: response.threadId,\n status: 'sent',\n metadata: { gmailMessageId: response.id, gmailThreadId: response.threadId, labelIds: response.labelIds ?? [] },\n }\n } catch (error) {\n if (error instanceof GmailApiError && error.status === 401) {\n // `requires_reauth` is a protocol sentinel the hub keys on (see\n // communication_channels error-classification.isReauthError), not a\n // user/log message \u2014 do NOT prefix or translate it.\n return { externalMessageId: '', status: 'failed', error: 'requires_reauth' }\n }\n const message = error instanceof Error ? error.message : 'Gmail send failed'\n return { externalMessageId: '', status: 'failed', error: message }\n }\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n // Gmail Pub/Sub push (Spec C) is handled by the dedicated `/webhooks/gmail`\n // route + `applyPushNotification`, not this generic hub-webhook hook, so this\n // returns an unhandled event for the generic route to ack 2xx.\n return { raw: {}, eventType: 'other', metadata: { reason: 'gmail-uses-polling-not-push' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n // Gmail exposes no per-message delivery-status API, so we return a\n // best-effort `'sent'` placeholder (a later bounce is not reflected here).\n // Mirrors the IMAP adapter's documented behavior.\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return convertOutboundForGmail({ ...input, fromAddress: 'me' })\n }\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n const payload = raw.raw as {\n rawBase64Url?: unknown\n rawBody?: unknown\n gmailMessageId?: unknown\n gmailThreadId?: unknown\n labelIds?: unknown\n accountIdentifier?: unknown\n }\n const rawMessage = pickRawMimeBuffer(payload)\n const gmailMessageId = typeof payload.gmailMessageId === 'string' ? payload.gmailMessageId : 'unknown'\n const gmailThreadId = typeof payload.gmailThreadId === 'string' ? payload.gmailThreadId : gmailMessageId\n const labelIds = Array.isArray(payload.labelIds) ? (payload.labelIds.filter((v) => typeof v === 'string') as string[]) : []\n const accountIdentifier = typeof payload.accountIdentifier === 'string' ? payload.accountIdentifier : 'unknown@gmail'\n return normalizeInboundGmailMessage({\n rawMessage,\n gmailMessageId,\n gmailThreadId,\n gmailLabelIds: labelIds,\n accountIdentifier,\n })\n }\n\n async buildOAuthAuthorizeUrl(input: BuildOAuthAuthorizeUrlInput): Promise<BuildOAuthAuthorizeUrlResult> {\n const client = parseClientCredentialsOrThrow(input.credentials)\n const scopes = parseScopes(client.scopes)\n const url = getGoogleOAuthClient().buildAuthorizeUrl({\n clientId: client.clientId,\n redirectUri: input.redirectUri,\n state: input.state,\n scopes,\n loginHint: input.loginHint,\n })\n return { authorizeUrl: url, extra: { scopes } }\n }\n\n async exchangeOAuthCode(input: ExchangeOAuthCodeInput): Promise<ExchangeOAuthCodeResult> {\n const client = parseClientCredentialsOrThrow(input.credentials)\n const token = await getGoogleOAuthClient().exchangeCode({\n clientId: client.clientId,\n clientSecret: client.clientSecret,\n redirectUri: input.redirectUri,\n code: input.code,\n })\n let email: string | undefined\n let displayName: string | undefined\n try {\n const userInfo = await getGoogleOAuthClient().fetchUserInfo(token.access_token)\n email = userInfo.email\n displayName = userInfo.name ?? userInfo.email\n } catch {\n // Userinfo failure is non-fatal; fall back to the optional `id_token` parser later.\n }\n const expiresAt = tokenResponseToExpiresAt(token)\n const credentials: GmailUserCredentials = {\n accessToken: token.access_token,\n refreshToken: token.refresh_token,\n expiresAt: expiresAt?.toISOString(),\n scopes: token.scope ? token.scope.split(' ').filter(Boolean) : undefined,\n email,\n }\n return {\n credentials: credentials as unknown as Record<string, unknown>,\n externalIdentifier: email,\n displayName: displayName ?? email,\n expiresAt,\n }\n }\n\n async refreshCredentials(input: RefreshCredentialsInput): Promise<RefreshedCredentials> {\n const current = parseUserCredentialsOrThrow(input.credentials)\n if (!current.refreshToken) {\n throw new Error('requires_reauth')\n }\n // Spec A: prefer the new `input.oauthClient` slot (resolved by the hub from\n // the `channel_gmail` integration's tenant-scoped client credentials). Fall\n // back to the deprecated `credentials._client` path for one minor release so\n // existing test fixtures keep working.\n const clientFromState = resolveGmailOAuthClient(input)\n const token = await getGoogleOAuthClient().refreshToken({\n clientId: clientFromState.clientId,\n clientSecret: clientFromState.clientSecret,\n refreshToken: current.refreshToken,\n })\n const expiresAt = tokenResponseToExpiresAt(token)\n const refreshed: GmailUserCredentials = {\n accessToken: token.access_token,\n // Google does NOT always return a new refresh token \u2014 keep the existing one.\n refreshToken: token.refresh_token ?? current.refreshToken,\n expiresAt: expiresAt?.toISOString(),\n scopes: token.scope ? token.scope.split(' ').filter(Boolean) : current.scopes,\n email: current.email,\n }\n return {\n credentials: refreshed as unknown as Record<string, unknown>,\n expiresAt,\n }\n }\n\n async fetchHistory(input: FetchHistoryInput): Promise<HistoryPage> {\n const userCredentials = parseUserCredentialsOrThrow(input.credentials)\n const channelState = gmailChannelStateSchema.parse(input.channelState ?? {})\n const auth = { accessToken: userCredentials.accessToken }\n const api = getGmailApiClient()\n const limit = input.limit ?? 50\n\n // L3 first-page retry: a prior fallback scan hard-failed on its FIRST page and\n // pinned only the history snapshot (no page token). Re-enter the fallback scan\n // from the first INBOX page so unprocessed messages are retried, not skipped by\n // the bootstrap path below.\n if (channelState.pendingMessagesHistoryIdSnapshot && !channelState.pendingMessagesPageToken) {\n return await this.startMessagesListFallback(\n api,\n auth,\n userCredentials.email ?? 'me',\n channelState.pendingMessagesHistoryIdSnapshot,\n limit,\n )\n }\n\n // Bootstrap path: no historyId yet \u2192 just persist current historyId and skip fetch.\n if (!channelState.historyId && !channelState.pendingMessagesPageToken) {\n const profile = await api.getProfile(auth)\n const nextState: GmailChannelState = {\n historyId: profile.historyId,\n lastSyncedAt: new Date().toISOString(),\n }\n return {\n messages: [],\n nextCursor: encodeCursor(nextState),\n hasMore: false,\n }\n }\n\n // Mid-drain fallback (404 path): we previously fell back to messages.list\n // for an expired historyId and still have pages to drain. Resume from the\n // stored pageToken without re-issuing the history.list call.\n if (channelState.pendingMessagesPageToken) {\n return await this.continueMessagesListDrain(\n api,\n auth,\n userCredentials.email ?? 'me',\n channelState,\n limit,\n )\n }\n\n // Incremental path: history.list since stored historyId, walking\n // nextPageToken until either (a) all pages drained, or (b) we've collected\n // `limit` messages.\n // CRITICAL: terminal historyId is ONLY advanced after full drain. While a\n // pending pageToken exists in channelState, the original startHistoryId is\n // retained so the next tick re-enters the same history window.\n let messages: NormalizedInboundMessage[] = []\n try {\n return await this.continueHistoryListDrain(\n api,\n auth,\n userCredentials.email ?? 'me',\n channelState,\n String(channelState.historyId),\n limit,\n )\n } catch (error) {\n if (error instanceof GmailApiError && error.status === 404) {\n // Gmail history expired (~7-day retention). Fall back to a paged inbox\n // scan via messages.list. Snapshot the post-fallback historyId now so\n // we can advance it once the messages.list drain completes.\n const profile = await api.getProfile(auth)\n return await this.startMessagesListFallback(\n api,\n auth,\n userCredentials.email ?? 'me',\n profile.historyId,\n limit,\n )\n }\n throw error\n }\n }\n\n /**\n * Spec C \u00A7 Phase C2 \u2014 Register Gmail Pub/Sub watch.\n *\n * Calls `gmail.users.watch` with the operator-configured Pub/Sub topic.\n * Returns `historyId` (cursor for subsequent `history.list` calls) and\n * `expiration` (ms since epoch \u2014 Gmail caps at ~7 days). Persists both\n * onto `CommunicationChannel.channelState` via the hub's\n * `push.register` command.\n */\n async registerPush(input: RegisterPushInput): Promise<PushRegistration> {\n const userCredentials = parseUserCredentialsOrThrow(input.credentials)\n const auth = { accessToken: userCredentials.accessToken }\n const api = getGmailApiClient()\n const topicName = (input.providerConfig?.pubsubTopic as string | undefined) ?? ''\n if (!topicName) {\n return {\n providerKey: this.providerKey,\n status: 'failed',\n channelStatePatch: {\n pushStatus: 'failed',\n lastPushError: {\n code: 'missing_topic',\n message: 'Pub/Sub topic not configured',\n at: new Date().toISOString(),\n },\n },\n error: {\n code: 'missing_topic',\n message: 'OM_GMAIL_PUBSUB_TOPIC not configured for this tenant',\n },\n }\n }\n try {\n const result = await api.watchInbox(auth, { topicName, labelIds: ['INBOX'] })\n const expirationMs = Number(result.expiration)\n return {\n providerKey: this.providerKey,\n status: 'active',\n channelStatePatch: {\n historyId: result.historyId,\n watchExpirationMs: Number.isFinite(expirationMs) ? expirationMs : Date.now() + 6 * 24 * 3600 * 1000,\n pubsubTopic: topicName,\n pushStatus: 'active',\n lastPushError: null,\n },\n recommendedPollIntervalSeconds: 1800,\n }\n } catch (error) {\n const status = error instanceof GmailApiError ? error.status : 0\n const detail = error instanceof Error ? error.message : 'watch failed'\n return {\n providerKey: this.providerKey,\n status: 'failed',\n channelStatePatch: {\n pushStatus: 'failed',\n lastPushError: {\n code: `gmail_watch_${status || 'error'}`,\n message: detail.slice(0, 500),\n at: new Date().toISOString(),\n },\n },\n error: { code: `gmail_watch_${status || 'error'}`, message: detail },\n }\n }\n }\n\n /**\n * Spec C \u00A7 Phase C2 \u2014 Tear down Gmail watch via `gmail.users.stop`.\n * Idempotent: a 404 (no active watch) is swallowed.\n */\n async unregisterPush(input: UnregisterPushInput): Promise<void> {\n const userCredentials = parseUserCredentialsOrThrow(input.credentials)\n const auth = { accessToken: userCredentials.accessToken }\n const api = getGmailApiClient()\n try {\n await api.stopWatch(auth)\n } catch (error) {\n if (error instanceof GmailApiError && error.status === 404) return\n throw error\n }\n }\n\n /**\n * Spec C \u00A7 Phase C2 \u2014 Convert a verified Pub/Sub notification into a\n * `HistoryPage`. The notification body itself is just `{ emailAddress,\n * historyId }`; the actual messages come from `history.list` against\n * `channelState.historyId`. We delegate to `fetchHistory` so the\n * pagination / 404-fallback logic stays in one place.\n */\n async applyPushNotification(input: ApplyPushNotificationInput): Promise<HistoryPage> {\n // The notification's `historyId` is informational \u2014 Gmail guarantees\n // it is `>= channelState.historyId`, but a multi-event batch may\n // advance further than what `history.list` returns in a single page.\n // Treat the call as \"drain whatever is new since the stored cursor\".\n // If `channelState.historyId` is absent (a push arrived before the cursor\n // was seeded), `fetchHistory` bootstraps \u2014 persists the current historyId\n // and returns zero messages \u2014 so this notification's delta is picked up on\n // the next call. In practice `registerPush` seeds the cursor before any\n // push flows, so this edge is not hit in the normal lifecycle.\n return this.fetchHistory({\n conversationId: 'INBOX',\n credentials: input.credentials,\n channelState: input.channelState,\n scope: input.scope,\n // Push notifications are bursty (1/s max per Gmail user); use a\n // smaller per-call limit so the worker drains quickly between\n // notifications without holding the API quota.\n limit: 50,\n } as FetchHistoryInput)\n }\n\n private async continueHistoryListDrain(\n api: ReturnType<typeof getGmailApiClient>,\n auth: { accessToken: string },\n accountIdentifier: string,\n channelState: GmailChannelState,\n startHistoryId: string,\n limit: number,\n ): Promise<HistoryPage> {\n const collected: Array<{ id: string; threadId: string; labelIds?: string[] }> = []\n const seen = new Set<string>()\n let pageToken: string | undefined = channelState.pendingHistoryPageToken\n let lastResponseHistoryId: string | undefined\n let drained = false\n\n // Fully consume each page before deciding to stop: `pageToken` must only ever\n // advance past refs we have actually collected, otherwise a page carrying more\n // than `limit` new refs would silently drop the overflow. `collected` may\n // therefore exceed `limit` by up to one page \u2014 bounded and intentional.\n while (true) {\n const history = await api.listHistory(auth, {\n startHistoryId,\n pageToken,\n })\n lastResponseHistoryId = history.historyId ?? lastResponseHistoryId\n for (const ref of collectMessageRefs(history.history ?? [])) {\n if (seen.has(ref.id)) continue\n seen.add(ref.id)\n collected.push(ref)\n }\n if (!history.nextPageToken) {\n drained = true\n break\n }\n pageToken = history.nextPageToken\n if (collected.length >= limit) break\n }\n\n const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, collected, accountIdentifier)\n const nextState: GmailChannelState = {\n lastSyncedAt: new Date().toISOString(),\n }\n if (hardFailed) {\n // L3: a message failed transiently. Restart the window from startHistoryId\n // on the next tick (drop any page token) so every page \u2014 including the one\n // carrying the failed message \u2014 is re-read; already-ingested messages dedup\n // at the hub. Do NOT advance the terminal historyId or pin a forward token,\n // which would skip the failed message's page.\n nextState.historyId = startHistoryId\n } else if (drained) {\n // All pages drained \u2014 advance the terminal historyId.\n nextState.historyId = lastResponseHistoryId ?? startHistoryId\n } else {\n // Mid-drain \u2014 keep the prior startHistoryId pinned + remember the next\n // unconsumed pageToken so the following tick resumes without re-walking.\n nextState.historyId = startHistoryId\n nextState.pendingHistoryPageToken = pageToken\n }\n return {\n messages,\n nextCursor: encodeCursor(nextState),\n // Re-enqueue immediately when a transient failure left work behind.\n hasMore: hardFailed || !drained,\n }\n }\n\n private async startMessagesListFallback(\n api: ReturnType<typeof getGmailApiClient>,\n auth: { accessToken: string },\n accountIdentifier: string,\n historyIdSnapshot: string,\n limit: number,\n ): Promise<HistoryPage> {\n const list = await api.listMessages(auth, {\n labelIds: ['INBOX'],\n maxResults: limit,\n })\n const refs = (list.messages ?? []).map((m) => ({\n id: m.id,\n threadId: m.threadId,\n labelIds: ['INBOX'],\n }))\n const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, refs, accountIdentifier)\n const drained = !list.nextPageToken\n const nextState: GmailChannelState = {\n lastSyncedAt: new Date().toISOString(),\n }\n if (hardFailed) {\n // L3: this is the FIRST fallback page (no prior page token), so there is\n // nothing to pin. Deliberately leave `historyId` unset so the cursor does\n // NOT advance past the unprocessed messages \u2014 the next tick re-enters the\n // same fallback scan and retries them.\n nextState.pendingMessagesHistoryIdSnapshot = historyIdSnapshot\n } else if (drained) {\n nextState.historyId = historyIdSnapshot\n } else {\n nextState.pendingMessagesPageToken = list.nextPageToken\n nextState.pendingMessagesHistoryIdSnapshot = historyIdSnapshot\n }\n return {\n messages,\n nextCursor: encodeCursor(nextState),\n hasMore: hardFailed || !drained,\n }\n }\n\n private async continueMessagesListDrain(\n api: ReturnType<typeof getGmailApiClient>,\n auth: { accessToken: string },\n accountIdentifier: string,\n channelState: GmailChannelState,\n limit: number,\n ): Promise<HistoryPage> {\n const list = await api.listMessages(auth, {\n labelIds: ['INBOX'],\n maxResults: limit,\n pageToken: channelState.pendingMessagesPageToken,\n })\n const refs = (list.messages ?? []).map((m) => ({\n id: m.id,\n threadId: m.threadId,\n labelIds: ['INBOX'],\n }))\n const { messages, hardFailed } = await this.fetchAndNormalize(api, auth, refs, accountIdentifier)\n const drained = !list.nextPageToken\n const nextState: GmailChannelState = {\n lastSyncedAt: new Date().toISOString(),\n }\n if (hardFailed) {\n // L3: re-pin the SAME page token (not list.nextPageToken) so the next\n // tick re-fetches this page and retries the unprocessed messages.\n nextState.pendingMessagesPageToken = channelState.pendingMessagesPageToken\n nextState.pendingMessagesHistoryIdSnapshot = channelState.pendingMessagesHistoryIdSnapshot\n } else if (drained) {\n nextState.historyId = channelState.pendingMessagesHistoryIdSnapshot ?? channelState.historyId\n } else {\n nextState.pendingMessagesPageToken = list.nextPageToken\n nextState.pendingMessagesHistoryIdSnapshot = channelState.pendingMessagesHistoryIdSnapshot\n }\n return {\n messages,\n nextCursor: encodeCursor(nextState),\n hasMore: hardFailed || !drained,\n }\n }\n\n async deleteMessage(input: DeleteChannelMessageInput): Promise<void> {\n const userCredentials = parseUserCredentialsOrThrow(input.credentials)\n const api = getGmailApiClient()\n // Gmail's \"delete\" capability is delivered as Trash to match the user's\n // mental model and avoid permanent loss on accidental clicks. The user can\n // restore from Trash in the Gmail web UI within 30 days.\n await api.trashMessage({ accessToken: userCredentials.accessToken }, input.externalMessageId)\n }\n\n async resolveContact(input: ResolveContactInput): Promise<ContactHint | null> {\n return emailResolveContact(input)\n }\n\n /**\n * Fetch + normalize each collected message ref.\n *\n * L3 fix: a non-404/410 error on `getMessageRaw` (e.g. a transient 500/403)\n * used to re-throw and abort the whole tick, discarding messages that had\n * already normalized in the same page. Worse, because the cursor could be\n * advanced from a different source (a push notification carrying a higher\n * historyId), the transiently-failed messages could be skipped permanently.\n *\n * We now treat a hard failure as a stop point: keep the messages normalized\n * BEFORE the failure and signal `hardFailed: true` so the caller pins the\n * persisted `historyId` (does NOT advance past the failure) and re-fetches on\n * the next tick. 404/410 stay skipped (the message is genuinely gone).\n */\n private async fetchAndNormalize(\n api: ReturnType<typeof getGmailApiClient>,\n auth: { accessToken: string },\n refs: Array<{ id: string; threadId: string; labelIds?: string[] }>,\n accountIdentifier: string,\n ): Promise<{ messages: NormalizedInboundMessage[]; hardFailed: boolean }> {\n const out: NormalizedInboundMessage[] = []\n for (const ref of refs) {\n let raw: GmailGetMessageRawResponse\n try {\n raw = await api.getMessageRaw(auth, ref.id)\n } catch (error) {\n // 404/410: the message is gone \u2014 skip it and keep draining.\n if (error instanceof GmailApiError && (error.status === 404 || error.status === 410)) continue\n // Any other failure is potentially transient. Stop here without\n // advancing past the unprocessed messages so the next tick retries them.\n return { messages: out, hardFailed: true }\n }\n const rawBuffer = decodeBase64Url(raw.raw)\n const fallbackDate = raw.internalDate ? new Date(Number(raw.internalDate)) : undefined\n const normalized = await normalizeInboundGmailMessage({\n rawMessage: rawBuffer,\n gmailMessageId: raw.id,\n gmailThreadId: raw.threadId,\n gmailLabelIds: raw.labelIds ?? ref.labelIds ?? [],\n accountIdentifier,\n fallbackDate,\n })\n out.push(normalized)\n }\n return { messages: out, hardFailed: false }\n }\n}\n\nfunction collectMessageRefs(\n history: Array<{\n messagesAdded?: Array<{ message: { id: string; threadId: string; labelIds?: string[] } }>\n }>,\n): Array<{ id: string; threadId: string; labelIds?: string[] }> {\n const seen = new Set<string>()\n const refs: Array<{ id: string; threadId: string; labelIds?: string[] }> = []\n for (const entry of history) {\n for (const added of entry.messagesAdded ?? []) {\n if (seen.has(added.message.id)) continue\n seen.add(added.message.id)\n refs.push({ id: added.message.id, threadId: added.message.threadId, labelIds: added.message.labelIds })\n }\n }\n return refs\n}\n\nfunction parseUserCredentialsOrThrow(value: unknown): GmailUserCredentials {\n const parsed = gmailUserCredentialsSchema.safeParse(value)\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n throw new Error(`Invalid Gmail credentials: ${first?.message ?? 'unknown validation error'}`)\n }\n return parsed.data\n}\n\nfunction parseClientCredentialsOrThrow(value: unknown): GmailClientCredentials {\n const parsed = gmailClientCredentialsSchema.safeParse(value)\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n throw new Error(`Invalid Gmail OAuth client credentials: ${first?.message ?? 'unknown validation error'}`)\n }\n return parsed.data\n}\n\nlet warnedLegacyClientPath = false\n\n/**\n * Resolve the OAuth client config for a Gmail refresh, preferring the new\n * `RefreshCredentialsInput.oauthClient` field (Spec A,\n * .ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md).\n *\n * Falls back to the deprecated `credentials._client` read path for one\n * minor release so existing tests keep working. The legacy path emits a\n * one-time deprecation warning per process so production logs stay quiet.\n */\nfunction resolveGmailOAuthClient(input: RefreshCredentialsInput): GmailClientCredentials {\n if (input.oauthClient) {\n const client = input.oauthClient\n if (!client.clientId) {\n throw new Error('[internal] Invalid Gmail OAuth client credentials: OAuth Client ID required')\n }\n if (!client.clientSecret) {\n throw new Error('[internal] Invalid Gmail OAuth client credentials: clientSecret required')\n }\n return {\n clientId: client.clientId,\n clientSecret: client.clientSecret,\n // `GmailClientCredentials.scopes` is the wire format the legacy\n // `credentials._client` blob carried \u2014 comma/space-separated string.\n // Spec A's `OAuthClientConfig.scopes` is the canonical `string[]`.\n // `parseScopes` accepts either separator, so join with a single space.\n ...(client.scopes !== undefined ? { scopes: client.scopes.join(' ') } : {}),\n }\n }\n // Legacy path \u2014 DEPRECATED. Remove in the next minor release.\n if (!warnedLegacyClientPath) {\n warnedLegacyClientPath = true\n console.warn(\n '[channel-gmail] reading OAuth client config from credentials._client is deprecated;' +\n ' pass via RefreshCredentialsInput.oauthClient instead (Spec A).',\n )\n }\n return parseClientCredentialsOrThrow(\n (input.credentials as unknown as { _client?: unknown })._client ?? input.credentials,\n )\n}\n\nfunction pickRawMimeBuffer(payload: { rawBase64Url?: unknown; rawBody?: unknown }): Buffer {\n if (typeof payload.rawBase64Url === 'string') return decodeBase64Url(payload.rawBase64Url)\n const value = payload.rawBody\n if (Buffer.isBuffer(value)) return value\n if (value instanceof Uint8Array) return Buffer.from(value)\n if (typeof value === 'string') return Buffer.from(value, 'utf-8')\n throw new Error('[internal] Gmail normalizeInbound requires `raw.rawBase64Url` or `raw.rawBody`')\n}\n\nlet cachedAdapter: GmailChannelAdapter | null = null\n\nexport function getGmailChannelAdapter(): GmailChannelAdapter {\n if (!cachedAdapter) cachedAdapter = new GmailChannelAdapter()\n return cachedAdapter\n}\n\nexport { GmailChannelAdapter }\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ import { baseEmailCapabilities } from "@open-mercato/core/modules/communication_channels/lib/email-capabilities";
2
+ const gmailCapabilities = {
3
+ ...baseEmailCapabilities,
4
+ // Gmail supports moving a message to Trash via the API.
5
+ deleteMessage: true
6
+ };
7
+ export {
8
+ gmailCapabilities
9
+ };
10
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_gmail/lib/capabilities.ts"],
4
+ "sourcesContent": ["import type { ChannelCapabilities } from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport { baseEmailCapabilities } from '@open-mercato/core/modules/communication_channels/lib/email-capabilities'\n\n/**\n * Gmail capabilities. `realtimePush: false` is deliberate: Gmail Pub/Sub push IS\n * implemented (the adapter registers/renews `users.watch` and applies history\n * notifications), but the hub keeps polling as a belt-and-suspenders fallback, so\n * the capability flag stays false to preserve the polling cadence.\n *\n * Threading is supported natively via Gmail `threadId` plus RFC2822\n * In-Reply-To/References. Attachment ceiling matches the shared email baseline.\n *\n * `fileSharing: false` (R2-M4 / F11, 2026-05-26): the adapter's\n * `convertOutbound` does not yet stitch attachment URLs into the base64-encoded\n * RFC2822 body it sends via `users.messages.send`. Re-enable when the URL-fetch\n * + MIME stitching flow lands.\n */\nexport const gmailCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n // Gmail supports moving a message to Trash via the API.\n deleteMessage: true,\n}\n"],
5
+ "mappings": "AACA,SAAS,6BAA6B;AAgB/B,MAAM,oBAAyC;AAAA,EACpD,GAAG;AAAA;AAAA,EAEH,eAAe;AACjB;",
6
+ "names": []
7
+ }