samlesa 4.3.2 → 4.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/metadata-sp.js +123 -164
- package/package.json +1 -1
- package/types/src/metadata-sp.d.ts +8 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
package/build/src/metadata-sp.js
CHANGED
|
@@ -238,177 +238,136 @@ export class SpMetadata extends Metadata {
|
|
|
238
238
|
}
|
|
239
239
|
return value === 'true';
|
|
240
240
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
targetBindingName = resolved;
|
|
241
|
+
getAssertionConsumerService(binding, options = {}) {
|
|
242
|
+
const mode = options.mode ?? 'lenient';
|
|
243
|
+
const normalizeBinding = (value) => String(value ?? '').trim().toLowerCase();
|
|
244
|
+
const normalizeLocation = (value) => String(value ?? '').trim();
|
|
245
|
+
const parseBool = (value) => {
|
|
246
|
+
if (typeof value === 'boolean')
|
|
247
|
+
return value;
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
const t = value.trim().toLowerCase();
|
|
250
|
+
if (t === 'true')
|
|
251
|
+
return true;
|
|
252
|
+
if (t === 'false')
|
|
253
|
+
return false;
|
|
255
254
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
const parseIndex = (value) => {
|
|
258
|
+
if (value === undefined || value === null || String(value).trim() === '')
|
|
259
|
+
return Number.POSITIVE_INFINITY;
|
|
260
|
+
const n = Number.parseInt(String(value), 10);
|
|
261
|
+
return Number.isNaN(n) ? Number.POSITIVE_INFINITY : n;
|
|
262
|
+
};
|
|
263
|
+
const indexHint = parseIndex(options.assertionConsumerServiceIndex);
|
|
264
|
+
const hasIndexHint = Number.isFinite(indexHint);
|
|
265
|
+
const toSafeLocation = (value) => {
|
|
266
|
+
const text = String(value || '').trim();
|
|
267
|
+
// 仅校验协议前缀,保持原始字符串不被规范化改写(兼容历史用例:如 "https:sp.example.org/...")
|
|
268
|
+
if (/^https?:/i.test(text)) {
|
|
269
|
+
return text;
|
|
260
270
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
271
|
+
return '';
|
|
272
|
+
};
|
|
273
|
+
const resolveBindingUri = (input) => {
|
|
274
|
+
if (typeof input !== 'string')
|
|
275
|
+
return null;
|
|
276
|
+
const raw = input.trim();
|
|
277
|
+
if (!raw)
|
|
278
|
+
return null;
|
|
279
|
+
const byKey = namespace.binding[raw];
|
|
280
|
+
if (typeof byKey === 'string')
|
|
281
|
+
return byKey;
|
|
282
|
+
const lower = raw.toLowerCase();
|
|
283
|
+
const candidates = Object.values(namespace.binding).filter((v) => typeof v === 'string');
|
|
284
|
+
const byUri = candidates.find((v) => v.toLowerCase() === lower);
|
|
285
|
+
if (byUri)
|
|
286
|
+
return byUri;
|
|
287
|
+
const aliasMap = {
|
|
288
|
+
post: namespace.binding.post,
|
|
289
|
+
redirect: namespace.binding.redirect,
|
|
290
|
+
artifact: namespace.binding.artifact,
|
|
291
|
+
simplesign: namespace.binding.simpleSign,
|
|
292
|
+
'simple-sign': namespace.binding.simpleSign,
|
|
293
|
+
soap: namespace.binding.soap,
|
|
294
|
+
};
|
|
295
|
+
return aliasMap[lower] || null;
|
|
296
|
+
};
|
|
297
|
+
const sortCandidates = (list) => [...list].sort((a, b) => {
|
|
298
|
+
const score = (item) => item.isDefault === true ? 0 : (item.isDefault === null ? 1 : 2);
|
|
299
|
+
const sA = score(a);
|
|
300
|
+
const sB = score(b);
|
|
301
|
+
if (sA !== sB)
|
|
302
|
+
return sA - sB;
|
|
303
|
+
if (a.index !== b.index)
|
|
304
|
+
return a.index - b.index;
|
|
305
|
+
return a.order - b.order;
|
|
306
|
+
});
|
|
307
|
+
const pickBest = (list) => {
|
|
290
308
|
if (list.length === 0)
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
return list[0];
|
|
294
|
-
const tier1 = []; // isDefault === 'true'
|
|
295
|
-
const tier2 = []; // isDefault 不存在 或 不为 'false'
|
|
296
|
-
const tier3 = []; // 明确 isDefault === 'false'
|
|
297
|
-
list.forEach((item, originalIndex) => {
|
|
298
|
-
const isDef = item.isDefault;
|
|
299
|
-
const isTrue = (typeof isDef === 'string' && isDef.toLowerCase() === 'true');
|
|
300
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
301
|
-
if (isTrue) {
|
|
302
|
-
tier1.push(item);
|
|
303
|
-
}
|
|
304
|
-
else if (!isFalse) {
|
|
305
|
-
tier2.push(item);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
tier3.push(item);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
let selectedTier = [];
|
|
312
|
-
if (tier1.length > 0) {
|
|
313
|
-
selectedTier = tier1;
|
|
314
|
-
}
|
|
315
|
-
else if (tier2.length > 0) {
|
|
316
|
-
selectedTier = tier2;
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
selectedTier = tier3;
|
|
320
|
-
}
|
|
321
|
-
// 二级排序:Index 升序 -> 原始顺序
|
|
322
|
-
const indexedTier = selectedTier.map((item, idx) => ({ item, originalIdx: idx }));
|
|
323
|
-
indexedTier.sort((a, b) => {
|
|
324
|
-
// 安全解析 index
|
|
325
|
-
let idxA = Infinity;
|
|
326
|
-
let idxB = Infinity;
|
|
327
|
-
if (a.item.index !== undefined && a.item.index !== null) {
|
|
328
|
-
const parsed = parseInt(a.item.index, 10);
|
|
329
|
-
if (!isNaN(parsed))
|
|
330
|
-
idxA = parsed;
|
|
331
|
-
}
|
|
332
|
-
if (b.item.index !== undefined && b.item.index !== null) {
|
|
333
|
-
const parsed = parseInt(b.item.index, 10);
|
|
334
|
-
if (!isNaN(parsed))
|
|
335
|
-
idxB = parsed;
|
|
336
|
-
}
|
|
337
|
-
if (idxA !== idxB) {
|
|
338
|
-
return idxA - idxB;
|
|
339
|
-
}
|
|
340
|
-
return a.originalIdx - b.originalIdx;
|
|
341
|
-
});
|
|
342
|
-
return indexedTier[0].item;
|
|
309
|
+
return null;
|
|
310
|
+
return sortCandidates(list)[0] ?? null;
|
|
343
311
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
312
|
+
const pickByIndex = (list) => {
|
|
313
|
+
if (!hasIndexHint)
|
|
314
|
+
return null;
|
|
315
|
+
const hit = list.filter((item) => item.index === indexHint);
|
|
316
|
+
return pickBest(hit);
|
|
317
|
+
};
|
|
318
|
+
const acsData = this.meta.assertionConsumerService;
|
|
319
|
+
const rawCandidates = Array.isArray(acsData) ? acsData : (acsData && typeof acsData === 'object' ? [acsData] : []);
|
|
320
|
+
if (rawCandidates.length === 0)
|
|
321
|
+
return '';
|
|
322
|
+
const normalizedCandidates = rawCandidates
|
|
323
|
+
.map((item, order) => ({
|
|
324
|
+
binding: String(item?.binding ?? item?.Binding ?? '').trim(),
|
|
325
|
+
location: toSafeLocation(normalizeLocation(item?.location ?? item?.Location ?? item?.responseLocation ?? item?.ResponseLocation ?? '')),
|
|
326
|
+
isDefault: parseBool(item?.isDefault ?? item?.IsDefault ?? item?.default),
|
|
327
|
+
index: parseIndex(item?.index ?? item?.Index),
|
|
328
|
+
order,
|
|
329
|
+
}))
|
|
330
|
+
.filter((item) => item.location);
|
|
331
|
+
if (normalizedCandidates.length === 0)
|
|
332
|
+
return '';
|
|
333
|
+
const dedupMap = new Map();
|
|
334
|
+
for (const item of normalizedCandidates) {
|
|
335
|
+
const key = `${normalizeBinding(item.binding)}|${item.location}|${Number.isFinite(item.index) ? item.index : 'NA'}`;
|
|
336
|
+
if (!dedupMap.has(key))
|
|
337
|
+
dedupMap.set(key, item);
|
|
359
338
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//console.log(`[ACS Selection] ⚠️ 指定 Binding 未找到可用项,进入降级策略...`);
|
|
370
|
-
}
|
|
371
|
-
// 2.1 全局寻找 isDefault='true'
|
|
372
|
-
const globalDefaults = allCandidates.filter(obj => obj.isDefault && String(obj.isDefault).toLowerCase() === 'true');
|
|
373
|
-
if (globalDefaults.length > 0) {
|
|
374
|
-
const best = selectBestFromList(globalDefaults);
|
|
375
|
-
if (best) {
|
|
376
|
-
//console.log(`[ACS Selection] ✅ (阶段2) 跨 Binding 找到 isDefault=true: ${best.location} (${best.binding})`);
|
|
377
|
-
resultLocation = best.location;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// 2.2 全局寻找 isDefault 不为 'false' (隐式默认)
|
|
381
|
-
if (!resultLocation) {
|
|
382
|
-
const globalNonFalse = allCandidates.filter(obj => {
|
|
383
|
-
const isDef = obj.isDefault;
|
|
384
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
385
|
-
return !isFalse;
|
|
386
|
-
});
|
|
387
|
-
if (globalNonFalse.length > 0) {
|
|
388
|
-
const best = selectBestFromList(globalNonFalse);
|
|
389
|
-
if (best) {
|
|
390
|
-
//console.log(`[ACS Selection] ✅ (阶段3) 跨 Binding 找到隐式默认: ${best.location} (${best.binding})`);
|
|
391
|
-
resultLocation = best.location;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// 2.3 终极兜底:所有数据中 index 最小
|
|
396
|
-
if (!resultLocation) {
|
|
397
|
-
const best = selectBestFromList(allCandidates);
|
|
398
|
-
if (best) {
|
|
399
|
-
// console.log(`[ACS Selection] ✅ (阶段4) 终极兜底,选择 index 最小: ${best.location} (${best.binding})`);
|
|
400
|
-
resultLocation = best.location;
|
|
401
|
-
}
|
|
339
|
+
const allCandidates = Array.from(dedupMap.values());
|
|
340
|
+
const targetBindingName = resolveBindingUri(binding);
|
|
341
|
+
const boundCandidates = targetBindingName
|
|
342
|
+
? allCandidates.filter((item) => normalizeBinding(item.binding) === normalizeBinding(targetBindingName))
|
|
343
|
+
: [];
|
|
344
|
+
const warnDefaultConflict = (list, label) => {
|
|
345
|
+
const defaults = list.filter((item) => item.isDefault === true);
|
|
346
|
+
if (defaults.length > 1) {
|
|
347
|
+
console.warn(`[ACS Selection] Multiple isDefault=true found in ${label}; fallback to index/order.`);
|
|
402
348
|
}
|
|
349
|
+
};
|
|
350
|
+
warnDefaultConflict(targetBindingName ? boundCandidates : allCandidates, targetBindingName ? `binding=${targetBindingName}` : 'all bindings');
|
|
351
|
+
// 1) index 优先:先同 binding,再全局(仅 lenient)
|
|
352
|
+
const indexedInBinding = pickByIndex(boundCandidates);
|
|
353
|
+
if (indexedInBinding)
|
|
354
|
+
return indexedInBinding.location;
|
|
355
|
+
if (targetBindingName && mode === 'strict' && boundCandidates.length === 0) {
|
|
356
|
+
return '';
|
|
403
357
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const errorMsg = "SAML Configuration Error: Unable to select any valid AssertionConsumerService URL. Metadata might be empty or malformed.";
|
|
409
|
-
console.error(`[ACS Selection] ❌ ${errorMsg}`);
|
|
410
|
-
throw new Error(errorMsg);
|
|
358
|
+
if (mode === 'lenient') {
|
|
359
|
+
const indexedGlobal = pickByIndex(allCandidates);
|
|
360
|
+
if (indexedGlobal)
|
|
361
|
+
return indexedGlobal.location;
|
|
411
362
|
}
|
|
412
|
-
|
|
363
|
+
// 2) 绑定内最优
|
|
364
|
+
const bestBound = pickBest(boundCandidates);
|
|
365
|
+
if (bestBound)
|
|
366
|
+
return bestBound.location;
|
|
367
|
+
if (mode === 'strict' && targetBindingName)
|
|
368
|
+
return '';
|
|
369
|
+
// 3) 全局回退
|
|
370
|
+
const bestGlobal = pickBest(allCandidates);
|
|
371
|
+
return bestGlobal?.location ?? '';
|
|
413
372
|
}
|
|
414
373
|
}
|
package/package.json
CHANGED
|
@@ -7,6 +7,11 @@ import Metadata, { type MetadataInterface } from './metadata.js';
|
|
|
7
7
|
import type { MetadataSpConstructor } from './types.js';
|
|
8
8
|
export interface SpMetadataInterface extends MetadataInterface {
|
|
9
9
|
}
|
|
10
|
+
type AcsSelectionMode = 'strict' | 'lenient';
|
|
11
|
+
interface AcsSelectionOptions {
|
|
12
|
+
mode?: AcsSelectionMode;
|
|
13
|
+
assertionConsumerServiceIndex?: string | number | null;
|
|
14
|
+
}
|
|
10
15
|
export default function (meta: MetadataSpConstructor): SpMetadata;
|
|
11
16
|
/**
|
|
12
17
|
* @desc SP Metadata is for creating Service Provider, provides a set of API to manage the actions in SP.
|
|
@@ -32,6 +37,8 @@ export declare class SpMetadata extends Metadata {
|
|
|
32
37
|
* @param {string} binding protocol binding (e.g. redirect, post)
|
|
33
38
|
* @return {string/[string]} URL of endpoint(s)
|
|
34
39
|
*/
|
|
35
|
-
getAssertionConsumerService(binding
|
|
40
|
+
getAssertionConsumerService(binding?: string): string;
|
|
41
|
+
getAssertionConsumerService(binding: string | undefined, options: AcsSelectionOptions): string;
|
|
36
42
|
}
|
|
43
|
+
export {};
|
|
37
44
|
//# sourceMappingURL=metadata-sp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-sp.d.ts","sourceRoot":"","sources":["../../src/metadata-sp.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,OAAO,QAAQ,EAAE,EAAC,KAAK,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAMxD,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;CAE7D;
|
|
1
|
+
{"version":3,"file":"metadata-sp.d.ts","sourceRoot":"","sources":["../../src/metadata-sp.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,OAAO,QAAQ,EAAE,EAAC,KAAK,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAMxD,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;CAE7D;AAYD,KAAK,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC7C,UAAU,mBAAmB;IAC3B,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,6BAA6B,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACxD;AAKD,MAAM,CAAC,OAAO,WAAU,IAAI,EAAE,qBAAqB,cAElD;AAED;;EAEE;AACF,qBAAa,UAAW,SAAQ,QAAQ;IAEtC;;;MAGE;gBACU,IAAI,EAAE,qBAAqB;IA+NvC;;;MAGE;IACK,sBAAsB,IAAI,OAAO;IAOxC;;;MAGE;IACK,oBAAoB,IAAI,OAAO;IAOtC;;;;MAIE;IACK,2BAA2B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IACrD,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM;CAmItG"}
|