@aglyn/plugins-outreach 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +93 -0
- package/package.json +52 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/compliance-section.d.ts +20 -0
- package/src/lib/components/compliance-section.js +291 -0
- package/src/lib/components/compliance-section.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +36 -0
- package/src/lib/components/enroll-dialog.js +656 -0
- package/src/lib/components/enroll-dialog.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +28 -0
- package/src/lib/components/enrollments-table.js +375 -0
- package/src/lib/components/enrollments-table.js.map +1 -0
- package/src/lib/components/mailbox-card.d.ts +46 -0
- package/src/lib/components/mailbox-card.js +517 -0
- package/src/lib/components/mailbox-card.js.map +1 -0
- package/src/lib/components/mailboxes-section.d.ts +24 -0
- package/src/lib/components/mailboxes-section.js +258 -0
- package/src/lib/components/mailboxes-section.js.map +1 -0
- package/src/lib/components/outreach-console-page.d.ts +17 -0
- package/src/lib/components/outreach-console-page.js +81 -0
- package/src/lib/components/outreach-console-page.js.map +1 -0
- package/src/lib/components/outreach-console-sections.d.ts +43 -0
- package/src/lib/components/outreach-console-sections.js +55 -0
- package/src/lib/components/outreach-console-sections.js.map +1 -0
- package/src/lib/components/outreach-ui.d.ts +51 -0
- package/src/lib/components/outreach-ui.js +171 -0
- package/src/lib/components/outreach-ui.js.map +1 -0
- package/src/lib/components/send-window-fields.d.ts +25 -0
- package/src/lib/components/send-window-fields.js +178 -0
- package/src/lib/components/send-window-fields.js.map +1 -0
- package/src/lib/components/sequence-detail.d.ts +54 -0
- package/src/lib/components/sequence-detail.js +435 -0
- package/src/lib/components/sequence-detail.js.map +1 -0
- package/src/lib/components/sequence-editor.d.ts +37 -0
- package/src/lib/components/sequence-editor.js +485 -0
- package/src/lib/components/sequence-editor.js.map +1 -0
- package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
- package/src/lib/components/sequence-mailbox-picker.js +137 -0
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
- package/src/lib/components/sequence-preview.d.ts +27 -0
- package/src/lib/components/sequence-preview.js +146 -0
- package/src/lib/components/sequence-preview.js.map +1 -0
- package/src/lib/components/sequence-step-card.d.ts +31 -0
- package/src/lib/components/sequence-step-card.js +326 -0
- package/src/lib/components/sequence-step-card.js.map +1 -0
- package/src/lib/components/sequences-section.d.ts +40 -0
- package/src/lib/components/sequences-section.js +310 -0
- package/src/lib/components/sequences-section.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +38 -0
- package/src/lib/components/use-outreach-api.js +139 -0
- package/src/lib/components/use-outreach-api.js.map +1 -0
- package/src/lib/components/use-outreach-crm.d.ts +50 -0
- package/src/lib/components/use-outreach-crm.js +200 -0
- package/src/lib/components/use-outreach-crm.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +55 -0
- package/src/lib/components/use-outreach-data.js +199 -0
- package/src/lib/components/use-outreach-data.js.map +1 -0
- package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
- package/src/lib/components/use-outreach-mailbox-api.js +101 -0
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
- package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
- package/src/lib/components/use-outreach-mailboxes.js +68 -0
- package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
- package/src/lib/components/use-outreach-settings.d.ts +17 -0
- package/src/lib/components/use-outreach-settings.js +64 -0
- package/src/lib/components/use-outreach-settings.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +68 -0
- package/src/lib/constants/api-routes.js +57 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +46 -0
- package/src/lib/constants/bundle-common.js +55 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/runtime-jobs.d.ts +24 -0
- package/src/lib/constants/runtime-jobs.js +24 -0
- package/src/lib/constants/runtime-jobs.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +99 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/engine/compose.d.ts +86 -0
- package/src/lib/engine/compose.js +212 -0
- package/src/lib/engine/compose.js.map +1 -0
- package/src/lib/engine/delivery-status.d.ts +53 -0
- package/src/lib/engine/delivery-status.js +184 -0
- package/src/lib/engine/delivery-status.js.map +1 -0
- package/src/lib/engine/do-not-contact.d.ts +18 -0
- package/src/lib/engine/do-not-contact.js +34 -0
- package/src/lib/engine/do-not-contact.js.map +1 -0
- package/src/lib/engine/enrollment-state.d.ts +156 -0
- package/src/lib/engine/enrollment-state.js +303 -0
- package/src/lib/engine/enrollment-state.js.map +1 -0
- package/src/lib/engine/gates.d.ts +125 -0
- package/src/lib/engine/gates.js +232 -0
- package/src/lib/engine/gates.js.map +1 -0
- package/src/lib/engine/index.d.ts +28 -0
- package/src/lib/engine/index.js +33 -0
- package/src/lib/engine/index.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +49 -0
- package/src/lib/engine/mailbox-health.js +83 -0
- package/src/lib/engine/mailbox-health.js.map +1 -0
- package/src/lib/engine/opt-out-intent.d.ts +41 -0
- package/src/lib/engine/opt-out-intent.js +220 -0
- package/src/lib/engine/opt-out-intent.js.map +1 -0
- package/src/lib/engine/recipient-country.d.ts +41 -0
- package/src/lib/engine/recipient-country.js +171 -0
- package/src/lib/engine/recipient-country.js.map +1 -0
- package/src/lib/engine/schedule.d.ts +70 -0
- package/src/lib/engine/schedule.js +115 -0
- package/src/lib/engine/schedule.js.map +1 -0
- package/src/lib/engine/sending-capacity.d.ts +96 -0
- package/src/lib/engine/sending-capacity.js +172 -0
- package/src/lib/engine/sending-capacity.js.map +1 -0
- package/src/lib/engine/sequence-validation.d.ts +90 -0
- package/src/lib/engine/sequence-validation.js +291 -0
- package/src/lib/engine/sequence-validation.js.map +1 -0
- package/src/lib/engine/thread-classification.d.ts +78 -0
- package/src/lib/engine/thread-classification.js +224 -0
- package/src/lib/engine/thread-classification.js.map +1 -0
- package/src/lib/engine/thread-message.d.ts +80 -0
- package/src/lib/engine/thread-message.js +143 -0
- package/src/lib/engine/thread-message.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +81 -0
- package/src/lib/enrollment/enroll-people.js +250 -0
- package/src/lib/enrollment/enroll-people.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.d.ts +32 -0
- package/src/lib/enrollment/gate-lookups.js +154 -0
- package/src/lib/enrollment/gate-lookups.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
- package/src/lib/mailboxes/mailbox-api.js +66 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -0
- package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
- package/src/lib/mailboxes/mailbox-credentials.js +102 -0
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
- package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
- package/src/lib/mailboxes/mailbox-erasure.js +148 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
- package/src/lib/mailboxes/mailbox-gate.js +98 -0
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
- package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
- package/src/lib/mailboxes/mailbox-revoke.js +41 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
- package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
- package/src/lib/mailboxes/mailbox-routes.js +605 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
- package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
- package/src/lib/mailboxes/mailbox-settings.js +212 -0
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
- package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
- package/src/lib/mailboxes/mailbox-transport.js +94 -0
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
- package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
- package/src/lib/mailboxes/oauth-redirect.js +63 -0
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
- package/src/lib/mailboxes/oauth-state.d.ts +152 -0
- package/src/lib/mailboxes/oauth-state.js +212 -0
- package/src/lib/mailboxes/oauth-state.js.map +1 -0
- package/src/lib/mailboxes/outreach-config.d.ts +64 -0
- package/src/lib/mailboxes/outreach-config.js +77 -0
- package/src/lib/mailboxes/outreach-config.js.map +1 -0
- package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
- package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
- package/src/lib/model/compliance-settings.d.ts +78 -0
- package/src/lib/model/compliance-settings.js +155 -0
- package/src/lib/model/compliance-settings.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +210 -0
- package/src/lib/model/outreach-api.js +28 -0
- package/src/lib/model/outreach-api.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +563 -0
- package/src/lib/model/outreach.types.js +225 -0
- package/src/lib/model/outreach.types.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +84 -0
- package/src/lib/model/sequence-draft.js +239 -0
- package/src/lib/model/sequence-draft.js.map +1 -0
- package/src/lib/model/step-preview.d.ts +45 -0
- package/src/lib/model/step-preview.js +98 -0
- package/src/lib/model/step-preview.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +23 -0
- package/src/lib/model/stored-records.js +84 -0
- package/src/lib/model/stored-records.js.map +1 -0
- package/src/lib/plugin.d.ts +46 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +64 -0
- package/src/lib/routes/enroll-routes.js +350 -0
- package/src/lib/routes/enroll-routes.js.map +1 -0
- package/src/lib/routes/enrollment-routes.d.ts +19 -0
- package/src/lib/routes/enrollment-routes.js +171 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -0
- package/src/lib/routes/platform-deps.d.ts +51 -0
- package/src/lib/routes/platform-deps.js +66 -0
- package/src/lib/routes/platform-deps.js.map +1 -0
- package/src/lib/routes/preview-routes.d.ts +33 -0
- package/src/lib/routes/preview-routes.js +117 -0
- package/src/lib/routes/preview-routes.js.map +1 -0
- package/src/lib/routes/register-routes.d.ts +22 -0
- package/src/lib/routes/register-routes.js +92 -0
- package/src/lib/routes/register-routes.js.map +1 -0
- package/src/lib/routes/route-deps.d.ts +49 -0
- package/src/lib/routes/route-deps.js +27 -0
- package/src/lib/routes/route-deps.js.map +1 -0
- package/src/lib/routes/route-gate.d.ts +94 -0
- package/src/lib/routes/route-gate.js +87 -0
- package/src/lib/routes/route-gate.js.map +1 -0
- package/src/lib/routes/route-http.d.ts +32 -0
- package/src/lib/routes/route-http.js +62 -0
- package/src/lib/routes/route-http.js.map +1 -0
- package/src/lib/routes/sequence-routes.d.ts +32 -0
- package/src/lib/routes/sequence-routes.js +359 -0
- package/src/lib/routes/sequence-routes.js.map +1 -0
- package/src/lib/routes/settings-routes.d.ts +30 -0
- package/src/lib/routes/settings-routes.js +73 -0
- package/src/lib/routes/settings-routes.js.map +1 -0
- package/src/lib/runtime/enrollment-events.d.ts +86 -0
- package/src/lib/runtime/enrollment-events.js +141 -0
- package/src/lib/runtime/enrollment-events.js.map +1 -0
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
- package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
- package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
- package/src/lib/runtime/mailbox-health-store.js +168 -0
- package/src/lib/runtime/mailbox-health-store.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +39 -0
- package/src/lib/runtime/person-erasure.js +57 -0
- package/src/lib/runtime/person-erasure.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
- package/src/lib/runtime/platform-runtime-deps.js +98 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
- package/src/lib/runtime/runtime-deps.d.ts +77 -0
- package/src/lib/runtime/runtime-deps.js +18 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -0
- package/src/lib/runtime/send-job.d.ts +42 -0
- package/src/lib/runtime/send-job.js +780 -0
- package/src/lib/runtime/send-job.js.map +1 -0
- package/src/lib/runtime/sync-job.d.ts +41 -0
- package/src/lib/runtime/sync-job.js +480 -0
- package/src/lib/runtime/sync-job.js.map +1 -0
- package/src/lib/runtime/timeline.d.ts +67 -0
- package/src/lib/runtime/timeline.js +93 -0
- package/src/lib/runtime/timeline.js.map +1 -0
- package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
- package/src/lib/runtime/unsubscribe-link.js +121 -0
- package/src/lib/runtime/unsubscribe-link.js.map +1 -0
- package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
- package/src/lib/runtime/unsubscribe-route.js +103 -0
- package/src/lib/runtime/unsubscribe-route.js.map +1 -0
- package/src/lib/server.d.ts +53 -0
- package/src/lib/server.js +94 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/storage/compliance-settings-store.d.ts +44 -0
- package/src/lib/storage/compliance-settings-store.js +78 -0
- package/src/lib/storage/compliance-settings-store.js.map +1 -0
- package/src/lib/storage/do-not-contact-store.d.ts +60 -0
- package/src/lib/storage/do-not-contact-store.js +168 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -0
- package/src/lib/storage/outreach-records.d.ts +33 -0
- package/src/lib/storage/outreach-records.js +35 -0
- package/src/lib/storage/outreach-records.js.map +1 -0
- package/src/lib/subprocessors.d.ts +48 -0
- package/src/lib/subprocessors.js +70 -0
- package/src/lib/subprocessors.js.map +1 -0
- package/src/lib/transport/gmail-client.d.ts +145 -0
- package/src/lib/transport/gmail-client.js +229 -0
- package/src/lib/transport/gmail-client.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +84 -0
- package/src/lib/transport/gmail-errors.js +138 -0
- package/src/lib/transport/gmail-errors.js.map +1 -0
- package/src/lib/transport/google-oauth.d.ts +138 -0
- package/src/lib/transport/google-oauth.js +247 -0
- package/src/lib/transport/google-oauth.js.map +1 -0
- package/src/lib/transport/http.d.ts +53 -0
- package/src/lib/transport/http.js +79 -0
- package/src/lib/transport/http.js.map +1 -0
- package/src/lib/transport/rfc5322.d.ts +116 -0
- package/src/lib/transport/rfc5322.js +330 -0
- package/src/lib/transport/rfc5322.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +69 -0
- package/src/lib/transport/send-message.js +77 -0
- package/src/lib/transport/send-message.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/gmail-errors.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A GOOGLE CALL CAN FAIL WITH, AS OUTREACH ACTS ON IT (AGL-2978).\n *\n * Google answers in two dialects — the OAuth token endpoint's\n * `{ error: 'invalid_grant' }` and the Gmail API's\n * `{ error: { code, status, errors: [{ reason }] } }` — and a caller should\n * not have to speak either. Every failure the transport raises is one\n * {@link GmailTransportError} carrying the three decisions a caller makes:\n *\n * - `reconnectRequired` — the grant itself is gone (revoked, expired, a\n * password change, a scope removed). Nothing will work until the rep\n * connects again, so the mailbox goes to `reconnect_required`.\n * - `retryable` — the request may succeed later as it stands (a rate limit,\n * an outage, a dropped connection). `retryAfterMs` says when, if Google\n * said.\n * - neither — the request itself is wrong, or the deployment is (a bad\n * client id or secret), and retrying it changes nothing.\n */\n\nexport type GmailTransportErrorCode =\n /** The refresh token was refused: revoked, expired or otherwise dead. */\n | 'invalid_grant'\n /** The grant is missing a scope the call needs. */\n | 'insufficient_scope'\n /** The OAuth client id or secret was refused — a deployment problem. */\n | 'client_misconfigured'\n /** Rate-limited, still after backing off. */\n | 'rate_limited'\n /** The account's sending or API quota for the day is spent. */\n | 'quota_exceeded'\n /** Google answered 5xx, still after backing off. */\n | 'unavailable'\n /** The request never got an answer: a network failure or a timeout. */\n | 'network'\n /** The access token was refused even after a fresh one was minted. */\n | 'unauthorized'\n /** The account has no Gmail service (a Workspace user without Gmail). */\n | 'mail_service_unavailable'\n | 'not_found'\n /** Google refused the request as malformed. */\n | 'invalid_request'\n | 'forbidden'\n /** An answer the transport could not read. */\n | 'unexpected'\n\nexport interface GmailTransportErrorInit {\n status?: number | null\n retryAfterMs?: number | null\n /** The provider's own reason string, kept for logs and health rows. */\n providerReason?: string | null\n}\n\nconst RECONNECT: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'invalid_grant',\n 'insufficient_scope',\n])\n\nconst RETRYABLE: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'rate_limited',\n 'unavailable',\n 'network',\n])\n\nexport class GmailTransportError extends Error {\n readonly code: GmailTransportErrorCode\n readonly status: number | null\n readonly retryAfterMs: number | null\n readonly providerReason: string | null\n\n constructor(code: GmailTransportErrorCode, message: string, init: GmailTransportErrorInit = {}) {\n super(message)\n this.name = 'GmailTransportError'\n this.code = code\n this.status = init.status ?? null\n this.retryAfterMs = init.retryAfterMs ?? null\n this.providerReason = init.providerReason ?? null\n }\n\n /** The grant is gone; the mailbox needs connecting again. */\n get reconnectRequired(): boolean {\n return RECONNECT.has(this.code)\n }\n\n /** The same request may succeed later. */\n get retryable(): boolean {\n return RETRYABLE.has(this.code)\n }\n}\n\n/** Whether a thrown value says the mailbox's grant is gone. */\nexport function isReconnectRequired(error: unknown): boolean {\n return error instanceof GmailTransportError && error.reconnectRequired\n}\n\n/** The OAuth token endpoint's error body: `{ error, error_description }`. */\nexport function tokenEndpointError(status: number, body: unknown): GmailTransportError {\n const record = (body ?? {}) as { error?: unknown; error_description?: unknown }\n const reason = typeof record.error === 'string' ? record.error : null\n const described = typeof record.error_description === 'string' ? record.error_description : ''\n if (reason === 'invalid_grant') {\n return new GmailTransportError(\n 'invalid_grant',\n 'Google refused the mailbox grant. The mailbox must be connected again.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_client' || reason === 'unauthorized_client') {\n return new GmailTransportError(\n 'client_misconfigured',\n 'Google refused this deployment’s OAuth client. Check its client id and secret.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_scope') {\n return new GmailTransportError('insufficient_scope', 'Google refused a requested scope.', {\n status,\n providerReason: reason,\n })\n }\n return new GmailTransportError(\n status >= 500 ? 'unavailable' : 'invalid_request',\n `Google’s token endpoint refused the request${described ? `: ${described.slice(0, 200)}` : '.'}`,\n { status, providerReason: reason },\n )\n}\n\ninterface GoogleApiErrorBody {\n error?: {\n code?: number\n message?: string\n status?: string\n errors?: Array<{ reason?: string; message?: string }>\n details?: Array<{ reason?: string }>\n }\n}\n\n/** Every reason string a Gmail API error body carries. */\nfunction apiReasons(body: unknown): string[] {\n const error = (body as GoogleApiErrorBody | null)?.error\n return [\n ...(error?.errors ?? []).map((entry) => entry?.reason),\n ...(error?.details ?? []).map((entry) => entry?.reason),\n error?.status,\n ].filter((reason): reason is string => typeof reason === 'string' && reason.length > 0)\n}\n\n/** Whether a Gmail API 403 is a rate limit, which backs off like a 429. */\nexport function isRateLimitBody(body: unknown): boolean {\n return apiReasons(body).some((reason) =>\n ['rateLimitExceeded', 'userRateLimitExceeded', 'RESOURCE_EXHAUSTED'].includes(reason),\n )\n}\n\n/** A Gmail API error response, mapped. */\nexport function gmailApiError(\n status: number,\n body: unknown,\n init: { retryAfterMs?: number | null } = {},\n): GmailTransportError {\n const reasons = apiReasons(body)\n const providerReason = reasons[0] ?? null\n const has = (...names: string[]) => reasons.some((reason) => names.includes(reason))\n const message = (body as GoogleApiErrorBody | null)?.error?.message\n const detail = typeof message === 'string' && message ? `: ${message.slice(0, 200)}` : '.'\n const make = (code: GmailTransportErrorCode, text: string) =>\n new GmailTransportError(code, text, { status, providerReason, retryAfterMs: init.retryAfterMs })\n\n if (status === 429 || (status === 403 && isRateLimitBody(body))) {\n return make('rate_limited', 'Gmail is rate-limiting this mailbox. Try again later.')\n }\n if (has('dailyLimitExceeded', 'quotaExceeded')) {\n return make('quota_exceeded', 'This mailbox has spent its Gmail quota for the day.')\n }\n if (has('insufficientPermissions', 'ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {\n return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.')\n }\n if (status >= 500) return make('unavailable', `Gmail is unavailable${detail}`)\n if (status === 401) return make('unauthorized', 'Gmail refused the mailbox’s access token.')\n if (status === 400 && has('failedPrecondition') && /mail service not enabled/i.test(String(message))) {\n return make('mail_service_unavailable', 'This Google account has no Gmail service.')\n }\n if (status === 404) return make('not_found', `Gmail found nothing there${detail}`)\n if (status === 400) return make('invalid_request', `Gmail refused the request${detail}`)\n if (status === 403) return make('forbidden', `Gmail refused the request${detail}`)\n return make('unexpected', `Gmail answered ${status}${detail}`)\n}\n"],"names":["RECONNECT","Set","RETRYABLE","GmailTransportError","Error","reconnectRequired","has","code","retryable","message","init","name","status","retryAfterMs","providerReason","isReconnectRequired","error","tokenEndpointError","body","record","reason","described","error_description","slice","apiReasons","errors","map","entry","details","filter","length","isRateLimitBody","some","includes","gmailApiError","reasons","names","detail","make","text","test","String"],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type TransportDeps } from './http';
|
|
18
|
+
/**
|
|
19
|
+
* GOOGLE'S OAUTH ENDPOINTS, AS OUTREACH USES THEM (AGL-2978).
|
|
20
|
+
*
|
|
21
|
+
* Fetch-based, with no `googleapis` dependency: four endpoints and one ID
|
|
22
|
+
* token do not justify a client library's size, and a dependency that talks
|
|
23
|
+
* to Google is one more thing the egress register has to account for.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here reads configuration. The client id and secret, the redirect
|
|
26
|
+
* address and the tokens are all handed in, so this module is as testable
|
|
27
|
+
* with a fake `fetch` as it is safe to import.
|
|
28
|
+
*/
|
|
29
|
+
export declare const GOOGLE_OAUTH_ENDPOINTS: {
|
|
30
|
+
readonly authorize: "https://accounts.google.com/o/oauth2/v2/auth";
|
|
31
|
+
readonly token: "https://oauth2.googleapis.com/token";
|
|
32
|
+
readonly revoke: "https://oauth2.googleapis.com/revoke";
|
|
33
|
+
};
|
|
34
|
+
export declare const GMAIL_SEND_SCOPE = "https://www.googleapis.com/auth/gmail.send";
|
|
35
|
+
export declare const GMAIL_READONLY_SCOPE = "https://www.googleapis.com/auth/gmail.readonly";
|
|
36
|
+
/**
|
|
37
|
+
* What a mailbox grant asks for.
|
|
38
|
+
*
|
|
39
|
+
* `openid` and `email` identify the account. `gmail.send` sends as the rep.
|
|
40
|
+
* `gmail.readonly` reads the replies and bounces a sequence stops on —
|
|
41
|
+
* `gmail.metadata` would read headers but refuses the `q` search the bounce
|
|
42
|
+
* detection needs.
|
|
43
|
+
*/
|
|
44
|
+
export declare const OUTREACH_GOOGLE_SCOPES: readonly ["openid", "email", "https://www.googleapis.com/auth/gmail.send", "https://www.googleapis.com/auth/gmail.readonly"];
|
|
45
|
+
/** The two scopes a grant is useless without; `openid`/`email` come back renamed. */
|
|
46
|
+
export declare const OUTREACH_REQUIRED_GRANTED_SCOPES: readonly ["https://www.googleapis.com/auth/gmail.send", "https://www.googleapis.com/auth/gmail.readonly"];
|
|
47
|
+
/** The PKCE S256 challenge for a verifier. */
|
|
48
|
+
export declare function pkceChallenge(verifier: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The consent address a rep's browser is sent to.
|
|
51
|
+
*
|
|
52
|
+
* `access_type=offline` and `prompt=consent` together are what make Google
|
|
53
|
+
* issue a refresh token on EVERY connect, including a reconnect — without
|
|
54
|
+
* `prompt=consent` a second grant for the same account comes back with an
|
|
55
|
+
* access token only, and the mailbox could not send an hour later.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildGoogleAuthorizationUrl(input: {
|
|
58
|
+
clientId: string;
|
|
59
|
+
redirectUri: string;
|
|
60
|
+
state: string;
|
|
61
|
+
codeChallenge: string;
|
|
62
|
+
nonce: string;
|
|
63
|
+
loginHint?: string | null;
|
|
64
|
+
}): string;
|
|
65
|
+
/** What the token endpoint answers for a code or a refresh. */
|
|
66
|
+
export interface GoogleTokenResponse {
|
|
67
|
+
accessToken: string;
|
|
68
|
+
expiresInSeconds: number;
|
|
69
|
+
/** Only on a code exchange, and only when Google chose to issue one. */
|
|
70
|
+
refreshToken: string | null;
|
|
71
|
+
/** Space-separated, as granted — which may be less than requested. */
|
|
72
|
+
scope: string;
|
|
73
|
+
idToken: string | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Exchanges an authorization code, with its PKCE verifier.
|
|
77
|
+
*
|
|
78
|
+
* ONE attempt, whatever the caller's policy: a code is single-use, so a retry
|
|
79
|
+
* after an answer that was lost in transit is refused as `invalid_grant` and
|
|
80
|
+
* reads as a dead grant. A failed connect is one click to start again.
|
|
81
|
+
*/
|
|
82
|
+
export declare function exchangeGoogleAuthorizationCode(input: {
|
|
83
|
+
clientId: string;
|
|
84
|
+
clientSecret: string;
|
|
85
|
+
code: string;
|
|
86
|
+
redirectUri: string;
|
|
87
|
+
codeVerifier: string;
|
|
88
|
+
}, deps?: TransportDeps): Promise<GoogleTokenResponse>;
|
|
89
|
+
/** Mints an access token from a refresh token. */
|
|
90
|
+
export declare function refreshGoogleAccessToken(input: {
|
|
91
|
+
clientId: string;
|
|
92
|
+
clientSecret: string;
|
|
93
|
+
refreshToken: string;
|
|
94
|
+
}, deps?: TransportDeps): Promise<GoogleTokenResponse>;
|
|
95
|
+
/**
|
|
96
|
+
* Revokes a token at Google.
|
|
97
|
+
*
|
|
98
|
+
* Revoking a refresh token ends the whole grant — every token Google issued
|
|
99
|
+
* this client for the account — which is what a disconnect means, and why a
|
|
100
|
+
* RECONNECT must never revoke the token it replaces: the new one belongs to
|
|
101
|
+
* the same grant and would die with it.
|
|
102
|
+
*
|
|
103
|
+
* `already-invalid` when Google says the token was already dead, which is
|
|
104
|
+
* the outcome a disconnect wanted all along.
|
|
105
|
+
*/
|
|
106
|
+
export declare function revokeGoogleToken(token: string, deps?: TransportDeps): Promise<'revoked' | 'already-invalid'>;
|
|
107
|
+
/** Whether a granted scope string carries every scope in `required`. */
|
|
108
|
+
export declare function grantedScopesInclude(scope: string, required: readonly string[]): boolean;
|
|
109
|
+
/** The ID token claims Outreach relies on. */
|
|
110
|
+
export interface GoogleIdentity {
|
|
111
|
+
/** Google's stable account id. */
|
|
112
|
+
sub: string;
|
|
113
|
+
email: string;
|
|
114
|
+
}
|
|
115
|
+
export type GoogleIdTokenRefusal = 'malformed' | 'issuer' | 'audience' | 'expired' | 'nonce' | 'email-unverified';
|
|
116
|
+
/**
|
|
117
|
+
* The identity an ID token asserts, after the checks OpenID Connect requires
|
|
118
|
+
* of a token that arrived from the token endpoint itself.
|
|
119
|
+
*
|
|
120
|
+
* The signature is not re-verified here, on OIDC Core §3.1.3.7's allowance:
|
|
121
|
+
* the token was read off Google's own TLS response to our server's code
|
|
122
|
+
* exchange, never off a browser, so the channel authenticates it. What still
|
|
123
|
+
* has to be checked is that it is ABOUT this exchange: Google issued it
|
|
124
|
+
* (`iss`), to this client (`aud`, and `azp` when there are several
|
|
125
|
+
* audiences), it has not expired, it carries the nonce this connect minted,
|
|
126
|
+
* and the address is one Google verified.
|
|
127
|
+
*/
|
|
128
|
+
export declare function readGoogleIdToken(idToken: string | null | undefined, expected: {
|
|
129
|
+
clientId: string;
|
|
130
|
+
nonce: string;
|
|
131
|
+
nowMs: number;
|
|
132
|
+
}): {
|
|
133
|
+
ok: true;
|
|
134
|
+
identity: GoogleIdentity;
|
|
135
|
+
} | {
|
|
136
|
+
ok: false;
|
|
137
|
+
refusal: GoogleIdTokenRefusal;
|
|
138
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { createHash } from "node:crypto";
|
|
18
|
+
import { GmailTransportError, tokenEndpointError } from "./gmail-errors.js";
|
|
19
|
+
import { fetchWithBackoff } from "./http.js";
|
|
20
|
+
/**
|
|
21
|
+
* GOOGLE'S OAUTH ENDPOINTS, AS OUTREACH USES THEM (AGL-2978).
|
|
22
|
+
*
|
|
23
|
+
* Fetch-based, with no `googleapis` dependency: four endpoints and one ID
|
|
24
|
+
* token do not justify a client library's size, and a dependency that talks
|
|
25
|
+
* to Google is one more thing the egress register has to account for.
|
|
26
|
+
*
|
|
27
|
+
* Nothing here reads configuration. The client id and secret, the redirect
|
|
28
|
+
* address and the tokens are all handed in, so this module is as testable
|
|
29
|
+
* with a fake `fetch` as it is safe to import.
|
|
30
|
+
*/ export const GOOGLE_OAUTH_ENDPOINTS = {
|
|
31
|
+
authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
32
|
+
token: 'https://oauth2.googleapis.com/token',
|
|
33
|
+
revoke: 'https://oauth2.googleapis.com/revoke'
|
|
34
|
+
};
|
|
35
|
+
export const GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send';
|
|
36
|
+
export const GMAIL_READONLY_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly';
|
|
37
|
+
/**
|
|
38
|
+
* What a mailbox grant asks for.
|
|
39
|
+
*
|
|
40
|
+
* `openid` and `email` identify the account. `gmail.send` sends as the rep.
|
|
41
|
+
* `gmail.readonly` reads the replies and bounces a sequence stops on —
|
|
42
|
+
* `gmail.metadata` would read headers but refuses the `q` search the bounce
|
|
43
|
+
* detection needs.
|
|
44
|
+
*/ export const OUTREACH_GOOGLE_SCOPES = [
|
|
45
|
+
'openid',
|
|
46
|
+
'email',
|
|
47
|
+
GMAIL_SEND_SCOPE,
|
|
48
|
+
GMAIL_READONLY_SCOPE
|
|
49
|
+
];
|
|
50
|
+
/** The two scopes a grant is useless without; `openid`/`email` come back renamed. */ export const OUTREACH_REQUIRED_GRANTED_SCOPES = [
|
|
51
|
+
GMAIL_SEND_SCOPE,
|
|
52
|
+
GMAIL_READONLY_SCOPE
|
|
53
|
+
];
|
|
54
|
+
/** The issuers Google signs ID tokens as. */ const GOOGLE_ISSUERS = new Set([
|
|
55
|
+
'https://accounts.google.com',
|
|
56
|
+
'accounts.google.com'
|
|
57
|
+
]);
|
|
58
|
+
/** The PKCE S256 challenge for a verifier. */ export function pkceChallenge(verifier) {
|
|
59
|
+
return createHash('sha256').update(verifier, 'ascii').digest('base64url');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The consent address a rep's browser is sent to.
|
|
63
|
+
*
|
|
64
|
+
* `access_type=offline` and `prompt=consent` together are what make Google
|
|
65
|
+
* issue a refresh token on EVERY connect, including a reconnect — without
|
|
66
|
+
* `prompt=consent` a second grant for the same account comes back with an
|
|
67
|
+
* access token only, and the mailbox could not send an hour later.
|
|
68
|
+
*/ export function buildGoogleAuthorizationUrl(input) {
|
|
69
|
+
const url = new URL(GOOGLE_OAUTH_ENDPOINTS.authorize);
|
|
70
|
+
url.searchParams.set('client_id', input.clientId);
|
|
71
|
+
url.searchParams.set('redirect_uri', input.redirectUri);
|
|
72
|
+
url.searchParams.set('response_type', 'code');
|
|
73
|
+
url.searchParams.set('scope', OUTREACH_GOOGLE_SCOPES.join(' '));
|
|
74
|
+
url.searchParams.set('access_type', 'offline');
|
|
75
|
+
url.searchParams.set('prompt', 'consent');
|
|
76
|
+
url.searchParams.set('include_granted_scopes', 'false');
|
|
77
|
+
url.searchParams.set('state', input.state);
|
|
78
|
+
url.searchParams.set('nonce', input.nonce);
|
|
79
|
+
url.searchParams.set('code_challenge', input.codeChallenge);
|
|
80
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
81
|
+
if (input.loginHint) url.searchParams.set('login_hint', input.loginHint);
|
|
82
|
+
return url.toString();
|
|
83
|
+
}
|
|
84
|
+
async function postForm(url, form, deps) {
|
|
85
|
+
const response = await fetchWithBackoff(url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
89
|
+
},
|
|
90
|
+
body: new URLSearchParams(form).toString()
|
|
91
|
+
}, deps);
|
|
92
|
+
const body = await response.json().catch(()=>null);
|
|
93
|
+
return {
|
|
94
|
+
status: response.status,
|
|
95
|
+
body
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function readTokenResponse(body) {
|
|
99
|
+
const record = body != null ? body : {};
|
|
100
|
+
const accessToken = typeof record['access_token'] === 'string' ? record['access_token'] : '';
|
|
101
|
+
if (!accessToken) {
|
|
102
|
+
throw new GmailTransportError('unexpected', 'Google’s token endpoint answered without an access token.');
|
|
103
|
+
}
|
|
104
|
+
const expires = Number(record['expires_in']);
|
|
105
|
+
return {
|
|
106
|
+
accessToken,
|
|
107
|
+
expiresInSeconds: Number.isFinite(expires) && expires > 0 ? expires : 3600,
|
|
108
|
+
refreshToken: typeof record['refresh_token'] === 'string' && record['refresh_token'] ? record['refresh_token'] : null,
|
|
109
|
+
scope: typeof record['scope'] === 'string' ? record['scope'] : '',
|
|
110
|
+
idToken: typeof record['id_token'] === 'string' && record['id_token'] ? record['id_token'] : null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Exchanges an authorization code, with its PKCE verifier.
|
|
115
|
+
*
|
|
116
|
+
* ONE attempt, whatever the caller's policy: a code is single-use, so a retry
|
|
117
|
+
* after an answer that was lost in transit is refused as `invalid_grant` and
|
|
118
|
+
* reads as a dead grant. A failed connect is one click to start again.
|
|
119
|
+
*/ export async function exchangeGoogleAuthorizationCode(input, deps = {}) {
|
|
120
|
+
const { status, body } = await postForm(GOOGLE_OAUTH_ENDPOINTS.token, {
|
|
121
|
+
grant_type: 'authorization_code',
|
|
122
|
+
code: input.code,
|
|
123
|
+
client_id: input.clientId,
|
|
124
|
+
client_secret: input.clientSecret,
|
|
125
|
+
redirect_uri: input.redirectUri,
|
|
126
|
+
code_verifier: input.codeVerifier
|
|
127
|
+
}, _extends({}, deps, {
|
|
128
|
+
maxAttempts: 1
|
|
129
|
+
}));
|
|
130
|
+
if (status !== 200) throw tokenEndpointError(status, body);
|
|
131
|
+
return readTokenResponse(body);
|
|
132
|
+
}
|
|
133
|
+
/** Mints an access token from a refresh token. */ export async function refreshGoogleAccessToken(input, deps = {}) {
|
|
134
|
+
const { status, body } = await postForm(GOOGLE_OAUTH_ENDPOINTS.token, {
|
|
135
|
+
grant_type: 'refresh_token',
|
|
136
|
+
refresh_token: input.refreshToken,
|
|
137
|
+
client_id: input.clientId,
|
|
138
|
+
client_secret: input.clientSecret
|
|
139
|
+
}, deps);
|
|
140
|
+
if (status !== 200) throw tokenEndpointError(status, body);
|
|
141
|
+
return readTokenResponse(body);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Revokes a token at Google.
|
|
145
|
+
*
|
|
146
|
+
* Revoking a refresh token ends the whole grant — every token Google issued
|
|
147
|
+
* this client for the account — which is what a disconnect means, and why a
|
|
148
|
+
* RECONNECT must never revoke the token it replaces: the new one belongs to
|
|
149
|
+
* the same grant and would die with it.
|
|
150
|
+
*
|
|
151
|
+
* `already-invalid` when Google says the token was already dead, which is
|
|
152
|
+
* the outcome a disconnect wanted all along.
|
|
153
|
+
*/ export async function revokeGoogleToken(token, deps = {}) {
|
|
154
|
+
const { status, body } = await postForm(GOOGLE_OAUTH_ENDPOINTS.revoke, {
|
|
155
|
+
token
|
|
156
|
+
}, deps);
|
|
157
|
+
if (status === 200) return 'revoked';
|
|
158
|
+
const reason = body == null ? void 0 : body.error;
|
|
159
|
+
if (status === 400 && (reason === 'invalid_token' || reason === 'invalid_grant')) {
|
|
160
|
+
return 'already-invalid';
|
|
161
|
+
}
|
|
162
|
+
throw tokenEndpointError(status, body);
|
|
163
|
+
}
|
|
164
|
+
/** Whether a granted scope string carries every scope in `required`. */ export function grantedScopesInclude(scope, required) {
|
|
165
|
+
const granted = new Set(String(scope != null ? scope : '').split(/\s+/).filter(Boolean));
|
|
166
|
+
return required.every((entry)=>granted.has(entry));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The identity an ID token asserts, after the checks OpenID Connect requires
|
|
170
|
+
* of a token that arrived from the token endpoint itself.
|
|
171
|
+
*
|
|
172
|
+
* The signature is not re-verified here, on OIDC Core §3.1.3.7's allowance:
|
|
173
|
+
* the token was read off Google's own TLS response to our server's code
|
|
174
|
+
* exchange, never off a browser, so the channel authenticates it. What still
|
|
175
|
+
* has to be checked is that it is ABOUT this exchange: Google issued it
|
|
176
|
+
* (`iss`), to this client (`aud`, and `azp` when there are several
|
|
177
|
+
* audiences), it has not expired, it carries the nonce this connect minted,
|
|
178
|
+
* and the address is one Google verified.
|
|
179
|
+
*/ export function readGoogleIdToken(idToken, expected) {
|
|
180
|
+
const parts = typeof idToken === 'string' ? idToken.split('.') : [];
|
|
181
|
+
if (parts.length !== 3) return {
|
|
182
|
+
ok: false,
|
|
183
|
+
refusal: 'malformed'
|
|
184
|
+
};
|
|
185
|
+
let claims;
|
|
186
|
+
try {
|
|
187
|
+
claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
188
|
+
} catch (unused) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
refusal: 'malformed'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (!claims || typeof claims !== 'object') return {
|
|
195
|
+
ok: false,
|
|
196
|
+
refusal: 'malformed'
|
|
197
|
+
};
|
|
198
|
+
if (!GOOGLE_ISSUERS.has(String(claims['iss']))) return {
|
|
199
|
+
ok: false,
|
|
200
|
+
refusal: 'issuer'
|
|
201
|
+
};
|
|
202
|
+
const audience = claims['aud'];
|
|
203
|
+
const audiences = Array.isArray(audience) ? audience.map(String) : [
|
|
204
|
+
String(audience)
|
|
205
|
+
];
|
|
206
|
+
if (!audiences.includes(expected.clientId)) return {
|
|
207
|
+
ok: false,
|
|
208
|
+
refusal: 'audience'
|
|
209
|
+
};
|
|
210
|
+
if (audiences.length > 1 && claims['azp'] !== expected.clientId) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
refusal: 'audience'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const exp = Number(claims['exp']);
|
|
217
|
+
if (!Number.isFinite(exp) || exp * 1000 <= expected.nowMs) return {
|
|
218
|
+
ok: false,
|
|
219
|
+
refusal: 'expired'
|
|
220
|
+
};
|
|
221
|
+
if (typeof claims['nonce'] !== 'string' || claims['nonce'] !== expected.nonce) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
refusal: 'nonce'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const verified = claims['email_verified'];
|
|
228
|
+
if (verified !== true && verified !== 'true') return {
|
|
229
|
+
ok: false,
|
|
230
|
+
refusal: 'email-unverified'
|
|
231
|
+
};
|
|
232
|
+
const sub = typeof claims['sub'] === 'string' ? claims['sub'] : '';
|
|
233
|
+
const email = typeof claims['email'] === 'string' ? claims['email'].trim().toLowerCase() : '';
|
|
234
|
+
if (!sub || !email) return {
|
|
235
|
+
ok: false,
|
|
236
|
+
refusal: 'malformed'
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
identity: {
|
|
241
|
+
sub,
|
|
242
|
+
email
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//# sourceMappingURL=google-oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/google-oauth.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { GmailTransportError, tokenEndpointError } from './gmail-errors'\nimport { fetchWithBackoff, type TransportDeps } from './http'\n\n/**\n * GOOGLE'S OAUTH ENDPOINTS, AS OUTREACH USES THEM (AGL-2978).\n *\n * Fetch-based, with no `googleapis` dependency: four endpoints and one ID\n * token do not justify a client library's size, and a dependency that talks\n * to Google is one more thing the egress register has to account for.\n *\n * Nothing here reads configuration. The client id and secret, the redirect\n * address and the tokens are all handed in, so this module is as testable\n * with a fake `fetch` as it is safe to import.\n */\n\nexport const GOOGLE_OAUTH_ENDPOINTS = {\n authorize: 'https://accounts.google.com/o/oauth2/v2/auth',\n token: 'https://oauth2.googleapis.com/token',\n revoke: 'https://oauth2.googleapis.com/revoke',\n} as const\n\nexport const GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send'\nexport const GMAIL_READONLY_SCOPE = 'https://www.googleapis.com/auth/gmail.readonly'\n\n/**\n * What a mailbox grant asks for.\n *\n * `openid` and `email` identify the account. `gmail.send` sends as the rep.\n * `gmail.readonly` reads the replies and bounces a sequence stops on —\n * `gmail.metadata` would read headers but refuses the `q` search the bounce\n * detection needs.\n */\nexport const OUTREACH_GOOGLE_SCOPES = [\n 'openid',\n 'email',\n GMAIL_SEND_SCOPE,\n GMAIL_READONLY_SCOPE,\n] as const\n\n/** The two scopes a grant is useless without; `openid`/`email` come back renamed. */\nexport const OUTREACH_REQUIRED_GRANTED_SCOPES = [GMAIL_SEND_SCOPE, GMAIL_READONLY_SCOPE] as const\n\n/** The issuers Google signs ID tokens as. */\nconst GOOGLE_ISSUERS = new Set(['https://accounts.google.com', 'accounts.google.com'])\n\n/** The PKCE S256 challenge for a verifier. */\nexport function pkceChallenge(verifier: string): string {\n return createHash('sha256').update(verifier, 'ascii').digest('base64url')\n}\n\n/**\n * The consent address a rep's browser is sent to.\n *\n * `access_type=offline` and `prompt=consent` together are what make Google\n * issue a refresh token on EVERY connect, including a reconnect — without\n * `prompt=consent` a second grant for the same account comes back with an\n * access token only, and the mailbox could not send an hour later.\n */\nexport function buildGoogleAuthorizationUrl(input: {\n clientId: string\n redirectUri: string\n state: string\n codeChallenge: string\n nonce: string\n loginHint?: string | null\n}): string {\n const url = new URL(GOOGLE_OAUTH_ENDPOINTS.authorize)\n url.searchParams.set('client_id', input.clientId)\n url.searchParams.set('redirect_uri', input.redirectUri)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('scope', OUTREACH_GOOGLE_SCOPES.join(' '))\n url.searchParams.set('access_type', 'offline')\n url.searchParams.set('prompt', 'consent')\n url.searchParams.set('include_granted_scopes', 'false')\n url.searchParams.set('state', input.state)\n url.searchParams.set('nonce', input.nonce)\n url.searchParams.set('code_challenge', input.codeChallenge)\n url.searchParams.set('code_challenge_method', 'S256')\n if (input.loginHint) url.searchParams.set('login_hint', input.loginHint)\n return url.toString()\n}\n\n/** What the token endpoint answers for a code or a refresh. */\nexport interface GoogleTokenResponse {\n accessToken: string\n expiresInSeconds: number\n /** Only on a code exchange, and only when Google chose to issue one. */\n refreshToken: string | null\n /** Space-separated, as granted — which may be less than requested. */\n scope: string\n idToken: string | null\n}\n\nasync function postForm(\n url: string,\n form: Record<string, string>,\n deps: TransportDeps,\n): Promise<{ status: number; body: unknown }> {\n const response = await fetchWithBackoff(\n url,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(form).toString(),\n },\n deps,\n )\n const body = await response.json().catch(() => null)\n return { status: response.status, body }\n}\n\nfunction readTokenResponse(body: unknown): GoogleTokenResponse {\n const record = (body ?? {}) as Record<string, unknown>\n const accessToken = typeof record['access_token'] === 'string' ? record['access_token'] : ''\n if (!accessToken) {\n throw new GmailTransportError('unexpected', 'Google’s token endpoint answered without an access token.')\n }\n const expires = Number(record['expires_in'])\n return {\n accessToken,\n expiresInSeconds: Number.isFinite(expires) && expires > 0 ? expires : 3600,\n refreshToken: typeof record['refresh_token'] === 'string' && record['refresh_token'] ? record['refresh_token'] : null,\n scope: typeof record['scope'] === 'string' ? record['scope'] : '',\n idToken: typeof record['id_token'] === 'string' && record['id_token'] ? record['id_token'] : null,\n }\n}\n\n/**\n * Exchanges an authorization code, with its PKCE verifier.\n *\n * ONE attempt, whatever the caller's policy: a code is single-use, so a retry\n * after an answer that was lost in transit is refused as `invalid_grant` and\n * reads as a dead grant. A failed connect is one click to start again.\n */\nexport async function exchangeGoogleAuthorizationCode(\n input: {\n clientId: string\n clientSecret: string\n code: string\n redirectUri: string\n codeVerifier: string\n },\n deps: TransportDeps = {},\n): Promise<GoogleTokenResponse> {\n const { status, body } = await postForm(\n GOOGLE_OAUTH_ENDPOINTS.token,\n {\n grant_type: 'authorization_code',\n code: input.code,\n client_id: input.clientId,\n client_secret: input.clientSecret,\n redirect_uri: input.redirectUri,\n code_verifier: input.codeVerifier,\n },\n { ...deps, maxAttempts: 1 },\n )\n if (status !== 200) throw tokenEndpointError(status, body)\n return readTokenResponse(body)\n}\n\n/** Mints an access token from a refresh token. */\nexport async function refreshGoogleAccessToken(\n input: { clientId: string; clientSecret: string; refreshToken: string },\n deps: TransportDeps = {},\n): Promise<GoogleTokenResponse> {\n const { status, body } = await postForm(\n GOOGLE_OAUTH_ENDPOINTS.token,\n {\n grant_type: 'refresh_token',\n refresh_token: input.refreshToken,\n client_id: input.clientId,\n client_secret: input.clientSecret,\n },\n deps,\n )\n if (status !== 200) throw tokenEndpointError(status, body)\n return readTokenResponse(body)\n}\n\n/**\n * Revokes a token at Google.\n *\n * Revoking a refresh token ends the whole grant — every token Google issued\n * this client for the account — which is what a disconnect means, and why a\n * RECONNECT must never revoke the token it replaces: the new one belongs to\n * the same grant and would die with it.\n *\n * `already-invalid` when Google says the token was already dead, which is\n * the outcome a disconnect wanted all along.\n */\nexport async function revokeGoogleToken(\n token: string,\n deps: TransportDeps = {},\n): Promise<'revoked' | 'already-invalid'> {\n const { status, body } = await postForm(GOOGLE_OAUTH_ENDPOINTS.revoke, { token }, deps)\n if (status === 200) return 'revoked'\n const reason = (body as { error?: unknown } | null)?.error\n if (status === 400 && (reason === 'invalid_token' || reason === 'invalid_grant')) {\n return 'already-invalid'\n }\n throw tokenEndpointError(status, body)\n}\n\n/** Whether a granted scope string carries every scope in `required`. */\nexport function grantedScopesInclude(scope: string, required: readonly string[]): boolean {\n const granted = new Set(String(scope ?? '').split(/\\s+/).filter(Boolean))\n return required.every((entry) => granted.has(entry))\n}\n\n/** The ID token claims Outreach relies on. */\nexport interface GoogleIdentity {\n /** Google's stable account id. */\n sub: string\n email: string\n}\n\nexport type GoogleIdTokenRefusal =\n | 'malformed'\n | 'issuer'\n | 'audience'\n | 'expired'\n | 'nonce'\n | 'email-unverified'\n\n/**\n * The identity an ID token asserts, after the checks OpenID Connect requires\n * of a token that arrived from the token endpoint itself.\n *\n * The signature is not re-verified here, on OIDC Core §3.1.3.7's allowance:\n * the token was read off Google's own TLS response to our server's code\n * exchange, never off a browser, so the channel authenticates it. What still\n * has to be checked is that it is ABOUT this exchange: Google issued it\n * (`iss`), to this client (`aud`, and `azp` when there are several\n * audiences), it has not expired, it carries the nonce this connect minted,\n * and the address is one Google verified.\n */\nexport function readGoogleIdToken(\n idToken: string | null | undefined,\n expected: { clientId: string; nonce: string; nowMs: number },\n): { ok: true; identity: GoogleIdentity } | { ok: false; refusal: GoogleIdTokenRefusal } {\n const parts = typeof idToken === 'string' ? idToken.split('.') : []\n if (parts.length !== 3) return { ok: false, refusal: 'malformed' }\n let claims: Record<string, unknown>\n try {\n claims = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as Record<string, unknown>\n } catch {\n return { ok: false, refusal: 'malformed' }\n }\n if (!claims || typeof claims !== 'object') return { ok: false, refusal: 'malformed' }\n if (!GOOGLE_ISSUERS.has(String(claims['iss']))) return { ok: false, refusal: 'issuer' }\n const audience = claims['aud']\n const audiences = Array.isArray(audience) ? audience.map(String) : [String(audience)]\n if (!audiences.includes(expected.clientId)) return { ok: false, refusal: 'audience' }\n if (audiences.length > 1 && claims['azp'] !== expected.clientId) {\n return { ok: false, refusal: 'audience' }\n }\n const exp = Number(claims['exp'])\n if (!Number.isFinite(exp) || exp * 1000 <= expected.nowMs) return { ok: false, refusal: 'expired' }\n if (typeof claims['nonce'] !== 'string' || claims['nonce'] !== expected.nonce) {\n return { ok: false, refusal: 'nonce' }\n }\n const verified = claims['email_verified']\n if (verified !== true && verified !== 'true') return { ok: false, refusal: 'email-unverified' }\n const sub = typeof claims['sub'] === 'string' ? claims['sub'] : ''\n const email = typeof claims['email'] === 'string' ? claims['email'].trim().toLowerCase() : ''\n if (!sub || !email) return { ok: false, refusal: 'malformed' }\n return { ok: true, identity: { sub, email } }\n}\n"],"names":["createHash","GmailTransportError","tokenEndpointError","fetchWithBackoff","GOOGLE_OAUTH_ENDPOINTS","authorize","token","revoke","GMAIL_SEND_SCOPE","GMAIL_READONLY_SCOPE","OUTREACH_GOOGLE_SCOPES","OUTREACH_REQUIRED_GRANTED_SCOPES","GOOGLE_ISSUERS","Set","pkceChallenge","verifier","update","digest","buildGoogleAuthorizationUrl","input","url","URL","searchParams","set","clientId","redirectUri","join","state","nonce","codeChallenge","loginHint","toString","postForm","form","deps","response","method","headers","body","URLSearchParams","json","catch","status","readTokenResponse","record","accessToken","expires","Number","expiresInSeconds","isFinite","refreshToken","scope","idToken","exchangeGoogleAuthorizationCode","grant_type","code","client_id","client_secret","clientSecret","redirect_uri","code_verifier","codeVerifier","maxAttempts","refreshGoogleAccessToken","refresh_token","revokeGoogleToken","reason","error","grantedScopesInclude","required","granted","String","split","filter","Boolean","every","entry","has","readGoogleIdToken","expected","parts","length","ok","refusal","claims","JSON","parse","Buffer","from","audience","audiences","Array","isArray","map","includes","exp","nowMs","verified","sub","email","trim","toLowerCase","identity"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,mBAAmB,EAAEC,kBAAkB,QAAQ,oBAAgB;AACxE,SAASC,gBAAgB,QAA4B,YAAQ;AAE7D;;;;;;;;;;CAUC,GAED,OAAO,MAAMC,yBAAyB;IACpCC,WAAW;IACXC,OAAO;IACPC,QAAQ;AACV,EAAU;AAEV,OAAO,MAAMC,mBAAmB,6CAA4C;AAC5E,OAAO,MAAMC,uBAAuB,iDAAgD;AAEpF;;;;;;;CAOC,GACD,OAAO,MAAMC,yBAAyB;IACpC;IACA;IACAF;IACAC;CACD,CAAS;AAEV,mFAAmF,GACnF,OAAO,MAAME,mCAAmC;IAACH;IAAkBC;CAAqB,CAAS;AAEjG,2CAA2C,GAC3C,MAAMG,iBAAiB,IAAIC,IAAI;IAAC;IAA+B;CAAsB;AAErF,4CAA4C,GAC5C,OAAO,SAASC,cAAcC,QAAgB;IAC5C,OAAOf,WAAW,UAAUgB,MAAM,CAACD,UAAU,SAASE,MAAM,CAAC;AAC/D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,4BAA4BC,KAO3C;IACC,MAAMC,MAAM,IAAIC,IAAIjB,uBAAuBC,SAAS;IACpDe,IAAIE,YAAY,CAACC,GAAG,CAAC,aAAaJ,MAAMK,QAAQ;IAChDJ,IAAIE,YAAY,CAACC,GAAG,CAAC,gBAAgBJ,MAAMM,WAAW;IACtDL,IAAIE,YAAY,CAACC,GAAG,CAAC,iBAAiB;IACtCH,IAAIE,YAAY,CAACC,GAAG,CAAC,SAASb,uBAAuBgB,IAAI,CAAC;IAC1DN,IAAIE,YAAY,CAACC,GAAG,CAAC,eAAe;IACpCH,IAAIE,YAAY,CAACC,GAAG,CAAC,UAAU;IAC/BH,IAAIE,YAAY,CAACC,GAAG,CAAC,0BAA0B;IAC/CH,IAAIE,YAAY,CAACC,GAAG,CAAC,SAASJ,MAAMQ,KAAK;IACzCP,IAAIE,YAAY,CAACC,GAAG,CAAC,SAASJ,MAAMS,KAAK;IACzCR,IAAIE,YAAY,CAACC,GAAG,CAAC,kBAAkBJ,MAAMU,aAAa;IAC1DT,IAAIE,YAAY,CAACC,GAAG,CAAC,yBAAyB;IAC9C,IAAIJ,MAAMW,SAAS,EAAEV,IAAIE,YAAY,CAACC,GAAG,CAAC,cAAcJ,MAAMW,SAAS;IACvE,OAAOV,IAAIW,QAAQ;AACrB;AAaA,eAAeC,SACbZ,GAAW,EACXa,IAA4B,EAC5BC,IAAmB;IAEnB,MAAMC,WAAW,MAAMhC,iBACrBiB,KACA;QACEgB,QAAQ;QACRC,SAAS;YAAE,gBAAgB;QAAoC;QAC/DC,MAAM,IAAIC,gBAAgBN,MAAMF,QAAQ;IAC1C,GACAG;IAEF,MAAMI,OAAO,MAAMH,SAASK,IAAI,GAAGC,KAAK,CAAC,IAAM;IAC/C,OAAO;QAAEC,QAAQP,SAASO,MAAM;QAAEJ;IAAK;AACzC;AAEA,SAASK,kBAAkBL,IAAa;IACtC,MAAMM,SAAUN,eAAAA,OAAQ,CAAC;IACzB,MAAMO,cAAc,OAAOD,MAAM,CAAC,eAAe,KAAK,WAAWA,MAAM,CAAC,eAAe,GAAG;IAC1F,IAAI,CAACC,aAAa;QAChB,MAAM,IAAI5C,oBAAoB,cAAc;IAC9C;IACA,MAAM6C,UAAUC,OAAOH,MAAM,CAAC,aAAa;IAC3C,OAAO;QACLC;QACAG,kBAAkBD,OAAOE,QAAQ,CAACH,YAAYA,UAAU,IAAIA,UAAU;QACtEI,cAAc,OAAON,MAAM,CAAC,gBAAgB,KAAK,YAAYA,MAAM,CAAC,gBAAgB,GAAGA,MAAM,CAAC,gBAAgB,GAAG;QACjHO,OAAO,OAAOP,MAAM,CAAC,QAAQ,KAAK,WAAWA,MAAM,CAAC,QAAQ,GAAG;QAC/DQ,SAAS,OAAOR,MAAM,CAAC,WAAW,KAAK,YAAYA,MAAM,CAAC,WAAW,GAAGA,MAAM,CAAC,WAAW,GAAG;IAC/F;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAeS,gCACpBlC,KAMC,EACDe,OAAsB,CAAC,CAAC;IAExB,MAAM,EAAEQ,MAAM,EAAEJ,IAAI,EAAE,GAAG,MAAMN,SAC7B5B,uBAAuBE,KAAK,EAC5B;QACEgD,YAAY;QACZC,MAAMpC,MAAMoC,IAAI;QAChBC,WAAWrC,MAAMK,QAAQ;QACzBiC,eAAetC,MAAMuC,YAAY;QACjCC,cAAcxC,MAAMM,WAAW;QAC/BmC,eAAezC,MAAM0C,YAAY;IACnC,GACA,aAAK3B;QAAM4B,aAAa;;IAE1B,IAAIpB,WAAW,KAAK,MAAMxC,mBAAmBwC,QAAQJ;IACrD,OAAOK,kBAAkBL;AAC3B;AAEA,gDAAgD,GAChD,OAAO,eAAeyB,yBACpB5C,KAAuE,EACvEe,OAAsB,CAAC,CAAC;IAExB,MAAM,EAAEQ,MAAM,EAAEJ,IAAI,EAAE,GAAG,MAAMN,SAC7B5B,uBAAuBE,KAAK,EAC5B;QACEgD,YAAY;QACZU,eAAe7C,MAAM+B,YAAY;QACjCM,WAAWrC,MAAMK,QAAQ;QACzBiC,eAAetC,MAAMuC,YAAY;IACnC,GACAxB;IAEF,IAAIQ,WAAW,KAAK,MAAMxC,mBAAmBwC,QAAQJ;IACrD,OAAOK,kBAAkBL;AAC3B;AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAe2B,kBACpB3D,KAAa,EACb4B,OAAsB,CAAC,CAAC;IAExB,MAAM,EAAEQ,MAAM,EAAEJ,IAAI,EAAE,GAAG,MAAMN,SAAS5B,uBAAuBG,MAAM,EAAE;QAAED;IAAM,GAAG4B;IAClF,IAAIQ,WAAW,KAAK,OAAO;IAC3B,MAAMwB,SAAU5B,wBAAD,AAACA,KAAqC6B,KAAK;IAC1D,IAAIzB,WAAW,OAAQwB,CAAAA,WAAW,mBAAmBA,WAAW,eAAc,GAAI;QAChF,OAAO;IACT;IACA,MAAMhE,mBAAmBwC,QAAQJ;AACnC;AAEA,sEAAsE,GACtE,OAAO,SAAS8B,qBAAqBjB,KAAa,EAAEkB,QAA2B;IAC7E,MAAMC,UAAU,IAAIzD,IAAI0D,OAAOpB,gBAAAA,QAAS,IAAIqB,KAAK,CAAC,OAAOC,MAAM,CAACC;IAChE,OAAOL,SAASM,KAAK,CAAC,CAACC,QAAUN,QAAQO,GAAG,CAACD;AAC/C;AAiBA;;;;;;;;;;;CAWC,GACD,OAAO,SAASE,kBACd1B,OAAkC,EAClC2B,QAA4D;IAE5D,MAAMC,QAAQ,OAAO5B,YAAY,WAAWA,QAAQoB,KAAK,CAAC,OAAO,EAAE;IACnE,IAAIQ,MAAMC,MAAM,KAAK,GAAG,OAAO;QAAEC,IAAI;QAAOC,SAAS;IAAY;IACjE,IAAIC;IACJ,IAAI;QACFA,SAASC,KAAKC,KAAK,CAACC,OAAOC,IAAI,CAACR,KAAK,CAAC,EAAE,EAAE,aAAajD,QAAQ,CAAC;IAClE,EAAE,eAAM;QACN,OAAO;YAAEmD,IAAI;YAAOC,SAAS;QAAY;IAC3C;IACA,IAAI,CAACC,UAAU,OAAOA,WAAW,UAAU,OAAO;QAAEF,IAAI;QAAOC,SAAS;IAAY;IACpF,IAAI,CAACvE,eAAeiE,GAAG,CAACN,OAAOa,MAAM,CAAC,MAAM,IAAI,OAAO;QAAEF,IAAI;QAAOC,SAAS;IAAS;IACtF,MAAMM,WAAWL,MAAM,CAAC,MAAM;IAC9B,MAAMM,YAAYC,MAAMC,OAAO,CAACH,YAAYA,SAASI,GAAG,CAACtB,UAAU;QAACA,OAAOkB;KAAU;IACrF,IAAI,CAACC,UAAUI,QAAQ,CAACf,SAASvD,QAAQ,GAAG,OAAO;QAAE0D,IAAI;QAAOC,SAAS;IAAW;IACpF,IAAIO,UAAUT,MAAM,GAAG,KAAKG,MAAM,CAAC,MAAM,KAAKL,SAASvD,QAAQ,EAAE;QAC/D,OAAO;YAAE0D,IAAI;YAAOC,SAAS;QAAW;IAC1C;IACA,MAAMY,MAAMhD,OAAOqC,MAAM,CAAC,MAAM;IAChC,IAAI,CAACrC,OAAOE,QAAQ,CAAC8C,QAAQA,MAAM,QAAQhB,SAASiB,KAAK,EAAE,OAAO;QAAEd,IAAI;QAAOC,SAAS;IAAU;IAClG,IAAI,OAAOC,MAAM,CAAC,QAAQ,KAAK,YAAYA,MAAM,CAAC,QAAQ,KAAKL,SAASnD,KAAK,EAAE;QAC7E,OAAO;YAAEsD,IAAI;YAAOC,SAAS;QAAQ;IACvC;IACA,MAAMc,WAAWb,MAAM,CAAC,iBAAiB;IACzC,IAAIa,aAAa,QAAQA,aAAa,QAAQ,OAAO;QAAEf,IAAI;QAAOC,SAAS;IAAmB;IAC9F,MAAMe,MAAM,OAAOd,MAAM,CAAC,MAAM,KAAK,WAAWA,MAAM,CAAC,MAAM,GAAG;IAChE,MAAMe,QAAQ,OAAOf,MAAM,CAAC,QAAQ,KAAK,WAAWA,MAAM,CAAC,QAAQ,CAACgB,IAAI,GAAGC,WAAW,KAAK;IAC3F,IAAI,CAACH,OAAO,CAACC,OAAO,OAAO;QAAEjB,IAAI;QAAOC,SAAS;IAAY;IAC7D,OAAO;QAAED,IAAI;QAAMoB,UAAU;YAAEJ;YAAKC;QAAM;IAAE;AAC9C"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* ONE HTTP ATTEMPT POLICY FOR EVERY GOOGLE CALL (AGL-2978).
|
|
19
|
+
*
|
|
20
|
+
* A send runs inside a cron invocation with a deadline, so the policy is
|
|
21
|
+
* bounded on every axis: a timeout per attempt, a small number of attempts,
|
|
22
|
+
* and a wait between them that is short and jittered. What it retries is
|
|
23
|
+
* exactly what might succeed unchanged — a 429, a 5xx, a rate-limit 403, a
|
|
24
|
+
* dropped connection — and everything else is handed straight back.
|
|
25
|
+
*
|
|
26
|
+
* When Google asks for longer than this policy will wait, the response is
|
|
27
|
+
* returned without waiting, and the caller reports the delay as
|
|
28
|
+
* `retryAfterMs` for the NEXT invocation to honor.
|
|
29
|
+
*/
|
|
30
|
+
export interface TransportDeps {
|
|
31
|
+
/** The network. The real one is the global `fetch`. */
|
|
32
|
+
fetch?: typeof fetch;
|
|
33
|
+
/** Waits between attempts. Tests pass one that returns at once. */
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
/** Jitter source, `[0, 1)`. */
|
|
36
|
+
random?: () => number;
|
|
37
|
+
/** Clock, for token expiry. */
|
|
38
|
+
now?: () => number;
|
|
39
|
+
/** Attempts per request, first included. Default 3. */
|
|
40
|
+
maxAttempts?: number;
|
|
41
|
+
/** Milliseconds one attempt may take before it is abandoned. Default 20s. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
/** `Retry-After` in milliseconds — seconds or an HTTP date — or `null`. */
|
|
45
|
+
export declare function retryAfterMs(response: Response, nowMs?: number): number | null;
|
|
46
|
+
/** Exponential backoff with jitter: half the step, plus up to half again. */
|
|
47
|
+
export declare function backoffDelayMs(attempt: number, random?: () => number): number;
|
|
48
|
+
/**
|
|
49
|
+
* `fetch`, with the attempt policy above. Resolves with the last response
|
|
50
|
+
* Google gave; rejects with a `network` {@link GmailTransportError} only when
|
|
51
|
+
* no attempt got an answer at all.
|
|
52
|
+
*/
|
|
53
|
+
export declare function fetchWithBackoff(url: string, init: RequestInit, deps?: TransportDeps): Promise<Response>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { GmailTransportError, isRateLimitBody } from "./gmail-errors.js";
|
|
18
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
20
|
+
const BASE_DELAY_MS = 500;
|
|
21
|
+
const MAX_DELAY_MS = 8000;
|
|
22
|
+
/** The longest `Retry-After` this invocation will sit out. */ const MAX_HONORED_RETRY_AFTER_MS = 10000;
|
|
23
|
+
const defaultSleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
24
|
+
/** `Retry-After` in milliseconds — seconds or an HTTP date — or `null`. */ export function retryAfterMs(response, nowMs = Date.now()) {
|
|
25
|
+
const header = response.headers.get('retry-after');
|
|
26
|
+
if (!header) return null;
|
|
27
|
+
const seconds = Number(header);
|
|
28
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000);
|
|
29
|
+
const at = Date.parse(header);
|
|
30
|
+
return Number.isFinite(at) ? Math.max(0, at - nowMs) : null;
|
|
31
|
+
}
|
|
32
|
+
/** Exponential backoff with jitter: half the step, plus up to half again. */ export function backoffDelayMs(attempt, random = Math.random) {
|
|
33
|
+
const step = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1));
|
|
34
|
+
return Math.round(step / 2 + step / 2 * random());
|
|
35
|
+
}
|
|
36
|
+
async function shouldRetry(response) {
|
|
37
|
+
if (response.status === 429 || response.status >= 500) return true;
|
|
38
|
+
if (response.status !== 403) return false;
|
|
39
|
+
const body = await response.clone().json().catch(()=>null);
|
|
40
|
+
return isRateLimitBody(body);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `fetch`, with the attempt policy above. Resolves with the last response
|
|
44
|
+
* Google gave; rejects with a `network` {@link GmailTransportError} only when
|
|
45
|
+
* no attempt got an answer at all.
|
|
46
|
+
*/ export async function fetchWithBackoff(url, init, deps = {}) {
|
|
47
|
+
var _deps_fetch, _deps_sleep, _deps_maxAttempts, _deps_timeoutMs, _deps_now;
|
|
48
|
+
const doFetch = (_deps_fetch = deps.fetch) != null ? _deps_fetch : fetch;
|
|
49
|
+
const sleep = (_deps_sleep = deps.sleep) != null ? _deps_sleep : defaultSleep;
|
|
50
|
+
const attempts = Math.max(1, Math.floor((_deps_maxAttempts = deps.maxAttempts) != null ? _deps_maxAttempts : DEFAULT_ATTEMPTS));
|
|
51
|
+
const timeoutMs = (_deps_timeoutMs = deps.timeoutMs) != null ? _deps_timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
52
|
+
const now = (_deps_now = deps.now) != null ? _deps_now : Date.now;
|
|
53
|
+
for(let attempt = 1;; attempt += 1){
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await doFetch(url, _extends({}, init, {
|
|
59
|
+
signal: controller.signal
|
|
60
|
+
}));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
if (attempt >= attempts) {
|
|
64
|
+
throw new GmailTransportError('network', `Google could not be reached${controller.signal.aborted ? ' before the request timed out' : ''}.`, {
|
|
65
|
+
providerReason: error instanceof Error ? error.name : null
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
await sleep(backoffDelayMs(attempt, deps.random));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
if (attempt >= attempts || !await shouldRetry(response)) return response;
|
|
73
|
+
const asked = retryAfterMs(response, now());
|
|
74
|
+
if (asked !== null && asked > MAX_HONORED_RETRY_AFTER_MS) return response;
|
|
75
|
+
await sleep(asked != null ? asked : backoffDelayMs(attempt, deps.random));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/http.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { GmailTransportError, isRateLimitBody } from './gmail-errors'\n\n/**\n * ONE HTTP ATTEMPT POLICY FOR EVERY GOOGLE CALL (AGL-2978).\n *\n * A send runs inside a cron invocation with a deadline, so the policy is\n * bounded on every axis: a timeout per attempt, a small number of attempts,\n * and a wait between them that is short and jittered. What it retries is\n * exactly what might succeed unchanged — a 429, a 5xx, a rate-limit 403, a\n * dropped connection — and everything else is handed straight back.\n *\n * When Google asks for longer than this policy will wait, the response is\n * returned without waiting, and the caller reports the delay as\n * `retryAfterMs` for the NEXT invocation to honor.\n */\nexport interface TransportDeps {\n /** The network. The real one is the global `fetch`. */\n fetch?: typeof fetch\n /** Waits between attempts. Tests pass one that returns at once. */\n sleep?: (ms: number) => Promise<void>\n /** Jitter source, `[0, 1)`. */\n random?: () => number\n /** Clock, for token expiry. */\n now?: () => number\n /** Attempts per request, first included. Default 3. */\n maxAttempts?: number\n /** Milliseconds one attempt may take before it is abandoned. Default 20s. */\n timeoutMs?: number\n}\n\nconst DEFAULT_ATTEMPTS = 3\nconst DEFAULT_TIMEOUT_MS = 20_000\nconst BASE_DELAY_MS = 500\nconst MAX_DELAY_MS = 8_000\n/** The longest `Retry-After` this invocation will sit out. */\nconst MAX_HONORED_RETRY_AFTER_MS = 10_000\n\nconst defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/** `Retry-After` in milliseconds — seconds or an HTTP date — or `null`. */\nexport function retryAfterMs(response: Response, nowMs: number = Date.now()): number | null {\n const header = response.headers.get('retry-after')\n if (!header) return null\n const seconds = Number(header)\n if (Number.isFinite(seconds) && seconds >= 0) return Math.round(seconds * 1000)\n const at = Date.parse(header)\n return Number.isFinite(at) ? Math.max(0, at - nowMs) : null\n}\n\n/** Exponential backoff with jitter: half the step, plus up to half again. */\nexport function backoffDelayMs(attempt: number, random: () => number = Math.random): number {\n const step = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1))\n return Math.round(step / 2 + (step / 2) * random())\n}\n\nasync function shouldRetry(response: Response): Promise<boolean> {\n if (response.status === 429 || response.status >= 500) return true\n if (response.status !== 403) return false\n const body = await response.clone().json().catch(() => null)\n return isRateLimitBody(body)\n}\n\n/**\n * `fetch`, with the attempt policy above. Resolves with the last response\n * Google gave; rejects with a `network` {@link GmailTransportError} only when\n * no attempt got an answer at all.\n */\nexport async function fetchWithBackoff(\n url: string,\n init: RequestInit,\n deps: TransportDeps = {},\n): Promise<Response> {\n const doFetch = deps.fetch ?? fetch\n const sleep = deps.sleep ?? defaultSleep\n const attempts = Math.max(1, Math.floor(deps.maxAttempts ?? DEFAULT_ATTEMPTS))\n const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const now = deps.now ?? Date.now\n\n for (let attempt = 1; ; attempt += 1) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n let response: Response\n try {\n response = await doFetch(url, { ...init, signal: controller.signal })\n } catch (error) {\n clearTimeout(timer)\n if (attempt >= attempts) {\n throw new GmailTransportError(\n 'network',\n `Google could not be reached${controller.signal.aborted ? ' before the request timed out' : ''}.`,\n { providerReason: error instanceof Error ? error.name : null },\n )\n }\n await sleep(backoffDelayMs(attempt, deps.random))\n continue\n }\n clearTimeout(timer)\n if (attempt >= attempts || !(await shouldRetry(response))) return response\n const asked = retryAfterMs(response, now())\n if (asked !== null && asked > MAX_HONORED_RETRY_AFTER_MS) return response\n await sleep(asked ?? backoffDelayMs(attempt, deps.random))\n }\n}\n"],"names":["GmailTransportError","isRateLimitBody","DEFAULT_ATTEMPTS","DEFAULT_TIMEOUT_MS","BASE_DELAY_MS","MAX_DELAY_MS","MAX_HONORED_RETRY_AFTER_MS","defaultSleep","ms","Promise","resolve","setTimeout","retryAfterMs","response","nowMs","Date","now","header","headers","get","seconds","Number","isFinite","Math","round","at","parse","max","backoffDelayMs","attempt","random","step","min","shouldRetry","status","body","clone","json","catch","fetchWithBackoff","url","init","deps","doFetch","fetch","sleep","attempts","floor","maxAttempts","timeoutMs","controller","AbortController","timer","abort","signal","error","clearTimeout","aborted","providerReason","Error","name","asked"],"mappings":"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"}
|