samlesa 3.2.2 → 3.3.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.
@@ -3,6 +3,8 @@ import { uniq, last, zipObject, notEmpty } from './utility.js';
3
3
  import { getContext } from './api.js';
4
4
  import camelCase from 'camelcase';
5
5
  function buildAbsoluteXPath(paths) {
6
+ if (!paths || paths.length === 0)
7
+ return '';
6
8
  return paths.reduce((currentPath, name) => {
7
9
  let appendedPath = currentPath;
8
10
  const isWildcard = name.startsWith('~');
@@ -10,14 +12,14 @@ function buildAbsoluteXPath(paths) {
10
12
  const pathName = name.replace('~', '');
11
13
  appendedPath = currentPath + `/*[contains(local-name(), '${pathName}')]`;
12
14
  }
13
- if (!isWildcard) {
15
+ else {
14
16
  appendedPath = currentPath + `/*[local-name(.)='${name}']`;
15
17
  }
16
18
  return appendedPath;
17
19
  }, '');
18
20
  }
19
21
  function buildAttributeXPath(attributes) {
20
- if (attributes.length === 0) {
22
+ if (!attributes || attributes.length === 0) {
21
23
  return '/text()';
22
24
  }
23
25
  if (attributes.length === 1) {
@@ -26,355 +28,359 @@ function buildAttributeXPath(attributes) {
26
28
  const filters = attributes.map(attribute => `name()='${attribute}'`).join(' or ');
27
29
  return `/@*[${filters}]`;
28
30
  }
31
+ // ... (其他字段配置如 loginRequestFields 等保持不变,为节省篇幅此处省略,请保留你原有的所有 fields 定义) ...
32
+ // 为了完整性,这里假设你保留了之前所有的 fields 定义 (loginRequestFields, idpMetadataFields 等)
33
+ // 重点修正下方的 spMetadataFields 和 extract 函数
29
34
  export const loginRequestFields = [
30
- {
31
- key: 'request',
32
- localPath: ['AuthnRequest'],
33
- attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL', 'ProtocolBinding', 'ForceAuthn', 'IsPassive', 'AssertionConsumerServiceIndex', 'AttributeConsumingServiceIndex']
34
- },
35
- {
36
- key: 'issuer',
37
- localPath: ['AuthnRequest', 'Issuer'],
38
- attributes: []
39
- },
40
- {
41
- key: 'nameIDPolicy',
42
- localPath: ['AuthnRequest', 'NameIDPolicy'],
43
- attributes: ['Format', 'AllowCreate']
44
- },
45
- {
46
- key: 'authnContextClassRef',
47
- localPath: ['AuthnRequest', 'AuthnContextClassRef'],
48
- attributes: []
49
- },
50
- {
51
- key: 'signature',
52
- localPath: ['AuthnRequest', 'Signature'],
53
- attributes: [],
54
- context: true
55
- }
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: [] },
39
+ { key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
56
40
  ];
57
41
  export const artifactResolveFields = [
58
- {
59
- key: 'request',
60
- localPath: ['ArtifactResolve'],
61
- attributes: ['ID', 'IssueInstant', 'Version']
62
- },
63
- {
64
- key: 'issuer', localPath: ['ArtifactResolve', 'Issuer'], attributes: []
65
- },
66
- {
67
- key: 'Artifact', localPath: ['ArtifactResolve', 'Artifact'], attributes: []
68
- },
69
- {
70
- key: 'signature', localPath: ['ArtifactResolve', 'Signature'], attributes: [], context: true
71
- },
42
+ { key: 'request', localPath: ['ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
43
+ { key: 'issuer', localPath: ['ArtifactResolve', 'Issuer'], attributes: [] },
44
+ { key: 'Artifact', localPath: ['ArtifactResolve', 'Artifact'], attributes: [] },
45
+ { key: 'signature', localPath: ['ArtifactResolve', 'Signature'], attributes: [], context: true },
72
46
  ];
73
47
  export const artifactResponseFields = [
74
- {
75
- key: 'request',
76
- localPath: ['Envelope', 'Body', 'ArtifactResolve'],
77
- attributes: ['ID', 'IssueInstant', 'Version']
78
- },
79
- {
80
- key: 'issuer', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'], attributes: []
81
- },
82
- {
83
- key: 'Artifact', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'], attributes: []
84
- },
85
- {
86
- key: 'signature', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'], attributes: [], context: true
87
- },
48
+ { key: 'request', localPath: ['Envelope', 'Body', 'ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
49
+ { key: 'issuer', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'], attributes: [] },
50
+ { key: 'Artifact', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'], attributes: [] },
51
+ { key: 'signature', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'], attributes: [], context: true },
88
52
  ];
89
- // support two-tiers status code
90
53
  export const loginResponseStatusFields = [
91
- {
92
- key: 'top',
93
- localPath: ['Response', 'Status', 'StatusCode'],
94
- attributes: ['Value'],
95
- },
96
- {
97
- key: 'second',
98
- localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'],
99
- attributes: ['Value'],
100
- }
54
+ { key: 'top', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
55
+ { key: 'second', localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
101
56
  ];
102
- // support two-tiers status code
103
57
  export const loginArtifactResponseStatusFields = [
104
- {
105
- key: 'top',
106
- localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'],
107
- attributes: ['Value'],
108
- },
109
- {
110
- key: 'second',
111
- localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'],
112
- attributes: ['Value'],
113
- }
58
+ { key: 'top', localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'], attributes: ['Value'] },
59
+ { key: 'second', localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
114
60
  ];
115
- // support two-tiers status code
116
61
  export const logoutResponseStatusFields = [
117
- {
118
- key: 'top',
119
- localPath: ['LogoutResponse', 'Status', 'StatusCode'],
120
- attributes: ['Value']
121
- },
122
- {
123
- key: 'second',
124
- localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'],
125
- attributes: ['Value'],
126
- }
62
+ { key: 'top', localPath: ['LogoutResponse', 'Status', 'StatusCode'], attributes: ['Value'] },
63
+ { key: 'second', localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
127
64
  ];
128
65
  export const loginResponseFields = assertion => [
129
- {
130
- key: 'conditions',
131
- localPath: ['Assertion', 'Conditions'],
132
- attributes: ['NotBefore', 'NotOnOrAfter'],
133
- shortcut: assertion
66
+ { key: 'conditions', localPath: ['Assertion', 'Conditions'], attributes: ['NotBefore', 'NotOnOrAfter'], shortcut: assertion },
67
+ { key: 'response', localPath: ['Response'], attributes: ['ID', 'IssueInstant', 'Destination', 'InResponseTo', 'Version'] },
68
+ { key: 'audience', localPath: ['Assertion', 'Conditions', 'AudienceRestriction', 'Audience'], attributes: [], shortcut: assertion },
69
+ { key: 'issuer', localPath: ['Assertion', 'Issuer'], attributes: [], shortcut: assertion },
70
+ { key: 'nameID', localPath: ['Assertion', 'Subject', 'NameID'], attributes: [], shortcut: assertion },
71
+ { key: 'sessionIndex', localPath: ['Assertion', 'AuthnStatement'], attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'], shortcut: assertion },
72
+ { key: 'attributes', localPath: ['Assertion', 'AttributeStatement', 'Attribute'], index: ['Name'], attributePath: ['AttributeValue'], attributes: [], shortcut: assertion },
73
+ { key: 'subjectConfirmation', localPath: ['Assertion', 'Subject', 'SubjectConfirmation', 'SubjectConfirmationData'], attributes: ['Recipient', 'InResponseTo', 'NotOnOrAfter'], shortcut: assertion },
74
+ { key: 'oneTimeUse', localPath: ['Assertion', 'Conditions', 'OneTimeUse'], attributes: [], shortcut: assertion },
75
+ { key: 'status', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
76
+ ];
77
+ export const logoutRequestFields = [
78
+ { key: 'request', localPath: ['LogoutRequest'], attributes: ['ID', 'IssueInstant', 'Destination'] },
79
+ { key: 'issuer', localPath: ['LogoutRequest', 'Issuer'], attributes: [] },
80
+ { key: 'nameID', localPath: ['LogoutRequest', 'NameID'], attributes: [] },
81
+ { key: 'sessionIndex', localPath: ['LogoutRequest', 'SessionIndex'], attributes: [] },
82
+ { key: 'signature', localPath: ['LogoutRequest', 'Signature'], attributes: [], context: true }
83
+ ];
84
+ export const logoutResponseFields = [
85
+ { key: 'response', localPath: ['LogoutResponse'], attributes: ['ID', 'Destination', 'InResponseTo'] },
86
+ { key: 'issuer', localPath: ['LogoutResponse', 'Issuer'], attributes: [] },
87
+ { key: 'signature', localPath: ['LogoutResponse', 'Signature'], attributes: [], context: true }
88
+ ];
89
+ // ============================================================================
90
+ // 增强版:IdP 元数据提取字段配置
91
+ // ============================================================================
92
+ export const idpMetadataFields = [
93
+ // --- 1. 基础标识 ---
94
+ {
95
+ key: 'entityID',
96
+ localPath: ['EntityDescriptor'],
97
+ attributes: ['entityID']
134
98
  },
135
99
  {
136
- key: 'response',
137
- localPath: ['Response'],
138
- attributes: ['ID', 'IssueInstant', 'Destination', 'InResponseTo', 'Version'],
100
+ // 可选:提取整个 EntityDescriptor 的 validUntil 和 cacheDuration
101
+ key: 'entityDescriptor',
102
+ localPath: ['EntityDescriptor'],
103
+ attributes: ['validUntil', 'cacheDuration']
139
104
  },
140
- {
141
- key: 'audience',
142
- localPath: ['Assertion', 'Conditions', 'AudienceRestriction', 'Audience'],
143
- attributes: [],
144
- shortcut: assertion
105
+ // --- 2. IDPSSODescriptor 核心属性 ---
106
+ {
107
+ key: 'idpSSODescriptor',
108
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor'],
109
+ attributes: [
110
+ 'protocolSupportEnumeration',
111
+ 'WantAuthnRequestsSigned', // IdP 是否希望 SP 对请求签名
112
+ 'cacheDuration'
113
+ ]
145
114
  },
146
- // {
147
- // key: 'issuer',
148
- // localPath: ['Response', 'Issuer'],
149
- // attributes: []
150
- // },
115
+ // --- 3. 服务端点列表 (Endpoints) ---
151
116
  {
152
- key: 'issuer',
153
- localPath: ['Assertion', 'Issuer'],
154
- attributes: [],
155
- shortcut: assertion
117
+ // 单点登录服务 (SSO) - 核心
118
+ key: 'singleSignOnService',
119
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'SingleSignOnService'],
120
+ attributes: ['Binding', 'Location', 'ResponseLocation'], // ResponseLocation 用于某些绑定
121
+ listMode: true
156
122
  },
157
123
  {
158
- key: 'nameID',
159
- localPath: ['Assertion', 'Subject', 'NameID'],
160
- attributes: [],
161
- shortcut: assertion
124
+ // 单点注销服务 (SLO)
125
+ key: 'singleLogoutService',
126
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'SingleLogoutService'],
127
+ attributes: ['Binding', 'Location'],
128
+ listMode: true
162
129
  },
163
130
  {
164
- key: 'sessionIndex',
165
- localPath: ['Assertion', 'AuthnStatement'],
166
- attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'],
167
- shortcut: assertion
131
+ // Artifact 解析服务 (如果使用 Artifact 绑定)
132
+ key: 'artifactResolutionService',
133
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ArtifactResolutionService'],
134
+ attributes: ['Binding', 'Location', 'index', 'isDefault'],
135
+ listMode: true
168
136
  },
169
137
  {
170
- key: 'attributes',
171
- localPath: ['Assertion', 'AttributeStatement', 'Attribute'],
172
- index: ['Name'],
173
- attributePath: ['AttributeValue'],
174
- attributes: [],
175
- shortcut: assertion
138
+ // ManageNameID 服务
139
+ key: 'manageNameIDService',
140
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ManageNameIDService'],
141
+ attributes: ['Binding', 'Location'],
142
+ listMode: true
176
143
  },
144
+ // --- 4. 名称 ID 格式 (NameID Formats) ---
177
145
  {
178
- key: 'subjectConfirmation',
179
- localPath: ['Assertion', 'Subject', 'SubjectConfirmation', 'SubjectConfirmationData'],
180
- attributes: ['Recipient', 'InResponseTo', 'NotOnOrAfter'],
181
- shortcut: assertion
146
+ // 提取所有支持的 NameID 格式 (文本内容)
147
+ key: 'nameIDFormat',
148
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'NameIDFormat'],
149
+ attributes: []
150
+ // 注意:如果 extract 函数未完全支持 text() 的 listMode,这里可能只返回第一个。
151
+ // 如果需要数组,需确保 extract 逻辑完善,或者此处暂时只取主要的一个。
182
152
  },
183
153
  {
184
- key: 'oneTimeUse',
185
- localPath: ['Assertion', 'Conditions', 'OneTimeUse'],
186
- attributes: [],
187
- shortcut: assertion
154
+ // 获取主要的 NameID Policy 格式 (如果有显式声明)
155
+ key: 'nameIDPolicyFormat',
156
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'NameIDPolicy'],
157
+ attributes: ['Format']
188
158
  },
159
+ // --- 5. 组织信息 (Organization) ---
189
160
  {
190
- key: 'status',
191
- localPath: ['Response', 'Status', 'StatusCode'],
192
- attributes: ['Value']
161
+ key: 'organizationName',
162
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'],
163
+ attributes: [] // 取文本内容
193
164
  },
194
- ];
195
- export const logoutRequestFields = [
196
165
  {
197
- key: 'request',
198
- localPath: ['LogoutRequest'],
199
- attributes: ['ID', 'IssueInstant', 'Destination']
166
+ key: 'organizationDisplayName',
167
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'],
168
+ attributes: []
200
169
  },
201
170
  {
202
- key: 'issuer',
203
- localPath: ['LogoutRequest', 'Issuer'],
171
+ key: 'organizationURL',
172
+ localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'],
204
173
  attributes: []
205
174
  },
175
+ // --- 6. 联系人信息 (ContactPerson) ---
206
176
  {
207
- key: 'nameID',
208
- localPath: ['LogoutRequest', 'NameID'],
209
- attributes: []
177
+ key: 'contactPerson',
178
+ localPath: ['EntityDescriptor', 'ContactPerson'],
179
+ attributes: ['contactType'],
180
+ listMode: true
181
+ // 局限:目前只能提取 contactType。如需提取 Email/GivenName,需扩展 extract 逻辑。
210
182
  },
183
+ // --- 7. 证书与密钥信息 (Certificates & Keys) ---
184
+ // 这些 key 会触发 extract 函数内部的硬编码逻辑,自动查找对应 @use 的证书
185
+ // 7.1 签名证书 (IdP 用来签名响应/断言)
211
186
  {
212
- key: 'sessionIndex',
213
- localPath: ['LogoutRequest', 'SessionIndex'],
214
- attributes: []
187
+ key: 'signingCert'
188
+ // localPath attributes 将被内部逻辑忽略
215
189
  },
190
+ // 7.2 加密证书 (IdP 用来加密断言中的敏感信息,如果有)
216
191
  {
217
- key: 'signature',
218
- localPath: ['LogoutRequest', 'Signature'],
219
- attributes: [],
220
- context: true
221
- }
222
- ];
223
- export const logoutResponseFields = [
192
+ key: 'encryptCert'
193
+ },
194
+ // 7.3 签名密钥名称 (KeyName)
224
195
  {
225
- key: 'response',
226
- localPath: ['LogoutResponse'],
227
- attributes: ['ID', 'Destination', 'InResponseTo']
196
+ key: 'signingKeyName'
228
197
  },
198
+ // 7.4 加密密钥名称 (KeyName)
229
199
  {
230
- key: 'issuer',
231
- localPath: ['LogoutResponse', 'Issuer'],
232
- attributes: []
200
+ key: 'encryptionKeyName'
233
201
  },
202
+ // --- 8. 其他扩展属性 (可选) ---
234
203
  {
235
- key: 'signature',
236
- localPath: ['LogoutResponse', 'Signature'],
237
- attributes: [],
238
- context: true
204
+ // 提取 AttributeConsumingService (如果 IdP 声明了它需要的属性)
205
+ key: 'attributeConsumingService',
206
+ localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'AttributeConsumingService'],
207
+ attributes: ['index', 'isDefault'],
208
+ listMode: true
239
209
  }
240
210
  ];
211
+ // ============================================================================
212
+ // 修正后的 SP 元数据提取字段配置
213
+ // ============================================================================
214
+ 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 在这里不起作用,仅作为占位符
228
+ { key: 'signingCert' },
229
+ { key: 'encryptCert' },
230
+ { key: 'signingKeyName' },
231
+ { key: 'encryptionKeyName' }
232
+ ];
241
233
  export function extract(context, fields) {
242
234
  const { dom } = getContext();
243
235
  const rootDoc = dom.parseFromString(context, 'application/xml');
244
236
  return fields.reduce((result, field) => {
245
- // get essential fields
246
237
  const key = field.key;
247
- const localPath = field.localPath;
248
- const attributes = field.attributes;
238
+ // 安全解构,防止 undefined
239
+ const localPath = field.localPath || [];
240
+ const attributes = field.attributes || [];
249
241
  const isEntire = field.context;
250
242
  const shortcut = field.shortcut;
251
- // get optional fields
252
243
  const index = field.index;
253
244
  const attributePath = field.attributePath;
254
- // set allowing overriding if there is a shortcut injected
245
+ const listMode = field.listMode;
255
246
  let targetDoc = rootDoc;
256
- // if shortcut is used, then replace the doc
257
- // it's a design for overriding the doc used during runtime
258
247
  if (shortcut) {
259
248
  targetDoc = dom.parseFromString(shortcut, 'application/xml');
260
249
  }
261
- // special case: multiple path
262
- /*
263
- {
264
- key: 'issuer',
265
- localPath: [
266
- ['Response', 'Issuer'],
267
- ['Response', 'Assertion', 'Issuer']
268
- ],
269
- attributes: []
270
- }
271
- */
272
- if (localPath.every(path => Array.isArray(path))) {
273
- const multiXPaths = localPath
274
- .map(path => {
275
- // not support attribute yet, so ignore it
276
- return `${buildAbsoluteXPath(path)}/text()`;
277
- })
278
- .join(' | ');
250
+ // ==========================================================================
251
+ // 【核心修复】特殊处理:证书和 KeyName 提取 (Hardcoded logic)
252
+ // 不再硬编码 IDPSSODescriptor 或 SPSSODescriptor,而是全局搜索 @use 属性
253
+ // ==========================================================================
254
+ if (key === 'signingCert' || key === 'encryptCert' || key === 'signingKeyName' || key === 'encryptionKeyName') {
255
+ const isSigning = key.startsWith('signing');
256
+ const useType = isSigning ? 'signing' : 'encryption';
257
+ const isKeyName = key.endsWith('KeyName');
258
+ // 通用 XPath:查找任意层级下符合 @use 条件的 KeyDescriptor
259
+ const kdXPath = `//*[local-name(.)='KeyDescriptor' and @use='${useType}']`;
260
+ let fullXPath = '';
261
+ if (isKeyName) {
262
+ // 提取 KeyName 文本
263
+ fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='KeyName']/text()`;
264
+ }
265
+ else {
266
+ // 提取 X509Certificate 文本
267
+ fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='X509Data']/*[local-name(.)='X509Certificate']/text()`;
268
+ }
269
+ try {
270
+ // @ts-ignore
271
+ const nodes = select(fullXPath, targetDoc);
272
+ if (isKeyName) {
273
+ const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
274
+ return {
275
+ ...result,
276
+ [key]: keyNames.length > 0 ? keyNames[0] : null
277
+ };
278
+ }
279
+ else {
280
+ const certs = nodes.map((n) => {
281
+ const val = n.nodeValue || n.value;
282
+ return val ? val.replace(/\r\n|\r|\n/g, '') : null;
283
+ }).filter(notEmpty);
284
+ return {
285
+ ...result,
286
+ [key]: certs.length > 0 ? certs[0] : null
287
+ };
288
+ }
289
+ }
290
+ catch (e) {
291
+ console.error(`Error extracting ${key}:`, e);
292
+ return { ...result, [key]: null };
293
+ }
294
+ }
295
+ // 特殊 case: 多路径 (原有逻辑)
296
+ if (Array.isArray(localPath) && localPath.length > 0 && Array.isArray(localPath[0])) {
297
+ const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
298
+ // @ts-ignore
299
+ const nodes = select(multiXPaths, targetDoc);
300
+ return {
301
+ ...result,
302
+ [key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty))
303
+ };
304
+ }
305
+ // 此时 localPath 必然是 string[]
306
+ const currentLocalPath = localPath;
307
+ // 如果 localPath 为空数组(如特殊字段未定义 path),且未命中上面的特殊逻辑,则跳过
308
+ if (currentLocalPath.length === 0 && !isEntire) {
309
+ // 对于没有 path 且不是特殊处理的字段,返回 null 或跳过
310
+ return { ...result, [key]: null };
311
+ }
312
+ const baseXPath = buildAbsoluteXPath(currentLocalPath);
313
+ // --- 新增:列表模式处理 (用于 SSO Service, ACS 等) ---
314
+ if (listMode && attributes.length > 0) {
315
+ // @ts-ignore
316
+ const nodes = select(baseXPath, targetDoc);
317
+ const resultList = nodes.map((node) => {
318
+ const attrResult = {};
319
+ attributes.forEach(attr => {
320
+ if (node.getAttribute) {
321
+ const val = node.getAttribute(attr);
322
+ if (val) {
323
+ attrResult[camelCase(attr, { locale: 'en-us' })] = val;
324
+ }
325
+ }
326
+ });
327
+ return attrResult;
328
+ });
279
329
  return {
280
330
  ...result,
281
- // @ts-expect-error misssing Node properties are not needed
282
- [key]: uniq(select(multiXPaths, targetDoc).map((n) => n.nodeValue).filter(notEmpty))
331
+ [key]: resultList
283
332
  };
284
333
  }
285
- // eo special case: multiple path
286
- const baseXPath = buildAbsoluteXPath(localPath);
287
334
  const attributeXPath = buildAttributeXPath(attributes);
288
- // special case: get attributes where some are in child, some are in parent
289
- /*
290
- {
291
- key: 'attributes',
292
- localPath: ['Response', 'Assertion', 'AttributeStatement', 'Attribute'],
293
- index: ['Name'],
294
- attributePath: ['AttributeValue'],
295
- attributes: []
296
- }
297
- */
335
+ // 特殊 case: 属性聚合 (原有逻辑 - Attributes with index)
298
336
  if (index && attributePath) {
299
- // find the index in localpath
300
337
  const indexPath = buildAttributeXPath(index);
301
338
  const fullLocalXPath = `${baseXPath}${indexPath}`;
302
- // @ts-expect-error misssing Node properties are not needed
339
+ // @ts-ignore
303
340
  const parentNodes = select(baseXPath, targetDoc);
304
- // [uid, mail, edupersonaffiliation], ready for aggregate
305
- // @ts-expect-error misssing Node properties are not needed
341
+ // @ts-ignore
306
342
  const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
307
- // [attribute, attributevalue]
308
- const childXPath = buildAbsoluteXPath([last(localPath)].concat(attributePath));
343
+ const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
309
344
  const childAttributeXPath = buildAttributeXPath(attributes);
310
345
  const fullChildXPath = `${childXPath}${childAttributeXPath}`;
311
- // [ 'test', 'test@example.com', [ 'users', 'examplerole1' ] ]
312
- const childAttributes = parentNodes.map(node => {
346
+ const childAttributes = parentNodes.map((node) => {
313
347
  const nodeDoc = dom.parseFromString(node.toString(), 'application/xml');
314
348
  if (attributes.length === 0) {
315
349
  // @ts-ignore
316
350
  const childValues = select(fullChildXPath, nodeDoc).map((n) => n.nodeValue);
317
- if (childValues.length === 1) {
318
- return childValues[0];
319
- }
320
- return childValues;
351
+ return childValues.length === 1 ? childValues[0] : childValues;
321
352
  }
322
353
  if (attributes.length > 0) {
323
354
  // @ts-ignore
324
355
  const childValues = select(fullChildXPath, nodeDoc).map((n) => n.value);
325
- if (childValues.length === 1) {
326
- return childValues[0];
327
- }
328
- return childValues;
356
+ return childValues.length === 1 ? childValues[0] : childValues;
329
357
  }
330
358
  return null;
331
359
  });
332
- // aggregation
333
360
  const obj = zipObject(parentAttributes, childAttributes, false);
334
- return {
335
- ...result,
336
- [key]: obj
337
- };
361
+ return { ...result, [key]: obj };
338
362
  }
339
- // case: fetch entire content, only allow one existence
340
- /*
341
- {
342
- key: 'signature',
343
- localPath: ['AuthnRequest', 'Signature'],
344
- attributes: [],
345
- context: true
346
- }
347
- */
363
+ // 特殊 case: 获取整个节点内容 (原有逻辑)
348
364
  if (isEntire) {
349
- // @ts-expect-error misssing Node properties are not needed
365
+ // @ts-ignore
350
366
  const node = select(baseXPath, targetDoc);
351
367
  let value = null;
352
368
  if (node.length === 1) {
353
369
  value = node[0].toString();
354
370
  }
355
371
  if (node.length > 1) {
356
- value = node.map(n => n.toString());
372
+ value = node.map((n) => n.toString());
357
373
  }
358
- return {
359
- ...result,
360
- [key]: value
361
- };
374
+ return { ...result, [key]: value };
362
375
  }
363
- // case: multiple attribute
364
- /*
365
- {
366
- key: 'nameIDPolicy',
367
- localPath: ['AuthnRequest', 'NameIDPolicy'],
368
- attributes: ['Format', 'AllowCreate']
369
- }
370
- */
371
- if (attributes.length > 1) {
376
+ // 特殊 case: 多属性对象 (原有逻辑,非 listMode)
377
+ if (attributes.length > 1 && !listMode) {
372
378
  // @ts-ignore
373
- const baseNode = select(baseXPath, targetDoc).map(n => n.toString());
374
- const childXPath = `${buildAbsoluteXPath([last(localPath)])}${attributeXPath}`;
375
- const attributeValues = baseNode.map((node) => {
379
+ const baseNode = select(baseXPath, targetDoc).map((n) => n.toString());
380
+ const childXPath = `${buildAbsoluteXPath([last(currentLocalPath)])}${attributeXPath}`;
381
+ const attributeValues = baseNode.map((nodeStr) => {
376
382
  // @ts-ignore
377
- const nodeDoc = dom.parseFromString(node, 'application/xml');
383
+ const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
378
384
  // @ts-ignore
379
385
  const values = select(childXPath, nodeDoc).reduce((r, n) => {
380
386
  r[camelCase(n.name, { locale: 'en-us' })] = n.value;
@@ -387,49 +393,35 @@ export function extract(context, fields) {
387
393
  [key]: attributeValues.length === 1 ? attributeValues[0] : attributeValues
388
394
  };
389
395
  }
390
- // case: single attribute
391
- /*
392
- {
393
- key: 'statusCode',
394
- localPath: ['Response', 'Status', 'StatusCode'],
395
- attributes: ['Value'],
396
- }
397
- */
398
- if (attributes.length === 1) {
396
+ // 特殊 case: 单个属性 (原有逻辑)
397
+ if (attributes.length === 1 && !listMode) {
399
398
  const fullPath = `${baseXPath}${attributeXPath}`;
400
399
  // @ts-ignore
401
400
  const attributeValues = select(fullPath, targetDoc).map((n) => n.value);
402
- return {
403
- ...result,
404
- [key]: attributeValues[0]
405
- };
401
+ return { ...result, [key]: attributeValues[0] };
406
402
  }
407
- // case: zero attribute
408
- /*
409
- {
410
- key: 'issuer',
411
- localPath: ['AuthnRequest', 'Issuer'],
412
- attributes: []
413
- }
414
- */
415
- if (attributes.length === 0) {
403
+ // 特殊 case: 无属性/文本内容 (原有逻辑)
404
+ if (attributes.length === 0 && !listMode) {
416
405
  let attributeValue = null;
417
- // @ts-expect-error misssing Node properties are not needed
406
+ // @ts-ignore
418
407
  const node = select(baseXPath, targetDoc);
419
408
  if (node.length === 1) {
420
409
  const fullPath = `string(${baseXPath}${attributeXPath})`;
421
- // @ts-expect-error misssing Node properties are not needed
410
+ // @ts-ignore
422
411
  attributeValue = select(fullPath, targetDoc);
423
412
  }
424
413
  if (node.length > 1) {
425
414
  attributeValue = node.filter((n) => n.firstChild)
426
- .map((n) => n.firstChild.nodeValue);
415
+ .map((n) => n.firstChild?.nodeValue);
427
416
  }
428
- return {
429
- ...result,
430
- [key]: attributeValue
431
- };
417
+ return { ...result, [key]: attributeValue };
432
418
  }
433
419
  return result;
434
420
  }, {});
435
421
  }
422
+ export function extractIdp(context) {
423
+ return extract(context, idpMetadataFields);
424
+ }
425
+ export function extractSp(context) {
426
+ return extract(context, spMetadataFields);
427
+ }