@open-mercato/channel-gmail 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/AGENTS.md +47 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js +17 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js.map +7 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js +16 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js.map +7 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js +16 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js.map +7 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js +17 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js.map +7 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js +26 -0
- package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js.map +7 -0
- package/dist/modules/channel_gmail/acl.js +10 -0
- package/dist/modules/channel_gmail/acl.js.map +7 -0
- package/dist/modules/channel_gmail/di.js +23 -0
- package/dist/modules/channel_gmail/di.js.map +7 -0
- package/dist/modules/channel_gmail/index.js +9 -0
- package/dist/modules/channel_gmail/index.js.map +7 -0
- package/dist/modules/channel_gmail/integration.js +69 -0
- package/dist/modules/channel_gmail/integration.js.map +7 -0
- package/dist/modules/channel_gmail/lib/adapter.js +542 -0
- package/dist/modules/channel_gmail/lib/adapter.js.map +7 -0
- package/dist/modules/channel_gmail/lib/capabilities.js +10 -0
- package/dist/modules/channel_gmail/lib/capabilities.js.map +7 -0
- package/dist/modules/channel_gmail/lib/convert-outbound.js +84 -0
- package/dist/modules/channel_gmail/lib/convert-outbound.js.map +7 -0
- package/dist/modules/channel_gmail/lib/credentials.js +48 -0
- package/dist/modules/channel_gmail/lib/credentials.js.map +7 -0
- package/dist/modules/channel_gmail/lib/gmail-client.js +160 -0
- package/dist/modules/channel_gmail/lib/gmail-client.js.map +7 -0
- package/dist/modules/channel_gmail/lib/health.js +10 -0
- package/dist/modules/channel_gmail/lib/health.js.map +7 -0
- package/dist/modules/channel_gmail/lib/normalize-inbound.js +28 -0
- package/dist/modules/channel_gmail/lib/normalize-inbound.js.map +7 -0
- package/dist/modules/channel_gmail/lib/oauth.js +77 -0
- package/dist/modules/channel_gmail/lib/oauth.js.map +7 -0
- package/dist/modules/channel_gmail/setup.js +25 -0
- package/dist/modules/channel_gmail/setup.js.map +7 -0
- package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js +24 -0
- package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_gmail/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_gmail/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_gmail/widgets/injection-table.js +14 -0
- package/dist/modules/channel_gmail/widgets/injection-table.js.map +7 -0
- package/jest.config.cjs +34 -0
- package/package.json +95 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.ts +24 -0
- package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.ts +23 -0
- package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.ts +23 -0
- package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.ts +39 -0
- package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.ts +48 -0
- package/src/modules/channel_gmail/acl.ts +6 -0
- package/src/modules/channel_gmail/di.ts +21 -0
- package/src/modules/channel_gmail/index.ts +6 -0
- package/src/modules/channel_gmail/integration.ts +67 -0
- package/src/modules/channel_gmail/lib/__tests__/adapter.test.ts +838 -0
- package/src/modules/channel_gmail/lib/__tests__/convert-outbound.test.ts +128 -0
- package/src/modules/channel_gmail/lib/__tests__/credentials.test.ts +76 -0
- package/src/modules/channel_gmail/lib/__tests__/gmail-client.test.ts +209 -0
- package/src/modules/channel_gmail/lib/__tests__/normalize-inbound.test.ts +106 -0
- package/src/modules/channel_gmail/lib/__tests__/oauth.test.ts +148 -0
- package/src/modules/channel_gmail/lib/adapter.ts +734 -0
- package/src/modules/channel_gmail/lib/capabilities.ts +22 -0
- package/src/modules/channel_gmail/lib/convert-outbound.ts +136 -0
- package/src/modules/channel_gmail/lib/credentials.ts +90 -0
- package/src/modules/channel_gmail/lib/gmail-client.ts +305 -0
- package/src/modules/channel_gmail/lib/health.ts +14 -0
- package/src/modules/channel_gmail/lib/normalize-inbound.ts +57 -0
- package/src/modules/channel_gmail/lib/oauth.ts +128 -0
- package/src/modules/channel_gmail/setup.ts +36 -0
- package/src/modules/channel_gmail/widgets/injection/connect/widget.client.tsx +28 -0
- package/src/modules/channel_gmail/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_gmail/widgets/injection-table.ts +12 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin Gmail OAuth client wrapper. Uses raw `fetch` against Google's well-known
|
|
3
|
+
* endpoints so the adapter can stay agnostic of the `googleapis` SDK and tests
|
|
4
|
+
* can stub `setGoogleOAuthClient(...)` without loading the SDK at all.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints — locked to Google's documented OAuth2 v2 surface:
|
|
7
|
+
* - Authorize https://accounts.google.com/o/oauth2/v2/auth
|
|
8
|
+
* - Token https://oauth2.googleapis.com/token
|
|
9
|
+
* - Userinfo https://www.googleapis.com/oauth2/v3/userinfo
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
requestOAuthToken,
|
|
14
|
+
tokenResponseToExpiresAt,
|
|
15
|
+
type OAuthTokenResponse,
|
|
16
|
+
} from '@open-mercato/core/modules/communication_channels/lib/oauth-token'
|
|
17
|
+
import { parseScopes } from './credentials'
|
|
18
|
+
|
|
19
|
+
export { tokenResponseToExpiresAt }
|
|
20
|
+
|
|
21
|
+
export const GMAIL_OAUTH_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
22
|
+
export const GMAIL_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
|
23
|
+
export const GMAIL_OAUTH_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
|
|
24
|
+
|
|
25
|
+
export interface BuildAuthorizeUrlInput {
|
|
26
|
+
clientId: string
|
|
27
|
+
redirectUri: string
|
|
28
|
+
state: string
|
|
29
|
+
scopes: string[]
|
|
30
|
+
loginHint?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ExchangeCodeInput {
|
|
34
|
+
clientId: string
|
|
35
|
+
clientSecret: string
|
|
36
|
+
redirectUri: string
|
|
37
|
+
code: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RefreshTokenInput {
|
|
41
|
+
clientId: string
|
|
42
|
+
clientSecret: string
|
|
43
|
+
refreshToken: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TokenResponse = OAuthTokenResponse
|
|
47
|
+
|
|
48
|
+
export interface UserInfoResponse {
|
|
49
|
+
sub?: string
|
|
50
|
+
email?: string
|
|
51
|
+
email_verified?: boolean
|
|
52
|
+
name?: string
|
|
53
|
+
picture?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GoogleOAuthClient {
|
|
57
|
+
buildAuthorizeUrl(input: BuildAuthorizeUrlInput): string
|
|
58
|
+
exchangeCode(input: ExchangeCodeInput): Promise<TokenResponse>
|
|
59
|
+
refreshToken(input: RefreshTokenInput): Promise<TokenResponse>
|
|
60
|
+
fetchUserInfo(accessToken: string): Promise<UserInfoResponse>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class RealGoogleOAuthClient implements GoogleOAuthClient {
|
|
64
|
+
buildAuthorizeUrl(input: BuildAuthorizeUrlInput): string {
|
|
65
|
+
const url = new URL(GMAIL_OAUTH_AUTHORIZE_URL)
|
|
66
|
+
url.searchParams.set('client_id', input.clientId)
|
|
67
|
+
url.searchParams.set('redirect_uri', input.redirectUri)
|
|
68
|
+
url.searchParams.set('response_type', 'code')
|
|
69
|
+
url.searchParams.set('scope', (input.scopes.length ? input.scopes : parseScopes(undefined)).join(' '))
|
|
70
|
+
url.searchParams.set('state', input.state)
|
|
71
|
+
url.searchParams.set('access_type', 'offline')
|
|
72
|
+
url.searchParams.set('prompt', 'consent')
|
|
73
|
+
url.searchParams.set('include_granted_scopes', 'true')
|
|
74
|
+
if (input.loginHint) url.searchParams.set('login_hint', input.loginHint)
|
|
75
|
+
return url.toString()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async exchangeCode(input: ExchangeCodeInput): Promise<TokenResponse> {
|
|
79
|
+
const params = new URLSearchParams()
|
|
80
|
+
params.set('grant_type', 'authorization_code')
|
|
81
|
+
params.set('code', input.code)
|
|
82
|
+
params.set('redirect_uri', input.redirectUri)
|
|
83
|
+
params.set('client_id', input.clientId)
|
|
84
|
+
params.set('client_secret', input.clientSecret)
|
|
85
|
+
return requestOAuthToken(GMAIL_OAUTH_TOKEN_URL, params, {
|
|
86
|
+
errorLabel: 'Gmail OAuth code exchange failed',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async refreshToken(input: RefreshTokenInput): Promise<TokenResponse> {
|
|
91
|
+
const params = new URLSearchParams()
|
|
92
|
+
params.set('grant_type', 'refresh_token')
|
|
93
|
+
params.set('refresh_token', input.refreshToken)
|
|
94
|
+
params.set('client_id', input.clientId)
|
|
95
|
+
params.set('client_secret', input.clientSecret)
|
|
96
|
+
return requestOAuthToken(GMAIL_OAUTH_TOKEN_URL, params, {
|
|
97
|
+
errorLabel: 'Gmail OAuth refresh failed',
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async fetchUserInfo(accessToken: string): Promise<UserInfoResponse> {
|
|
102
|
+
const controller = new AbortController()
|
|
103
|
+
const timeout = setTimeout(() => controller.abort(), 10_000)
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(GMAIL_OAUTH_USERINFO_URL, {
|
|
106
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
})
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
throw new Error(`Gmail userinfo fetch failed: ${res.status} ${res.statusText}`)
|
|
111
|
+
}
|
|
112
|
+
return (await res.json()) as UserInfoResponse
|
|
113
|
+
} finally {
|
|
114
|
+
clearTimeout(timeout)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let cachedClient: GoogleOAuthClient | null = null
|
|
120
|
+
|
|
121
|
+
export function getGoogleOAuthClient(): GoogleOAuthClient {
|
|
122
|
+
if (!cachedClient) cachedClient = new RealGoogleOAuthClient()
|
|
123
|
+
return cachedClient
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function setGoogleOAuthClient(client: GoogleOAuthClient | null): void {
|
|
127
|
+
cachedClient = client
|
|
128
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
2
|
+
import {
|
|
3
|
+
hasChannelAdapter,
|
|
4
|
+
registerChannelAdapter,
|
|
5
|
+
} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'
|
|
6
|
+
import { getGmailChannelAdapter } from './lib/adapter'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register the Gmail `ChannelAdapter` once per process at import time. The
|
|
10
|
+
* registry is process-wide so the underlying `setRegister` call is idempotent;
|
|
11
|
+
* we guard with `hasChannelAdapter` to silence the registry's duplicate error
|
|
12
|
+
* on dev-mode HMR + repeated test imports.
|
|
13
|
+
*
|
|
14
|
+
* Tenant-level OAuth client config (Client ID + Client Secret) is persisted via
|
|
15
|
+
* the standard `IntegrationCredentials` flow for the `gmail` provider; this
|
|
16
|
+
* module does not preconfigure per-tenant credentials from env (Google Cloud
|
|
17
|
+
* Console projects are explicit per-tenant).
|
|
18
|
+
*/
|
|
19
|
+
function ensureGmailAdapterRegistered(): void {
|
|
20
|
+
if (hasChannelAdapter('gmail')) return
|
|
21
|
+
registerChannelAdapter(getGmailChannelAdapter())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ensureGmailAdapterRegistered()
|
|
25
|
+
|
|
26
|
+
export const setup: ModuleSetupConfig = {
|
|
27
|
+
defaultRoleFeatures: {
|
|
28
|
+
superadmin: ['channel_gmail.view', 'channel_gmail.configure'],
|
|
29
|
+
admin: ['channel_gmail.view', 'channel_gmail.configure'],
|
|
30
|
+
},
|
|
31
|
+
async onTenantCreated() {
|
|
32
|
+
ensureGmailAdapterRegistered()
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default setup
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { SocialButton } from '@open-mercato/ui/primitives/social-button'
|
|
7
|
+
import { useConnectChannel } from '@open-mercato/core/modules/communication_channels/lib/use-connect-channel'
|
|
8
|
+
|
|
9
|
+
export default function ConnectGmailWidget(
|
|
10
|
+
_props: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>,
|
|
11
|
+
) {
|
|
12
|
+
const t = useT()
|
|
13
|
+
const { connect, pending } = useConnectChannel({ providerKey: 'gmail' })
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SocialButton
|
|
17
|
+
type="button"
|
|
18
|
+
brand="google"
|
|
19
|
+
appearance="stroke"
|
|
20
|
+
onClick={() => void connect()}
|
|
21
|
+
disabled={pending}
|
|
22
|
+
>
|
|
23
|
+
{pending
|
|
24
|
+
? t('communication_channels.profile.connect.connecting', 'Connecting...')
|
|
25
|
+
: t('communication_channels.profile.connect.gmail', 'Connect Gmail')}
|
|
26
|
+
</SocialButton>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import ConnectGmailWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
|
|
5
|
+
metadata: {
|
|
6
|
+
id: 'channel_gmail.injection.connect',
|
|
7
|
+
title: 'Connect Gmail',
|
|
8
|
+
description: 'Starts the per-user Gmail OAuth connection flow.',
|
|
9
|
+
features: ['communication_channels.connect_user_channel'],
|
|
10
|
+
priority: 120,
|
|
11
|
+
enabled: true,
|
|
12
|
+
},
|
|
13
|
+
Widget: ConnectGmailWidget,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default widget
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
|
|
3
|
+
export const injectionTable: ModuleInjectionTable = {
|
|
4
|
+
'profile:communication-channels:connect': [
|
|
5
|
+
{
|
|
6
|
+
widgetId: 'channel_gmail.injection.connect',
|
|
7
|
+
priority: 120,
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default injectionTable
|
package/tsconfig.json
ADDED