@elevasis/core 0.13.0 → 0.15.0

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 (42) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +9 -2
  3. package/dist/organization-model/index.d.ts +1 -1
  4. package/dist/organization-model/index.js +9 -2
  5. package/dist/test-utils/index.d.ts +463 -377
  6. package/dist/test-utils/index.js +9 -2
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2336 -0
  9. package/src/business/acquisition/activity-events.test.ts +250 -0
  10. package/src/business/acquisition/activity-events.ts +7 -65
  11. package/src/business/acquisition/api-schemas.test.ts +1180 -0
  12. package/src/business/acquisition/api-schemas.ts +317 -73
  13. package/src/business/acquisition/crm-state-actions.test.ts +160 -0
  14. package/src/business/acquisition/derive-actions.test.ts +518 -0
  15. package/src/business/acquisition/derive-actions.ts +101 -78
  16. package/src/business/acquisition/index.ts +51 -9
  17. package/src/business/acquisition/stateful.ts +30 -0
  18. package/src/business/acquisition/types.ts +48 -80
  19. package/src/execution/engine/index.ts +437 -434
  20. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -360
  21. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
  22. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
  23. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
  24. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
  25. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
  26. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
  27. package/src/execution/engine/tools/integration/service.test.ts +34 -9
  28. package/src/execution/engine/tools/integration/service.ts +6 -3
  29. package/src/execution/engine/tools/lead-service-types.ts +934 -874
  30. package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
  31. package/src/execution/engine/tools/registry.ts +701 -699
  32. package/src/execution/engine/tools/tool-maps.ts +30 -2
  33. package/src/execution/engine/workflow/types.ts +11 -0
  34. package/src/organization-model/contracts.ts +4 -4
  35. package/src/organization-model/domains/navigation.ts +62 -62
  36. package/src/organization-model/domains/sales.test.ts +189 -0
  37. package/src/organization-model/domains/sales.ts +456 -94
  38. package/src/organization-model/published.ts +21 -21
  39. package/src/organization-model/resolve.ts +21 -8
  40. package/src/platform/constants/versions.ts +1 -1
  41. package/src/reference/_generated/contracts.md +2336 -0
  42. package/src/supabase/database.types.ts +2958 -2886
@@ -1,134 +1,141 @@
1
- import { ToolingError } from '../../../../../../types'
2
- import type { ExecutionContext } from '../../../../../../../base/types'
3
- import { withRetry, createHttpError, DEFAULT_RETRY_POLICY } from '../../../../../../../../../platform/resilience'
4
- import type { ResendCredentials, SendEmailParams, SendEmailResult, ResendSendEmailResponse } from '../utils/types'
5
-
6
- /**
7
- * Send an email using Resend API
8
- *
9
- * @param credentials - Resend API credentials (apiKey)
10
- * @param params - Email parameters (from, to, subject, html/text, etc.)
11
- * @param context - Execution context for logging
12
- * @returns Email ID and success status
13
- * @throws ToolingError if email sending fails
14
- *
15
- * @example
16
- * // Send a simple email
17
- * const result = await sendEmail(
18
- * { apiKey: 're_xxx' },
19
- * {
20
- * from: 'Alex <alexl@elevasis.io>',
21
- * to: 'john@example.com',
22
- * subject: 'Discovery Call Reminder',
23
- * html: '<p>Your call is tomorrow...</p>',
24
- * headers: {
25
- * 'List-Unsubscribe': '<https://elevasis.io/api/unsubscribe?email=...>',
26
- * 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
27
- * }
28
- * },
29
- * context
30
- * )
31
- */
32
- export async function sendEmail(
33
- credentials: ResendCredentials,
34
- params: SendEmailParams,
35
- context?: ExecutionContext
36
- ): Promise<SendEmailResult> {
37
- const { apiKey } = credentials
38
- const { from, to, subject, html, text, replyTo, cc, bcc, headers, tags } = params
39
-
40
- // Validate required parameters
41
- if (!apiKey) {
42
- throw new ToolingError('credentials_invalid', 'Resend API credentials must include apiKey', {
43
- integration: 'resend',
44
- method: 'sendEmail'
45
- })
46
- }
47
-
48
- if (!from || !to || !subject) {
49
- throw new ToolingError('validation_error', 'From, to, and subject are required', {
50
- integration: 'resend',
51
- method: 'sendEmail',
52
- params: { from, to, subject }
53
- })
54
- }
55
-
56
- if (!html && !text) {
57
- throw new ToolingError('validation_error', 'Either html or text content is required', {
58
- integration: 'resend',
59
- method: 'sendEmail'
60
- })
61
- }
62
-
63
- // Build URL
64
- const url = 'https://api.resend.com/emails'
65
-
66
- // Build request body
67
- const requestBody: Record<string, unknown> = {
68
- from,
69
- to: Array.isArray(to) ? to : [to],
70
- subject
71
- }
72
-
73
- if (html) requestBody.html = html
74
- if (text) requestBody.text = text
75
- if (replyTo) requestBody.reply_to = replyTo
76
- if (cc) requestBody.cc = Array.isArray(cc) ? cc : [cc]
77
- if (bcc) requestBody.bcc = Array.isArray(bcc) ? bcc : [bcc]
78
- if (headers) requestBody.headers = headers
79
- if (tags) requestBody.tags = tags
80
-
81
- try {
82
- // Execute with retry logic
83
- const response = await withRetry(async () => {
84
- const res = await fetch(url, {
85
- method: 'POST',
86
- headers: {
87
- Authorization: `Bearer ${apiKey}`,
88
- 'Content-Type': 'application/json'
89
- },
90
- body: JSON.stringify(requestBody)
91
- })
92
-
93
- // Handle HTTP errors
94
- if (!res.ok) {
95
- throw await createHttpError(res, {
96
- integration: 'resend',
97
- method: 'sendEmail',
98
- organizationId: context?.organizationId
99
- })
100
- }
101
-
102
- return res
103
- }, DEFAULT_RETRY_POLICY)
104
-
105
- // Parse response
106
- const result = (await response.json()) as ResendSendEmailResponse
107
-
108
- // Log success
109
- if (context?.logger) {
110
- context.logger.info(
111
- `[ResendAdapter] Email sent: organizationId=${context.organizationId} executionId=${context.executionId} emailId=${result.id} to=${Array.isArray(to) ? to.join(',') : to}`
112
- )
113
- }
114
-
115
- // Return standardized result
116
- return {
117
- id: result.id,
118
- success: true
119
- }
120
- } catch (error) {
121
- // Re-throw ToolingError as-is
122
- if (error instanceof ToolingError) {
123
- throw error
124
- }
125
-
126
- // Wrap unknown errors
127
- throw new ToolingError('api_error', `Failed to send email via Resend: ${(error as Error).message}`, {
128
- integration: 'resend',
129
- method: 'sendEmail',
130
- organizationId: context?.organizationId,
131
- originalError: error
132
- })
133
- }
134
- }
1
+ import { ToolingError } from '../../../../../../types'
2
+ import type { ExecutionContext } from '../../../../../../../base/types'
3
+ import { withRetry, createHttpError, DEFAULT_RETRY_POLICY } from '../../../../../../../../../platform/resilience'
4
+ import type { ResendCredentials, SendEmailParams, SendEmailResult, ResendSendEmailResponse } from '../utils/types'
5
+
6
+ /**
7
+ * Send an email using Resend API
8
+ *
9
+ * @param credentials - Resend API credentials (apiKey)
10
+ * @param params - Email parameters (from, to, subject, html/text, etc.)
11
+ * @param context - Execution context for logging
12
+ * @returns Email ID and success status
13
+ * @throws ToolingError if email sending fails
14
+ *
15
+ * @example
16
+ * // Send a simple email
17
+ * const result = await sendEmail(
18
+ * { apiKey: 're_xxx' },
19
+ * {
20
+ * from: 'Alex <alexl@elevasis.io>',
21
+ * to: 'john@example.com',
22
+ * subject: 'Discovery Call Reminder',
23
+ * html: '<p>Your call is tomorrow...</p>',
24
+ * headers: {
25
+ * 'List-Unsubscribe': '<https://elevasis.io/api/unsubscribe?email=...>',
26
+ * 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
27
+ * }
28
+ * },
29
+ * context
30
+ * )
31
+ */
32
+ export async function sendEmail(
33
+ credentials: ResendCredentials,
34
+ params: SendEmailParams,
35
+ context?: ExecutionContext
36
+ ): Promise<SendEmailResult> {
37
+ const { apiKey } = credentials
38
+ const { from, to, subject, html, text, replyTo, cc, bcc, headers, tags, idempotencyKey } = params
39
+
40
+ // Validate required parameters
41
+ if (!apiKey) {
42
+ throw new ToolingError('credentials_invalid', 'Resend API credentials must include apiKey', {
43
+ integration: 'resend',
44
+ method: 'sendEmail'
45
+ })
46
+ }
47
+
48
+ if (!from || !to || !subject) {
49
+ throw new ToolingError('validation_error', 'From, to, and subject are required', {
50
+ integration: 'resend',
51
+ method: 'sendEmail',
52
+ params: { from, to, subject }
53
+ })
54
+ }
55
+
56
+ if (!html && !text) {
57
+ throw new ToolingError('validation_error', 'Either html or text content is required', {
58
+ integration: 'resend',
59
+ method: 'sendEmail'
60
+ })
61
+ }
62
+
63
+ // Build URL
64
+ const url = 'https://api.resend.com/emails'
65
+
66
+ // Build request body
67
+ const requestBody: Record<string, unknown> = {
68
+ from,
69
+ to: Array.isArray(to) ? to : [to],
70
+ subject
71
+ }
72
+
73
+ if (html) requestBody.html = html
74
+ if (text) requestBody.text = text
75
+ if (replyTo) requestBody.reply_to = replyTo
76
+ if (cc) requestBody.cc = Array.isArray(cc) ? cc : [cc]
77
+ if (bcc) requestBody.bcc = Array.isArray(bcc) ? bcc : [bcc]
78
+ if (headers) requestBody.headers = headers
79
+ if (tags) requestBody.tags = tags
80
+
81
+ try {
82
+ // Build request headers; include Idempotency-Key when provided so platform_internal
83
+ // retries dedupe server-side instead of double-sending.
84
+ const requestHeaders: Record<string, string> = {
85
+ Authorization: `Bearer ${apiKey}`,
86
+ 'Content-Type': 'application/json'
87
+ }
88
+ if (idempotencyKey) {
89
+ requestHeaders['Idempotency-Key'] = idempotencyKey
90
+ }
91
+
92
+ // Execute with retry logic
93
+ const response = await withRetry(async () => {
94
+ const res = await fetch(url, {
95
+ method: 'POST',
96
+ headers: requestHeaders,
97
+ body: JSON.stringify(requestBody)
98
+ })
99
+
100
+ // Handle HTTP errors
101
+ if (!res.ok) {
102
+ throw await createHttpError(res, {
103
+ integration: 'resend',
104
+ method: 'sendEmail',
105
+ organizationId: context?.organizationId
106
+ })
107
+ }
108
+
109
+ return res
110
+ }, DEFAULT_RETRY_POLICY)
111
+
112
+ // Parse response
113
+ const result = (await response.json()) as ResendSendEmailResponse
114
+
115
+ // Log success
116
+ if (context?.logger) {
117
+ context.logger.info(
118
+ `[ResendAdapter] Email sent: organizationId=${context.organizationId} executionId=${context.executionId} emailId=${result.id} to=${Array.isArray(to) ? to.join(',') : to}`
119
+ )
120
+ }
121
+
122
+ // Return standardized result
123
+ return {
124
+ id: result.id,
125
+ success: true
126
+ }
127
+ } catch (error) {
128
+ // Re-throw ToolingError as-is
129
+ if (error instanceof ToolingError) {
130
+ throw error
131
+ }
132
+
133
+ // Wrap unknown errors
134
+ throw new ToolingError('api_error', `Failed to send email via Resend: ${(error as Error).message}`, {
135
+ integration: 'resend',
136
+ method: 'sendEmail',
137
+ organizationId: context?.organizationId,
138
+ originalError: error
139
+ })
140
+ }
141
+ }
@@ -1,75 +1,76 @@
1
- /**
2
- * Resend API Types
3
- *
4
- * Type definitions for Resend email API operations.
5
- */
6
-
7
- /**
8
- * Resend credentials format
9
- * Stored in credentials table, encrypted
10
- */
11
- export interface ResendCredentials {
12
- apiKey: string // API key (re_xxxxxxxxx)
13
- }
14
-
15
- /**
16
- * Send email parameters
17
- */
18
- export interface SendEmailParams {
19
- from: string // "Name <sender@domain.com>"
20
- to: string | string[] // Recipients (max 50)
21
- subject: string // Email subject
22
- html?: string // HTML content
23
- text?: string // Plain text (auto-generated from HTML if omitted)
24
- replyTo?: string | string[] // Reply-to address
25
- cc?: string | string[] // CC recipients
26
- bcc?: string | string[] // BCC recipients
27
- headers?: Record<string, string> // Custom headers (for unsubscribe)
28
- tags?: Array<{ name: string; value: string }> // Key-value metadata
29
- }
30
-
31
- /**
32
- * Send email result
33
- */
34
- export interface SendEmailResult {
35
- id: string // Email ID
36
- success: boolean
37
- }
38
-
39
- /**
40
- * Get email parameters
41
- */
42
- export interface GetEmailParams {
43
- emailId: string // Email ID to retrieve
44
- }
45
-
46
- /**
47
- * Get email result
48
- */
49
- export interface GetEmailResult {
50
- id: string
51
- to: string[]
52
- from: string
53
- subject: string
54
- lastEvent: string // 'sent', 'delivered', 'bounced', 'complained', 'failed'
55
- createdAt: string
56
- }
57
-
58
- /**
59
- * Resend API response for send email
60
- */
61
- export interface ResendSendEmailResponse {
62
- id: string
63
- }
64
-
65
- /**
66
- * Resend API response for get email
67
- */
68
- export interface ResendGetEmailResponse {
69
- id: string
70
- to: string[]
71
- from: string
72
- subject: string
73
- last_event: string
74
- created_at: string
75
- }
1
+ /**
2
+ * Resend API Types
3
+ *
4
+ * Type definitions for Resend email API operations.
5
+ */
6
+
7
+ /**
8
+ * Resend credentials format
9
+ * Stored in credentials table, encrypted
10
+ */
11
+ export interface ResendCredentials {
12
+ apiKey: string // API key (re_xxxxxxxxx)
13
+ }
14
+
15
+ /**
16
+ * Send email parameters
17
+ */
18
+ export interface SendEmailParams {
19
+ from: string // "Name <sender@domain.com>"
20
+ to: string | string[] // Recipients (max 50)
21
+ subject: string // Email subject
22
+ html?: string // HTML content
23
+ text?: string // Plain text (auto-generated from HTML if omitted)
24
+ replyTo?: string | string[] // Reply-to address
25
+ cc?: string | string[] // CC recipients
26
+ bcc?: string | string[] // BCC recipients
27
+ headers?: Record<string, string> // Custom headers (for unsubscribe)
28
+ tags?: Array<{ name: string; value: string }> // Key-value metadata
29
+ idempotencyKey?: string // Resend Idempotency-Key header — same key returns the original send instead of duplicating
30
+ }
31
+
32
+ /**
33
+ * Send email result
34
+ */
35
+ export interface SendEmailResult {
36
+ id: string // Email ID
37
+ success: boolean
38
+ }
39
+
40
+ /**
41
+ * Get email parameters
42
+ */
43
+ export interface GetEmailParams {
44
+ emailId: string // Email ID to retrieve
45
+ }
46
+
47
+ /**
48
+ * Get email result
49
+ */
50
+ export interface GetEmailResult {
51
+ id: string
52
+ to: string[]
53
+ from: string
54
+ subject: string
55
+ lastEvent: string // 'sent', 'delivered', 'bounced', 'complained', 'failed'
56
+ createdAt: string
57
+ }
58
+
59
+ /**
60
+ * Resend API response for send email
61
+ */
62
+ export interface ResendSendEmailResponse {
63
+ id: string
64
+ }
65
+
66
+ /**
67
+ * Resend API response for get email
68
+ */
69
+ export interface ResendGetEmailResponse {
70
+ id: string
71
+ to: string[]
72
+ from: string
73
+ subject: string
74
+ last_event: string
75
+ created_at: string
76
+ }
@@ -185,30 +185,55 @@ describe('IntegrationService — credential cache (context.store)', () => {
185
185
  })
186
186
  })
187
187
 
188
- it('throws TypeError when context.store is undefined (simulates the pre-fix bug)', async () => {
189
- // This documents the exact failure mode that shipped: dispatchIntegrationTool
190
- // constructed a context without `store`. The crash was:
188
+ it('treats context.store as opportunistic missing store falls through to credential fetch instead of crashing', async () => {
189
+ // 2026-04-25 regression guard: the original bug crashed with
191
190
  // "Cannot read properties of undefined (reading 'get')"
191
+ // because context.store.get(cacheKey) had no optional chaining. After the
192
+ // cache-as-contract-leak fix, the cache is treated as a perf optimization,
193
+ // not a load-bearing contract. A missing store must therefore fall through
194
+ // to the credentialsService fetch path and succeed.
192
195
  const adapter = makeStubAdapter('stripe')
193
196
  const service = new IntegrationService(new Map([['stripe', adapter]]))
194
197
  const credentials = { apiKey: 'sk_test_123' }
195
198
 
196
199
  mockGetCredential.mockResolvedValue(credentials)
197
200
 
198
- // Deliberately construct a context without `store` (mimics the pre-fix bug)
199
- const brokenContext = {
201
+ const contextWithoutStore = {
200
202
  executionId: 'exec-broken',
201
203
  organizationId: 'org-test-001',
202
204
  organizationName: 'Test Org',
203
205
  resourceId: 'resource-broken',
204
206
  executionDepth: 0,
205
207
  logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }
206
- // store is intentionally absent — this was the bug
208
+ // store is intentionally absent
207
209
  } as unknown as ExecutionContext
208
210
 
209
- await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', brokenContext)).rejects.toThrow(
210
- /Cannot read properties of undefined \(reading '(get|set)'\)/
211
- )
211
+ const result = await service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', contextWithoutStore)
212
+
213
+ expect(result).toEqual({ stubResult: true })
214
+ // Credentials must have been fetched once (no cache hit, no cache write).
215
+ expect(mockGetCredential).toHaveBeenCalledOnce()
216
+ expect(mockGetCredential).toHaveBeenCalledWith('org-test-001', 'stripe-cred')
217
+ })
218
+
219
+ it('handles a null context.store the same way as undefined (opportunistic fallthrough)', async () => {
220
+ const adapter = makeStubAdapter('stripe')
221
+ const service = new IntegrationService(new Map([['stripe', adapter]]))
222
+ mockGetCredential.mockResolvedValue({ apiKey: 'sk_test_123' })
223
+
224
+ const contextNullStore = {
225
+ executionId: 'exec-null',
226
+ organizationId: 'org-test-001',
227
+ organizationName: 'Test Org',
228
+ resourceId: 'resource-null',
229
+ executionDepth: 0,
230
+ logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
231
+ store: null
232
+ } as unknown as ExecutionContext
233
+
234
+ await expect(service.call('stripe', 'listPaymentLinks', {}, 'stripe-cred', contextNullStore)).resolves.toEqual({
235
+ stubResult: true
236
+ })
212
237
  })
213
238
  })
214
239
  })
@@ -89,9 +89,12 @@ export class IntegrationService {
89
89
  })
90
90
  }
91
91
 
92
- // 4. Get credentials from registry (per-execution cache keyed by credential name)
92
+ // 4. Get credentials from registry (per-execution cache keyed by credential name).
93
+ // The store is an opportunistic perf cache — never a load-bearing contract.
94
+ // Optional chaining lets a missing/undefined store fall through to the fetch path
95
+ // instead of crashing with "Cannot read properties of undefined (reading 'get')".
93
96
  const cacheKey = `__cred_cache__:${credentialName}`
94
- let credentials = context.store.get(cacheKey) as Record<string, unknown> | undefined
97
+ let credentials = context.store?.get(cacheKey) as Record<string, unknown> | undefined
95
98
 
96
99
  if (!credentials) {
97
100
  const { credentialsService } = getToolServices()
@@ -112,7 +115,7 @@ export class IntegrationService {
112
115
  })
113
116
  }
114
117
 
115
- context.store.set(cacheKey, fetched)
118
+ context.store?.set(cacheKey, fetched)
116
119
  credentials = fetched
117
120
  }
118
121