@elevasis/core 0.13.0 → 0.14.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 (41) 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 +2324 -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 +1075 -859
  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 +103 -90
  16. package/src/business/acquisition/index.ts +149 -111
  17. package/src/business/acquisition/stateful.ts +30 -0
  18. package/src/business/acquisition/types.ts +44 -77
  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 +945 -888
  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 +816 -791
  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.ts +272 -0
  37. package/src/organization-model/published.ts +21 -21
  38. package/src/organization-model/resolve.ts +21 -8
  39. package/src/platform/constants/versions.ts +1 -1
  40. package/src/reference/_generated/contracts.md +2324 -0
  41. package/src/supabase/database.types.ts +2958 -2886
@@ -1,210 +1,204 @@
1
- import type { gmail_v1 } from '@googleapis/gmail'
2
- import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
3
- import { ToolingError } from '../../../../types'
4
- import type { ExecutionContext } from '../../../../../base/types'
5
-
6
- /**
7
- * Gmail credentials format
8
- * Stored in credentials table, encrypted
9
- */
10
- interface GmailCredentials {
11
- type: 'oauth2' | 'service-account'
12
- // OAuth2 credentials
13
- clientId?: string
14
- clientSecret?: string
15
- refreshToken?: string
16
- // Service account credentials
17
- serviceAccountKey?: Record<string, unknown>
18
- }
19
-
20
- /**
21
- * Send email parameters
22
- */
23
- interface SendEmailParams {
24
- to: string
25
- subject: string
26
- body: string
27
- cc?: string
28
- from?: string
29
- }
30
-
31
- /**
32
- * Send email result
33
- */
34
- interface SendEmailResult {
35
- messageId: string
36
- threadId: string
37
- }
38
-
39
- /**
40
- * Gmail adapter for sending emails via Gmail API
41
- *
42
- * Supported methods:
43
- * - sendEmail: Send email message
44
- *
45
- * @example
46
- * const adapter = new GmailAdapter()
47
- * const result = await adapter.call('sendEmail', {
48
- * to: 'user@example.com',
49
- * subject: 'Hello',
50
- * body: 'This is a test email'
51
- * }, {
52
- * type: 'oauth2',
53
- * clientId: '...',
54
- * clientSecret: '...',
55
- * refreshToken: '...'
56
- * })
57
- */
58
- export class GmailAdapter implements BaseIntegrationAdapter {
59
- readonly name = 'gmail'
60
-
61
- /**
62
- * Call Gmail API method
63
- * @param method - Method name (e.g., 'sendEmail')
64
- * @param params - Method parameters
65
- * @param credentials - OAuth2 or service account credentials
66
- * @param context - Execution context for logging and tracing
67
- */
68
- async call(
69
- method: string,
70
- params: unknown,
71
- credentials: Record<string, unknown>,
72
- context?: ExecutionContext
73
- ): Promise<unknown> {
74
- // Validate credentials format
75
- if (!this.validateCredentials(credentials)) {
76
- throw new ToolingError(
77
- 'credentials_invalid',
78
- 'Invalid Gmail credentials',
79
- { integration: 'gmail', method }
80
- )
81
- }
82
- const gmailCreds = credentials as unknown as GmailCredentials
83
-
84
- // Create authenticated Gmail client
85
- const gmail = await this.createClient(gmailCreds)
86
-
87
- // Route to method handler
88
- switch (method) {
89
- case 'sendEmail':
90
- return this.sendEmail(gmail, params as SendEmailParams, context)
91
- default:
92
- throw new ToolingError(
93
- 'method_not_found',
94
- `Unknown method: ${method}`,
95
- { integration: 'gmail', method }
96
- )
97
- }
98
- }
99
-
100
- /**
101
- * Create authenticated Gmail client (lazy loads @googleapis/gmail SDK)
102
- */
103
- private async createClient(creds: GmailCredentials): Promise<gmail_v1.Gmail> {
104
- const { gmail } = await import('@googleapis/gmail')
105
- const { OAuth2Client, GoogleAuth } = await import('googleapis-common')
106
-
107
- if (creds.type === 'oauth2') {
108
- const auth = new OAuth2Client(
109
- creds.clientId,
110
- creds.clientSecret
111
- )
112
- auth.setCredentials({ refresh_token: creds.refreshToken })
113
-
114
- return gmail({ version: 'v1', auth })
115
- } else {
116
- // Service account (for workspace-wide access)
117
- const auth = new GoogleAuth({
118
- credentials: creds.serviceAccountKey,
119
- scopes: ['https://www.googleapis.com/auth/gmail.send']
120
- })
121
-
122
- return gmail({ version: 'v1', auth: await auth.getClient() as any })
123
- }
124
- }
125
-
126
- /**
127
- * Send email via Gmail API
128
- */
129
- private async sendEmail(
130
- gmail: gmail_v1.Gmail,
131
- params: SendEmailParams,
132
- context?: ExecutionContext
133
- ): Promise<SendEmailResult> {
134
- // Validate params
135
- if (!params.to || !params.subject || !params.body) {
136
- throw new ToolingError(
137
- 'validation_error',
138
- 'Missing required fields: to, subject, body',
139
- { params }
140
- )
141
- }
142
-
143
- // Build RFC 2822 formatted message
144
- const messageParts = [
145
- `To: ${params.to}`,
146
- `Subject: ${params.subject}`,
147
- params.cc ? `Cc: ${params.cc}` : '',
148
- 'Content-Type: text/html; charset=utf-8',
149
- '',
150
- params.body
151
- ]
152
- const message = messageParts.filter(Boolean).join('\n')
153
-
154
- // Base64 encode (URL-safe)
155
- const encodedMessage = Buffer.from(message)
156
- .toString('base64')
157
- .replace(/\+/g, '-')
158
- .replace(/\//g, '_')
159
- .replace(/=+$/, '')
160
-
161
- try {
162
- const response = await gmail.users.messages.send({
163
- userId: 'me',
164
- requestBody: {
165
- raw: encodedMessage
166
- }
167
- })
168
-
169
- if (context?.logger) {
170
- console.log('[GmailAdapter] Email sent:', {
171
- organizationId: context.organizationId,
172
- executionId: context.executionId,
173
- to: params.to,
174
- subject: params.subject,
175
- messageId: response.data.id,
176
- threadId: response.data.threadId
177
- })
178
- }
179
-
180
- return {
181
- messageId: response.data.id!,
182
- threadId: response.data.threadId!
183
- }
184
- } catch (error: any) {
185
- throw new ToolingError(
186
- 'api_error',
187
- `Gmail API error: ${error.message}`,
188
- { statusCode: error.code, details: error }
189
- )
190
- }
191
- }
192
-
193
- /**
194
- * Validate credentials structure
195
- * Required by BaseIntegrationAdapter interface
196
- */
197
- validateCredentials(creds: Record<string, unknown>): boolean {
198
- if (!creds.type) {
199
- return false
200
- }
201
-
202
- if (creds.type === 'oauth2') {
203
- return !!(creds.clientId && creds.clientSecret && creds.refreshToken)
204
- } else if (creds.type === 'service-account') {
205
- return !!creds.serviceAccountKey
206
- }
207
-
208
- return false
209
- }
210
- }
1
+ import type { gmail_v1 } from '@googleapis/gmail'
2
+ import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
3
+ import { ToolingError } from '../../../../types'
4
+ import type { ExecutionContext } from '../../../../../base/types'
5
+
6
+ /**
7
+ * Gmail credentials format
8
+ * Stored in credentials table, encrypted
9
+ */
10
+ interface GmailCredentials {
11
+ type: 'oauth2' | 'service-account'
12
+ // OAuth2 credentials
13
+ clientId?: string
14
+ clientSecret?: string
15
+ refreshToken?: string
16
+ // Service account credentials
17
+ serviceAccountKey?: Record<string, unknown>
18
+ }
19
+
20
+ /**
21
+ * Send email parameters
22
+ */
23
+ interface SendEmailParams {
24
+ to: string
25
+ subject: string
26
+ body: string
27
+ cc?: string
28
+ from?: string
29
+ /**
30
+ * Idempotency key. When provided, embedded as the RFC 2822 Message-ID header so
31
+ * duplicate sends from platform_internal retries are deduped on the recipient side.
32
+ * Pass a stable workflow-derived hash (e.g. `<execId>.<step>@elevasis.io`).
33
+ */
34
+ idempotencyKey?: string
35
+ }
36
+
37
+ /**
38
+ * Send email result
39
+ */
40
+ interface SendEmailResult {
41
+ messageId: string
42
+ threadId: string
43
+ }
44
+
45
+ /**
46
+ * Gmail adapter for sending emails via Gmail API
47
+ *
48
+ * Supported methods:
49
+ * - sendEmail: Send email message
50
+ *
51
+ * @example
52
+ * const adapter = new GmailAdapter()
53
+ * const result = await adapter.call('sendEmail', {
54
+ * to: 'user@example.com',
55
+ * subject: 'Hello',
56
+ * body: 'This is a test email'
57
+ * }, {
58
+ * type: 'oauth2',
59
+ * clientId: '...',
60
+ * clientSecret: '...',
61
+ * refreshToken: '...'
62
+ * })
63
+ */
64
+ export class GmailAdapter implements BaseIntegrationAdapter {
65
+ readonly name = 'gmail'
66
+
67
+ /**
68
+ * Call Gmail API method
69
+ * @param method - Method name (e.g., 'sendEmail')
70
+ * @param params - Method parameters
71
+ * @param credentials - OAuth2 or service account credentials
72
+ * @param context - Execution context for logging and tracing
73
+ */
74
+ async call(
75
+ method: string,
76
+ params: unknown,
77
+ credentials: Record<string, unknown>,
78
+ context?: ExecutionContext
79
+ ): Promise<unknown> {
80
+ // Validate credentials format
81
+ if (!this.validateCredentials(credentials)) {
82
+ throw new ToolingError('credentials_invalid', 'Invalid Gmail credentials', { integration: 'gmail', method })
83
+ }
84
+ const gmailCreds = credentials as unknown as GmailCredentials
85
+
86
+ // Create authenticated Gmail client
87
+ const gmail = await this.createClient(gmailCreds)
88
+
89
+ // Route to method handler
90
+ switch (method) {
91
+ case 'sendEmail':
92
+ return this.sendEmail(gmail, params as SendEmailParams, context)
93
+ default:
94
+ throw new ToolingError('method_not_found', `Unknown method: ${method}`, { integration: 'gmail', method })
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Create authenticated Gmail client (lazy loads @googleapis/gmail SDK)
100
+ */
101
+ private async createClient(creds: GmailCredentials): Promise<gmail_v1.Gmail> {
102
+ const { gmail } = await import('@googleapis/gmail')
103
+ const { OAuth2Client, GoogleAuth } = await import('googleapis-common')
104
+
105
+ if (creds.type === 'oauth2') {
106
+ const auth = new OAuth2Client(creds.clientId, creds.clientSecret)
107
+ auth.setCredentials({ refresh_token: creds.refreshToken })
108
+
109
+ return gmail({ version: 'v1', auth })
110
+ } else {
111
+ // Service account (for workspace-wide access)
112
+ const auth = new GoogleAuth({
113
+ credentials: creds.serviceAccountKey,
114
+ scopes: ['https://www.googleapis.com/auth/gmail.send']
115
+ })
116
+
117
+ return gmail({ version: 'v1', auth: (await auth.getClient()) as any })
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Send email via Gmail API
123
+ */
124
+ private async sendEmail(
125
+ gmail: gmail_v1.Gmail,
126
+ params: SendEmailParams,
127
+ context?: ExecutionContext
128
+ ): Promise<SendEmailResult> {
129
+ // Validate params
130
+ if (!params.to || !params.subject || !params.body) {
131
+ throw new ToolingError('validation_error', 'Missing required fields: to, subject, body', { params })
132
+ }
133
+
134
+ // Build RFC 2822 formatted message. When idempotencyKey is provided, emit a stable
135
+ // Message-ID header duplicate sends from platform_internal retries land with the
136
+ // same Message-ID and are deduped by recipient MTAs / threading clients.
137
+ const messageIdHeader = params.idempotencyKey ? `Message-ID: <${params.idempotencyKey}@elevasis.io>` : ''
138
+ const messageParts = [
139
+ `To: ${params.to}`,
140
+ `Subject: ${params.subject}`,
141
+ params.cc ? `Cc: ${params.cc}` : '',
142
+ messageIdHeader,
143
+ 'Content-Type: text/html; charset=utf-8',
144
+ '',
145
+ params.body
146
+ ]
147
+ const message = messageParts.filter(Boolean).join('\n')
148
+
149
+ // Base64 encode (URL-safe)
150
+ const encodedMessage = Buffer.from(message)
151
+ .toString('base64')
152
+ .replace(/\+/g, '-')
153
+ .replace(/\//g, '_')
154
+ .replace(/=+$/, '')
155
+
156
+ try {
157
+ const response = await gmail.users.messages.send({
158
+ userId: 'me',
159
+ requestBody: {
160
+ raw: encodedMessage
161
+ }
162
+ })
163
+
164
+ if (context?.logger) {
165
+ console.log('[GmailAdapter] Email sent:', {
166
+ organizationId: context.organizationId,
167
+ executionId: context.executionId,
168
+ to: params.to,
169
+ subject: params.subject,
170
+ messageId: response.data.id,
171
+ threadId: response.data.threadId
172
+ })
173
+ }
174
+
175
+ return {
176
+ messageId: response.data.id!,
177
+ threadId: response.data.threadId!
178
+ }
179
+ } catch (error: any) {
180
+ throw new ToolingError('api_error', `Gmail API error: ${error.message}`, {
181
+ statusCode: error.code,
182
+ details: error
183
+ })
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Validate credentials structure
189
+ * Required by BaseIntegrationAdapter interface
190
+ */
191
+ validateCredentials(creds: Record<string, unknown>): boolean {
192
+ if (!creds.type) {
193
+ return false
194
+ }
195
+
196
+ if (creds.type === 'oauth2') {
197
+ return !!(creds.clientId && creds.clientSecret && creds.refreshToken)
198
+ } else if (creds.type === 'service-account') {
199
+ return !!creds.serviceAccountKey
200
+ }
201
+
202
+ return false
203
+ }
204
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Resend sendEmail — idempotency-key regression tests
3
+ *
4
+ * Guards the Idempotency-Key header plumbing added as part of the
5
+ * cache-as-contract-leak fix. Platform-internal retries (TypeError /
6
+ * ReferenceError reclassified as platform_internal) must be safe for write
7
+ * tools — Resend dedupes server-side when the same Idempotency-Key arrives.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
11
+ import { sendEmail } from './index'
12
+ import type { ResendCredentials, SendEmailParams } from '../utils/types'
13
+
14
+ const credentials: ResendCredentials = { apiKey: 're_test_123' }
15
+ const baseParams: SendEmailParams = {
16
+ from: 'Test <test@elevasis.io>',
17
+ to: 'recipient@example.com',
18
+ subject: 'Hello',
19
+ html: '<p>Hi</p>'
20
+ }
21
+
22
+ function mockResendOk(): ReturnType<typeof vi.fn> {
23
+ const fetchMock = vi.fn(async () =>
24
+ Promise.resolve(
25
+ new Response(JSON.stringify({ id: 'email_abc' }), {
26
+ status: 200,
27
+ headers: { 'Content-Type': 'application/json' }
28
+ })
29
+ )
30
+ )
31
+ ;(globalThis as unknown as { fetch: typeof fetch }).fetch = fetchMock as unknown as typeof fetch
32
+ return fetchMock
33
+ }
34
+
35
+ describe('Resend sendEmail — idempotency key', () => {
36
+ let originalFetch: typeof fetch
37
+
38
+ beforeEach(() => {
39
+ originalFetch = globalThis.fetch
40
+ })
41
+
42
+ afterEach(() => {
43
+ globalThis.fetch = originalFetch
44
+ })
45
+
46
+ it('omits the Idempotency-Key header when idempotencyKey is not provided', async () => {
47
+ const fetchMock = mockResendOk()
48
+
49
+ await sendEmail(credentials, baseParams)
50
+
51
+ expect(fetchMock).toHaveBeenCalledOnce()
52
+ const init = fetchMock.mock.calls[0][1] as RequestInit
53
+ const headers = init.headers as Record<string, string>
54
+ expect(headers['Idempotency-Key']).toBeUndefined()
55
+ // Sanity check: required headers are still present
56
+ expect(headers.Authorization).toBe('Bearer re_test_123')
57
+ expect(headers['Content-Type']).toBe('application/json')
58
+ })
59
+
60
+ it('passes idempotencyKey through as the Idempotency-Key header', async () => {
61
+ const fetchMock = mockResendOk()
62
+
63
+ await sendEmail(credentials, { ...baseParams, idempotencyKey: 'wf-exec-001-step-send-approval' })
64
+
65
+ const init = fetchMock.mock.calls[0][1] as RequestInit
66
+ const headers = init.headers as Record<string, string>
67
+ expect(headers['Idempotency-Key']).toBe('wf-exec-001-step-send-approval')
68
+ })
69
+
70
+ it('sends the same Idempotency-Key on caller-driven retries (regression: platform_internal retries must dedupe)', async () => {
71
+ const fetchMock = mockResendOk()
72
+
73
+ const params: SendEmailParams = { ...baseParams, idempotencyKey: 'stable-key-xyz' }
74
+
75
+ // Simulate two calls with identical params (e.g. caller retries after a
76
+ // platform_internal failure). Both must carry the same Idempotency-Key
77
+ // so Resend collapses them into a single send.
78
+ await sendEmail(credentials, params)
79
+ await sendEmail(credentials, params)
80
+
81
+ expect(fetchMock).toHaveBeenCalledTimes(2)
82
+
83
+ const firstHeaders = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record<string, string>
84
+ const secondHeaders = (fetchMock.mock.calls[1][1] as RequestInit).headers as Record<string, string>
85
+ expect(firstHeaders['Idempotency-Key']).toBe('stable-key-xyz')
86
+ expect(secondHeaders['Idempotency-Key']).toBe('stable-key-xyz')
87
+ })
88
+ })