@msgly/gmail 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createGmailAdapter: () => createGmailAdapter
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var DEFAULT_TOKEN_URL = "https://oauth2.googleapis.com/token";
27
+ var DEFAULT_API_BASE = "https://gmail.googleapis.com";
28
+ var DEFAULT_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
29
+ var DEFAULT_CLOCK_SKEW_SEC = 300;
30
+ var DEFAULT_MAX_MESSAGES = 25;
31
+ var CAPABILITIES = {
32
+ text: true,
33
+ media: { image: false, video: false, audio: false, file: false },
34
+ interactive: { buttons: false, quickReplies: false },
35
+ templates: false,
36
+ reactions: false,
37
+ typing: false
38
+ };
39
+ function randomId() {
40
+ if (typeof globalThis.crypto?.randomUUID === "function") {
41
+ return globalThis.crypto.randomUUID();
42
+ }
43
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
44
+ }
45
+ function headerValue(headers, name) {
46
+ const v = headers[name] ?? headers[name.toLowerCase()];
47
+ if (Array.isArray(v)) return v[0];
48
+ return v;
49
+ }
50
+ function b64urlEncode(input) {
51
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
52
+ let binary = "";
53
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
54
+ return btoa(binary).replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
55
+ }
56
+ function b64urlDecodeToBytes(input) {
57
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
58
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
59
+ const binary = atob(padded);
60
+ const out = new Uint8Array(binary.length);
61
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
62
+ return out;
63
+ }
64
+ function b64urlDecodeToString(input) {
65
+ return new TextDecoder().decode(b64urlDecodeToBytes(input));
66
+ }
67
+ function createJwksCache(jwksUrl, ttlMs) {
68
+ let cache = null;
69
+ let inflight = null;
70
+ async function load(force = false) {
71
+ if (!force && cache && Date.now() - cache.fetchedAt < ttlMs) return cache;
72
+ if (inflight) return inflight;
73
+ inflight = (async () => {
74
+ const res = await fetch(jwksUrl);
75
+ if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);
76
+ const data = await res.json();
77
+ const keys = /* @__PURE__ */ new Map();
78
+ for (const jwk of data.keys ?? []) {
79
+ if (!jwk.kid || jwk.kty !== "RSA") continue;
80
+ try {
81
+ const key = await globalThis.crypto.subtle.importKey(
82
+ "jwk",
83
+ jwk,
84
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
85
+ false,
86
+ ["verify"]
87
+ );
88
+ keys.set(jwk.kid, key);
89
+ } catch {
90
+ }
91
+ }
92
+ const entry = { keys, fetchedAt: Date.now() };
93
+ cache = entry;
94
+ return entry;
95
+ })();
96
+ try {
97
+ return await inflight;
98
+ } finally {
99
+ inflight = null;
100
+ }
101
+ }
102
+ async function getKey(kid) {
103
+ let jwks = await load();
104
+ if (jwks.keys.has(kid)) return jwks.keys.get(kid);
105
+ jwks = await load(true);
106
+ return jwks.keys.get(kid) ?? null;
107
+ }
108
+ return { getKey };
109
+ }
110
+ async function verifyGoogleJwt(token, getKey, expectedAudience, expectedServiceAccountEmail, clockSkewSec) {
111
+ const parts = token.split(".");
112
+ if (parts.length !== 3) return false;
113
+ const [headerB64, payloadB64, sigB64] = parts;
114
+ let header;
115
+ let claims;
116
+ try {
117
+ header = JSON.parse(b64urlDecodeToString(headerB64));
118
+ claims = JSON.parse(b64urlDecodeToString(payloadB64));
119
+ } catch {
120
+ return false;
121
+ }
122
+ if (header.alg !== "RS256" || !header.kid) return false;
123
+ const key = await getKey(header.kid);
124
+ if (!key) return false;
125
+ const signature = b64urlDecodeToBytes(sigB64);
126
+ const signedInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
127
+ const ok = await globalThis.crypto.subtle.verify(
128
+ "RSASSA-PKCS1-v1_5",
129
+ key,
130
+ signature,
131
+ signedInput
132
+ );
133
+ if (!ok) return false;
134
+ const nowSec = Math.floor(Date.now() / 1e3);
135
+ if (typeof claims.exp === "number" && nowSec > claims.exp + clockSkewSec) return false;
136
+ if (typeof claims.nbf === "number" && nowSec + clockSkewSec < claims.nbf) return false;
137
+ if (claims.iss !== "https://accounts.google.com" && claims.iss !== "accounts.google.com") {
138
+ return false;
139
+ }
140
+ if (claims.aud !== expectedAudience) return false;
141
+ if (expectedServiceAccountEmail && claims.email !== expectedServiceAccountEmail) {
142
+ return false;
143
+ }
144
+ return true;
145
+ }
146
+ function createTokenCache(tokenUrl, clientId, clientSecret, refreshToken) {
147
+ let accessToken = null;
148
+ let expiresAt = 0;
149
+ let inflight = null;
150
+ async function fetchToken() {
151
+ const res = await fetch(tokenUrl, {
152
+ method: "POST",
153
+ headers: { "content-type": "application/x-www-form-urlencoded" },
154
+ body: new URLSearchParams({
155
+ grant_type: "refresh_token",
156
+ client_id: clientId,
157
+ client_secret: clientSecret,
158
+ refresh_token: refreshToken
159
+ }).toString()
160
+ });
161
+ const data = await res.json().catch(() => ({}));
162
+ if (!res.ok || !data.access_token) {
163
+ throw new Error(
164
+ `Google token refresh failed (${res.status}): ${data.error_description ?? data.error ?? "no body"}`
165
+ );
166
+ }
167
+ accessToken = data.access_token;
168
+ expiresAt = Date.now() + (data.expires_in ?? 3600) * 1e3 - 6e4;
169
+ return accessToken;
170
+ }
171
+ async function get() {
172
+ if (accessToken && Date.now() < expiresAt) return accessToken;
173
+ if (inflight) return inflight;
174
+ inflight = fetchToken();
175
+ try {
176
+ return await inflight;
177
+ } finally {
178
+ inflight = null;
179
+ }
180
+ }
181
+ return { get };
182
+ }
183
+ function findHeader(payload, name) {
184
+ const lower = name.toLowerCase();
185
+ for (const h of payload?.headers ?? []) {
186
+ if (h.name.toLowerCase() === lower) return h.value;
187
+ }
188
+ return void 0;
189
+ }
190
+ function findBodyByMimeType(payload, mimeType) {
191
+ if (!payload) return null;
192
+ if (payload.mimeType === mimeType && payload.body?.data) {
193
+ try {
194
+ return b64urlDecodeToString(payload.body.data);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ for (const part of payload.parts ?? []) {
200
+ const t = findBodyByMimeType(part, mimeType);
201
+ if (t !== null) return t;
202
+ }
203
+ return null;
204
+ }
205
+ function extractPlainText(payload) {
206
+ const plain = findBodyByMimeType(payload, "text/plain");
207
+ if (plain !== null) return plain.trim() || null;
208
+ const html = findBodyByMimeType(payload, "text/html");
209
+ if (html === null) return null;
210
+ return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() || null;
211
+ }
212
+ function parseEmailAddress(header) {
213
+ if (!header) return null;
214
+ const trimmed = header.trim();
215
+ const angle = trimmed.match(/^(?:"?([^"<]+?)"?\s*)?<([^>]+)>$/);
216
+ if (angle) {
217
+ return {
218
+ address: angle[2].trim(),
219
+ displayName: angle[1]?.trim() || void 0
220
+ };
221
+ }
222
+ if (/^\S+@\S+$/.test(trimmed)) return { address: trimmed };
223
+ return null;
224
+ }
225
+ function sanitizeHeaderValue(value) {
226
+ return value.replace(/[\r\n]/g, "");
227
+ }
228
+ function buildReplyEmail(opts) {
229
+ const headers = [
230
+ `From: ${sanitizeHeaderValue(opts.from)}`,
231
+ `To: ${sanitizeHeaderValue(opts.to)}`,
232
+ `Subject: ${sanitizeHeaderValue(opts.subject)}`,
233
+ "MIME-Version: 1.0",
234
+ "Content-Type: text/plain; charset=utf-8",
235
+ "Content-Transfer-Encoding: 8bit",
236
+ `Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`
237
+ ];
238
+ if (opts.inReplyTo) headers.push(`In-Reply-To: ${sanitizeHeaderValue(opts.inReplyTo)}`);
239
+ if (opts.references) headers.push(`References: ${sanitizeHeaderValue(opts.references)}`);
240
+ return `${headers.join("\r\n")}\r
241
+ \r
242
+ ${opts.body}`;
243
+ }
244
+ function constantTimeEqual(a, b) {
245
+ if (a.length !== b.length) return false;
246
+ let diff = 0;
247
+ for (let i = 0; i < a.length; i++) {
248
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
249
+ }
250
+ return diff === 0;
251
+ }
252
+ function stripReplyPrefix(subject) {
253
+ return subject.replace(/^(?:re|RE|Re)\s*:\s*/i, "").trim();
254
+ }
255
+ function createGmailAdapter(config) {
256
+ const tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
257
+ const apiBase = config.apiBase ?? DEFAULT_API_BASE;
258
+ const jwksUrl = config.jwksUrl ?? DEFAULT_JWKS_URL;
259
+ const clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
260
+ const maxMessages = config.maxMessagesPerNotification ?? DEFAULT_MAX_MESSAGES;
261
+ const tokens = createTokenCache(tokenUrl, config.clientId, config.clientSecret, config.refreshToken);
262
+ const jwks = createJwksCache(jwksUrl, 24 * 60 * 60 * 1e3);
263
+ let lastHistoryId = null;
264
+ async function authedFetch(path, init = {}) {
265
+ const token = await tokens.get();
266
+ const headers = new Headers(init.headers);
267
+ headers.set("authorization", `Bearer ${token}`);
268
+ if (!headers.has("content-type") && init.body) {
269
+ headers.set("content-type", "application/json");
270
+ }
271
+ return fetch(`${apiBase}${path}`, { ...init, headers });
272
+ }
273
+ async function watch(topicName, labelIds = ["INBOX"]) {
274
+ const res = await authedFetch("/gmail/v1/users/me/watch", {
275
+ method: "POST",
276
+ body: JSON.stringify({ topicName, labelIds, labelFilterAction: "include" })
277
+ });
278
+ const data = await res.json().catch(() => ({}));
279
+ if (!res.ok || !data.historyId) {
280
+ throw new Error(`Gmail watch failed (${res.status}): ${data.error?.message ?? "no historyId"}`);
281
+ }
282
+ lastHistoryId = data.historyId;
283
+ return { historyId: data.historyId };
284
+ }
285
+ async function stopWatch() {
286
+ await authedFetch("/gmail/v1/users/me/stop", { method: "POST" });
287
+ }
288
+ async function fetchMessage(messageId) {
289
+ const res = await authedFetch(
290
+ `/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}?format=full`
291
+ );
292
+ if (!res.ok) return null;
293
+ return await res.json();
294
+ }
295
+ async function listRecentInboxMessageIds(limit) {
296
+ const res = await authedFetch(
297
+ `/gmail/v1/users/me/messages?maxResults=${limit}&q=${encodeURIComponent("in:inbox -in:drafts is:unread")}`
298
+ );
299
+ if (!res.ok) return [];
300
+ const data = await res.json();
301
+ return (data.messages ?? []).map((m) => m.id);
302
+ }
303
+ async function listMessageIdsSince(startHistoryId) {
304
+ const res = await authedFetch(
305
+ `/gmail/v1/users/me/history?startHistoryId=${encodeURIComponent(startHistoryId)}&historyTypes=messageAdded&labelId=INBOX`
306
+ );
307
+ if (!res.ok) return [];
308
+ const data = await res.json();
309
+ const ids = /* @__PURE__ */ new Set();
310
+ for (const entry of data.history ?? []) {
311
+ for (const added of entry.messagesAdded ?? []) {
312
+ if (added.message?.id) ids.add(added.message.id);
313
+ }
314
+ }
315
+ return [...ids].slice(0, maxMessages);
316
+ }
317
+ function messageToInbound(msg) {
318
+ const text = extractPlainText(msg.payload);
319
+ if (!text) return null;
320
+ const from = parseEmailAddress(findHeader(msg.payload, "From"));
321
+ if (!from) return null;
322
+ const messageIdHeader = findHeader(msg.payload, "Message-ID") ?? findHeader(msg.payload, "Message-Id");
323
+ const subject = findHeader(msg.payload, "Subject") ?? "";
324
+ const references = findHeader(msg.payload, "References");
325
+ const dateHeader = findHeader(msg.payload, "Date");
326
+ const timestamp = (() => {
327
+ if (msg.internalDate) {
328
+ const ms = Number(msg.internalDate);
329
+ if (Number.isFinite(ms)) return new Date(ms).toISOString();
330
+ }
331
+ if (dateHeader) {
332
+ const parsed = Date.parse(dateHeader);
333
+ if (Number.isFinite(parsed)) return new Date(parsed).toISOString();
334
+ }
335
+ return (/* @__PURE__ */ new Date()).toISOString();
336
+ })();
337
+ return {
338
+ id: randomId(),
339
+ externalId: msg.id,
340
+ channel: "gmail",
341
+ direction: "inbound",
342
+ account: { channel: "gmail", channelAccountId: config.emailAddress },
343
+ contact: {
344
+ channel: "gmail",
345
+ channelUserId: from.address,
346
+ ...from.displayName ? { displayName: from.displayName } : {}
347
+ },
348
+ content: { type: "text", text },
349
+ timestamp,
350
+ raw: msg,
351
+ metadata: {
352
+ ...msg.threadId ? { threadId: msg.threadId } : {},
353
+ ...messageIdHeader ? { messageId: messageIdHeader } : {},
354
+ ...subject ? { subject } : {},
355
+ ...references ? { references } : {}
356
+ }
357
+ };
358
+ }
359
+ async function handleWebhook(req) {
360
+ const body = req.body;
361
+ const dataB64 = body?.message?.data;
362
+ if (!dataB64) return [];
363
+ let notification;
364
+ try {
365
+ notification = JSON.parse(b64urlDecodeToString(dataB64.replace(/=+$/g, "")));
366
+ } catch {
367
+ return [];
368
+ }
369
+ if (!notification.historyId) return [];
370
+ let messageIds;
371
+ if (lastHistoryId) {
372
+ messageIds = await listMessageIdsSince(lastHistoryId);
373
+ } else {
374
+ messageIds = await listRecentInboxMessageIds(maxMessages);
375
+ }
376
+ lastHistoryId = notification.historyId;
377
+ const out = [];
378
+ for (const id of messageIds) {
379
+ const msg = await fetchMessage(id);
380
+ if (!msg) continue;
381
+ const inbound = messageToInbound(msg);
382
+ if (inbound) out.push(inbound);
383
+ }
384
+ return out;
385
+ }
386
+ async function verifySignature(req) {
387
+ switch (config.pushAuth.kind) {
388
+ case "none":
389
+ return true;
390
+ case "token": {
391
+ const provided = req.query["token"];
392
+ const value = Array.isArray(provided) ? provided[0] : provided;
393
+ if (typeof value !== "string") return false;
394
+ return constantTimeEqual(value, config.pushAuth.token);
395
+ }
396
+ case "jwt": {
397
+ const auth = headerValue(req.headers, "authorization");
398
+ if (!auth || !auth.toLowerCase().startsWith("bearer ")) return false;
399
+ const token = auth.slice(7).trim();
400
+ if (!token) return false;
401
+ try {
402
+ return await verifyGoogleJwt(
403
+ token,
404
+ (kid) => jwks.getKey(kid),
405
+ config.pushAuth.expectedAudience,
406
+ config.pushAuth.expectedServiceAccountEmail,
407
+ clockSkewSec
408
+ );
409
+ } catch {
410
+ return false;
411
+ }
412
+ }
413
+ }
414
+ }
415
+ async function send(message) {
416
+ if (message.content.type !== "text") {
417
+ return {
418
+ messageId: message.id,
419
+ status: "failed",
420
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
421
+ error: {
422
+ code: "gmail_unsupported_content",
423
+ message: `Gmail adapter only supports text content in v1 (received: ${message.content.type})`
424
+ }
425
+ };
426
+ }
427
+ const subjectMeta = message.metadata?.["subject"];
428
+ const baseSubject = subjectMeta ? stripReplyPrefix(subjectMeta) : "";
429
+ const subject = baseSubject ? `Re: ${baseSubject}` : "(no subject)";
430
+ const inReplyTo = message.metadata?.["messageId"];
431
+ const referencesPrev = message.metadata?.["references"];
432
+ const references = inReplyTo ? referencesPrev ? `${referencesPrev} ${inReplyTo}` : inReplyTo : void 0;
433
+ const raw = buildReplyEmail({
434
+ from: config.emailAddress,
435
+ to: message.contact.channelUserId,
436
+ subject,
437
+ body: message.content.text,
438
+ inReplyTo,
439
+ references
440
+ });
441
+ const payload = { raw: b64urlEncode(raw) };
442
+ const threadId = message.metadata?.["threadId"];
443
+ if (threadId) payload["threadId"] = threadId;
444
+ const res = await authedFetch("/gmail/v1/users/me/messages/send", {
445
+ method: "POST",
446
+ body: JSON.stringify(payload)
447
+ });
448
+ const data = await res.json().catch(() => ({}));
449
+ if (res.status >= 200 && res.status < 300 && data.id) {
450
+ return {
451
+ messageId: message.id,
452
+ externalId: data.id,
453
+ status: "sent",
454
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
455
+ };
456
+ }
457
+ return {
458
+ messageId: message.id,
459
+ status: "failed",
460
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
461
+ error: {
462
+ code: `gmail_${data.error?.code ?? res.status}`,
463
+ message: data.error?.message ?? `HTTP ${res.status}`
464
+ }
465
+ };
466
+ }
467
+ async function verifyCredentials() {
468
+ if (!config.clientId || !config.clientSecret) {
469
+ return {
470
+ ok: false,
471
+ reason: "unauthorized",
472
+ hint: "GmailConfig.clientId / clientSecret missing. Generate them in Google Cloud Console \u2192 APIs & Services \u2192 Credentials \u2192 OAuth 2.0 Client ID."
473
+ };
474
+ }
475
+ if (!config.refreshToken) {
476
+ return {
477
+ ok: false,
478
+ reason: "unauthorized",
479
+ hint: "GmailConfig.refreshToken missing. Run the consent flow once with prompt=consent and access_type=offline to obtain a long-lived refresh token."
480
+ };
481
+ }
482
+ if (!config.emailAddress) {
483
+ return {
484
+ ok: false,
485
+ reason: "unauthorized",
486
+ hint: "GmailConfig.emailAddress missing. Set this to the mailbox the refresh token belongs to (e.g. agent@yourcompany.com)."
487
+ };
488
+ }
489
+ try {
490
+ const token = await tokens.get();
491
+ const res = await fetch(`${apiBase}/gmail/v1/users/me/profile`, {
492
+ headers: { authorization: `Bearer ${token}` }
493
+ });
494
+ if (res.status === 401 || res.status === 403) {
495
+ return {
496
+ ok: false,
497
+ reason: "unauthorized",
498
+ hint: "Google rejected the access token. Check scopes (need gmail.modify) and re-run consent with prompt=consent."
499
+ };
500
+ }
501
+ if (!res.ok) {
502
+ return {
503
+ ok: false,
504
+ reason: "unknown",
505
+ hint: `Gmail profile lookup returned ${res.status}`
506
+ };
507
+ }
508
+ const data = await res.json();
509
+ return { ok: true, accountInfo: data.emailAddress ?? config.emailAddress };
510
+ } catch (err) {
511
+ const msg = err instanceof Error ? err.message : String(err);
512
+ if (/401|invalid_grant|invalid_client/i.test(msg)) {
513
+ return {
514
+ ok: false,
515
+ reason: "unauthorized",
516
+ hint: `Google rejected credentials: ${msg}. Re-check clientId/clientSecret/refreshToken.`
517
+ };
518
+ }
519
+ return { ok: false, reason: "network_error", hint: msg };
520
+ }
521
+ }
522
+ async function uploadMedia(_file) {
523
+ throw new Error("Gmail uploadMedia is not yet implemented in v1.");
524
+ }
525
+ async function downloadMedia(_ref) {
526
+ throw new Error("Gmail downloadMedia is not yet implemented in v1.");
527
+ }
528
+ return {
529
+ channel: "gmail",
530
+ capabilities: CAPABILITIES,
531
+ send,
532
+ handleWebhook,
533
+ verifySignature,
534
+ verifyCredentials,
535
+ uploadMedia,
536
+ downloadMedia,
537
+ watch,
538
+ stopWatch
539
+ };
540
+ }
541
+ // Annotate the CommonJS export names for ESM import in node:
542
+ 0 && (module.exports = {
543
+ createGmailAdapter
544
+ });
@@ -0,0 +1,98 @@
1
+ import { Adapter } from '@msgly/core';
2
+
3
+ interface GmailConfig {
4
+ /** OAuth client id from Google Cloud Console → Credentials → OAuth 2.0 Client ID. */
5
+ clientId: string;
6
+ /** OAuth client secret from the same place. */
7
+ clientSecret: string;
8
+ /**
9
+ * Long-lived refresh token for the agent's mailbox. Generated once via the
10
+ * OAuth 2.0 consent flow with `prompt=consent` and `access_type=offline`.
11
+ * Required scopes: `https://www.googleapis.com/auth/gmail.modify` (read +
12
+ * send + watch).
13
+ */
14
+ refreshToken: string;
15
+ /**
16
+ * The email address of the mailbox the refresh token belongs to.
17
+ * Used as the `From:` header on outgoing replies and as
18
+ * `account.channelAccountId`.
19
+ */
20
+ emailAddress: string;
21
+ /**
22
+ * How Pub/Sub push requests prove they came from Google. Pick ONE:
23
+ *
24
+ * { kind: 'jwt', expectedAudience }
25
+ * — Verify the OIDC JWT in `Authorization: Bearer <token>`.
26
+ * expectedAudience should match the audience you configured on the
27
+ * Pub/Sub push subscription (often the webhook URL itself).
28
+ *
29
+ * { kind: 'token', token }
30
+ * — Simpler: configure your push subscription with
31
+ * `?token=...` and we'll match it against `req.query.token`.
32
+ *
33
+ * { kind: 'none' } — DEV ONLY. No verification.
34
+ */
35
+ pushAuth: {
36
+ kind: 'jwt';
37
+ expectedAudience: string;
38
+ expectedServiceAccountEmail?: string;
39
+ } | {
40
+ kind: 'token';
41
+ token: string;
42
+ } | {
43
+ kind: 'none';
44
+ };
45
+ /** Cap how many messages we fetch per Pub/Sub notification. Default: 25. */
46
+ maxMessagesPerNotification?: number;
47
+ /** Override the Google OAuth token endpoint. Default: oauth2.googleapis.com. */
48
+ tokenUrl?: string;
49
+ /** Override the Gmail API base. Default: gmail.googleapis.com. */
50
+ apiBase?: string;
51
+ /** Override the JWKS URL used when `pushAuth.kind === 'jwt'`. Default: Google certs. */
52
+ jwksUrl?: string;
53
+ /** Allowed clock skew (sec) when validating JWT exp/nbf. Default: 300. */
54
+ clockSkewSec?: number;
55
+ }
56
+ interface GmailAdapter extends Adapter {
57
+ readonly channel: 'gmail';
58
+ /**
59
+ * Call once at deploy time to subscribe the mailbox to a Pub/Sub topic.
60
+ * The topic must already grant publish permission to
61
+ * `gmail-api-push@system.gserviceaccount.com`. Returns the historyId
62
+ * baseline.
63
+ */
64
+ watch(topicName: string, labelIds?: string[]): Promise<{
65
+ historyId: string;
66
+ }>;
67
+ /** Stop the existing watch. Mailbox stops emitting notifications. */
68
+ stopWatch(): Promise<void>;
69
+ }
70
+ /**
71
+ * Gmail adapter for Msgly — receives via Pub/Sub push notifications,
72
+ * sends via the Gmail REST API.
73
+ *
74
+ * **Receive flow.** Gmail publishes change notifications to a Pub/Sub topic
75
+ * (you set this up once via `users.watch`). Pub/Sub forwards each event to
76
+ * your webhook with body `{ message: { data: base64, ... }, subscription }`.
77
+ * `data` decodes to `{ emailAddress, historyId }`. The adapter calls
78
+ * `history.list` from the previously-seen historyId to discover new message
79
+ * IDs, fetches each via `messages.get?format=full`, and emits an inbound
80
+ * message per item.
81
+ *
82
+ * **State.** The "last seen historyId" is held in adapter memory. On the
83
+ * very first notification (cold start), the adapter falls back to fetching
84
+ * recent unread INBOX messages so nothing gets lost between deploys.
85
+ *
86
+ * **Send flow.** Builds an RFC 5322 email with proper In-Reply-To /
87
+ * References headers and posts to `users.messages.send`. If you pass
88
+ * `metadata.threadId` from the inbound message, Gmail keeps the reply in
89
+ * the original thread.
90
+ *
91
+ * **Auth.** Pub/Sub authenticates inbound webhooks via OIDC JWT
92
+ * (Authorization: Bearer) or a shared verification token, configurable via
93
+ * `pushAuth`. JWT verification uses Google's public JWKS and runs on pure
94
+ * WebCrypto (Node 18+, Bun, Deno, browsers).
95
+ */
96
+ declare function createGmailAdapter(config: GmailConfig): GmailAdapter;
97
+
98
+ export { type GmailAdapter, type GmailConfig, createGmailAdapter };
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@msgly/gmail",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Gmail adapter for Msgly — receive and reply to emails via Gmail API + Pub/Sub push",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "main": "./dist/index.js",
7
+ "main": "./dist/index.cjs",
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js"
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
14
15
  }
15
16
  },
16
17
  "files": [
@@ -18,7 +19,7 @@
18
19
  "README.md"
19
20
  ],
20
21
  "dependencies": {
21
- "@msgly/core": "0.2.2"
22
+ "@msgly/core": "0.2.3"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^20.11.0",
@@ -44,8 +45,8 @@
44
45
  "webhook"
45
46
  ],
46
47
  "scripts": {
47
- "build": "tsup src/index.ts --format esm --dts --clean",
48
- "dev": "tsup src/index.ts --format esm --dts --watch",
48
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
49
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
49
50
  "test": "vitest run",
50
51
  "lint": "eslint src --ext .ts",
51
52
  "typecheck": "tsc --noEmit",