m365connector 0.3.6 → 0.3.8

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,989 @@
1
+ import { TOKEN_AUDIENCES } from "../token-providers.js";
2
+
3
+ const CLIENT_APP_OVERRIDE_USERP = "4765445b-32c6-49b0-83e6-1d93765276ca";
4
+
5
+ const SEARCH_ENDPOINT = `https://substrate.office.com/searchservice/api/v3/query?clientappoverride=${CLIENT_APP_OVERRIDE_USERP}&setFlight=EnableAggregationHelper,EnableM365CopilotAggregationV2`;
6
+
7
+ const MIN_FLIGHTS = "EnableAggregationHelper,EnableM365CopilotAggregationV2";
8
+
9
+ const FILE_FIELDS = [
10
+ ".callerStack",
11
+ ".correlationId",
12
+ ".mediaBaseUrl",
13
+ ".spResourceUrl",
14
+ ".thumbnailUrl",
15
+ "AuthorOWSUSER",
16
+ "ContainerTypeId",
17
+ "ContentClass",
18
+ "ContentTypeId",
19
+ "DefaultEncodingURL",
20
+ "Description",
21
+ "DocId",
22
+ "EditorOWSUSER",
23
+ "FileExtension",
24
+ "Filename",
25
+ "GeoLocationSource",
26
+ "HitHighlightedSummary",
27
+ "IsContainer",
28
+ "LastModifiedTime",
29
+ "LinkingUrl",
30
+ "ListID",
31
+ "ModifiedBy",
32
+ "ParentLink",
33
+ "Path",
34
+ "PiSearchResultId",
35
+ "PictureThumbnailURL",
36
+ "ProgID",
37
+ "PromotedState",
38
+ "SPWebUrl",
39
+ "SecondaryFileExtension",
40
+ "SensitivityLabel",
41
+ "ServerRedirectedPreviewURL",
42
+ "ServerRedirectedUrl",
43
+ "ShortcutUrl",
44
+ "SiteId",
45
+ "SiteTemplateId",
46
+ "SiteTitle",
47
+ "Title",
48
+ "UniqueID",
49
+ "UniqueId",
50
+ "ViewCount",
51
+ "ViewsLifeTimeUniqueUsers",
52
+ "WebId",
53
+ "filetype",
54
+ "isDocument",
55
+ "isexternalcontent",
56
+ "Created",
57
+ "PrivacyIndicator",
58
+ "MediaDuration",
59
+ "ColorHex",
60
+ "ModifierUPNs",
61
+ "InformationProtectionLabelId",
62
+ "SiteSensitivityLabelID",
63
+ "Summary",
64
+ "People",
65
+ "DisplayAuthor",
66
+ "GroupId",
67
+ "SiteLogo",
68
+ "SiteName",
69
+ "WebUrl",
70
+ "WebLink",
71
+ ];
72
+
73
+ const EMAIL_FIELDS = [
74
+ "Id",
75
+ "ParentFolderHexId",
76
+ "ParentFolderId",
77
+ "ParentFolderRestId",
78
+ "SortOrderSource",
79
+ "ItemId",
80
+ "ImmutableId",
81
+ "ClientConversationId",
82
+ "ClientThreadId",
83
+ "ConversationTopic",
84
+ "ConversationIndex",
85
+ "DateTimeCreated",
86
+ "DateTimeReceived",
87
+ "DateTimeSent",
88
+ "DisplayBcc",
89
+ "DisplayTo",
90
+ "HasAttachments",
91
+ "IconIndex",
92
+ "ImmutableEntryId",
93
+ "Importance",
94
+ "InferenceClassification",
95
+ "InternetMessageId",
96
+ "IsDraft",
97
+ "IsRead",
98
+ "ItemClass",
99
+ "NormalizedSubject",
100
+ "ParentFolderDisplayName",
101
+ "Preview",
102
+ "ReceivedOrRenewTime",
103
+ "Sender",
104
+ "Sensitivity",
105
+ "Size",
106
+ "Subject",
107
+ "ConversationHexId",
108
+ "From",
109
+ "ConversationId",
110
+ "ConversationRestId",
111
+ "ConversationThreadId",
112
+ "ItemHexId",
113
+ "ItemRestId",
114
+ "MailboxGuids",
115
+ "MentionsPreview",
116
+ "SortKey",
117
+ "Extensions",
118
+ "Extension_SkypeSpaces_ConversationPost_Extension_FromSkypeInternalId_String",
119
+ "Extension_SkypeSpaces_ConversationPost_Extension_ThreadType_String",
120
+ "ChannelName",
121
+ "HitHighlightedSummary",
122
+ "RawContent",
123
+ "InformationProtectionLabelId",
124
+ "HasIRM",
125
+ "ExtractRightsDisabled",
126
+ "DrmRights",
127
+ "AttachmentMetadata",
128
+ "MailboxOwnerUPN",
129
+ "MailboxOwnerId",
130
+ "MailboxOwnerEmailAddress",
131
+ ];
132
+
133
+ const CHAT_FIELDS = [
134
+ "Id",
135
+ "ParentFolderHexId",
136
+ "ParentFolderId",
137
+ "ParentFolderRestId",
138
+ "SortOrderSource",
139
+ "ItemId",
140
+ "ImmutableId",
141
+ "ClientConversationId",
142
+ "ClientThreadId",
143
+ "ConversationTopic",
144
+ "ConversationIndex",
145
+ "DateTimeCreated",
146
+ "DateTimeReceived",
147
+ "DateTimeSent",
148
+ "DisplayBcc",
149
+ "DisplayTo",
150
+ "HasAttachments",
151
+ "IconIndex",
152
+ "ImmutableEntryId",
153
+ "Importance",
154
+ "InferenceClassification",
155
+ "InternetMessageId",
156
+ "IsDraft",
157
+ "IsRead",
158
+ "ItemClass",
159
+ "ParentFolderDisplayName",
160
+ "Preview",
161
+ "ReceivedOrRenewTime",
162
+ "Sender",
163
+ "Size",
164
+ "Subject",
165
+ "ConversationHexId",
166
+ "From",
167
+ "ConversationId",
168
+ "ConversationRestId",
169
+ "ConversationThreadId",
170
+ "ItemHexId",
171
+ "ItemRestId",
172
+ "MailboxGuids",
173
+ "MentionsPreview",
174
+ "SortKey",
175
+ "Extensions",
176
+ "Extension_SkypeSpaces_ConversationPost_Extension_FromSkypeInternalId_String",
177
+ "Extension_SkypeSpaces_ConversationPost_Extension_ThreadType_String",
178
+ "Extension_SkypeSpaces_ConversationPost_Extension_Topic_String",
179
+ "Extension_SkypeSpaces_ConversationPost_Extension_ParentMessageId_String",
180
+ "ChannelName",
181
+ "TeamName",
182
+ "GroupId",
183
+ "HitHighlightedSummary",
184
+ "WebLink",
185
+ ];
186
+
187
+ const EXTERNAL_FIELDS = [
188
+ "label_Title",
189
+ "label_URL",
190
+ "label_LastModifiedBy",
191
+ "label_LastModifiedDateTime",
192
+ "Label_ItemPath",
193
+ "Label_State",
194
+ "Label_ContainerName",
195
+ "Label_Tags",
196
+ "BestAnswerAuthorName",
197
+ "Author",
198
+ "Url",
199
+ "AccessUrl",
200
+ "IconUrl",
201
+ "Summary",
202
+ "FileID",
203
+ "HitHighlightedSummary",
204
+ ];
205
+
206
+ const EVENT_FIELDS = [];
207
+
208
+ const PEOPLE_FIELDS = [
209
+ "FieldsWithMatch",
210
+ "Topics",
211
+ "Skills",
212
+ "Confidence",
213
+ "Department",
214
+ "DisplayName",
215
+ "EmailAddresses",
216
+ "Id",
217
+ "JobTitle",
218
+ "OfficeLocation",
219
+ "Phones",
220
+ "PropertyHits",
221
+ "Score",
222
+ "Text",
223
+ "UserPrincipalName",
224
+ "ImAddress",
225
+ "GivenName",
226
+ ];
227
+
228
+ const BOOKMARK_FIELDS = [];
229
+
230
+ const ACRONYM_FIELDS = [];
231
+
232
+ const COPILOT_CHAT_FIELDS = [
233
+ "Id",
234
+ "ParentFolderHexId",
235
+ "ParentFolderId",
236
+ "ParentFolderRestId",
237
+ "SortOrderSource",
238
+ "ItemId",
239
+ "ImmutableId",
240
+ "ClientConversationId",
241
+ "ClientThreadId",
242
+ "ConversationIndex",
243
+ "DateTimeCreated",
244
+ "DateTimeLastModified",
245
+ "DateTimeReceived",
246
+ "DateTimeSent",
247
+ "DisplayBcc",
248
+ "DisplayTo",
249
+ "HasAttachments",
250
+ "IconIndex",
251
+ "Importance",
252
+ "InferenceClassification",
253
+ "InternetMessageId",
254
+ "IsDraft",
255
+ "IsRead",
256
+ "ItemClass",
257
+ "ParentFolderDisplayName",
258
+ "Preview",
259
+ "ReceivedOrRenewTime",
260
+ "Sender",
261
+ "Size",
262
+ "ConversationHexId",
263
+ "From",
264
+ "ItemHexId",
265
+ "ItemRestId",
266
+ "MailboxGuids",
267
+ "MentionsPreview",
268
+ "SortKey",
269
+ "Extensions",
270
+ "Extension_SkypeSpaces_ConversationPost_Extension_FromSkypeInternalId_String",
271
+ "Extension_SkypeSpaces_ConversationPost_Extension_ThreadType_String",
272
+ "Extension_SkypeSpaces_ConversationPost_Extension_Topic_String",
273
+ "Extension_SkypeSpaces_ConversationPost_Extension_UpdatedTopic_String",
274
+ "Extension_SkypeSpaces_ConversationPost_Extension_CopilotAgentIdentifier_String",
275
+ "ImmutableEntryId",
276
+ "Subject",
277
+ "ConversationId",
278
+ "ConversationRestId",
279
+ "ConversationThreadId",
280
+ "IsLegacyWebChat",
281
+ "BigFunnelConversationGroupIndex",
282
+ "BigFunnelConversationItemCount",
283
+ "Sensitivity",
284
+ "WebLink",
285
+ ];
286
+
287
+ function cryptoUuid() {
288
+ return crypto.randomUUID();
289
+ }
290
+
291
+ function stripTags(value) {
292
+ if (!value || typeof value !== "string") {
293
+ return "";
294
+ }
295
+ return value
296
+ .replace(/<c\d+>/g, "")
297
+ .replace(/<\/c\d+>/g, "")
298
+ .replace(/<[^>]*>/g, "")
299
+ .replace(/\s+/g, " ")
300
+ .trim();
301
+ }
302
+
303
+ function textOrEmpty(value) {
304
+ if (value == null) {
305
+ return "";
306
+ }
307
+ return String(value).trim();
308
+ }
309
+
310
+ function pickFirst(...values) {
311
+ for (const value of values) {
312
+ if (value !== undefined && value !== null && value !== "") {
313
+ return value;
314
+ }
315
+ }
316
+ return "";
317
+ }
318
+
319
+ function getTimeZoneInfo() {
320
+ const now = new Date();
321
+ return {
322
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
323
+ offset: -now.getTimezoneOffset(),
324
+ };
325
+ }
326
+
327
+ function buildAggregationRequests(source, connector) {
328
+ if (connector) {
329
+ return [
330
+ { DataSource: { Name: "All", Composition: [] }, Filters: [] },
331
+ { DataSource: { Name: connector, Composition: [] }, Filters: [] },
332
+ ];
333
+ }
334
+
335
+ if (source === "all") {
336
+ return [
337
+ {
338
+ DataSource: { Name: "All", Composition: [] },
339
+ Filters: [],
340
+ Aggregations: [
341
+ {
342
+ $type: "Histogram",
343
+ Field: "Label_Source",
344
+ From: 0,
345
+ Size: 50,
346
+ BucketDefinition: {
347
+ SortBy: "Count",
348
+ IsDescending: true,
349
+ MinimumCount: 1,
350
+ },
351
+ },
352
+ {
353
+ $type: "Histogram",
354
+ Field: "Label_Type",
355
+ From: 0,
356
+ Size: 50,
357
+ BucketDefinition: {
358
+ SortBy: "Count",
359
+ IsDescending: true,
360
+ MinimumCount: 1,
361
+ },
362
+ },
363
+ ],
364
+ },
365
+ ];
366
+ }
367
+
368
+ return [{ DataSource: { Name: "All", Composition: [] }, Filters: [] }];
369
+ }
370
+
371
+ function buildEntityRequests(source, connector) {
372
+ const fileReq = {
373
+ EntityType: "File",
374
+ SearchMechanisms: ["Lexical", "Semantic"],
375
+ Fields: FILE_FIELDS,
376
+ FilterExpression:
377
+ '(({searchterms}) (NOT FileExtension:vtt AND NOT FileExtension:pod AND NOT (Title:OneNote_DeletedPages OR Title:OneNote_RecycleBin) AND NOT SecondaryFileExtension:onetoc2 AND NOT (ContentClass:STS_List_544 OR ContentClass:STS_ListItem_544) AND NOT WebTemplate:SPSPERS AND NOT (ContentClass:STS_List_DocumentLibrary AND SiteTemplateId:21) AND NOT (ContentClass:STS_List_DocumentLibrary AND Author:"system account")) AND (SPTranslationLanguage:en-us OR (NOT SPTranslatedLanguages:en-us AND NOT SPIsTranslation:true))) AND NOT FileType:pod',
378
+ Sort: { Field: "PersonalScore", SortDirection: "Desc" },
379
+ IdFormat: "EwsId",
380
+ };
381
+
382
+ const externalReq = {
383
+ EntityType: "External",
384
+ IdFormat: "EwsId",
385
+ Fields: EXTERNAL_FIELDS,
386
+ Connections: ["Connectors"],
387
+ SearchMechanisms: ["Lexical", "Semantic"],
388
+ AggregationFilters: [],
389
+ };
390
+
391
+ const emailReq = {
392
+ EntityType: "Email",
393
+ FilterExpression:
394
+ "DistinguishedFolderName:msgfolderroot OR DistinguishedFolderName:deleteditems",
395
+ Sort: { Field: "Score", SortDirection: "Desc" },
396
+ TopResultsCount: 7,
397
+ IdFormat: "EwsId",
398
+ Fields: EMAIL_FIELDS,
399
+ SearchMechanisms: ["Lexical", "Semantic"],
400
+ };
401
+
402
+ const chatReq = {
403
+ EntityType: "Chat",
404
+ FilterExpression: "",
405
+ Sort: { Field: "Score", SortDirection: "Desc" },
406
+ IdFormat: "EwsId",
407
+ Fields: CHAT_FIELDS,
408
+ };
409
+
410
+ const bookmarkReq = {
411
+ EntityType: "Bookmark",
412
+ IdFormat: "EwsId",
413
+ Fields: BOOKMARK_FIELDS,
414
+ };
415
+
416
+ const peoplePrecisionReq = {
417
+ EntityType: "People",
418
+ RequestIntent: "Precision",
419
+ IdFormat: "EwsId",
420
+ Fields: PEOPLE_FIELDS,
421
+ };
422
+
423
+ const acronymReq = {
424
+ EntityType: "Acronym",
425
+ IdFormat: "EwsId",
426
+ Fields: ACRONYM_FIELDS,
427
+ };
428
+
429
+ const peopleRecallReq = {
430
+ EntityType: "People",
431
+ RequestIntent: "Recall",
432
+ IdFormat: "EwsId",
433
+ Fields: PEOPLE_FIELDS,
434
+ FilterExpression:
435
+ "{searchterms} (PeopleType:Person AND PeopleSubtype:OrganizationUser)",
436
+ };
437
+
438
+ const eventReq = {
439
+ EntityType: "Event",
440
+ FilterExpression: "",
441
+ Sort: { Field: "Score", SortDirection: "Desc" },
442
+ IdFormat: "EwsId",
443
+ Fields: EVENT_FIELDS,
444
+ RequestIntent: "Recall",
445
+ };
446
+
447
+ const copilotChatReq = {
448
+ EntityType: "CopilotChat",
449
+ FilterExpression: "",
450
+ Sort: { Field: "Time", SortDirection: "Desc" },
451
+ IdFormat: "EwsId",
452
+ Fields: COPILOT_CHAT_FIELDS,
453
+ };
454
+
455
+ switch (source) {
456
+ case "email":
457
+ return [emailReq];
458
+ case "files":
459
+ return [fileReq];
460
+ case "chat":
461
+ return [chatReq];
462
+ case "events":
463
+ return [eventReq];
464
+ case "external":
465
+ return [externalReq];
466
+ case "all":
467
+ default:
468
+ return [
469
+ fileReq,
470
+ externalReq,
471
+ emailReq,
472
+ chatReq,
473
+ bookmarkReq,
474
+ peoplePrecisionReq,
475
+ acronymReq,
476
+ peopleRecallReq,
477
+ eventReq,
478
+ copilotChatReq,
479
+ ];
480
+ }
481
+ }
482
+
483
+ function safeEmailAddress(value) {
484
+ return (
485
+ value?.EmailAddress?.Address ||
486
+ value?.EmailAddress?.Name ||
487
+ value?.Address ||
488
+ value?.Name ||
489
+ ""
490
+ );
491
+ }
492
+
493
+ function ownerIdentity(resource, type) {
494
+ if (!resource || typeof resource !== "object") {
495
+ return "";
496
+ }
497
+
498
+ if (type === "email") {
499
+ return textOrEmpty(
500
+ pickFirst(
501
+ resource?.MailboxOwnerId,
502
+ resource?.MailboxOwnerUPN,
503
+ resource?.MailboxOwnerEmailAddress,
504
+ resource?.OwnerId,
505
+ resource?.OwnerEmailAddress,
506
+ ),
507
+ );
508
+ }
509
+
510
+ if (type === "event") {
511
+ return textOrEmpty(
512
+ pickFirst(
513
+ resource?.CalendarOwnerId,
514
+ resource?.CalendarOwnerUPN,
515
+ resource?.CalendarOwnerEmailAddress,
516
+ resource?.MailboxOwnerId,
517
+ resource?.MailboxOwnerUPN,
518
+ resource?.OwnerId,
519
+ resource?.OwnerEmailAddress,
520
+ ),
521
+ );
522
+ }
523
+
524
+ return "";
525
+ }
526
+
527
+ function normalizeResult(result, entityType, index) {
528
+ const resource = result?.Resource || {};
529
+ const normalizedType = String(
530
+ entityType || result?.EntityType || "",
531
+ ).toLowerCase();
532
+ const snippet = stripTags(
533
+ result?.Computations?.HitHighlightedSummary?.Value ||
534
+ resource?.HitHighlightedSummary ||
535
+ "",
536
+ );
537
+
538
+ const common = {
539
+ index,
540
+ type: normalizedType,
541
+ title: textOrEmpty(
542
+ pickFirst(
543
+ result?.Title,
544
+ resource?.Title,
545
+ resource?.Subject,
546
+ resource?.DisplayName,
547
+ resource?.label_Title,
548
+ ),
549
+ ),
550
+ url: textOrEmpty(
551
+ pickFirst(
552
+ result?.Url,
553
+ resource?.WebLink,
554
+ resource?.Url,
555
+ resource?.AccessUrl,
556
+ resource?.Path,
557
+ resource?.ServerRedirectedUrl,
558
+ resource?.DefaultEncodingURL,
559
+ ),
560
+ ),
561
+ contentSource: textOrEmpty(result?.ContentSource || ""),
562
+ snippet,
563
+ date: textOrEmpty(
564
+ pickFirst(
565
+ resource?.DateTimeReceived,
566
+ resource?.DateTimeSent,
567
+ resource?.LastModifiedTime,
568
+ resource?.Created,
569
+ resource?.DateTimeCreated,
570
+ ),
571
+ ),
572
+ };
573
+
574
+ if (normalizedType === "email") {
575
+ const id = textOrEmpty(
576
+ pickFirst(resource?.ItemRestId, resource?.Id, result?.Id),
577
+ );
578
+ const mailboxOwnerId = ownerIdentity(resource, "email");
579
+ return {
580
+ ...common,
581
+ from: textOrEmpty(safeEmailAddress(resource?.From || resource?.Sender)),
582
+ to: textOrEmpty(resource?.DisplayTo || ""),
583
+ subject: textOrEmpty(
584
+ resource?.Subject || resource?.NormalizedSubject || common.title,
585
+ ),
586
+ _readContext: {
587
+ type: "email",
588
+ id,
589
+ mailboxOwnerId,
590
+ title: common.title,
591
+ url: common.url,
592
+ snippet: common.snippet,
593
+ },
594
+ };
595
+ }
596
+
597
+ if (normalizedType === "file") {
598
+ const id = textOrEmpty(
599
+ pickFirst(
600
+ resource?.UniqueId,
601
+ resource?.UniqueID,
602
+ resource?.DocId,
603
+ result?.Id,
604
+ ),
605
+ );
606
+ const summary = textOrEmpty(resource?.Summary || "");
607
+ const webUrl = textOrEmpty(
608
+ pickFirst(
609
+ resource?.WebUrl,
610
+ resource?.WebLink,
611
+ resource?.ServerRedirectedUrl,
612
+ resource?.DefaultEncodingURL,
613
+ common.url,
614
+ ),
615
+ );
616
+ const parsedSize = Number(resource?.Size);
617
+ const size =
618
+ Number.isFinite(parsedSize) && parsedSize > 0 ? parsedSize : undefined;
619
+ return {
620
+ ...common,
621
+ author: textOrEmpty(
622
+ resource?.ModifiedBy || resource?.DisplayAuthor || "",
623
+ ),
624
+ fileType: textOrEmpty(
625
+ resource?.FileType || resource?.FileExtension || "",
626
+ ),
627
+ preview: summary.slice(0, 2300),
628
+ _readContext: {
629
+ type: "file",
630
+ id,
631
+ webUrl,
632
+ size,
633
+ title: common.title,
634
+ url: common.url,
635
+ snippet: common.snippet,
636
+ // TODO(v0.2): avoid returning large summaries by introducing a server-side context cache key.
637
+ summary: summary.slice(0, 12000),
638
+ metadata: {
639
+ author: textOrEmpty(
640
+ resource?.ModifiedBy || resource?.DisplayAuthor || "",
641
+ ),
642
+ fileType: textOrEmpty(
643
+ resource?.FileType || resource?.FileExtension || "",
644
+ ),
645
+ path: textOrEmpty(resource?.Path || ""),
646
+ size: size || null,
647
+ },
648
+ },
649
+ };
650
+ }
651
+
652
+ if (normalizedType === "external") {
653
+ const id = textOrEmpty(
654
+ pickFirst(result?.Id, resource?.FileID, resource?.Url),
655
+ );
656
+ const summary = textOrEmpty(resource?.Summary || "");
657
+ return {
658
+ ...common,
659
+ author: textOrEmpty(
660
+ resource?.label_LastModifiedBy || resource?.Author || "",
661
+ ),
662
+ preview: summary.slice(0, 2300),
663
+ _readContext: {
664
+ type: "external",
665
+ id,
666
+ title: common.title,
667
+ url: common.url,
668
+ snippet: common.snippet,
669
+ // TODO(v0.2): avoid returning large summaries by introducing a server-side context cache key.
670
+ summary: summary.slice(0, 12000),
671
+ metadata: {
672
+ state: textOrEmpty(resource?.Label_State || ""),
673
+ container: textOrEmpty(resource?.Label_ContainerName || ""),
674
+ author: textOrEmpty(
675
+ resource?.label_LastModifiedBy || resource?.Author || "",
676
+ ),
677
+ },
678
+ },
679
+ };
680
+ }
681
+
682
+ if (normalizedType === "chat") {
683
+ const fromAddress = safeEmailAddress(resource?.From || resource?.Sender);
684
+ const preview = textOrEmpty(resource?.Preview || "");
685
+ const id = textOrEmpty(pickFirst(resource?.ItemRestId, result?.Id));
686
+ return {
687
+ ...common,
688
+ from: textOrEmpty(fromAddress),
689
+ teamName: textOrEmpty(resource?.TeamName || ""),
690
+ channelName: textOrEmpty(resource?.ChannelName || ""),
691
+ preview,
692
+ _readContext: {
693
+ type: "chat",
694
+ id,
695
+ title: common.title,
696
+ url: common.url,
697
+ snippet: common.snippet,
698
+ preview,
699
+ metadata: {
700
+ from: textOrEmpty(fromAddress),
701
+ teamName: textOrEmpty(resource?.TeamName || ""),
702
+ channelName: textOrEmpty(resource?.ChannelName || ""),
703
+ },
704
+ },
705
+ };
706
+ }
707
+
708
+ if (normalizedType === "event") {
709
+ // Substrate returns flat fields for events: Start/End as ISO strings,
710
+ // OrganizerName/OrganizerAddress as flat strings, NumberOfAttendees as a count.
711
+ const id = textOrEmpty(
712
+ pickFirst(
713
+ resource?.EventId?.Id,
714
+ resource?.OriginalId,
715
+ resource?.ImmutableId,
716
+ resource?.Id,
717
+ result?.Id,
718
+ ),
719
+ );
720
+ const calendarOwnerId = ownerIdentity(resource, "event");
721
+ const organizer = textOrEmpty(
722
+ resource?.OrganizerAddress || resource?.OrganizerName || "",
723
+ );
724
+ // Substrate returns Start/End as plain ISO strings, not { DateTime: "..." } objects.
725
+ const startTime = textOrEmpty(
726
+ typeof resource?.Start === "string"
727
+ ? resource.Start
728
+ : resource?.Start?.DateTime || "",
729
+ );
730
+ const endTime = textOrEmpty(
731
+ typeof resource?.End === "string"
732
+ ? resource.End
733
+ : resource?.End?.DateTime || "",
734
+ );
735
+ const meetingUrl = textOrEmpty(
736
+ resource?.SkypeTeamsMeetingUrl ||
737
+ resource?.MeetingUrl ||
738
+ resource?.OnlineMeetingUrl ||
739
+ "",
740
+ );
741
+
742
+ return {
743
+ ...common,
744
+ date: textOrEmpty(pickFirst(startTime, common.date)),
745
+ subject: textOrEmpty(resource?.Subject || common.title),
746
+ location: textOrEmpty(
747
+ resource?.Location?.DisplayName || resource?.Location || "",
748
+ ),
749
+ startTime,
750
+ endTime,
751
+ organizer,
752
+ organizerName: textOrEmpty(resource?.OrganizerName || ""),
753
+ isAllDay: Boolean(resource?.IsAllDay),
754
+ isCancelled: Boolean(resource?.IsCancelled),
755
+ showAs: textOrEmpty(resource?.ShowAs || ""),
756
+ _readContext: {
757
+ type: "event",
758
+ id,
759
+ calendarOwnerId,
760
+ title: common.title,
761
+ url: common.url,
762
+ snippet: common.snippet,
763
+ metadata: {
764
+ subject: textOrEmpty(resource?.Subject || ""),
765
+ location: textOrEmpty(
766
+ resource?.Location?.DisplayName || resource?.Location || "",
767
+ ),
768
+ startTime,
769
+ endTime,
770
+ organizer,
771
+ organizerName: textOrEmpty(resource?.OrganizerName || ""),
772
+ meetingUrl,
773
+ isAllDay: Boolean(resource?.IsAllDay),
774
+ isCancelled: Boolean(resource?.IsCancelled),
775
+ showAs: textOrEmpty(resource?.ShowAs || ""),
776
+ numberOfAttendees: Number(resource?.NumberOfAttendees) || 0,
777
+ importance: textOrEmpty(resource?.Importance || ""),
778
+ },
779
+ },
780
+ };
781
+ }
782
+
783
+ // Unknown types are ignored by the tool surface.
784
+ return null;
785
+ }
786
+
787
+ function buildSearchHeaders(tokenEntry) {
788
+ const mailbox = `Oid:${tokenEntry.oid}@${tokenEntry.tid}`;
789
+ return {
790
+ Authorization: `Bearer ${tokenEntry.token}`,
791
+ "Content-Type": "application/system.text.json",
792
+ "X-Anchormailbox": mailbox,
793
+ "X-Routingparameter-Sessionkey": mailbox,
794
+ "x-scenario": "aihub.search",
795
+ "X-Client-Flights": MIN_FLIGHTS,
796
+ "X-Client-LocalTime": new Date().toISOString(),
797
+ "X-CallingContext": "SERPSearchbox",
798
+ "X-CafeLatencyHeaderEnabled": "1",
799
+ "Client-Request-id": cryptoUuid(),
800
+ };
801
+ }
802
+
803
+ export class SearchApi {
804
+ /**
805
+ * @param {import("./token-manager.js").TokenManager} tokenManager
806
+ * @param {{ onAuthError?: (() => void | Promise<void>) | null }} [options]
807
+ */
808
+ constructor(tokenManager, options = {}) {
809
+ this.tokenManager = tokenManager;
810
+ this.onAuthError = options.onAuthError || null;
811
+ }
812
+
813
+ async search(params = {}) {
814
+ const query = String(params.query || "").trim();
815
+ if (!query) {
816
+ throw new Error("query is required");
817
+ }
818
+
819
+ const connector = params.connector ? String(params.connector).trim() : "";
820
+ const source = connector
821
+ ? "external"
822
+ : String(params.source || "all")
823
+ .trim()
824
+ .toLowerCase();
825
+ const size = Number.isInteger(params.size) ? params.size : 10;
826
+ const from = Number.isInteger(params.from) ? params.from : 0;
827
+
828
+ const tokenEntry = this.tokenManager.getTokenEntry(
829
+ TOKEN_AUDIENCES.SUBSTRATE,
830
+ );
831
+ if (!tokenEntry) {
832
+ throw new Error("Substrate token missing or expired");
833
+ }
834
+
835
+ const tz = getTimeZoneInfo();
836
+ const entityRequests = buildEntityRequests(source, connector);
837
+ const body = {
838
+ ResponseMetadata: ["SearchTerms", "WholePageRankings"],
839
+ EntityRequests: entityRequests,
840
+ RequestOptions: {
841
+ Query: { QueryString: query },
842
+ Sort: { Field: "Relevance", SortDirection: "Desc" },
843
+ From: from,
844
+ Size: size,
845
+ ComputedFields: [
846
+ {
847
+ ComputationType: "ContentExtraction",
848
+ ExtractionType: "Caption",
849
+ MaxExtractLength: 200,
850
+ OutputFieldName: "HitHighlightedSummary",
851
+ },
852
+ ],
853
+ SemanticLabels: [
854
+ "Label_Source",
855
+ "Label_Title",
856
+ "Label_Url",
857
+ "Label_LastModifiedBy",
858
+ "Label_LastModifiedDateTime",
859
+ "Label_Authors",
860
+ "Label_Type",
861
+ "Label_Id",
862
+ "Label_CreatedDateTime",
863
+ "Label_CreatedBy",
864
+ "Label_SubTypes",
865
+ ],
866
+ },
867
+ DebugOptions: "Full",
868
+ Cvid: cryptoUuid(),
869
+ LogicalId: cryptoUuid(),
870
+ WholePageRankingOptions: {
871
+ SupportedRankingVersion: "V3",
872
+ MultiEntityMerge: [
873
+ {
874
+ EntityTypes: ["File", "External", "Email", "Chat", "Event"],
875
+ MergeInline: true,
876
+ From: from,
877
+ Size: size,
878
+ },
879
+ ],
880
+ EnableEnrichedRanking: true,
881
+ EnableLayoutHints: true,
882
+ SupportedSerpRegions: ["ContextRegion"],
883
+ EntityResultTypeRankingOptions: [
884
+ {
885
+ ResultType: "Answer",
886
+ MaxEntitySetCount: 6,
887
+ },
888
+ ],
889
+ },
890
+ QueryAlterationOptions: {
891
+ EnableSuggestion: true,
892
+ EnableAlteration: true,
893
+ SupportedRecourseDisplayTypes: [
894
+ "Suggestion",
895
+ "Modification",
896
+ "NoRequeryModification",
897
+ "ServiceSideRecourseLink",
898
+ ],
899
+ },
900
+ AggregationRequests: buildAggregationRequests(source, connector),
901
+ TimeZone: tz.timeZone,
902
+ TimeZoneOffsetInMinutes: tz.offset,
903
+ };
904
+
905
+ const headers = buildSearchHeaders(tokenEntry);
906
+
907
+ const response = await fetch(SEARCH_ENDPOINT, {
908
+ method: "POST",
909
+ headers,
910
+ body: JSON.stringify(body),
911
+ });
912
+
913
+ if (!response.ok) {
914
+ const text = await response.text();
915
+ const err = new Error(
916
+ `Substrate search failed (${response.status}): ${text.slice(0, 500)}`,
917
+ );
918
+ err.status = response.status;
919
+
920
+ // When the server rejects the token, evict it so the UI shows the real
921
+ // state ("Missing" instead of "Valid") and notify the caller for auto-refresh.
922
+ if (response.status === 401) {
923
+ await this.tokenManager.evictToken(
924
+ TOKEN_AUDIENCES.SUBSTRATE,
925
+ tokenEntry.token,
926
+ );
927
+ if (this.onAuthError) {
928
+ try {
929
+ await this.onAuthError();
930
+ } catch (_e) {
931
+ // Best-effort; preserve the original search failure.
932
+ }
933
+ }
934
+ }
935
+
936
+ throw err;
937
+ }
938
+
939
+ const payload = await response.json();
940
+ return this.normalizeSearchResponse(payload, from, size);
941
+ }
942
+
943
+ normalizeSearchResponse(payload, from, size) {
944
+ const groups = Array.isArray(payload?.ResultSetGroups)
945
+ ? payload.ResultSetGroups
946
+ : [];
947
+ const results = [];
948
+
949
+ let totalEstimatedMatches = 0;
950
+ let moreAvailable = false;
951
+
952
+ for (const group of groups) {
953
+ const resultSets = Array.isArray(group?.ResultSets)
954
+ ? group.ResultSets
955
+ : [];
956
+ const entityType = Array.isArray(group?.Types)
957
+ ? group.Types[0]
958
+ : resultSets[0]?.Types?.[0];
959
+
960
+ for (const set of resultSets) {
961
+ totalEstimatedMatches += Number(set?.TotalEstimatedMatches || 0);
962
+ moreAvailable = moreAvailable || Boolean(set?.MoreResultsAvailable);
963
+
964
+ const rawResults = Array.isArray(set?.Results) ? set.Results : [];
965
+ for (const raw of rawResults) {
966
+ const normalized = normalizeResult(raw, entityType, results.length);
967
+ if (normalized) {
968
+ results.push(normalized);
969
+ }
970
+ }
971
+ }
972
+ }
973
+
974
+ return {
975
+ results,
976
+ totalEstimatedMatches,
977
+ moreAvailable,
978
+ nextFrom: from + Math.max(size, results.length),
979
+ warnings: [],
980
+ };
981
+ }
982
+ }
983
+
984
+ export const _testable = {
985
+ stripTags,
986
+ normalizeResult,
987
+ ownerIdentity,
988
+ safeEmailAddress,
989
+ };