@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/organization-model/index.d.ts +1 -1
- package/dist/organization-model/index.js +9 -2
- package/dist/test-utils/index.d.ts +463 -377
- package/dist/test-utils/index.js +9 -2
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2336 -0
- package/src/business/acquisition/activity-events.test.ts +250 -0
- package/src/business/acquisition/activity-events.ts +7 -65
- package/src/business/acquisition/api-schemas.test.ts +1180 -0
- package/src/business/acquisition/api-schemas.ts +317 -73
- package/src/business/acquisition/crm-state-actions.test.ts +160 -0
- package/src/business/acquisition/derive-actions.test.ts +518 -0
- package/src/business/acquisition/derive-actions.ts +101 -78
- package/src/business/acquisition/index.ts +51 -9
- package/src/business/acquisition/stateful.ts +30 -0
- package/src/business/acquisition/types.ts +48 -80
- package/src/execution/engine/index.ts +437 -434
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -360
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
- package/src/execution/engine/tools/integration/service.test.ts +34 -9
- package/src/execution/engine/tools/integration/service.ts +6 -3
- package/src/execution/engine/tools/lead-service-types.ts +934 -874
- package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
- package/src/execution/engine/tools/registry.ts +701 -699
- package/src/execution/engine/tools/tool-maps.ts +30 -2
- package/src/execution/engine/workflow/types.ts +11 -0
- package/src/organization-model/contracts.ts +4 -4
- package/src/organization-model/domains/navigation.ts +62 -62
- package/src/organization-model/domains/sales.test.ts +189 -0
- package/src/organization-model/domains/sales.ts +456 -94
- package/src/organization-model/published.ts +21 -21
- package/src/organization-model/resolve.ts +21 -8
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +2336 -0
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
auth
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts
ADDED
|
@@ -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
|
+
})
|