@webex/internal-plugin-call-ai-summary 0.0.0-next.1

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.
@@ -0,0 +1,318 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {WebexPlugin} from '@webex/webex-core';
6
+
7
+ import {AI_SUMMARY_SERVICE, AI_SUMMARY_CONTAINERS_RESOURCE, ERROR_MESSAGES} from './constants';
8
+ import type {
9
+ GetContainerOptions,
10
+ GetSummaryContentOptions,
11
+ PragyaContainerResponse,
12
+ SummaryContent,
13
+ SummaryNotes,
14
+ SummaryActionItems,
15
+ TranscriptContent,
16
+ } from './types';
17
+
18
+ const AISummary = WebexPlugin.extend({
19
+ namespace: 'AISummary',
20
+
21
+ /**
22
+ * Resolve a Pragya container by ID.
23
+ * Returns container metadata including summary URLs and encryption key.
24
+ *
25
+ * @param {GetContainerOptions} options
26
+ * @returns {Promise<PragyaContainerResponse>}
27
+ */
28
+ getContainer(options: GetContainerOptions): Promise<PragyaContainerResponse> {
29
+ const {containerId} = options;
30
+
31
+ this._validateContainerId(containerId);
32
+
33
+ return this.webex
34
+ .request({
35
+ method: 'GET',
36
+ service: AI_SUMMARY_SERVICE,
37
+ resource: `${AI_SUMMARY_CONTAINERS_RESOURCE}/${containerId}`,
38
+ })
39
+ .then(({body}) => {
40
+ // Pragya API nests summary URLs under summaryData.data — flatten
41
+ // so consumers can access summaryData.summaryUrl directly.
42
+ if (body.summaryData?.data) {
43
+ body.summaryData = body.summaryData.data;
44
+ }
45
+
46
+ return body;
47
+ })
48
+ .catch((error) => {
49
+ this.logger.error('AISummary->getContainer failed', {error, containerId});
50
+ throw this._handleError(error, 'getContainer');
51
+ });
52
+ },
53
+
54
+ /**
55
+ * Get AI-generated full summary for a call.
56
+ * Fetches from containerInfo.summaryData.summaryUrl and decrypts content.
57
+ *
58
+ * @param {GetSummaryContentOptions} options
59
+ * @returns {Promise<SummaryContent>}
60
+ */
61
+ async getSummary(options: GetSummaryContentOptions): Promise<SummaryContent> {
62
+ const {containerInfo} = options;
63
+
64
+ this._validateContainerInfo(containerInfo, 'summaryUrl');
65
+
66
+ try {
67
+ const {body} = await this.webex.request({
68
+ method: 'GET',
69
+ uri: `${containerInfo.summaryData.summaryUrl}?fields=note,shortnote,actionitems`,
70
+ });
71
+
72
+ const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
73
+
74
+ const decryptedNote = await this._decryptContent(body.note.aiGeneratedContent, keyUrl);
75
+
76
+ const decryptedShortNote = await this._decryptContent(
77
+ body.shortnote.aiGeneratedContent,
78
+ keyUrl
79
+ );
80
+
81
+ const decryptedSnippets = await Promise.all(
82
+ (body.actionitems?.snippets || []).map(async (snippet: any) => {
83
+ const decryptedAiContent = await this._decryptContent(snippet.aiGeneratedContent, keyUrl);
84
+
85
+ return {
86
+ id: snippet.id,
87
+ editedContent: snippet.content || undefined,
88
+ aiGeneratedContent: decryptedAiContent,
89
+ };
90
+ })
91
+ );
92
+
93
+ // feedbackUrl may be in the links array as rel="feedback"
94
+ const feedbackLink = (body.links || []).find((link: any) => link.rel === 'feedback');
95
+
96
+ return {
97
+ id: body.id,
98
+ note: decryptedNote,
99
+ shortNote: decryptedShortNote,
100
+ actionItems: decryptedSnippets,
101
+ feedbackUrl: feedbackLink?.href,
102
+ };
103
+ } catch (error) {
104
+ this.logger.error('AISummary->getSummary failed', {error});
105
+ throw this._handleError(error, 'getSummary');
106
+ }
107
+ },
108
+
109
+ /**
110
+ * Get AI-generated notes for a call.
111
+ * Fetches from containerInfo.summaryData.notesUrl and decrypts content.
112
+ *
113
+ * @param {GetSummaryContentOptions} options
114
+ * @returns {Promise<SummaryNotes>}
115
+ */
116
+ async getNotes(options: GetSummaryContentOptions): Promise<SummaryNotes> {
117
+ const {containerInfo} = options;
118
+
119
+ this._validateContainerInfo(containerInfo, 'notesUrl');
120
+
121
+ try {
122
+ const {body} = await this.webex.request({
123
+ method: 'GET',
124
+ uri: containerInfo.summaryData.notesUrl,
125
+ });
126
+
127
+ const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
128
+
129
+ const decryptedContent = await this._decryptContent(body.aiGeneratedContent, keyUrl);
130
+
131
+ return {
132
+ id: body.id,
133
+ content: decryptedContent,
134
+ feedbackUrl: body.feedbackUrl,
135
+ };
136
+ } catch (error) {
137
+ this.logger.error('AISummary->getNotes failed', {error});
138
+ throw this._handleError(error, 'getNotes');
139
+ }
140
+ },
141
+
142
+ /**
143
+ * Get AI-generated action items for a call.
144
+ * Fetches from containerInfo.summaryData.actionItemsUrl and decrypts content.
145
+ *
146
+ * @param {GetSummaryContentOptions} options
147
+ * @returns {Promise<SummaryActionItems>}
148
+ */
149
+ async getActionItems(options: GetSummaryContentOptions): Promise<SummaryActionItems> {
150
+ const {containerInfo} = options;
151
+
152
+ this._validateContainerInfo(containerInfo, 'actionItemsUrl');
153
+
154
+ try {
155
+ const {body} = await this.webex.request({
156
+ method: 'GET',
157
+ uri: containerInfo.summaryData.actionItemsUrl,
158
+ });
159
+
160
+ // Action items response is an array; take the first element
161
+ const actionItemsData = Array.isArray(body) ? body[0] : body;
162
+
163
+ if (!actionItemsData) {
164
+ return {id: undefined, snippets: []};
165
+ }
166
+
167
+ const keyUrl = actionItemsData.keyUrl || containerInfo.encryptionKeyUrl;
168
+
169
+ const decryptedSnippets = await Promise.all(
170
+ (actionItemsData.snippets || []).map(async (snippet: any) => {
171
+ const decryptedAiContent = await this._decryptContent(snippet.aiGeneratedContent, keyUrl);
172
+
173
+ return {
174
+ id: snippet.id,
175
+ editedContent: snippet.content || undefined,
176
+ aiGeneratedContent: decryptedAiContent,
177
+ };
178
+ })
179
+ );
180
+
181
+ return {
182
+ id: actionItemsData.id,
183
+ snippets: decryptedSnippets,
184
+ feedbackUrl: actionItemsData.feedbackUrl,
185
+ };
186
+ } catch (error) {
187
+ this.logger.error('AISummary->getActionItems failed', {error});
188
+ throw this._handleError(error, 'getActionItems');
189
+ }
190
+ },
191
+
192
+ /**
193
+ * Get the transcript URL for a call.
194
+ * Returns the URL string from the container info.
195
+ *
196
+ * @param {GetSummaryContentOptions} options
197
+ * @returns {string}
198
+ */
199
+ getTranscriptUrl(options: GetSummaryContentOptions): string {
200
+ const {containerInfo} = options;
201
+
202
+ this._validateContainerInfo(containerInfo, 'transcriptUrl');
203
+
204
+ return containerInfo.summaryData.transcriptUrl;
205
+ },
206
+
207
+ /**
208
+ * Get decrypted transcript for a call.
209
+ * Fetches from containerInfo.summaryData.transcriptUrl and decrypts each snippet.
210
+ *
211
+ * @param {GetSummaryContentOptions} options
212
+ * @returns {Promise<TranscriptContent>}
213
+ */
214
+ async getTranscript(options: GetSummaryContentOptions): Promise<TranscriptContent> {
215
+ const {containerInfo} = options;
216
+
217
+ this._validateContainerInfo(containerInfo, 'transcriptUrl');
218
+
219
+ try {
220
+ const {body} = await this.webex.request({
221
+ method: 'GET',
222
+ uri: containerInfo.summaryData.transcriptUrl,
223
+ });
224
+
225
+ const keyUrl = body.keyUrl || containerInfo.encryptionKeyUrl;
226
+
227
+ const decryptedSnippets = await Promise.all(
228
+ (body.transcriptSnippetList || []).map(async (snippet: any) => {
229
+ const decryptedContent = await this._decryptContent(snippet.content, keyUrl);
230
+
231
+ return {
232
+ startTime: snippet.startTime,
233
+ endTime: snippet.endTime,
234
+ content: decryptedContent,
235
+ audioCSI: snippet.audioCSI,
236
+ speaker: snippet.speaker,
237
+ };
238
+ })
239
+ );
240
+
241
+ return {
242
+ id: body.id,
243
+ totalCount: body.totalCount,
244
+ snippets: decryptedSnippets,
245
+ };
246
+ } catch (error) {
247
+ this.logger.error('AISummary->getTranscript failed', {error});
248
+ throw this._handleError(error, 'getTranscript');
249
+ }
250
+ },
251
+
252
+ /**
253
+ * Validate containerId parameter.
254
+ * @param {string} containerId - The container ID to validate.
255
+ * @returns {void}
256
+ * @private
257
+ */
258
+ _validateContainerId(containerId: string): void {
259
+ if (!containerId || typeof containerId !== 'string' || containerId.trim().length === 0) {
260
+ throw new Error(ERROR_MESSAGES.INVALID_CONTAINER_ID);
261
+ }
262
+ },
263
+
264
+ /**
265
+ * Validate containerInfo has the required URL field and encryption key.
266
+ * @param {PragyaContainerResponse} containerInfo - The container info to validate.
267
+ * @param {string} urlField - The summaryData field name to check.
268
+ * @returns {void}
269
+ * @private
270
+ */
271
+ _validateContainerInfo(containerInfo: PragyaContainerResponse, urlField: string): void {
272
+ if (!containerInfo?.summaryData?.[urlField] || !containerInfo?.encryptionKeyUrl) {
273
+ throw new Error(ERROR_MESSAGES.INVALID_CONTAINER_INFO);
274
+ }
275
+ },
276
+
277
+ /**
278
+ * Decrypt encrypted content using KMS.
279
+ * Delegates to the internal encryption plugin.
280
+ * @param {string} encryptedContent - The encrypted text (JWE format).
281
+ * @param {string} encryptionKeyUrl - KMS key URL.
282
+ * @returns {Promise<string>} Decrypted plaintext.
283
+ * @private
284
+ */
285
+ _decryptContent(encryptedContent: string, encryptionKeyUrl: string): Promise<string> {
286
+ return this.webex.internal.encryption.decryptText(encryptionKeyUrl, encryptedContent);
287
+ },
288
+
289
+ /**
290
+ * Handle and normalize errors.
291
+ * @param {object} error - The error object from the request.
292
+ * @param {string} methodName - The name of the calling method.
293
+ * @returns {Error} A normalized Error instance.
294
+ * @private
295
+ */
296
+ _handleError(error: any, methodName: string): Error {
297
+ if (error.statusCode === 404) {
298
+ const message =
299
+ methodName === 'getContainer'
300
+ ? ERROR_MESSAGES.CONTAINER_NOT_FOUND
301
+ : ERROR_MESSAGES.CONTENT_NOT_FOUND;
302
+
303
+ return new Error(message);
304
+ }
305
+
306
+ if (error.statusCode === 403) {
307
+ return new Error(ERROR_MESSAGES.ACCESS_DENIED);
308
+ }
309
+
310
+ if (error.statusCode === 401) {
311
+ return new Error(ERROR_MESSAGES.AUTHENTICATION_FAILED);
312
+ }
313
+
314
+ return new Error(`${methodName} failed: ${error.message || 'Unknown error'}`);
315
+ },
316
+ });
317
+
318
+ export default AISummary;
package/src/config.ts ADDED
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ export default {
6
+ aisummary: {},
7
+ };
@@ -0,0 +1,19 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ export const AI_SUMMARY_SERVICE = 'pragya';
6
+ export const AI_SUMMARY_CONTAINERS_RESOURCE = 'containers';
7
+
8
+ export const SUMMARY_STATUSES = {
9
+ ACTIVE: 'Active',
10
+ } as const;
11
+
12
+ export const ERROR_MESSAGES = {
13
+ INVALID_CONTAINER_ID: 'containerId is required and must be a non-empty string',
14
+ INVALID_CONTAINER_INFO: 'containerInfo with valid summaryData and encryptionKeyUrl is required',
15
+ CONTAINER_NOT_FOUND: 'Container not found',
16
+ CONTENT_NOT_FOUND: 'Summary content not available or expired',
17
+ ACCESS_DENIED: 'Access denied: User not authorized to view this summary',
18
+ AUTHENTICATION_FAILED: 'Authentication failed: Invalid or expired token',
19
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import '@webex/internal-plugin-encryption';
6
+ import {registerInternalPlugin} from '@webex/webex-core';
7
+
8
+ import AISummary from './ai-summary';
9
+ import config from './config';
10
+
11
+ registerInternalPlugin('aisummary', AISummary, {config});
12
+
13
+ export {default} from './ai-summary';
@@ -0,0 +1,129 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ *
4
+ * Manual integration test for internal-plugin-call-ai-summary
5
+ * Tests the full flow using the SDK service catalog (WDM):
6
+ * device.register() -> getContainer -> getSummary (with KMS decryption)
7
+ *
8
+ * The SDK resolves `service: 'pragya'` to the correct base URL via the
9
+ * service catalog populated during device registration.
10
+ *
11
+ * Usage:
12
+ * WEBEX_TOKEN='<token>' node src/manual-integration-test.js
13
+ */
14
+
15
+ /* eslint-disable no-console, require-jsdoc */
16
+
17
+ require('@webex/internal-plugin-call-ai-summary');
18
+
19
+ const WebexCore = require('@webex/webex-core').default;
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Configuration
23
+ // ---------------------------------------------------------------------------
24
+ const WEBEX_TOKEN = process.env.WEBEX_TOKEN || '<PASTE_YOUR_TOKEN_HERE>';
25
+ const CONTAINER_ID = process.env.CONTAINER_ID || '<PASTE_CONTAINER_ID_HERE>';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Main
29
+ // ---------------------------------------------------------------------------
30
+ async function main() {
31
+ console.log('=== Step 1: Create WebexCore ===\n');
32
+ const webex = new WebexCore({
33
+ credentials: {
34
+ access_token: WEBEX_TOKEN,
35
+ },
36
+ });
37
+
38
+ // Step 2: Register device to populate service catalog
39
+ console.log('=== Step 2: Register device (WDM) ===\n');
40
+ await webex.internal.device.register();
41
+ console.log('Device registered successfully.');
42
+ console.log('Device URL:', webex.internal.device.url);
43
+
44
+ // Log the pragya service URL from the service catalog
45
+ try {
46
+ const pragyaUrl = webex.internal.services.get('pragya');
47
+ console.log('Pragya service URL (from catalog):', pragyaUrl);
48
+ } catch (e) {
49
+ console.log('Could not resolve pragya from service catalog:', e.message);
50
+ }
51
+
52
+ // Step 3: Get container via plugin (uses service: 'pragya' + resource)
53
+ console.log('\n=== Step 3: getContainer via plugin ===\n');
54
+ const container = await webex.internal.aisummary.getContainer({
55
+ containerId: CONTAINER_ID,
56
+ });
57
+ console.log(
58
+ 'Container Info:',
59
+ JSON.stringify(
60
+ {
61
+ id: container.id,
62
+ objectType: container.objectType,
63
+ encryptionKeyUrl: container.encryptionKeyUrl,
64
+ summaryUrl: container.summaryData?.summaryUrl,
65
+ transcriptUrl: container.summaryData?.transcriptUrl,
66
+ },
67
+ null,
68
+ 2
69
+ )
70
+ );
71
+
72
+ // Step 4: Call getSummary via plugin (fetches + decrypts all content)
73
+ console.log('\n=== Step 4: getSummary via plugin ===\n');
74
+ const summaryResult = await webex.internal.aisummary.getSummary({
75
+ containerInfo: container,
76
+ });
77
+
78
+ console.log('=== getSummary return structure ===');
79
+ const noteStr = summaryResult.note || '';
80
+ const shortNoteStr = summaryResult.shortNote || '';
81
+ const truncNote = noteStr.length > 200 ? `${noteStr.substring(0, 200)}...` : noteStr;
82
+ const truncShort =
83
+ shortNoteStr.length > 200 ? `${shortNoteStr.substring(0, 200)}...` : shortNoteStr;
84
+ const truncated = {
85
+ id: summaryResult.id,
86
+ note: truncNote,
87
+ shortNote: truncShort,
88
+ actionItems: (summaryResult.actionItems || []).map((item) => {
89
+ const content = item.aiGeneratedContent || '';
90
+ const truncContent = content.length > 100 ? `${content.substring(0, 100)}...` : content;
91
+
92
+ return {
93
+ id: item.id,
94
+ aiGeneratedContent: truncContent,
95
+ editedContent: item.editedContent,
96
+ };
97
+ }),
98
+ feedbackUrl: summaryResult.feedbackUrl,
99
+ };
100
+ console.log(JSON.stringify(truncated, null, 2));
101
+
102
+ // Step 5: Get transcript URL via plugin
103
+ console.log('\n=== Step 5: getTranscriptUrl via plugin ===\n');
104
+ const transcriptUrl = webex.internal.aisummary.getTranscriptUrl({
105
+ containerInfo: container,
106
+ });
107
+ console.log('Transcript URL:', transcriptUrl);
108
+
109
+ // Step 6: Fetch transcript content
110
+ console.log('\n=== Step 6: Fetch transcript content ===\n');
111
+ try {
112
+ const {body: transcriptBody} = await webex.request({
113
+ method: 'GET',
114
+ uri: `${transcriptUrl}?fields=id,content`,
115
+ });
116
+ console.log('Transcript response keys:', Object.keys(transcriptBody));
117
+ console.log(JSON.stringify(transcriptBody, null, 2).substring(0, 500));
118
+ } catch (err) {
119
+ console.error('Transcript fetch failed:', err.message);
120
+ }
121
+
122
+ console.log('\nDone.');
123
+ process.exit(0);
124
+ }
125
+
126
+ main().catch((err) => {
127
+ console.error('FATAL:', err.message || err);
128
+ process.exit(1);
129
+ });
@@ -0,0 +1,166 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ *
4
+ * Manual test for internal-plugin-call-ai-summary
5
+ *
6
+ * Usage:
7
+ * WEBEX_TOKEN='<your-token>' node manual-test.js
8
+ *
9
+ * Or paste your token directly into WEBEX_TOKEN below.
10
+ */
11
+
12
+ /* eslint-disable no-console, require-jsdoc */
13
+
14
+ require('@webex/internal-plugin-call-ai-summary');
15
+
16
+ const WebexCore = require('@webex/webex-core').default;
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Configuration
20
+ // ---------------------------------------------------------------------------
21
+ const WEBEX_TOKEN = process.env.WEBEX_TOKEN || '<PASTE_YOUR_TOKEN_HERE>';
22
+ const CONTAINER_ID = '<PASTE_CONTAINER_ID_HERE>';
23
+ const PRAGYA_BASE_URL = '<PASTE_PRAGYA_BASE_URL_HERE>';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Main
27
+ // ---------------------------------------------------------------------------
28
+ async function main() {
29
+ if (WEBEX_TOKEN === '<PASTE_YOUR_TOKEN_HERE>') {
30
+ console.error('ERROR: Set WEBEX_TOKEN env var or paste your token in the script.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const webex = new WebexCore({
35
+ credentials: {
36
+ access_token: WEBEX_TOKEN,
37
+ },
38
+ });
39
+
40
+ console.log('--- Fetching container', CONTAINER_ID, '(SDK auth) ---\n');
41
+
42
+ const response = await webex.request({
43
+ method: 'GET',
44
+ uri: `${PRAGYA_BASE_URL}/containers/${CONTAINER_ID}`,
45
+ headers: {
46
+ 'content-type': 'application/json',
47
+ },
48
+ });
49
+
50
+ const container = response.body;
51
+ let passed = 0;
52
+ let failed = 0;
53
+
54
+ function check(label, actual, expected) {
55
+ if (actual === expected) {
56
+ console.log(` PASS: ${label}`);
57
+ passed += 1;
58
+ } else {
59
+ console.log(
60
+ ` FAIL: ${label} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
61
+ );
62
+ failed += 1;
63
+ }
64
+ }
65
+
66
+ function checkExists(label, value) {
67
+ if (value !== undefined && value !== null) {
68
+ console.log(` PASS: ${label} exists`);
69
+ passed += 1;
70
+ } else {
71
+ console.log(` FAIL: ${label} is missing`);
72
+ failed += 1;
73
+ }
74
+ }
75
+
76
+ function checkType(label, value, type) {
77
+ const actual = Array.isArray(value) ? 'array' : typeof value;
78
+ const expected = type;
79
+ if (type === 'array' ? Array.isArray(value) : actual === expected) {
80
+ console.log(` PASS: ${label} is ${type}`);
81
+ passed += 1;
82
+ } else {
83
+ console.log(` FAIL: ${label} — expected ${type}, got ${actual}`);
84
+ failed += 1;
85
+ }
86
+ }
87
+
88
+ function checkMatch(label, value, regex) {
89
+ if (regex.test(value)) {
90
+ console.log(` PASS: ${label} matches ${regex}`);
91
+ passed += 1;
92
+ } else {
93
+ console.log(` FAIL: ${label} — ${JSON.stringify(value)} does not match ${regex}`);
94
+ failed += 1;
95
+ }
96
+ }
97
+
98
+ // --- Top-level container ---
99
+ console.log('\n--- Top-level container ---');
100
+ checkType('container', container, 'object');
101
+ check('container.id', container.id, CONTAINER_ID);
102
+ check('container.objectType', container.objectType, 'callingAIContainer');
103
+ checkExists('container.summaryData', container.summaryData);
104
+ checkExists('container.memberships', container.memberships);
105
+ checkMatch('container.encryptionKeyUrl', container.encryptionKeyUrl, /^kms:\/\//);
106
+ checkMatch('container.kmsResourceObjectUrl', container.kmsResourceObjectUrl, /^kms:\/\//);
107
+ checkMatch('container.aclUrl', container.aclUrl, /^https?:\/\//);
108
+ checkExists('container.start', container.start);
109
+ checkExists('container.end', container.end);
110
+ checkExists('container.forkSessionId', container.forkSessionId);
111
+ checkExists('container.callSessionId', container.callSessionId);
112
+ checkExists('container.ownerUserId', container.ownerUserId);
113
+ checkExists('container.orgId', container.orgId);
114
+
115
+ // --- summaryData ---
116
+ console.log('\n--- summaryData ---');
117
+ const {summaryData} = container;
118
+ checkType('summaryData', summaryData, 'object');
119
+ checkExists('summaryData.extensionId', summaryData.extensionId);
120
+ check('summaryData.objectType', summaryData.objectType, 'extension');
121
+ check('summaryData.extensionType', summaryData.extensionType, 'callingAISummary');
122
+
123
+ // --- summaryData.data ---
124
+ console.log('\n--- summaryData.data ---');
125
+ const summaryDataData = summaryData.data;
126
+ checkType('summaryData.data', summaryDataData, 'object');
127
+ checkExists('summaryData.data.id', summaryDataData.id);
128
+ check('summaryData.data.objectType', summaryDataData.objectType, 'callingAISummary');
129
+ checkExists('summaryData.data.status', summaryDataData.status);
130
+ checkMatch('summaryData.data.summaryUrl', summaryDataData.summaryUrl, /^https?:\/\//);
131
+ checkMatch('summaryData.data.transcriptUrl', summaryDataData.transcriptUrl, /^https?:\/\//);
132
+ checkMatch('summaryData.data.aclUrl', summaryDataData.aclUrl, /^https?:\/\//);
133
+ checkMatch(
134
+ 'summaryData.data.kmsResourceObjectUrl',
135
+ summaryDataData.kmsResourceObjectUrl,
136
+ /^kms:\/\//
137
+ );
138
+ checkType('summaryData.data.summarizeAfterCall', summaryDataData.summarizeAfterCall, 'boolean');
139
+ checkType('summaryData.data.contentRetention', summaryDataData.contentRetention, 'object');
140
+
141
+ // --- memberships ---
142
+ console.log('\n--- memberships ---');
143
+ const {memberships} = container;
144
+ checkType('memberships', memberships, 'object');
145
+ checkType('memberships.items', memberships.items, 'array');
146
+ if (memberships.items.length > 0) {
147
+ console.log(` PASS: memberships.items has ${memberships.items.length} member(s)`);
148
+ passed += 1;
149
+ const first = memberships.items[0];
150
+ checkExists('memberships.items[0].id', first.id);
151
+ checkType('memberships.items[0].roles', first.roles, 'array');
152
+ check('memberships.items[0].objectType', first.objectType, 'containerMembership');
153
+ } else {
154
+ console.log(' FAIL: memberships.items is empty');
155
+ failed += 1;
156
+ }
157
+
158
+ // --- Summary ---
159
+ console.log(`\n=== ${passed} passed, ${failed} failed ===`);
160
+ process.exit(failed > 0 ? 1 : 0);
161
+ }
162
+
163
+ main().catch((err) => {
164
+ console.error('FAILED:', err.message || err);
165
+ process.exit(1);
166
+ });