samlesa 3.3.7 → 3.4.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.
@@ -1,6 +1,6 @@
1
1
  import { select } from 'xpath';
2
- import { uniq, last, zipObject, notEmpty } from './utility.js';
3
- import { getContext } from './api.js';
2
+ import { uniq, last, zipObject, notEmpty } from './utility.js'; // 假设这些工具函数存在
3
+ import { getContext } from './api.js'; // 假设这个API存在
4
4
  import camelCase from 'camelcase';
5
5
  function buildAbsoluteXPath(paths) {
6
6
  if (!paths || paths.length === 0)
@@ -28,14 +28,85 @@ function buildAttributeXPath(attributes) {
28
28
  const filters = attributes.map(attribute => `name()='${attribute}'`).join(' or ');
29
29
  return `/@*[${filters}]`;
30
30
  }
31
- // ... (其他字段配置如 loginRequestFields 等保持不变,为节省篇幅此处省略,请保留你原有的所有 fields 定义) ...
32
- // 为了完整性,这里假设你保留了之前所有的 fields 定义 (loginRequestFields, idpMetadataFields 等)
33
- // 重点修正下方的 spMetadataFields 和 extract 函数
34
31
  export const loginRequestFields = [
35
- { key: 'request', localPath: ['AuthnRequest'], attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL', 'ProtocolBinding', 'ForceAuthn', 'IsPassive', 'AssertionConsumerServiceIndex', 'AttributeConsumingServiceIndex'] },
36
- { key: 'issuer', localPath: ['AuthnRequest', 'Issuer'], attributes: [] },
37
- { key: 'nameIDPolicy', localPath: ['AuthnRequest', 'NameIDPolicy'], attributes: ['Format', 'AllowCreate'] },
38
- { key: 'authnContextClassRef', localPath: ['AuthnRequest', 'AuthnContextClassRef'], attributes: [] },
32
+ // --- 1. AuthnRequest 根元素增强 ---
33
+ {
34
+ key: 'request',
35
+ localPath: ['AuthnRequest'],
36
+ attributes: [
37
+ 'ID',
38
+ 'Version', // [新增] 版本号
39
+ 'IssueInstant',
40
+ 'Destination',
41
+ 'Consent', // [新增] 用户同意状态
42
+ 'AssertionConsumerServiceURL',
43
+ 'ProtocolBinding',
44
+ 'ForceAuthn',
45
+ 'IsPassive',
46
+ 'AssertionConsumerServiceIndex',
47
+ 'AttributeConsumingServiceIndex',
48
+ 'ProviderName' // [新增] SP 显示名称
49
+ ]
50
+ },
51
+ // --- 2. Issuer ---
52
+ { key: 'issuer', localPath: ['AuthnRequest', 'Issuer'], attributes: [] }, // [增强] 提取 Issuer 的属性
53
+ // --- 3. NameIDPolicy 增强 ---
54
+ {
55
+ key: 'nameIDPolicy',
56
+ localPath: ['AuthnRequest', 'NameIDPolicy'],
57
+ attributes: [
58
+ 'Format',
59
+ 'AllowCreate',
60
+ 'SPNameQualifier' // [新增] 关键属性,指定 NameID 的作用域
61
+ ]
62
+ },
63
+ // --- 4. RequestedAuthnContext [全新添加] ---
64
+ // 提取比较策略
65
+ {
66
+ key: 'requestedAuthnContextComparison',
67
+ localPath: ['AuthnRequest', 'RequestedAuthnContext'],
68
+ attributes: ['Comparison']
69
+ },
70
+ // 提取要求的认证类列表 (listMode 因为可能有多个 ClassRef)
71
+ {
72
+ key: 'requestedAuthnContextClasses',
73
+ localPath: ['AuthnRequest', 'RequestedAuthnContext', 'AuthnContextClassRef'],
74
+ attributes: [],
75
+ listMode: true
76
+ },
77
+ // 提取认证声明引用 (较少用,但为了完整性)
78
+ {
79
+ key: 'requestedAuthnContextDeclRefs',
80
+ localPath: ['AuthnRequest', 'RequestedAuthnContext', 'AuthnContextDeclRef'],
81
+ attributes: [],
82
+ listMode: true
83
+ },
84
+ // --- 5. Scoping (用于 IdP 代理场景) [全新添加] ---
85
+ {
86
+ key: 'scoping',
87
+ localPath: ['AuthnRequest', 'Scoping'],
88
+ attributes: ['ProxyCount']
89
+ },
90
+ {
91
+ key: 'requesterIDs',
92
+ localPath: ['AuthnRequest', 'Scoping', 'RequesterID'],
93
+ attributes: [],
94
+ listMode: true
95
+ },
96
+ {
97
+ key: 'idpListEntries',
98
+ localPath: ['AuthnRequest', 'Scoping', 'IDPList', 'IDPEntry'],
99
+ attributes: ['ProviderID', 'Name', 'Loc'],
100
+ listMode: true
101
+ },
102
+ // --- 6. Extensions (捕获自定义扩展) ---
103
+ {
104
+ key: 'extensions',
105
+ localPath: ['AuthnRequest', 'Extensions'],
106
+ attributes: [],
107
+ context: true // 获取整个 XML 片段
108
+ },
109
+ // --- 7. Signature ---
39
110
  { key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
40
111
  ];
41
112
  export const artifactResolveFields = [
@@ -74,6 +145,201 @@ export const loginResponseFields = assertion => [
74
145
  { key: 'oneTimeUse', localPath: ['Assertion', 'Conditions', 'OneTimeUse'], attributes: [], shortcut: assertion },
75
146
  { key: 'status', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
76
147
  ];
148
+ /*export const loginResponseFieldsFullList: ExtractorFields = [
149
+ // ==========================================================================
150
+ // 1. Response 根元素
151
+ // ==========================================================================
152
+ {
153
+ key: 'response',
154
+ localPath: ['Response'],
155
+ attributes: [
156
+ 'ID',
157
+ 'Version',
158
+ 'IssueInstant',
159
+ 'Destination',
160
+ 'InResponseTo',
161
+ 'Consent',
162
+ 'RelayState'
163
+ ]
164
+ },
165
+ // ==========================================================================
166
+ // 2. Status 详细信息
167
+ // ==========================================================================
168
+ {
169
+ key: 'statusTopCode',
170
+ localPath: ['Response', 'Status', 'StatusCode'],
171
+ attributes: ['Value']
172
+ },
173
+ {
174
+ key: 'statusSecondCode',
175
+ localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'],
176
+ attributes: ['Value']
177
+ },
178
+ {
179
+ key: 'statusMessage',
180
+ localPath: ['Response', 'Status', 'StatusMessage'],
181
+ attributes: [],
182
+ // 提示:如果 StatusMessage 有文本内容但 attributes 为空导致结果为空,
183
+ // 可能需要解析器支持提取 textContent。如果解析器仅支持属性,此处保持为空,结果可能为空字符串。
184
+ },
185
+ {
186
+ key: 'statusDetail',
187
+ localPath: ['Response', 'Status', 'StatusDetail'],
188
+ attributes: [],
189
+ context: true // 保持返回完整 XML 片段,除非解析器支持递归子字段定义
190
+ },
191
+ // ==========================================================================
192
+ // 3. Assertion 元数据
193
+ // ==========================================================================
194
+ {
195
+ key: 'assertionID',
196
+ localPath: ['Response', 'Assertion'],
197
+ attributes: ['ID']
198
+ },
199
+ {
200
+ key: 'assertionVersion',
201
+ localPath: ['Response', 'Assertion'],
202
+ attributes: ['Version']
203
+ },
204
+ {
205
+ key: 'assertionIssueInstant',
206
+ localPath: ['Response', 'Assertion'],
207
+ attributes: ['IssueInstant']
208
+ },
209
+ {
210
+ key: 'assertionSignature',
211
+ localPath: ['Response', 'Assertion', 'Signature'],
212
+ attributes: [],
213
+ context: true
214
+ },
215
+ // ==========================================================================
216
+ // 4. Conditions
217
+ // ==========================================================================
218
+ {
219
+ key: 'conditions',
220
+ localPath: ['Response', 'Assertion', 'Conditions'],
221
+ attributes: ['NotBefore', 'NotOnOrAfter']
222
+ },
223
+ {
224
+ key: 'audiences',
225
+ localPath: ['Response', 'Assertion', 'Conditions', 'AudienceRestriction', 'Audience'],
226
+ attributes: [],
227
+ listMode: true,
228
+ // 注意:Audience 通常只有文本内容,没有属性。如果解析器只提属性,这里会是空字符串。
229
+ // 如果解析器支持,它应该自动抓取 textContent。
230
+ },
231
+ {
232
+ key: 'oneTimeUse',
233
+ localPath: ['Response', 'Assertion', 'Conditions', 'OneTimeUse'],
234
+ attributes: []
235
+ // OneTimeUse 通常是一个空标签 <OneTimeUse/>,没有属性。
236
+ },
237
+ {
238
+ key: 'proxyRestriction',
239
+ localPath: ['Response', 'Assertion', 'Conditions', 'ProxyRestriction'],
240
+ attributes: ['Count']
241
+ },
242
+ {
243
+ key: 'proxyRestrictionAudiences',
244
+ localPath: ['Response', 'Assertion', 'Conditions', 'ProxyRestriction', 'Audience'],
245
+ attributes: [],
246
+ listMode: true
247
+ },
248
+ // ==========================================================================
249
+ // 5. Subject & NameID
250
+ // ==========================================================================
251
+ {
252
+ key: 'nameID',
253
+ localPath: ['Response', 'Assertion', 'Subject', 'NameID'],
254
+ attributes: [
255
+ 'Format',
256
+ 'NameQualifier',
257
+ 'SPNameQualifier',
258
+ 'SPProvidedID'
259
+ ]
260
+ },
261
+ {
262
+ key: 'subjectConfirmation',
263
+ localPath: ['Response', 'Assertion', 'Subject', 'SubjectConfirmation', 'SubjectConfirmationData'],
264
+ attributes: [
265
+ 'Recipient',
266
+ 'InResponseTo',
267
+ 'NotOnOrAfter',
268
+ 'Address',
269
+ 'NotBefore'
270
+ ]
271
+ },
272
+ {
273
+ key: 'subjectBaseID',
274
+ localPath: ['Response', 'Assertion', 'Subject', 'BaseID'],
275
+ attributes: [], // BaseID 主要靠属性,如果这里有文本需求需额外处理
276
+ context: true
277
+ },
278
+ // ==========================================================================
279
+ // 6. AuthnStatement
280
+ // ==========================================================================
281
+ {
282
+ key: 'sessionIndex',
283
+ localPath: ['Response', 'Assertion', 'AuthnStatement'],
284
+ attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex']
285
+ },
286
+ {
287
+ key: 'authnContextClassRef',
288
+ localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthnContextClassRef'],
289
+ attributes: []
290
+ // AuthnContextClassRef 通常是文本内容 (urn:...), 没有属性。
291
+ // 如果解析结果为空,说明解析器未提取文本内容。
292
+ },
293
+ {
294
+ key: 'authnContextDecl',
295
+ localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthnContextDecl'],
296
+ attributes: [],
297
+ context: true
298
+ },
299
+ {
300
+ key: 'authenticatingAuthorities',
301
+ localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthenticatingAuthority'],
302
+ attributes: [],
303
+ listMode: true
304
+ // AuthenticatingAuthority 通常也是文本内容。
305
+ },
306
+ // ==========================================================================
307
+ // 7. AttributeStatement (重点修复区域)
308
+ // ==========================================================================
309
+ {
310
+ key: 'attributes',
311
+ localPath: ['Response', 'Assertion', 'AttributeStatement', 'Attribute'],
312
+ index: ['Name'], // 使用 Name 属性作为 Key
313
+ attributePath: ['AttributeValue'], // 指向值的节点
314
+ attributes: [],
315
+ // 【关键修改】:
316
+ // 很多解析器在 attributePath 层级如果只配置 attributes 数组,会忽略文本内容。
317
+ // 如果可能,尝试添加一个特殊标记或确保解析器逻辑包含:
318
+ // if (node has text && no attributes matched) return text;
319
+ // 如果无法修改解析器内核,且 AttributeValue 只有文本没有属性,
320
+ // 这里的解析结果可能依然为空。
321
+ //
322
+ // 高级技巧:如果解析器支持,可以尝试将 '_' 或 'value' 加入 attributes 数组,
323
+ // 但这取决于 extractSpToll 的具体实现。
324
+ // 在此配置中,我们保持标准写法,依赖解析器对 attributePath 的默认文本提取。
325
+ },
326
+ {
327
+ key: 'encryptedAttributes',
328
+ localPath: ['Response', 'Assertion', 'AttributeStatement', 'EncryptedAttribute'],
329
+ attributes: [],
330
+ context: true
331
+ },
332
+
333
+ // ==========================================================================
334
+ // 9. Response Signature
335
+ // ==========================================================================
336
+ {
337
+ key: 'responseSignature',
338
+ localPath: ['Response', 'Signature'],
339
+ attributes: [],
340
+ context: true
341
+ }
342
+ ];*/
77
343
  export const logoutRequestFields = [
78
344
  { key: 'request', localPath: ['LogoutRequest'], attributes: ['ID', 'IssueInstant', 'Destination'] },
79
345
  { key: 'issuer', localPath: ['LogoutRequest', 'Issuer'], attributes: [] },
@@ -209,26 +475,185 @@ export const idpMetadataFields = [
209
475
  }
210
476
  ];
211
477
  // ============================================================================
212
- // 修正后的 SP 元数据提取字段配置
478
+ // SAML2 SP 元数据完整字段配置 - 包含所有可选属性
213
479
  // ============================================================================
214
480
  export const spMetadataFields = [
215
- { key: 'entityID', localPath: ['EntityDescriptor'], attributes: ['entityID'] },
216
- { key: 'spSSODescriptor', localPath: ['EntityDescriptor', 'SPSSODescriptor'], attributes: ['protocolSupportEnumeration', 'AuthnRequestsSigned', 'WantAssertionsSigned'] },
217
- { key: 'assertionConsumerService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'], attributes: ['Binding', 'Location', 'index', 'isDefault'], listMode: true },
218
- { key: 'singleLogoutService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'SingleLogoutService'], attributes: ['Binding', 'Location'], listMode: true },
219
- { key: 'artifactResolutionService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'], attributes: ['Binding', 'Location', 'index', 'isDefault'], listMode: true },
220
- { key: 'manageNameIDService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ManageNameIDService'], attributes: ['Binding', 'Location'], listMode: true },
221
- { key: 'nameIDFormat', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDFormat'], attributes: [] },
222
- { key: 'organizationName', localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'], attributes: [] },
223
- { key: 'organizationDisplayName', localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'], attributes: [] },
224
- { key: 'organizationURL', localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'], attributes: [] },
225
- { key: 'contactPerson', localPath: ['EntityDescriptor', 'ContactPerson'], attributes: ['contactType'], listMode: true },
226
- // 特殊字段:触发 extract 内部的硬编码逻辑
227
- // localPath 和 attributes 在这里不起作用,仅作为占位符
481
+ // --- 1. 基础标识 ---
482
+ {
483
+ key: 'entityID',
484
+ localPath: ['EntityDescriptor'],
485
+ attributes: ['entityID']
486
+ },
487
+ {
488
+ key: 'validUntil',
489
+ localPath: ['EntityDescriptor'],
490
+ attributes: ['validUntil']
491
+ },
492
+ {
493
+ key: 'cacheDuration',
494
+ localPath: ['EntityDescriptor'],
495
+ attributes: ['cacheDuration']
496
+ },
497
+ {
498
+ key: 'ID',
499
+ localPath: ['EntityDescriptor'],
500
+ attributes: ['ID']
501
+ },
502
+ {
503
+ key: 'extensions',
504
+ localPath: ['EntityDescriptor', 'Extensions'],
505
+ attributes: [],
506
+ context: true
507
+ },
508
+ // --- 2. SPSSODescriptor 核心属性 ---
509
+ {
510
+ key: 'spSSODescriptor',
511
+ localPath: ['EntityDescriptor', 'SPSSODescriptor'],
512
+ attributes: [
513
+ 'protocolSupportEnumeration',
514
+ 'AuthnRequestsSigned', // SP 是否会对认证请求签名
515
+ 'WantAssertionsSigned', // SP 是否希望接收签名的断言
516
+ 'cacheDuration',
517
+ 'validUntil',
518
+ 'ID'
519
+ ]
520
+ },
521
+ {
522
+ key: 'extensions',
523
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'Extensions'],
524
+ attributes: [],
525
+ context: true
526
+ },
527
+ // --- 3. 服务端点列表 (Endpoints) ---
528
+ {
529
+ key: 'assertionConsumerService',
530
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
531
+ attributes: ['Binding', 'Location', 'index', 'isDefault', 'ResponseLocation'],
532
+ listMode: true
533
+ },
534
+ {
535
+ key: 'singleLogoutService',
536
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'SingleLogoutService'],
537
+ attributes: ['Binding', 'Location', 'index', 'isDefault', 'ResponseLocation'],
538
+ listMode: true
539
+ },
540
+ {
541
+ key: 'artifactResolutionService',
542
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
543
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
544
+ listMode: true
545
+ },
546
+ {
547
+ key: 'manageNameIDService',
548
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ManageNameIDService'],
549
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
550
+ listMode: true
551
+ },
552
+ {
553
+ key: 'nameIDMappingService',
554
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDMappingService'],
555
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
556
+ listMode: true
557
+ },
558
+ {
559
+ key: 'assertionIDRequestService',
560
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionIDRequestService'],
561
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
562
+ listMode: true
563
+ },
564
+ {
565
+ key: 'attributeService',
566
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeService'],
567
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
568
+ listMode: true
569
+ },
570
+ // --- 4. NameID 格式 ---
571
+ {
572
+ key: 'nameIDFormat',
573
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDFormat'],
574
+ attributes: [],
575
+ listMode: true
576
+ },
577
+ {
578
+ key: 'nameIDPolicy',
579
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDPolicy'],
580
+ attributes: ['Format', 'AllowCreate']
581
+ },
582
+ // --- 5. 属性消费服务 (AttributeConsumingService) ---
583
+ {
584
+ key: 'attributeConsumingService',
585
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService'],
586
+ attributes: ['index', 'isDefault', 'Name', 'NameFormat'],
587
+ listMode: true
588
+ },
589
+ {
590
+ key: 'serviceName',
591
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'ServiceName'],
592
+ attributes: [],
593
+ listMode: true
594
+ },
595
+ {
596
+ key: 'serviceDescription',
597
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'ServiceDescription'],
598
+ attributes: [],
599
+ listMode: true
600
+ },
601
+ {
602
+ key: 'requestedAttribute',
603
+ localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'RequestedAttribute'],
604
+ attributes: ['Name', 'NameFormat', 'FriendlyName', 'isRequired'],
605
+ listMode: true
606
+ },
607
+ // --- 6. 证书与密钥信息 ---
228
608
  { key: 'signingCert' },
229
609
  { key: 'encryptCert' },
230
610
  { key: 'signingKeyName' },
231
- { key: 'encryptionKeyName' }
611
+ { key: 'encryptionKeyName' },
612
+ // --- 7. 组织信息 ---
613
+ {
614
+ key: 'organizationName',
615
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'],
616
+ attributes: [],
617
+ listMode: true
618
+ },
619
+ {
620
+ key: 'organizationDisplayName',
621
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'],
622
+ attributes: [],
623
+ listMode: true
624
+ },
625
+ {
626
+ key: 'organizationURL',
627
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'],
628
+ attributes: [],
629
+ listMode: true
630
+ },
631
+ // --- 8. 联系人信息 ---
632
+ {
633
+ key: 'contactPerson',
634
+ localPath: ['EntityDescriptor', 'ContactPerson'],
635
+ attributes: ['contactType', 'company', 'givenName', 'surName', 'emailAddress', 'telephoneNumber'],
636
+ listMode: true
637
+ },
638
+ {
639
+ key: 'additionalMetadataLocation',
640
+ localPath: ['EntityDescriptor', 'AdditionalMetadataLocation'],
641
+ attributes: ['namespace'],
642
+ listMode: true
643
+ },
644
+ // --- 9. 其他可选元素 ---
645
+ {
646
+ key: 'affiliationDescriptor',
647
+ localPath: ['EntityDescriptor', 'AffiliationDescriptor'],
648
+ attributes: ['affiliationOwnerID', 'validUntil', 'cacheDuration', 'ID'],
649
+ context: true
650
+ },
651
+ {
652
+ key: 'roleDescriptor',
653
+ localPath: ['EntityDescriptor', 'RoleDescriptor'],
654
+ attributes: ['protocolSupportEnumeration', 'errorURL', 'cacheDuration', 'validUntil', 'ID'],
655
+ context: true
656
+ }
232
657
  ];
233
658
  export function extract(context, fields) {
234
659
  const { dom } = getContext();
@@ -419,9 +844,642 @@ export function extract(context, fields) {
419
844
  return result;
420
845
  }, {});
421
846
  }
847
+ // 按照 SAML 2.0 Response 结构分类字段配置
848
+ // 按照 SAML 2.0 Response 结构分类字段配置,优化聚合
849
+ export const loginResponseFieldsFullList = [
850
+ // ==========================================================================
851
+ // 1. Response 根元素
852
+ // ==========================================================================
853
+ {
854
+ key: 'response',
855
+ localPath: ['Response'],
856
+ attributes: [
857
+ 'ID',
858
+ 'Version',
859
+ 'IssueInstant',
860
+ 'Destination',
861
+ 'InResponseTo',
862
+ 'Consent',
863
+ 'RelayState'
864
+ ]
865
+ },
866
+ // ==========================================================================
867
+ // 2. Status 详细信息
868
+ // ==========================================================================
869
+ {
870
+ key: 'status',
871
+ localPath: ['Response', 'Status'],
872
+ attributes: [],
873
+ context: true,
874
+ group: 'status',
875
+ parseCallback: (node) => {
876
+ if (!node)
877
+ return null;
878
+ const statusData = {};
879
+ // 提取顶级状态码
880
+ const statusCodeNodes = node.getElementsByTagNameNS('*', 'StatusCode');
881
+ if (statusCodeNodes.length > 0) {
882
+ statusData.topCode = statusCodeNodes[0].getAttribute('Value') || '';
883
+ // 检查是否有二级状态码
884
+ if (statusCodeNodes[0].getElementsByTagNameNS('*', 'StatusCode').length > 0) {
885
+ const secondStatusCode = statusCodeNodes[0].getElementsByTagNameNS('*', 'StatusCode')[0];
886
+ statusData.secondCode = secondStatusCode.getAttribute('Value') || '';
887
+ }
888
+ }
889
+ // 提取状态消息
890
+ const statusMessageNodes = node.getElementsByTagNameNS('*', 'StatusMessage');
891
+ if (statusMessageNodes.length > 0) {
892
+ statusData.message = statusMessageNodes[0].textContent?.trim() || '';
893
+ }
894
+ // 提取状态详情
895
+ const statusDetailNodes = node.getElementsByTagNameNS('*', 'StatusDetail');
896
+ if (statusDetailNodes.length > 0) {
897
+ const detailNode = statusDetailNodes[0];
898
+ statusData.detail = detailNode.toString();
899
+ // 解析 StatusDetail 中的子元素
900
+ const extraInfoNodes = detailNode.getElementsByTagNameNS('*', 'ExtraInfo');
901
+ if (extraInfoNodes.length > 0) {
902
+ const sessionDurationNodes = extraInfoNodes[0].getElementsByTagNameNS('*', 'SessionDuration');
903
+ if (sessionDurationNodes.length > 0) {
904
+ statusData.sessionDuration = sessionDurationNodes[0].textContent?.trim() || null;
905
+ }
906
+ const mfaUsedNodes = extraInfoNodes[0].getElementsByTagNameNS('*', 'MFAUsed');
907
+ if (mfaUsedNodes.length > 0) {
908
+ statusData.mfaUsed = mfaUsedNodes[0].textContent?.trim() || null;
909
+ }
910
+ }
911
+ }
912
+ return statusData;
913
+ }
914
+ },
915
+ // ==========================================================================
916
+ // 3. Assertion 元数据
917
+ // ==========================================================================
918
+ {
919
+ key: 'assertion',
920
+ localPath: ['Response', 'Assertion'],
921
+ attributes: [
922
+ 'ID',
923
+ 'Version',
924
+ 'IssueInstant'
925
+ ]
926
+ },
927
+ {
928
+ key: 'assertionSignature',
929
+ localPath: ['Response', 'Assertion', 'Signature'],
930
+ attributes: [],
931
+ context: true,
932
+ parseCallback: (node) => {
933
+ if (!node)
934
+ return null;
935
+ const signatureData = {};
936
+ // 提取签名算法相关信息
937
+ const signatureMethods = node.getElementsByTagNameNS('*', 'SignatureMethod');
938
+ if (signatureMethods.length > 0) {
939
+ signatureData.signatureMethodAlgorithm = signatureMethods[0].getAttribute('Algorithm');
940
+ }
941
+ const canonicalizationMethods = node.getElementsByTagNameNS('*', 'CanonicalizationMethod');
942
+ if (canonicalizationMethods.length > 0) {
943
+ signatureData.canonicalizationMethodAlgorithm = canonicalizationMethods[0].getAttribute('Algorithm');
944
+ }
945
+ const digestMethods = node.getElementsByTagNameNS('*', 'DigestMethod');
946
+ if (digestMethods.length > 0) {
947
+ signatureData.digestMethodAlgorithm = digestMethods[0].getAttribute('Algorithm');
948
+ }
949
+ const digestValues = node.getElementsByTagNameNS('*', 'DigestValue');
950
+ if (digestValues.length > 0) {
951
+ signatureData.digestValue = digestValues[0].textContent?.trim();
952
+ }
953
+ const signatureValues = node.getElementsByTagNameNS('*', 'SignatureValue');
954
+ if (signatureValues.length > 0) {
955
+ signatureData.signatureValue = signatureValues[0].textContent?.trim();
956
+ }
957
+ const certificates = node.getElementsByTagNameNS('*', 'X509Certificate');
958
+ if (certificates.length > 0) {
959
+ signatureData.x509Certificate = certificates[0].textContent?.replace(/\s+/g, '');
960
+ }
961
+ return signatureData;
962
+ }
963
+ },
964
+ // ==========================================================================
965
+ // 4. Conditions
966
+ // ==========================================================================
967
+ {
968
+ key: 'conditions',
969
+ localPath: ['Response', 'Assertion', 'Conditions'],
970
+ attributes: ['NotBefore', 'NotOnOrAfter'],
971
+ context: true,
972
+ group: 'conditions',
973
+ parseCallback: (node) => {
974
+ if (!node)
975
+ return null;
976
+ const conditionsData = {};
977
+ // 基本条件
978
+ conditionsData.notBefore = node.getAttribute('NotBefore');
979
+ conditionsData.notOnOrAfter = node.getAttribute('NotOnOrAfter');
980
+ // Audience Restrictions
981
+ const audienceRestrictions = node.getElementsByTagNameNS('*', 'AudienceRestriction');
982
+ if (audienceRestrictions.length > 0) {
983
+ const audiences = [];
984
+ for (let i = 0; i < audienceRestrictions.length; i++) {
985
+ const audienceNodes = audienceRestrictions[i].getElementsByTagNameNS('*', 'Audience');
986
+ for (let j = 0; j < audienceNodes.length; j++) {
987
+ const audienceValue = audienceNodes[j].textContent?.trim();
988
+ if (audienceValue)
989
+ audiences.push(audienceValue);
990
+ }
991
+ }
992
+ conditionsData.audiences = audiences;
993
+ }
994
+ // One Time Use
995
+ const oneTimeUses = node.getElementsByTagNameNS('*', 'OneTimeUse');
996
+ conditionsData.oneTimeUse = oneTimeUses.length > 0;
997
+ // Proxy Restriction
998
+ const proxyRestrictions = node.getElementsByTagNameNS('*', 'ProxyRestriction');
999
+ if (proxyRestrictions.length > 0) {
1000
+ const proxyRestriction = proxyRestrictions[0];
1001
+ conditionsData.proxyRestriction = proxyRestriction.getAttribute('Count');
1002
+ // Proxy Restriction Audiences
1003
+ const proxyAudiences = [];
1004
+ const proxyAudienceNodes = proxyRestriction.getElementsByTagNameNS('*', 'Audience');
1005
+ for (let i = 0; i < proxyAudienceNodes.length; i++) {
1006
+ const audienceValue = proxyAudienceNodes[i].textContent?.trim();
1007
+ if (audienceValue)
1008
+ proxyAudiences.push(audienceValue);
1009
+ }
1010
+ conditionsData.proxyRestrictionAudiences = proxyAudiences;
1011
+ }
1012
+ return conditionsData;
1013
+ }
1014
+ },
1015
+ // ==========================================================================
1016
+ // 5. Subject & NameID
1017
+ // ==========================================================================
1018
+ {
1019
+ key: 'subject',
1020
+ localPath: ['Response', 'Assertion', 'Subject'],
1021
+ attributes: [],
1022
+ context: true,
1023
+ group: 'subject',
1024
+ parseCallback: (node) => {
1025
+ if (!node)
1026
+ return null;
1027
+ const subjectData = {};
1028
+ // NameID
1029
+ const nameIDNodes = node.getElementsByTagNameNS('*', 'NameID');
1030
+ if (nameIDNodes.length > 0) {
1031
+ const nameIDNode = nameIDNodes[0];
1032
+ subjectData.nameID = {
1033
+ format: nameIDNode.getAttribute('Format'),
1034
+ nameQualifier: nameIDNode.getAttribute('NameQualifier'),
1035
+ spNameQualifier: nameIDNode.getAttribute('SPNameQualifier'),
1036
+ spProvidedID: nameIDNode.getAttribute('SPProvidedID'),
1037
+ value: nameIDNode.textContent?.trim() || ''
1038
+ };
1039
+ }
1040
+ // Subject Confirmation
1041
+ const subjectConfirmations = node.getElementsByTagNameNS('*', 'SubjectConfirmation');
1042
+ if (subjectConfirmations.length > 0) {
1043
+ const confirmationData = {};
1044
+ const confirmationDataNodes = subjectConfirmations[0].getElementsByTagNameNS('*', 'SubjectConfirmationData');
1045
+ if (confirmationDataNodes.length > 0) {
1046
+ const dataNode = confirmationDataNodes[0];
1047
+ confirmationData.recipient = dataNode.getAttribute('Recipient');
1048
+ confirmationData.inResponseTo = dataNode.getAttribute('InResponseTo');
1049
+ confirmationData.notOnOrAfter = dataNode.getAttribute('NotOnOrAfter');
1050
+ confirmationData.address = dataNode.getAttribute('Address');
1051
+ confirmationData.notBefore = dataNode.getAttribute('NotBefore');
1052
+ }
1053
+ subjectData.subjectConfirmation = confirmationData;
1054
+ }
1055
+ // BaseID
1056
+ const baseIDNodes = node.getElementsByTagNameNS('*', 'BaseID');
1057
+ if (baseIDNodes.length > 0) {
1058
+ const baseIDNode = baseIDNodes[0];
1059
+ subjectData.baseID = {
1060
+ nameQualifier: baseIDNode.getAttribute('NameQualifier'),
1061
+ spNameQualifier: baseIDNode.getAttribute('SPNameQualifier'),
1062
+ value: baseIDNode.textContent?.trim() || baseIDNode.toString()
1063
+ };
1064
+ }
1065
+ return subjectData;
1066
+ }
1067
+ },
1068
+ // ==========================================================================
1069
+ // 6. AuthnStatement
1070
+ // ==========================================================================
1071
+ {
1072
+ key: 'authnStatement',
1073
+ localPath: ['Response', 'Assertion', 'AuthnStatement'],
1074
+ attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'],
1075
+ context: true,
1076
+ group: 'authnStatement',
1077
+ parseCallback: (node) => {
1078
+ if (!node)
1079
+ return null;
1080
+ const authnData = {};
1081
+ // 基本认证信息
1082
+ authnData.authnInstant = node.getAttribute('AuthnInstant');
1083
+ authnData.sessionNotOnOrAfter = node.getAttribute('SessionNotOnOrAfter');
1084
+ authnData.sessionIndex = node.getAttribute('SessionIndex');
1085
+ // 认证上下文
1086
+ const authnContextNodes = node.getElementsByTagNameNS('*', 'AuthnContext');
1087
+ if (authnContextNodes.length > 0) {
1088
+ const authnContextNode = authnContextNodes[0];
1089
+ // 认证上下文类引用
1090
+ const classRefNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthnContextClassRef');
1091
+ if (classRefNodes.length > 0) {
1092
+ authnData.authnContextClassRef = classRefNodes[0].textContent?.trim() || '';
1093
+ }
1094
+ // 认证上下文声明
1095
+ const declNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthnContextDecl');
1096
+ if (declNodes.length > 0) {
1097
+ const declNode = declNodes[0];
1098
+ authnData.authnContextDecl = {
1099
+ method: declNode.getElementsByTagNameNS('*', 'Method')[0]?.textContent?.trim() || '',
1100
+ strength: declNode.getElementsByTagNameNS('*', 'Strength')[0]?.textContent?.trim() || ''
1101
+ };
1102
+ }
1103
+ // 认证机构
1104
+ const authorityNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthenticatingAuthority');
1105
+ if (authorityNodes.length > 0) {
1106
+ authnData.authenticatingAuthorities = [];
1107
+ for (let i = 0; i < authorityNodes.length; i++) {
1108
+ const authorityValue = authorityNodes[i].textContent?.trim();
1109
+ if (authorityValue) {
1110
+ authnData.authenticatingAuthorities.push(authorityValue);
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ return authnData;
1116
+ }
1117
+ },
1118
+ // ==========================================================================
1119
+ // 7. AttributeStatement
1120
+ // ==========================================================================
1121
+ {
1122
+ key: 'attributeStatement',
1123
+ localPath: ['Response', 'Assertion', 'AttributeStatement'],
1124
+ attributes: [],
1125
+ context: true,
1126
+ group: 'attributeStatement',
1127
+ parseCallback: (node) => {
1128
+ if (!node)
1129
+ return null;
1130
+ const attributeData = {
1131
+ attributes: {},
1132
+ encryptedAttributes: null
1133
+ };
1134
+ // 提取普通属性
1135
+ const attributeNodes = node.getElementsByTagNameNS('*', 'Attribute');
1136
+ for (let i = 0; i < attributeNodes.length; i++) {
1137
+ const attrNode = attributeNodes[i];
1138
+ const name = attrNode.getAttribute('Name') || attrNode.getAttribute('FriendlyName');
1139
+ if (!name)
1140
+ continue;
1141
+ const values = [];
1142
+ const valueNodes = attrNode.getElementsByTagNameNS('*', 'AttributeValue');
1143
+ for (let j = 0; j < valueNodes.length; j++) {
1144
+ const value = valueNodes[j].textContent?.trim();
1145
+ if (value)
1146
+ values.push(value);
1147
+ }
1148
+ attributeData.attributes[name] = values;
1149
+ }
1150
+ // 提取加密属性
1151
+ const encryptedAttrNodes = node.getElementsByTagNameNS('*', 'EncryptedAttribute');
1152
+ if (encryptedAttrNodes.length > 0) {
1153
+ const encryptedAttrNode = encryptedAttrNodes[0];
1154
+ const encryptionMethods = encryptedAttrNode.getElementsByTagNameNS('*', 'EncryptionMethod');
1155
+ const cipherValues = encryptedAttrNode.getElementsByTagNameNS('*', 'CipherValue');
1156
+ attributeData.encryptedAttributes = {
1157
+ encryptionMethodAlgorithm: encryptionMethods.length > 0 ?
1158
+ encryptionMethods[0].getAttribute('Algorithm') : null,
1159
+ cipherValue: cipherValues.length > 0 ?
1160
+ cipherValues[0].textContent?.trim() : null
1161
+ };
1162
+ }
1163
+ return attributeData;
1164
+ }
1165
+ },
1166
+ // ==========================================================================
1167
+ // 8. Response Signature
1168
+ // ==========================================================================
1169
+ {
1170
+ key: 'responseSignature',
1171
+ localPath: ['Response', 'Signature'],
1172
+ attributes: [],
1173
+ context: true,
1174
+ parseCallback: (node) => {
1175
+ if (!node)
1176
+ return null;
1177
+ const signatureData = {};
1178
+ // 提取签名算法相关信息
1179
+ const signatureMethods = node.getElementsByTagNameNS('*', 'SignatureMethod');
1180
+ if (signatureMethods.length > 0) {
1181
+ signatureData.signatureMethodAlgorithm = signatureMethods[0].getAttribute('Algorithm');
1182
+ }
1183
+ const canonicalizationMethods = node.getElementsByTagNameNS('*', 'CanonicalizationMethod');
1184
+ if (canonicalizationMethods.length > 0) {
1185
+ signatureData.canonicalizationMethodAlgorithm = canonicalizationMethods[0].getAttribute('Algorithm');
1186
+ }
1187
+ const digestMethods = node.getElementsByTagNameNS('*', 'DigestMethod');
1188
+ if (digestMethods.length > 0) {
1189
+ signatureData.digestMethodAlgorithm = digestMethods[0].getAttribute('Algorithm');
1190
+ }
1191
+ const digestValues = node.getElementsByTagNameNS('*', 'DigestValue');
1192
+ if (digestValues.length > 0) {
1193
+ signatureData.digestValue = digestValues[0].textContent?.trim();
1194
+ }
1195
+ const signatureValues = node.getElementsByTagNameNS('*', 'SignatureValue');
1196
+ if (signatureValues.length > 0) {
1197
+ signatureData.signatureValue = signatureValues[0].textContent?.trim();
1198
+ }
1199
+ const certificates = node.getElementsByTagNameNS('*', 'X509Certificate');
1200
+ if (certificates.length > 0) {
1201
+ signatureData.x509Certificate = certificates[0].textContent?.replace(/\s+/g, '');
1202
+ }
1203
+ return signatureData;
1204
+ }
1205
+ }
1206
+ ];
1207
+ /**
1208
+ * 核心提取函数 - 优化版
1209
+ */
1210
+ export function extractSpToll(context, fields) {
1211
+ const { dom } = getContext();
1212
+ if (!context || typeof context !== 'string' || context.trim() === '') {
1213
+ return {};
1214
+ }
1215
+ let rootDoc;
1216
+ try {
1217
+ rootDoc = dom.parseFromString(context, 'application/xml');
1218
+ }
1219
+ catch (e) {
1220
+ console.error('Failed to parse XML context:', e);
1221
+ return {};
1222
+ }
1223
+ if (rootDoc.getElementsByTagName('parsererror').length > 0) {
1224
+ console.error('XML Parse Error detected in context');
1225
+ return {};
1226
+ }
1227
+ return fields.reduce((result, field) => {
1228
+ const key = field.key;
1229
+ const localPath = field.localPath || [];
1230
+ const attributes = field.attributes || [];
1231
+ const isEntire = field.context;
1232
+ const shortcut = field.shortcut;
1233
+ const index = field.index;
1234
+ const attributePath = field.attributePath;
1235
+ const listMode = field.listMode;
1236
+ const parseCallback = field.parseCallback;
1237
+ let targetDoc = rootDoc;
1238
+ if (shortcut) {
1239
+ try {
1240
+ targetDoc = dom.parseFromString(shortcut, 'application/xml');
1241
+ }
1242
+ catch (e) {
1243
+ return result;
1244
+ }
1245
+ }
1246
+ // 特殊处理:证书和 KeyName
1247
+ if (key === 'signingCert' || key === 'encryptCert' || key === 'signingKeyName' || key === 'encryptionKeyName') {
1248
+ const isSigning = key.startsWith('signing');
1249
+ const useType = isSigning ? 'signing' : 'encryption';
1250
+ const isKeyName = key.endsWith('KeyName');
1251
+ const kdXPath = `//*[local-name(.)='KeyDescriptor' and @use='${useType}']`;
1252
+ let fullXPath = '';
1253
+ if (isKeyName) {
1254
+ fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='KeyName']/text()`;
1255
+ }
1256
+ else {
1257
+ fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='X509Data']/*[local-name(.)='X509Certificate']/text()`;
1258
+ }
1259
+ try {
1260
+ // @ts-ignore
1261
+ const nodes = select(fullXPath, targetDoc);
1262
+ if (isKeyName) {
1263
+ const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
1264
+ return { ...result, [key]: keyNames.length > 0 ? keyNames[0] : null };
1265
+ }
1266
+ else {
1267
+ const certs = nodes.map((n) => {
1268
+ const val = n.nodeValue || n.value;
1269
+ return val ? val.replace(/\r\n|\r|\n|\s/g, '') : null;
1270
+ }).filter(notEmpty);
1271
+ return { ...result, [key]: certs.length > 0 ? certs[0] : null };
1272
+ }
1273
+ }
1274
+ catch (e) {
1275
+ return { ...result, [key]: null };
1276
+ }
1277
+ }
1278
+ // 多路径处理
1279
+ if (Array.isArray(localPath) && localPath.length > 0 && Array.isArray(localPath[0])) {
1280
+ const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
1281
+ try {
1282
+ // @ts-ignore
1283
+ const nodes = select(multiXPaths, targetDoc);
1284
+ return { ...result, [key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty)) };
1285
+ }
1286
+ catch (e) {
1287
+ return { ...result, [key]: [] };
1288
+ }
1289
+ }
1290
+ const currentLocalPath = localPath;
1291
+ if (currentLocalPath.length === 0 && !isEntire) {
1292
+ return { ...result, [key]: null };
1293
+ }
1294
+ const baseXPath = buildAbsoluteXPath(currentLocalPath);
1295
+ // 列表模式处理
1296
+ if (listMode) {
1297
+ try {
1298
+ // @ts-ignore
1299
+ const nodes = select(baseXPath, targetDoc);
1300
+ if (parseCallback) {
1301
+ // 使用自定义回调函数处理列表
1302
+ return { ...result, [key]: parseCallback(nodes) };
1303
+ }
1304
+ // 通用列表处理
1305
+ const resultList = nodes.map((node) => {
1306
+ if (attributes.length > 0) {
1307
+ const attrResult = {};
1308
+ attributes.forEach(attr => {
1309
+ if (node.getAttribute) {
1310
+ const val = node.getAttribute(attr);
1311
+ if (val !== null && val !== undefined) {
1312
+ attrResult[camelCase(attr, { locale: 'en-us' })] = val;
1313
+ }
1314
+ }
1315
+ });
1316
+ return attrResult;
1317
+ }
1318
+ else {
1319
+ // 当没有属性时,获取文本内容
1320
+ let text = node.textContent;
1321
+ if (!text && node.firstChild)
1322
+ text = node.firstChild.nodeValue;
1323
+ const trimmed = text ? text.trim() : '';
1324
+ return trimmed;
1325
+ }
1326
+ });
1327
+ return { ...result, [key]: resultList };
1328
+ }
1329
+ catch (e) {
1330
+ console.error(`Error extracting list ${key}:`, e);
1331
+ return { ...result, [key]: [] };
1332
+ }
1333
+ }
1334
+ // 属性聚合 (Index + AttributePath)
1335
+ if (index && attributePath) {
1336
+ try {
1337
+ const indexPath = buildAttributeXPath(index);
1338
+ const fullLocalXPath = `${baseXPath}${indexPath}`;
1339
+ // @ts-ignore
1340
+ const parentNodes = select(baseXPath, targetDoc);
1341
+ // @ts-ignore
1342
+ const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
1343
+ const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
1344
+ const childAttributeXPath = buildAttributeXPath(attributes);
1345
+ const fullChildXPath = `${childXPath}${childAttributeXPath}`;
1346
+ const childAttributes = parentNodes.map((node) => {
1347
+ try {
1348
+ const nodeStr = node.toString();
1349
+ if (!nodeStr)
1350
+ return null;
1351
+ const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
1352
+ if (attributes.length === 0) {
1353
+ // @ts-ignore
1354
+ const childValues = select(fullChildXPath, nodeDoc).map((n) => n.nodeValue);
1355
+ return childValues.length === 1 ? childValues[0] : childValues;
1356
+ }
1357
+ if (attributes.length > 0) {
1358
+ // @ts-ignore
1359
+ const childValues = select(fullChildXPath, nodeDoc).map((n) => n.value);
1360
+ return childValues.length === 1 ? childValues[0] : childValues;
1361
+ }
1362
+ return null;
1363
+ }
1364
+ catch (e) {
1365
+ return null;
1366
+ }
1367
+ });
1368
+ const obj = zipObject(parentAttributes, childAttributes, false);
1369
+ return { ...result, [key]: obj };
1370
+ }
1371
+ catch (e) {
1372
+ return { ...result, [key]: {} };
1373
+ }
1374
+ }
1375
+ // 获取整个节点 (Context)
1376
+ if (isEntire) {
1377
+ try {
1378
+ // @ts-ignore
1379
+ const node = select(baseXPath, targetDoc);
1380
+ let value = null;
1381
+ if (node.length === 1) {
1382
+ value = node[0].toString();
1383
+ // 如果有自定义解析回调,使用它
1384
+ if (parseCallback) {
1385
+ const parsedValue = parseCallback(node[0]);
1386
+ return { ...result, [key]: parsedValue };
1387
+ }
1388
+ }
1389
+ if (node.length > 1) {
1390
+ value = node.map((n) => n.toString());
1391
+ }
1392
+ return { ...result, [key]: value };
1393
+ }
1394
+ catch (e) {
1395
+ return { ...result, [key]: null };
1396
+ }
1397
+ }
1398
+ // 多属性对象
1399
+ if (attributes.length > 1 && !listMode) {
1400
+ try {
1401
+ // @ts-ignore
1402
+ const baseNodeList = select(baseXPath, targetDoc);
1403
+ if (baseNodeList.length === 0)
1404
+ return { ...result, [key]: null };
1405
+ const attributeValues = baseNodeList.map((node) => {
1406
+ const nodeStr = node.toString();
1407
+ if (!nodeStr)
1408
+ return {};
1409
+ const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
1410
+ const childXPath = `${buildAbsoluteXPath([last(currentLocalPath)])}${buildAttributeXPath(attributes)}`;
1411
+ // @ts-ignore
1412
+ const values = select(childXPath, nodeDoc).reduce((r, n) => {
1413
+ if (n.name && n.value !== undefined)
1414
+ r[camelCase(n.name, { locale: 'en-us' })] = n.value;
1415
+ return r;
1416
+ }, {});
1417
+ return values;
1418
+ });
1419
+ return { ...result, [key]: attributeValues.length === 1 ? attributeValues[0] : attributeValues };
1420
+ }
1421
+ catch (e) {
1422
+ return { ...result, [key]: null };
1423
+ }
1424
+ }
1425
+ // 单个属性
1426
+ if (attributes.length === 1 && !listMode) {
1427
+ try {
1428
+ const fullPath = `${baseXPath}${buildAttributeXPath(attributes)}`;
1429
+ // @ts-ignore
1430
+ const attributeValues = select(fullPath, targetDoc).map((n) => n.value);
1431
+ return { ...result, [key]: attributeValues[0] || null };
1432
+ }
1433
+ catch (e) {
1434
+ return { ...result, [key]: null };
1435
+ }
1436
+ }
1437
+ // 纯文本内容
1438
+ if (attributes.length === 0 && !listMode) {
1439
+ try {
1440
+ // @ts-ignore
1441
+ const node = select(baseXPath, targetDoc);
1442
+ if (parseCallback) {
1443
+ // 使用自定义回调函数处理单个节点
1444
+ return { ...result, [key]: parseCallback(node[0]) };
1445
+ }
1446
+ let attributeValue = null;
1447
+ if (node.length === 1) {
1448
+ const fullPath = `string(${baseXPath})`;
1449
+ // @ts-ignore
1450
+ attributeValue = select(fullPath, targetDoc);
1451
+ }
1452
+ if (node.length > 1) {
1453
+ attributeValue = node.filter((n) => n.firstChild)
1454
+ .map((n) => {
1455
+ let t = n.firstChild.nodeValue;
1456
+ if (!t && n.textContent)
1457
+ t = n.textContent;
1458
+ return t ? t.trim() : null;
1459
+ });
1460
+ }
1461
+ return { ...result, [key]: attributeValue };
1462
+ }
1463
+ catch (e) {
1464
+ return { ...result, [key]: null };
1465
+ }
1466
+ }
1467
+ return result;
1468
+ }, {});
1469
+ }
1470
+ // 新增函数:调用 extractSpToll 提取 SP 数据
1471
+ export function extractSpData(context) {
1472
+ return extractSpToll(context, spMetadataFields);
1473
+ }
422
1474
  export function extractIdp(context) {
423
1475
  return extract(context, idpMetadataFields);
424
1476
  }
425
1477
  export function extractSp(context) {
426
1478
  return extract(context, spMetadataFields);
427
1479
  }
1480
+ export function extractAuthRequest(context) {
1481
+ return extract(context, loginRequestFields);
1482
+ }
1483
+ export function extractResponse(context, ass) {
1484
+ return extractSpToll(context, loginResponseFieldsFullList);
1485
+ }