@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/build.mjs +7 -0
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/modules/channel_resend/acl.js +10 -0
  6. package/dist/modules/channel_resend/acl.js.map +7 -0
  7. package/dist/modules/channel_resend/capabilities.js +10 -0
  8. package/dist/modules/channel_resend/capabilities.js.map +7 -0
  9. package/dist/modules/channel_resend/di.js +22 -0
  10. package/dist/modules/channel_resend/di.js.map +7 -0
  11. package/dist/modules/channel_resend/index.js +13 -0
  12. package/dist/modules/channel_resend/index.js.map +7 -0
  13. package/dist/modules/channel_resend/integration.js +61 -0
  14. package/dist/modules/channel_resend/integration.js.map +7 -0
  15. package/dist/modules/channel_resend/lib/adapter.js +109 -0
  16. package/dist/modules/channel_resend/lib/adapter.js.map +7 -0
  17. package/dist/modules/channel_resend/lib/credentials.js +9 -0
  18. package/dist/modules/channel_resend/lib/credentials.js.map +7 -0
  19. package/dist/modules/channel_resend/lib/health.js +43 -0
  20. package/dist/modules/channel_resend/lib/health.js.map +7 -0
  21. package/dist/modules/channel_resend/lib/preset.js +61 -0
  22. package/dist/modules/channel_resend/lib/preset.js.map +7 -0
  23. package/dist/modules/channel_resend/lib/system-email-config.js +13 -0
  24. package/dist/modules/channel_resend/lib/system-email-config.js.map +7 -0
  25. package/dist/modules/channel_resend/setup.js +30 -0
  26. package/dist/modules/channel_resend/setup.js.map +7 -0
  27. package/jest.config.cjs +33 -0
  28. package/package.json +87 -0
  29. package/src/index.ts +1 -0
  30. package/src/modules/channel_resend/__tests__/contracts.test.ts +27 -0
  31. package/src/modules/channel_resend/acl.ts +6 -0
  32. package/src/modules/channel_resend/capabilities.ts +8 -0
  33. package/src/modules/channel_resend/di.ts +20 -0
  34. package/src/modules/channel_resend/index.ts +9 -0
  35. package/src/modules/channel_resend/integration.ts +56 -0
  36. package/src/modules/channel_resend/lib/__tests__/adapter.test.ts +70 -0
  37. package/src/modules/channel_resend/lib/__tests__/health.test.ts +38 -0
  38. package/src/modules/channel_resend/lib/__tests__/preset.test.ts +179 -0
  39. package/src/modules/channel_resend/lib/adapter.ts +139 -0
  40. package/src/modules/channel_resend/lib/credentials.ts +8 -0
  41. package/src/modules/channel_resend/lib/health.ts +51 -0
  42. package/src/modules/channel_resend/lib/preset.ts +95 -0
  43. package/src/modules/channel_resend/lib/system-email-config.ts +10 -0
  44. package/src/modules/channel_resend/setup.ts +30 -0
  45. package/tsconfig.json +9 -0
  46. 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
@@ -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)