@open-mercato/channel-gmail 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/AGENTS.md +47 -0
  3. package/build.mjs +7 -0
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js +17 -0
  7. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.js.map +7 -0
  8. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js +16 -0
  9. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.js.map +7 -0
  10. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js +16 -0
  11. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.js.map +7 -0
  12. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js +17 -0
  13. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.js.map +7 -0
  14. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js +26 -0
  15. package/dist/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.js.map +7 -0
  16. package/dist/modules/channel_gmail/acl.js +10 -0
  17. package/dist/modules/channel_gmail/acl.js.map +7 -0
  18. package/dist/modules/channel_gmail/di.js +23 -0
  19. package/dist/modules/channel_gmail/di.js.map +7 -0
  20. package/dist/modules/channel_gmail/index.js +9 -0
  21. package/dist/modules/channel_gmail/index.js.map +7 -0
  22. package/dist/modules/channel_gmail/integration.js +69 -0
  23. package/dist/modules/channel_gmail/integration.js.map +7 -0
  24. package/dist/modules/channel_gmail/lib/adapter.js +542 -0
  25. package/dist/modules/channel_gmail/lib/adapter.js.map +7 -0
  26. package/dist/modules/channel_gmail/lib/capabilities.js +10 -0
  27. package/dist/modules/channel_gmail/lib/capabilities.js.map +7 -0
  28. package/dist/modules/channel_gmail/lib/convert-outbound.js +84 -0
  29. package/dist/modules/channel_gmail/lib/convert-outbound.js.map +7 -0
  30. package/dist/modules/channel_gmail/lib/credentials.js +48 -0
  31. package/dist/modules/channel_gmail/lib/credentials.js.map +7 -0
  32. package/dist/modules/channel_gmail/lib/gmail-client.js +160 -0
  33. package/dist/modules/channel_gmail/lib/gmail-client.js.map +7 -0
  34. package/dist/modules/channel_gmail/lib/health.js +10 -0
  35. package/dist/modules/channel_gmail/lib/health.js.map +7 -0
  36. package/dist/modules/channel_gmail/lib/normalize-inbound.js +28 -0
  37. package/dist/modules/channel_gmail/lib/normalize-inbound.js.map +7 -0
  38. package/dist/modules/channel_gmail/lib/oauth.js +77 -0
  39. package/dist/modules/channel_gmail/lib/oauth.js.map +7 -0
  40. package/dist/modules/channel_gmail/setup.js +25 -0
  41. package/dist/modules/channel_gmail/setup.js.map +7 -0
  42. package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js +24 -0
  43. package/dist/modules/channel_gmail/widgets/injection/connect/widget.client.js.map +7 -0
  44. package/dist/modules/channel_gmail/widgets/injection/connect/widget.js +17 -0
  45. package/dist/modules/channel_gmail/widgets/injection/connect/widget.js.map +7 -0
  46. package/dist/modules/channel_gmail/widgets/injection-table.js +14 -0
  47. package/dist/modules/channel_gmail/widgets/injection-table.js.map +7 -0
  48. package/jest.config.cjs +34 -0
  49. package/package.json +95 -0
  50. package/src/index.ts +1 -0
  51. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-006.spec.ts +24 -0
  52. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-007.spec.ts +23 -0
  53. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-008.spec.ts +23 -0
  54. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-A01-token-refresh.spec.ts +39 -0
  55. package/src/modules/channel_gmail/__integration__/TC-CHANNEL-EMAIL-C01.spec.ts +48 -0
  56. package/src/modules/channel_gmail/acl.ts +6 -0
  57. package/src/modules/channel_gmail/di.ts +21 -0
  58. package/src/modules/channel_gmail/index.ts +6 -0
  59. package/src/modules/channel_gmail/integration.ts +67 -0
  60. package/src/modules/channel_gmail/lib/__tests__/adapter.test.ts +838 -0
  61. package/src/modules/channel_gmail/lib/__tests__/convert-outbound.test.ts +128 -0
  62. package/src/modules/channel_gmail/lib/__tests__/credentials.test.ts +76 -0
  63. package/src/modules/channel_gmail/lib/__tests__/gmail-client.test.ts +209 -0
  64. package/src/modules/channel_gmail/lib/__tests__/normalize-inbound.test.ts +106 -0
  65. package/src/modules/channel_gmail/lib/__tests__/oauth.test.ts +148 -0
  66. package/src/modules/channel_gmail/lib/adapter.ts +734 -0
  67. package/src/modules/channel_gmail/lib/capabilities.ts +22 -0
  68. package/src/modules/channel_gmail/lib/convert-outbound.ts +136 -0
  69. package/src/modules/channel_gmail/lib/credentials.ts +90 -0
  70. package/src/modules/channel_gmail/lib/gmail-client.ts +305 -0
  71. package/src/modules/channel_gmail/lib/health.ts +14 -0
  72. package/src/modules/channel_gmail/lib/normalize-inbound.ts +57 -0
  73. package/src/modules/channel_gmail/lib/oauth.ts +128 -0
  74. package/src/modules/channel_gmail/setup.ts +36 -0
  75. package/src/modules/channel_gmail/widgets/injection/connect/widget.client.tsx +28 -0
  76. package/src/modules/channel_gmail/widgets/injection/connect/widget.ts +16 -0
  77. package/src/modules/channel_gmail/widgets/injection-table.ts +12 -0
  78. package/tsconfig.json +9 -0
  79. package/watch.mjs +7 -0
@@ -0,0 +1,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
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "noEmit": true
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "**/__tests__/**"]
9
+ }
package/watch.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { watch } from '../../scripts/watch.mjs'
2
+ import { dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+
7
+ watch(__dirname)