@open-mercato/channel-resend 0.7.1-develop.7151.1.00d0391847
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/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_resend/acl.js +10 -0
- package/dist/modules/channel_resend/acl.js.map +7 -0
- package/dist/modules/channel_resend/capabilities.js +10 -0
- package/dist/modules/channel_resend/capabilities.js.map +7 -0
- package/dist/modules/channel_resend/di.js +22 -0
- package/dist/modules/channel_resend/di.js.map +7 -0
- package/dist/modules/channel_resend/index.js +13 -0
- package/dist/modules/channel_resend/index.js.map +7 -0
- package/dist/modules/channel_resend/integration.js +61 -0
- package/dist/modules/channel_resend/integration.js.map +7 -0
- package/dist/modules/channel_resend/lib/adapter.js +109 -0
- package/dist/modules/channel_resend/lib/adapter.js.map +7 -0
- package/dist/modules/channel_resend/lib/credentials.js +9 -0
- package/dist/modules/channel_resend/lib/credentials.js.map +7 -0
- package/dist/modules/channel_resend/lib/health.js +43 -0
- package/dist/modules/channel_resend/lib/health.js.map +7 -0
- package/dist/modules/channel_resend/lib/preset.js +61 -0
- package/dist/modules/channel_resend/lib/preset.js.map +7 -0
- package/dist/modules/channel_resend/lib/system-email-config.js +13 -0
- package/dist/modules/channel_resend/lib/system-email-config.js.map +7 -0
- package/dist/modules/channel_resend/setup.js +30 -0
- package/dist/modules/channel_resend/setup.js.map +7 -0
- package/jest.config.cjs +33 -0
- package/package.json +87 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_resend/__tests__/contracts.test.ts +27 -0
- package/src/modules/channel_resend/acl.ts +6 -0
- package/src/modules/channel_resend/capabilities.ts +8 -0
- package/src/modules/channel_resend/di.ts +20 -0
- package/src/modules/channel_resend/index.ts +9 -0
- package/src/modules/channel_resend/integration.ts +56 -0
- package/src/modules/channel_resend/lib/__tests__/adapter.test.ts +70 -0
- package/src/modules/channel_resend/lib/__tests__/health.test.ts +38 -0
- package/src/modules/channel_resend/lib/__tests__/preset.test.ts +179 -0
- package/src/modules/channel_resend/lib/adapter.ts +139 -0
- package/src/modules/channel_resend/lib/credentials.ts +8 -0
- package/src/modules/channel_resend/lib/health.ts +51 -0
- package/src/modules/channel_resend/lib/preset.ts +95 -0
- package/src/modules/channel_resend/lib/system-email-config.ts +10 -0
- package/src/modules/channel_resend/setup.ts +30 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
|
|
2
|
+
import { fetchWithTimeout } from '@open-mercato/shared/lib/http/fetchWithTimeout'
|
|
3
|
+
import { resendCredentialsSchema } from './credentials'
|
|
4
|
+
|
|
5
|
+
type HealthCheckResult = {
|
|
6
|
+
status: 'healthy' | 'unhealthy'
|
|
7
|
+
message: string
|
|
8
|
+
details: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const channelResendHealthCheck = {
|
|
12
|
+
async check(
|
|
13
|
+
credentials: Record<string, unknown> | null,
|
|
14
|
+
_scope: IntegrationScope,
|
|
15
|
+
): Promise<HealthCheckResult> {
|
|
16
|
+
const parsed = resendCredentialsSchema.safeParse(credentials ?? {})
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
return {
|
|
19
|
+
status: 'unhealthy',
|
|
20
|
+
message: `Resend credentials invalid: ${parsed.error.issues[0]?.message ?? 'unknown validation error'}`,
|
|
21
|
+
details: { reason: 'invalid_credentials' },
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchWithTimeout('https://api.resend.com/domains?limit=1', {
|
|
27
|
+
headers: { Authorization: `Bearer ${parsed.data.apiKey}` },
|
|
28
|
+
timeoutMs: 8_000,
|
|
29
|
+
})
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
return {
|
|
32
|
+
status: 'unhealthy',
|
|
33
|
+
message: `Resend API rejected the credentials with status ${response.status}`,
|
|
34
|
+
details: { reason: 'api_rejected', status: response.status },
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
status: 'healthy',
|
|
39
|
+
message: 'Resend API credentials are valid',
|
|
40
|
+
details: { endpoint: 'domains' },
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : 'Resend API request failed'
|
|
44
|
+
return {
|
|
45
|
+
status: 'unhealthy',
|
|
46
|
+
message: `Resend health check failed: ${message}`,
|
|
47
|
+
details: { reason: 'request_failed' },
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { normalizeEnvString, resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
import { ensureSystemEmailChannel } from '@open-mercato/core/modules/communication_channels/lib/ensure-system-email-channel'
|
|
6
|
+
import { isSelectedSystemEmailProvider } from '@open-mercato/core/modules/communication_channels/lib/system-email-provider-config'
|
|
7
|
+
import { resendCapabilities } from '../capabilities'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('channel_resend')
|
|
10
|
+
|
|
11
|
+
type PresetScope = {
|
|
12
|
+
em: EntityManager
|
|
13
|
+
container: AppContainer
|
|
14
|
+
tenantId: string
|
|
15
|
+
organizationId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type CredentialsServiceLike = {
|
|
19
|
+
save: (
|
|
20
|
+
integrationId: string,
|
|
21
|
+
credentials: Record<string, unknown>,
|
|
22
|
+
scope: { organizationId: string; tenantId: string; userId?: string | null },
|
|
23
|
+
) => Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type IntegrationStateServiceLike = {
|
|
27
|
+
upsert: (
|
|
28
|
+
integrationId: string,
|
|
29
|
+
input: { isEnabled: boolean },
|
|
30
|
+
scope: { organizationId: string; tenantId: string },
|
|
31
|
+
) => Promise<unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readResendEnvPreset(): { apiKey: string; fromAddress: string } | null {
|
|
35
|
+
const apiKey = normalizeEnvString(process.env.RESEND_API_KEY)
|
|
36
|
+
const fromAddress = resolveDefaultEmailFromAddress()
|
|
37
|
+
if (!apiKey || !fromAddress) {
|
|
38
|
+
// A key with no from-address is the trap worth naming: `.env.example` documents RESEND_API_KEY
|
|
39
|
+
// prominently but the from-address separately, so this combination looks configured and seeds
|
|
40
|
+
// nothing. Say so rather than returning null in silence.
|
|
41
|
+
if (apiKey && !fromAddress) {
|
|
42
|
+
logger.warn('RESEND_API_KEY is set but no from-address is configured; skipping Resend preset', {
|
|
43
|
+
remedy: 'set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL',
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return { apiKey, fromAddress }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function applyResendEnvPreset(ctx: PresetScope): Promise<void> {
|
|
52
|
+
// Only the provider this instance actually selected seeds anything, so a leftover RESEND_API_KEY
|
|
53
|
+
// on an instance that moved to `SYSTEM_EMAIL_PROVIDER=ses` no longer advertises an Enabled Resend
|
|
54
|
+
// integration and a connected channel that nothing sends through. `resend` is the default, so the
|
|
55
|
+
// documented `RESEND_API_KEY`-only setup is unaffected.
|
|
56
|
+
if (!isSelectedSystemEmailProvider('resend')) return
|
|
57
|
+
const preset = readResendEnvPreset()
|
|
58
|
+
if (!preset) return
|
|
59
|
+
|
|
60
|
+
let credentialsService: CredentialsServiceLike
|
|
61
|
+
try {
|
|
62
|
+
credentialsService = ctx.container.resolve('integrationCredentialsService') as CredentialsServiceLike
|
|
63
|
+
} catch {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await credentialsService.save('channel_resend', preset, {
|
|
68
|
+
tenantId: ctx.tenantId,
|
|
69
|
+
organizationId: ctx.organizationId,
|
|
70
|
+
userId: null,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const integrationStateService = ctx.container.resolve('integrationStateService') as IntegrationStateServiceLike
|
|
75
|
+
await integrationStateService.upsert('channel_resend', { isEnabled: true }, {
|
|
76
|
+
tenantId: ctx.tenantId,
|
|
77
|
+
organizationId: ctx.organizationId,
|
|
78
|
+
})
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn('Failed to enable the Resend integration state; Integrations will read Disabled while email is live', {
|
|
81
|
+
err,
|
|
82
|
+
tenantId: ctx.tenantId,
|
|
83
|
+
organizationId: ctx.organizationId,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await ensureSystemEmailChannel(ctx.em, {
|
|
88
|
+
tenantId: ctx.tenantId,
|
|
89
|
+
organizationId: ctx.organizationId,
|
|
90
|
+
providerKey: 'resend',
|
|
91
|
+
externalIdentifier: preset.fromAddress,
|
|
92
|
+
displayName: 'Resend system email',
|
|
93
|
+
capabilities: { ...resendCapabilities },
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { registerSystemEmailProviderConfigResolver } from '@open-mercato/core/modules/communication_channels/lib/system-email-provider-config'
|
|
2
|
+
import { readResendEnvPreset } from './preset'
|
|
3
|
+
|
|
4
|
+
export function registerResendSystemEmailConfigResolver(): void {
|
|
5
|
+
registerSystemEmailProviderConfigResolver({
|
|
6
|
+
providerKey: 'resend',
|
|
7
|
+
isConfigured: () => Boolean(readResendEnvPreset()),
|
|
8
|
+
resolveCredentials: ({ fromAddress }) => readResendEnvPreset() ?? { fromAddress },
|
|
9
|
+
})
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { getResendChannelAdapter } from './lib/adapter'
|
|
7
|
+
import { applyResendEnvPreset } from './lib/preset'
|
|
8
|
+
import { registerResendSystemEmailConfigResolver } from './lib/system-email-config'
|
|
9
|
+
|
|
10
|
+
function ensureResendAdapterRegistered(): void {
|
|
11
|
+
if (hasChannelAdapter('resend')) return
|
|
12
|
+
registerChannelAdapter(getResendChannelAdapter())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ensureResendAdapterRegistered()
|
|
16
|
+
registerResendSystemEmailConfigResolver()
|
|
17
|
+
|
|
18
|
+
export const setup: ModuleSetupConfig = {
|
|
19
|
+
defaultRoleFeatures: {
|
|
20
|
+
superadmin: ['channel_resend.view', 'channel_resend.configure'],
|
|
21
|
+
admin: ['channel_resend.view', 'channel_resend.configure'],
|
|
22
|
+
},
|
|
23
|
+
async seedDefaults(ctx) {
|
|
24
|
+
ensureResendAdapterRegistered()
|
|
25
|
+
registerResendSystemEmailConfigResolver()
|
|
26
|
+
await applyResendEnvPreset(ctx)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default setup
|
package/tsconfig.json
ADDED